Skip to content

Actions

components.payment_gateway.subcomponents.cards.business_logic.actions.card_authentication_actions

CARD_ISSUING_CONFIGURATIONS_CONFIG_KEY module-attribute

CARD_ISSUING_CONFIGURATIONS_CONFIG_KEY = (
    "ADYEN_CARD_ISSUING_CONFIGURATIONS"
)

CardAuthenticationActions

CardAuthenticationActions(
    adyen_client,
    default_card_configuration_key,
    card_configurations,
    card_reveal_queries,
)

This class contains all the actions relative to card authentication.

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/business_logic/actions/card_authentication_actions.py
def __init__(
    self,
    adyen_client: "AdyenPaymentInstrumentsApiClient | None",
    default_card_configuration_key: str | None,
    card_configurations: dict[str, dict[str, str]],
    card_reveal_queries: CardRevealQueries,
) -> None:
    self._adyen_client = adyen_client
    self.default_card_configuration_key = default_card_configuration_key
    self.card_configurations = card_configurations
    self.card_reveal_queries = card_reveal_queries

adyen_client property

adyen_client

Ensures the Adyen client is available when accessing it.

card_configurations instance-attribute

card_configurations = card_configurations

card_reveal_queries instance-attribute

card_reveal_queries = card_reveal_queries

create classmethod

create()

Normal factory

Source code in components/payment_gateway/subcomponents/cards/business_logic/actions/card_authentication_actions.py
@classmethod
def create(cls) -> "CardAuthenticationActions":
    """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 = {}

    return cls(
        adyen_client=adyen_client,
        default_card_configuration_key=default_card_configuration_key,
        card_configurations=card_configurations,
        card_reveal_queries=CardRevealQueries.create(),
    )

create_null classmethod

create_null(track_adyen_requests=None)

Null factory

Source code in components/payment_gateway/subcomponents/cards/business_logic/actions/card_authentication_actions.py
@classmethod
def create_null(
    cls,
    track_adyen_requests: list[tuple[str, dict, dict]] | None = None,  # type: ignore[type-arg]
) -> "CardAuthenticationActions":
    """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
    )

    # Dummy configuration for null client
    default_card_configuration_key = "dummy_configuration"
    card_configurations = {
        "dummy_configuration": {
            "country_code": "FR",
            "brand": "dummy_brand",
            "brand_variant": "dummy_brand_variant",
            "profile_id": "dummy_profile_id",
        },
    }

    return cls(
        adyen_client=adyen_client,
        default_card_configuration_key=default_card_configuration_key,
        card_configurations=card_configurations,
        card_reveal_queries=CardRevealQueries.create_null(),
    )

default_card_configuration_key instance-attribute

default_card_configuration_key = (
    default_card_configuration_key
)

edit_card_authentication_info

edit_card_authentication_info(
    session, /, id, phone_number, email
)

Edit the contact info for card card authentication.

Note

This operation is currently only supported for Adyen.

Source code in components/payment_gateway/subcomponents/cards/business_logic/actions/card_authentication_actions.py
def edit_card_authentication_info(
    self,
    session: Session,
    /,
    id: CardId,
    phone_number: str,
    email: str | None,
) -> None:
    """
    Edit the contact info for card card authentication.

    Note:
        This operation is currently only supported for Adyen.
    """
    from components.payment_gateway.subcomponents.cards.adapters.adyen.helpers import (
        _to_authentication,
    )
    from shared.services.payment_providers.adyen.openapi.balance_platform_service_v2 import (
        CardInfo,
        PaymentInstrumentUpdateRequest,
    )

    with raise_if_card_not_found(id):
        card = CardModelBroker.get_card(session, id=id)
    with raise_if_card_configuration_not_found(card.configuration_key):
        configuration_key = mandatory(
            card.configuration_key or self.default_card_configuration_key
        )
        card_configuration = self.card_configurations[configuration_key]
    raise_on_terminated_card(card)

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

    # Adyen requires both the phone number/email and the password to
    # be updated at the same time, so get the password here
    password = self.card_reveal_queries.reveal_card_default_password(id)
    authentication = _to_authentication(
        phone_number=phone_number, email=email, password=password
    )

    card_info = CardInfo(
        authentication=authentication,
        cardholderName=card.display_name,
        brand=card_configuration["brand"],
        brandVariant=card_configuration["brand_variant"],
        formFactor=("virtual" if card.is_virtual else "physical"),
    )

    self.adyen_client.update_payment_instrument(
        card.external_id,
        request=PaymentInstrumentUpdateRequest(
            card=card_info,
        ),
        idempotency_key=None,  # TODO use this for retries
    )

DEFAULT_CARD_ISSUING_CONFIGURATION_KEY_CONFIG_KEY module-attribute

DEFAULT_CARD_ISSUING_CONFIGURATION_KEY_CONFIG_KEY = (
    "ADYEN_DEFAULT_CARD_ISSUING_CONFIGURATION_KEY"
)

components.payment_gateway.subcomponents.cards.business_logic.actions.card_delivery_actions

CardDeliveryActions

This class contains all the actions related to the delivery of a card.

create_card_order

create_card_order(
    session,
    /,
    workspace_key,
    external_card_id,
    delivery_status,
    shipping_method,
    tracking_number=None,
)

Create a card order upon reception of a card order creation event.

Source code in components/payment_gateway/subcomponents/cards/business_logic/actions/card_delivery_actions.py
def create_card_order(
    self,
    session: Session,
    /,
    workspace_key: str,
    external_card_id: str,
    delivery_status: CardDeliveryStatus,
    shipping_method: str,
    tracking_number: str | None = None,
) -> CardOrderId:
    """
    Create a card order upon reception of a card order creation event.
    """

    with raise_if_card_not_found_for_external_id(external_card_id):
        card_id = CardModelBroker.get_card_id_by_external_id(
            session,
            workspace_key=workspace_key,
            external_id=external_card_id,
        )

    # TODO what should we do if an order already exists for the same card or the same external ID?
    card_order = CardOrderModelBroker.create_card_order(
        session,
        card_id=card_id,
        delivery_status=delivery_status,
        shipping_method=shipping_method,
        tracking_number=tracking_number,
    )
    return CardOrderId(card_order.id)

declare_card_not_received

declare_card_not_received(session, /, id)

Declare that a card has not been received by the card holder.

This can happen if the card has been lost or stolen during the shipment.

Source code in components/payment_gateway/subcomponents/cards/business_logic/actions/card_delivery_actions.py
def declare_card_not_received(
    self,
    session: Session,
    /,
    id: CardId,
) -> None:
    """
    Declare that a card has not been received by the card holder.

    This can happen if the card has been lost or stolen during the shipment.
    """

    # TODO
    raise NotImplementedError()

declare_card_received

declare_card_received(session, /, id)

Declare that a card has been received by the card holder.

Source code in components/payment_gateway/subcomponents/cards/business_logic/actions/card_delivery_actions.py
def declare_card_received(
    self,
    session: Session,
    /,
    id: CardId,
) -> None:
    """
    Declare that a card has been received by the card holder.
    """

    # TODO
    raise NotImplementedError()

update_card_order

update_card_order(
    session,
    /,
    workspace_key,
    external_card_id,
    delivery_status,
    tracking_number=None,
)

Update a card order upon reception of a card order update event.

Source code in components/payment_gateway/subcomponents/cards/business_logic/actions/card_delivery_actions.py
def update_card_order(
    self,
    session: Session,
    /,
    workspace_key: str,
    external_card_id: str,
    delivery_status: CardDeliveryStatus,
    tracking_number: str | None = None,
) -> None:
    """
    Update a card order upon reception of a card order update event.
    """

    with raise_if_card_not_found_for_external_id(external_card_id):
        card_id = CardModelBroker.get_card_id_by_external_id(
            session,
            workspace_key=workspace_key,
            external_id=external_card_id,
        )

    with raise_if_card_order_not_found_for_card_id(card_id):
        card_order = CardOrderModelBroker.get_card_order_for_card(
            session, card_id=card_id
        )

    CardOrderModelBroker.set_card_order_delivery_status(
        session,
        id=card_order.id,
        delivery_status=delivery_status,
    )
    if tracking_number is not None:
        CardOrderModelBroker.set_card_order_tracking_number(
            session,
            id=card_order.id,
            tracking_number=tracking_number,
        )

components.payment_gateway.subcomponents.cards.business_logic.actions.card_holder_actions

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/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

card_authentication_actions instance-attribute

card_authentication_actions = card_authentication_actions

card_logic instance-attribute

card_logic = card_logic

create classmethod

create()

Normal factory

Source code in components/payment_gateway/subcomponents/cards/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/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/business_logic/actions/card_holder_actions.py
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/business_logic/actions/card_holder_actions.py
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/business_logic/actions/card_holder_actions.py
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/business_logic/actions/card_holder_actions.py
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,
        )

components.payment_gateway.subcomponents.cards.business_logic.actions.card_incidents_actions

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/business_logic/actions/card_incidents_actions.py
def __init__(
    self,
    adyen_client: "AdyenPaymentInstrumentsApiClient | None",
) -> None:
    self._adyen_client = adyen_client

adyen_client property

adyen_client

Ensures the Adyen client is available when accessing it.

create classmethod

create()

Normal factory

Source code in components/payment_gateway/subcomponents/cards/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/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/business_logic/actions/card_incidents_actions.py
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 (
        PaymentInstrumentUpdateRequest,
    )

    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=PaymentInstrumentUpdateRequest(
            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/business_logic/actions/card_incidents_actions.py
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 (
        PaymentInstrumentUpdateRequest,
    )

    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=PaymentInstrumentUpdateRequest(
            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/business_logic/actions/card_incidents_actions.py
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 (
        PaymentInstrumentUpdateRequest,
    )

    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=PaymentInstrumentUpdateRequest(
            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
)

Declare a card as temporarily suspended.

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

    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=PaymentInstrumentUpdateRequest(
            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="temporarily suspended",
        suspension_source=suspension_source,
    )

components.payment_gateway.subcomponents.cards.business_logic.actions.card_lifecycle_actions

CARD_ISSUING_CONFIGURATIONS_CONFIG_KEY module-attribute

CARD_ISSUING_CONFIGURATIONS_CONFIG_KEY = (
    "ADYEN_CARD_ISSUING_CONFIGURATIONS"
)

CARD_PASSWORD_SALT_CONFIG_KEY module-attribute

CARD_PASSWORD_SALT_CONFIG_KEY = (
    "ADYEN_CARD_PASSWORD_SALT_SECRET_NAME"
)

CARD_PASSWORD_SALT_DEFAULT_KEY module-attribute

CARD_PASSWORD_SALT_DEFAULT_KEY = 'ADYEN_CARD_PASSWORD_SALT'

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/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

adyen_client property

adyen_client

Ensures the Adyen client is available when accessing it.

card_configurations instance-attribute

card_configurations = card_configurations

create classmethod

create()

Normal factory

Source code in components/payment_gateway/subcomponents/cards/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/business_logic/actions/card_lifecycle_actions.py
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/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,
    )

default_card_configuration_key instance-attribute

default_card_configuration_key = (
    default_card_configuration_key
)

password_salt instance-attribute

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/business_logic/actions/card_lifecycle_actions.py
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 (
        PaymentInstrumentUpdateRequest,
    )

    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=PaymentInstrumentUpdateRequest(
                    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)

DEFAULT_CARD_ISSUING_CONFIGURATION_KEY_CONFIG_KEY module-attribute

DEFAULT_CARD_ISSUING_CONFIGURATION_KEY_CONFIG_KEY = (
    "ADYEN_DEFAULT_CARD_ISSUING_CONFIGURATION_KEY"
)

components.payment_gateway.subcomponents.cards.business_logic.actions.card_provisioning_actions

CardProvisioningActions

This class contains all the actions related to card provisioning.

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/business_logic/actions/card_provisioning_actions.py
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)

components.payment_gateway.subcomponents.cards.business_logic.actions.card_renewal_actions

CardRenewalActions

This class contains all the actions related to the renewal of a card.

renew_card

renew_card(id)

Renew a card.

Renewal can be triggered by the business logic or the card holder. For example, if the card is about to expire or is declared lost, stolen or damaged.

Source code in components/payment_gateway/subcomponents/cards/business_logic/actions/card_renewal_actions.py
def renew_card(
    self,
    id: CardId,
) -> None:
    """
    Renew a card.

    Renewal can be triggered by the business logic or the card holder. For example,
    if the card is about to expire or is declared lost, stolen or damaged.
    """

    # TODO
    raise NotImplementedError()

components.payment_gateway.subcomponents.cards.business_logic.actions.card_status_actions

CardStatusActions

CardStatusActions(adyen_client)

This class contains all the actions used to change the status 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/business_logic/actions/card_status_actions.py
def __init__(
    self,
    adyen_client: "AdyenPaymentInstrumentsApiClient | None",
) -> None:
    self._adyen_client = adyen_client

activate_card

activate_card(session, /, id)

Activate a card.

Source code in components/payment_gateway/subcomponents/cards/business_logic/actions/card_status_actions.py
def activate_card(
    self,
    session: Session,
    /,
    id: CardId,
) -> None:
    """
    Activate a card.
    """
    from shared.services.payment_providers.adyen.openapi.balance_platform_service_v2 import (
        PaymentInstrumentUpdateRequest,
    )

    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.active)

    self.adyen_client.update_payment_instrument(
        card.external_id,
        request=PaymentInstrumentUpdateRequest(
            status="active",
        ),
        idempotency_key=None,  # TODO use this for retries
    )
    CardModelBroker.set_card_status(session, id=card.id, status=CardStatus.active)

adyen_client property

adyen_client

Ensures the Adyen client is available when accessing it.

close_card

close_card(session, /, id, reason=None)

Close a card. This is non-revertible.

Source code in components/payment_gateway/subcomponents/cards/business_logic/actions/card_status_actions.py
def close_card(
    self,
    session: Session,
    /,
    id: CardId,
    reason: str | None = None,
) -> None:
    """
    Close a card. This is non-revertible.
    """
    from shared.services.payment_providers.adyen.openapi.balance_platform_service_v2 import (
        PaymentInstrumentUpdateRequest,
    )

    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.closed)

    self.adyen_client.update_payment_instrument(
        card.external_id,
        request=PaymentInstrumentUpdateRequest(
            status="closed",
            statusReason="endOfLife",
            statusComment=reason,
        ),
        idempotency_key=None,  # TODO use this for retries
    )
    CardModelBroker.set_card_status(session, id=card.id, status=CardStatus.closed)

create classmethod

create()

Normal factory

Source code in components/payment_gateway/subcomponents/cards/business_logic/actions/card_status_actions.py
@classmethod
def create(cls) -> "CardStatusActions":
    """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/business_logic/actions/card_status_actions.py
@classmethod
def create_null(
    cls,
    track_adyen_requests: list[tuple[str, dict, dict]] | None = None,  # type: ignore[type-arg]
) -> "CardStatusActions":
    """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
        )
    )

suspend_card

suspend_card(
    session, /, id, reason=None, suspension_source=None
)

Suspend a card.

Source code in components/payment_gateway/subcomponents/cards/business_logic/actions/card_status_actions.py
def suspend_card(
    self,
    session: Session,
    /,
    id: CardId,
    reason: str | None = None,
    suspension_source: CardSuspensionSource | None = None,
) -> None:
    """
    Suspend a card.
    """
    from shared.services.payment_providers.adyen.openapi.balance_platform_service_v2 import (
        PaymentInstrumentUpdateRequest,
    )

    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=PaymentInstrumentUpdateRequest(
            status="suspended",
            statusReason="other" if reason is not None else None,
            statusComment=reason,
        ),
        idempotency_key=None,  # TODO use this for retries
    )
    CardModelBroker.set_card_status(
        session,
        id=card.id,
        status=CardStatus.suspended,
        reason=reason,
        suspension_source=suspension_source,
    )