Skip to content

Business logic

Bank Transfers

BankTransferLogic

BankTransferLogic(payout_service_provider_registry)

Business logic related to bank transfers like payouts.

Tags
Source code in components/payment_gateway/subcomponents/payments/protected/business_logic/bank_transfers.py
def __init__(
    self,
    payout_service_provider_registry: PayoutServiceProviderRegistry,
) -> None:
    self.payout_service_provider_registry = payout_service_provider_registry

payout_service_provider_registry instance-attribute

payout_service_provider_registry = (
    payout_service_provider_registry
)

create classmethod

create()

Creates a BankTransferLogic instance with real payout service providers.

Source code in components/payment_gateway/subcomponents/payments/protected/business_logic/bank_transfers.py
@classmethod
def create(cls) -> "BankTransferLogic":
    """
    Creates a BankTransferLogic instance with real payout service providers.
    """
    return BankTransferLogic(PayoutServiceProviderRegistry.create())

create_null classmethod

create_null()

Creates a BankTransferLogic instance with null (no-op) payout service providers.

Source code in components/payment_gateway/subcomponents/payments/protected/business_logic/bank_transfers.py
@classmethod
def create_null(cls) -> "BankTransferLogic":
    """
    Creates a BankTransferLogic instance with null (no-op) payout service providers.
    """
    return BankTransferLogic(PayoutServiceProviderRegistry.create_null())

initiate_payout_bank_transfer

initiate_payout_bank_transfer(
    debtor_account_id,
    payout_request_id,
    amount_in_cents,
    currency,
    creditor_bank_account,
    creditor_legal_entity_id,
    transfer_history_id,
    payment_description=None,
)

Initiates a payout bank transfer through the appropriate payout service provider.

Source code in components/payment_gateway/subcomponents/payments/protected/business_logic/bank_transfers.py
def initiate_payout_bank_transfer(
    self,
    debtor_account_id: AccountId,
    payout_request_id: str,
    amount_in_cents: int,
    currency: str,
    creditor_bank_account: BankTransferAccount,
    creditor_legal_entity_id: LegalEntityId,
    transfer_history_id: TransferHistoryId,
    payment_description: str | None = None,
) -> PayoutBankTransferTransaction:
    """
    Initiates a payout bank transfer through the appropriate payout service provider.
    """
    with raise_if_legal_entity_not_found(creditor_legal_entity_id):
        creditor_legal_entity = LegalEntityModelBroker.get_legal_entity(
            current_session, id=creditor_legal_entity_id
        )
    raise_on_terminated_legal_entity(creditor_legal_entity)

    # 1. Get Account to retrieve the payment service provider
    debtor_account = AccountModelBroker.get_account(
        current_session,
        id=debtor_account_id,
    )

    payment_service_provider = get_provider_for_workspace(
        debtor_account.workspace_key
    )
    if payment_service_provider is None:
        raise WorkspaceNotRegisteredException(debtor_account.workspace_key)
    payout_service_provider = self.payout_service_provider_registry.get(
        payment_service_provider
    )

    # 2. Create a SepaBeneficiary if not existing -> TODO
    #  - use "iban" field to store CaBankAccount.bank_account_info (interac)
    #      - add "iban_type" for interac ?
    #  - use "external_id" field to store CaBankAccount.id

    with transaction(propagation=Propagation.REQUIRES_NEW) as session:
        # 3. Record a BankTransfer
        # This will create a BankTransfer in a new transaction
        # even if the call to payment service provider fails. We want to keep track of all
        # attempts to initiate a transfer.
        bank_transfer, _ = BankTransferModelBroker.record_bank_transfer(
            session,
            workspace_key=debtor_account.workspace_key,
            external_id=payout_request_id,
            effective_date=datetime.now(),
            direction=TransferDirection.OUTGOING,
            transfer_history_id=transfer_history_id,
            account_id=debtor_account_id,
        )

    bank_transfer_id = bank_transfer.id

    with transaction(propagation=Propagation.REQUIRES_NEW) as session:
        # 4. Call the payout service provider to initiate the transfer
        try:
            transaction_id, raw_response = payout_service_provider.pay(
                request_id=payout_request_id,
                amount_in_cents=amount_in_cents,
                currency=currency,
                recipient_bank_account=creditor_bank_account,
                creditor=creditor_legal_entity,
                description=payment_description,
            )
            status = PayoutBankTransferStatus.submitted
        except Exception as exception:
            current_logger.error("Failed to initiate payout", exc_info=exception)
            transaction_id = None
            raw_response = {"error": str(exception)}
            status = PayoutBankTransferStatus.submission_failed

        # 5. Record a TransferUpdate with the response (even if failed)
        bank_transfer_update, _ = TransferUpdateModelBroker.record_transfer_update(
            session,
            workspace_key=debtor_account.workspace_key,
            external_transfer_id=payout_request_id,
            sequence_number=1,
            transfer_id=bank_transfer_id,
            transfer_type=TransferUpdateTransferType.BANK,
            direction=TransferDirection.OUTGOING,
            occurred_at=datetime.now(),
            amount=amount_in_cents,
            currency=currency,
            status=status,
            raw=raw_response,
            external_transaction_id=transaction_id,
        )

    return PayoutBankTransferTransaction(
        bank_transfer_id=BankTransferId(bank_transfer_update.transfer_id),
        external_transfer_id=bank_transfer_update.external_transfer_id,
        external_transaction_id=bank_transfer_update.external_transaction_id,
        is_submitted=bank_transfer_update.status
        == PayoutBankTransferStatus.submitted,
        amount=bank_transfer_update.amount,
        currency=bank_transfer_update.currency,
    )