def delete_account(cls, auth_account_id: str) -> PaymentAccount: """Delete the payment account.""" current_app.logger.debug('<delete_account') pay_account: PaymentAccountModel = PaymentAccountModel.find_by_auth_account_id( auth_account_id) cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id( pay_account.id) # 1 - Check if account have any credits # 2 - Check if account have any PAD transactions done in last N (10) days. if pay_account.credit and pay_account.credit > 0: raise BusinessException(Error.OUTSTANDING_CREDIT) # Check if account is frozen. cfs_status: str = cfs_account.status if cfs_account else None if cfs_status == CfsAccountStatus.FREEZE.value: raise BusinessException(Error.FROZEN_ACCOUNT) if InvoiceModel.find_outstanding_invoices_for_account( pay_account.id, get_outstanding_txns_from_date()): # Check if there is any recent PAD transactions in N days. raise BusinessException(Error.TRANSACTIONS_IN_PROGRESS) # If CFS Account present, mark it as INACTIVE. if cfs_status and cfs_status != CfsAccountStatus.INACTIVE.value: cfs_account.status = CfsAccountStatus.INACTIVE.value # If account is active or pending pad activation stop PAD payments. if pay_account.payment_method == PaymentMethod.PAD.value \ and cfs_status in [CfsAccountStatus.ACTIVE.value, CfsAccountStatus.PENDING_PAD_ACTIVATION.value]: CFSService.suspend_cfs_account(cfs_account) cfs_account.save() if pay_account.statement_notification_enabled: pay_account.statement_notification_enabled = False pay_account.save()
def unlock_frozen_accounts(account_id: int): """Unlock frozen accounts.""" from pay_api.services.cfs_service import CFSService # pylint: disable=import-outside-toplevel,cyclic-import pay_account: PaymentAccount = PaymentAccount.find_by_id(account_id) if pay_account.cfs_account_status == CfsAccountStatus.FREEZE.value: # update CSF cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id( pay_account.id) CFSService.unsuspend_cfs_account(cfs_account=cfs_account) cfs_account.status = CfsAccountStatus.ACTIVE.value cfs_account.save() payload = pay_account._create_account_event_payload( # pylint:disable=protected-access 'bc.registry.payment.unlockAccount') try: publish_response( payload=payload, client_name=current_app.config['NATS_ACCOUNT_CLIENT_NAME'], subject=current_app.config['NATS_ACCOUNT_SUBJECT']) except Exception as e: # pylint: disable=broad-except current_app.logger.error(e) current_app.logger.error( 'Notification to Queue failed for the Unlock Account %s - %s', pay_account.auth_account_id, pay_account.auth_account_name) capture_message( 'Notification to Queue failed for the Unlock Account : {msg}.' .format(msg=payload), level='error')
def _cancel_rs_invoices(cls): """Cancel routing slip invoices in CFS.""" invoices: List[InvoiceModel] = InvoiceModel.query \ .filter(InvoiceModel.payment_method_code == PaymentMethod.INTERNAL.value) \ .filter(InvoiceModel.invoice_status_code == InvoiceStatus.REFUND_REQUESTED.value) \ .filter(InvoiceModel.routing_slip is not None) \ .order_by(InvoiceModel.created_on.asc()).all() current_app.logger.info( f'Found {len(invoices)} to be cancelled in CFS.') for invoice in invoices: # call unapply rcpts # adjust invoice to zero current_app.logger.debug(f'Calling the invoice {invoice.id}') routing_slip = RoutingSlipModel.find_by_number( invoice.routing_slip) routing_slip_payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id( routing_slip.payment_account_id) cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id( routing_slip_payment_account.id) # Find COMPLETED invoice reference; as unapply has to be done only if invoice is created and applied in CFS. invoice_reference = InvoiceReferenceModel. \ find_reference_by_invoice_id_and_status(invoice.id, status_code=InvoiceReferenceStatus.COMPLETED.value) if invoice_reference: current_app.logger.debug( f'Found invoice reference - {invoice_reference.invoice_number}' ) try: # find receipts against the invoice and unapply # apply receipt now receipts: List[ ReceiptModel] = ReceiptModel.find_all_receipts_for_invoice( invoice_id=invoice.id) for receipt in receipts: CFSService.unapply_receipt( cfs_account, receipt.receipt_number, invoice_reference.invoice_number) adjustment_negative_amount = -invoice.total CFSService.adjust_invoice( cfs_account=cfs_account, inv_number=invoice_reference.invoice_number, amount=adjustment_negative_amount) except Exception as e: # NOQA # pylint: disable=broad-except capture_message( f'Error on canelling Routing Slip invoice: invoice id={invoice.id}, ' f'routing slip : {routing_slip.id}, ERROR : {str(e)}', level='error') current_app.logger.error(e) # TODO stop execution ? what should be the invoice stats ; should we set it to error or retry? continue invoice_reference.status_code = InvoiceReferenceStatus.CANCELLED.value invoice.invoice_status_code = InvoiceStatus.REFUNDED.value invoice.save()
def _process_failed_payments(row): """Handle failed payments.""" # 1. Set the cfs_account status as FREEZE. # 2. Call cfs api to Stop further PAD on this account. # 3. Reverse the invoice_reference status to ACTIVE, invoice status to SETTLEMENT_SCHED, and delete receipt. # 4. Create an NSF invoice for this account. # 5. Create invoice reference for the newly created NSF invoice. # 6. Adjust invoice in CFS to include NSF fees. inv_number = _get_row_value(row, Column.TARGET_TXN_NO) # If there is a FAILED payment record for this; it means it's a duplicate event. Ignore it. payment: PaymentModel = PaymentModel.find_payment_by_invoice_number_and_status( inv_number, PaymentStatus.FAILED.value) if payment: logger.info('Ignoring duplicate NSF message for invoice : %s ', inv_number) return # Set CFS Account Status. payment_account: PaymentAccountModel = _get_payment_account(row) cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id( payment_account.id) logger.info('setting payment account id : %s status as FREEZE', payment_account.id) cfs_account.status = CfsAccountStatus.FREEZE.value # Call CFS to stop any further PAD transactions on this account. CFSService.suspend_cfs_account(cfs_account) # Find the invoice_reference for this invoice and mark it as ACTIVE. inv_references: List[InvoiceReferenceModel] = db.session.query(InvoiceReferenceModel). \ filter(InvoiceReferenceModel.status_code == InvoiceReferenceStatus.COMPLETED.value). \ filter(InvoiceReferenceModel.invoice_number == inv_number). \ all() # Update status to ACTIVE, if it was marked COMPLETED for inv_reference in inv_references: inv_reference.status_code = InvoiceReferenceStatus.ACTIVE.value # Find receipt and delete it. receipt: ReceiptModel = ReceiptModel.find_by_invoice_id_and_receipt_number( invoice_id=inv_reference.invoice_id) if receipt: db.session.delete(receipt) # Find invoice and update the status to SETTLEMENT_SCHED invoice: InvoiceModel = InvoiceModel.find_by_id( identifier=inv_reference.invoice_id) invoice.invoice_status_code = InvoiceStatus.SETTLEMENT_SCHEDULED.value invoice.paid = 0 # Create an invoice for NSF for this account invoice = _create_nsf_invoice(cfs_account, inv_number, payment_account) # Adjust CFS invoice CFSService.add_nsf_adjustment(cfs_account=cfs_account, inv_number=inv_number, amount=invoice.total)
def apply_routing_slips_to_invoice(cls, # pylint: disable = too-many-arguments, too-many-locals routing_slip_payment_account: PaymentAccountModel, active_cfs_account: CfsAccountModel, parent_routing_slip: RoutingSlipModel, invoice: InvoiceModel, invoice_number: str) -> bool: """Apply routing slips (receipts in CFS) to invoice.""" has_errors = False child_routing_slips: List[RoutingSlipModel] = RoutingSlipModel.find_children(parent_routing_slip.number) # an invoice has to be applied to multiple receipts (incl. all linked RS); apply till the balance is zero for routing_slip in (parent_routing_slip, *child_routing_slips): try: # apply receipt now current_app.logger.debug(f'Apply receipt {routing_slip.number} on invoice {invoice_number} ' f'for routing slip {routing_slip.number}') receipt_number = routing_slip.number # For linked routing slips, new receipt numbers ends with 'L' if routing_slip.status == RoutingSlipStatus.LINKED.value: receipt_number = f'{routing_slip.number}L' # If balance of receipt is zero, continue to next receipt. receipt_balance_before_apply = float( CFSService.get_receipt(active_cfs_account, receipt_number).get('unapplied_amount') ) current_app.logger.debug(f'Current balance on {receipt_number} = {receipt_balance_before_apply}') if receipt_balance_before_apply == 0: continue current_app.logger.debug(f'Applying receipt {receipt_number} to {invoice_number}') receipt_response = CFSService.apply_receipt(active_cfs_account, receipt_number, invoice_number) # Create receipt. receipt = Receipt() receipt.receipt_number = receipt_response.json().get('receipt_number', None) receipt_amount = receipt_balance_before_apply - float(receipt_response.json().get('unapplied_amount')) receipt.receipt_amount = receipt_amount receipt.invoice_id = invoice.id receipt.receipt_date = datetime.now() receipt.flush() invoice_from_cfs = CFSService.get_invoice(active_cfs_account, invoice_number) if invoice_from_cfs.get('amount_due') == 0: break except Exception as e: # NOQA # pylint: disable=broad-except capture_message( f'Error on creating Routing Slip invoice: account id={routing_slip_payment_account.id}, ' f'routing slip : {routing_slip.id}, ERROR : {str(e)}', level='error') current_app.logger.error(e) has_errors = True continue return has_errors
def get_bank_info( party_number: str, # pylint: disable=too-many-arguments account_number: str, site_number: str): """Get bank details to the site.""" current_app.logger.debug('<Updating CFS payment details ') site_payment_url = current_app.config.post( 'CFS_BASE_URL' ) + f'/cfs/parties/{party_number}/accs/{account_number}/sites/{site_number}/payment/' access_token: str = CFSService.get_token().json().get('access_token') payment_details = CFSService.get(site_payment_url, access_token, AuthHeaderType.BEARER, ContentType.JSON) return payment_details.json()
def get_receipt(self, payment_account: PaymentAccount, pay_response_url: str, invoice_reference: InvoiceReference): """Get receipt from paybc for the receipt number or get receipt against invoice number.""" current_app.logger.debug('<paybc_service_Getting token') access_token: str = CFSService.get_token().json().get('access_token') current_app.logger.debug('<Getting receipt') receipt_url = current_app.config.get('CFS_BASE_URL') + '/cfs/parties/{}/accs/{}/sites/{}/rcpts/'.format( payment_account.cfs_party, payment_account.cfs_account, payment_account.cfs_site) parsed_url = parse_url_params(pay_response_url) receipt_number: str = parsed_url.get('receipt_number') if 'receipt_number' in parsed_url else None if not receipt_number: # Find all receipts for the site and then match with invoice number receipts_response = self.get(receipt_url, access_token, AuthHeaderType.BEARER, ContentType.JSON, retry_on_failure=True).json() for receipt in receipts_response.get('items'): expanded_receipt = self.__get_receipt_by_number(access_token, receipt_url, receipt.get('receipt_number')) for invoice in expanded_receipt.get('invoices'): if invoice.get('invoice_number') == invoice_reference.invoice_number: return receipt.get('receipt_number'), parser.parse( expanded_receipt.get('receipt_date')), float(invoice.get('amount_applied')) if receipt_number: receipt_response = self.__get_receipt_by_number(access_token, receipt_url, receipt_number) receipt_date = parser.parse(receipt_response.get('receipt_date')) amount: float = 0 for invoice in receipt_response.get('invoices'): if invoice.get('invoice_number') == invoice_reference.invoice_number: amount += float(invoice.get('amount_applied')) return receipt_number, receipt_date, amount return None
def _process_cfs_refund(cls, invoice: InvoiceModel): """Process refund in CFS.""" if invoice.payment_method_code == PaymentMethod.DIRECT_PAY.value: cls._publish_to_mailer(invoice) payment: PaymentModel = PaymentModel.find_payment_for_invoice( invoice.id) payment.payment_status_code = PaymentStatus.REFUNDED.value payment.flush() else: # Create credit memo in CFS. # TODO Refactor this when actual task is done. This is just a quick fix for CFS UAT - Dec 2020 cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id( invoice.payment_account_id) line_items: List[PaymentLineItemModel] = [] for line_item in invoice.payment_line_items: line_items.append(PaymentLineItemModel.find_by_id( line_item.id)) cms_response = CFSService.create_cms(line_items=line_items, cfs_account=cfs_account) # TODO Create a payment record for this to show up on transactions, when the ticket comes. # Create a credit with CM identifier as CMs are not reported in payment interface file # until invoice is applied. CreditModel(cfs_identifier=cms_response.get('credit_memo_number'), is_credit_memo=True, amount=invoice.total, remaining_amount=invoice.total, account_id=invoice.payment_account_id).save()
def update_account(self, name: str, cfs_account: CfsAccountModel, payment_info: Dict[str, Any]) -> CfsAccountModel: """Update account in CFS.""" 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, PAD account details have changed. So update banking details for this CFS account # Call cfs service to add new bank info. bank_details = CFSService.update_bank_details(name=cfs_account.payment_account.auth_account_name, party_number=cfs_account.cfs_party, account_number=cfs_account.cfs_account, site_number=cfs_account.cfs_site, payment_info=payment_info) instrument_number = bank_details.get('payment_instrument_number', None) # Make the current CFS Account as INACTIVE in DB cfs_account.status = CfsAccountStatus.INACTIVE.value cfs_account.flush() # Create new CFS Account updated_cfs_account = CfsAccountModel() updated_cfs_account.bank_account_number = payment_info.get('bankAccountNumber') updated_cfs_account.bank_number = payment_info.get('bankInstitutionNumber') updated_cfs_account.bank_branch_number = payment_info.get('bankTransitNumber') updated_cfs_account.cfs_site = cfs_account.cfs_site updated_cfs_account.cfs_party = cfs_account.cfs_party updated_cfs_account.cfs_account = cfs_account.cfs_account updated_cfs_account.payment_account = cfs_account.payment_account updated_cfs_account.status = CfsAccountStatus.ACTIVE.value updated_cfs_account.payment_instrument_number = instrument_number updated_cfs_account.flush() return cfs_account
def _apply_routing_slips_to_pending_invoices(cls, routing_slip: RoutingSlipModel) -> float: """Apply the routing slips again, when routing slip is linked to an NSF parent.""" current_app.logger.info(f'Starting NSF recovery process for {routing_slip.number}') routing_slip_payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id( routing_slip.payment_account_id) # apply invoice to the active CFS_ACCOUNT which will be the parent routing slip active_cfs_account = CfsAccountModel.find_effective_by_account_id(routing_slip_payment_account.id) invoices: List[InvoiceModel] = db.session.query(InvoiceModel) \ .filter(InvoiceModel.routing_slip == routing_slip.number, InvoiceModel.invoice_status_code.in_([InvoiceStatus.CREATED.value, InvoiceStatus.APPROVED.value])) \ .all() current_app.logger.info(f'Found {len(invoices)} to apply receipt') applied_amount = 0 for inv in invoices: inv_ref: InvoiceReferenceModel = InvoiceReferenceModel.find_reference_by_invoice_id_and_status( inv.id, InvoiceReferenceStatus.ACTIVE.value ) cls.apply_routing_slips_to_invoice( routing_slip_payment_account, active_cfs_account, routing_slip, inv, inv_ref.invoice_number ) # IF invoice balance is zero, then update records. if CFSService.get_invoice(cfs_account=active_cfs_account, inv_number=inv_ref.invoice_number) \ .get('amount_due') == 0: applied_amount += inv.total inv_ref.status_code = InvoiceReferenceStatus.COMPLETED.value inv.invoice_status_code = InvoiceStatus.PAID.value return applied_amount
def _process_cfs_refund(cls, invoice: InvoiceModel): """Process refund in CFS.""" if invoice.payment_method_code in ([ PaymentMethod.DIRECT_PAY.value, PaymentMethod.DRAWDOWN.value ]): cls._publish_to_mailer(invoice) payment: PaymentModel = PaymentModel.find_payment_for_invoice( invoice.id) payment.payment_status_code = PaymentStatus.REFUNDED.value payment.flush() else: # Create credit memo in CFS if the invoice status is PAID. # Don't do anything is the status is APPROVED. if invoice.invoice_status_code == InvoiceStatus.APPROVED.value: return cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id( invoice.payment_account_id) line_items: List[PaymentLineItemModel] = [] for line_item in invoice.payment_line_items: line_items.append(PaymentLineItemModel.find_by_id( line_item.id)) cms_response = CFSService.create_cms(line_items=line_items, cfs_account=cfs_account) # TODO Create a payment record for this to show up on transactions, when the ticket comes. # Create a credit with CM identifier as CMs are not reported in payment interface file # until invoice is applied. CreditModel(cfs_identifier=cms_response.get('credit_memo_number'), is_credit_memo=True, amount=invoice.total, remaining_amount=invoice.total, account_id=invoice.payment_account_id).save()
def create_payment_receipt(auth_account_id: str, credit_request: Dict[str, str], **kwargs) -> Payment: """Create a payment record for the account.""" pay_account = PaymentAccountService.find_by_auth_account_id( auth_account_id) # Create a payment record # Create a receipt in CFS for the amount. payment = Payment.create( payment_method=credit_request.get('paymentMethod'), payment_system=PaymentSystem.PAYBC.value, payment_account_id=pay_account.id) receipt_number: str = generate_receipt_number(payment.id) receipt_date = credit_request.get('completedOn') amount = credit_request.get('paidAmount') receipt_response = CFSService.create_cfs_receipt( payment_account=pay_account, rcpt_number=receipt_number, rcpt_date=receipt_date, amount=amount, payment_method=credit_request.get('paymentMethod')) payment.receipt_number = receipt_response.get('receipt_number', receipt_number) payment.paid_amount = amount payment.created_by = kwargs['user'].user_name payment.completed_on = parser.parse(receipt_date) payment.payment_status_code = PaymentStatus.COMPLETED.value payment.save() return payment
def adjust_routing_slips(cls): """Adjust routing slips. Steps: 1. Adjust routing slip receipts for any Write off routing slips. 2. Adjust routing slip receipts for any Refund approved routing slips. """ current_app.logger.info('<<adjust_routing_slips') adjust_statuses = [RoutingSlipStatus.REFUND_AUTHORIZED.value, RoutingSlipStatus.WRITE_OFF_AUTHORIZED.value] # For any pending refund/write off balance should be more than $0 routing_slips = db.session.query(RoutingSlipModel) \ .filter(RoutingSlipModel.status.in_(adjust_statuses), RoutingSlipModel.remaining_amount > 0).all() current_app.logger.info(f'Found {len(routing_slips)} to write off or refund authorized.') for routing_slip in routing_slips: try: # 1.Adjust the routing slip and it's child routing slips for the remaining balance. current_app.logger.debug(f'Adjusting routing slip {routing_slip.number}') payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id(routing_slip.payment_account_id) cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id(payment_account.id) # reverse routing slip receipt # Find all child routing slip and reverse it, as all linked routing slips are also considered as NSF. child_routing_slips: List[RoutingSlipModel] = RoutingSlipModel.find_children(routing_slip.number) for rs in (routing_slip, *child_routing_slips): receipt_number = rs.number is_refund = routing_slip.status == RoutingSlipStatus.REFUND_AUTHORIZED.value if rs.parent_number: receipt_number = f'{receipt_number}L' # Adjust the receipt to zero in CFS CFSService.adjust_receipt_to_zero(cfs_account, receipt_number, is_refund) routing_slip.refund_amount = routing_slip.remaining_amount routing_slip.remaining_amount = 0 routing_slip.save() except Exception as e: # NOQA # pylint: disable=broad-except capture_message( f'Error on Adjusting Routing Slip for :={routing_slip.number}, ' f'routing slip : {routing_slip.id}, ERROR : {str(e)}', level='error') current_app.logger.error(e) continue
def _sync_credit_records(): """Sync credit records with CFS.""" # 1. Get all credit records with balance > 0 # 2. If it's on account receipt call receipt endpoint and calculate balance. # 3. If it's credit memo, call credit memo endpoint and calculate balance. # 4. Roll up the credits to credit field in payment_account. active_credits: List[CreditModel] = db.session.query(CreditModel).filter( CreditModel.remaining_amount > 0).all() logger.info('Found %s credit records', len(active_credits)) account_ids: List[int] = [] for credit in active_credits: account_ids.append(credit.account_id) cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id( credit.account_id) if credit.is_credit_memo: credit_memo = CFSService.get_cms(cfs_account=cfs_account, cms_number=credit.cfs_identifier) credit.remaining_amount = abs(float(credit_memo.get('amount_due'))) else: receipt = CFSService.get_receipt( cfs_account=cfs_account, receipt_number=credit.cfs_identifier) receipt_amount = float(receipt.get('receipt_amount')) applied_amount: float = 0 for invoice in receipt.get('invoices', []): applied_amount += float(invoice.get('amount_applied')) credit.remaining_amount = receipt_amount - applied_amount credit.save() # Roll up the credits and add up to credit in payment_account. for account_id in set(account_ids): account_credits: List[CreditModel] = db.session.query( CreditModel).filter(CreditModel.remaining_amount > 0).filter( CreditModel.account_id == account_id).all() credit_total: float = 0 for account_credit in account_credits: credit_total += account_credit.remaining_amount pay_account: PaymentAccountModel = PaymentAccountModel.find_by_id( account_id) pay_account.credit = credit_total pay_account.save()
def _create_nsf_invoice(cls, cfs_account: CfsAccountModel, rs_number: str, payment_account: PaymentAccountModel) -> InvoiceModel: """Create Invoice, line item and invoice reference records.""" fee_schedule: FeeScheduleModel = FeeScheduleModel.find_by_filing_type_and_corp_type(corp_type_code='BCR', filing_type_code='NSF') invoice = InvoiceModel( bcol_account=payment_account.bcol_account, payment_account_id=payment_account.id, cfs_account_id=cfs_account.id, invoice_status_code=InvoiceStatus.CREATED.value, total=fee_schedule.fee.amount, service_fees=0, paid=0, payment_method_code=PaymentMethod.INTERNAL.value, corp_type_code='BCR', created_on=datetime.now(), created_by='SYSTEM', routing_slip=rs_number ) invoice = invoice.save() distribution: DistributionCodeModel = DistributionCodeModel.find_by_active_for_fee_schedule( fee_schedule.fee_schedule_id) line_item = PaymentLineItemModel( invoice_id=invoice.id, total=invoice.total, fee_schedule_id=fee_schedule.fee_schedule_id, description=fee_schedule.filing_type.description, filing_fees=invoice.total, gst=0, priority_fees=0, pst=0, future_effective_fees=0, line_item_status_code=LineItemStatus.ACTIVE.value, service_fees=0, fee_distribution_id=distribution.distribution_code_id) line_item.save() invoice_response = CFSService.create_account_invoice(transaction_number=invoice.id, line_items=invoice.payment_line_items, cfs_account=cfs_account) invoice_number = invoice_response.json().get('invoice_number', None) current_app.logger.info(f'invoice_number {invoice_number} created in CFS for NSF.') InvoiceReferenceModel( invoice_id=invoice.id, invoice_number=invoice_number, reference_number=invoice_response.json().get('pbc_ref_number', None), status_code=InvoiceReferenceStatus.ACTIVE.value ).save() return invoice
def _create_single_invoice_per_purchase(cls, payment_method: PaymentMethod): """Create one CFS invoice per purchase.""" invoices: List[InvoiceModel] = InvoiceModel.query \ .filter_by(payment_method_code=payment_method.value) \ .filter_by(invoice_status_code=InvoiceStatus.CREATED.value) \ .order_by(InvoiceModel.created_on.asc()).all() 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 current_app.logger.debug( f'Creating cfs invoice for invoice {invoice.id}') try: invoice_response = CFSService.create_account_invoice( transaction_number=invoice.id, line_items=invoice.payment_line_items, cfs_account=cfs_account) except Exception as e: # NOQA # pylint: disable=broad-except capture_message( f'Error on creating Online Banking invoice: account id={payment_account.id}, ' f'auth account : {payment_account.auth_account_id}, ERROR : {str(e)}', level='error') current_app.logger.error(e) continue # Create invoice reference, payment record and a payment transaction InvoiceReference.create( invoice_id=invoice.id, invoice_number=invoice_response.json().get('invoice_number'), reference_number=invoice_response.json().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()
def create_cfs_account(cfs_account: CfsAccountModel, pay_account: PaymentAccountModel): """Create CFS account for routing slip.""" routing_slip: RoutingSlipModel = RoutingSlipModel.find_by_payment_account_id( pay_account.id) try: # TODO add status check so that LINKED etc can be skipped. # for RS , entity/business number=party name ; RS Number=site name cfs_account_details: Dict[str, any] = CFSService.create_cfs_account( identifier=pay_account.name, contact_info={}, site_name=routing_slip.number, is_fas=True) cfs_account.cfs_account = cfs_account_details.get('account_number') cfs_account.cfs_party = cfs_account_details.get('party_number') cfs_account.cfs_site = cfs_account_details.get('site_number') cfs_account.status = CfsAccountStatus.ACTIVE.value # Create receipt in CFS for the payment. CFSService.create_cfs_receipt( cfs_account=cfs_account, rcpt_number=routing_slip.number, 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')) cfs_account.commit() return except Exception as e: # NOQA # pylint: disable=broad-except capture_message( f'Error on creating Routing Slip CFS Account: account id={pay_account.id}, ' f'auth account : {pay_account.auth_account_id}, ERROR : {str(e)}', level='error') current_app.logger.error(e) cfs_account.rollback() return
def run_update(pay_account_id, num_records): """Update bank info.""" current_app.logger.info( f'<<<< Running Update for account id from :{pay_account_id} and total:{num_records} >>>>' ) pad_accounts: List[PaymentAccountModel] = db.session.query(PaymentAccountModel).filter( PaymentAccountModel.payment_method == PaymentMethod.PAD.value) \ .filter(PaymentAccountModel.id >= pay_account_id) \ .order_by(PaymentAccountModel.id.asc()) \ .limit(num_records) \ .all() access_token: str = CFSService.get_token().json().get('access_token') current_app.logger.info( f'<<<< Total number of records founds: {len(pad_accounts)}') current_app.logger.info( f'<<<< records founds: {[accnt.id for accnt in pad_accounts]}') if len(pad_accounts) == 0: return for payment_account in pad_accounts: cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id( payment_account.id) current_app.logger.info( f'<<<< Running Update for account id :{payment_account.id} and cfs_account:{cfs_account.id} >>>>' ) # payment_details = get_bank_info(cfs_account.cfs_party, cfs_account.cfs_account, cfs_account.cfs_site) # current_app.logger.info(payment_details) name = re.sub(r'[^a-zA-Z0-9]+', ' ', payment_account.name) payment_info: Dict[str, any] = { 'bankInstitutionNumber': cfs_account.bank_number, 'bankTransitNumber': cfs_account.bank_branch_number, 'bankAccountNumber': cfs_account.bank_account_number, 'bankAccountName': name } save_bank_details(access_token, cfs_account.cfs_party, cfs_account.cfs_account, cfs_account.cfs_site, payment_info) current_app.logger.info( f'<<<< Successfully Updated for account id :{payment_account.id} and cfs_account:{cfs_account.id} >>>>' )
def _refund_and_create_credit_memo(invoice: InvoiceModel): # Create credit memo in CFS if the invoice status is PAID. # Don't do anything is the status is APPROVED. current_app.logger.info( f'Creating credit memo for invoice : {invoice.id}, {invoice.invoice_status_code}' ) if invoice.invoice_status_code == InvoiceStatus.APPROVED.value \ and InvoiceReferenceModel.find_reference_by_invoice_id_and_status( invoice.id, InvoiceReferenceStatus.ACTIVE.value) is None: return cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id( invoice.payment_account_id) line_items: List[PaymentLineItemModel] = [] for line_item in invoice.payment_line_items: line_items.append(PaymentLineItemModel.find_by_id(line_item.id)) cms_response = CFSService.create_cms(line_items=line_items, cfs_account=cfs_account) # TODO Create a payment record for this to show up on transactions, when the ticket comes. # Create a credit with CM identifier as CMs are not reported in payment interface file # until invoice is applied. CreditModel(cfs_identifier=cms_response.get('credit_memo_number'), is_credit_memo=True, amount=invoice.total, remaining_amount=invoice.total, account_id=invoice.payment_account_id).flush() # Add up the credit amount and update payment account table. payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id( invoice.payment_account_id) payment_account.credit = (payment_account.credit or 0) + invoice.total current_app.logger.info( f'Updating credit amount to {payment_account.credit} for account {payment_account.auth_account_id}' ) payment_account.flush()
def _create_pad_invoices(cls): # pylint: disable=too-many-locals """Create PAD invoices in to CFS system.""" # Find all accounts which have done a transaction with PAD transactions inv_subquery = db.session.query(InvoiceModel.payment_account_id) \ .filter(InvoiceModel.payment_method_code == PaymentMethod.PAD.value) \ .filter(InvoiceModel.invoice_status_code == InvoiceStatus.APPROVED.value).subquery() # Exclude the accounts which are in FREEZE state. pad_accounts: List[PaymentAccountModel] = db.session.query(PaymentAccountModel) \ .join(CfsAccountModel, CfsAccountModel.account_id == PaymentAccountModel.id) \ .filter(CfsAccountModel.status != CfsAccountStatus.FREEZE.value) \ .filter(PaymentAccountModel.id.in_(inv_subquery)).all() current_app.logger.info( f'Found {len(pad_accounts)} with PAD transactions.') invoice_ref_subquery = db.session.query(InvoiceReferenceModel.invoice_id). \ filter(InvoiceReferenceModel.status_code.in_((InvoiceReferenceStatus.ACTIVE.value,))) for account in pad_accounts: # Find all PAD invoices for this account account_invoices = db.session.query(InvoiceModel) \ .filter(InvoiceModel.payment_account_id == account.id) \ .filter(InvoiceModel.payment_method_code == PaymentMethod.PAD.value) \ .filter(InvoiceModel.invoice_status_code == InvoiceStatus.APPROVED.value) \ .filter(InvoiceModel.id.notin_(invoice_ref_subquery)) \ .order_by(InvoiceModel.created_on.desc()).all() # Get cfs account payment_account: PaymentAccountService = PaymentAccountService.find_by_id( account.id) current_app.logger.debug( f'Found {len(account_invoices)} invoices for account {payment_account.auth_account_id}' ) if len(account_invoices) == 0: continue cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id( payment_account.id) if cfs_account is None: # Get the last cfs_account for it, as the account might have got upgraded from PAD to DRAWDOWN. cfs_account: CfsAccountModel = CfsAccountModel.query.\ filter(CfsAccountModel.account_id == payment_account.id).order_by(CfsAccountModel.id.desc()).first() # If the CFS Account status is not ACTIVE or INACTIVE (for above case), raise error and continue if cfs_account.status not in (CfsAccountStatus.ACTIVE.value, CfsAccountStatus.INACTIVE.value): capture_message( f'CFS Account status is not ACTIVE. for account {payment_account.auth_account_id} ' f'is {payment_account.cfs_account_status}', level='error') current_app.logger.error( f'CFS status for account {payment_account.auth_account_id} ' f'is {payment_account.cfs_account_status}') continue # Add all lines together lines = [] invoice_total: float = 0 for invoice in account_invoices: lines.extend(invoice.payment_line_items) invoice_total += invoice.total invoice_number = account_invoices[-1].id try: # Get the first invoice id as the trx number for CFS invoice_response = CFSService.create_account_invoice( transaction_number=invoice_number, line_items=lines, cfs_account=cfs_account) except Exception as e: # NOQA # pylint: disable=broad-except # There is a chance that the error is a timeout from CAS side, # so to make sure we are not missing any data, make a GET call for the invoice we tried to create # and use it if it got created. current_app.logger.info( e ) # INFO is intentional as sentry alerted only after the following try/catch has_invoice_created: bool = False try: # add a 60 seconds delay here as safe bet, as CFS takes time to create the invoice and # since this is a job, delay doesn't cause any performance issue time.sleep(60) invoice_number = generate_transaction_number( str(invoice_number)) invoice_response = CFSService.get_invoice( cfs_account=cfs_account, inv_number=invoice_number) has_invoice_created = invoice_response.json().get( 'invoice_number', None) == invoice_number except Exception as exc: # NOQA # pylint: disable=broad-except,unused-variable # Ignore this error, as it is irrelevant and error on outer level is relevant. pass # If no invoice is created raise an error for sentry if not has_invoice_created: capture_message( f'Error on creating PAD invoice: account id={payment_account.id}, ' f'auth account : {payment_account.auth_account_id}, ERROR : {str(e)}', level='error') current_app.logger.error(e) continue # emit account mailer event mailer.publish_mailer_events('pad.invoiceCreated', payment_account, {'invoice_total': invoice_total}) # Iterate invoice and create invoice reference records for invoice in account_invoices: # Create invoice reference, payment record and a payment transaction InvoiceReference.create( invoice_id=invoice.id, invoice_number=invoice_response.json().get( 'invoice_number'), reference_number=invoice_response.json().get( 'pbc_ref_number', None)) # Misc invoice.cfs_account_id = payment_account.cfs_account_id # no longer set to settlement sceduled # invoice.invoice_status_code = InvoiceStatus.SETTLEMENT_SCHEDULED.value invoice.save()
def _create_rs_invoices(cls): # pylint: disable=too-many-locals """Create RS invoices in to CFS system.""" # Find all pending routing slips. # find all routing slip invoices [cash or cheque] # create invoices in csf # do the receipt apply invoices: List[InvoiceModel] = db.session.query(InvoiceModel) \ .join(RoutingSlipModel, RoutingSlipModel.number == InvoiceModel.routing_slip) \ .join(CfsAccountModel, CfsAccountModel.account_id == RoutingSlipModel.payment_account_id) \ .filter(InvoiceModel.payment_method_code == PaymentMethod.INTERNAL.value) \ .filter(InvoiceModel.invoice_status_code == InvoiceStatus.APPROVED.value) \ .filter(CfsAccountModel.status.in_([CfsAccountStatus.ACTIVE.value, CfsAccountStatus.FREEZE.value])) \ .filter(InvoiceModel.routing_slip is not None) \ .order_by(InvoiceModel.created_on.asc()).all() current_app.logger.info(f'Found {len(invoices)} to be created in CFS.') for invoice in invoices: # Create a CFS invoice current_app.logger.debug( f'Creating cfs invoice for invoice {invoice.id}') routing_slip = RoutingSlipModel.find_by_number( invoice.routing_slip) # If routing slip is not found in Pay-DB, assume legacy RS and move on to next one. if not routing_slip: continue routing_slip_payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id( routing_slip.payment_account_id) # apply invoice to the active CFS_ACCOUNT which will be the parent routing slip active_cfs_account = CfsAccountModel.find_effective_by_account_id( routing_slip_payment_account.id) invoice_response = CFSService.create_account_invoice( transaction_number=invoice.id, line_items=invoice.payment_line_items, cfs_account=active_cfs_account) invoice_number = invoice_response.json().get( 'invoice_number', None) current_app.logger.info( f'invoice_number {invoice_number} created in CFS.') has_error_in_apply_receipt = RoutingSlipTask.apply_routing_slips_to_invoice( routing_slip_payment_account, active_cfs_account, routing_slip, invoice, invoice_number) if has_error_in_apply_receipt: # move on to next invoice continue invoice_reference: InvoiceReference = InvoiceReference.create( invoice.id, invoice_number, invoice_response.json().get('pbc_ref_number', None)) current_app.logger.debug('>create_invoice') # leave the status as PAID invoice_reference.status_code = InvoiceReferenceStatus.COMPLETED.value invoice.invoice_status_code = InvoiceStatus.PAID.value invoice.paid = invoice.total Payment.create(payment_method=PaymentMethod.INTERNAL.value, payment_system=PaymentSystem.INTERNAL.value, payment_status=PaymentStatus.COMPLETED.value, invoice_number=invoice_reference.invoice_number, invoice_amount=invoice.total, payment_account_id=invoice.payment_account_id) invoice.save()
def _create_pad_invoices(cls): # pylint: disable=too-many-locals """Create PAD invoices in to CFS system.""" # Find all accounts which have done a transaction with PAD transactions inv_subquery = db.session.query(InvoiceModel.payment_account_id) \ .filter(InvoiceModel.payment_method_code == PaymentMethod.PAD.value) \ .filter(InvoiceModel.invoice_status_code == PaymentStatus.CREATED.value).subquery() # Exclude the accounts which are in FREEZE state. pad_accounts: List[PaymentAccountModel] = db.session.query(PaymentAccountModel) \ .join(CfsAccountModel, CfsAccountModel.account_id == PaymentAccountModel.id) \ .filter(CfsAccountModel.status != CfsAccountStatus.FREEZE.value) \ .filter(PaymentAccountModel.id.in_(inv_subquery)).all() current_app.logger.info( f'Found {len(pad_accounts)} with PAD transactions.') for account in pad_accounts: # Find all PAD invoices for this account account_invoices = db.session.query(InvoiceModel) \ .filter(InvoiceModel.payment_account_id == account.id) \ .filter(InvoiceModel.payment_method_code == PaymentMethod.PAD.value) \ .filter(InvoiceModel.invoice_status_code == InvoiceStatus.CREATED.value) \ .order_by(InvoiceModel.created_on.desc()).all() # Get cfs account payment_account: PaymentAccountService = PaymentAccountService.find_by_id( account.id) current_app.logger.debug( f'Found {len(account_invoices)} invoices for account {payment_account.auth_account_id}' ) if len(account_invoices) == 0: continue cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id( payment_account.id) if cfs_account is None: # Get the last invoice and look up cfs_account for it, as the account might have got upgraded. cfs_account = CfsAccountModel.find_by_id( account_invoices[0].cfs_account_id) # If the CFS Account status is not ACTIVE, raise error and continue if cfs_account.status not in (CfsAccountStatus.ACTIVE.value, CfsAccountStatus.INACTIVE.value): capture_message( f'CFS Account status is not ACTIVE. for account {payment_account.auth_account_id} ' f'is {payment_account.cfs_account_status}', level='error') current_app.logger.error( f'CFS status for account {payment_account.auth_account_id} ' f'is {payment_account.cfs_account_status}') continue # Add all lines together lines = [] invoice_total: float = 0 for invoice in account_invoices: lines.append(*invoice.payment_line_items) invoice_total += invoice.total try: # Get the first invoice id as the trx number for CFS invoice_response = CFSService.create_account_invoice( transaction_number=account_invoices[0].id, line_items=lines, payment_account=cfs_account) except Exception as e: # pylint: disable=broad-except capture_message( f'Error on creating PAD invoice: account id={payment_account.id}, ' f'auth account : {payment_account.auth_account_id}, ERROR : {str(e)}', level='error') current_app.logger.error(e) continue # emit account mailer event mailer.publish_mailer_events('pad.invoiceCreated', payment_account, {'invoice_total': invoice_total}) # Iterate invoice and create invoice reference records for invoice in account_invoices: # Create invoice reference, payment record and a payment transaction InvoiceReference.create( invoice_id=invoice.id, invoice_number=invoice_response.json().get( 'invoice_number'), reference_number=invoice_response.json().get( 'pbc_ref_number', None)) # Misc invoice.cfs_account_id = payment_account.cfs_account_id invoice.invoice_status_code = InvoiceStatus.SETTLEMENT_SCHEDULED.value invoice.save()
def link_routing_slips(cls): """Create invoice in CFS. Steps: 1. Find all pending rs with pending status. 1. Notify mailer """ routing_slips = db.session.query(RoutingSlipModel) \ .join(PaymentAccountModel, PaymentAccountModel.id == RoutingSlipModel.payment_account_id) \ .join(CfsAccountModel, CfsAccountModel.account_id == PaymentAccountModel.id) \ .filter(RoutingSlipModel.status == RoutingSlipStatus.LINKED.value) \ .filter(CfsAccountModel.status == CfsAccountStatus.ACTIVE.value).all() for routing_slip in routing_slips: # 1.reverse the child routing slip # 2.create receipt to the parent # 3.change the payment account of child to parent # 4. change the status try: current_app.logger.debug(f'Linking Routing Slip: {routing_slip.number}') payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id( routing_slip.payment_account_id) cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id( payment_account.id) # reverse routing slip receipt CFSService.reverse_rs_receipt_in_cfs(cfs_account, routing_slip.number) cfs_account.status = CfsAccountStatus.INACTIVE.value # apply receipt to parent cfs account parent_rs: RoutingSlipModel = RoutingSlipModel.find_by_number(routing_slip.parent_number) parent_payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id( parent_rs.payment_account_id) parent_cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id( parent_payment_account.id) # For linked routing slip receipts, append 'L' to the number to avoid duplicate error receipt_number = f'{routing_slip.number}L' CFSService.create_cfs_receipt(cfs_account=parent_cfs_account, rcpt_number=receipt_number, 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')) # Add to the list if parent is NSF, to apply the receipts. if parent_rs.status == RoutingSlipStatus.NSF.value: total_invoice_amount = cls._apply_routing_slips_to_pending_invoices(parent_rs) current_app.logger.debug(f'Total Invoice Amount : {total_invoice_amount}') # Update the parent routing slip status to ACTIVE parent_rs.status = RoutingSlipStatus.ACTIVE.value # linking routing slip balance is transferred ,so use the total parent_rs.remaining_amount = float(routing_slip.total) - total_invoice_amount routing_slip.save() except Exception as e: # NOQA # pylint: disable=broad-except capture_message( f'Error on Linking Routing Slip number:={routing_slip.number}, ' f'routing slip : {routing_slip.id}, ERROR : {str(e)}', level='error') current_app.logger.error(e) continue
def _create_cfs_account(cls, pending_account: CfsAccountModel, pay_account: PaymentAccountModel, auth_token: str): # If PAD Account creation in CFS is paused, then just continue # TODO Remove once PAD account bugs are fixed and stable on CAS side. if current_app.config.get('CFS_STOP_PAD_ACCOUNT_CREATION') and \ pay_account.payment_method == PaymentMethod.PAD.value: current_app.logger.info( 'Continuing to next record as CFS PAD account creation is stopped.' ) return current_app.logger.info( f'Creating pay system instance for {pay_account.payment_method} for account {pay_account.id}.' ) # For an existing CFS Account, call update.. This is to handle PAD update when CFS is offline try: account_contact = cls._get_account_contact( auth_token, pay_account.auth_account_id) contact_info: Dict[str, str] = { 'city': account_contact.get('city'), 'postalCode': account_contact.get('postalCode'), 'province': account_contact.get('region'), 'addressLine1': account_contact.get('street'), 'country': account_contact.get('country') } payment_info: Dict[str, any] = { 'bankInstitutionNumber': pending_account.bank_number, 'bankTransitNumber': pending_account.bank_branch_number, 'bankAccountNumber': pending_account.bank_account_number, 'bankAccountName': pay_account.name } if pending_account.cfs_account and pending_account.cfs_party and pending_account.cfs_site: # This means, PAD account details have changed. So update banking details for this CFS account bank_details = CFSService.update_bank_details( name=pay_account.auth_account_id, party_number=pending_account.cfs_party, account_number=pending_account.cfs_account, site_number=pending_account.cfs_site, payment_info=payment_info) pending_account.payment_instrument_number = bank_details.get( 'payment_instrument_number', None) else: # It's a new account, now create # If the account have banking information, then create a PAD account else a regular account. if pending_account.bank_number and pending_account.bank_branch_number \ and pending_account.bank_account_number: cfs_account_details = CFSService.create_cfs_account( identifier=pay_account.auth_account_id, contact_info=contact_info, payment_info=payment_info, receipt_method=RECEIPT_METHOD_PAD_DAILY) else: cfs_account_details = CFSService.create_cfs_account( identifier=pay_account.auth_account_id, contact_info=contact_info, receipt_method=None) pending_account.payment_instrument_number = cfs_account_details.get( 'payment_instrument_number', None) pending_account.cfs_account = cfs_account_details.get( 'account_number') pending_account.cfs_site = cfs_account_details.get( 'site_number') pending_account.cfs_party = cfs_account_details.get( 'party_number') except Exception as e: # NOQA # pylint: disable=broad-except # publish to mailer queue. is_user_error = False if pay_account.payment_method == PaymentMethod.PAD.value: is_user_error = CreateAccountTask._check_user_error(e.response) # pylint: disable=no-member capture_message( f'Error on creating CFS Account: account id={pay_account.id}, ' f'auth account : {pay_account.auth_account_id}, ERROR : {str(e)}', level='error') current_app.logger.error(e) pending_account.rollback() if is_user_error: capture_message( f'User Input needed for creating CFS Account: account id={pay_account.id}, ' f'auth account : {pay_account.auth_account_id}, ERROR : Invalid Bank Details', level='error') mailer.publish_mailer_events('PadSetupFailed', pay_account) pending_account.status = CfsAccountStatus.INACTIVE.value pending_account.save() return # If the account has an activation time set , # before that it shud be set to the PENDING_PAD_ACTIVATION status. is_account_in_pad_confirmation_period = pay_account.pad_activation_date is not None and \ pay_account.pad_activation_date > datetime.today() pending_account.status = CfsAccountStatus.PENDING_PAD_ACTIVATION.value if \ is_account_in_pad_confirmation_period else CfsAccountStatus.ACTIVE.value pending_account.save()
def create_accounts(cls): # pylint: disable=too-many-locals """Find all pending accounts to be created in CFS. Steps: 1. Find all pending CFS accounts. 2. Create CFS accounts. 3. Publish a message to the queue if successful. """ # Pass payment method if offline account creation has be restricted based on payment method. pending_accounts: List[ CfsAccountModel] = CfsAccountModel.find_all_pending_accounts() current_app.logger.info( f'Found {len(pending_accounts)} CFS Accounts to be created.') if len(pending_accounts) == 0: return auth_token = get_token() for pending_account in pending_accounts: # Find the payment account and create the pay system instance. pay_account: PaymentAccountModel = PaymentAccountModel.find_by_id( pending_account.account_id) # If PAD Account creation in CFS is paused, then just continue # TODO Remove once PAD account bugs are fixed and stable on CAS side. if current_app.config.get('CFS_STOP_PAD_ACCOUNT_CREATION') and \ pay_account.payment_method == PaymentMethod.PAD.value: current_app.logger.info( 'Continuing to next record as CFS PAD account creation is stopped.' ) continue current_app.logger.info( f'Creating pay system instance for {pay_account.payment_method} for account {pay_account.id}.' ) account_contact = cls._get_account_contact( auth_token, pay_account.auth_account_id) contact_info: Dict[str, str] = { 'city': account_contact.get('city'), 'postalCode': account_contact.get('postalCode'), 'province': account_contact.get('region'), 'addressLine1': account_contact.get('street'), 'country': account_contact.get('country') } payment_info: Dict[str, any] = { 'bankInstitutionNumber': pending_account.bank_number, 'bankTransitNumber': pending_account.bank_branch_number, 'bankAccountNumber': pending_account.bank_account_number, } account_name = pay_account.auth_account_name # For an existing CFS Account, call update.. This is to handle PAD update when CFS is offline try: if pending_account.cfs_account and pending_account.cfs_party and pending_account.cfs_site: # This means, PAD account details have changed. So update banking details for this CFS account bank_details = CFSService.update_bank_details( name=account_name, party_number=pending_account.cfs_party, account_number=pending_account.cfs_account, site_number=pending_account.cfs_site, payment_info=payment_info) pending_account.payment_instrument_number = bank_details.get( 'payment_instrument_number', None) else: # It's a new account, now create # If the account have banking information, then create a PAD account else a regular account. if pending_account.bank_number and pending_account.bank_branch_number \ and pending_account.bank_account_number: cfs_account_details = CFSService.create_cfs_account( name=account_name, contact_info=contact_info, payment_info=payment_info, receipt_method=RECEIPT_METHOD_PAD_DAILY) else: cfs_account_details = CFSService.create_cfs_account( name=account_name, contact_info=contact_info, receipt_method=None) pending_account.payment_instrument_number = cfs_account_details.get( 'payment_instrument_number', None) pending_account.cfs_account = cfs_account_details.get( 'account_number') pending_account.cfs_site = cfs_account_details.get( 'site_number') pending_account.cfs_party = cfs_account_details.get( 'party_number') except Exception as e: # NOQA # pylint: disable=broad-except # publish to mailer queue. mailer.publish_mailer_events('PadSetupFailed', pay_account) capture_message( f'Error on creating CFS Account: account id={pay_account.id}, ' f'auth account : {pay_account.auth_account_id}, ERROR : {str(e)}', level='error') current_app.logger.error(e) pending_account.rollback() # pending_account.status = CfsAccountStatus.INACTIVE.value # pending_account.save() continue # If the account has an activation time set , # before that it shud be set to the PENDING_PAD_ACTIVATION status. is_account_in_pad_confirmation_period = pay_account.pad_activation_date is not None and \ pay_account.pad_activation_date > datetime.today() pending_account.status = CfsAccountStatus.PENDING_PAD_ACTIVATION.value if \ is_account_in_pad_confirmation_period else CfsAccountStatus.ACTIVE.value pending_account.save()
# 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 CFS service layer. Test-Suite to ensure that the CFS Service layer is working as expected. """ from unittest.mock import patch from requests import ConnectTimeout from pay_api.services.cfs_service import CFSService cfs_service = CFSService() def test_validate_bank_account_valid(session): """Test create_account.""" input_bank_details = { 'bankInstitutionNumber': '2001', 'bankTransitNumber': '00720', 'bankAccountNumber': '1234567', } with patch('pay_api.services.oauth_service.requests.post') as mock_post: # Configure the mock to return a response with an OK status code. mock_post.return_value.ok = True mock_post.return_value.status_code = 200 valid_address = { 'bank_number': '0001',
def process_nsf(cls): """Process NSF routing slips. Steps: 1. Find all routing slips with NSF status. 2. Reverse the receipt for the NSF routing slips. 3. Add an invoice for NSF fees. """ routing_slips: List[RoutingSlipModel] = db.session.query(RoutingSlipModel) \ .join(PaymentAccountModel, PaymentAccountModel.id == RoutingSlipModel.payment_account_id) \ .join(CfsAccountModel, CfsAccountModel.account_id == PaymentAccountModel.id) \ .filter(RoutingSlipModel.status == RoutingSlipStatus.NSF.value) \ .filter(CfsAccountModel.status == CfsAccountStatus.ACTIVE.value).all() current_app.logger.info(f'Found {len(routing_slips)} to process NSF.') for routing_slip in routing_slips: # 1. Reverse the routing slip receipt. # 2. Reverse all the child receipts. # 3. Change the CFS Account status to FREEZE. try: current_app.logger.debug(f'Reverse receipt {routing_slip.number}') payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id(routing_slip.payment_account_id) cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id(payment_account.id) # Find all child routing slip and reverse it, as all linked routing slips are also considered as NSF. child_routing_slips: List[RoutingSlipModel] = RoutingSlipModel.find_children(routing_slip.number) for rs in (routing_slip, *child_routing_slips): receipt_number = rs.number if rs.parent_number: receipt_number = f'{receipt_number}L' CFSService.reverse_rs_receipt_in_cfs(cfs_account, receipt_number, is_nsf=True) for payment in db.session.query(PaymentModel) \ .filter(PaymentModel.receipt_number == receipt_number).all(): payment.payment_status_code = PaymentStatus.FAILED.value # Update the CFS Account status to FREEZE. cfs_account.status = CfsAccountStatus.FREEZE.value # Update all invoice status to CREATED. invoices: List[InvoiceModel] = db.session.query(InvoiceModel) \ .filter(InvoiceModel.routing_slip == routing_slip.number) \ .filter(InvoiceModel.invoice_status_code == InvoiceStatus.PAID.value) \ .all() for inv in invoices: # Reset the statuses inv.invoice_status_code = InvoiceStatus.CREATED.value inv_ref = InvoiceReferenceModel.find_reference_by_invoice_id_and_status( inv.id, InvoiceReferenceStatus.COMPLETED.value ) inv_ref.status_code = InvoiceReferenceStatus.ACTIVE.value # Delete receipts as receipts are reversed in CFS. for receipt in ReceiptModel.find_all_receipts_for_invoice(inv.id): db.session.delete(receipt) inv = cls._create_nsf_invoice(cfs_account, routing_slip.number, payment_account) # Reduce the NSF fee from remaining amount. routing_slip.remaining_amount = float(routing_slip.remaining_amount) - inv.total routing_slip.save() except Exception as e: # NOQA # pylint: disable=broad-except capture_message( f'Error on Processing NSF for :={routing_slip.number}, ' f'routing slip : {routing_slip.id}, ERROR : {str(e)}', level='error') current_app.logger.error(e) continue
def _consolidate_payments(cls, auth_account_id: str, failed_payments: List[PaymentModel]) -> Payment: # If the payment is for consolidating failed payments, # 1. Cancel the invoices in CFS # 2. Update status of invoice_reference to CANCELLED # 3. Create new consolidated invoice in CFS. # 4. Create new invoice reference records. # 5. Create new payment records for the invoice as CREATED. pay_account = PaymentAccountService.find_by_auth_account_id( auth_account_id) consolidated_invoices: List[InvoiceModel] = [] consolidated_line_items: List[PaymentLineItem] = [] invoice_total: float = 0 for failed_payment in failed_payments: # Reverse invoice balance CFSService.reverse_invoice( inv_number=failed_payment.invoice_number) # Find all invoices for this payment. # Add all line items to the array for invoice in InvoiceModel.find_invoices_for_payment( payment_id=failed_payment.id): consolidated_invoices.append(invoice) invoice_total += invoice.total consolidated_line_items.append(*invoice.payment_line_items) # Create consolidated invoice invoice_response = CFSService.create_account_invoice( transaction_number=str(consolidated_invoices[-1].id) + '-C', line_items=consolidated_line_items, payment_account=pay_account) invoice_number: str = invoice_response.json().get('invoice_number') # Mark all invoice references to status CANCELLED, and create a new one for the new invoice number. for invoice in consolidated_invoices: inv_ref: InvoiceReferenceModel = InvoiceReferenceModel.find_reference_by_invoice_id_and_status( invoice_id=invoice.id, status_code=InvoiceReferenceStatus.ACTIVE.value) inv_ref.status_code = InvoiceReferenceStatus.CANCELLED.value # print(inv_ref) InvoiceReferenceModel( invoice_id=invoice.id, status_code=InvoiceReferenceStatus.ACTIVE.value, invoice_number=invoice_number, reference_number=invoice_response.json().get( 'pbc_ref_number')).flush() payment = Payment.create(payment_method=PaymentMethod.CC.value, payment_system=PaymentSystem.PAYBC.value, invoice_number=invoice_number, invoice_amount=invoice_total, payment_account_id=pay_account.id) # Update all failed payment with consolidated invoice number. for failed_payment in failed_payments: failed_payment.cons_inv_number = invoice_number # If the payment status is CREATED, which means we consolidated one payment which was already consolidated. # Set the status as DELETED if failed_payment.payment_status_code == PaymentStatus.CREATED.value: failed_payment.payment_status_code = PaymentStatus.DELETED.value # commit transaction BaseModel.commit() return payment