Skip to content

Commit

Permalink
19723 - Linking EFT Job (#1464)
Browse files Browse the repository at this point in the history
* Linking EFTs

* Minor cleanup

* Tests

* Remove print
  • Loading branch information
rodrigo-barraza authored Apr 15, 2024
1 parent 727d338 commit e020b2d
Show file tree
Hide file tree
Showing 5 changed files with 315 additions and 15 deletions.
3 changes: 3 additions & 0 deletions jobs/payment-jobs/invoke_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from services import oracle_db
from tasks.eft_transfer_task import EftTransferTask
from tasks.routing_slip_task import RoutingSlipTask
from tasks.electronic_funds_transfer_task import ElectronicFundsTransferTask
from tasks.statement_due_task import StatementDueTask
from utils.logger import setup_logging

Expand Down Expand Up @@ -131,6 +132,8 @@ def run(job_name, argument=None):
RoutingSlipTask.process_correction()
RoutingSlipTask.adjust_routing_slips()
application.logger.info(f'<<<< Completed Routing Slip tasks >>>>')
elif job_name == 'EFT':
ElectronicFundsTransferTask.link_electronic_funds_transfer()
elif job_name == 'EJV_PAYMENT':
EjvPaymentTask.create_ejv_file()
application.logger.info(f'<<<< Completed running EJV payment >>>>')
Expand Down
16 changes: 8 additions & 8 deletions jobs/payment-jobs/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

187 changes: 187 additions & 0 deletions jobs/payment-jobs/tasks/electronic_funds_transfer_task.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
# 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.
"""Task for linking electronic funds transfers."""

from datetime import datetime
from typing import List

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 EFTCredit as EFTCreditModel
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 db
from pay_api.services.cfs_service import CFSService
from pay_api.services import EFTShortNamesService
from pay_api.services.receipt import Receipt
from pay_api.utils.enums import CfsAccountStatus, EFTShortnameState, InvoiceReferenceStatus, InvoiceStatus
from pay_api.utils.util import generate_receipt_number
from sentry_sdk import capture_message


class ElectronicFundsTransferTask: # pylint:disable=too-few-public-methods
"""Task to link electronic funds transfers."""

@classmethod
def link_electronic_funds_transfers(cls):
"""Create invoice in CFS.
Steps:
1. Find all pending EFT Short Names in a LINKED state.
2. Receipts created in CAS representing the EFT.
3. Apply the receipts to the invoices.
4. Notify mailer
"""
eft_short_names = cls._get_eft_short_names_by_state(EFTShortnameState.LINKED.value)
for eft_short_name in eft_short_names:
try:
current_app.logger.debug(f'Linking 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)
eft_credit: EFTCreditModel = EFTCreditModel.find_by_payment_account_id(payment_account.id)

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()

receipt_number = generate_receipt_number(payment.id)
CFSService.create_cfs_receipt(
cfs_account=cfs_account,
rcpt_number=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'))

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

eft_credit.remaining_amount = eft_credit.amount - total_invoice_amount

eft_short_name.save()

except Exception as e: # NOQA # pylint: disable=broad-except
capture_message(
f'Error on Electronics Funds Transfer ID:={eft_short_name.id}, '
f'electronic funds transfer : {eft_short_name.id}, ERROR : {str(e)}', level='error')
current_app.logger.error(e)
continue

@classmethod
def _get_eft_short_names_by_state(cls, state: EFTShortnameState) -> List[EFTShortnameModel]:
"""Get electronic funds transfer by state."""
query = db.session.query(EFTShortnameModel) \
.join(PaymentAccountModel, PaymentAccountModel.auth_account_id == EFTShortnameModel.auth_account_id) \
.join(CfsAccountModel, CfsAccountModel.account_id == PaymentAccountModel.id) \
.filter(CfsAccountModel.status == CfsAccountStatus.ACTIVE.value)

if state == EFTShortnameState.UNLINKED.value:
query = query.filter(EFTShortnameModel.auth_account_id.is_(None))
if state == EFTShortnameState.LINKED.value:
query = query.filter(EFTShortnameModel.auth_account_id.isnot(None))

return query.all()

@classmethod
def _apply_electronic_funds_transfers_to_pending_invoices(cls,
eft_short_name: EFTShortnameModel,
payment: PaymentModel) -> float:
"""Apply the electronic funds transfers again."""
current_app.logger.info(
f'Applying electronic funds transfer to pending invoices: {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)

invoices: List[InvoiceModel] = EFTShortNamesService.get_invoices_owing(
eft_short_name.auth_account_id)

current_app.logger.info(f'Found {len(invoices)} to apply receipt')
applied_amount = 0
for invoice in invoices:
invoice_reference: InvoiceReferenceModel = InvoiceReferenceModel.find_by_invoice_id_and_status(
invoice.id, InvoiceReferenceStatus.ACTIVE.value
)

# 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
)

# 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:
applied_amount += invoice.total
invoice_reference.status_code = InvoiceReferenceStatus.COMPLETED.value
invoice.invoice_status_code = InvoiceStatus.PAID.value
invoice.payment_date = datetime.now()

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:
"""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}')

# If balance of receipt is zero, continue to next receipt.
receipt_balance_before_apply = float(
CFSService.get_receipt(cfs_account, receipt_number).get('unapplied_amount')
)
current_app.logger.debug(f'Current balance on {receipt_number} = {receipt_balance_before_apply}')
if receipt_balance_before_apply == 0:
current_app.logger.debug(f'Applying receipt {receipt_number} to {invoice_number}')
receipt_response = CFSService.apply_receipt(cfs_account, receipt_number, invoice_number)

receipt = Receipt()
receipt.receipt_number = receipt_response.json().get('receipt_number', None)
receipt_amount = receipt_balance_before_apply - float(receipt_response.json().get('unapplied_amount'))
receipt.receipt_amount = receipt_amount
receipt.invoice_id = invoice.id
receipt.receipt_date = datetime.now()
receipt.flush()

invoice_from_cfs = CFSService.get_invoice(cfs_account, invoice_number)
if invoice_from_cfs.get('amount_due') == 0:
has_errors = True
return has_errors

except Exception as e: # NOQA # pylint: disable=broad-except
capture_message(
f'Error on creating electronic funds transfer invoice: account id={payment_account.id}, '
f'electronic funds transfer : {eft_short_name.id}, ERROR : {str(e)}', level='error')
current_app.logger.error(e)
has_errors = True

return has_errors
53 changes: 46 additions & 7 deletions jobs/payment-jobs/tests/jobs/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@
from datetime import datetime, timedelta

from pay_api.models import (
CfsAccount, DistributionCode, DistributionCodeLink, EFTShortnames, Invoice, InvoiceReference, Payment,
PaymentAccount, PaymentLineItem, Receipt, Refund, RefundsPartial, RoutingSlip, StatementRecipients,
StatementSettings)
CfsAccount, DistributionCode, DistributionCodeLink, EFTCredit, EFTFile, EFTShortnames, EFTTransaction, Invoice,
InvoiceReference, Payment, PaymentAccount, PaymentLineItem, Receipt, Refund, RefundsPartial, RoutingSlip,
StatementRecipients, StatementSettings)
from pay_api.utils.enums import (
CfsAccountStatus, InvoiceReferenceStatus, InvoiceStatus, LineItemStatus, PaymentMethod, PaymentStatus,
PaymentSystem, RoutingSlipStatus)
CfsAccountStatus, EFTProcessStatus, InvoiceReferenceStatus, InvoiceStatus, LineItemStatus, PaymentMethod,
PaymentStatus, PaymentSystem, RoutingSlipStatus)


def factory_premium_payment_account(bcol_user_id='PB25020', bcol_account_id='1234567890', auth_account_id='1234'):
Expand Down Expand Up @@ -65,15 +65,19 @@ def factory_payment(
payment_system_code: str = 'PAYBC', payment_method_code: str = 'CC',
payment_status_code: str = PaymentStatus.CREATED.value,
payment_date: datetime = datetime.now(),
invoice_number: str = None
invoice_number: str = None,
payment_account_id: int = None,
invoice_amount: float = None,
):
"""Return Factory."""
return Payment(
payment_system_code=payment_system_code,
payment_method_code=payment_method_code,
payment_status_code=payment_status_code,
payment_date=payment_date,
invoice_number=invoice_number
invoice_number=invoice_number,
payment_account_id=payment_account_id,
invoice_amount=invoice_amount
).save()


Expand Down Expand Up @@ -230,6 +234,41 @@ def factory_create_eft_shortname(auth_account_id: str, short_name: str):
return short_name


def factory_create_eft_credit(amount=100, remaining_amount=0, eft_file_id=1, short_name_id=1, payment_account_id=1,
eft_transaction_id=1):
"""Return Factory."""
eft_credit = EFTCredit(
amount=amount,
remaining_amount=remaining_amount,
eft_file_id=eft_file_id,
short_name_id=short_name_id,
payment_account_id=payment_account_id,
eft_transaction_id=eft_transaction_id
).save()
return eft_credit


def factory_create_eft_file(file_ref='test.txt', status_code=EFTProcessStatus.COMPLETED.value):
"""Return Factory."""
eft_file = EFTFile(
file_ref=file_ref,
status_code=status_code
).save()
return eft_file


def factory_create_eft_transaction(file_id=1, line_number=1, line_type='T',
status_code=EFTProcessStatus.COMPLETED.value):
"""Return Factory."""
eft_transaction = EFTTransaction(
file_id=file_id,
line_number=line_number,
line_type=line_type,
status_code=status_code,
).save()
return eft_transaction


def factory_create_account(auth_account_id: str = '1234', payment_method_code: str = PaymentMethod.DIRECT_PAY.value,
status: str = CfsAccountStatus.PENDING.value, statement_notification_enabled: bool = True):
"""Return payment account model."""
Expand Down
Loading

0 comments on commit e020b2d

Please sign in to comment.