Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

17534 - New NSF resource implementation #1341

Closed
wants to merge 41 commits into from
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
93390b7
Non sufficient funds implementation
rodrigo-barraza Dec 4, 2023
6c7e6c0
Merge branch 'main' into feature/17534
rodrigo-barraza Dec 4, 2023
bb0ccdd
Linting and test fix
rodrigo-barraza Dec 4, 2023
01aa77e
Lint and test fixes
rodrigo-barraza Dec 4, 2023
40a4b47
Flake8 fixes
rodrigo-barraza Dec 4, 2023
df55123
More flake8 test fixes
rodrigo-barraza Dec 4, 2023
7f663da
Fixes
rodrigo-barraza Dec 4, 2023
04742b8
isort fix
rodrigo-barraza Dec 4, 2023
e7cc660
import fix
rodrigo-barraza Dec 4, 2023
b7c6a93
New NSF template, adding search model, migration script, cleanup
rodrigo-barraza Dec 7, 2023
a9fa502
Lint cleanup
rodrigo-barraza Dec 7, 2023
91e3ed5
Ignoring lint on NonSufficientFundsSearchModel
rodrigo-barraza Dec 7, 2023
f6c676f
Query fix
rodrigo-barraza Dec 8, 2023
456eb65
PDF fixes
rodrigo-barraza Dec 11, 2023
31aa3da
Merge branch 'main' into feature/17534
rodrigo-barraza Dec 11, 2023
6cc9bdc
Lint fixes
rodrigo-barraza Dec 11, 2023
be2d5e2
Cleanup
rodrigo-barraza Dec 11, 2023
e08b591
asdict
rodrigo-barraza Dec 11, 2023
5d24d0d
Model and service changes
rodrigo-barraza Dec 11, 2023
baa28b4
flake8 fix
rodrigo-barraza Dec 11, 2023
5063764
Version bump
rodrigo-barraza Dec 11, 2023
dff9b98
Test fixes
rodrigo-barraza Dec 11, 2023
08d62ac
New NSF query and cleanup for NSF PDF template
rodrigo-barraza Dec 11, 2023
13a4eb7
Feedback updates
rodrigo-barraza Dec 11, 2023
e2946ad
Moving logic to query, updating template
rodrigo-barraza Dec 12, 2023
a6fbaa4
NSF query refactoring
rodrigo-barraza Dec 12, 2023
f2cebe8
Unit test fixes
rodrigo-barraza Dec 13, 2023
848ec10
Merge branch 'main' into feature/17534
rodrigo-barraza Dec 13, 2023
fad3b66
Lint fix
rodrigo-barraza Dec 13, 2023
23c4099
Expanding unit test
rodrigo-barraza Dec 14, 2023
4aab599
Cleanup, new CFS method
rodrigo-barraza Dec 14, 2023
5d75820
Checking auth, getting latest CFS account
rodrigo-barraza Dec 15, 2023
9005132
Expanding tests
rodrigo-barraza Dec 15, 2023
521b252
Unit test fix
rodrigo-barraza Dec 15, 2023
b034d16
Flake8 fix
rodrigo-barraza Dec 15, 2023
ae9c1de
Expanding API test
rodrigo-barraza Dec 15, 2023
2b50ab4
Unit test fix
rodrigo-barraza Dec 15, 2023
89d80bc
Merge branch 'main' into feature/17534
rodrigo-barraza Jan 2, 2024
915cc0d
Updating NSF query
rodrigo-barraza Jan 12, 2024
19b8a90
Merge branch 'main' into feature/17534
rodrigo-barraza Jan 12, 2024
91785fa
revision update
rodrigo-barraza Jan 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pay-api/src/pay_api/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from .invoice_reference_status_code import InvoiceReferenceStatusCode, InvoiceReferenceStatusCodeSchema
from .invoice_status_code import InvoiceStatusCode, InvoiceStatusCodeSchema
from .line_item_status_code import LineItemStatusCode, LineItemStatusCodeSchema
from .non_sufficient_funds import NonSufficientFundsModel, NonSufficientFundsSchema
from .notification_status_code import NotificationStatusCode, NotificationStatusCodeSchema
from .payment import Payment, PaymentSchema
from .payment_account import PaymentAccount, PaymentAccountSchema # noqa: I001
Expand Down
61 changes: 61 additions & 0 deletions pay-api/src/pay_api/models/non_sufficient_funds.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Copyright © 2019 Province of British Columbia
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Model to handle all operations related to Non-Sufficient Funds."""
from __future__ import annotations

from marshmallow import fields
from sqlalchemy import ForeignKey

from .base_model import BaseModel
from .base_schema import BaseSchema
from .db import db


class NonSufficientFundsModel(BaseModel): # pylint: disable=too-many-instance-attributes
"""This class manages all of the base data about Non-Sufficient Funds."""

__tablename__ = 'non_sufficient_funds'
# this mapper is used so that new and old versions of the service can be run simultaneously,
# making rolling upgrades easier
# This is used by SQLAlchemy to explicitly define which fields we're interested
# so it doesn't freak out and say it can't map the structure if other fields are present.
# This could occur from a failed deploy or during an upgrade.
# The other option is to tell SQLAlchemy to ignore differences, but that is ambiguous
# and can interfere with Alembic upgrades.
#
# NOTE: please keep mapper names in alpha-order, easier to track that way
# Exception, id is always first, _fields first
__mapper_args__ = {
'include_properties': [
'id',
'description',
'invoice_id',
]
}

id = db.Column(db.Integer, primary_key=True, autoincrement=True)
description = db.Column(db.String(50), nullable=True)
invoice_id = db.Column(db.Integer, ForeignKey('invoice.id'), nullable=False)


class NonSufficientFundsSchema(BaseSchema): # pylint: disable=too-many-ancestors
seeker25 marked this conversation as resolved.
Show resolved Hide resolved
"""Main schema used to serialize the Non-Sufficient Funds."""

class Meta(BaseSchema.Meta): # pylint: disable=too-few-public-methods
"""Returns all the fields from the SQLAlchemy class."""

model = NonSufficientFundsModel

description = fields.String(data_key='description')
invoice_id = fields.Integer(data_key='invoice_id')
47 changes: 47 additions & 0 deletions pay-api/src/pay_api/resources/v1/non_sufficient_funds.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Copyright © 2019 Province of British Columbia
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Resource for Account Non-Sufficient Funds endpoints."""
from http import HTTPStatus

from flask import Blueprint, current_app, jsonify, request
from flask_cors import cross_origin

from pay_api.services import NonSufficientFundsService
from pay_api.services.auth import check_auth
from pay_api.utils.auth import jwt as _jwt
from pay_api.utils.constants import EDIT_ROLE, MAKE_PAYMENT, VIEW_ROLE
from pay_api.utils.endpoints_enums import EndpointEnum
from pay_api.utils.trace import tracing as _tracing


bp = Blueprint('NON_SUFFICIENT_FUNDS', __name__,
url_prefix=f'{EndpointEnum.API_V1.value}/accounts/<string:account_id>/nsf')


@bp.route('', methods=['GET', 'OPTIONS'])
@cross_origin(origins='*', methods=['GET', 'POST'])
@_tracing.trace()
@_jwt.requires_auth
def get_non_sufficient_funds(account_id: str):
"""Get non sufficient funds."""
current_app.logger.info('<get_non_sufficient_funds')
# Check if user is authorized to perform this action
check_auth(business_identifier=None, account_id=account_id, one_of_roles=[MAKE_PAYMENT, EDIT_ROLE, VIEW_ROLE])
page: int = int(request.args.get('page', '1'))
limit: int = int(request.args.get('limit', '10'))
response, status = NonSufficientFundsService.find_all_non_sufficient_funds_invoices(account_id=account_id,
page=page,
limit=limit), HTTPStatus.OK
current_app.logger.debug('>get_non_sufficient_funds')
return jsonify(response), status
1 change: 1 addition & 0 deletions pay-api/src/pay_api/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from .hashing import HashingService
from .internal_pay_service import InternalPayService
from .invoice import Invoice as InvoiceService
from .non_sufficient_funds import NonSufficientFundsService
from .payment import Payment
from .payment_service import PaymentService
from .payment_transaction import PaymentTransaction as TransactionService
Expand Down
192 changes: 192 additions & 0 deletions pay-api/src/pay_api/services/non_sufficient_funds.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# Copyright © 2019 Province of British Columbia
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Service to manage Non-Sufficient Funds."""
from __future__ import annotations

from typing import Optional

from flask import current_app

from pay_api.models import Invoice as InvoiceModel
from pay_api.models import InvoiceReference as InvoiceReferenceModel
from pay_api.models import InvoiceSchema, NonSufficientFundsModel, NonSufficientFundsSchema
from pay_api.models import Payment as PaymentModel
from pay_api.models import PaymentAccount as PaymentAccountModel
from pay_api.models import PaymentSchema, db


class NonSufficientFundsService: # pylint: disable=too-many-instance-attributes,too-many-public-methods
"""Service to manage Non-Sufficient Funds related operations."""

def __init__(self):
"""Initialize the service."""
self.__dao = None
self._id: int = None
self._invoice_id: int = None
self._description: Optional[str] = None

@property
def _dao(self):
if not self.__dao:
self.__dao = NonSufficientFundsModel()
return self.__dao

@_dao.setter
def _dao(self, value):
seeker25 marked this conversation as resolved.
Show resolved Hide resolved
self.__dao = value
self.id: int = self._dao.id
self.invoice_id: int = self._dao.invoice_id
self.description: str = self._dao.description

@property
def id(self):
"""Return the _id."""
return self._id

@id.setter
def id(self, value: int):
"""Set the _id."""
self._id = value
self._dao.id = value

@property
def invoice_id(self):
"""Return the _invoice_id."""
return self._invoice_id

@invoice_id.setter
def invoice_id(self, value: int):
"""Set the _invoice_id."""
self._invoice_id = value
self._dao.invoice_id = value

@property
def description(self):
"""Return the Non-Sufficient Funds description."""
return self._description

@description.setter
def description(self, value: str):
"""Set the Non-Sufficient Funds description."""
self._description = value
self._dao.description = value

def commit(self):
"""Save the information to the DB."""
return self._dao.commit()

def rollback(self):
"""Rollback."""
return self._dao.rollback()

def flush(self):
"""Save the information to the DB."""
return self._dao.flush()

def save(self):
"""Save the information to the DB."""
return self._dao.save()

def asdict(self):
"""Return the Non-Sufficient Funds as a python dict."""
non_sufficient_funds_schema = NonSufficientFundsSchema()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CATTRS

d = non_sufficient_funds_schema.dump(self._dao)
return d

@staticmethod
def populate(value: NonSufficientFundsModel):
"""Populate Non-Sufficient Funds Service."""
non_sufficient_funds_service: NonSufficientFundsService = NonSufficientFundsService()
non_sufficient_funds_service._dao = value # pylint: disable=protected-access
return non_sufficient_funds_service

@staticmethod
def query_all_non_sufficient_funds_invoices(account_id: str, page: int, limit: int):
"""Return all Non-Sufficient Funds invoices."""
query = db.session.query(PaymentModel, InvoiceModel) \
seeker25 marked this conversation as resolved.
Show resolved Hide resolved
.join(PaymentAccountModel, PaymentAccountModel.id == PaymentModel.payment_account_id) \
.outerjoin(InvoiceReferenceModel, InvoiceReferenceModel.invoice_number == PaymentModel.invoice_number) \
.outerjoin(InvoiceModel, InvoiceReferenceModel.invoice_id == InvoiceModel.id) \
.join(NonSufficientFundsModel, InvoiceModel.id == NonSufficientFundsModel.invoice_id) \
.filter(PaymentAccountModel.auth_account_id == account_id) \
.filter(PaymentModel.paid_amount > 0)

query = query.order_by(PaymentModel.id.asc())
pagination = query.paginate(per_page=limit, page=page)
seeker25 marked this conversation as resolved.
Show resolved Hide resolved
results, total = pagination.items, pagination.total

return results, total

@staticmethod
def find_all_non_sufficient_funds_invoices(account_id: str, page: int, limit: int):
# pylint: disable=too-many-locals
seeker25 marked this conversation as resolved.
Show resolved Hide resolved
"""Return all Non-Sufficient Funds."""
results, total = NonSufficientFundsService.query_all_non_sufficient_funds_invoices(account_id=account_id,
seeker25 marked this conversation as resolved.
Show resolved Hide resolved
page=page, limit=limit)

data = {
'total': total,
'page': page,
'limit': limit,
'invoices': [],
'total_amount': 0,
'total_amount_remaining': 0,
'total_nsf_amount': 0,
'total_nsf_count': 0
}

payment_schema = PaymentSchema()
inv_schema = InvoiceSchema(exclude=('receipts', 'references', '_links'))
last_payment_id = None
total_amount_to_pay = 0
total_amount_paid = 0
total_nsf_amount = 0
total_nsf_count = 0

for payment, invoice in results:
seeker25 marked this conversation as resolved.
Show resolved Hide resolved
if payment.id != last_payment_id:
total_amount_paid += payment.paid_amount
total_amount_to_pay += invoice.total

nsf_line_items = [item for item in invoice.line_items if item.description == 'NSF']
total_nsf_count += len(nsf_line_items)
total_nsf_amount += sum(item.total for item in nsf_line_items)

payment_dict = payment_schema.dump(payment)
payment_dict['invoices'] = [inv_schema.dump(invoice)]
data['invoices'].append(payment_dict)
else:
payment_dict['invoices'].append(inv_schema.dump(invoice))
last_payment_id = payment.id

data['total_amount'] = total_amount_to_pay - total_nsf_amount
data['total_amount_remaining'] = total_amount_to_pay - total_amount_paid
data['total_nsf_amount'] = total_nsf_amount
data['total_nsf_count'] = total_nsf_count

return data

@staticmethod
def save_non_sufficient_funds(invoice_id: int, description: str) -> NonSufficientFundsService:
"""Create Non-Sufficient Funds record."""
current_app.logger.debug('<save_non_sufficient_funds')
non_sufficient_funds_service = NonSufficientFundsService()

non_sufficient_funds_service.invoice_id = invoice_id
non_sufficient_funds_service.description = description
non_sufficient_funds_dao = non_sufficient_funds_service.save()

non_sufficient_funds_service = NonSufficientFundsService.populate(non_sufficient_funds_dao)
current_app.logger.debug('>save_non_sufficient_funds')
return non_sufficient_funds_service
50 changes: 50 additions & 0 deletions pay-api/tests/unit/api/test_non_sufficient_funds.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Copyright © 2019 Province of British Columbia
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Tests to assure the Non-Sufficient Funds end-point.

Test-Suite to ensure that the /nsf endpoint is working as expected.
"""
from pay_api.models import FeeSchedule as FeeScheduleModel
from pay_api.models import PaymentAccount as PaymentAccountModel
from pay_api.utils.enums import InvoiceStatus
from tests.utilities.base_test import (
factory_invoice, factory_invoice_reference, factory_non_sufficient_funds, factory_payment, factory_payment_account,
factory_payment_line_item, get_claims, token_header)


def test_get_non_sufficient_funds(session, client, jwt, app):
"""Assert that the endpoint returns 200."""
token = jwt.create_jwt(get_claims(), token_header)
headers = {'Authorization': f'Bearer {token}', 'content-type': 'application/json'}

invoice_number = '10001'
payment_account = factory_payment_account()
payment_account.save()
payment = factory_payment(payment_account_id=payment_account.id, paid_amount=1, invoice_number=invoice_number)
payment.save()
invoice = factory_invoice(
payment_account=payment_account, status_code=InvoiceStatus.SETTLEMENT_SCHEDULED.value, paid=1, total=200)
invoice.save()
fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type('CP', 'OTANN')
factory_payment_line_item(invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id, description='NSF',
total=100)
factory_invoice_reference(invoice_id=invoice.id, invoice_number=invoice_number).save()
factory_non_sufficient_funds(invoice_id=invoice.id, description='NSF').save()

auth_account_id = PaymentAccountModel.find_by_id(payment_account.id).auth_account_id

nsf = client.get(f'/api/v1/accounts/{auth_account_id}/nsf', headers=headers)
assert nsf.status_code == 200
assert nsf.json.get('total') == 1
Loading
Loading