Skip to content

Actions

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

BRAND_KEY module-attribute

BRAND_KEY = 'ADYEN_CARD_ISSUING_BRAND'

BRAND_VARIANT_KEY module-attribute

BRAND_VARIANT_KEY = 'ADYEN_CARD_ISSUING_BRAND_VARIANT'

COUNTRY_CODE_KEY module-attribute

COUNTRY_CODE_KEY = 'ADYEN_CARD_ISSUING_COUNTRY_CODE'

CardAuthenticationActions

CardAuthenticationActions(
    adyen_client, configuration, 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",
    configuration: CardIssuingConfiguration | None,
    card_reveal_queries: CardRevealQueries,
) -> None:
    self._adyen_client = adyen_client
    self.configuration = configuration
    self.card_reveal_queries = card_reveal_queries

adyen_client property

adyen_client

Ensures the Adyen client is available when accessing it.

card_reveal_queries instance-attribute

card_reveal_queries = card_reveal_queries

configuration instance-attribute

configuration = configuration

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.adyen.clients.adyen_payment_instruments_api_client import (
        AdyenPaymentInstrumentsApiClient,
    )
    from shared.services.adyen.clients.exceptions import (
        AdyenClientMissingCredentialsException,
    )

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

    try:
        # TODO @frederic.bonnet 2024-06-30: add configuration_id to the card model
        # For now use the default configuration for all card, this will work
        # because we only use brand and brand_variant to update the cards below,
        # and all configs use the same values.
        configuration = CardIssuingConfiguration(
            country_code=CountryCode.from_iso_alpha2(
                current_config[COUNTRY_CODE_KEY]
            ),
            brand=current_config[BRAND_KEY],
            brand_variant=current_config[BRAND_VARIANT_KEY],
            profile_id=current_config[PROFILE_ID_KEY],
        )
    except KeyError:
        configuration = None

    return cls(
        adyen_client=adyen_client,
        configuration=configuration,
        card_reveal_queries=CardRevealQueries.create(),
    )

create_null classmethod

create_null(configuration=None, 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,
    configuration: CardIssuingConfiguration | None = None,
    track_adyen_requests: list[tuple[str, dict, dict]] | None = None,  # type: ignore[type-arg]
) -> "CardAuthenticationActions":
    """Null factory"""
    from shared.services.adyen.clients.adyen_payment_instruments_api_client import (
        AdyenPaymentInstrumentsApiClient,
    )

    adyen_client = AdyenPaymentInstrumentsApiClient.create_null(
        track_requests=track_adyen_requests
    )
    # Dummy configuration for null client
    if configuration is None:
        configuration = CardIssuingConfiguration(
            country_code=CountryCode.FRANCE,
            brand="dummy_brand",
            brand_variant="dummy_brand_variant",
            profile_id="dummy_profile_id",
        )

    return cls(
        adyen_client=adyen_client,
        configuration=configuration,
        card_reveal_queries=CardRevealQueries.create_null(),
    )

edit_card_authentication_info

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

Edit the contact info for card card authentication.

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.
    """
    from components.payment_gateway.subcomponents.cards.adapters.adyen.helpers import (
        _to_authentication,
    )
    from shared.services.adyen.openapi.balance_platform_service_v2 import (
        CardInfo,
        PaymentInstrumentUpdateRequest,
    )

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

    if self.configuration is None:
        raise MissingCardIssuingConfigurationException()

    raise_on_terminated_card(card)

    # 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=self.configuration.brand,
        brandVariant=self.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
    )

PROFILE_ID_KEY module-attribute

PROFILE_ID_KEY = 'ADYEN_CARD_ISSUING_PROFILE_ID'

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,
    /,
    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,
    /,
    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,
            provider=PaymentServiceProvider.adyen,
            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,
    /,
    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,
    /,
    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,
            provider=PaymentServiceProvider.adyen,
            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,
    /,
    first_name,
    last_name,
    display_name=None,
    short_name=None,
    provider=PaymentServiceProvider.adyen,
    external_id=None,
)

Declare a card holder.

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

    card_holder = CardHolderModelBroker.create_card_holder(
        session,
        first_name=first_name,
        last_name=last_name,
        display_name=display_name,
        short_name=short_name,
        provider=provider,
        external_id=external_id,
    )
    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.adyen.clients.adyen_payment_instruments_api_client import (
        AdyenPaymentInstrumentsApiClient,
    )
    from shared.services.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.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.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.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.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.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

BRAND_KEY module-attribute

BRAND_KEY = 'ADYEN_CARD_ISSUING_BRAND'

BRAND_VARIANT_KEY module-attribute

BRAND_VARIANT_KEY = 'ADYEN_CARD_ISSUING_BRAND_VARIANT'

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'

COUNTRY_CODE_KEY module-attribute

COUNTRY_CODE_KEY = 'ADYEN_CARD_ISSUING_COUNTRY_CODE'

CardLifecycleActions

CardLifecycleActions(
    adyen_client, configuration, 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",
    configuration: CardIssuingConfiguration | None,
    password_salt: str | None,
) -> None:
    self._adyen_client = adyen_client
    self.configuration = configuration
    self.password_salt = password_salt

adyen_client property

adyen_client

Ensures the Adyen client is available when accessing it.

configuration instance-attribute

configuration = configuration

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.adyen.clients.adyen_payment_instruments_api_client import (
        AdyenPaymentInstrumentsApiClient,
    )
    from shared.services.adyen.clients.exceptions import (
        AdyenClientMissingCredentialsException,
    )

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

    try:
        configuration = CardIssuingConfiguration(
            country_code=CountryCode.from_iso_alpha2(
                current_config[COUNTRY_CODE_KEY]
            ),
            brand=current_config[BRAND_KEY],
            brand_variant=current_config[BRAND_VARIANT_KEY],
            profile_id=current_config[PROFILE_ID_KEY],
        )
    except KeyError:
        configuration = None

    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,
        configuration=configuration,
        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=None,
)

Create a card for a card holder.

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,
    # TODO @frederic.bonnet 2024-06-30: pass configuration_id instead
    configuration: CardIssuingConfiguration | None = None,
) -> CardId:
    """
    Create a card for a card holder.
    """
    # 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()

    configuration = configuration or self.configuration
    if configuration is None:
        raise MissingCardIssuingConfigurationException()

    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)

    # 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(
        configuration=configuration,
        card_holder=card_holder,
        account=account,
        form_factor=form_factor,
        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,
        display_name=payment_instrument.card.cardholderName,
        expiration_date=expiration_date,
        last_four_digits=mandatory(payment_instrument.card.lastFour),
        is_virtual=(form_factor == CardFormFactor.virtual),
        provider=PaymentServiceProvider.adyen,
        external_id=payment_instrument.id,
        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(configuration=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,
    configuration: CardIssuingConfiguration | None = None,
    track_adyen_requests: list[tuple[str, dict, dict]] | None = None,  # type: ignore[type-arg]
) -> "CardLifecycleActions":
    """Null factory"""
    from shared.services.adyen.clients.adyen_payment_instruments_api_client import (
        AdyenPaymentInstrumentsApiClient,
    )

    adyen_client = AdyenPaymentInstrumentsApiClient.create_null(
        track_requests=track_adyen_requests
    )
    # Dummy configuration for null client
    if configuration is None:
        configuration = CardIssuingConfiguration(
            country_code=CountryCode.FRANCE,
            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,
        configuration=configuration,
        password_salt=password_salt,
    )

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.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:
        # Terminating a card should close it at Adyen
        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)

PROFILE_ID_KEY module-attribute

PROFILE_ID_KEY = 'ADYEN_CARD_ISSUING_PROFILE_ID'

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

CardProvisioningActions

This class contains all the actions related to card provisioning.

create_card_provisioning

create_card_provisioning(
    session,
    /,
    card_id,
    provider,
    external_id,
    provisioning_type,
    provisioning_date,
    wallet_provider,
)

Create a card provisioning for the given card.

Source code in components/payment_gateway/subcomponents/cards/business_logic/actions/card_provisioning_actions.py
def create_card_provisioning(
    self,
    session: Session,
    /,
    card_id: CardId,
    provider: PaymentServiceProvider,
    external_id: str,
    provisioning_type: ProvisioningType,
    provisioning_date: datetime,
    wallet_provider: str,
) -> CardProvisioningId:
    """
    Create a card provisioning for the given card.
    """

    card_provisioning = CardProvisioningModelBroker.create_card_provisioning(
        session,
        card_id=card_id,
        provider=provider,
        external_id=external_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.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.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.adyen.clients.adyen_payment_instruments_api_client import (
        AdyenPaymentInstrumentsApiClient,
    )
    from shared.services.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.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.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,
    )