diff --git a/jobs/payment-jobs/invoke_jobs.py b/jobs/payment-jobs/invoke_jobs.py index 7d03dbdd3..55fef4ffe 100755 --- a/jobs/payment-jobs/invoke_jobs.py +++ b/jobs/payment-jobs/invoke_jobs.py @@ -24,7 +24,6 @@ import config 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 @@ -146,9 +145,6 @@ def run(job_name, argument=None): elif job_name == 'BCOL_REFUND_CONFIRMATION': BcolRefundConfirmationTask.update_bcol_refund_invoices() application.logger.info(f'<<<< Completed running BCOL Refund Confirmation Job >>>>') - elif job_name == 'EFT_TRANSFER': - EftTransferTask.create_ejv_file() - application.logger.info(f'<<<< Completed Creating EFT Transfer File for transfer to internal GLs>>>>') else: application.logger.debug('No valid args passed. Exiting job without running any ***************') diff --git a/jobs/payment-jobs/poetry.lock b/jobs/payment-jobs/poetry.lock index 88cecd800..ff1195ca7 100644 --- a/jobs/payment-jobs/poetry.lock +++ b/jobs/payment-jobs/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "alembic" @@ -1693,7 +1693,7 @@ werkzeug = "3.0.1" type = "git" url = "https://github.com/bcgov/sbc-pay.git" reference = "feature-queue-python-upgrade" -resolved_reference = "727d3385439146f9616ded4548c22a4867f42ff3" +resolved_reference = "ba20cecf7e65065fa22dbfedb0f0bb5c1ee7ec94" subdirectory = "pay-api" [[package]] diff --git a/jobs/payment-jobs/tasks/eft_transfer_task.py b/jobs/payment-jobs/tasks/eft_transfer_task.py deleted file mode 100644 index c43bd7412..000000000 --- a/jobs/payment-jobs/tasks/eft_transfer_task.py +++ /dev/null @@ -1,292 +0,0 @@ -# Copyright © 2023 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 to create EFT Transfer Journal Voucher.""" - -import time -from datetime import datetime -from typing import List - -from flask import current_app -from pay_api.models import DistributionCode as DistributionCodeModel -from pay_api.models import EFTGLTransfer as EFTGLTransferModel -from pay_api.models import EFTShortnames as EFTShortnameModel -from pay_api.models import EjvFile as EjvFileModel -from pay_api.models import EjvHeader as EjvHeaderModel -from pay_api.models import EjvLink as EjvLinkModel -from pay_api.models import Invoice as InvoiceModel -from pay_api.models import PaymentAccount as PaymentAccountModel -from pay_api.models import PaymentLineItem as PaymentLineItemModel -from pay_api.models import db -from pay_api.services.flags import flags -from pay_api.utils.enums import ( - DisbursementStatus, EFTGlTransferType, EjvFileType, EJVLinkType, InvoiceStatus, PaymentMethod) -from sqlalchemy import exists, func - -from tasks.common.cgi_ejv import CgiEjv - - -class EftTransferTask(CgiEjv): - """Task to create EJV Files.""" - - @classmethod - def create_ejv_file(cls): - """Create JV files and upload to CGI. - - Steps: - 1. Find all invoices from invoice table for EFT Transfer. - 2. Group by fee schedule and create JV Header and JV Details. - 3. Upload the file to minio for future reference. - 4. Upload to sftp for processing. First upload JV file and then a TRG file. - 5. Update the statuses and create records to for the batch. - """ - eft_enabled = flags.is_on('enable-eft-payment-method', default=False) - if eft_enabled: - cls._create_ejv_file_for_eft_transfer() - - @staticmethod - def get_invoices_for_transfer(payment_account_id: int): - """Return invoices for EFT Holdings transfer.""" - # Return all EFT Paid invoices that don't already have an EFT GL Transfer record - invoices: List[InvoiceModel] = db.session.query(InvoiceModel) \ - .filter(InvoiceModel.invoice_status_code == InvoiceStatus.PAID.value) \ - .filter(InvoiceModel.payment_method_code == PaymentMethod.EFT.value) \ - .filter(InvoiceModel.payment_account_id == payment_account_id) \ - .filter(~exists().where((EFTGLTransferModel.invoice_id == InvoiceModel.id) & - (EFTGLTransferModel.transfer_type == EFTGlTransferType.TRANSFER.value))).all() - return invoices - - @staticmethod - def get_invoices_for_refund_reversal(payment_account_id: int): - """Return invoices for EFT reversal.""" - refund_inv_statuses = (InvoiceStatus.REFUNDED.value, InvoiceStatus.REFUND_REQUESTED.value, - InvoiceStatus.CREDITED.value) - # Future may need to re-evaluate when EFT Short name unlinking use cases are defined - invoices: List[InvoiceModel] = db.session.query(InvoiceModel) \ - .filter(InvoiceModel.invoice_status_code.in_(refund_inv_statuses)) \ - .filter(InvoiceModel.payment_method_code == PaymentMethod.EFT.value) \ - .filter(InvoiceModel.payment_account_id == payment_account_id) \ - .filter(InvoiceModel.disbursement_status_code == DisbursementStatus.COMPLETED.value) \ - .filter(~exists().where((EFTGLTransferModel.invoice_id == InvoiceModel.id) & - (EFTGLTransferModel.transfer_type == EFTGlTransferType.REVERSAL.value))).all() - current_app.logger.info(invoices) - return invoices - - @staticmethod - def get_account_ids() -> List[int]: - """Return account IDs for EFT payments.""" - query = db.session.query(func.DISTINCT(InvoiceModel.payment_account_id)) \ - .filter(InvoiceModel.invoice_status_code == InvoiceStatus.PAID.value) \ - .filter(InvoiceModel.payment_method_code == PaymentMethod.EFT.value) \ - .filter(~exists().where((EFTGLTransferModel.invoice_id == InvoiceModel.id) & - (EFTGLTransferModel.transfer_type == EFTGlTransferType.TRANSFER.value))) - return db.session.scalars(query).all() - - @staticmethod - def create_eft_gl_transfer(eft_holding_gl: str, line_distribution_gl: str, transfer_type: str, - line_item: PaymentLineItemModel, payment_account: PaymentAccountModel): - """Create EFT GL Transfer record.""" - short_name_id = db.session.query(EFTShortnameModel.id) \ - .filter(EFTShortnameModel.auth_account_id == payment_account.auth_account_id).one()[0] - source_gl = eft_holding_gl if transfer_type == EFTGlTransferType.TRANSFER.value else line_distribution_gl - target_gl = line_distribution_gl if transfer_type == EFTGlTransferType.TRANSFER.value else eft_holding_gl - now = datetime.now() - return EFTGLTransferModel( - invoice_id=line_item.invoice_id, - is_processed=True, - processed_on=now, - short_name_id=short_name_id, - source_gl=source_gl.strip(), - target_gl=target_gl.strip(), - transfer_amount=line_item.total, - transfer_type=transfer_type, - transfer_date=now - ) - - @classmethod - def _process_eft_transfer_invoices(cls, invoices: List[InvoiceModel], transfer_type: str, - eft_gl_transfers: dict = None) -> List[EFTGLTransferModel]: - """Create EFT GL Transfer for invoice line items.""" - eft_holding_gl = current_app.config.get('EFT_HOLDING_GL') - eft_gl_transfers = eft_gl_transfers or {} - - for invoice in invoices: - payment_account = PaymentAccountModel.find_by_id(invoice.payment_account_id) - for line_item in invoice.payment_line_items: - distribution_code: DistributionCodeModel = \ - DistributionCodeModel.find_by_id(line_item.fee_distribution_id) - - # Create line distribution transfer - line_distribution_code: DistributionCodeModel = DistributionCodeModel.find_by_id( - distribution_code.distribution_code_id - ) - - line_distribution = cls.get_distribution_string(line_distribution_code) - - line_gl_transfer = cls.create_eft_gl_transfer( - eft_holding_gl=eft_holding_gl, - line_distribution_gl=line_distribution, - transfer_type=transfer_type, - line_item=line_item, - payment_account=payment_account - ) - - eft_gl_transfers.setdefault(invoice.payment_account_id, []) - eft_gl_transfers[invoice.payment_account_id].append(line_gl_transfer) - db.session.add(line_gl_transfer) - - # Check for service fee, if there is one create a transfer record - if distribution_code.service_fee_distribution_code_id: - service_fee_distribution_code: DistributionCodeModel = DistributionCodeModel.find_by_id( - distribution_code.service_fee_distribution_code_id - ) - - service_fee_distribution = cls.get_distribution_string(service_fee_distribution_code) - - service_fee_gl_transfer = cls.create_eft_gl_transfer( - eft_holding_gl=eft_holding_gl, - line_distribution_gl=service_fee_distribution, - transfer_type=transfer_type, - line_item=line_item, - payment_account=payment_account - ) - service_fee_gl_transfer.transfer_amount = line_item.service_fees - eft_gl_transfers[invoice.payment_account_id].append(service_fee_gl_transfer) - db.session.add(service_fee_gl_transfer) - - return eft_gl_transfers - - @staticmethod - def process_invoice_ejv_links(invoices: List[InvoiceModel], ejv_header_model_id: int): - """Create EJV Invoice Links.""" - current_app.logger.info('Creating ejv invoice link records and setting invoice status.') - sequence = 1 - for inv in invoices: - current_app.logger.debug(f'Creating EJV Invoice Link for invoice id: {inv.id}') - # Create Ejv file link and flush - ejv_invoice_link = EjvLinkModel(link_id=inv.id, link_type=EJVLinkType.INVOICE.value, - ejv_header_id=ejv_header_model_id, - disbursement_status_code=DisbursementStatus.UPLOADED.value, - sequence=sequence) - db.session.add(ejv_invoice_link) - sequence += 1 - - @classmethod - def _create_ejv_file_for_eft_transfer(cls): # pylint:disable=too-many-locals, too-many-statements - """Create EJV file for the EFT Transfer and upload.""" - ejv_content: str = '' - batch_total: float = 0 - control_total: int = 0 - today = datetime.now() - transfer_desc = current_app.config.get('EFT_TRANSFER_DESC'). \ - format(today.strftime('%B').upper(), f'{today.day:0>2}')[:100] - transfer_desc = f'{transfer_desc:<100}' - - # Create a ejv file model record. - ejv_file_model: EjvFileModel = EjvFileModel( - file_type=EjvFileType.TRANSFER.value, - file_ref=cls.get_file_name(), - disbursement_status_code=DisbursementStatus.UPLOADED.value - ).flush() - batch_number = cls.get_batch_number(ejv_file_model.id) - batch_type = 'GA' - - account_ids = cls.get_account_ids() - - # JV Batch Header - batch_header: str = cls.get_batch_header(batch_number, batch_type) - - effective_date: str = cls.get_effective_date() - for account_id in account_ids: - account_jv: str = '' - payment_invoices = cls.get_invoices_for_transfer(account_id) - refund_invoices = cls.get_invoices_for_refund_reversal(account_id) - transfers = cls._process_eft_transfer_invoices(payment_invoices, EFTGlTransferType.TRANSFER.value) - cls._process_eft_transfer_invoices(refund_invoices, EFTGlTransferType.REVERSAL.value, transfers) - invoices = payment_invoices + refund_invoices - - ejv_header_model: EjvFileModel = EjvHeaderModel( - payment_account_id=account_id, - disbursement_status_code=DisbursementStatus.UPLOADED.value, - ejv_file_id=ejv_file_model.id - ).flush() - journal_name: str = cls.get_journal_name(ejv_header_model.id) - - line_number: int = 0 - total: float = 0 - - current_app.logger.info(f'Processing EFT Transfers for account_id: {account_id}.') - account_transfers: List[EFTGLTransferModel] = transfers[account_id] - - for eft_transfer in account_transfers: - invoice_number = f'#{eft_transfer.invoice_id}' - description = transfer_desc[:-len(invoice_number)] + invoice_number - description = f'{description[:100]:<100}' - - if eft_transfer.transfer_amount > 0: - total += eft_transfer.transfer_amount - flow_through = f'{eft_transfer.invoice_id:<110}' - - line_number += 1 - control_total += 1 - - # Debit from source gl - source_gl = f'{eft_transfer.source_gl}{cls.EMPTY:<16}' - target_gl = f'{eft_transfer.target_gl}{cls.EMPTY:<16}' - - account_jv = account_jv + cls.get_jv_line(batch_type, source_gl, description, - effective_date, flow_through, journal_name, - eft_transfer.transfer_amount, - line_number, 'D') - # Credit to target gl - account_jv = account_jv + cls.get_jv_line(batch_type, target_gl, description, - effective_date, flow_through, journal_name, - eft_transfer.transfer_amount, - line_number, 'C') - line_number += 1 - control_total += 1 - - batch_total += total - - # Skip if we have no total from the transfers. - if total > 0: - # A JV header for each account. - control_total += 1 - account_jv = cls.get_jv_header(batch_type, cls.get_journal_batch_name(batch_number), - journal_name, total) + account_jv - ejv_content = ejv_content + account_jv - - # Create ejv invoice link records and set invoice status - cls.process_invoice_ejv_links(invoices, ejv_header_model.id) - - db.session.flush() - - if not ejv_content: - db.session.rollback() - return - - # JV Batch Trailer - batch_trailer: str = cls.get_batch_trailer(batch_number, batch_total, batch_type, control_total) - ejv_content = f'{batch_header}{ejv_content}{batch_trailer}' - - # Create a file add this content. - file_path_with_name, trg_file_path = cls.create_inbox_and_trg_files(ejv_content) - - # Upload file and trg to FTP - current_app.logger.info('Uploading EFT Transfer file to ftp.') - cls.upload(ejv_content, cls.get_file_name(), file_path_with_name, trg_file_path) - - db.session.commit() - - # Add a sleep to prevent collision on file name. - time.sleep(1) diff --git a/jobs/payment-jobs/tasks/electronic_funds_transfer_task.py b/jobs/payment-jobs/tasks/electronic_funds_transfer_task.py index d8137b464..ce1983a88 100644 --- a/jobs/payment-jobs/tasks/electronic_funds_transfer_task.py +++ b/jobs/payment-jobs/tasks/electronic_funds_transfer_task.py @@ -12,13 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. """Task for linking electronic funds transfers.""" - +from dataclasses import dataclass 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 EFTShortnameLinks as EFTShortnameLinksModel from pay_api.models import EFTCredit as EFTCreditModel from pay_api.models import Invoice as InvoiceModel from pay_api.models import InvoiceReference as InvoiceReferenceModel @@ -28,11 +29,19 @@ 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.enums import CfsAccountStatus, EFTShortnameStatus, InvoiceReferenceStatus, InvoiceStatus from pay_api.utils.util import generate_receipt_number from sentry_sdk import capture_message +@dataclass +class EFTShortnameInfo: + """Consolidated EFT Short name information for processing.""" + + id: int + auth_account_id: str + + class ElectronicFundsTransferTask: # pylint:disable=too-few-public-methods """Task to link electronic funds transfers.""" @@ -46,7 +55,7 @@ def link_electronic_funds_transfers(cls): 3. Apply the receipts to the invoices. 4. Notify mailer """ - eft_short_names = cls._get_eft_short_names_by_state(EFTShortnameState.LINKED.value) + eft_short_names: List[EFTShortnameInfo] = cls._get_eft_short_names_by_status(EFTShortnameStatus.LINKED.value) for eft_short_name in eft_short_names: try: current_app.logger.debug(f'Linking Electronic Funds Transfer: {eft_short_name.id}') @@ -87,19 +96,28 @@ def link_electronic_funds_transfers(cls): continue @classmethod - def _get_eft_short_names_by_state(cls, state: EFTShortnameState) -> List[EFTShortnameModel]: + def _get_eft_short_names_by_status(cls, status: str) -> List[EFTShortnameModel]: """Get electronic funds transfer by state.""" - query = db.session.query(EFTShortnameModel) \ - .join(PaymentAccountModel, PaymentAccountModel.auth_account_id == EFTShortnameModel.auth_account_id) \ + query = db.session.query(EFTShortnameModel.id.label('short_name_id'), EFTShortnameLinksModel.auth_account_id) \ + .join(EFTShortnameLinksModel, EFTShortnameLinksModel.eft_short_name_id == EFTShortnameModel.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 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)) + if status == EFTShortnameStatus.UNLINKED.value: + query = query.filter(EFTShortnameLinksModel.id.is_(None)) + if status == EFTShortnameStatus.LINKED.value: + query = query.filter(EFTShortnameLinksModel.status_code == status) + + result = query.all() + short_name_results = [] + + # 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)) - return query.all() + return short_name_results @classmethod def _apply_electronic_funds_transfers_to_pending_invoices(cls, diff --git a/jobs/payment-jobs/tests/jobs/factory.py b/jobs/payment-jobs/tests/jobs/factory.py index b916f811a..e199cf1cc 100644 --- a/jobs/payment-jobs/tests/jobs/factory.py +++ b/jobs/payment-jobs/tests/jobs/factory.py @@ -20,12 +20,12 @@ from datetime import datetime, timedelta from pay_api.models import ( - CfsAccount, DistributionCode, DistributionCodeLink, EFTCredit, EFTFile, EFTShortnames, EFTTransaction, Invoice, - InvoiceReference, Payment, PaymentAccount, PaymentLineItem, Receipt, Refund, RefundsPartial, RoutingSlip, - StatementRecipients, StatementSettings) + CfsAccount, DistributionCode, DistributionCodeLink, EFTCredit, EFTFile, EFTShortnameLinks, EFTShortnames, + EFTTransaction, Invoice, InvoiceReference, Payment, PaymentAccount, PaymentLineItem, Receipt, Refund, + RefundsPartial, RoutingSlip, StatementRecipients, StatementSettings) from pay_api.utils.enums import ( - CfsAccountStatus, EFTProcessStatus, InvoiceReferenceStatus, InvoiceStatus, LineItemStatus, PaymentMethod, - PaymentStatus, PaymentSystem, RoutingSlipStatus) + CfsAccountStatus, EFTProcessStatus, EFTShortnameStatus, InvoiceReferenceStatus, InvoiceStatus, LineItemStatus, + PaymentMethod, PaymentStatus, PaymentSystem, RoutingSlipStatus) def factory_premium_payment_account(bcol_user_id='PB25020', bcol_account_id='1234567890', auth_account_id='1234'): @@ -225,15 +225,27 @@ def factory_create_eft_account(auth_account_id='1234', status=CfsAccountStatus.P return account -def factory_create_eft_shortname(auth_account_id: str, short_name: str): +def factory_create_eft_shortname(short_name: str): """Return Factory.""" short_name = EFTShortnames( - auth_account_id=auth_account_id, short_name=short_name ).save() return short_name +def factory_eft_shortname_link(short_name_id: int, auth_account_id: str = '1234', + updated_by: str = None, updated_on: datetime = datetime.now()): + """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, + updated_by=updated_by, + updated_by_name=updated_by, + updated_on=updated_on + ) + + 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.""" diff --git a/jobs/payment-jobs/tests/jobs/test_eft_transfer_task.py b/jobs/payment-jobs/tests/jobs/test_eft_transfer_task.py deleted file mode 100644 index 27aaf3256..000000000 --- a/jobs/payment-jobs/tests/jobs/test_eft_transfer_task.py +++ /dev/null @@ -1,162 +0,0 @@ -# Copyright © 2023 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 EFT CGI Transfer Job. - -Test-Suite to ensure that the EFT Transfer task is working as expected. -""" -from datetime import datetime -from typing import List - -from pay_api.models import DistributionCode, EFTGLTransfer, EjvFile, EjvHeader, EjvLink, FeeSchedule, Invoice, db -from pay_api.utils.enums import DisbursementStatus, EFTGlTransferType, EjvFileType, InvoiceStatus, PaymentMethod - -from tasks.eft_transfer_task import EftTransferTask - -from .factory import ( - factory_create_eft_account, factory_create_eft_shortname, factory_distribution, factory_invoice, - factory_payment_line_item) - - -def test_eft_transfer(app, session, monkeypatch): - """Test EFT Holdings GL Transfer for EFT invoices. - - Steps: - 1) Create GL codes to match GA batch type. - 2) Create account to short name mappings - 3) Create paid invoices for EFT. - 4) Run the job and assert results. - """ - monkeypatch.setattr('pysftp.Connection.put', lambda *args, **kwargs: None) - - corp_type = 'BEN' - filing_type = 'BCINC' - - # Find fee schedule which have service fees. - fee_schedule: FeeSchedule = FeeSchedule.find_by_filing_type_and_corp_type(corp_type, filing_type) - # Create a service fee distribution code - service_fee_dist_code = factory_distribution(name='service fee', client='112', reps_centre='99999', - service_line='99999', - stob='9999', project_code='9999999') - service_fee_dist_code.save() - - dist_code: DistributionCode = DistributionCode.find_by_active_for_fee_schedule(fee_schedule.fee_schedule_id) - # Update fee dist code to match the requirement. - dist_code.client = '112' - dist_code.responsibility_centre = '11111' - dist_code.service_line = '22222' - dist_code.stob = '3333' - dist_code.project_code = '4444444' - dist_code.service_fee_distribution_code_id = service_fee_dist_code.distribution_code_id - dist_code.save() - - app.config['EFT_HOLDING_GL'] = '1128888888888888888000000000000000' - eft_holding_gl = app.config['EFT_HOLDING_GL'] - distribution_gl = EftTransferTask.get_distribution_string(dist_code).strip() - service_fee_gl = EftTransferTask.get_distribution_string(service_fee_dist_code).strip() - - # GA - eft_account_1 = factory_create_eft_account(auth_account_id='1') - eft_shortname_1 = factory_create_eft_shortname(auth_account_id='1', short_name='SHORTNAME1') - eft_account_2 = factory_create_eft_account(auth_account_id='2') - eft_shortname_2 = factory_create_eft_shortname(auth_account_id='2', short_name='SHORTNAME2') - - eft_accounts = [eft_account_1, eft_account_2] - invoices: List[Invoice] = [] - for account in eft_accounts: - inv = factory_invoice(payment_account=account, corp_type_code=corp_type, total=101.5, - status_code=InvoiceStatus.PAID.value, payment_method_code=PaymentMethod.EFT.value) - factory_payment_line_item(invoice_id=inv.id, - fee_schedule_id=fee_schedule.fee_schedule_id, - filing_fees=100, - total=100, - service_fees=1.5, - fee_dist_id=dist_code.distribution_code_id) - invoices.append(inv) - - EftTransferTask.create_ejv_file() - - # Lookup invoice and assert disbursement status - for invoice in invoices: - ejv_inv_link: EjvLink = db.session.query(EjvLink) \ - .filter(EjvLink.link_id == invoice.id).first() - assert ejv_inv_link - - ejv_header = db.session.query(EjvHeader).filter(EjvHeader.id == ejv_inv_link.ejv_header_id).first() - assert ejv_header.disbursement_status_code == DisbursementStatus.UPLOADED.value - assert ejv_header - - ejv_file: EjvFile = EjvFile.find_by_id(ejv_header.ejv_file_id) - assert ejv_file - assert ejv_file.disbursement_status_code == DisbursementStatus.UPLOADED.value - assert ejv_file.file_type == EjvFileType.TRANSFER.value - - eft_transfers: List[EFTGLTransfer] = db.session.query(EFTGLTransfer).all() - - now = datetime.now().date() - - assert eft_transfers - assert len(eft_transfers) == 4 - - # Assert first short name line item distribution - assert eft_transfers[0].id is not None - assert eft_transfers[0].short_name_id == eft_shortname_1.id - assert eft_transfers[0].invoice_id == invoices[0].id - assert eft_transfers[0].transfer_amount == invoices[0].payment_line_items[0].total - assert eft_transfers[0].transfer_type == EFTGlTransferType.TRANSFER.value - assert eft_transfers[0].transfer_date.date() == now - assert eft_transfers[0].is_processed - assert eft_transfers[0].processed_on.date() == now - assert eft_transfers[0].created_on.date() == now - assert eft_transfers[0].source_gl == eft_holding_gl - assert eft_transfers[0].target_gl == distribution_gl - - # Assert first short name service fee distribution - assert eft_transfers[1].id is not None - assert eft_transfers[1].short_name_id == eft_shortname_1.id - assert eft_transfers[1].invoice_id == invoices[0].id - assert eft_transfers[1].transfer_type == EFTGlTransferType.TRANSFER.value - assert eft_transfers[1].transfer_amount == invoices[0].payment_line_items[0].service_fees - assert eft_transfers[1].transfer_date.date() == now - assert eft_transfers[1].is_processed - assert eft_transfers[1].processed_on.date() == now - assert eft_transfers[1].created_on.date() == now - assert eft_transfers[1].source_gl == eft_holding_gl - assert eft_transfers[1].target_gl == service_fee_gl - - # Assert second short name line item distribution - assert eft_transfers[2].id is not None - assert eft_transfers[2].short_name_id == eft_shortname_2.id - assert eft_transfers[2].invoice_id == invoices[1].id - assert eft_transfers[2].transfer_type == EFTGlTransferType.TRANSFER.value - assert eft_transfers[2].transfer_amount == invoices[1].payment_line_items[0].total - assert eft_transfers[2].transfer_date.date() == now - assert eft_transfers[2].is_processed - assert eft_transfers[2].processed_on.date() == now - assert eft_transfers[2].created_on.date() == now - assert eft_transfers[2].source_gl == eft_holding_gl - assert eft_transfers[2].target_gl == distribution_gl - - # Assert second short name service fee distribution - assert eft_transfers[3].id is not None - assert eft_transfers[3].short_name_id == eft_shortname_2.id - assert eft_transfers[3].invoice_id == invoices[1].id - assert eft_transfers[3].transfer_type == EFTGlTransferType.TRANSFER.value - assert eft_transfers[3].transfer_amount == invoices[1].payment_line_items[0].service_fees - assert eft_transfers[3].transfer_date.date() == now - assert eft_transfers[3].is_processed - assert eft_transfers[3].processed_on.date() == now - assert eft_transfers[3].created_on.date() == now - assert eft_transfers[3].source_gl == eft_holding_gl - assert eft_transfers[3].target_gl == service_fee_gl diff --git a/jobs/payment-jobs/tests/jobs/test_electronic_funds_transfer_task.py b/jobs/payment-jobs/tests/jobs/test_electronic_funds_transfer_task.py index de524fdfa..ee3d183d3 100644 --- a/jobs/payment-jobs/tests/jobs/test_electronic_funds_transfer_task.py +++ b/jobs/payment-jobs/tests/jobs/test_electronic_funds_transfer_task.py @@ -21,6 +21,7 @@ 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.services import CFSService @@ -29,7 +30,8 @@ from .factory import ( factory_create_eft_account, factory_create_eft_credit, factory_create_eft_file, factory_create_eft_shortname, - factory_create_eft_transaction, factory_invoice, factory_invoice_reference, factory_payment) + factory_create_eft_transaction, factory_eft_shortname_link, factory_invoice, factory_invoice_reference, + factory_payment) def test_link_electronic_funds_transfer(session): @@ -40,7 +42,12 @@ def test_link_electronic_funds_transfer(session): payment_account = factory_create_eft_account(auth_account_id=auth_account_id, status=CfsAccountStatus.ACTIVE.value) eft_file = factory_create_eft_file() factory_create_eft_transaction(file_id=eft_file.id) - eft_short_name = factory_create_eft_shortname(auth_account_id=auth_account_id, short_name=short_name) + 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' + ).save() invoice = factory_invoice(payment_account=payment_account, payment_method_code=PaymentMethod.EFT.value) factory_invoice_reference(invoice_id=invoice.id) factory_payment(payment_account_id=payment_account.id, payment_method_code=PaymentMethod.EFT.value, @@ -50,13 +57,14 @@ def test_link_electronic_funds_transfer(session): payment_account_id=payment_account.id) eft_short_name = EFTShortnameModel.find_by_short_name(short_name) - eft_short_name.linked_by = 'test' - eft_short_name.linked_by_name = 'test' - eft_short_name.linked_on = datetime.now() + eft_short_name_link = EFTShortnameLinksModel.find_by_short_name_id(eft_short_name.id)[0] + eft_short_name_link.updated_by = 'test' + eft_short_name_link.updated_by_name = 'test' + eft_short_name_link.updated_on = datetime.now() eft_short_name.save() payment_account: PaymentAccountModel = PaymentAccountModel.find_by_auth_account_id( - eft_short_name.auth_account_id) + eft_short_name_link.auth_account_id) cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id( payment_account.id) diff --git a/pay-queue/poetry.lock b/pay-queue/poetry.lock index c9e095cb8..d053b1e55 100644 --- a/pay-queue/poetry.lock +++ b/pay-queue/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "alembic" @@ -1649,9 +1649,9 @@ werkzeug = "3.0.1" [package.source] type = "git" -url = "https://github.com/Jxio/sbc-pay.git" -reference = "20457" -resolved_reference = "da7c2f0d798ad46e8bcd6f010ef1d0a7a66d8be0" +url = "https://github.com/bcgov/sbc-pay.git" +reference = "feature-queue-python-upgrade" +resolved_reference = "ba20cecf7e65065fa22dbfedb0f0bb5c1ee7ec94" subdirectory = "pay-api" [[package]] @@ -2285,13 +2285,13 @@ subdirectory = "python" [[package]] name = "scramp" -version = "1.4.4" +version = "1.4.5" description = "An implementation of the SCRAM protocol." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "scramp-1.4.4-py3-none-any.whl", hash = "sha256:b142312df7c2977241d951318b7ee923d6b7a4f75ba0f05b621ece1ed616faa3"}, - {file = "scramp-1.4.4.tar.gz", hash = "sha256:b7022a140040f33cf863ab2657917ed05287a807b917950489b89b9f685d59bc"}, + {file = "scramp-1.4.5-py3-none-any.whl", hash = "sha256:50e37c464fc67f37994e35bee4151e3d8f9320e9c204fca83a5d313c121bbbe7"}, + {file = "scramp-1.4.5.tar.gz", hash = "sha256:be3fbe774ca577a7a658117dca014e5d254d158cecae3dd60332dfe33ce6d78e"}, ] [package.dependencies] @@ -2358,18 +2358,18 @@ tornado = ["tornado (>=5)"] [[package]] name = "setuptools" -version = "69.2.0" +version = "69.5.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, - {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, + {file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"}, + {file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] @@ -2425,7 +2425,7 @@ develop = false type = "git" url = "https://github.com/bcgov/lear.git" reference = "feature-legal-name" -resolved_reference = "8ccb5e1bfdda45cca72ec65f38394ff064d256b1" +resolved_reference = "e7d04a5b6a6b7fd278a4a445f8b6303583f68aec" subdirectory = "python/common/sql-versioning" [[package]] @@ -2664,4 +2664,4 @@ watchdog = ["watchdog (>=2.3)"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "f09623e21148a2625eb0fda3d9ae9a8e645d7fa142f649894b625363a9e0b3d4" +content-hash = "f7e978de8d7c39e508ff106d26b6a9fedac7896203b705955ed5b3a5d481c775" diff --git a/pay-queue/pyproject.toml b/pay-queue/pyproject.toml index abc9acb73..299044c9d 100644 --- a/pay-queue/pyproject.toml +++ b/pay-queue/pyproject.toml @@ -22,7 +22,7 @@ protobuf = "4.25.3" launchdarkly-server-sdk = "^9.2.2" cachecontrol = "^0.14.0" sbc-common-components = {git = "https://github.com/bcgov/sbc-common-components.git", subdirectory = "python"} -pay-api = {git = "https://github.com/Jxio/sbc-pay.git", rev = "20457", subdirectory = "pay-api"} +pay-api = {git = "https://github.com/bcgov/sbc-pay.git", branch = "feature-queue-python-upgrade", subdirectory = "pay-api"} flask-jwt-oidc = {git = "https://github.com/thorwolpert/flask-jwt-oidc.git"} simple-cloudevent = {git = "https://github.com/daxiom/simple-cloudevent.py.git"} pg8000 = "^1.30.5" diff --git a/pay-queue/src/pay_queue/services/eft/eft_reconciliation.py b/pay-queue/src/pay_queue/services/eft/eft_reconciliation.py index d63e23e29..dd0774b1b 100644 --- a/pay-queue/src/pay_queue/services/eft/eft_reconciliation.py +++ b/pay-queue/src/pay_queue/services/eft/eft_reconciliation.py @@ -28,7 +28,7 @@ from pay_api.models import PaymentAccount as PaymentAccountModel from pay_api.services.eft_service import EftService as EFTService from pay_api.services.eft_short_names import EFTShortnames -from pay_api.utils.enums import EFTFileLineType, EFTProcessStatus, InvoiceStatus, PaymentMethod +from pay_api.utils.enums import EFTFileLineType, EFTProcessStatus, EFTShortnameStatus, InvoiceStatus, PaymentMethod from sentry_sdk import capture_message from pay_queue.minio import get_object @@ -103,6 +103,10 @@ def reconcile_eft_payments(msg: Dict[str, any]): # pylint: disable=too-many-loc has_eft_transaction_errors = False + # Include only transactions that are eft or has an error - ignore non EFT + eft_transactions = [transaction for transaction in eft_transactions + if transaction.has_errors() or transaction.is_eft] + # Parse EFT Transactions for eft_transaction in eft_transactions: if eft_transaction.has_errors(): # Flag any instance of an error - will indicate file is partially processed @@ -189,14 +193,6 @@ def _process_eft_credits(shortname_balance, eft_file_id): for shortname in shortname_balance.keys(): try: eft_shortname = _get_shortname(shortname) - payment_account_id = None - - # Get payment account if short name is mapped to an auth account - if eft_shortname.auth_account_id is not None: - payment_account: PaymentAccountModel = PaymentAccountModel.\ - find_by_auth_account_id(eft_shortname.auth_account_id) - payment_account_id = payment_account.id - eft_transactions = shortname_balance[shortname]['transactions'] for eft_transaction in eft_transactions: @@ -216,7 +212,6 @@ def _process_eft_credits(shortname_balance, eft_file_id): continue eft_credit_model.eft_file_id = eft_file_id - eft_credit_model.payment_account_id = payment_account_id eft_credit_model.short_name_id = eft_shortname.id eft_credit_model.amount = deposit_amount eft_credit_model.remaining_amount = deposit_amount @@ -236,6 +231,16 @@ def _process_eft_payments(shortname_balance: Dict, eft_file: EFTFileModel) -> bo for shortname in shortname_balance.keys(): # Retrieve or Create shortname mapping eft_shortname_model = _get_shortname(shortname) + shortname_links = EFTShortnames.get_shortname_links(eft_shortname_model.id) + + # Skip short names with no links or multiple links (manual processing by staff) + if not shortname_links['items'] or len(shortname_links['items']) > 1: + continue + + eft_short_link = shortname_links['items'][0] + # Skip if the link is in pending status - this will be officially linked via a scheduled job + if eft_short_link['status_code'] == EFTShortnameStatus.PENDING.value: + continue # No balance to apply - move to next shortname if shortname_balance[shortname]['balance'] <= 0: @@ -243,20 +248,18 @@ def _process_eft_payments(shortname_balance: Dict, eft_file: EFTFileModel) -> bo shortname, eft_file.file_ref) continue - # check if short name is mapped to an auth account - if eft_shortname_model is not None and eft_shortname_model.auth_account_id is not None: - # We have a mapping and can continue processing - try: - auth_account_id = eft_shortname_model.auth_account_id - # Find invoices to be paid - invoices: List[InvoiceModel] = EFTShortnames.get_invoices_owing(auth_account_id) - for invoice in invoices: - _pay_invoice(invoice=invoice, shortname_balance=shortname_balance[shortname]) - - except Exception as e: # NOQA pylint: disable=broad-exception-caught - has_eft_transaction_errors = True - current_app.logger.error(e) - capture_message('EFT Failed to apply balance to invoice.', level='error') + # We have a mapping and can continue processing + try: + auth_account_id = eft_short_link['account_id'] + # Find invoices to be paid + invoices: List[InvoiceModel] = EFTShortnames.get_invoices_owing(auth_account_id) + for invoice in invoices: + _pay_invoice(invoice=invoice, shortname_balance=shortname_balance[shortname]) + + except Exception as e: # NOQA pylint: disable=broad-exception-caught + has_eft_transaction_errors = True + current_app.logger.error(e) + capture_message('EFT Failed to apply balance to invoice.', level='error') return has_eft_transaction_errors diff --git a/pay-queue/src/pay_queue/services/eft/eft_record.py b/pay-queue/src/pay_queue/services/eft/eft_record.py index 4c002754e..89c483f80 100644 --- a/pay-queue/src/pay_queue/services/eft/eft_record.py +++ b/pay-queue/src/pay_queue/services/eft/eft_record.py @@ -24,6 +24,9 @@ class EFTRecord(EFTBase): """Defines the structure of the transaction record of a received EFT file.""" + PAD_DESCRIPTION_PATTERN = 'MISC PAYMENT BCONLINE' + EFT_DESCRIPTION_PATTERN = 'MISC PAYMENT' + ministry_code: str program_code: str location_id: str @@ -39,6 +42,7 @@ class EFTRecord(EFTBase): jv_type: str # I = inter, J = intra; mandatory if JV batch specified jv_number: str # mandatory if JV batch specified transaction_date: datetime # optional + is_eft: bool = False # Used to help with filtering transactions as not all transactions will necessary be EFT def __init__(self, content: str, index: int): """Return an EFT Transaction record.""" @@ -66,7 +70,11 @@ def _process(self): self.ministry_code = self.extract_value(1, 3) self.program_code = self.extract_value(3, 7) - self.deposit_datetime = self.parse_datetime(self.extract_value(7, 15) + self.extract_value(20, 24), + + deposit_time = self.extract_value(20, 24) + deposit_time = '0000' if len(deposit_time) == 0 else deposit_time # default to 0000 if time not provided + + self.deposit_datetime = self.parse_datetime(self.extract_value(7, 15) + deposit_time, EFTError.INVALID_DEPOSIT_DATETIME) self.location_id = self.extract_value(15, 20) self.transaction_sequence = self.extract_value(24, 27) @@ -75,6 +83,7 @@ def _process(self): self.transaction_description = self.extract_value(27, 67) if len(self.transaction_description) == 0: self.add_error(EFTParseError(EFTError.ACCOUNT_SHORTNAME_REQUIRED)) + self.parse_transaction_description() self.deposit_amount = self.parse_decimal(self.extract_value(67, 80), EFTError.INVALID_DEPOSIT_AMOUNT) self.currency = self.get_currency(self.extract_value(80, 82)) @@ -89,3 +98,14 @@ def _process(self): transaction_date = self.extract_value(131, 139) self.transaction_date = None if len(transaction_date) == 0 \ else self.parse_date(transaction_date, EFTError.INVALID_TRANSACTION_DATE) + + def parse_transaction_description(self): + """Determine if the transaction is an EFT and parse it.""" + if not self.transaction_description: + return + + # Check if this a PAD or EFT Transaction - ignore non EFT Transactions + if self.transaction_description.startswith(self.EFT_DESCRIPTION_PATTERN) \ + and not self.transaction_description.startswith(self.PAD_DESCRIPTION_PATTERN): + self.is_eft = True + self.transaction_description = self.transaction_description[len(self.EFT_DESCRIPTION_PATTERN):].strip() diff --git a/pay-queue/src/pay_queue/version.py b/pay-queue/src/pay_queue/version.py index 19ea931f0..3b723bf04 100644 --- a/pay-queue/src/pay_queue/version.py +++ b/pay-queue/src/pay_queue/version.py @@ -22,4 +22,4 @@ Development release segment: .devN """ -__version__ = '2.0.0' # pylint: disable=invalid-name +__version__ = '2.0.1' # pylint: disable=invalid-name diff --git a/pay-queue/tests/integration/test_eft_reconciliation.py b/pay-queue/tests/integration/test_eft_reconciliation.py index 5c7e9d081..114ae89e2 100644 --- a/pay-queue/tests/integration/test_eft_reconciliation.py +++ b/pay-queue/tests/integration/test_eft_reconciliation.py @@ -23,11 +23,12 @@ from pay_api.models import EFTCredit as EFTCreditModel from pay_api.models import EFTCreditInvoiceLink as EFTCreditInvoiceLinkModel from pay_api.models import EFTFile as EFTFileModel +from pay_api.models import EFTShortnameLinks as EFTShortnameLinksModel from pay_api.models import EFTShortnames as EFTShortnameModel from pay_api.models import EFTTransaction as EFTTransactionModel from pay_api.models import Invoice as InvoiceModel from pay_api.models import PaymentAccount as PaymentAccountModel -from pay_api.utils.enums import EFTFileLineType, EFTProcessStatus, MessageType, PaymentMethod +from pay_api.utils.enums import EFTFileLineType, EFTProcessStatus, EFTShortnameStatus, MessageType, PaymentMethod from pay_queue.services.eft.eft_enums import EFTConstants from tests.integration.factory import factory_create_eft_account, factory_invoice @@ -250,9 +251,13 @@ def test_eft_tdi17_basic_process(client): assert eft_shortnames is not None assert len(eft_shortnames) == 2 - assert eft_shortnames[0].auth_account_id is None + + short_name_link_1 = EFTShortnameLinksModel.find_by_short_name_id(eft_shortnames[0].id) + short_name_link_2 = EFTShortnameLinksModel.find_by_short_name_id(eft_shortnames[1].id) + + assert not short_name_link_1 assert eft_shortnames[0].short_name == 'ABC123' - assert eft_shortnames[1].auth_account_id is None + assert not short_name_link_2 assert eft_shortnames[1].short_name == 'DEF456' eft_credits: List[EFTCreditModel] = db.session.query(EFTCreditModel).order_by(EFTCreditModel.created_on.asc()).all() @@ -325,12 +330,15 @@ def test_eft_tdi17_process(client): assert eft_transactions[2].short_name_id is not None eft_shortnames = db.session.query(EFTShortnameModel).all() + short_name_link_1: EFTShortnameLinksModel = EFTShortnameLinksModel.find_by_short_name_id(eft_shortnames[0].id)[0] + short_name_link_2: EFTShortnameLinksModel = EFTShortnameLinksModel.find_by_short_name_id(eft_shortnames[1].id) assert eft_shortnames is not None assert len(eft_shortnames) == 2 - assert eft_shortnames[0].auth_account_id == eft_shortname.auth_account_id + assert short_name_link_1 + assert short_name_link_1.auth_account_id == payment_account.auth_account_id assert eft_shortnames[0].short_name == 'TESTSHORTNAME' - assert eft_shortnames[1].auth_account_id is None + assert not short_name_link_2 assert eft_shortnames[1].short_name == 'ABC123' # NOTE THIS NEEDS TO BE RE-WRITTEN INSIDE OF THE JOB. @@ -416,7 +424,7 @@ def test_eft_tdi17_rerun(client): transaction_1 = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, ministry_code='AT', program_code='0146', deposit_date='20230810', deposit_time='0000', location_id='85004', transaction_sequence='001', - transaction_description='TESTSHORTNAME', deposit_amount='13500', + transaction_description='MISC PAYMENT TESTSHORTNAME', deposit_amount='13500', currency='', exchange_adj_amount='0', deposit_amount_cad='FAIL', destination_bank_number='0003', batch_number='002400986', jv_type='I', jv_number='002425669', transaction_date='') @@ -456,7 +464,7 @@ def test_eft_tdi17_rerun(client): transaction_1 = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, ministry_code='AT', program_code='0146', deposit_date='20230810', deposit_time='0000', location_id='85004', transaction_sequence='001', - transaction_description='TESTSHORTNAME', deposit_amount='13500', + transaction_description='MISC PAYMENT TESTSHORTNAME', deposit_amount='13500', currency='', exchange_adj_amount='0', deposit_amount_cad='13500', destination_bank_number='0003', batch_number='002400986', jv_type='I', jv_number='002425669', transaction_date='') @@ -545,8 +553,15 @@ def test_eft_tdi17_rerun(client): def create_test_data(): """Create test seed data.""" payment_account: PaymentAccountModel = factory_create_eft_account() - eft_shortname: EFTShortnameModel = EFTShortnameModel(short_name='TESTSHORTNAME', - auth_account_id=payment_account.auth_account_id).save() + eft_shortname: EFTShortnameModel = EFTShortnameModel(short_name='TESTSHORTNAME').save() + EFTShortnameLinksModel( + eft_short_name_id=eft_shortname.id, + auth_account_id=payment_account.auth_account_id, + status_code=EFTShortnameStatus.LINKED.value, + updated_by='IDIR/JSMITH', + updated_by_name='IDIR/JSMITH', + updated_on=datetime.now() + ).save() invoice: InvoiceModel = factory_invoice(payment_account=payment_account, total=100, service_fees=10.0, payment_method_code=PaymentMethod.EFT.value) @@ -565,21 +580,41 @@ def generate_basic_tdi17_file(file_name: str): transaction_1 = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, ministry_code='AT', program_code='0146', deposit_date='20230810', deposit_time='0000', location_id='85004', transaction_sequence='001', - transaction_description='ABC123', deposit_amount='13500', + transaction_description='MISC PAYMENT ABC123', deposit_amount='13500', currency='', exchange_adj_amount='0', deposit_amount_cad='13500', destination_bank_number='0003', batch_number='002400986', jv_type='I', jv_number='002425669', transaction_date='') transaction_2 = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, ministry_code='AT', - program_code='0146', deposit_date='20230810', deposit_time='0000', + program_code='0146', deposit_date='20230810', deposit_time='', location_id='85004', transaction_sequence='002', - transaction_description='DEF456', + transaction_description='MISC PAYMENT DEF456', deposit_amount='525000', currency='', exchange_adj_amount='0', deposit_amount_cad='525000', destination_bank_number='0003', batch_number='002400986', jv_type='I', jv_number='002425669', transaction_date='') - create_and_upload_eft_file(file_name, [header, transaction_1, transaction_2, trailer]) + transaction_3 = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, ministry_code='AT', + program_code='0146', deposit_date='20230810', deposit_time='0000', + location_id='85004', transaction_sequence='003', + transaction_description='SHOULDIGNORE', + deposit_amount='525000', currency='', exchange_adj_amount='0', + deposit_amount_cad='525000', destination_bank_number='0003', + batch_number='002400986', jv_type='I', jv_number='002425669', + transaction_date='') + + transaction_4 = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, ministry_code='AT', + program_code='0146', deposit_date='20230810', deposit_time='0000', + location_id='85004', transaction_sequence='004', + transaction_description='MISC PAYMENT BCONLINE SHOULDIGNORE', + deposit_amount='525000', currency='', exchange_adj_amount='0', + deposit_amount_cad='525000', destination_bank_number='0003', + batch_number='002400986', jv_type='I', jv_number='002425669', + transaction_date='') + + create_and_upload_eft_file(file_name, [header, + transaction_1, transaction_2, transaction_3, transaction_4, + trailer]) def generate_tdi17_file(file_name: str): @@ -593,15 +628,15 @@ def generate_tdi17_file(file_name: str): transaction_1 = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, ministry_code='AT', program_code='0146', deposit_date='20230810', deposit_time='0000', location_id='85004', transaction_sequence='001', - transaction_description='TESTSHORTNAME', deposit_amount='10000', + transaction_description='MISC PAYMENT TESTSHORTNAME', deposit_amount='10000', currency='', exchange_adj_amount='0', deposit_amount_cad='10000', destination_bank_number='0003', batch_number='002400986', jv_type='I', jv_number='002425669', transaction_date='') transaction_2 = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, ministry_code='AT', - program_code='0146', deposit_date='20230810', deposit_time='0000', + program_code='0146', deposit_date='20230810', deposit_time='', location_id='85004', transaction_sequence='002', - transaction_description='TESTSHORTNAME', + transaction_description='MISC PAYMENT TESTSHORTNAME', deposit_amount='5050', currency='', exchange_adj_amount='0', deposit_amount_cad='5050', destination_bank_number='0003', batch_number='002400986', jv_type='I', jv_number='002425669', @@ -610,9 +645,20 @@ def generate_tdi17_file(file_name: str): transaction_3 = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, ministry_code='AT', program_code='0146', deposit_date='20230810', deposit_time='0000', location_id='85004', transaction_sequence='003', - transaction_description='ABC123', deposit_amount='35150', + transaction_description='MISC PAYMENT ABC123', deposit_amount='35150', currency='', exchange_adj_amount='0', deposit_amount_cad='35150', destination_bank_number='0003', batch_number='002400986', jv_type='I', jv_number='002425669', transaction_date='') - create_and_upload_eft_file(file_name, [header, transaction_1, transaction_2, transaction_3, trailer]) + transaction_4 = factory_eft_record(record_type=EFTConstants.TRANSACTION_RECORD_TYPE.value, ministry_code='AT', + program_code='0146', deposit_date='20230810', deposit_time='0000', + location_id='85004', transaction_sequence='004', + transaction_description='MISC PAYMENT BCONLINE SHOULDIGNORE', + deposit_amount='525000', currency='', exchange_adj_amount='0', + deposit_amount_cad='525000', destination_bank_number='0003', + batch_number='002400986', jv_type='I', jv_number='002425669', + transaction_date='') + + create_and_upload_eft_file(file_name, [header, + transaction_1, transaction_2, transaction_3, transaction_4, + trailer]) diff --git a/pay-queue/tests/unit/test_eft_file_parser.py b/pay-queue/tests/unit/test_eft_file_parser.py index fd667674f..73d27124e 100644 --- a/pay-queue/tests/unit/test_eft_file_parser.py +++ b/pay-queue/tests/unit/test_eft_file_parser.py @@ -555,6 +555,7 @@ def test_eft_parse_file(): assert eft_records[0].jv_type == 'I' assert eft_records[0].jv_number == '002425669' assert eft_records[0].transaction_date is None + assert not eft_records[0].is_eft assert eft_records[1].index == 2 assert eft_records[1].record_type == '2' @@ -573,6 +574,7 @@ def test_eft_parse_file(): assert eft_records[1].jv_type == 'I' assert eft_records[1].jv_number == '002425669' assert eft_records[1].transaction_date is None + assert not eft_records[1].is_eft assert eft_records[2].index == 3 assert eft_records[2].record_type == '2' @@ -591,6 +593,7 @@ def test_eft_parse_file(): assert eft_records[2].jv_type == 'I' assert eft_records[2].jv_number == '002425669' assert eft_records[2].transaction_date is None + assert not eft_records[2].is_eft assert eft_records[3].index == 4 assert eft_records[3].record_type == '2' @@ -609,6 +612,7 @@ def test_eft_parse_file(): assert eft_records[3].jv_type == 'I' assert eft_records[3].jv_number == '002425669' assert eft_records[3].transaction_date is None + assert not eft_records[3].is_eft assert eft_records[4].index == 5 assert eft_records[4].record_type == '2' @@ -627,3 +631,4 @@ def test_eft_parse_file(): assert eft_records[4].jv_type == 'I' assert eft_records[4].jv_number == '002425836' assert eft_records[4].transaction_date is None + assert not eft_records[4].is_eft diff --git a/pay-queue/tests/utilities/factory_utils.py b/pay-queue/tests/utilities/factory_utils.py index d8047880a..f493b3b88 100644 --- a/pay-queue/tests/utilities/factory_utils.py +++ b/pay-queue/tests/utilities/factory_utils.py @@ -47,7 +47,8 @@ def factory_eft_record(record_type: str, ministry_code: str, program_code: str, exchange_adj_amount = transform_money_string(exchange_adj_amount) deposit_amount_cad = transform_money_string(deposit_amount_cad) - result = f'{record_type}{ministry_code}{program_code}{deposit_date}{location_id}{deposit_time}' \ + result = f'{record_type}{ministry_code}{program_code}{deposit_date}{location_id}' \ + f'{right_pad_space(deposit_time, 4)}' \ f'{transaction_sequence}{right_pad_space(transaction_description, 40)}' \ f'{left_pad_zero(deposit_amount, 13)}{right_pad_space(currency, 2)}' \ f'{left_pad_zero(exchange_adj_amount, 13)}{left_pad_zero(deposit_amount_cad, 13)}' \