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

22263 - EFT Refund AP Job Implementation #1719

Merged
merged 46 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
84f2802
EFT Refunds
rodrigo-barraza Aug 29, 2024
46181fc
Merge branch 'main' into feature/22263
rodrigo-barraza Aug 29, 2024
92a24bc
More AP task implementation and cleanup
rodrigo-barraza Sep 3, 2024
a7141ab
Lint fixes
rodrigo-barraza Sep 3, 2024
76ebedd
Merge branch 'main' into feature/22263
rodrigo-barraza Sep 3, 2024
aa6bd0a
Readding factory_create_eft_account
rodrigo-barraza Sep 4, 2024
ba89348
Fixes
rodrigo-barraza Sep 4, 2024
d1c553d
removing comments
rodrigo-barraza Sep 4, 2024
dc1e4c6
Comment cleanup
rodrigo-barraza Sep 4, 2024
d8161ed
Lint fix
rodrigo-barraza Sep 4, 2024
a5aa6f5
Feedback updates
rodrigo-barraza Sep 4, 2024
5e5d779
Merge branch 'main' into feature/22263
rodrigo-barraza Sep 4, 2024
014b222
Adding disbursement_status_codes to eft_refund
rodrigo-barraza Sep 4, 2024
4166f4a
Description
rodrigo-barraza Sep 4, 2024
a695bd4
Merge branch 'main' into feature/22263
rodrigo-barraza Sep 4, 2024
aa74b54
Update pyproject
rodrigo-barraza Sep 4, 2024
7740245
Updating poetry.lock
rodrigo-barraza Sep 4, 2024
3d9fe37
Revert version change
rodrigo-barraza Sep 4, 2024
8ae97f3
Revert lock
rodrigo-barraza Sep 4, 2024
57d24e1
Merge branch 'main' into feature/22263
rodrigo-barraza Sep 4, 2024
d788dc6
Pyproject and poetry.lock
rodrigo-barraza Sep 4, 2024
9013100
Lint fix
rodrigo-barraza Sep 4, 2024
8a023ea
New CP job variables
rodrigo-barraza Sep 5, 2024
1059f36
Merge branch 'main' into feature/22263
rodrigo-barraza Sep 9, 2024
afa57fc
Tweaks
rodrigo-barraza Sep 13, 2024
e9356e8
Merge branch 'main' into feature/22263
rodrigo-barraza Sep 18, 2024
2fe4b5a
Passing CAS Supplier number
rodrigo-barraza Sep 24, 2024
2a33c54
Merge branch 'main' into feature/22263
rodrigo-barraza Sep 24, 2024
4008568
Adding comment to EFT refund AP job
rodrigo-barraza Sep 24, 2024
c3b0a09
Updating migration
rodrigo-barraza Sep 24, 2024
02d60ba
EFTShortnameRefundStatus
rodrigo-barraza Sep 24, 2024
bf936a5
Fixes
rodrigo-barraza Sep 24, 2024
da44192
REFUND_REQUESTED to APPROVED
rodrigo-barraza Sep 27, 2024
174cd16
Merge branch 'main' into feature/22263
rodrigo-barraza Sep 27, 2024
6953947
Merge branch 'main' into feature/22263
rodrigo-barraza Sep 27, 2024
361ebdf
Cleanup
rodrigo-barraza Oct 1, 2024
bf4e431
Lint fix
rodrigo-barraza Oct 1, 2024
27543c6
Fixing deps
rodrigo-barraza Oct 1, 2024
0b77a32
Deps fix
rodrigo-barraza Oct 1, 2024
c70746c
Revision fix
rodrigo-barraza Oct 1, 2024
6ae8c67
isort fix
rodrigo-barraza Oct 1, 2024
4d9943f
isort fix
rodrigo-barraza Oct 1, 2024
fa51d23
Revision fix
rodrigo-barraza Oct 1, 2024
86f2388
Revision fix
rodrigo-barraza Oct 1, 2024
ddaa201
Deps fix
rodrigo-barraza Oct 1, 2024
890bf03
small test fix
rodrigo-barraza Oct 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 24 additions & 23 deletions jobs/payment-jobs/poetry.lock

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

3 changes: 1 addition & 2 deletions jobs/payment-jobs/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ readme = "README.md"

[tool.poetry.dependencies]
python = "^3.12"
pay-api = {git = "https://github.com/ochiu/sbc-pay.git", branch = "22391-21560", subdirectory = "pay-api"}
gunicorn = "^21.2.0"
pay-api = {git = "https://github.com/bcgov/sbc-pay.git", branch = "main", subdirectory = "pay-api"}
flask = "^3.0.2"
flask-sqlalchemy = "^3.1.1"
sqlalchemy = "^2.0.28"
Expand Down
55 changes: 54 additions & 1 deletion jobs/payment-jobs/tasks/ap_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,18 @@
from more_itertools import batched
from pay_api.models import CorpType as CorpTypeModel
from pay_api.models import DistributionCode as DistributionCodeModel
from pay_api.models import EFTCredit as EFTCreditModel
from pay_api.models import EFTCreditInvoiceLink as EFTCreditInvoiceLinkModel
from pay_api.models import EFTShortnameLinks as EFTShortnameLinksModel
from pay_api.models import EFTRefund as EFTRefundModel
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 Refund as RefundModel
from pay_api.models import RoutingSlip as RoutingSlipModel
from pay_api.models import db
from pay_api.utils.enums import DisbursementStatus, EjvFileType, EJVLinkType, RoutingSlipStatus
from pay_api.utils.enums import DisbursementStatus, EjvFileType, EJVLinkType, InvoiceStatus, RoutingSlipStatus
from tasks.common.cgi_ap import CgiAP
from tasks.common.dataclasses import APLine
from tasks.ejv_partner_distribution_task import EjvPartnerDistributionTask
Expand Down Expand Up @@ -59,9 +63,58 @@ def create_ap_files(cls):
6. Create AP file and upload to SFTP.
7. After some time, a feedback file will arrive and Payment-reconciliation queue will set disbursement
status to COMPLETED or REVERSED (filter out)
8. Find all EFT refunds with status REFUND_REQUESTED.
9. Create AP file and upload to SFTP.
10. After some time, a feedback file will arrive and Payment-reconciliation queue will move these
EFT refunds to REFUNDED. (filter out)
"""
cls._create_routing_slip_refund_file()
cls._create_non_gov_disbursement_file()
cls._create_eft_refund_file()

@classmethod
def _create_eft_refund_file(cls):
"""Create AP file for EFT refunds and upload to CGI."""
cls.ap_type = EjvFileType.EFT_REFUND
eft_refunds_dao: List[tuple[InvoiceModel, EFTRefundModel]] = db.session.query(InvoiceModel, EFTRefundModel) \
.join(EFTCreditInvoiceLinkModel, EFTCreditInvoiceLinkModel.invoice_id == InvoiceModel.id) \
.join(EFTCreditModel, EFTCreditModel.id == EFTCreditInvoiceLinkModel.eft_credit_id) \
.join(EFTShortnameLinksModel, EFTShortnameLinksModel.eft_short_name_id == EFTCreditModel.short_name_id) \
.join(EFTRefundModel, EFTRefundModel.short_name_id == EFTShortnameLinksModel.eft_short_name_id) \
.filter(EFTRefundModel.status == InvoiceStatus.REFUND_REQUESTED.value) \
Copy link
Collaborator

Choose a reason for hiding this comment

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

Need to filter out eft_refund.disbursement_status_code = UPLOADED ?

Copy link
Collaborator

Choose a reason for hiding this comment

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

EFTRefundModels.status probably shouldn't be compared to the InvoiceStatus.

It also looks like when we create the eft_refund in pay api it uses EFTCreditInvoiceStatus. EFT refunds has its own flow, I would suggest it has its own set of statuses.

Something similar to:

PENDING_APPROVAL
REJECTED
APPROVED

.filter(EFTRefundModel.refund_amount > 0) \
.all()

current_app.logger.info(f'Found {len(eft_refunds_dao)} to refund.')

for refunds in list(batched(eft_refunds_dao, 250)):
ejv_file_model = EjvFileModel(
file_type=cls.ap_type.value,
file_ref=cls.get_file_name(),
disbursement_status_code=DisbursementStatus.UPLOADED.value
).flush()

batch_number: str = cls.get_batch_number(ejv_file_model.id)
Copy link
Collaborator

Choose a reason for hiding this comment

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

change hint for get_batch_number to return -> str

ap_content: str = cls.get_batch_header(batch_number)
batch_total = 0
line_count_total: int = 0
seeker25 marked this conversation as resolved.
Show resolved Hide resolved
for invoice_refund in refunds:
invoice, eft_refund = invoice_refund
current_app.logger.info(f'Creating refund for {invoice.id}, Amount {eft_refund.refund_amount}.')
ap_content = f'{ap_content}{cls.get_ap_header(
eft_refund.refund_amount, invoice.id, invoice.created_on)}'
ap_line = APLine(
total=eft_refund.refund_amount,
invoice_number=invoice.id,
line_number=line_count_total + 1)
ap_content = f'{ap_content}{cls.get_ap_invoice_line(ap_line)}'
line_count_total += 2
batch_total += eft_refund.refund_amount
eft_refund.disbursement_status_code = DisbursementStatus.UPLOADED.value

batch_trailer: str = cls.get_batch_trailer(batch_number, batch_total, control_total=line_count_total)
ap_content = f'{ap_content}{batch_trailer}'
cls._create_file_and_upload(ap_content)

@classmethod
def _create_routing_slip_refund_file(cls): # pylint:disable=too-many-locals, too-many-statements
Expand Down
80 changes: 45 additions & 35 deletions jobs/payment-jobs/tasks/common/cgi_ap.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from datetime import datetime, timezone

from flask import current_app
from pay_api.utils.enums import EjvFileType
from pay_api.utils.enums import DisbursementMethod, EjvFileType
from pay_api.utils.util import get_fiscal_year
from tasks.common.dataclasses import APLine

Expand Down Expand Up @@ -51,7 +51,7 @@ def get_ap_header(cls, total, invoice_number, invoice_date):
effective_date = cls._get_date(datetime.now(tz=timezone.utc))
invoice_date = cls._get_date(invoice_date)
oracle_invoice_batch_name = cls._get_oracle_invoice_batch_name(invoice_number)
disbursement_method = 'CHQ' if cls.ap_type == EjvFileType.REFUND else 'EFT'
disbursement_method = DisbursementMethod.CHEQUE if cls.ap_type == EjvFileType.REFUND else DisbursementMethod.EFT
term = f'{cls.EMPTY:<50}' if cls.ap_type == EjvFileType.REFUND else f'Immediate{cls.EMPTY:<41}'
ap_header = f'{cls._feeder_number()}APIH{cls.DELIMITER}{cls._supplier_number()}{cls._supplier_location()}' \
f'{invoice_number:<50}{cls._po_number()}{invoice_type}{invoice_date}GEN {disbursement_method} N' \
Expand Down Expand Up @@ -122,29 +122,35 @@ def get_ap_comment(cls, refund_details, routing_slip_number):
@classmethod
def _supplier_number(cls):
"""Return vendor number."""
if cls.ap_type == EjvFileType.NON_GOV_DISBURSEMENT:
return f"{current_app.config.get('BCA_SUPPLIER_NUMBER'):<9}"
if cls.ap_type == EjvFileType.REFUND:
return f"{current_app.config.get('CGI_AP_SUPPLIER_NUMBER'):<9}"
raise RuntimeError('ap_type not selected.')
match cls.ap_type:
case EjvFileType.NON_GOV_DISBURSEMENT:
return f"{current_app.config.get('BCA_SUPPLIER_NUMBER'):<9}"
case EjvFileType.REFUND | EjvFileType.EFT_REFUND:
return f"{current_app.config.get('CGI_AP_SUPPLIER_NUMBER'):<9}"
seeker25 marked this conversation as resolved.
Show resolved Hide resolved
case _:
raise RuntimeError('ap_type not selected.')

@classmethod
def _dist_vendor(cls):
"""Return distribution vendor number."""
if cls.ap_type == EjvFileType.NON_GOV_DISBURSEMENT:
return f"{current_app.config.get('BCA_SUPPLIER_NUMBER'):<30}"
if cls.ap_type == EjvFileType.REFUND:
return f"{current_app.config.get('CGI_AP_SUPPLIER_NUMBER'):<30}"
raise RuntimeError('ap_type not selected.')
match cls.ap_type:
case EjvFileType.NON_GOV_DISBURSEMENT:
return f"{current_app.config.get('BCA_SUPPLIER_NUMBER'):<30}"
case EjvFileType.REFUND | EjvFileType.EFT_REFUND:
return f"{current_app.config.get('CGI_AP_SUPPLIER_NUMBER'):<30}"
seeker25 marked this conversation as resolved.
Show resolved Hide resolved
case _:
raise RuntimeError('ap_type not selected.')

@classmethod
def _supplier_location(cls):
"""Return location."""
if cls.ap_type == EjvFileType.NON_GOV_DISBURSEMENT:
return f"{current_app.config.get('BCA_SUPPLIER_LOCATION'):<3}"
if cls.ap_type == EjvFileType.REFUND:
return f"{current_app.config.get('CGI_AP_SUPPLIER_LOCATION'):<3}"
raise RuntimeError('ap_type not selected.')
match cls.ap_type:
case EjvFileType.NON_GOV_DISBURSEMENT:
return f"{current_app.config.get('BCA_SUPPLIER_LOCATION'):<3}"
case EjvFileType.REFUND | EjvFileType.EFT_REFUND:
return f"{current_app.config.get('CGI_AP_SUPPLIER_LOCATION'):<3}"
seeker25 marked this conversation as resolved.
Show resolved Hide resolved
case _:
raise RuntimeError('ap_type not selected.')

@classmethod
def _po_number(cls):
Expand All @@ -159,28 +165,32 @@ def _get_date(cls, date):
@classmethod
def _distribution(cls, distribution_code: str = None):
"""Return Distribution Code string."""
if cls.ap_type == EjvFileType.NON_GOV_DISBURSEMENT:
return f'{distribution_code}0000000000{cls.EMPTY:<16}'
if cls.ap_type == EjvFileType.REFUND:
return f"{current_app.config.get('CGI_AP_DISTRIBUTION')}{cls.EMPTY:<16}"
raise RuntimeError('ap_type not selected.')
match cls.ap_type:
case EjvFileType.NON_GOV_DISBURSEMENT:
return f'{distribution_code}0000000000{cls.EMPTY:<16}'
case EjvFileType.REFUND | EjvFileType.EFT_REFUND:
return f"{current_app.config.get('CGI_AP_DISTRIBUTION')}{cls.EMPTY:<16}"
seeker25 marked this conversation as resolved.
Show resolved Hide resolved
case _:
raise RuntimeError('ap_type not selected.')

@classmethod
def _get_oracle_invoice_batch_name(cls, invoice_number):
"""Return Oracle Invoice Batch Name."""
if cls.ap_type == EjvFileType.NON_GOV_DISBURSEMENT:
return f'{invoice_number}'[:30]
if cls.ap_type == EjvFileType.REFUND:
return f'REFUND_FAS_RS_{invoice_number}'[:30]
raise RuntimeError('ap_type not selected.')
match cls.ap_type:
case EjvFileType.NON_GOV_DISBURSEMENT:
return f'{invoice_number}'[:30]
case EjvFileType.REFUND | EjvFileType.EFT_REFUND:
return f'REFUND_FAS_RS_{invoice_number}'[:30]
seeker25 marked this conversation as resolved.
Show resolved Hide resolved
case _:
raise RuntimeError('ap_type not selected.')

@classmethod
def _get_line_code(cls, ap_line: APLine):
# Routing slip refunds always DEBIT the internal GL and mails out cheques.
if cls.ap_type == EjvFileType.REFUND:
return 'D'
if cls.ap_type == EjvFileType.NON_GOV_DISBURSEMENT:
if ap_line.is_reversal:
return 'C'
return 'D'
raise RuntimeError('ap_type not selected.')
"""Get line code."""
match cls.ap_type:
case EjvFileType.REFUND | EjvFileType.EFT_REFUND:
return 'D'
case EjvFileType.NON_GOV_DISBURSEMENT:
return 'C' if ap_line.is_reversal else 'D'
case _:
raise RuntimeError('ap_type not selected.')
28 changes: 24 additions & 4 deletions jobs/payment-jobs/tests/jobs/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
from random import randrange

from pay_api.models import (
CfsAccount, DistributionCode, DistributionCodeLink, EFTCredit, EFTCreditInvoiceLink, EFTFile, EFTShortnameLinks,
EFTShortnames, EFTShortnamesHistorical, EFTTransaction, Invoice, InvoiceReference, Payment, PaymentAccount,
PaymentLineItem, Receipt, Refund, RefundsPartial, RoutingSlip, Statement, StatementInvoices, StatementRecipients,
StatementSettings)
CfsAccount, DistributionCode, DistributionCodeLink, EFTCredit, EFTCreditInvoiceLink, EFTFile, EFTRefund,
EFTShortnameLinks, EFTShortnames, EFTShortnamesHistorical, EFTTransaction, Invoice, InvoiceReference, Payment,
PaymentAccount, PaymentLineItem, Receipt, Refund, RefundsPartial, RoutingSlip, Statement, StatementInvoices,
StatementRecipients, StatementSettings)
from pay_api.utils.enums import (
CfsAccountStatus, EFTHistoricalTypes, EFTProcessStatus, EFTShortnameStatus, InvoiceReferenceStatus, InvoiceStatus,
LineItemStatus, PaymentMethod, PaymentStatus, PaymentSystem, RoutingSlipStatus)
Expand Down Expand Up @@ -346,6 +346,26 @@ def factory_create_eft_shortname_historical(payment_account_id=1, related_group_
return eft_historical


def factory_create_eft_refund(
cas_supplier_number: str = '1234',
comment: str = 'Test Comment',
refund_amount: float = 100.0,
refund_email: str = '',
short_name_id: int = 1,
status: str = InvoiceStatus.REFUND_REQUESTED.value
):
"""Return Factory."""
eft_refund = EFTRefund(
cas_supplier_number=cas_supplier_number,
comment=comment,
refund_amount=refund_amount,
refund_email=refund_email,
short_name_id=short_name_id,
status=status
).save()
return eft_refund


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
Loading