Skip to content

components.payment_gateway.public.cards

This module defines the public API for the cards subcomponent.

Only business logic is exposed here. Basic entities and enums are exposed in separate modules to avoid loading the entire subcomponent with its models and dependencies when they are not needed.

Classes

CardDeliveryLogic

CardDeliveryLogic()

This class is the public interface to the card delivery logic.

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/card_delivery.py
def __init__(self) -> None:
    self.card_order_queries = CardOrderQueries()
    self.card_delivery_actions = CardDeliveryActions()

    # TODO WIP we haven't decided on the pattern we'll use for public events. For now let's declare them as topics.
    self.card_ordered_topic = Topic[CardOrdered]()
    self.card_delivery_status_changed_topic = Topic[CardDeliveryStatusChanged]()
    self.card_delivered_topic = Topic[CardDelivered]()
    self.card_not_delivered_topic = Topic[CardNotDelivered]()

Attributes

card_delivered_topic instance-attribute
card_delivered_topic = Topic[CardDelivered]()
card_delivery_actions instance-attribute
card_delivery_actions = CardDeliveryActions()
card_delivery_status_changed_topic instance-attribute
card_delivery_status_changed_topic = Topic[
    CardDeliveryStatusChanged
]()
card_not_delivered_topic instance-attribute
card_not_delivered_topic = Topic[CardNotDelivered]()
card_order_queries instance-attribute
card_order_queries = CardOrderQueries()
card_ordered_topic instance-attribute
card_ordered_topic = Topic[CardOrdered]()

Functions

declare_card_not_received
declare_card_not_received(session, /, id)
Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/card_delivery.py
@obs.api_call()
def declare_card_not_received(  # noqa: D102
    self,
    session: Session,
    /,
    id: CardId,
) -> None:
    self.card_delivery_actions.declare_card_not_received(
        session,
        id,
    )
declare_card_received
declare_card_received(session, /, id)
Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/card_delivery.py
@obs.api_call()
def declare_card_received(  # noqa: D102
    self,
    session: Session,
    /,
    id: CardId,
) -> None:
    self.card_delivery_actions.declare_card_received(
        session,
        id,
    )
get_card_order
get_card_order(session, /, card_id)

Get the card order info for a card.

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/card_delivery.py
@obs.api_call()
def get_card_order(
    self,
    session: Session,
    /,
    card_id: CardId,
) -> CardOrder:
    """
    Get the card order info for a card.
    """
    return self.card_order_queries.get_card_order(
        session,
        card_id,
    )

CardHolderActions

CardHolderActions(card_authentication_actions, card_logic)

This class contains all the actions related to card holders.

Implements the following Nullable patterns: - Nullables: https://www.jamesshore.com/v2/projects/nullables/testing-without-mocks#nullables ⧉ - Parameterless instantiation: https://www.jamesshore.com/v2/projects/nullables/testing-without-mocks#instantiation ⧉

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/actions/card_holder_actions.py
def __init__(
    self,
    card_authentication_actions: CardAuthenticationActions,
    card_logic: CardLogic,
) -> None:
    self.card_authentication_actions = card_authentication_actions
    self.card_logic = card_logic

Attributes

card_authentication_actions instance-attribute
card_authentication_actions = card_authentication_actions
card_logic instance-attribute
card_logic = card_logic

Functions

create classmethod
create()

Normal factory

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/actions/card_holder_actions.py
@classmethod
def create(cls) -> "CardHolderActions":
    """Normal factory"""
    return cls(
        card_authentication_actions=CardAuthenticationActions.create(),
        card_logic=CardLogic.create(),
    )
create_null classmethod
create_null()

Null factory

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/actions/card_holder_actions.py
@classmethod
def create_null(cls) -> "CardHolderActions":
    """Null factory"""
    return cls(
        card_authentication_actions=CardAuthenticationActions.create_null(),
        card_logic=CardLogic.create_null(),
    )
declare_card_holder
declare_card_holder(
    session,
    /,
    workspace_key,
    external_id,
    first_name,
    last_name,
    display_name=None,
    short_name=None,
)

Declare a card holder.

The card holder must exist in the PSP workspace if an external ID is provided.

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/actions/card_holder_actions.py
@obs.api_call()
def declare_card_holder(
    self,
    session: Session,
    /,
    workspace_key: str,
    external_id: str | None,
    first_name: str,
    last_name: str,
    display_name: str | None = None,
    short_name: str | None = None,
) -> CardHolderId:
    """
    Declare a card holder.

    The card holder must exist in the PSP workspace if an external ID is provided.
    """
    card_holder = CardHolderModelBroker.create_card_holder(
        session,
        workspace_key=workspace_key,
        external_id=external_id,
        first_name=first_name,
        last_name=last_name,
        display_name=display_name,
        short_name=short_name,
    )
    return CardHolderId(card_holder.id)
terminate_card_holder
terminate_card_holder(session, /, id)

Terminate a card holder.

The operation is idempotent, i.e. it has no effect on already terminated entities.

Card holders in terminal state cannot be modified or used anymore. Any attempt to use or retrieve a terminated card holder will raise a CardHolderTerminatedException.

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/actions/card_holder_actions.py
@obs.api_call()
def terminate_card_holder(
    self,
    session: Session,
    /,
    id: CardHolderId,
) -> None:
    """
    Terminate a card holder.

    The operation is idempotent, i.e. it has no effect on already terminated
    entities.

    Card holders in terminal state cannot be modified or used anymore. Any
    attempt to use or retrieve a terminated card holder will raise a
    `CardHolderTerminatedException`.
    """
    with raise_if_card_holder_not_found(id):
        card_holder = CardHolderModelBroker.get_card_holder(session, id=id)

    if not card_holder.is_terminated:
        CardHolderModelBroker.terminate_card_holder(session, id=id)
update_card_holder_contact_info
update_card_holder_contact_info(
    session, /, id, phone_number, email
)

Update the card holder contact info on the PSP. Phone number is needed to add cards to digital wallets. Email is optional as a second option for adding cards to digit wallets.

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/actions/card_holder_actions.py
@obs.api_call()
def update_card_holder_contact_info(
    self,
    session: Session,
    /,
    id: CardHolderId,
    phone_number: str,
    email: str | None,
) -> None:
    """
    Update the card holder contact info on the PSP.
    Phone number is needed to add cards to digital wallets.
    Email is optional as a second option for adding cards to digit wallets.
    """
    with raise_if_card_holder_not_found(id):
        card_holder = CardHolderModelBroker.get_card_holder(session, id=id)

        if len(card_holder.cards) == 0 or phone_number is None:
            return

        active_card = max(card_holder.cards, key=lambda card: card.issued_at)

        self.card_logic.edit_card_authentication_info(
            session,
            id=CardId(active_card.id),
            phone_number=phone_number,
            email=email,
        )
update_card_holder_identity
update_card_holder_identity(
    session,
    /,
    id,
    first_name,
    last_name,
    display_name,
    short_name,
)

Update the identity of a card holder.

The current identity of a card holder is used when issuing a card. The identity of a card holder may change over time, but cards are immutable.

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/actions/card_holder_actions.py
@obs.api_call()
def update_card_holder_identity(
    self,
    session: Session,
    /,
    id: CardHolderId,
    first_name: str,
    last_name: str,
    display_name: str | None,
    short_name: str | None,
) -> None:
    """
    Update the identity of a card holder.

    The current identity of a card holder is used when issuing a card.
    The identity of a card holder may change over time, but cards are immutable.
    """
    with raise_if_card_holder_not_found(id):
        CardHolderModelBroker.update_card_holder(
            session,
            id=id,
            first_name=first_name,
            last_name=last_name,
            display_name=display_name,
            short_name=short_name,
        )

CardHolderNotFoundException

Bases: PaymentCardException

Exception raised when trying to use a non-existing Card Holder.

CardHolderQueries

This class contains all the queries related to card holders.

Functions

get_card_holder
get_card_holder(session, /, id)

Get a card holder entity from its ID.

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/queries/card_holder_queries.py
@obs.api_call()
def get_card_holder(
    self,
    session: Session,
    /,
    id: CardHolderId,
) -> CardHolder:
    """
    Get a card holder entity from its ID.
    """
    with raise_if_card_holder_not_found(id):
        card_holder = CardHolderModelBroker.get_card_holder(session, id=id)

    raise_on_terminated_card_holder(card_holder)

    return CardHolder(
        id=CardHolderId(card_holder.id),
        workspace_key=card_holder.workspace_key,
        external_id=card_holder.external_id,
        first_name=card_holder.first_name,
        last_name=card_holder.last_name,
        display_name=card_holder.display_name,
        short_name=card_holder.short_name,
    )

CardHolderTerminatedException

Bases: PaymentCardException

Exception raised when trying to use a terminated Card Holder.

CardIncidentsActions

CardIncidentsActions(adyen_client)

This class contains all the actions relative to card incidents.

Incidents should eventually lead to the replacement of the card.

Implements the following Nullable patterns: - Nullables: https://www.jamesshore.com/v2/projects/nullables/testing-without-mocks#nullables ⧉ - Parameterless instantiation: https://www.jamesshore.com/v2/projects/nullables/testing-without-mocks#instantiation ⧉

Tags
Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/actions/card_incidents_actions.py
def __init__(
    self,
    adyen_client: "AdyenPaymentInstrumentsApiClient | None",
) -> None:
    self._adyen_client = adyen_client

Attributes

adyen_client property
adyen_client

Ensures the Adyen client is available when accessing it.

Functions

create classmethod
create()

Normal factory

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/actions/card_incidents_actions.py
@classmethod
def create(cls) -> "CardIncidentsActions":
    """Normal factory"""
    from shared.services.payment_providers.adyen.clients.adyen_payment_instruments_api_client import (
        AdyenPaymentInstrumentsApiClient,
    )
    from shared.services.payment_providers.adyen.clients.exceptions import (
        AdyenClientMissingCredentialsException,
    )

    try:
        adyen_client = AdyenPaymentInstrumentsApiClient.create()
    except AdyenClientMissingCredentialsException:
        adyen_client = None
    return cls(adyen_client)
create_null classmethod
create_null(track_adyen_requests=None)

Null factory

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/actions/card_incidents_actions.py
@classmethod
def create_null(
    cls,
    track_adyen_requests: list[tuple[str, dict, dict]] | None = None,  # type: ignore[type-arg]
) -> "CardIncidentsActions":
    """Null factory"""
    from shared.services.payment_providers.adyen.clients.adyen_payment_instruments_api_client import (
        AdyenPaymentInstrumentsApiClient,
    )

    return cls(
        AdyenPaymentInstrumentsApiClient.create_null(
            track_requests=track_adyen_requests
        )
    )
declare_card_damaged
declare_card_damaged(
    session, /, id, suspension_source=None
)

Declare a card as damaged.

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/actions/card_incidents_actions.py
@obs.api_call()
def declare_card_damaged(
    self,
    session: Session,
    /,
    id: CardId,
    suspension_source: CardSuspensionSource | None = None,
) -> None:
    """
    Declare a card as damaged.
    """
    from shared.services.payment_providers.adyen.openapi.balance_platform_service_v2 import (
        AdyenPaymentInstrumentUpdateRequest,
    )

    with raise_if_card_not_found(id):
        card = CardModelBroker.get_card(session, id=id)

    raise_on_terminated_card(card)
    raise_on_invalid_card_status_transition(card, CardStatus.suspended)

    self.adyen_client.update_payment_instrument(
        card.external_id,
        request=AdyenPaymentInstrumentUpdateRequest(
            status="suspended",
            statusReason="damaged",
        ),
        idempotency_key=None,  # TODO use this for retries
    )
    CardModelBroker.set_card_status(
        session,
        id=card.id,
        status=CardStatus.suspended,
        reason="damaged",
        suspension_source=suspension_source,
    )
declare_card_lost
declare_card_lost(session, /, id, suspension_source=None)

Declare a card as lost.

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/actions/card_incidents_actions.py
@obs.api_call()
def declare_card_lost(
    self,
    session: Session,
    /,
    id: CardId,
    suspension_source: CardSuspensionSource | None = None,
) -> None:
    """
    Declare a card as lost.
    """
    from shared.services.payment_providers.adyen.openapi.balance_platform_service_v2 import (
        AdyenPaymentInstrumentUpdateRequest,
    )

    with raise_if_card_not_found(id):
        card = CardModelBroker.get_card(session, id=id)

    raise_on_terminated_card(card)
    raise_on_invalid_card_status_transition(card, CardStatus.suspended)

    self.adyen_client.update_payment_instrument(
        card.external_id,
        request=AdyenPaymentInstrumentUpdateRequest(
            status="suspended",
            statusReason="lost",
        ),
        idempotency_key=None,  # TODO use this for retries
    )
    CardModelBroker.set_card_status(
        session,
        id=card.id,
        status=CardStatus.suspended,
        reason="lost",
        suspension_source=suspension_source,
    )
declare_card_stolen
declare_card_stolen(session, /, id, suspension_source=None)

Declare a card as stolen.

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/actions/card_incidents_actions.py
@obs.api_call()
def declare_card_stolen(
    self,
    session: Session,
    /,
    id: CardId,
    suspension_source: CardSuspensionSource | None = None,
) -> None:
    """
    Declare a card as stolen.
    """
    from shared.services.payment_providers.adyen.openapi.balance_platform_service_v2 import (
        AdyenPaymentInstrumentUpdateRequest,
    )

    with raise_if_card_not_found(id):
        card = CardModelBroker.get_card(session, id=id)

    raise_on_terminated_card(card)
    raise_on_invalid_card_status_transition(card, CardStatus.suspended)

    self.adyen_client.update_payment_instrument(
        card.external_id,
        request=AdyenPaymentInstrumentUpdateRequest(
            status="suspended",
            statusReason="stolen",
        ),
        idempotency_key=None,  # TODO use this for retries
    )
    CardModelBroker.set_card_status(
        session,
        id=card.id,
        status=CardStatus.suspended,
        reason="stolen",
        suspension_source=suspension_source,
    )
declare_card_temporarily_suspended
declare_card_temporarily_suspended(
    session, /, id, suspension_source=None, reason=None
)

Declare a card as temporarily suspended.

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/actions/card_incidents_actions.py
@obs.api_call()
def declare_card_temporarily_suspended(
    self,
    session: Session,
    /,
    id: CardId,
    suspension_source: CardSuspensionSource | None = None,
    reason: str | None = None,
) -> None:
    """
    Declare a card as temporarily suspended.
    """
    from shared.services.payment_providers.adyen.openapi.balance_platform_service_v2 import (
        AdyenPaymentInstrumentUpdateRequest,
    )

    with raise_if_card_not_found(id):
        card = CardModelBroker.get_card(session, id=id)

    raise_on_terminated_card(card)
    raise_on_invalid_card_status_transition(card, CardStatus.suspended)

    self.adyen_client.update_payment_instrument(
        card.external_id,
        request=AdyenPaymentInstrumentUpdateRequest(
            status="suspended",
            statusReason="other",
            statusComment="temporarily suspended",
        ),
        idempotency_key=None,  # TODO use this for retries
    )
    CardModelBroker.set_card_status(
        session,
        id=card.id,
        status=CardStatus.suspended,
        reason=reason or "temporarily suspended",
        suspension_source=suspension_source,
    )

CardLifecycleActions

CardLifecycleActions(
    adyen_client,
    default_card_configuration_key,
    card_configurations,
    password_salt,
)

This class contains all the actions used to manage the lifecycle of a card.

Implements the following Nullable patterns: - Nullables: https://www.jamesshore.com/v2/projects/nullables/testing-without-mocks#nullables ⧉ - Parameterless instantiation: https://www.jamesshore.com/v2/projects/nullables/testing-without-mocks#instantiation ⧉

Tags
Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/actions/card_lifecycle_actions.py
def __init__(
    self,
    adyen_client: "AdyenPaymentInstrumentsApiClient | None",
    default_card_configuration_key: str | None,
    card_configurations: dict[str, dict[str, str]],
    password_salt: str | None,
) -> None:
    self._adyen_client = adyen_client
    self.default_card_configuration_key = default_card_configuration_key
    self.card_configurations = card_configurations
    self.password_salt = password_salt

Attributes

adyen_client property
adyen_client

Ensures the Adyen client is available when accessing it.

card_configurations instance-attribute
card_configurations = card_configurations
default_card_configuration_key instance-attribute
default_card_configuration_key = (
    default_card_configuration_key
)
password_salt instance-attribute
password_salt = password_salt

Functions

create classmethod
create()

Normal factory

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/actions/card_lifecycle_actions.py
@classmethod
def create(cls) -> "CardLifecycleActions":
    """Normal factory"""
    from shared.services.payment_providers.adyen.clients.adyen_payment_instruments_api_client import (
        AdyenPaymentInstrumentsApiClient,
    )
    from shared.services.payment_providers.adyen.clients.exceptions import (
        AdyenClientMissingCredentialsException,
    )

    try:
        adyen_client = AdyenPaymentInstrumentsApiClient.create()
    except AdyenClientMissingCredentialsException:
        adyen_client = None

    try:
        default_card_configuration_key = current_config[
            DEFAULT_CARD_ISSUING_CONFIGURATION_KEY_CONFIG_KEY
        ]
        card_configurations = current_config[CARD_ISSUING_CONFIGURATIONS_CONFIG_KEY]
    except KeyError:
        default_card_configuration_key = None
        card_configurations = {}

    try:
        password_salt = raw_secret_from_config(
            config_key=CARD_PASSWORD_SALT_CONFIG_KEY,
            default_secret_value=current_config.get(CARD_PASSWORD_SALT_DEFAULT_KEY),
        )
    except KeyError:
        password_salt = None

    return cls(
        adyen_client=adyen_client,
        default_card_configuration_key=default_card_configuration_key,
        card_configurations=card_configurations,
        password_salt=password_salt,
    )
create_card
create_card(
    session,
    /,
    card_holder_id,
    account_id,
    form_factor,
    phone_number,
    email,
    shipment_info=None,
    issued_at=None,
    issuance_reason=None,
    description=None,
    reference=None,
    configuration_key=None,
)

Create a card for a card holder.

Note

This operation is currently only supported for Adyen.

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/actions/card_lifecycle_actions.py
@obs.api_call()
def create_card(
    self,
    session: Session,
    /,
    card_holder_id: CardHolderId,
    account_id: AccountId,
    form_factor: CardFormFactor,
    phone_number: str,
    email: str | None,
    shipment_info: CardShipmentInfo | None = None,
    issued_at: datetime | None = None,
    issuance_reason: str | None = None,
    description: str | None = None,
    reference: str | None = None,
    configuration_key: str | None = None,
) -> CardId:
    """
    Create a card for a card holder.

    Note:
        This operation is currently only supported for Adyen.
    """
    # TODO: pass the phone number here
    from components.payment_gateway.subcomponents.cards.adapters.adyen.helpers import (
        generate_card_password,
        to_adyen_payment_instrument_info,
    )

    if form_factor == CardFormFactor.physical and not shipment_info:
        raise MissingCardShipmentInfoException(
            "Shipment info is required for physical cards"
        )

    if self.password_salt is None:
        raise MissingCardPasswordSaltException()

    with raise_if_card_configuration_not_found(configuration_key):
        configuration_key = mandatory(
            configuration_key
            if configuration_key in self.card_configurations
            else self.default_card_configuration_key
        )
        card_configuration = self.card_configurations[configuration_key]

    with raise_if_card_holder_not_found(card_holder_id):
        card_holder = CardHolderModelBroker.get_card_holder(
            session, id=card_holder_id
        )
    with raise_if_account_not_found(account_id):
        account = AccountModelBroker.get_account(session, id=account_id)

    raise_on_inconsistent_workspace_keys(
        account.workspace_key, card_holder.workspace_key
    )

    # Only Adyen is supported for now
    raise_on_provider_not_supported(
        card_holder.workspace_key, PaymentServiceProvider.adyen
    )

    # 1. Generate a UUID for the card model we're going to create; we will
    #    use it to generate the 3DS password deterministically without
    #    having to store it in the DB
    #
    #    See below for full context:
    #    https://github.com/alan-eu/Topics/discussions/24840?sort=old
    card_id = uuid.uuid4()

    # 2. Generate card password for the card ID + salt
    password = generate_card_password(card_id, self.password_salt)

    # 3. Create Adyen request payload from our model
    payment_instrument_info = to_adyen_payment_instrument_info(
        card_holder=card_holder,
        account=account,
        form_factor=form_factor,
        country_code=card_configuration["country_code"],
        brand=card_configuration["brand"],
        brand_variant=card_configuration["brand_variant"],
        profile_id=card_configuration["profile_id"],
        phone_number=phone_number,
        email=email,
        shipment_info=shipment_info,
        password=password,
        description=description,
        reference=reference,
    )

    # 4. Call Adyen API
    payment_instrument = self.adyen_client.create_payment_instrument(
        request=payment_instrument_info,
        idempotency_key=None,  # TODO use this for retries
    )

    # 5. Create our Card entity from API result
    assert payment_instrument.card
    expiration_date = date(
        int(payment_instrument.card.expiration.year),  # type: ignore[union-attr,arg-type]
        int(payment_instrument.card.expiration.month),  # type: ignore[union-attr,arg-type]
        1,
    ) + relativedelta(months=1, days=-1)  # Last day of expiration month
    card = CardModelBroker.create_card(
        session,
        id=card_id,
        workspace_key=account.workspace_key,
        external_id=payment_instrument.id,
        display_name=payment_instrument.card.cardholderName,
        expiration_date=expiration_date,
        last_four_digits=mandatory(payment_instrument.card.lastFour),
        is_virtual=(form_factor == CardFormFactor.virtual),
        configuration_key=configuration_key,
        card_holder_id=card_holder.id,
        account_id=account.id,
        status=CardStatus.inactive,
        issued_at=issued_at if issued_at else datetime.now(),
        issuance_reason=issuance_reason,
        description=payment_instrument.description,
        reference=payment_instrument.reference,
    )
    return CardId(card.id)
create_null classmethod
create_null(
    default_card_configuration_key=None,
    card_configurations=None,
    track_adyen_requests=None,
)

Null factory

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/actions/card_lifecycle_actions.py
@classmethod
def create_null(
    cls,
    default_card_configuration_key: str | None = None,
    card_configurations: dict[str, dict[str, str]] | None = None,
    track_adyen_requests: list[tuple[str, dict, dict]] | None = None,  # type: ignore[type-arg]
) -> "CardLifecycleActions":
    """Null factory"""
    from shared.services.payment_providers.adyen.clients.adyen_payment_instruments_api_client import (
        AdyenPaymentInstrumentsApiClient,
    )

    adyen_client = AdyenPaymentInstrumentsApiClient.create_null(
        track_requests=track_adyen_requests
    )

    default_card_configuration_key = (
        default_card_configuration_key
        or current_config.get(
            DEFAULT_CARD_ISSUING_CONFIGURATION_KEY_CONFIG_KEY,
            "dummy_configuration",
        )
    )
    card_configurations = card_configurations or current_config.get(
        CARD_ISSUING_CONFIGURATIONS_CONFIG_KEY,
        {
            "dummy_configuration": {
                "country_code": "FR",
                "brand": "dummy_brand",
                "brand_variant": "dummy_brand_variant",
                "profile_id": "dummy_profile_id",
            },
        },
    )
    password_salt = "dummy_password_salt"  # gitleaks:allow

    return cls(
        adyen_client=adyen_client,
        default_card_configuration_key=default_card_configuration_key,
        card_configurations=card_configurations,
        password_salt=password_salt,
    )
terminate_card
terminate_card(session, /, id)

Terminate a card.

The operation is idempotent, i.e. it has no effect on already terminated entities.

Cards in terminal state cannot be modified or used anymore. Any attempt to use or retrieve a terminated card will raise a CardTerminatedException.

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/actions/card_lifecycle_actions.py
@obs.api_call()
def terminate_card(
    self,
    session: Session,
    /,
    id: CardId,
) -> None:
    """
    Terminate a card.

    The operation is idempotent, i.e. it has no effect on already terminated
    entities.

    Cards in terminal state cannot be modified or used anymore. Any attempt
    to use or retrieve a terminated card will raise a
    `CardTerminatedException`.
    """
    from shared.services.payment_providers.adyen.openapi.balance_platform_service_v2 import (
        AdyenPaymentInstrumentUpdateRequest,
    )

    with raise_if_card_not_found(id):
        card = CardModelBroker.get_card(session, id=id)

    if card.status != CardStatus.closed:
        provider = get_provider_for_workspace(card.workspace_key)
        if provider == PaymentServiceProvider.adyen:
            # Terminating a card should also close it on Adyen side
            self.adyen_client.update_payment_instrument(
                card.external_id,
                request=AdyenPaymentInstrumentUpdateRequest(
                    status="closed",
                    statusReason="accountClosure",
                    statusComment="Terminated by Alan",
                ),
                idempotency_key=None,  # TODO use this for retries
            )

        CardModelBroker.set_card_status(
            session, id=card.id, status=CardStatus.closed
        )

    if not card.is_terminated:
        # Terminating a card should set its termination date on first call only
        CardModelBroker.terminate_card(session, id=id)

CardLogic

CardLogic(
    card_queries,
    card_reveal_queries,
    card_authentication_actions,
)

This class is the public interface to the card logic.

Implements the following Nullable patterns: - Nullables: https://www.jamesshore.com/v2/projects/nullables/testing-without-mocks#nullables ⧉ - Parameterless instantiation: https://www.jamesshore.com/v2/projects/nullables/testing-without-mocks#instantiation ⧉

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/cards.py
def __init__(
    self,
    card_queries: CardQueries,
    card_reveal_queries: CardRevealQueries,
    card_authentication_actions: CardAuthenticationActions,
) -> None:
    self.card_queries = card_queries
    self.card_reveal_queries = card_reveal_queries
    self.card_authentication_actions = card_authentication_actions

Attributes

card_authentication_actions instance-attribute
card_authentication_actions = card_authentication_actions
card_queries instance-attribute
card_queries = card_queries
card_reveal_queries instance-attribute
card_reveal_queries = card_reveal_queries

Functions

create classmethod
create()

Normal factory

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/cards.py
@classmethod
def create(cls) -> "CardLogic":
    """Normal factory"""
    return cls(
        card_queries=CardQueries(),
        card_reveal_queries=CardRevealQueries.create(),
        card_authentication_actions=CardAuthenticationActions.create(),
    )
create_null classmethod
create_null()

Null factory

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/cards.py
@classmethod
def create_null(cls) -> "CardLogic":
    """Null factory"""
    return cls(
        card_queries=CardQueries(),
        card_reveal_queries=CardRevealQueries.create_null(),
        card_authentication_actions=CardAuthenticationActions.create_null(),
    )
edit_card_authentication_info
edit_card_authentication_info(
    session, /, id, phone_number, email
)

Update the card authentication info. The phone number is mandatory, the email is optional

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/cards.py
@obs.api_call()
def edit_card_authentication_info(
    self,
    session: Session,
    /,
    id: CardId,
    phone_number: str,
    email: str | None,
) -> None:
    """
    Update the card authentication info. The phone number is mandatory, the email is optional
    """
    self.card_authentication_actions.edit_card_authentication_info(
        session,
        id,
        phone_number,
        email,
    )
get_card
get_card(session, id)

Get a card entity from its ID.

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/cards.py
@obs.api_call()
def get_card(
    self,
    session: Session,
    id: CardId,
) -> Card:
    """
    Get a card entity from its ID.
    """
    return self.card_queries.get_card(
        session,
        id,
    )
get_card_ids_for_account
get_card_ids_for_account(session, account_id)

Get all the card IDs for an account.

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/cards.py
@obs.api_call()
def get_card_ids_for_account(
    self,
    session: Session,
    account_id: AccountId,
) -> list[CardId]:
    """
    Get all the card IDs for an account.
    """
    return self.card_queries.get_card_ids_for_account(
        session,
        account_id,
    )
get_card_ids_for_card_holder
get_card_ids_for_card_holder(session, card_holder_id)

Get all the card IDs for a card holder.

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/cards.py
@obs.api_call()
def get_card_ids_for_card_holder(
    self,
    session: Session,
    card_holder_id: CardHolderId,
) -> list[CardId]:
    """
    Get all the card IDs for a card holder.
    """
    return self.card_queries.get_card_ids_for_card_holder(
        session,
        card_holder_id,
    )
get_card_pan_reveal_public_key
get_card_pan_reveal_public_key()

Get the base-64 public key used for client-side encryption of card PAN reveal requests.

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/cards.py
@obs.api_call()
def get_card_pan_reveal_public_key(self) -> str:
    """
    Get the base-64 public key used for client-side encryption of card PAN reveal requests.
    """
    return self.card_reveal_queries.get_card_pan_reveal_public_key()
get_card_pin_reveal_public_key
get_card_pin_reveal_public_key()

Get the base-64 public key used for client-side encryption of card PIN reveal requests.

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/cards.py
@obs.api_call()
def get_card_pin_reveal_public_key(self) -> str:
    """
    Get the base-64 public key used for client-side encryption of card PIN reveal requests.
    """
    return self.card_reveal_queries.get_card_pin_reveal_public_key()
get_cards_for_account
get_cards_for_account(session, account_id)

Get all the cards for an account.

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/cards.py
@obs.api_call()
def get_cards_for_account(
    self,
    session: Session,
    account_id: AccountId,
) -> list[Card]:
    """
    Get all the cards for an account.
    """
    return self.card_queries.get_cards_for_account(
        session,
        account_id,
    )
get_cards_for_card_holder
get_cards_for_card_holder(session, card_holder_id)

Get all the cards for a card holder.

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/cards.py
@obs.api_call()
def get_cards_for_card_holder(
    self,
    session: Session,
    card_holder_id: CardHolderId,
) -> list[Card]:
    """
    Get all the cards for a card holder.
    """
    return self.card_queries.get_cards_for_card_holder(
        session,
        card_holder_id,
    )
reveal_card_default_password
reveal_card_default_password(id)

Reveal the default password of a card.

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/cards.py
@obs.api_call()
def reveal_card_default_password(
    self,
    /,
    id: CardId,
) -> str:
    """
    Reveal the default password of a card.
    """
    return self.card_reveal_queries.reveal_card_default_password(id)
reveal_card_pan
reveal_card_pan(session, id, encrypted_aes_key)

Reveal the PAN of a card.

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/cards.py
@obs.api_call()
def reveal_card_pan(
    self,
    session: Session,
    id: CardId,
    encrypted_aes_key: str,
) -> CardRevealPANData:
    """
    Reveal the PAN of a card.
    """
    return self.card_reveal_queries.reveal_card_pan(
        session,
        id,
        encrypted_aes_key,
    )
reveal_card_pin
reveal_card_pin(session, id, encrypted_aes_key)

Reveal the PAN of a card.

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/cards.py
@obs.api_call()
def reveal_card_pin(
    self,
    session: Session,
    id: CardId,
    encrypted_aes_key: str,
) -> CardRevealPINData:
    """
    Reveal the PAN of a card.
    """
    return self.card_reveal_queries.reveal_card_pin(
        session,
        id,
        encrypted_aes_key,
    )

CardNotFoundException

Bases: PaymentCardException

Exception raised when trying to use a non-existing Card.

CardOrderNotFoundException

Bases: PaymentCardException

Exception raised when trying to use a non-existing Card Order.

CardProvisioningActions

This class contains all the actions related to card provisioning.

Functions

declare_card_provisioning
declare_card_provisioning(
    session,
    /,
    workspace_key,
    external_id,
    card_id,
    provisioning_type,
    provisioning_date,
    wallet_provider,
)

Declare a card provisioning for the given card.

The card provisioning must exist in the PSP workspace.

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/actions/card_provisioning_actions.py
@obs.api_call()
def declare_card_provisioning(
    self,
    session: Session,
    /,
    workspace_key: str,
    external_id: str,
    card_id: CardId,
    provisioning_type: ProvisioningType,
    provisioning_date: datetime,
    wallet_provider: str,
) -> CardProvisioningId:
    """
    Declare a card provisioning for the given card.

    The card provisioning must exist in the PSP workspace.
    """
    card_provisioning = CardProvisioningModelBroker.create_card_provisioning(
        session,
        workspace_key=workspace_key,
        external_id=external_id,
        card_id=card_id,
        provisioning_type=provisioning_type,
        provisioning_date=provisioning_date,
        wallet_provider=wallet_provider,
    )

    return CardProvisioningId(card_provisioning.id)

CardProvisioningQueries

This class contains all the queries related to card provisioning.

Functions

get_all_card_provisioning
get_all_card_provisioning(session, /, card_id)

Get all card provisionings for a given card ID.

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/queries/card_provisioning_queries.py
@obs.api_call()
def get_all_card_provisioning(
    self,
    session: Session,
    /,
    card_id: CardId,
) -> list[CardProvisioning]:
    """
    Get all card provisionings for a given card ID.
    """
    return [
        _to_dataclass(card_provisioning=card_provisioning)
        for card_provisioning in CardProvisioningModelBroker.list_card_provisionings_for_card(
            session, card_id=card_id
        )
    ]

CardStatusLogic

CardStatusLogic(card_status_actions)

This class is the public interface to the card status logic.

Implements the following Nullable patterns: - Nullables: https://www.jamesshore.com/v2/projects/nullables/testing-without-mocks#nullables ⧉ - Parameterless instantiation: https://www.jamesshore.com/v2/projects/nullables/testing-without-mocks#instantiation ⧉

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/card_status.py
def __init__(self, card_status_actions: CardStatusActions) -> None:
    self.card_status_actions = card_status_actions

    # TODO WIP we haven't decided on the pattern we'll use for public events. For now let's declare them as topics.
    self.card_suspended_by_provider_topic = Topic[CardSuspendedByProvider]()

Attributes

card_status_actions instance-attribute
card_status_actions = card_status_actions
card_suspended_by_provider_topic instance-attribute
card_suspended_by_provider_topic = Topic[
    CardSuspendedByProvider
]()

Functions

activate_card
activate_card(session, /, id)

Activate a card.

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/card_status.py
@obs.api_call()
def activate_card(
    self,
    session: Session,
    /,
    id: CardId,
) -> None:
    """
    Activate a card.
    """
    self.card_status_actions.activate_card(
        session,
        id,
    )
close_card
close_card(session, /, id, reason)

Close a card. This action is non-revertible.

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/card_status.py
@obs.api_call()
def close_card(
    self,
    session: Session,
    /,
    id: CardId,
    reason: str,
) -> None:
    """
    Close a card. This action is non-revertible.
    """
    self.card_status_actions.close_card(
        session,
        id,
        reason=reason,
    )
create classmethod
create()

Normal factory

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/card_status.py
@classmethod
def create(cls) -> "CardStatusLogic":
    """Normal factory"""
    return cls(CardStatusActions.create())
create_null classmethod
create_null(track_adyen_requests=None)

Null factory

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/card_status.py
@classmethod
def create_null(
    cls,
    track_adyen_requests: list[tuple[str, dict, dict]] | None = None,  # type: ignore[type-arg]
) -> "CardStatusLogic":
    """Null factory"""
    return cls(
        CardStatusActions.create_null(track_adyen_requests=track_adyen_requests)
    )
suspend_card
suspend_card(
    session, /, id, reason, suspension_source=None
)

Suspend a card.

Source code in components/payment_gateway/subcomponents/cards/protected/business_logic/card_status.py
@obs.api_call()
def suspend_card(
    self,
    session: Session,
    /,
    id: CardId,
    reason: str,
    suspension_source: CardSuspensionSource | None = None,
) -> None:
    """
    Suspend a card.
    """
    self.card_status_actions.suspend_card(
        session,
        id,
        reason=reason,
        suspension_source=suspension_source,
    )

CardTerminatedException

Bases: PaymentCardException

Exception raised when trying to use a terminated Card.

InvalidCardStatusTransitionException

Bases: PaymentCardException, ValueError

Exception raised when attempting an invalid Card status transition.

MissingCardPasswordSaltException

Bases: PaymentCardException

Exception raised when trying to generate a Card password without a password salt.

MissingCardShipmentInfoException

Bases: PaymentCardException, ValueError

Exception raised when trying to create a physical Card without providing shipment info.

PaymentCardException

Bases: PaymentGatewayException

Base class for all Card exceptions.