def test_activate_bcol_change_to_pad(session): """Test Activate account.""" # Create a pending account first, then call the job account = factory_create_pad_account( auth_account_id='1', payment_method=PaymentMethod.DRAWDOWN.value) CreateAccountTask.create_accounts() account = PaymentAccount.find_by_id(account.id) cfs_account: CfsAccount = CfsAccount.find_effective_by_account_id( account.id) assert cfs_account.status == CfsAccountStatus.PENDING_PAD_ACTIVATION.value, 'Created account has pending pad status' assert account.payment_method == PaymentMethod.DRAWDOWN.value ActivatePadAccountTask.activate_pad_accounts() cfs_account: CfsAccount = CfsAccount.find_effective_by_account_id( account.id) assert cfs_account.status == CfsAccountStatus.PENDING_PAD_ACTIVATION.value, \ 'Same day Job runs and shouldnt change anything.' account = PaymentAccount.find_by_id(account.id) assert account.payment_method == PaymentMethod.DRAWDOWN.value time_delay = current_app.config['PAD_CONFIRMATION_PERIOD_IN_DAYS'] with freeze_time(datetime.today() + timedelta(days=time_delay, minutes=1)): ActivatePadAccountTask.activate_pad_accounts() assert cfs_account.status == CfsAccountStatus.ACTIVE.value, \ 'After the confirmation period is over , status should be active' account = PaymentAccount.find_by_id(account.id) assert account.payment_method == PaymentMethod.PAD.value
def test_get_statement_report_for_empty_invoices(session): """Assert that the get statement report works for statement with no invoices.""" bcol_account = factory_premium_payment_account() bcol_account.save() payment = factory_payment() payment.save() i = factory_invoice(payment_account=bcol_account) i.save() factory_invoice_reference(i.id).save() factory_payment_line_item(invoice_id=i.id, fee_schedule_id=1).save() settings_model = factory_statement_settings( payment_account_id=bcol_account.id, frequency=StatementFrequency.DAILY.value) statement_model = factory_statement( payment_account_id=bcol_account.id, frequency=StatementFrequency.DAILY.value, statement_settings_id=settings_model.id) payment_account = PaymentAccount.find_by_id(bcol_account.id) statements = StatementService.find_by_account_id( payment_account.auth_account_id, page=1, limit=10) assert statements is not None report_response, report_name = StatementService.get_statement_report( statement_id=statement_model.id, content_type='application/pdf', auth=get_auth_premium_user()) assert report_response is not None
def test_statement_find_by_account(session): """Assert that the statement settings by id works.""" bcol_account = factory_premium_payment_account() bcol_account.save() payment = factory_payment() payment.save() i = factory_invoice(payment_account=bcol_account) i.save() factory_invoice_reference(i.id).save() settings_model = factory_statement_settings( payment_account_id=bcol_account.id, frequency=StatementFrequency.DAILY.value) statement_model = factory_statement( payment_account_id=bcol_account.id, frequency=StatementFrequency.DAILY.value, statement_settings_id=settings_model.id) factory_statement_invoices(statement_id=statement_model.id, invoice_id=i.id) payment_account = PaymentAccount.find_by_id(bcol_account.id) statements = StatementService.find_by_account_id( payment_account.auth_account_id, page=1, limit=10) assert statements is not None assert statements.get('total') == 1
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 pay_account.payment_method in (PaymentMethod.CASH.value, PaymentMethod.CHEQUE.value): routing_slip.create_cfs_account(pending_account, pay_account) else: cls._create_cfs_account(pending_account, pay_account, auth_token)
def test_update_statement_daily_to_daily(session): """Assert that going from daily to daily creates a new record.""" bcol_account = factory_premium_payment_account() bcol_account.save() payment = factory_payment() payment.save() i = factory_invoice(payment_account=bcol_account) i.save() factory_invoice_reference(i.id).save() factory_statement_settings(payment_account_id=bcol_account.id, frequency=StatementFrequency.DAILY.value) # update to weekly payment_account = PaymentAccount.find_by_id(bcol_account.id) statement_settings = StatementSettingsService.update_statement_settings(payment_account.auth_account_id, StatementFrequency.DAILY.value) assert statement_settings is not None assert statement_settings.get('frequency') == StatementFrequency.DAILY.value assert statement_settings.get('to_date') is None # daily to daily - assert daily should start by tomorow assert statement_settings.get('from_date') == (datetime.today() + timedelta(days=1)).strftime('%Y-%m-%d') # daily to daily - assert current active one is stil daily ending today current_statement_settings = StatementSettingsModel.find_active_settings(payment_account.auth_account_id, datetime.today()) assert current_statement_settings is not None assert current_statement_settings.frequency == StatementFrequency.DAILY.value assert current_statement_settings.to_date == datetime.today().date()
def test_get_monthly_statement_report_as_csv(session, client, jwt, app): """Assert that the monthly statement report is returning response.""" # Create a payment account and statement details, then get all statements for the account token = jwt.create_jwt(get_claims(), token_header) headers = { 'Authorization': f'Bearer {token}', 'content-type': 'application/json', 'Accept': ContentType.CSV.value } rv = client.post('/api/v1/payment-requests', data=json.dumps( get_payment_request(business_identifier='CP0002000')), headers=headers) payment: Payment = Payment.find_by_id(rv.json.get('id')) bcol_account: BcolPaymentAccount = BcolPaymentAccount.find_by_id( payment.invoices[0].bcol_account_id) pay_account: PaymentAccount = PaymentAccount.find_by_id( bcol_account.account_id) settings_model = factory_statement_settings( payment_account_id=pay_account.id, frequency=StatementFrequency.DAILY.value) statement_model = factory_statement( payment_account_id=pay_account.id, frequency=StatementFrequency.DAILY.value, statement_settings_id=settings_model.id) factory_statement_invoices(statement_id=statement_model.id, invoice_id=payment.invoices[0].id) rv = client.get( f'/api/v1/accounts/{pay_account.auth_account_id}/statements/{statement_model.id}', headers=headers) assert rv.status_code == 200
def test_update_pad_account(session): """Test update account.""" # Create a pending account first, then call the job account = factory_create_pad_account(auth_account_id='2') CreateAccountTask.create_accounts() account = PaymentAccount.find_by_id(account.id) cfs_account = CfsAccount.find_effective_by_account_id(account.id) assert cfs_account.payment_instrument_number # Now update the account. new_payment_details = { 'bankInstitutionNumber': '111', 'bankTransitNumber': '222', 'bankAccountNumber': '3333333333' } PadService().update_account(name='Test', cfs_account=cfs_account, payment_info=new_payment_details) cfs_account = CfsAccount.find_by_id(cfs_account.id) # Run the job again CreateAccountTask.create_accounts() updated_cfs_account: CfsAccount = CfsAccount.find_effective_by_account_id(account.id) assert updated_cfs_account.id != cfs_account.id assert updated_cfs_account.bank_account_number == new_payment_details.get('bankAccountNumber') assert updated_cfs_account.bank_branch_number == new_payment_details.get('bankTransitNumber') assert updated_cfs_account.bank_number == new_payment_details.get('bankInstitutionNumber') assert cfs_account.status == CfsAccountStatus.INACTIVE.value assert updated_cfs_account.status == CfsAccountStatus.PENDING_PAD_ACTIVATION.value assert updated_cfs_account.payment_instrument_number
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 _notify_for_ob(cls): # pylint: disable=too-many-locals """Notify for online banking. 1) Find the accounts with pending invoices 2) get total remaining for that account """ unpaid_status = (InvoiceStatus.SETTLEMENT_SCHEDULED.value, InvoiceStatus.PARTIAL.value, InvoiceStatus.CREATED.value) notification_date = datetime.today() - timedelta( days=current_app.config.get('NOTIFY_AFTER_DAYS')) # Get distinct accounts with pending invoices for that exact day notification_pending_accounts = db.session.query( InvoiceModel.payment_account_id ).distinct().filter( and_( InvoiceModel.invoice_status_code.in_(unpaid_status), InvoiceModel.payment_method_code == PaymentMethod.ONLINE_BANKING.value, # cast is used to get the exact match stripping the timestamp from date cast(InvoiceModel.created_on, Date) == notification_date.date())).all() current_app.logger.debug( f'Found {len(notification_pending_accounts)} invoices to notify admins.' ) for payment_account in notification_pending_accounts: try: payment_account_id = payment_account[0] total = db.session.query( func.sum(InvoiceModel.total).label('total') ).filter( and_( InvoiceModel.invoice_status_code.in_(unpaid_status), InvoiceModel.payment_account_id == payment_account_id, InvoiceModel.payment_method_code == PaymentMethod.ONLINE_BANKING.value)).group_by( InvoiceModel.payment_account_id).all() pay_account: PaymentAccountModel = \ PaymentAccountModel.find_by_id(payment_account_id) cfs_account = CfsAccountModel.find_effective_by_account_id( payment_account_id) # emit account mailer event addition_params_to_mailer = { 'transactionAmount': total[0][0], 'cfsAccountId': cfs_account.cfs_account, 'authAccountId': pay_account.auth_account_id, } mailer.publish_mailer_events('ob.outstandingInvoice', pay_account, addition_params_to_mailer) except Exception as e: # NOQA # pylint: disable=broad-except capture_message( f'Error on notifying mailer OB Pending invoice: account id={pay_account.id}, ' f'auth account : {pay_account.auth_account_id}, ERROR : {str(e)}', level='error') current_app.logger.error(e)
def find_by_id(cls, account_id: int): """Find pay account by id.""" current_app.logger.debug('<find_by_id') account = PaymentAccount() account._dao = PaymentAccountModel.find_by_id(account_id) # pylint: disable=protected-access current_app.logger.debug('>find_by_id') return account
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 find_by_id(account_id: int): """Find account by id.""" account_dao = PaymentAccountModel.find_by_id(account_id) if not account_dao: raise BusinessException(Error.PAY009) account = PaymentAccount() account._dao = account_dao # pylint: disable=protected-access current_app.logger.debug('>find_by_id') return account
def test_create_pad_account(session): """Test create account.""" # Create a pending account first, then call the job account = factory_create_pad_account(auth_account_id='1') CreateAccountTask.create_accounts() account = PaymentAccount.find_by_id(account.id) cfs_account: CfsAccount = CfsAccount.find_effective_by_account_id(account.id) assert cfs_account.status == CfsAccountStatus.PENDING_PAD_ACTIVATION.value assert cfs_account.bank_account_number assert cfs_account.cfs_party assert cfs_account.cfs_site assert cfs_account.cfs_account assert cfs_account.payment_instrument_number
def test_create_online_banking_account(session): """Test create account.""" # Create a pending account first, then call the job account = factory_create_online_banking_account(auth_account_id='2') CreateAccountTask.create_accounts() account = PaymentAccount.find_by_id(account.id) cfs_account = CfsAccount.find_effective_by_account_id(account.id) assert cfs_account.status == CfsAccountStatus.ACTIVE.value assert not cfs_account.bank_account_number assert cfs_account.cfs_party assert cfs_account.cfs_site assert cfs_account.cfs_account assert cfs_account.payment_instrument_number is None
def test_create_pad_account_no_confirmation_period(session): """Test create account.Arbitrary scenario when there is no confirmation period.""" # Create a pending account first, then call the job account = factory_create_pad_account(auth_account_id='1', confirmation_period=0) CreateAccountTask.create_accounts() account = PaymentAccount.find_by_id(account.id) cfs_account: CfsAccount = CfsAccount.find_effective_by_account_id(account.id) assert cfs_account.status == CfsAccountStatus.ACTIVE.value assert cfs_account.bank_account_number assert cfs_account.cfs_party assert cfs_account.cfs_site assert cfs_account.cfs_account assert cfs_account.payment_instrument_number
def test_update_online_banking_account(session): """Test update account.""" # Create a pending account first, then call the job account = factory_create_online_banking_account(auth_account_id='2') CreateAccountTask.create_accounts() account = PaymentAccount.find_by_id(account.id) cfs_account = CfsAccount.find_effective_by_account_id(account.id) # Update account, which shouldn't change any details OnlineBankingService().update_account(name='Test', cfs_account=cfs_account, payment_info=None) updated_cfs_account = CfsAccount.find_effective_by_account_id(account.id) assert updated_cfs_account.status == CfsAccountStatus.ACTIVE.value assert cfs_account.id == updated_cfs_account.id
def _publish_refund_to_mailer(invoice: InvoiceModel): """Construct message and send to mailer queue.""" from .payment_transaction import publish_response # pylint:disable=import-outside-toplevel,cyclic-import receipt: ReceiptModel = ReceiptModel.find_by_invoice_id_and_receipt_number( invoice_id=invoice.id) invoice_ref: InvoiceReferenceModel = InvoiceReferenceModel.find_reference_by_invoice_id_and_status( invoice_id=invoice.id, status_code=InvoiceReferenceStatus.COMPLETED.value) payment_transaction: PaymentTransactionModel = PaymentTransactionModel.find_recent_completed_by_invoice_id( invoice_id=invoice.id) message_type: str = f'bc.registry.payment.{invoice.payment_method_code.lower()}.refundRequest' transaction_date_time = receipt.receipt_date if invoice.payment_method_code == PaymentMethod.DRAWDOWN.value \ else payment_transaction.transaction_end_time filing_description = '' for line_item in invoice.payment_line_items: if filing_description: filing_description += ',' filing_description += line_item.description q_payload = dict( specversion='1.x-wip', type=message_type, source= f'https://api.pay.bcregistry.gov.bc.ca/v1/invoices/{invoice.id}', id=invoice.id, datacontenttype='application/json', data=dict(identifier=invoice.business_identifier, orderNumber=receipt.receipt_number, transactionDateTime=get_local_formatted_date_time( transaction_date_time), transactionAmount=receipt.receipt_amount, transactionId=invoice_ref.invoice_number, refundDate=get_local_formatted_date_time( datetime.now(), '%Y%m%d'), filingDescription=filing_description)) if invoice.payment_method_code == PaymentMethod.DRAWDOWN.value: payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id( invoice.payment_account_id) q_payload['data'].update( dict(bcolAccount=invoice.bcol_account, bcolUser=payment_account.bcol_user_id)) current_app.logger.debug( f'Publishing payment refund request to mailer for {invoice.id} : {q_payload}' ) publish_response( payload=q_payload, client_name=current_app.config.get('NATS_MAILER_CLIENT_NAME'), subject=current_app.config.get('NATS_MAILER_SUBJECT'))
def test_statement_settings_find_by_account(session): """Assert that the statement settings by id works.""" bcol_account = factory_premium_payment_account() bcol_account.save() payment = factory_payment() payment.save() i = factory_invoice(payment_account=bcol_account) i.save() factory_invoice_reference(i.id).save() factory_statement_settings(payment_account_id=bcol_account.id, frequency=StatementFrequency.DAILY.value) payment_account = PaymentAccount.find_by_id(bcol_account.id) statement_settings = StatementSettingsService.find_by_account_id(payment_account.auth_account_id) assert statement_settings is not None assert statement_settings.get('current_frequency').get('frequency') == StatementFrequency.DAILY.value
def test_link_rs(session): """Test link routing slip.""" child_rs_number = '1234' parent_rs_number = '89799' factory_routing_slip_account(number=child_rs_number, status=CfsAccountStatus.ACTIVE.value) factory_routing_slip_account(number=parent_rs_number, status=CfsAccountStatus.ACTIVE.value) child_rs = RoutingSlipModel.find_by_number(child_rs_number) parent_rs = RoutingSlipModel.find_by_number(parent_rs_number) # Do Link child_rs.status = RoutingSlipStatus.LINKED.value child_rs.parent_number = parent_rs.number child_rs.save() payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id( child_rs.payment_account_id) cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id( payment_account.id) with patch('pay_api.services.CFSService.reverse_rs_receipt_in_cfs' ) as mock_cfs_reverse: with patch('pay_api.services.CFSService.create_cfs_receipt' ) as mock_create_cfs: RoutingSlipTask.link_routing_slips() mock_cfs_reverse.assert_called() mock_cfs_reverse.assert_called_with(cfs_account, child_rs.number) mock_create_cfs.assert_called() # child_rs = RoutingSlipModel.find_by_number(child_rs_number) # parent_rs = RoutingSlipModel.find_by_number(parent_rs_number) # PS This has changed, no longer updating child rs payment account with parent. # assert child_rs.payment_account_id == parent_rs.payment_account_id cfs_account: CfsAccountModel = CfsAccountModel.find_by_id(cfs_account.id) assert cfs_account.status == CfsAccountStatus.INACTIVE.value # make sure next invocation doesnt fetch any records with patch('pay_api.services.CFSService.reverse_rs_receipt_in_cfs' ) as mock_cfs_reverse: with patch('pay_api.services.CFSService.create_cfs_receipt' ) as mock_create_cfs: RoutingSlipTask.link_routing_slips() mock_cfs_reverse.assert_not_called() mock_create_cfs.assert_not_called()
def test_update_statement_monthly(session): """Assert that the statement settings by id works.""" bcol_account = factory_premium_payment_account() bcol_account.save() payment = factory_payment() payment.save() i = factory_invoice(payment_account=bcol_account) i.save() factory_invoice_reference(i.id).save() factory_statement_settings(payment_account_id=bcol_account.id, frequency=StatementFrequency.MONTHLY.value) # update to weekly payment_account = PaymentAccount.find_by_id(bcol_account.id) statement_settings = StatementSettingsService.update_statement_settings( payment_account.auth_account_id, StatementFrequency.WEEKLY.value) assert statement_settings is not None assert statement_settings.get( 'frequency') == StatementFrequency.WEEKLY.value assert statement_settings.get('to_date') is None # monthly to weekly - assert weekly should start by next week first day end_of_month_date = get_first_and_last_dates_of_month( datetime.today().month, datetime.today().year)[1] assert statement_settings.get('from_date') == ( end_of_month_date + timedelta(days=1)).strftime(DT_SHORT_FORMAT) # monthly to weekly - assert current active one is stil monthly ending end of the week current_statement_settings = StatementSettingsModel.find_active_settings( payment_account.auth_account_id, datetime.today()) assert current_statement_settings is not None assert current_statement_settings.frequency == StatementFrequency.MONTHLY.value assert current_statement_settings.to_date == end_of_month_date.date() # travel to next week and see whats active with freeze_time(end_of_month_date + timedelta(days=2)): next_week_statement_settings = StatementSettingsModel.find_active_settings( payment_account.auth_account_id, datetime.today()) assert next_week_statement_settings is not None assert next_week_statement_settings.frequency == StatementFrequency.WEEKLY.value assert next_week_statement_settings.to_date is None
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 test_create_pad_account_system_error(session): """Test create account.""" # Create a pending account first, then call the job account = factory_create_pad_account(auth_account_id='1') cfs_account: CfsAccount = CfsAccount.find_effective_by_account_id( account.id) assert cfs_account.status == CfsAccountStatus.PENDING.value mock_response = requests.models.Response() mock_response.headers['CAS-Returned-Messages'] = '[CFS Down]' mock_response.status_code = 404 side_effect = HTTPError(response=mock_response) with patch.object(mailer, 'publish_mailer_events') as mock_mailer: with patch('pay_api.services.CFSService.create_cfs_account', side_effect=side_effect): CreateAccountTask.create_accounts() mock_mailer.assert_not_called() account = PaymentAccount.find_by_id(account.id) cfs_account: CfsAccount = CfsAccount.find_by_id(cfs_account.id) assert cfs_account.status == CfsAccountStatus.PENDING.value
def test_get_daily_statements_verify_order(session, client, jwt, app): """Assert that the default statement setting is daily.""" # Create a payment account and statement details, then get all statements for the account token = jwt.create_jwt(get_claims(), token_header) headers = { 'Authorization': f'Bearer {token}', 'content-type': 'application/json' } rv = client.post('/api/v1/payment-requests', data=json.dumps( get_payment_request(business_identifier='CP0002000')), headers=headers) invoice: Invoice = Invoice.find_by_id(rv.json.get('id')) pay_account: PaymentAccount = PaymentAccount.find_by_id( invoice.payment_account_id) settings_model = factory_statement_settings( payment_account_id=pay_account.id, frequency=StatementFrequency.DAILY.value) factory_statement(payment_account_id=pay_account.id, frequency=StatementFrequency.DAILY.value, statement_settings_id=settings_model.id) factory_statement(payment_account_id=pay_account.id, frequency=StatementFrequency.WEEKLY.value, statement_settings_id=settings_model.id) rv = client.get( f'/api/v1/accounts/{pay_account.auth_account_id}/statements', headers=headers) assert rv.status_code == 200 assert rv.json.get('total') == 2 # should come in the order latest first assert rv.json.get('items')[0].get( 'frequency') == StatementFrequency.WEEKLY.value assert rv.json.get('items')[1].get( 'frequency') == StatementFrequency.DAILY.value
def activate_pad_accounts(cls): """Find all accounts with pending account activation status. Steps: 1. Find all accounts with pending PAD account activation status. 2. Activate them. """ pending_pad_activation_accounts: List[ CfsAccountModel] = CfsAccountModel.find_all_accounts_with_status( status=CfsAccountStatus.PENDING_PAD_ACTIVATION.value) current_app.logger.info( f'Found {len(pending_pad_activation_accounts)} CFS Accounts to be pending PAD activation.' ) if len(pending_pad_activation_accounts) == 0: return for pending_account in pending_pad_activation_accounts: pay_account: PaymentAccountModel = PaymentAccountModel.find_by_id( pending_account.account_id) # check is still in the pad activation period is_activation_period_over = pay_account.pad_activation_date < datetime.now( ) current_app.logger.info( f'Account {pay_account.id} ready for activation:{is_activation_period_over}' ) if is_activation_period_over: pending_account.status = CfsAccountStatus.ACTIVE.value pending_account.save() # If account was in BCOL , change it to PAD if pay_account.payment_method != PaymentMethod.PAD.value: pay_account.payment_method = PaymentMethod.PAD.value pay_account.save() mailer.publish_mailer_events('confirmationPeriodOver', pay_account)
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_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 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 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 create_invoice_pdf(identifier: int, **kwargs) -> Tuple: """Find invoice by id.""" invoice_dao: InvoiceModel = InvoiceModel.find_by_id(identifier) if not invoice_dao: raise BusinessException(Error.INVALID_INVOICE_ID) payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id( invoice_dao.payment_account_id) cfs_account: CfsAccountModel = CfsAccountModel.find_by_id( invoice_dao.cfs_account_id) org_response = OAuthService.get( current_app.config.get('AUTH_API_ENDPOINT') + f'orgs/{payment_account.auth_account_id}', kwargs['user'].bearer_token, AuthHeaderType.BEARER, ContentType.JSON).json() org_contact_response = OAuthService.get( current_app.config.get('AUTH_API_ENDPOINT') + f'orgs/{payment_account.auth_account_id}/contacts', kwargs['user'].bearer_token, AuthHeaderType.BEARER, ContentType.JSON).json() org_contact = org_contact_response.get( 'contacts')[0] if org_contact_response.get('contacts', None) else {} invoice_number: str = invoice_dao.references[0].invoice_number if invoice_dao.references \ else generate_transaction_number(invoice_dao.id) filing_types: List[Dict[str, str]] = [] for line_item in invoice_dao.payment_line_items: business_identifier = invoice_dao.business_identifier \ if not invoice_dao.business_identifier.startswith('T') \ else '' filing_types.append({ 'folioNumber': invoice_dao.folio_number, 'description': line_item.description, 'businessIdentifier': business_identifier, 'createdOn': get_local_formatted_date(invoice_dao.created_on), 'filingTypeCode': line_item.fee_schedule.filing_type_code, 'fee': line_item.total, 'gst': line_item.gst, 'serviceFees': line_item.service_fees, 'total': line_item.total + line_item.service_fees }) template_vars: Dict[str, any] = { 'invoiceNumber': invoice_number, 'createdOn': get_local_formatted_date(invoice_dao.created_on), 'accountNumber': cfs_account.cfs_account if cfs_account else None, 'total': invoice_dao.total, 'gst': 0, 'serviceFees': invoice_dao.service_fees, 'fees': invoice_dao.total - invoice_dao.service_fees, 'filingTypes': filing_types, 'accountContact': { 'name': org_response.get('name'), 'city': org_contact.get('city', None), 'country': org_contact.get('country', None), 'postalCode': org_contact.get('postalCode', None), 'region': org_contact.get('region', None), 'street': org_contact.get('street', None), 'streetAdditional': org_contact.get('streetAdditional', None) } } invoice_pdf_dict = { 'templateName': 'invoice', 'reportName': invoice_number, 'templateVars': template_vars } current_app.logger.info('Invoice PDF Dict %s', invoice_pdf_dict) pdf_response = OAuthService.post( current_app.config.get('REPORT_API_BASE_URL'), kwargs['user'].bearer_token, AuthHeaderType.BEARER, ContentType.JSON, invoice_pdf_dict) current_app.logger.debug('<OAuthService responded to receipt.py') return pdf_response, invoice_pdf_dict.get('reportName')