def test_create_pad_account_to_drawdown(session): """Assert updating PAD to DRAWDOWN works.""" # Create a PAD Account first pad_account = PaymentAccountService.create(get_unlinked_pad_account_payload()) # Update this payment account with drawdown and assert payment method bcol_account = PaymentAccountService.update(pad_account.auth_account_id, get_premium_account_payload()) assert bcol_account.auth_account_id == bcol_account.auth_account_id assert bcol_account.payment_method == PaymentMethod.DRAWDOWN.value
def test_account_find_by_invalid_id(session): """Invalid account test.""" import pytest from pay_api.exceptions import BusinessException from pay_api.utils.errors import Error with pytest.raises(BusinessException) as excinfo: PaymentAccountService.find_by_id(999) assert excinfo.value.status == Error.PAY009.status
def test_update_online_banking_to_credit(session): """Assert that update from online banking to credit card works.""" online_banking_account = PaymentAccountService.create( get_basic_account_payload( payment_method=PaymentMethod.ONLINE_BANKING.value)) credit_account = PaymentAccountService.update( online_banking_account.auth_account_id, get_basic_account_payload()) assert credit_account.payment_method == PaymentMethod.DIRECT_PAY.value
def test_delete_account(session, payload): """Assert that delete payment account works.""" pay_account: PaymentAccountService = PaymentAccountService.create(payload) PaymentAccountService.delete_account(payload.get('accountId')) # Try to find the account by id. pay_account = PaymentAccountService.find_by_id(pay_account.id) for cfs_account in CfsAccountModel.find_by_account_id(pay_account.id): assert cfs_account.status == CfsAccountStatus.INACTIVE.value if cfs_account else True
def test_update_credit_to_online_banking(session): """Assert that update from credit card to online banking works.""" credit_account = PaymentAccountService.create(get_basic_account_payload()) online_banking_account = PaymentAccountService.update(credit_account.auth_account_id, get_basic_account_payload( payment_method=PaymentMethod.ONLINE_BANKING.value)) assert online_banking_account.payment_method == PaymentMethod.ONLINE_BANKING.value assert online_banking_account.cfs_account_id assert online_banking_account.cfs_account is None assert online_banking_account.cfs_party is None assert online_banking_account.cfs_site is None assert online_banking_account.bank_number is None
def test_account_invalid_lookup(session): """Invalid account test.""" p = PaymentAccountService.find_account('1234', 'CP', 'PAYBC') assert p is not None assert p.id is None import pytest from pay_api.exceptions import BusinessException from pay_api.utils.errors import Error with pytest.raises(BusinessException) as excinfo: PaymentAccountService.find_account(None, None, None) assert excinfo.value.status == Error.PAY004.status
def test_create_bcol_account_to_pad(session): """Assert that update from BCOL to PAD works.""" # Create a DRAWDOWN Account first bcol_account = PaymentAccountService.create(get_premium_account_payload()) # Update to PAD pad_account = PaymentAccountService.update(bcol_account.auth_account_id, get_unlinked_pad_account_payload()) assert bcol_account.auth_account_id == bcol_account.auth_account_id assert pad_account.payment_method == PaymentMethod.PAD.value assert pad_account.cfs_account_id assert pad_account.cfs_account is None assert pad_account.cfs_party is None assert pad_account.cfs_site is None
def create_invoice( self, payment_account: PaymentAccount, line_items: [PaymentLineItem], invoice: Invoice, **kwargs) -> InvoiceReference: # pylint: disable=unused-argument """Return a static invoice number for direct pay.""" # Do nothing here as the invoice references are created later. # If the account have credit, deduct the credit amount which will be synced when reconciliation runs. account_credit = payment_account.credit or 0 if account_credit > 0: current_app.logger.info( f'Account credit {account_credit}, found for {payment_account.auth_account_id}' ) payment_account.credit = 0 if account_credit < invoice.total else account_credit - invoice.total payment_account.flush()
def delete(account_number: str): """Get payment account details.""" current_app.logger.info('<Account.delete') # Check if user is authorized to perform this action check_auth(business_identifier=None, account_id=account_number, one_of_roles=[EDIT_ROLE, VIEW_ROLE]) try: PaymentAccountService.delete_account(account_number) except BusinessException as exception: return exception.response() except ServiceUnavailableException as exception: return exception.response() current_app.logger.debug('>Account.delete') return jsonify({}), HTTPStatus.NO_CONTENT
def test_account_invalid_lookup(session): """Invalid account test.""" business_info: Dict = {'businessIdentifier': '1234', 'corpType': 'CP'} p = PaymentAccountService.find_account(business_info, get_auth_basic_user(), 'PAYBC') assert p is not None assert p.id is None import pytest from pay_api.exceptions import BusinessException from pay_api.utils.errors import Error with pytest.raises(BusinessException) as excinfo: PaymentAccountService.find_account({}, get_auth_basic_user(), 'PAYBC') assert excinfo.value.status == Error.PAY004.status
def test_account_invalid_premium_account_lookup(session): """Invalid account test.""" business_info: Dict = {} p = PaymentAccountService.find_account(business_info, get_auth_premium_user(), 'BCOL') assert p is not None assert p.id is None import pytest from pay_api.exceptions import BusinessException from pay_api.utils.errors import Error with pytest.raises(BusinessException) as excinfo: PaymentAccountService.find_account(business_info, {}, 'BCOL') assert excinfo.value.status == Error.PAY015.status
def put(account_number: str): """Create the payment account records.""" current_app.logger.info('<Account.post') request_json = request.get_json() current_app.logger.debug(request_json) # Validate the input request valid_format, errors = schema_utils.validate(request_json, 'account_info') if not valid_format: return error_to_response( Error.INVALID_REQUEST, invalid_params=schema_utils.serialize(errors)) try: response = PaymentAccountService.update(account_number, request_json) except ServiceUnavailableException as exception: return exception.response() status = HTTPStatus.ACCEPTED \ if response.cfs_account_id and response.cfs_account_status == CfsAccountStatus.PENDING.value \ else HTTPStatus.OK current_app.logger.debug('>Account.post') return jsonify(response.asdict()), status
def get(account_number: str): """Get Fee details for the account.""" current_app.logger.info('<AccountFees.get') response, status = PaymentAccountService.get_account_fees( account_number), HTTPStatus.OK current_app.logger.debug('>AccountFees.get') return jsonify(response), status
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 post(): """Create the payment account records.""" current_app.logger.info('<Account.post') request_json = request.get_json() current_app.logger.debug(request_json) # Check if sandbox request is authorized. is_sandbox = request.args.get('sandbox', 'false').lower() == 'true' if is_sandbox and not _jwt.validate_roles( [Role.CREATE_SANDBOX_ACCOUNT.value]): abort(HTTPStatus.FORBIDDEN) # Validate the input request valid_format, errors = schema_utils.validate(request_json, 'account_info') if not valid_format: return error_to_response( Error.INVALID_REQUEST, invalid_params=schema_utils.serialize(errors)) try: response = PaymentAccountService.create(request_json, is_sandbox) status = HTTPStatus.ACCEPTED \ if response.cfs_account_id and response.cfs_account_status == CfsAccountStatus.PENDING.value \ else HTTPStatus.CREATED except BusinessException as exception: return exception.response() current_app.logger.debug('>Account.post') return jsonify(response.asdict()), status
def test_create_pad_account_but_drawdown_is_active(session): """Assert updating PAD to DRAWDOWN works.""" # Create a PAD Account first pad_account = PaymentAccountService.create(get_pad_account_payload()) # Update this payment account with drawdown and assert payment method assert pad_account.payment_method == PaymentMethod.DRAWDOWN.value assert pad_account.cfs_account_id
def test_create_pad_to_bcol_to_pad(session): """Assert that update from BCOL to PAD works.""" # Create a PAD Account first pad_account_1 = PaymentAccountService.create(get_unlinked_pad_account_payload(bank_number='009')) assert pad_account_1.bank_number == '009' # Update this payment account with drawdown and assert payment method bcol_account = PaymentAccountService.update(pad_account_1.auth_account_id, get_premium_account_payload()) assert bcol_account.auth_account_id == bcol_account.auth_account_id assert bcol_account.payment_method == PaymentMethod.DRAWDOWN.value # Update to PAD again pad_account_2 = PaymentAccountService.update(pad_account_1.auth_account_id, get_unlinked_pad_account_payload(bank_number='010')) assert pad_account_2.bank_number == '010' assert pad_account_2.payment_method == PaymentMethod.PAD.value assert pad_account_2.cfs_account_id != pad_account_1.cfs_account_id
def test_premium_account_saved_from_new(session): """Assert that the payment is saved to the table.""" payment_account = factory_premium_payment_account() payment_account.save() pa = PaymentAccountService.find_account(get_auth_premium_user()) assert pa is not None assert pa.id is not None
def test_direct_pay_account_saved_from_new(session): """Assert that the payment is saved to the table.""" payment_account = factory_payment_account(payment_method_code=PaymentMethod.DIRECT_PAY.value) payment_account.save() pa = PaymentAccountService.find_account(get_auth_basic_user()) assert pa is not None assert pa.id is not None
def test_premium_account_saved_from_new(session): """Assert that the payment is saved to the table.""" payment_account = factory_premium_payment_account() payment_account.save() pa = PaymentAccountService.find_account({}, get_auth_premium_user(), payment_system='BCOL', payment_method=PaymentMethod.DRAWDOWN.value) assert pa is not None assert pa.id is not None
def test_create_online_banking_account(session): """Assert that create online banking account works.""" online_banking_account = PaymentAccountService.create( get_basic_account_payload(payment_method=PaymentMethod.ONLINE_BANKING.value)) assert online_banking_account.payment_method == PaymentMethod.ONLINE_BANKING.value assert online_banking_account.cfs_account_id assert online_banking_account.cfs_account is None assert online_banking_account.cfs_party is None assert online_banking_account.cfs_site is None assert online_banking_account.bank_number is None
def get(account_number: str): """Get payment account details.""" current_app.logger.info('<Account.get') # Check if user is authorized to perform this action check_auth(business_identifier=None, account_id=account_number, one_of_roles=[EDIT_ROLE, VIEW_ROLE]) response, status = PaymentAccountService.find_by_auth_account_id( account_number).asdict(), HTTPStatus.OK current_app.logger.debug('>Account.get') return jsonify(response), status
def test_account_find_by_id(session): """Assert that the payment is saved to the table.""" payment_account = factory_payment_account() payment_account.save() pa = PaymentAccountService.find_by_id(payment_account.id) assert pa is not None assert pa.id is not None assert pa.corp_number is not None assert pa.corp_type_code is not None assert pa.payment_system_code is not None assert pa.asdict() is not None
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 test_create_pad_account(session): """Assert that pad account details are created.""" pad_account = PaymentAccountService.create(get_unlinked_pad_account_payload()) assert pad_account.bank_number == get_unlinked_pad_account_payload().get('paymentInfo'). \ get('bankInstitutionNumber') assert pad_account.bank_account_number == get_unlinked_pad_account_payload(). \ get('paymentInfo').get('bankAccountNumber') assert pad_account.bank_branch_number == get_unlinked_pad_account_payload(). \ get('paymentInfo').get('bankTransitNumber') assert pad_account.payment_method == PaymentMethod.PAD.value assert pad_account.cfs_account_id assert pad_account.cfs_account is None assert pad_account.cfs_party is None assert pad_account.cfs_site is None
def test_direct_pay_account_saved_from_new(session): """Assert that the payment is saved to the table.""" payment_account = factory_payment_account(payment_method_code=PaymentMethod.DIRECT_PAY.value) payment_account.save() business_info: Dict = { 'businessIdentifier': payment_account.corp_number, 'corpType': payment_account.corp_type_code } pa = PaymentAccountService.find_account(business_info, get_auth_basic_user(), 'PAYBC') assert pa is not None assert pa.id is not None assert pa.corp_number is not None assert pa.corp_type_code is not None
def test_pad_factory_for_system_fails(session, system_user_mock): """Test payment system creation for PAD payment instances.""" from pay_api.factory.payment_system_factory import PaymentSystemFactory # noqa I001; errors out the test case from pay_api.exceptions import BusinessException pad_account = PaymentAccountService.create(get_unlinked_pad_account_payload()) # Try a DRAWDOWN for system user with pytest.raises(BusinessException) as excinfo: PaymentSystemFactory.create(payment_method='PAD', payment_account=pad_account) assert excinfo.value.code == Error.ACCOUNT_IN_PAD_CONFIRMATION_PERIOD.name time_delay = current_app.config['PAD_CONFIRMATION_PERIOD_IN_DAYS'] with freeze_time(datetime.today() + timedelta(days=time_delay + 1, minutes=1)): instance = PaymentSystemFactory.create(payment_method='PAD', payment_account=pad_account) assert isinstance(instance, PadService)
def test_delete_account_failures(session): """Assert that delete payment account works.""" # Create a PAD Account. # Add credit and assert account cannot be deleted. # Remove the credit. # Add a PAD transaction for within N days and mark as PAID. # Assert account cannot be deleted. # Mark the account as NSF and assert account cannot be deleted. payload = get_pad_account_payload() pay_account: PaymentAccountService = PaymentAccountService.create(payload) pay_account.credit = 100 pay_account.save() with pytest.raises(BusinessException) as excinfo: PaymentAccountService.delete_account(payload.get('accountId')) assert excinfo.value.code == Error.OUTSTANDING_CREDIT.code # Now mark the credit as zero and mark teh CFS account status as FREEZE. pay_account.credit = 0 pay_account.save() cfs_account = CfsAccountModel.find_effective_by_account_id(pay_account.id) cfs_account.status = CfsAccountStatus.FREEZE.value cfs_account.save() with pytest.raises(BusinessException) as excinfo: PaymentAccountService.delete_account(payload.get('accountId')) assert excinfo.value.code == Error.FROZEN_ACCOUNT.code # Now mark the status ACTIVE and create transactions within configured time. cfs_account = CfsAccountModel.find_effective_by_account_id(pay_account.id) cfs_account.status = CfsAccountStatus.ACTIVE.value cfs_account.save() created_on: datetime = get_outstanding_txns_from_date() + timedelta( minutes=1) factory_invoice(pay_account, payment_method_code=PaymentMethod.PAD.value, created_on=created_on, status_code=InvoiceStatus.PAID.value).save() with pytest.raises(BusinessException) as excinfo: PaymentAccountService.delete_account(payload.get('accountId')) assert excinfo.value.code == Error.TRANSACTIONS_IN_PROGRESS.code
def put(account_number: str, product: str): """Create or update the account fee settings.""" current_app.logger.info('<AccountFee.post') request_json = request.get_json() # Validate the input request valid_format, errors = schema_utils.validate(request_json, 'account_fee') if not valid_format: return error_to_response( Error.INVALID_REQUEST, invalid_params=schema_utils.serialize(errors)) try: response, status = PaymentAccountService.save_account_fee( account_number, product, request_json), HTTPStatus.OK except BusinessException as exception: return exception.response() current_app.logger.debug('>AccountFee.post') return jsonify(response), status
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()