Skip to content

Adapters

components.payment_gateway.subcomponents.cards.adapters.adyen

helpers

CARD_DELIVERY_STATUS_MAP module-attribute

CARD_DELIVERY_STATUS_MAP = {
    "created": created,
    "processing": pending,
    "produced": pending,
    "shipped": shipped,
    "delivered": delivered,
    "rejected": error,
}

CARD_DISPLAY_NAME_LIMIT module-attribute

CARD_DISPLAY_NAME_LIMIT = 18

The maximum length of the display name for a card. This is the value that we use for branded cards with vertical layout.

DELIVERY_CONTACT_LINE_WIDTH module-attribute

DELIVERY_CONTACT_LINE_WIDTH = 38

The maximum width of each line in the delivery contact address. This value was provided to us by Adyen.

PAYMENT_INSTRUMENT_STATUS_MAP module-attribute

PAYMENT_INSTRUMENT_STATUS_MAP = {
    "Inactive": inactive,
    "Active": active,
    "Suspended": suspended,
    "Closed": closed,
}

from_adyen_card_delivery_status

from_adyen_card_delivery_status(status)

Map Adyen CardOrderItemDeliveryStatus.status string to our own enum.

Returns None for ignored value.

Source code in components/payment_gateway/subcomponents/cards/adapters/adyen/helpers.py
def from_adyen_card_delivery_status(status: str | None) -> CardDeliveryStatus | None:
    """
    Map Adyen CardOrderItemDeliveryStatus.status string to our own enum.

    Returns None for ignored value.
    """
    return CARD_DELIVERY_STATUS_MAP.get(status) if status is not None else None

from_adyen_payment_instrument_status

from_adyen_payment_instrument_status(status)

Map Adyen PaymentInstrument.status string to our own enum.

Returns None for ignored value.

Source code in components/payment_gateway/subcomponents/cards/adapters/adyen/helpers.py
def from_adyen_payment_instrument_status(status: str | None) -> CardStatus | None:
    """
    Map Adyen PaymentInstrument.status string to our own enum.

    Returns None for ignored value.
    """
    return PAYMENT_INSTRUMENT_STATUS_MAP.get(status) if status is not None else None

generate_card_password

generate_card_password(id, salt)

Utility to generate a deterministic password from a UUID and a salt.

This can be used to generate a lifetime 3DS password for a card.

We use SHA256 as a cryptographically secure hash function. The hash is computed on the concatenation of the UUID and the salt, and we extract the first 8 decimal digits of the hash as the password.

Source code in components/payment_gateway/subcomponents/cards/adapters/adyen/helpers.py
def generate_card_password(id: uuid.UUID, salt: str) -> str:
    """
    Utility to generate a deterministic password from a UUID and a salt.

    This can be used to generate a lifetime 3DS password for a card.

    We use SHA256 as a cryptographically secure hash function. The hash is
    computed on the concatenation of the UUID and the salt, and we extract the
    first 8 decimal digits of the hash as the password.
    """

    input_string = f"{id}{salt}"
    hash_object = hashlib.sha256(input_string.encode())
    password_hash = hash_object.hexdigest()

    # Extract the first 8 hex digits, convert them to decimal and keep the first 8 decimal digits
    first_hex_digits = password_hash[:8]
    decimal = int(first_hex_digits, 16)
    return str(decimal)[-8:]

get_short_name

get_short_name(first_name, last_name, limit)

Returns a shortened version of the full name by abbreviating the middle names or truncating the last name if necessary.

Parameters:

Name Type Description Default
first_name str

The first name.

required
last_name str

The last name.

required
limit int

The maximum length of the shortened name.

required

Returns:

Type Description
str

The shortened name.

Source code in components/payment_gateway/subcomponents/cards/adapters/adyen/helpers.py
def get_short_name(first_name: str, last_name: str, limit: int) -> str:
    """
    Returns a shortened version of the full name by abbreviating the middle
    names or truncating the last name if necessary.

    Args:
        first_name: The first name.
        last_name: The last name.
        limit: The maximum length of the shortened name.

    Returns:
        The shortened name.
    """
    return " ".join(get_short_names(first_name, last_name, limit))

get_short_names

get_short_names(first_name, last_name, limit)

Returns a shortened version of the first and last names by abbreviating the middle names or truncating the last name if necessary.

Assumes that both parts will be joined by a single space, hence the sum of their lengths is kept under limit-1.

Parameters:

Name Type Description Default
first_name str

The first name.

required
last_name str

The last name.

required
limit int

The maximum length of the shortened name.

required

Returns:

Type Description
tuple[str, str]

A tuple containing the shortened first and last names.

Source code in components/payment_gateway/subcomponents/cards/adapters/adyen/helpers.py
def get_short_names(first_name: str, last_name: str, limit: int) -> tuple[str, str]:
    """
    Returns a shortened version of the first and last names by abbreviating the
    middle names or truncating the last name if necessary.

    Assumes that both parts will be joined by a single space, hence the sum of
    their lengths is kept under limit-1.

    Args:
        first_name: The first name.
        last_name: The last name.
        limit: The maximum length of the shortened name.

    Returns:
        A tuple containing the shortened first and last names.
    """

    if not limit or len(first_name) + len(last_name) <= limit - 1:
        return (first_name, last_name)

    # First try abbreviating the middle names, leaving the first name intact
    first_names = first_name.split()
    if len(first_names) > 1:
        middle_names = first_names[1:]
        abbreviated_middle_names = "".join(
            [middle_name[0] + "." for middle_name in middle_names]
        )
        abbreviated_first_names = f"{first_names[0]} {abbreviated_middle_names}"
        if len(abbreviated_first_names) + len(last_name) <= limit - 1:
            return (abbreviated_first_names, last_name)

    # Then try abbreviating the first and middle names
    abbreviated_first_names = "".join([name[0] + "." for name in first_names])
    if len(abbreviated_first_names) + len(last_name) <= limit - 1:
        return (abbreviated_first_names, last_name)

    # Try abbreviating all but the last name
    last_names = last_name.split()
    if len(last_names) > 1:
        middle_names = last_names[:-1]
        abbreviated_middle_names = "".join(
            [middle_name[0] + "." for middle_name in middle_names]
        )
        abbreviated_last_names = f"{abbreviated_middle_names} {last_names[-1]}"
        if len(abbreviated_first_names) + len(abbreviated_last_names) <= limit - 1:
            return (abbreviated_first_names, abbreviated_last_names)

    # Truncate the last name as a last resort
    return (
        abbreviated_first_names,
        last_name[: limit - len(abbreviated_first_names) - 1],
    )

remove_accents

remove_accents(s)

Remove accents from a string.

Source code in components/payment_gateway/subcomponents/cards/adapters/adyen/helpers.py
def remove_accents(s: str) -> str:
    """Remove accents from a string."""
    return unicodedata.normalize("NFKD", s).encode("ascii", "ignore").decode("utf-8")

to_adyen_payment_instrument_info

to_adyen_payment_instrument_info(
    configuration,
    card_holder,
    account,
    form_factor,
    phone_number,
    email,
    shipment_info,
    password,
    description=None,
    reference=None,
)

Convert our card holder model into an Adyen PaymentInstrumentInfo for API creation calls.

shipment_info MUST be provided for physical cards.

Source code in components/payment_gateway/subcomponents/cards/adapters/adyen/helpers.py
def to_adyen_payment_instrument_info(
    configuration: CardIssuingConfiguration,
    card_holder: CardHolder,
    account: Account,
    form_factor: CardFormFactor,
    phone_number: str,
    email: str | None,
    shipment_info: CardShipmentInfo | None,
    password: str | None,
    description: str | None = None,
    reference: str | None = None,
) -> PaymentInstrumentInfo:
    """
    Convert our card holder model into an Adyen PaymentInstrumentInfo for API creation calls.

    `shipment_info` MUST be provided for physical cards.
    """

    card_info = _to_card_info(
        configuration=configuration,
        card_holder=card_holder,
        form_factor=form_factor,
        shipment_info=shipment_info,
        password=password,
        phone_number=phone_number,
        email=email,
    )
    description = description or _get_card_description(card_holder, form_factor)

    return PaymentInstrumentInfo(
        type="card",
        balanceAccountId=account.external_id,
        issuingCountryCode=configuration.country_code.to_iso_alpha2(),
        status="inactive",  # Create cards as inactive, let the business logic activate them
        card=card_info,
        description=description,
        reference=reference,
    )

topic_subscribers

CardOrderTopicSubscriber

CardOrderTopicSubscriber(card_order_tracking_policy)

Bases: Subscriber

This class subscribes to the Adyen card order topic messages and forwards them to the card order tracking policy.

Source code in components/payment_gateway/subcomponents/cards/adapters/adyen/topic_subscribers.py
def __init__(self, card_order_tracking_policy: CardOrderTrackingPolicy) -> None:
    self.card_order_tracking_policy = card_order_tracking_policy
card_order_tracking_policy instance-attribute
card_order_tracking_policy = card_order_tracking_policy
receive
receive(message)
Source code in components/payment_gateway/subcomponents/cards/adapters/adyen/topic_subscribers.py
@override
@obs.event_subscriber()
def receive(self, message: CardOrderNotificationRequest) -> None:
    from shared.helpers.db import current_session

    if message.data.card is None:
        # Ignore non-card order events (e.g. PIN).
        # See ADR-3 https://www.notion.so/alaninsurance/Don-t-track-payment-card-PIN-order-af478e5377d54a249984659b1a9d1e62?pvs=4
        current_logger.warning(
            "Ignoring Adyen card order webhook with no card info (PIN order?)",
            message=message,
        )
        return

    try:
        match message.type:
            case "balancePlatform.cardorder.created":
                self.card_order_tracking_policy.on_card_order_created(message.data)
            case "balancePlatform.cardorder.updated":
                self.card_order_tracking_policy.on_card_order_updated(message.data)
            case _:
                assert_never(message.type)  # Exhaustiveness check
        current_session.commit()
    except PaymentCardException as e:
        # Domain exceptions may occur if our model is out-of-sync with Adyen
        # and we get events on entities that don't exist in our DB. Simply
        # log them and alert for further investigation.
        current_logger.exception(
            "Error while processing Adyen card order webhook",
            exception=e,
        )
        alert_on_error_processing_card_order_notification(
            request=message,
            message="A PaymentCardException occurred while processing the card order notification",
        )
    except Exception as e:
        # Something's wrong so let's log and alert.
        current_logger.exception(
            "Unknown exception",
            exception=e,
        )
        alert_on_error_processing_card_order_notification(
            request=message,
            message="An unexpected error occurred while processing the card order notification",
        )

PaymentInstrumentTopicSubscriber

PaymentInstrumentTopicSubscriber(
    card_status_handling_policy,
)

Bases: Subscriber

This class subscribes to the Adyen payment instrument topic messages and forwards update events to the card status handling policy.

Source code in components/payment_gateway/subcomponents/cards/adapters/adyen/topic_subscribers.py
def __init__(self, card_status_handling_policy: CardStatusHandlingPolicy) -> None:
    self.card_status_handling_policy = card_status_handling_policy
card_status_handling_policy instance-attribute
card_status_handling_policy = card_status_handling_policy
receive
receive(message)
Source code in components/payment_gateway/subcomponents/cards/adapters/adyen/topic_subscribers.py
@override
@obs.event_subscriber()
def receive(self, message: PaymentNotificationRequest) -> None:
    from shared.helpers.db import current_session

    try:
        match message.type:
            case "balancePlatform.paymentInstrument.created":
                # Simply ignore create events as we only deal with cards
                # that we created ourselves and hence already exist. This
                # will avoid race conditions when getting creation events on
                # cards while we're still creating them.
                current_logger.info(
                    "Ignoring Adyen payment instrument creation webhook",
                    message=message,
                )
            case "balancePlatform.paymentInstrument.updated":
                self.card_status_handling_policy.on_payment_instrument_updated(
                    message.data
                )
            case _:
                assert_never(message.type)  # Exhaustiveness check
        current_session.commit()
    except PaymentCardException as e:
        # Domain exceptions may occur if our model is out-of-sync with Adyen
        # and we get events on entities that don't exist in our DB. Simply
        # log them and alert for further investigation.
        current_logger.exception(
            "Error while processing Adyen payment instrument webhook",
            exception=e,
        )
        alert_on_error_processing_payment_instrument_notification(
            request=message,
            message="A PaymentCardException occurred while processing the card order notification",
        )
    except Exception as e:
        # Something's wrong so let's log and alert.
        current_logger.exception(
            "Unknown exception",
            exception=e,
        )
        alert_on_error_processing_payment_instrument_notification(
            request=message,
            message="An unexpected error occurred while processing the card order notification",
        )