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

19722 - Unlinking EFT Task #1487

Merged
merged 11 commits into from
Apr 30, 2024
4 changes: 3 additions & 1 deletion jobs/payment-jobs/invoke_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@ def run(job_name, argument=None):
RoutingSlipTask.adjust_routing_slips()
application.logger.info(f'<<<< Completed Routing Slip tasks >>>>')
elif job_name == 'EFT':
ElectronicFundsTransferTask.link_electronic_funds_transfer()
ElectronicFundsTransferTask.link_electronic_funds_transfers()
ElectronicFundsTransferTask.unlink_electronic_funds_transfers()
application.logger.info(f'<<<< Completed EFT tasks >>>>')
elif job_name == 'EJV_PAYMENT':
EjvPaymentTask.create_ejv_file()
application.logger.info(f'<<<< Completed running EJV payment >>>>')
Expand Down
136 changes: 106 additions & 30 deletions jobs/payment-jobs/tasks/electronic_funds_transfer_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,21 @@

from flask import current_app
from pay_api.models import CfsAccount as CfsAccountModel
from pay_api.models import EFTShortnames as EFTShortnameModel
from pay_api.models import EFTShortnameLinks as EFTShortnameLinksModel
from pay_api.models import EFTCredit as EFTCreditModel
from pay_api.models import EFTShortnameLinks as EFTShortnameLinksModel
from pay_api.models import EFTShortnames as EFTShortNamesModel
from pay_api.models import Invoice as InvoiceModel
from pay_api.models import InvoiceReference as InvoiceReferenceModel
from pay_api.models import PaymentAccount as PaymentAccountModel
from pay_api.models import Payment as PaymentModel
from pay_api.models import PaymentAccount as PaymentAccountModel
from pay_api.models import PaymentLineItem as PaymentLineItemModel
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 import EFTShortNamesService
from pay_api.services.cfs_service import CFSService
from pay_api.services.receipt import Receipt
from pay_api.utils.enums import CfsAccountStatus, EFTShortnameStatus, InvoiceReferenceStatus, InvoiceStatus
from pay_api.utils.util import generate_receipt_number
from pay_api.utils.enums import (
CfsAccountStatus, EFTShortnameStatus, InvoiceReferenceStatus, InvoiceStatus, ReverseOperation)
from sentry_sdk import capture_message


Expand All @@ -40,6 +42,7 @@ class EFTShortnameInfo:

id: int
auth_account_id: str
version: int


class ElectronicFundsTransferTask: # pylint:disable=too-few-public-methods
Expand Down Expand Up @@ -67,10 +70,11 @@ def link_electronic_funds_transfers(cls):
payment = db.session.query(PaymentModel) \
.join(PaymentAccountModel, PaymentAccountModel.id == PaymentModel.payment_account_id) \
.join(EFTCreditModel, EFTCreditModel.payment_account_id == PaymentAccountModel.id) \
.join(EFTShortnameModel, EFTShortnameModel.id == EFTCreditModel.short_name_id) \
.filter(EFTShortnameModel.id == eft_short_name.id).first()
.join(EFTShortNamesModel, EFTShortNamesModel.id == EFTCreditModel.short_name_id) \
.filter(EFTShortNamesModel.id == eft_short_name.id).first()

receipt_number = payment.id

receipt_number = generate_receipt_number(payment.id)
CFSService.create_cfs_receipt(
cfs_account=cfs_account,
rcpt_number=receipt_number,
Expand All @@ -81,7 +85,7 @@ def link_electronic_funds_transfers(cls):

# apply receipt to cfs_account
total_invoice_amount = cls._apply_electronic_funds_transfers_to_pending_invoices(
eft_short_name, payment)
eft_short_name, receipt_number)
current_app.logger.debug(f'Total Invoice Amount : {total_invoice_amount}')

eft_credit.remaining_amount = eft_credit.amount - total_invoice_amount
Expand All @@ -96,16 +100,62 @@ def link_electronic_funds_transfers(cls):
continue

@classmethod
def _get_eft_short_names_by_status(cls, status: str) -> List[EFTShortnameModel]:
def unlink_electronic_funds_transfers(cls):
"""Unlink electronic funds transfers."""
eft_short_names = cls._get_eft_short_names_by_status(EFTShortnameStatus.UNLINKED.value)
for eft_short_name in eft_short_names:
try:
current_app.logger.debug(f'Unlinking Electronic Funds Transfer: {eft_short_name.id}')
payment_account: PaymentAccountModel = PaymentAccountModel.find_by_auth_account_id(
eft_short_name.auth_account_id)
cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id(payment_account.id)
Copy link
Collaborator

@seeker25 seeker25 Apr 20, 2024

Choose a reason for hiding this comment

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

This might get tricky if someone switches from EFT to PAD, I'm almost thinking for CFS account we should include the payment method in there? Thoughts? Otherwise someone could switch to PAD, this job runs and it's using the entirely wrong CFS account?

Because right now you can only go from Credit Card -> Online banking (ONLINE BANKING uses CFS account, can't remember if credit card does)
or PAD (CFS account) -> BCOL (No CFS account)
or just straight EJV

Copy link
Collaborator

@seeker25 seeker25 Apr 20, 2024

Choose a reason for hiding this comment

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

But we'll be introducing EFT -> PAD or EFT -> BCOL and vice versa

eft_credit: EFTCreditModel = EFTCreditModel.find_by_payment_account_id(payment_account.id)

payment = db.session.query(PaymentModel) \
Copy link
Collaborator

Choose a reason for hiding this comment

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

functionize this? looks duplicate to 60

.join(PaymentAccountModel, PaymentAccountModel.id == PaymentModel.payment_account_id) \
.join(EFTCreditModel, EFTCreditModel.payment_account_id == PaymentAccountModel.id) \
.join(EFTShortNamesModel, EFTShortNamesModel.id == EFTCreditModel.short_name_id) \
.filter(EFTShortNamesModel.id == eft_short_name.id).first()

eft_short_name.version += 1

receipt_number = f'{payment.id}R{eft_short_name.version}'

CFSService.reverse_rs_receipt_in_cfs(cfs_account, receipt_number, ReverseOperation.CORRECTION.value)

CFSService.create_cfs_receipt(
cfs_account=cfs_account,
rcpt_number=f'R{receipt_number}',
rcpt_date=payment.payment_date.strftime('%Y-%m-%d'),
amount=payment.invoice_amount,
payment_method=payment_account.payment_method,
access_token=CFSService.get_fas_token().json().get('access_token'))

cls._reset_invoices_and_references_to_created_for_electronic_funds_transfer(eft_short_name)

cls._apply_electronic_funds_transfers_to_pending_invoices(eft_short_name, receipt_number)

eft_credit.remaining_amount = eft_credit.amount

eft_short_name.save()

except Exception as e: # NOQA # pylint: disable=broad-except
capture_message(
f'Error on Processing UNLINKING for :={receipt_number}, '
f'EFT Short Name : {eft_short_name.id}, ERROR : {str(e)}', level='error')
current_app.logger.error(e)
continue

@classmethod
def _get_eft_short_names_by_status(cls, status: str) -> List[EFTShortNamesModel]:
"""Get electronic funds transfer by state."""
query = db.session.query(EFTShortnameModel.id.label('short_name_id'), EFTShortnameLinksModel.auth_account_id) \
.join(EFTShortnameLinksModel, EFTShortnameLinksModel.eft_short_name_id == EFTShortnameModel.id) \
query = db.session.query(EFTShortNamesModel.id.label('short_name_id'), EFTShortnameLinksModel.auth_account_id,
EFTShortNamesModel.version.label('short_name_version'), ) \
.join(EFTShortnameLinksModel, EFTShortnameLinksModel.eft_short_name_id == EFTShortNamesModel.id) \
.join(PaymentAccountModel, PaymentAccountModel.auth_account_id == EFTShortnameLinksModel.auth_account_id) \
.join(CfsAccountModel, CfsAccountModel.account_id == PaymentAccountModel.id) \
.filter(CfsAccountModel.status == CfsAccountStatus.ACTIVE.value)

if status == EFTShortnameStatus.UNLINKED.value:
query = query.filter(EFTShortnameLinksModel.id.is_(None))
if status == EFTShortnameStatus.LINKED.value:
query = query.filter(EFTShortnameLinksModel.status_code == status)

Expand All @@ -114,15 +164,16 @@ def _get_eft_short_names_by_status(cls, status: str) -> List[EFTShortnameModel]:

# Short name can have multiple linked accounts, prepare list of dataclasses with the associated
# auth_account_ids for the outer processing loops
for short_name_id, auth_account_id in result:
short_name_results.append(EFTShortnameInfo(id=short_name_id, auth_account_id=auth_account_id))
for short_name_id, auth_account_id, short_name_version in result:
short_name_results.append(EFTShortnameInfo(id=short_name_id, auth_account_id=auth_account_id,
version=short_name_version))

return short_name_results

@classmethod
def _apply_electronic_funds_transfers_to_pending_invoices(cls,
eft_short_name: EFTShortnameModel,
payment: PaymentModel) -> float:
eft_short_name: EFTShortNamesModel,
receipt_number) -> float:
"""Apply the electronic funds transfers again."""
current_app.logger.info(
f'Applying electronic funds transfer to pending invoices: {eft_short_name.id}')
Expand All @@ -143,10 +194,9 @@ def _apply_electronic_funds_transfers_to_pending_invoices(cls,
)

# apply invoice to the CFS_ACCOUNT
cls.apply_electronic_funds_transfer_to_invoice(
payment_account, cfs_account, eft_short_name, invoice, invoice_reference.invoice_number, payment
cls.apply_eft_receipt_to_invoice(
payment_account, cfs_account, eft_short_name, invoice, invoice_reference.invoice_number, receipt_number
)

# IF invoice balance is zero, then update records.
if CFSService.get_invoice(cfs_account=cfs_account, inv_number=invoice_reference.invoice_number) \
.get('amount_due') == 0:
Expand All @@ -158,18 +208,44 @@ def _apply_electronic_funds_transfers_to_pending_invoices(cls,
return applied_amount

@classmethod
def apply_electronic_funds_transfer_to_invoice(cls, # pylint: disable = too-many-arguments, too-many-locals
payment_account: PaymentAccountModel,
cfs_account: CfsAccountModel,
eft_short_name: EFTShortnameModel,
invoice: InvoiceModel,
invoice_number: str,
payment: PaymentModel) -> bool:
def _reset_invoices_and_references_to_created_for_electronic_funds_transfer(cls,
eft_short_name: EFTShortNamesModel):
"""Reset Invoices, Invoice references and Receipts for EFT."""
invoices: List[InvoiceModel] = db.session.query(InvoiceModel) \
.join(InvoiceReferenceModel, PaymentModel.invoice_number == InvoiceReferenceModel.invoice_number) \
.join(InvoiceModel, InvoiceReferenceModel.invoice_id == InvoiceModel.id) \
.join(PaymentLineItemModel, PaymentLineItemModel.invoice_id == InvoiceModel.id) \
.join(PaymentAccountModel, PaymentAccountModel.id == InvoiceModel.payment_account_id) \
.join(EFTCreditModel, EFTCreditModel.payment_account_id == PaymentAccountModel.id) \
.join(EFTShortNamesModel, EFTShortNamesModel.id == EFTCreditModel.short_name_id) \
.join(EFTShortnameLinksModel, EFTShortnameLinksModel.eft_short_name_id == EFTShortNamesModel.id) \
.filter(InvoiceModel.invoice_status_code == InvoiceStatus.PAID.value) \
.filter(EFTShortNamesModel.auth_account_id == eft_short_name.auth_account_id) \
.all()

for invoice in invoices:
# Reset Invoice and Invoice Reference statuses.
invoice.invoice_status_code = InvoiceStatus.CREATED.value
invoice_reference = InvoiceReferenceModel.find_by_invoice_id_and_status(
invoice.id, InvoiceReferenceStatus.COMPLETED.value
)
invoice_reference.status_code = InvoiceReferenceStatus.ACTIVE.value
# Delete receipts as they are now reversed in CFS.
for receipt in ReceiptModel.find_all_receipts_for_invoice(invoice.id):
db.session.delete(receipt)

@classmethod
def apply_eft_receipt_to_invoice(cls, # pylint: disable = too-many-arguments, too-many-locals
payment_account: PaymentAccountModel,
cfs_account: CfsAccountModel,
eft_short_name: EFTShortNamesModel,
invoice: InvoiceModel,
invoice_number: str,
receipt_number) -> bool:
"""Apply electronic funds transfers (receipts in CFS) to invoice."""
has_errors = False
# an invoice has to be applied to multiple receipts (incl. all linked RS); apply till the balance is zero
try:
receipt_number = generate_receipt_number(payment.id)
current_app.logger.debug(f'Apply receipt {receipt_number} on invoice {invoice_number} '
f'for electronic funds transfer {eft_short_name.id}')

Expand Down
15 changes: 8 additions & 7 deletions jobs/payment-jobs/tests/jobs/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,11 +218,11 @@ def factory_routing_slip_account(

def factory_create_eft_account(auth_account_id='1234', status=CfsAccountStatus.PENDING.value):
"""Return Factory."""
account = PaymentAccount(auth_account_id=auth_account_id,
payment_method=PaymentMethod.EFT.value,
name=f'Test {auth_account_id}').save()
CfsAccount(status=status, account_id=account.id).save()
return account
payment_account = PaymentAccount(auth_account_id=auth_account_id,
payment_method=PaymentMethod.EFT.value,
name=f'Test {auth_account_id}').save()
CfsAccount(status=status, account_id=payment_account.id).save()
return payment_account


def factory_create_eft_shortname(short_name: str):
Expand All @@ -234,12 +234,13 @@ def factory_create_eft_shortname(short_name: str):


def factory_eft_shortname_link(short_name_id: int, auth_account_id: str = '1234',
updated_by: str = None, updated_on: datetime = datetime.now()):
updated_by: str = None, updated_on: datetime = datetime.now(),
status_code: str = EFTShortnameStatus.LINKED.value):
"""Return an EFT short name link model."""
return EFTShortnameLinks(
eft_short_name_id=short_name_id,
auth_account_id=auth_account_id,
status_code=EFTShortnameStatus.LINKED.value,
status_code=status_code,
updated_by=updated_by,
updated_by_name=updated_by,
updated_on=updated_on
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,23 @@
from unittest.mock import patch

from pay_api.models import CfsAccount as CfsAccountModel
from pay_api.models import PaymentAccount as PaymentAccountModel
from pay_api.models import EFTShortnameLinks as EFTShortnameLinksModel
from pay_api.models import EFTShortnames as EFTShortnameModel
from pay_api.utils.enums import CfsAccountStatus, PaymentMethod
from pay_api.models import FeeSchedule as FeeScheduleModel
from pay_api.models import PaymentAccount as PaymentAccountModel
from pay_api.utils.enums import (
CfsAccountStatus, EFTShortnameStatus, InvoiceReferenceStatus, InvoiceStatus, PaymentMethod)
from pay_api.services import CFSService
from tasks.electronic_funds_transfer_task import ElectronicFundsTransferTask


from .factory import (
factory_create_eft_account, factory_create_eft_credit, factory_create_eft_file, factory_create_eft_shortname,
factory_create_eft_transaction, factory_eft_shortname_link, factory_invoice, factory_invoice_reference,
factory_payment)
factory_payment, factory_payment_line_item, factory_receipt)


def test_link_electronic_funds_transfer(session):
"""Test link electronic funds transfer."""
def test_link_electronic_funds_transfers(session):
"""Test link electronic funds transfers."""
auth_account_id = '1234'
short_name = 'TEST1'

Expand Down Expand Up @@ -77,3 +78,54 @@ def test_link_electronic_funds_transfer(session):

cfs_account: CfsAccountModel = CfsAccountModel.find_by_id(cfs_account.id)
assert cfs_account.status == CfsAccountStatus.ACTIVE.value


def test_unlink_electronic_funds_transfers(session):
"""Test unlink electronic funds transfers."""
auth_account_id = '1234'
short_name = 'TEST1'
receipt_number = '1111R'
invoice_number = '1234'

payment_account = factory_create_eft_account(auth_account_id=auth_account_id, status=CfsAccountStatus.ACTIVE.value)
eft_file = factory_create_eft_file()
eft_transaction = factory_create_eft_transaction(file_id=eft_file.id)
eft_short_name = factory_create_eft_shortname(short_name=short_name)
eft_short_name_link = factory_eft_shortname_link(
short_name_id=eft_short_name.id,
auth_account_id=auth_account_id,
updated_by='test',
status_code=EFTShortnameStatus.UNLINKED.value
).save()

invoice = factory_invoice(payment_account=payment_account, total=30,
status_code=InvoiceStatus.PAID.value,
payment_method_code=PaymentMethod.EFT.value)

factory_payment(payment_account_id=payment_account.id, payment_method_code=PaymentMethod.EFT.value,
invoice_amount=351.50, invoice_number=invoice_number)
fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type('CP', 'OTANN')
factory_payment_line_item(invoice_id=invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id)
factory_invoice_reference(invoice_id=invoice.id, status_code=InvoiceReferenceStatus.COMPLETED.value,
invoice_number=invoice_number)
factory_create_eft_credit(
amount=100, remaining_amount=0, eft_file_id=eft_file.id, short_name_id=eft_short_name.id,
payment_account_id=payment_account.id,
eft_transaction_id=eft_transaction.id)

factory_receipt(invoice.id, receipt_number)

eft_short_name = EFTShortnameModel.find_by_short_name(short_name)
eft_short_name_link = EFTShortnameLinksModel.find_by_short_name_id(eft_short_name.id)[0]
eft_short_name_link.updated_by = None
eft_short_name_link.updated_by_name = None
eft_short_name_link.updated_on = None
eft_short_name.save()

session.commit()

with patch('pay_api.services.CFSService.reverse_rs_receipt_in_cfs') as mock_reverse:
with patch('pay_api.services.CFSService.create_cfs_receipt') as mock_create_receipt:
ElectronicFundsTransferTask.unlink_electronic_funds_transfers()
mock_reverse.assert_called()
mock_create_receipt.assert_called()
Loading