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

22063 - Allow multiple payment methods, also create multiple CFS accounts to support EFT/PAD switching #1597

Merged
merged 47 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
ff4ccdc
Fix update_account, so it doesn't set cfs_account to inactive
seeker25 Jul 2, 2024
804f81c
Various warning fixes, small unit test fix
seeker25 Jul 2, 2024
7145f1f
minor
seeker25 Jul 2, 2024
0f2bda8
Changes for create_invoice job
seeker25 Jul 2, 2024
f46be8b
Change logging from debug -> info
seeker25 Jul 2, 2024
b8ea822
Merge branch 'main' of https://github.com/bcgov/sbc-pay into 22063-part2
seeker25 Jul 5, 2024
f692d77
Changes for CFS_ACCOUNT.payment_method
seeker25 Jul 5, 2024
1f6bb72
Small changes for payment
seeker25 Jul 5, 2024
23094a2
More changes
seeker25 Jul 8, 2024
0a9c116
More changes
seeker25 Jul 8, 2024
d9d2478
lint fixes
seeker25 Jul 8, 2024
1f7b9ef
More spots to add in payment_method for CC plus migration
seeker25 Jul 8, 2024
09a1377
Change routing slip job to use internal for payment method not CASH o…
seeker25 Jul 8, 2024
5588809
Update refs, more lint fixes
seeker25 Jul 8, 2024
132b299
Use created instead of approved for EFT invoice creation
seeker25 Jul 8, 2024
7e960a2
Minor changes and tweaks
seeker25 Jul 8, 2024
cb9791a
small unit test fixes
seeker25 Jul 9, 2024
40a07be
Fix linting
seeker25 Jul 9, 2024
e8d7c56
Update pay-queue
seeker25 Jul 9, 2024
7552a46
more lint
seeker25 Jul 9, 2024
3bc07d6
point at new pay-api, fix lint
seeker25 Jul 9, 2024
e753057
Small tweak
seeker25 Jul 9, 2024
ef9b801
Update CfsAccount so they have payment method
seeker25 Jul 9, 2024
17dc18c
Adding in some comments and slight tweaks
seeker25 Jul 9, 2024
f55ebe1
more lint test fixes
seeker25 Jul 9, 2024
973ac26
I must be getting sleepy
seeker25 Jul 9, 2024
1f0bc30
Fixes
seeker25 Jul 9, 2024
4d9fab2
Use Ody's refactored function
seeker25 Jul 9, 2024
2f24cbe
More lint fixes
seeker25 Jul 9, 2024
9104c22
test fixes
seeker25 Jul 9, 2024
0f94bc0
test fixes
seeker25 Jul 9, 2024
9d94f65
Missing function
seeker25 Jul 9, 2024
ca072dc
Merge branch 'main' of https://github.com/bcgov/sbc-pay into 22063-part2
seeker25 Jul 10, 2024
2ff681d
Fix statement payment method
seeker25 Jul 11, 2024
200df83
linting fixes
seeker25 Jul 11, 2024
06d4d1c
comment touch up
seeker25 Jul 11, 2024
09e8b2b
put db.session.commit = flush back in
seeker25 Jul 11, 2024
55b7b7f
Fix unit tests
seeker25 Jul 11, 2024
9458d62
Lint fixes
seeker25 Jul 11, 2024
7e84628
Small change for EFT blocking invoice creation
seeker25 Jul 11, 2024
a487bce
small test fix
seeker25 Jul 11, 2024
c01c653
Unit test fix
seeker25 Jul 11, 2024
4a8b427
Migration to update cfs_account.payment_method
seeker25 Jul 12, 2024
1e2e201
Update new pay-api
seeker25 Jul 12, 2024
20ea950
Remove migration, put into job
seeker25 Jul 12, 2024
fef6cb5
more lint cleanup, setuptools add
seeker25 Jul 12, 2024
db2aee6
Remove versioning restoring, not needed.
seeker25 Jul 12, 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
3 changes: 2 additions & 1 deletion jobs/payment-jobs/services/routing_slip.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from pay_api.models import PaymentAccount as PaymentAccountModel
from pay_api.models import RoutingSlip as RoutingSlipModel
from pay_api.services.cfs_service import CFSService
from pay_api.utils.constants import PaymentSystem
from pay_api.utils.enums import CfsAccountStatus
from sentry_sdk import capture_message

Expand All @@ -46,7 +47,7 @@ def create_cfs_account(cfs_account: CfsAccountModel, pay_account: PaymentAccount
rcpt_date=routing_slip.routing_slip_date.strftime('%Y-%m-%d'),
amount=routing_slip.total,
payment_method=pay_account.payment_method,
access_token=CFSService.get_fas_token().json().get('access_token'))
access_token=CFSService.get_token(PaymentSystem.FAS).json().get('access_token'))
cfs_account.commit()
return

Expand Down
50 changes: 42 additions & 8 deletions jobs/payment-jobs/tasks/cfs_create_invoice_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from pay_api.services.invoice_reference import InvoiceReference
from pay_api.services.payment import Payment
from pay_api.services.payment_account import PaymentAccount as PaymentAccountService
from pay_api.utils.constants import CFS_RCPT_EFT_WIRE, RECEIPT_METHOD_PAD_DAILY, RECEIPT_METHOD_PAD_STOP
from pay_api.utils.enums import (
CfsAccountStatus, InvoiceReferenceStatus, InvoiceStatus, PaymentMethod, PaymentStatus, PaymentSystem)
from pay_api.utils.util import generate_transaction_number
Expand Down Expand Up @@ -253,7 +254,6 @@ def _create_pad_invoices(cls): # pylint: disable=too-many-locals
.filter(InvoiceModel.id.notin_(cls._active_invoice_reference_subquery())) \
.order_by(InvoiceModel.created_on.desc()).all()

# Get cfs account
payment_account: PaymentAccountService = PaymentAccountService.find_by_id(account.id)

if len(account_invoices) == 0:
Expand All @@ -273,7 +273,9 @@ def _create_pad_invoices(cls): # pylint: disable=too-many-locals
f'is {payment_account.cfs_account_status} skipping.')
continue

# Add all lines together
if not cls._verify_and_correct_receipt_method(cfs_account, payment_account, PaymentMethod.PAD.value):
continue

lines = []
invoice_total = Decimal('0')
for invoice in account_invoices:
Expand Down Expand Up @@ -409,7 +411,9 @@ def _create_eft_invoices(cls):
f'is {payment_account.cfs_account_status} skipping.')
continue

# Add all payment line items together
if not cls._verify_and_correct_receipt_method(cfs_account, payment_account, PaymentMethod.EFT.value):
continue

lines = []
invoice_total = Decimal('0')
for invoice in account_invoices:
Expand Down Expand Up @@ -477,17 +481,17 @@ def _create_single_invoice_per_purchase(cls, payment_method: PaymentMethod):

current_app.logger.info(f'Found {len(invoices)} to be created in CFS.')
for invoice in invoices:
# Get cfs account
payment_account: PaymentAccountService = PaymentAccountService.find_by_id(invoice.payment_account_id)
cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id(payment_account.id)

# Check for corp type and see if online banking is allowed.
if invoice.payment_method_code == PaymentMethod.ONLINE_BANKING.value:
corp_type: CorpTypeModel = CorpTypeModel.find_by_code(invoice.corp_type_code)
if not corp_type.is_online_banking_allowed:
continue

# Create a CFS invoice
if not cls._verify_and_correct_receipt_method(cfs_account, payment_account, PaymentMethod.ONLINE_BANKING.value):
continue

current_app.logger.debug(f'Creating cfs invoice for invoice {invoice.id}')
try:
invoice_response = CFSService.create_account_invoice(transaction_number=invoice.id,
Expand All @@ -505,8 +509,38 @@ def _create_single_invoice_per_purchase(cls, payment_method: PaymentMethod):
invoice_number=invoice_response.get('invoice_number'),
reference_number=invoice_response.get('pbc_ref_number', None))

# Misc
invoice.cfs_account_id = payment_account.cfs_account_id
# leave the status as SETTLEMENT_SCHEDULED
invoice.invoice_status_code = InvoiceStatus.SETTLEMENT_SCHEDULED.value
invoice.save()

@classmethod
def _verify_and_correct_receipt_method(cls, cfs_account, payment_account, payment_method: str):
"""Verify and correct the receipt method site."""
try:
current_receipt_method = CFSService.get_site(cfs_account).get('receipt_method', None)
if current_receipt_method == RECEIPT_METHOD_PAD_STOP:
current_app.logger.error(f'Skipping the account as the receipt method is {RECEIPT_METHOD_PAD_STOP},'
' database is out of sync with CAS.')
return False

match payment_method:
case PaymentMethod.EFT.value:
new_receipt_method = CFS_RCPT_EFT_WIRE
case PaymentMethod.ONLINE_BANKING.value:
# According to the spec it should be "BCR Online Banking Payments", but in practice we use null.
new_receipt_method = None
case PaymentMethod.PAD.value:
new_receipt_method = RECEIPT_METHOD_PAD_DAILY
case _:
current_app.logging.error(f'Site switching for {payment_method} is not implemented.')
return False

if current_receipt_method != new_receipt_method:
current_app.logger.info('Switching site receipt_method from %s to %s',
current_receipt_method, new_receipt_method)
CFSService.update_site_receipt_method(cfs_account, receipt_method=new_receipt_method)
except Exception as e: # NO QA # pylint: disable=broad-except
capture_message(f'Error switching site for account id={cfs_account.account_id}, '
f'auth account : {payment_account.auth_account_id}, ERROR : {str(e)}', level='error')
return False
return True
6 changes: 3 additions & 3 deletions jobs/payment-jobs/tasks/electronic_funds_transfer_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
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, ReverseOperation)
CfsAccountStatus, EFTShortnameStatus, InvoiceReferenceStatus, InvoiceStatus, PaymentSystem, ReverseOperation)
from sentry_sdk import capture_message


Expand Down Expand Up @@ -81,7 +81,7 @@ def link_electronic_funds_transfers(cls):
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'))
access_token=CFSService.get_token(PaymentSystem.FAS).json().get('access_token'))

# apply receipt to cfs_account
total_invoice_amount = cls._apply_electronic_funds_transfers_to_pending_invoices(
Expand Down Expand Up @@ -129,7 +129,7 @@ def unlink_electronic_funds_transfers(cls):
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'))
access_token=CFSService.get_token(PaymentSystem.FAS).json().get('access_token'))

cls._reset_invoices_and_references_to_created_for_electronic_funds_transfer(eft_short_name)

Expand Down
15 changes: 11 additions & 4 deletions jobs/payment-jobs/tasks/routing_slip_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,14 @@ def link_routing_slips(cls):
receipt_number = routing_slip.generate_cas_receipt_number()
CFSService.create_cfs_receipt(cfs_account=parent_cfs_account,
rcpt_number=receipt_number,
rcpt_date=routing_slip.routing_slip_date.strftime('%Y-%m-%d'),
rcpt_date=routing_slip.routing_slip_date.strftime(
'%Y-%m-%d'),
amount=routing_slip.total,
payment_method=parent_payment_account.payment_method,
access_token=CFSService.get_fas_token().json().get('access_token'))
access_token=CFSService.get_token(
PaymentSystem.FAS).json()
.get('access_token')
)

# Add to the list if parent is NSF, to apply the receipts.
if parent_rs.status == RoutingSlipStatus.NSF.value:
Expand Down Expand Up @@ -129,10 +133,13 @@ def process_correction(cls):
# Recreate the receipt with the modified total.
CFSService.create_cfs_receipt(cfs_account=cfs_account,
rcpt_number=rs.generate_cas_receipt_number(),
rcpt_date=rs.routing_slip_date.strftime('%Y-%m-%d'),
rcpt_date=rs.routing_slip_date.strftime(
'%Y-%m-%d'),
amount=rs.total,
payment_method=payment_account.payment_method,
access_token=CFSService.get_fas_token().json().get('access_token'))
access_token=CFSService.get_token(
PaymentSystem.FAS).json().get('access_token')
)

cls._reset_invoices_and_references_to_created(rs)

Expand Down
1 change: 0 additions & 1 deletion pay-admin/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ def create_app(run_mode=os.getenv('DEPLOYMENT_ENV', 'production')):
db.init_app(app)
ma.init_app(app)

# Init Flask Admin
init_flask_admin(app)
Keycloak(app)

Expand Down
6 changes: 3 additions & 3 deletions pay-api/src/pay_api/factory/payment_system_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Factory to manage creation of pay system service."""
from datetime import datetime
from datetime import datetime, timezone

from flask import current_app

Expand Down Expand Up @@ -109,8 +109,8 @@ def create(**kwargs):
@staticmethod
def _validate_and_throw_error(instance: PaymentSystemService, payment_account: PaymentAccount):
if isinstance(instance, PadService):
is_in_pad_confirmation_period = payment_account.pad_activation_date > \
datetime.now(payment_account.pad_activation_date.tzinfo)
is_in_pad_confirmation_period = payment_account.pad_activation_date.replace(tzinfo=timezone.utc) > \
datetime.now(tz=timezone.utc)
is_cfs_account_in_pending_status = payment_account.cfs_account_status == \
CfsAccountStatus.PENDING_PAD_ACTIVATION.value

Expand Down
4 changes: 3 additions & 1 deletion pay-api/src/pay_api/models/base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,6 @@ def rollback():
@classmethod
def find_by_id(cls, identifier: int):
"""Return model by id."""
return db.session.get(cls, identifier)
if identifier:
return db.session.get(cls, identifier)
seeker25 marked this conversation as resolved.
Show resolved Hide resolved
return None
61 changes: 28 additions & 33 deletions pay-api/src/pay_api/services/cfs_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,8 @@
CFS_ADJ_ACTIVITY_NAME, CFS_BATCH_SOURCE, CFS_CASH_RCPT, CFS_CM_BATCH_SOURCE, CFS_CMS_TRX_TYPE, CFS_CUST_TRX_TYPE,
CFS_CUSTOMER_PROFILE_CLASS, CFS_DRAWDOWN_BALANCE, CFS_FAS_CUSTOMER_PROFILE_CLASS, CFS_LINE_TYPE,
CFS_NSF_REVERSAL_REASON, CFS_PAYMENT_REVERSAL_REASON, CFS_RCPT_EFT_WIRE, CFS_TERM_NAME, DEFAULT_ADDRESS_LINE_1,
DEFAULT_CITY, DEFAULT_COUNTRY, DEFAULT_CURRENCY, DEFAULT_JURISDICTION, DEFAULT_POSTAL_CODE,
RECEIPT_METHOD_PAD_DAILY, RECEIPT_METHOD_PAD_STOP)
from pay_api.utils.enums import AuthHeaderType, ContentType, PaymentMethod, ReverseOperation
DEFAULT_CITY, DEFAULT_COUNTRY, DEFAULT_CURRENCY, DEFAULT_JURISDICTION, DEFAULT_POSTAL_CODE)
from pay_api.utils.enums import AuthHeaderType, ContentType, PaymentMethod, PaymentSystem, ReverseOperation
from pay_api.utils.util import current_local_time, generate_transaction_number


Expand Down Expand Up @@ -62,18 +61,19 @@ def create_cfs_account(cls, identifier: str, contact_info: Dict[str, Any], # py

return account_details

@classmethod
def suspend_cfs_account(cls, cfs_account: CfsAccountModel) -> Dict[str, any]:
"""Suspend a CFS PAD Account from any further PAD transactions."""
return cls._update_site(cfs_account, receipt_method=RECEIPT_METHOD_PAD_STOP)

@classmethod
def unsuspend_cfs_account(cls, cfs_account: CfsAccountModel) -> Dict[str, any]:
"""Unuspend a CFS PAD Account from any further PAD transactions."""
return cls._update_site(cfs_account, receipt_method=RECEIPT_METHOD_PAD_DAILY)
@staticmethod
def get_site(cfs_account: CfsAccountModel) -> Dict[str, any]:
"""Get the site details."""
access_token = CFSService.get_token().json().get('access_token')
cfs_base: str = current_app.config.get('CFS_BASE_URL')
site_url = f'{cfs_base}/cfs/parties/{cfs_account.cfs_party}/accs/{cfs_account.cfs_account}/' \
f'sites/{cfs_account.cfs_site}/'
site_response = OAuthService.get(site_url, access_token, AuthHeaderType.BEARER, ContentType.JSON)
return site_response.json()

@classmethod
def _update_site(cls, cfs_account: CfsAccountModel, receipt_method: str):
@staticmethod
def update_site_receipt_method(cfs_account: CfsAccountModel, receipt_method: str):
"""Update the receipt method for the site."""
access_token = CFSService.get_token().json().get('access_token')
pad_stop_payload = {
'receipt_method': receipt_method
Expand Down Expand Up @@ -264,7 +264,7 @@ def get_invoice(cls, cfs_account: CfsAccountModel, inv_number: str):
def reverse_rs_receipt_in_cfs(cls, cfs_account, receipt_number, operation: ReverseOperation):
"""Reverse Receipt."""
current_app.logger.debug('>Reverse receipt: %s', receipt_number)
access_token: str = CFSService.get_fas_token().json().get('access_token')
access_token: str = CFSService.get_token(PaymentSystem.FAS).json().get('access_token')
cfs_base: str = current_app.config.get('CFS_BASE_URL')
receipt_url = f'{cfs_base}/cfs/parties/{cfs_account.cfs_party}/accs/{cfs_account.cfs_account}' \
f'/sites/{cfs_account.cfs_site}/rcpts/{receipt_number}/reverse'
Expand Down Expand Up @@ -313,33 +313,28 @@ def update_bank_details(cls, name: str, party_number: str, # pylint: disable=to
return cls._save_bank_details(access_token, party_number, account_number, site_number, payment_info)

@staticmethod
def get_token():
"""Generate oauth token from payBC which will be used for all communication."""
def get_token(payment_system=PaymentSystem.PAYBC):
"""Generate oauth token from PayBC/FAS which will be used for all communication."""
current_app.logger.debug('<Getting token')
token_url = current_app.config.get('CFS_BASE_URL', None) + '/oauth/token'
match payment_system:
case PaymentSystem.PAYBC:
client_id = current_app.config.get('CFS_CLIENT_ID')
secret = current_app.config.get('CFS_CLIENT_SECRET')
case PaymentSystem.FAS:
client_id = current_app.config.get('CFS_FAS_CLIENT_ID')
secret = current_app.config.get('CFS_FAS_CLIENT_SECRET')
case _:
raise ValueError('Invalid Payment System')
basic_auth_encoded = base64.b64encode(
bytes(current_app.config.get('CFS_CLIENT_ID') + ':' + current_app.config.get('CFS_CLIENT_SECRET'),
bytes(client_id + ':' + secret,
'utf-8')).decode('utf-8')
data = 'grant_type=client_credentials'
token_response = OAuthService.post(token_url, basic_auth_encoded, AuthHeaderType.BASIC,
ContentType.FORM_URL_ENCODED, data)
current_app.logger.debug('>Getting token')
return token_response

@staticmethod
def get_fas_token():
"""Generate oauth token for FAS client which will be used for all communication."""
current_app.logger.debug('<Getting FAS token')
token_url = current_app.config.get('CFS_BASE_URL', None) + '/oauth/token'
basic_auth_encoded = base64.b64encode(
bytes(current_app.config.get('CFS_FAS_CLIENT_ID') + ':' + current_app.config.get('CFS_FAS_CLIENT_SECRET'),
'utf-8')).decode('utf-8')
data = 'grant_type=client_credentials'
token_response = OAuthService.post(token_url, basic_auth_encoded, AuthHeaderType.BASIC,
ContentType.FORM_URL_ENCODED, data)
current_app.logger.debug('>Getting FAS token')
return token_response

@classmethod
def create_account_invoice(cls, transaction_number: str, line_items: List[PaymentLineItemModel],
cfs_account: CfsAccountModel) \
Expand Down Expand Up @@ -620,7 +615,7 @@ def adjust_receipt_to_zero(cls, cfs_account: CfsAccountModel, receipt_number: st
2. Adjust the receipt with activity name corresponding to refund or write off.
"""
current_app.logger.debug('<adjust_receipt_to_zero: %s %s', cfs_account, receipt_number)
access_token: str = CFSService.get_fas_token().json().get('access_token')
access_token: str = CFSService.get_token(PaymentSystem.FAS).json().get('access_token')
cfs_base: str = current_app.config.get('CFS_BASE_URL')
receipt_url = f'{cfs_base}/cfs/parties/{cfs_account.cfs_party}/accs/{cfs_account.cfs_account}/' \
f'sites/{cfs_account.cfs_site}/rcpts/{receipt_number}/'
Expand Down
11 changes: 0 additions & 11 deletions pay-api/src/pay_api/services/eft_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,6 @@ def create_account(self, identifier: str, contact_info: Dict[str, Any], payment_
cfs_account.status = CfsAccountStatus.PENDING.value
return cfs_account

def update_account(self, name: str, cfs_account: CfsAccountModel, payment_info: Dict[str, Any]) -> CfsAccountModel:
"""Update pad account."""
if str(payment_info.get('bankInstitutionNumber')) != cfs_account.bank_number or \
str(payment_info.get('bankTransitNumber')) != cfs_account.bank_branch_number or \
str(payment_info.get('bankAccountNumber')) != cfs_account.bank_account_number:
# This means, the current cfs_account is for PAD, not EFT
# Make the current CFS Account as INACTIVE in DB
cfs_account.status = CfsAccountStatus.INACTIVE.value
cfs_account.flush()
return cfs_account

def create_invoice(self, payment_account: PaymentAccount, line_items: List[PaymentLineItem], invoice: Invoice,
**kwargs) -> None:
"""Do nothing here, we create invoice references on the create CFS_INVOICES job."""
Expand Down
2 changes: 1 addition & 1 deletion pay-api/src/pay_api/services/eft_short_names.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ def get_pending_payment_count():
.filter(InvoiceModel.payment_account_id == PaymentAccountModel.id)
.filter(EFTCreditInvoiceLinkModel.status_code.in_([EFTCreditInvoiceStatus.PENDING.value]))
.correlate(PaymentAccountModel)
.as_scalar())
.scalar_subquery())
seeker25 marked this conversation as resolved.
Show resolved Hide resolved

@classmethod
def get_search_query(cls, search_criteria: EFTShortnamesSearch, is_count: bool = False):
Expand Down
Loading
Loading