def create_refund(cls, invoice_id: int, request: Dict[str, str], **kwargs) -> Dict[str, str]: """Create refund.""" current_app.logger.debug(f'Starting refund : {invoice_id}') # Do validation by looking up the invoice invoice: InvoiceModel = InvoiceModel.find_by_id(invoice_id) paid_statuses = (InvoiceStatus.PAID.value, InvoiceStatus.APPROVED.value, InvoiceStatus.UPDATE_REVENUE_ACCOUNT.value) if invoice.invoice_status_code not in paid_statuses: current_app.logger.info( f'Cannot process refund as status of {invoice_id} is {invoice.invoice_status_code}' ) raise BusinessException(Error.INVALID_REQUEST) refund: RefundService = RefundService() refund.invoice_id = invoice_id refund.reason = get_str_by_path(request, 'reason') refund.requested_by = kwargs['user'].user_name refund.requested_date = datetime.now() refund.flush() pay_system_service: PaymentSystemService = PaymentSystemFactory.create_from_payment_method( payment_method=invoice.payment_method_code) invoice_status = pay_system_service.process_cfs_refund(invoice) message = REFUND_SUCCESS_MESSAGES.get( f'{invoice.payment_method_code}.{invoice.invoice_status_code}') # set invoice status invoice.invoice_status_code = invoice_status or InvoiceStatus.REFUND_REQUESTED.value invoice.refund = invoice.total # no partial refund invoice.save() current_app.logger.debug(f'Completed refund : {invoice_id}') return {'message': message}
def test_paybc_system_factory(session, public_user_mock): """Assert a paybc service is returned.""" from pay_api.factory.payment_system_factory import PaymentSystemFactory # noqa I001; errors out the test case # Test for CC and CP instance = PaymentSystemFactory.create(payment_method='CC', corp_type='CP') assert isinstance(instance, PaybcService) assert isinstance(instance, PaymentSystemService) # Test for CC and CP instance = PaymentSystemFactory.create( payment_method=PaymentMethod.DIRECT_PAY.value, corp_type='CP') assert isinstance(instance, DirectPayService) assert isinstance(instance, PaymentSystemService) # Test for CC and CP with zero fees instance = PaymentSystemFactory.create(fees=0, payment_method='CC', corp_type='CP') assert isinstance(instance, InternalPayService) assert isinstance(instance, PaymentSystemService) # Test for PAYBC Service instance = PaymentSystemFactory.create_from_payment_method( PaymentMethod.CC.value) assert isinstance(instance, PaybcService) assert isinstance(instance, PaymentSystemService) # Test for Direct Pay Service instance = PaymentSystemFactory.create_from_payment_method( PaymentMethod.DIRECT_PAY.value) assert isinstance(instance, DirectPayService) assert isinstance(instance, PaymentSystemService) # Test for Internal Service instance = PaymentSystemFactory.create_from_payment_method( PaymentMethod.INTERNAL.value) assert isinstance(instance, InternalPayService) assert isinstance(instance, PaymentSystemService) # Test for BCOL Service instance = PaymentSystemFactory.create_from_payment_method( PaymentMethod.DRAWDOWN.value) assert isinstance(instance, BcolService) assert isinstance(instance, PaymentSystemService) # Test for EFT Service instance = PaymentSystemFactory.create_from_payment_method( PaymentMethod.EFT.value) assert isinstance(instance, EftService) assert isinstance(instance, DepositService) assert isinstance(instance, PaymentSystemService) # Test for WIRE Service instance = PaymentSystemFactory.create_from_payment_method( PaymentMethod.WIRE.value) assert isinstance(instance, WireService) assert isinstance(instance, DepositService) assert isinstance(instance, PaymentSystemService)
def delete_invoice(cls, invoice_id: int): # pylint: disable=too-many-locals,too-many-statements """Delete invoice related records. Does the following; 1. Check if payment is eligible to be deleted. 2. Mark the payment and invoices records as deleted. 3. Publish message to queue """ # update transaction function will update the status from PayBC _update_active_transactions(invoice_id) invoice: Invoice = Invoice.find_by_id(invoice_id, skip_auth_check=True) current_app.logger.debug( f'<Delete Invoice {invoice_id}, {invoice.invoice_status_code}') # Create the payment system implementation pay_service: PaymentSystemService = PaymentSystemFactory.create_from_payment_method( invoice.payment_method_code) # set payment status as deleted payment = Payment.find_payment_for_invoice(invoice_id) _check_if_invoice_can_be_deleted(invoice, payment) if payment: payment.payment_status_code = PaymentStatus.DELETED.value payment.flush() # Cancel invoice invoice_reference = InvoiceReference.find_active_reference_by_invoice_id( invoice.id) payment_account = PaymentAccount.find_by_id(invoice.payment_account_id) if invoice_reference: pay_service.cancel_invoice( payment_account=payment_account, inv_number=invoice_reference.invoice_number) invoice.invoice_status_code = InvoiceStatus.DELETED.value for line in invoice.payment_line_items: line.line_item_status_code = LineItemStatus.CANCELLED.value if invoice_reference: invoice_reference.status_code = InvoiceReferenceStatus.CANCELLED.value invoice_reference.flush() invoice.save() current_app.logger.debug('>delete_invoice')
def _save_payment( payment_date, inv_number, invoice_amount, # pylint: disable=too-many-arguments paid_amount, row, status, payment_method, receipt_number): # pylint: disable=import-outside-toplevel from pay_api.factory.payment_system_factory import PaymentSystemFactory payment_account = _get_payment_account(row) pay_service = PaymentSystemFactory.create_from_payment_method( payment_method) # If status is failed, which means NSF. We already have a COMPLETED payment record, find and update iit. payment: PaymentModel = None if status == PaymentStatus.FAILED.value: payment = _get_payment_by_inv_number_and_status( inv_number, PaymentStatus.COMPLETED.value) # Just to handle duplicate rows in settlement file, # pull out failed payment record if it exists and no COMPLETED payments are present. if not payment: payment = _get_payment_by_inv_number_and_status( inv_number, PaymentStatus.FAILED.value) elif status == PaymentStatus.COMPLETED.value: # if the payment status is COMPLETED, then make sure there are # no other COMPLETED payment for same invoice_number.If found, return. This is to avoid duplicate entries. payment = _get_payment_by_inv_number_and_status( inv_number, PaymentStatus.COMPLETED.value) if payment: return if not payment: payment = PaymentModel() payment.payment_method_code = pay_service.get_payment_method_code() payment.payment_status_code = status payment.payment_system_code = pay_service.get_payment_system_code() payment.invoice_number = inv_number payment.invoice_amount = invoice_amount payment.payment_account_id = payment_account.id payment.payment_date = payment_date payment.paid_amount = paid_amount payment.receipt_number = receipt_number db.session.add(payment)
def create_transaction_for_invoice( invoice_id: int, request_json: Dict) -> PaymentTransaction: """Create transaction record for invoice, by creating a payment record if doesn't exist.""" current_app.logger.debug('<create transaction') # Lookup invoice record invoice: Invoice = Invoice.find_by_id(invoice_id, skip_auth_check=True) if not invoice.id: raise BusinessException(Error.INVALID_INVOICE_ID) if invoice.payment_method_code == PaymentMethod.PAD.value: # No transaction needed for PAD invoices. raise BusinessException(Error.INVALID_TRANSACTION) pay_system_service: PaymentSystemService = PaymentSystemFactory.create_from_payment_method( payment_method=invoice.payment_method_code) # Check if return url is valid PaymentTransaction._validate_redirect_url_and_throw_error( invoice.payment_method_code, request_json.get('clientSystemUrl')) # Check if there is a payment created. If not, create a payment record with status CREATED payment: Payment = Payment.find_payment_for_invoice(invoice_id) if not payment: # Transaction is against payment, so create a payment if not present. invoice_reference = InvoiceReference.find_active_reference_by_invoice_id( invoice.id) # Create a payment record payment = Payment.create( payment_method=pay_system_service.get_payment_method_code(), payment_system=pay_system_service.get_payment_system_code(), payment_status=pay_system_service.get_default_payment_status(), invoice_number=invoice_reference.invoice_number, invoice_amount=invoice.total, payment_account_id=invoice.payment_account_id) transaction = PaymentTransaction._create_transaction(payment, request_json, invoice=invoice) current_app.logger.debug('>create transaction') return transaction
def update_invoice(cls, invoice_id: int, payment_request: Tuple[Dict[str, Any]]): """Update invoice related records.""" current_app.logger.debug('<update_invoice') invoice: Invoice = Invoice.find_by_id(invoice_id, skip_auth_check=False) payment_method = get_str_by_path(payment_request, 'paymentInfo/methodOfPayment') is_not_currently_on_ob = invoice.payment_method_code != PaymentMethod.ONLINE_BANKING.value is_not_changing_to_cc = payment_method not in ( PaymentMethod.CC.value, PaymentMethod.DIRECT_PAY.value) # can patch only if the current payment method is OB if is_not_currently_on_ob or is_not_changing_to_cc: raise BusinessException(Error.INVALID_REQUEST) # check if it has any invoice references already created # if there is any invoice ref , send them to the invoiced credit card flow invoice_reference = InvoiceReference.find_active_reference_by_invoice_id( invoice.id) if invoice_reference: invoice.payment_method_code = PaymentMethod.CC.value else: pay_service: PaymentSystemService = PaymentSystemFactory.create_from_payment_method( PaymentMethod.DIRECT_PAY.value) payment_account = PaymentAccount.find_by_id( invoice.payment_account_id) pay_service.create_invoice(payment_account, invoice.payment_line_items, invoice, corp_type_code=invoice.corp_type_code) invoice.payment_method_code = PaymentMethod.DIRECT_PAY.value invoice.save() current_app.logger.debug('>update_invoice') return invoice.asdict()
def _create_transaction(payment: Payment, request_json: Dict, invoice: Invoice = None): # Cannot start transaction on completed payment if payment.payment_status_code in (PaymentStatus.COMPLETED.value, PaymentStatus.DELETED.value): raise BusinessException(Error.COMPLETED_PAYMENT) pay_system_service: PaymentSystemService = PaymentSystemFactory.create_from_payment_method( # todo Remove this and use payment.payment_method_code when payment methods are not created upfront payment_method=invoice.payment_method_code if invoice else payment. payment_method_code) # If there are active transactions (status=CREATED), then invalidate all of them and create a new one. existing_transaction = PaymentTransactionModel.find_active_by_payment_id( payment.id) if existing_transaction and existing_transaction.status_code != TransactionStatus.CANCELLED.value: existing_transaction.status_code = TransactionStatus.CANCELLED.value existing_transaction.transaction_end_time = datetime.now() existing_transaction.save() transaction = PaymentTransaction() transaction.payment_id = payment.id transaction.client_system_url = request_json.get('clientSystemUrl') transaction.status_code = TransactionStatus.CREATED.value transaction_dao = transaction.flush() transaction._dao = transaction_dao # pylint: disable=protected-access if invoice: transaction.pay_system_url = PaymentTransaction._build_pay_system_url_for_invoice( invoice, pay_system_service, transaction.id, request_json.get('payReturnUrl')) else: transaction.pay_system_url = PaymentTransaction._build_pay_system_url_for_payment( payment, pay_system_service, transaction.id, request_json.get('payReturnUrl')) transaction_dao = transaction.save() transaction = PaymentTransaction.__wrap_dao(transaction_dao) return transaction
def update_transaction( transaction_id: uuid, # pylint: disable=too-many-locals pay_response_url: str): """Update transaction record. Does the following: 1. Find the payment record with the id 2. Find the invoice record using the payment identifier 3. Call the pay system service and get the receipt details 4. Save the receipt record 5. Change the status of Invoice 6. Change the status of Payment 7. Update the transaction record """ # TODO for now assumption is this def will be called only for credit card, bcol and internal payments. # When start to look into the PAD and Online Banking may need to refactor here transaction_dao: PaymentTransactionModel = PaymentTransactionModel.find_by_id( transaction_id) if not transaction_dao: raise BusinessException(Error.INVALID_TRANSACTION_ID) if transaction_dao.status_code == TransactionStatus.COMPLETED.value: raise BusinessException(Error.INVALID_TRANSACTION) payment: Payment = Payment.find_by_id(transaction_dao.payment_id) payment_account: PaymentAccount = PaymentAccount.find_by_id( payment.payment_account_id) # For transactions other than Credit Card, there could be more than one invoice per payment. invoices: [Invoice] = Invoice.find_invoices_for_payment( transaction_dao.payment_id) if payment.payment_status_code == PaymentStatus.COMPLETED.value: # if the transaction status is EVENT_FAILED then publish to queue and return, else raise error if transaction_dao.status_code == TransactionStatus.EVENT_FAILED.value: # Publish status to Queue for invoice in invoices: PaymentTransaction.publish_status(transaction_dao, invoice) transaction_dao.status_code = TransactionStatus.COMPLETED.value return PaymentTransaction.__wrap_dao(transaction_dao.save()) raise BusinessException(Error.COMPLETED_PAYMENT) pay_system_service: PaymentSystemService = PaymentSystemFactory.create_from_payment_method( payment_method=payment.payment_method_code) invoice_reference = InvoiceReference.find_any_active_reference_by_invoice_number( payment.invoice_number) try: receipt_details = pay_system_service.get_receipt( payment_account, pay_response_url, invoice_reference) txn_reason_code = None except ServiceUnavailableException as exc: txn_reason_code = exc.status transaction_dao.pay_system_reason_code = txn_reason_code receipt_details = None if receipt_details: PaymentTransaction._update_receipt_details(invoices, payment, receipt_details, transaction_dao) else: transaction_dao.status_code = TransactionStatus.FAILED.value # check if the pay_response_url contains any failure status if not txn_reason_code: pay_system_reason_code = pay_system_service.get_pay_system_reason_code( pay_response_url) transaction_dao.pay_system_reason_code = pay_system_reason_code # Save response URL transaction_dao.transaction_end_time = datetime.now() transaction_dao.pay_response_url = pay_response_url transaction_dao = transaction_dao.save() # Publish message to unlock account if account is locked. if payment.payment_status_code == PaymentStatus.COMPLETED.value: active_failed_payments = Payment.get_failed_payments( auth_account_id=payment_account.auth_account_id) current_app.logger.info('active_failed_payments %s', active_failed_payments) if not active_failed_payments: PaymentAccount.unlock_frozen_accounts( payment.payment_account_id) transaction = PaymentTransaction.__wrap_dao(transaction_dao) current_app.logger.debug('>update_transaction') return transaction
def _save_account(cls, account_request: Dict[str, any], payment_account: PaymentAccountModel): """Update and save payment account and CFS account model.""" # pylint:disable=cyclic-import, import-outside-toplevel from pay_api.factory.payment_system_factory import PaymentSystemFactory # If the payment method is CC, set the payment_method as DIRECT_PAY payment_method: str = get_str_by_path(account_request, 'paymentInfo/methodOfPayment') if not payment_method or payment_method == PaymentMethod.CC.value: payment_method = PaymentMethod.DIRECT_PAY.value payment_account.payment_method = payment_method payment_account.auth_account_id = account_request.get('accountId') payment_account.auth_account_name = account_request.get( 'accountName', None) payment_account.bcol_account = account_request.get( 'bcolAccountNumber', None) payment_account.bcol_user_id = account_request.get('bcolUserId', None) payment_account.pad_tos_accepted_by = account_request.get( 'padTosAcceptedBy', None) payment_account.pad_tos_accepted_date = datetime.now() payment_info = account_request.get('paymentInfo') billable = payment_info.get('billable', True) payment_account.billable = billable # Steps to decide on creating CFS Account or updating CFS bank account. # Updating CFS account apart from bank details not in scope now. # Create CFS Account IF: # 1. New payment account # 2. Existing payment account: # - If the account was on DIRECT_PAY and switching to Online Banking, and active CFS account is not present. # - If the account was on DRAWDOWN and switching to PAD, and active CFS account is not present cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id(payment_account.id) \ if payment_account.id else None pay_system = PaymentSystemFactory.create_from_payment_method( payment_method=payment_method) if pay_system.get_payment_system_code() == PaymentSystem.PAYBC.value: if cfs_account is None: cfs_account = pay_system.create_account( name=payment_account.auth_account_name, contact_info=account_request.get('contactInfo'), payment_info=account_request.get('paymentInfo')) if cfs_account: cfs_account.payment_account = payment_account cfs_account.flush() # If the account is PAD and bank details changed, then update bank details else: # Update details in CFS pay_system.update_account( name=payment_account.auth_account_name, cfs_account=cfs_account, payment_info=payment_info) elif cfs_account is not None: # if its not PAYBC ,it means switching to either drawdown or internal ,deactivate the cfs account cfs_account.status = CfsAccountStatus.INACTIVE.value cfs_account.flush() is_pad = payment_method == PaymentMethod.PAD.value if is_pad: # override payment method for since pad has 3 days wait period effective_pay_method, activation_date = PaymentAccount._get_payment_based_on_pad_activation( payment_account) payment_account.pad_activation_date = activation_date payment_account.payment_method = effective_pay_method payment_account.save()
def update_invoice(cls, invoice_id: int, payment_request: Tuple[Dict[str, Any]], is_apply_credit: bool = False): """Update invoice related records.""" current_app.logger.debug('<update_invoice') invoice: Invoice = Invoice.find_by_id(invoice_id, skip_auth_check=False) # If the call is to apply credit, apply credit and release records. if is_apply_credit: credit_balance: float = 0 payment_account: PaymentAccount = PaymentAccount.find_by_id( invoice.payment_account_id) invoice_balance = invoice.total - (invoice.paid or 0) if (payment_account.credit or 0) >= invoice_balance: pay_service: PaymentSystemService = PaymentSystemFactory.create_from_payment_method( invoice.payment_method_code) # Only release records, as the actual status change should happen during reconciliation. pay_service.apply_credit(invoice) credit_balance = payment_account.credit - invoice_balance invoice.paid = invoice.total invoice.save() elif (payment_account.credit or 0) <= invoice_balance: invoice.paid = (invoice.paid or 0) + payment_account.credit invoice.save() payment_account.credit = credit_balance payment_account.save() else: payment_method = get_str_by_path(payment_request, 'paymentInfo/methodOfPayment') is_not_currently_on_ob = invoice.payment_method_code != PaymentMethod.ONLINE_BANKING.value is_not_changing_to_cc = payment_method not in ( PaymentMethod.CC.value, PaymentMethod.DIRECT_PAY.value) # can patch only if the current payment method is OB if is_not_currently_on_ob or is_not_changing_to_cc: raise BusinessException(Error.INVALID_REQUEST) # check if it has any invoice references already created # if there is any invoice ref , send them to the invoiced credit card flow invoice_reference = InvoiceReference.find_active_reference_by_invoice_id( invoice.id) if invoice_reference: invoice.payment_method_code = PaymentMethod.CC.value else: pay_service: PaymentSystemService = PaymentSystemFactory.create_from_payment_method( PaymentMethod.DIRECT_PAY.value) payment_account = PaymentAccount.find_by_id( invoice.payment_account_id) pay_service.create_invoice( payment_account, invoice.payment_line_items, invoice, corp_type_code=invoice.corp_type_code) invoice.payment_method_code = PaymentMethod.DIRECT_PAY.value invoice.save() current_app.logger.debug('>update_invoice') return invoice.asdict()
if payment_info := account_request.get('paymentInfo'): billable = payment_info.get('billable', True) payment_account.billable = billable payment_account.flush() # Steps to decide on creating CFS Account or updating CFS bank account. # Updating CFS account apart from bank details not in scope now. # Create CFS Account IF: # 1. New payment account # 2. Existing payment account: # - If the account was on DIRECT_PAY and switching to Online Banking, and active CFS account is not present. # - If the account was on DRAWDOWN and switching to PAD, and active CFS account is not present if payment_method: pay_system = PaymentSystemFactory.create_from_payment_method( payment_method=payment_method) cls._handle_payment_details(account_request, is_sandbox, pay_system, payment_account, payment_info) payment_account.save() @classmethod def _handle_payment_details(cls, account_request, is_sandbox, pay_system, payment_account, payment_info): # pylint: disable=too-many-arguments cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id(payment_account.id) \ if payment_account.id else None if pay_system.get_payment_system_code() == PaymentSystem.PAYBC.value: if cfs_account is None: cfs_account = pay_system.create_account( # pylint:disable=assignment-from-none identifier=payment_account.auth_account_id,
def _save_account(cls, account_request: Dict[str, any], payment_account: PaymentAccountModel): """Update and save payment account and CFS account model.""" # pylint:disable=cyclic-import, import-outside-toplevel from pay_api.factory.payment_system_factory import PaymentSystemFactory # If the payment method is CC, set the payment_method as DIRECT_PAY payment_method: str = get_str_by_path(account_request, 'paymentInfo/methodOfPayment') if not payment_method or payment_method == PaymentMethod.CC.value: payment_method = PaymentMethod.DIRECT_PAY.value payment_account.payment_method = payment_method payment_account.auth_account_id = account_request.get('accountId') payment_account.auth_account_name = account_request.get( 'accountName', None) payment_account.bcol_account = account_request.get( 'bcolAccountNumber', None) payment_account.bcol_user_id = account_request.get('bcolUserId', None) payment_account.pad_tos_accepted_by = account_request.get( 'padTosAcceptedBy', None) if payment_account.pad_tos_accepted_by is not None: payment_account.pad_tos_accepted_date = datetime.now() payment_info = account_request.get('paymentInfo') billable = payment_info.get('billable', True) payment_account.billable = billable payment_account.flush() # Steps to decide on creating CFS Account or updating CFS bank account. # Updating CFS account apart from bank details not in scope now. # Create CFS Account IF: # 1. New payment account # 2. Existing payment account: # - If the account was on DIRECT_PAY and switching to Online Banking, and active CFS account is not present. # - If the account was on DRAWDOWN and switching to PAD, and active CFS account is not present cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id(payment_account.id) \ if payment_account.id else None pay_system = PaymentSystemFactory.create_from_payment_method( payment_method=payment_method) if pay_system.get_payment_system_code() == PaymentSystem.PAYBC.value: if cfs_account is None: cfs_account = pay_system.create_account( name=payment_account.auth_account_name, contact_info=account_request.get('contactInfo'), payment_info=account_request.get('paymentInfo')) if cfs_account: cfs_account.payment_account = payment_account cfs_account.flush() # If the account is PAD and bank details changed, then update bank details else: # Update details in CFS pay_system.update_account( name=payment_account.auth_account_name, cfs_account=cfs_account, payment_info=payment_info) is_pad = payment_method == PaymentMethod.PAD.value if is_pad: # override payment method for since pad has 3 days wait period effective_pay_method, activation_date = PaymentAccount._get_payment_based_on_pad_activation( payment_account) payment_account.pad_activation_date = activation_date payment_account.payment_method = effective_pay_method elif pay_system.get_payment_system_code() == PaymentSystem.CGI.value: # if distribution code exists, put an end date as previous day and create new. dist_code_svc: DistributionCode = DistributionCode.find_active_by_account_id( payment_account.id) if dist_code_svc and dist_code_svc.distribution_code_id: end_date: datetime = datetime.now() - timedelta(days=1) dist_code_svc.end_date = end_date.date() dist_code_svc.save() # Create distribution code details. if revenue_account := payment_info.get('revenueAccount'): revenue_account.update( dict( accountId=payment_account.id, name=payment_account.auth_account_name, )) DistributionCode.save_or_update(revenue_account)