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

19162 - Payment Reconciliation NSF Implementation #1382

Merged
merged 14 commits into from
Jan 23, 2024
Merged
27 changes: 27 additions & 0 deletions pay-api/migrations/versions/2024_01_22_fccdab259e05_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Adding invoice_number column to non_sufficient_funds table

Revision ID: fccdab259e05
Revises: b65365f7852b
Create Date: 2024-01-22 17:13:44.797905

"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = 'fccdab259e05'
down_revision = 'b65365f7852b'
branch_labels = None
depends_on = None


def upgrade():
op.add_column('non_sufficient_funds', sa.Column(
'invoice_number', sa.String(length=50), nullable=True, comment='CFS Invoice number'))
op.create_index(op.f('ix_non_sufficient_funds_invoice_number'), 'non_sufficient_funds', ['invoice_number'], unique=False)


def downgrade():
op.drop_index(op.f('ix_non_sufficient_funds_invoice_number'), table_name='non_sufficient_funds')
op.drop_column('non_sufficient_funds', 'invoice_number')
8 changes: 6 additions & 2 deletions pay-api/src/pay_api/models/non_sufficient_funds.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,26 +40,30 @@ class NonSufficientFundsModel(BaseModel): # pylint: disable=too-many-instance-a
'id',
'description',
'invoice_id',
'invoice_number'
]
}

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('invoices.id'), nullable=False)
invoice_number = db.Column(db.String(50), nullable=True, index=True, comment='CFS Invoice number')
seeker25 marked this conversation as resolved.
Show resolved Hide resolved


@define
class NonSufficientFundsSchema: # pylint: disable=too-few-public-methods
"""Used to search for NSF records."""

id: int
invoice_id: int
description: str
invoice_id: int
invoice_number: str

@classmethod
def from_row(cls, row: NonSufficientFundsModel):
"""From row is used so we don't tightly couple to our database class.

https://www.attrs.org/en/stable/init.html
"""
return cls(id=row.id, invoice_id=row.invoice_id, description=row.description)
return cls(id=row.id, invoice_id=row.invoice_id, invoice_number=row.invoice_number,
description=row.description)
12 changes: 7 additions & 5 deletions pay-api/src/pay_api/services/non_sufficient_funds.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,14 @@ def populate(value: NonSufficientFundsModel):
return non_sufficient_funds_service

@staticmethod
def save_non_sufficient_funds(invoice_id: int, description: str) -> NonSufficientFundsService:
def save_non_sufficient_funds(invoice_id: int, invoice_number: str, 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.dao.invoice_id = invoice_id
non_sufficient_funds_service.dao.description = description
non_sufficient_funds_service.dao.invoice_id = invoice_id
non_sufficient_funds_service.dao.invoice_number = invoice_number
non_sufficient_funds_dao = non_sufficient_funds_service.dao.save()

non_sufficient_funds_service = NonSufficientFundsService.populate(non_sufficient_funds_dao)
Expand Down Expand Up @@ -88,6 +89,7 @@ def query_all_non_sufficient_funds_invoices(account_id: str):
[(PaymentLineItemModel.description == ReverseOperation.NSF.value, PaymentLineItemModel.total)],
else_=0)).label('total_amount'))
.join(PaymentAccountModel, PaymentAccountModel.id == InvoiceModel.payment_account_id)
.outerjoin(NonSufficientFundsModel, NonSufficientFundsModel.invoice_id == InvoiceModel.id)
.join(PaymentLineItemModel, PaymentLineItemModel.invoice_id == InvoiceModel.id)
.filter(PaymentAccountModel.auth_account_id == account_id)
)
Expand All @@ -111,8 +113,8 @@ def find_all_non_sufficient_funds_invoices(account_id: str):
data = {
'total': total,
seeker25 marked this conversation as resolved.
Show resolved Hide resolved
'invoices': new_invoices,
'total_amount': float(aggregate_totals.total_amount),
'total_amount_remaining': float(aggregate_totals.total_amount_remaining),
'total_amount': float(aggregate_totals.total_amount) if total > 0 else 0,
seeker25 marked this conversation as resolved.
Show resolved Hide resolved
'total_amount_remaining': float(aggregate_totals.total_amount_remaining) if total > 0 else 0,
seeker25 marked this conversation as resolved.
Show resolved Hide resolved
'nsf_amount': float(aggregate_totals.nsf_amount)
}

Expand All @@ -139,7 +141,7 @@ def create_non_sufficient_funds_statement_pdf(account_id: str, **kwargs):
'totalAmount': invoice['total_amount'],
'nsfAmount': invoice['nsf_amount'],
'invoices': invoice['invoices'],
'invoiceNumber': invoice_reference.invoice_number,
'invoiceNumber': invoice_reference.invoice_number if hasattr(invoice_reference, 'invoice_number') else None
seeker25 marked this conversation as resolved.
Show resolved Hide resolved
}

invoice_pdf_dict = {
Expand Down
5 changes: 3 additions & 2 deletions pay-api/tests/unit/api/test_non_sufficient_funds.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def test_get_non_sufficient_funds(session, client, jwt, app):
token = jwt.create_jwt(get_claims(), token_header)
headers = {'Authorization': f'Bearer {token}', 'content-type': 'application/json'}

invoice_number = '10001'
invoice_number = 'REG00000001'
payment_account = factory_payment_account()
payment_account.save()
payment = factory_payment(payment_account_id=payment_account.id, paid_amount=0, invoice_number=invoice_number)
Expand Down Expand Up @@ -57,7 +57,8 @@ def test_get_non_sufficient_funds(session, client, jwt, app):

invoice_reference = factory_invoice_reference(invoice_id=invoice.id, invoice_number=invoice_number)
invoice_reference.save()
non_sufficient_funds = factory_non_sufficient_funds(invoice_id=invoice.id, description='NSF')
non_sufficient_funds = factory_non_sufficient_funds(invoice_id=invoice.id, invoice_number=payment.invoice_number,
description='NSF')
non_sufficient_funds.save()

nsf = client.get(f'/api/v1/accounts/{payment_account.auth_account_id}/nsf', headers=headers)
Expand Down
8 changes: 5 additions & 3 deletions pay-api/tests/unit/models/test_non_sufficient_funds.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,16 @@
def test_non_sufficient_funds(session):
"""Assert Non-Sufficient Funds defaults are stored."""
payment_account = factory_payment_account()
payment = factory_payment()
payment = factory_payment(invoice_number='REG00000001')
payment_account.save()
payment.save()
invoice = factory_invoice(payment_account=payment_account)
invoice.save()
non_sufficient_funds = factory_non_sufficient_funds(invoice_id=invoice.id, description='NSF')
non_sufficient_funds = factory_non_sufficient_funds(
invoice_id=invoice.id, invoice_number=payment.invoice_number, description='NSF')
non_sufficient_funds.save()

assert non_sufficient_funds.id is not None
assert non_sufficient_funds.invoice_id is not None
assert non_sufficient_funds.description == 'NSF'
assert non_sufficient_funds.invoice_id is not None
assert non_sufficient_funds.invoice_number == 'REG00000001'
8 changes: 5 additions & 3 deletions pay-api/tests/unit/services/test_non_sufficient_funds.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@
def test_save_non_sufficient_funds(session):
"""Test save_non_sufficient_funds."""
payment_account = factory_payment_account()
payment = factory_payment()
payment = factory_payment(invoice_number='REG00000001')
payment_account.save()
payment.save()
invoice = factory_invoice(payment_account=payment_account)
invoice.save()
non_sufficient_funds = NonSufficientFundsService.save_non_sufficient_funds(invoice_id=invoice.id,
invoice_number=payment.invoice_number,
description='NSF')
assert non_sufficient_funds
assert non_sufficient_funds['description'] == 'NSF'
Expand All @@ -43,7 +44,7 @@ def test_find_all_non_sufficient_funds_invoices(session):
"""Test find_all_non_sufficient_funds_invoices."""
payment_account = factory_payment_account()
payment_account.save()
payment = factory_payment(payment_account_id=payment_account.id, paid_amount=0, invoice_number='10001',
payment = factory_payment(payment_account_id=payment_account.id, paid_amount=0, invoice_number='REG00000001',
payment_method_code='PAD')
payment.save()
invoice = factory_invoice(
Expand All @@ -69,7 +70,8 @@ def test_find_all_non_sufficient_funds_invoices(session):

invoice_reference = factory_invoice_reference(invoice_id=invoice.id, invoice_number=payment.invoice_number)
invoice_reference.save()
non_sufficient_funds = factory_non_sufficient_funds(invoice_id=invoice.id, description='NSF')
non_sufficient_funds = factory_non_sufficient_funds(invoice_id=invoice.id, invoice_number=payment.invoice_number,
description='NSF')
non_sufficient_funds.save()

find_non_sufficient_funds = NonSufficientFundsService.find_all_non_sufficient_funds_invoices(
Expand Down
4 changes: 2 additions & 2 deletions pay-api/tests/utilities/base_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -888,9 +888,9 @@ def factory_eft_shortname(short_name: str, auth_account_id: str = None):
return EFTShortnames(short_name=short_name, auth_account_id=auth_account_id)


def factory_non_sufficient_funds(invoice_id: int, description: str = None):
def factory_non_sufficient_funds(invoice_id: int, invoice_number: str, description: str = None):
"""Return a Non-Sufficient Funds Model."""
return NonSufficientFundsModel(invoice_id=invoice_id, description=description)
return NonSufficientFundsModel(invoice_id=invoice_id, invoice_number=invoice_number, description=description)


def factory_distribution_code(name: str, client: str = '111', reps_centre: str = '22222', service_line: str = '33333',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from pay_api.models import Receipt as ReceiptModel
from pay_api.models import db
from pay_api.services.cfs_service import CFSService
from pay_api.services.non_sufficient_funds import NonSufficientFundsService
from pay_api.services.payment_transaction import PaymentTransaction as PaymentTransactionService
from pay_api.services.queue_publisher import publish
from pay_api.utils.enums import (
Expand Down Expand Up @@ -441,23 +442,24 @@ def _process_partial_paid_invoices(inv_ref: InvoiceReferenceModel, row):

def _process_failed_payments(row):
"""Handle failed payments."""
# 1. Set the cfs_account status as FREEZE.
# 2. Call cfs api to Stop further PAD on this account.
# 3. Reverse the invoice_reference status to ACTIVE, invoice status to SETTLEMENT_SCHED, and delete receipt.
# 4. Create an NSF invoice for this account.
# 5. Create invoice reference for the newly created NSF invoice.
# 6. Adjust invoice in CFS to include NSF fees.
# 1. Check if there is an NSF record for this account, if there isn't, proceed.
# 2. SET cfs_account status to FREEZE.
# 3. Call CFS API to stop further PAD on this account.
# 4. Reverse the invoice_reference status to ACTIVE, invoice status to SETTLEMENT_SCHED, and delete receipt.
# 5. Create an NSF invoice for this account.
# 6. Create invoice reference for the newly created NSF invoice.
# 7. Adjust invoice in CFS to include NSF fees.
inv_number = _get_row_value(row, Column.TARGET_TXN_NO)
# If there is a FAILED payment record for this; it means it's a duplicate event. Ignore it.
payment: PaymentModel = PaymentModel.find_payment_by_invoice_number_and_status(
inv_number, PaymentStatus.FAILED.value
)
if payment:
logger.info('Ignoring duplicate NSF message for invoice : %s ', inv_number)
payment_account: PaymentAccountModel = _get_payment_account(row)

# If there is an NSF invoice with a remaining nsf_amount balance, it means it's a duplicate NSF event. Ignore it.
seeker25 marked this conversation as resolved.
Show resolved Hide resolved
non_sufficient_funds = NonSufficientFundsService.find_all_non_sufficient_funds_invoices(
account_id=payment_account.auth_account_id)
if non_sufficient_funds['nsf_amount'] > 0:
logger.info('Ignoring duplicate NSF event for account: %s ', payment_account.auth_account_id)
return False

# Set CFS Account Status.
payment_account: PaymentAccountModel = _get_payment_account(row)
cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id(payment_account.id)
is_already_frozen = cfs_account.status == CfsAccountStatus.FREEZE.value
logger.info('setting payment account id : %s status as FREEZE', payment_account.id)
Expand Down Expand Up @@ -734,6 +736,9 @@ def _create_nsf_invoice(cfs_account: CfsAccountModel, inv_number: str,
created_by='SYSTEM'
)
invoice = invoice.save()

NonSufficientFundsService.save_non_sufficient_funds(invoice_id=invoice.id, description='NSF')

distribution: DistributionCodeModel = DistributionCodeModel.find_by_active_for_fee_schedule(
fee_schedule.fee_schedule_id)

Expand Down
Loading