Skip to content

Api reference

components.payment_method.public.api

Entry point of the public API.

PaymentMethodService

A service class to encapsulate the public API of the payment method component.

attach_signed_debit_mandate_to_sepa_direct_debit_payment_method staticmethod

attach_signed_debit_mandate_to_sepa_direct_debit_payment_method(
    *, payment_method_id, mandate_info, session=None
)

Attaches a signed SEPA mandate to an existing payment method.

When to Use

Use this function when you want to orchestrate the creation of the payment method from outside PaymentMethodService and use the PaymentMethodService exclusively for storage of the result.

Example use case: UCE creating an unsigned payment method from Marmot and attaching a mandate later via the Proposal Builder.

Parameters:

Name Type Description Default
payment_method_id UUID

UUID of the payment method to attach the mandate to

required
mandate_info _MandateDocumentInfo

Dictionary containing mandate document information with the following fields:

  • uri (str): S3 URI or URL where the signed mandate document is stored
  • unique_reference (str): SEPA-compliant unique identifier for the mandate (max 35 chars, Latin chars + spaces only)
  • signer (Signer): Identity of the person who signed the mandate
  • signature_id (str, optional): Dropbox Sign signature identifier
  • signature_request_id (str, optional): Dropbox Sign request identifier
  • signature_step (str, optional): Current step in the signature process
  • signature_event (dict, optional): Raw Dropbox Sign webhook payload
  • signed_at (datetime, optional): When the mandate was signed, defaults to utcnow()
required
session Session | None

Optional database session to use

None

Returns:

Type Description
tuple[BillingCustomer, SepaDirectDebitPaymentMethod]

The billing customer and the payment method with attached mandate.

Raises:

Type Description
`BaseErrorCode.missing_resource`

If the payment method does not exist

ValueError

If the signer field is None

ValueError

If unique_reference is malformed (> 35 chars or non-Latin chars)

Examples:

customer, payment_method = (
    PaymentMethodService.attach_signed_debit_mandate_to_sepa_direct_debit_payment_method(
        payment_method_id=pm.id,
        mandate_info={
            "uri": "s3://alan-documents/mandates/signed-mandate-789012.pdf",
            "unique_reference": "MANDATE789012",
            "signer": Signer(
                email="john@example.com",
                first_name="John",
                last_name="Doe"
            )
        }
    )
)
Source code in components/payment_method/public/api.py
@staticmethod
def attach_signed_debit_mandate_to_sepa_direct_debit_payment_method(
    *,
    payment_method_id: "uuid.UUID",
    mandate_info: _MandateDocumentInfo,
    session: "Session | None" = None,
) -> "tuple[BillingCustomer, SepaDirectDebitPaymentMethod]":
    """Attaches a signed SEPA mandate to an existing payment method.

    !!! info "When to Use"
        Use this function when you want to orchestrate the creation of the payment method
        from outside `PaymentMethodService` and use the `PaymentMethodService` exclusively for
        storage of the result.

        **Example use case:** UCE creating an unsigned payment method from Marmot and
        attaching a mandate later via the Proposal Builder.

    Args:
        payment_method_id: UUID of the payment method to attach the mandate to
        mandate_info: Dictionary containing mandate document information with the following fields:

            - uri (str): S3 URI or URL where the signed mandate document is stored
            - unique_reference (str): SEPA-compliant unique identifier for the mandate (max 35 chars, Latin chars + spaces only)
            - signer (Signer): Identity of the person who signed the mandate
            - signature_id (str, optional): Dropbox Sign signature identifier
            - signature_request_id (str, optional): Dropbox Sign request identifier
            - signature_step (str, optional): Current step in the signature process
            - signature_event (dict, optional): Raw Dropbox Sign webhook payload
            - signed_at (datetime, optional): When the mandate was signed, defaults to utcnow()

        session: Optional database session to use

    Returns:
        The billing customer and the payment method with attached mandate.

    Raises:
        `BaseErrorCode.missing_resource`: If the payment method does not exist
        ValueError: If the `signer` field is None
        ValueError: If `unique_reference` is malformed (> 35 chars or non-Latin chars)

    Examples:
        ```python
        customer, payment_method = (
            PaymentMethodService.attach_signed_debit_mandate_to_sepa_direct_debit_payment_method(
                payment_method_id=pm.id,
                mandate_info={
                    "uri": "s3://alan-documents/mandates/signed-mandate-789012.pdf",
                    "unique_reference": "MANDATE789012",
                    "signer": Signer(
                        email="john@example.com",
                        first_name="John",
                        last_name="Doe"
                    )
                }
            )
        )
        ```
    """
    from components.payment_method.internal.business_logic.actions import (
        attach_signed_debit_mandate_to_sepa_direct_debit_payment_method as attach_signed_debit_mandate_to_sepa_direct_debit_payment_method_logic,
    )

    billing_customer_repository = _get_billing_customer_repository(session=session)

    billing_customer, payment_method = (
        attach_signed_debit_mandate_to_sepa_direct_debit_payment_method_logic(
            payment_method_id=payment_method_id,
            debit_mandate_uri=mandate_info["uri"],
            debit_mandate_signed_at=mandate_info.get("signed_at"),
            signer=mandate_info.get("signer"),
            signature_id=mandate_info.get("signature_id"),
            signature_request_id=mandate_info.get("signature_request_id"),
            signature_step=mandate_info.get("signature_step"),
            signature_event=mandate_info.get("signature_event"),
            debit_mandate_unique_reference=mandate_info["unique_reference"],
            billing_customer_repository=billing_customer_repository,
        )
    )

    billing_customer_repository.session.commit()

    return billing_customer.to_public(), payment_method.to_public()

create_sepa_direct_debit_payment_method staticmethod

create_sepa_direct_debit_payment_method(
    *,
    profile_id,
    iban,
    display_name=None,
    session=None,
    commit=True
)

Creates a SEPA direct debit payment method and attaches it to a billing customer.

The created payment method does not have a DebitMandate attached.

When to Use

Use this function when you want to orchestrate the creation of the payment method from outside PaymentMethodService and use the PaymentMethodService exclusively for storage of the result.

Example use case: UCE creating an unsigned payment method from Marmot and attaching a mandate later via the Proposal Builder.

Parameters:

Name Type Description Default
profile_id UUID

UUID of the profile to attach the payment method to

required
iban str

IBAN of the bank account for direct debit

required
display_name str | None

Optional display name for the payment method

None
session Session | None

Optional database session to use

None
commit bool

Whether to commit the transaction (default: True)

True

Returns:

Type Description
tuple[BillingCustomer, SepaDirectDebitPaymentMethod]

The billing customer and the newly created payment method.

Raises:

Type Description
NotImplementedError

If the customer already has a payment method (temporary 1 PM limit)

Examples:

customer, payment_method = (
    PaymentMethodService.create_sepa_direct_debit_payment_method(
        profile_id=user.profile_id,
        iban="DE89370400440532013000",
    )
)
Source code in components/payment_method/public/api.py
@staticmethod
def create_sepa_direct_debit_payment_method(
    *,
    profile_id: "uuid.UUID",
    iban: str,
    display_name: str | None = None,
    session: "Session | None" = None,
    commit: bool = True,
) -> "tuple[BillingCustomer, SepaDirectDebitPaymentMethod]":
    """Creates a SEPA direct debit payment method and attaches it to a billing customer.

    The created payment method does not have a `DebitMandate` attached.

    !!! info "When to Use"
        Use this function when you want to orchestrate the creation of the payment method
        from outside `PaymentMethodService` and use the `PaymentMethodService` exclusively for
        storage of the result.

        **Example use case:** UCE creating an unsigned payment method from Marmot and
        attaching a mandate later via the Proposal Builder.

    Args:
        profile_id: UUID of the profile to attach the payment method to
        iban: IBAN of the bank account for direct debit
        display_name: Optional display name for the payment method
        session: Optional database session to use
        commit: Whether to commit the transaction (default: True)

    Returns:
        The billing customer and the newly created payment method.

    Raises:
        NotImplementedError: If the customer already has a payment method (temporary 1 PM limit)

    Examples:
        ```python
        customer, payment_method = (
            PaymentMethodService.create_sepa_direct_debit_payment_method(
                profile_id=user.profile_id,
                iban="DE89370400440532013000",
            )
        )
        ```
    """
    from components.payment_method.internal.business_logic.actions import (
        create_sepa_direct_debit_payment_method as create_sepa_direct_debit_payment_method_logic,
    )

    billing_customer_repository = _get_billing_customer_repository(session=session)

    billing_customer, payment_method = (
        create_sepa_direct_debit_payment_method_logic(
            profile_id=profile_id,
            iban=iban,
            display_name=display_name,
            billing_customer_repository=billing_customer_repository,
        )
    )

    if commit:
        billing_customer_repository.session.commit()

    return billing_customer.to_public(), payment_method.to_public()

create_sepa_mandate_signature_request staticmethod

create_sepa_mandate_signature_request(
    *, app_name, iban, profile_id
)

Creates a signature request on Dropbox Sign, for a SEPA mandate authorizing Marmot BE (the creditor) to withdraw money from the account described by the IBAN, belonging to the owner of the profile_id (the debtor).

The only side effect of this function is sending the request to Dropbox Sign, we expect it doesn't write to the database.

When to Use

When you want PaymentMethodService to orchestrate the creation of the payment method from start to finish, this function is the first step in the creation process.

Complete Process Overview

The process happens live working hand-in-hand with a frontend application, on-session. It is a 6 step process:

  • SEPA MANDATE GENERATION: A SEPA mandate is generated from a legally compliant template, in the preferred language of the profile's owner
  • SIGNATURE REQUEST: A signature request (containing information necessary for the creation of the payment method) is sent to Dropbox Sign
  • SIGNATURE URL RETURN: The signature URL is returned to the frontend client. Visiting this URL in a web browser will open a signature page on the Dropbox Sign domain. Our accounts on Dropbox Sign are configured to only accepts connections coming from alan.eu and alan.com domains
  • USER SIGNATURE: The frontend client displays the signature to the user, and the user signs all fields and confirms
  • WEBHOOK NOTIFICATION: Dropbox Sign sends a webhook to notify us the document is fully signed
  • PAYMENT METHOD CREATION: This webhook triggers an event handler within the PaymentMethodService: it creates the payment method and saves the signed mandate (see handle_hellosign_callback())

Security Considerations

The signature request sent to Dropbox Sign contains both personal and sensitive information:

  • IBAN of the debtor is visible on the PDF document and present in the request's metadata
  • SEPA mandate's unique reference is visible on the PDF document and present in the request's metadata
  • Address of the debtor is visible on the PDF document
  • Name of the debtor is visible on the PDF document
  • Name and Email address is present in the request's metadata

Parameters:

Name Type Description Default
app_name AppName

The application name for the signature request

required
iban str

IBAN of the bank account for the SEPA mandate

required
profile_id UUID

UUID of the profile requesting the signature

required

Returns:

Type Description
_SignatureRequest

A dictionary with the following fields:

  • sign_url (str): URL for embedding the signature iframe in frontend
  • signature_request_id (str): Dropbox Sign request identifier for tracking
  • signature_id (str): Individual signature identifier within the request
  • sepa_unique_id (str): SEPA-compliant unique identifier for the mandate

Raises:

Type Description
`PaymentMethodError.invalid_direct_debit_iban`

If the IBAN is not valid or not direct debit compliant

`BaseErrorCode.missing_resource`

If the profile does not have an email (mandatory for signature on Dropbox Sign)

`BaseErrorCode.missing_resource`

If the profile does not have an address (mandatory for SEPA mandate generation)

Examples:

signature_request = (
    PaymentMethodService.create_sepa_mandate_signature_request(
        iban="DE89370400440532013000",
        profile_id=profile_id
    )
)
sign_url = signature_request["sign_url"]
request_id = signature_request["signature_request_id"]

# Once completed, we receive a webhook from _Dropbox Sign_
event = server.listen()
PaymentMethodService.handle_hellosign_callback(
    event=event,
)
Source code in components/payment_method/public/api.py
@staticmethod
def create_sepa_mandate_signature_request(
    *,
    app_name: AppName,
    iban: str,
    profile_id: "uuid.UUID",
) -> _SignatureRequest:
    """Creates a signature request on _Dropbox Sign_, for a SEPA mandate authorizing Marmot BE (the creditor) to withdraw money from the account described by the IBAN, belonging to the owner of the `profile_id` (the debtor).

    The only side effect of this function is sending the request to _Dropbox Sign_,
    we expect it doesn't write to the database.

    !!! info "When to Use"
        When you want `PaymentMethodService` to orchestrate the creation of the payment
        method from start to finish, this function is the first step in the creation process.

    !!! info "Complete Process Overview"
        The process happens live working hand-in-hand with a frontend application,
        on-session. It is a 6 step process:

        - **SEPA MANDATE GENERATION**: A SEPA mandate is generated from a legally compliant template, in the preferred language of the profile's owner
        - **SIGNATURE REQUEST**: A signature request (containing information necessary for the creation of the payment method) is sent to _Dropbox Sign_
        - **SIGNATURE URL RETURN**: The signature URL is returned to the frontend client. Visiting this URL in a web browser will open a signature page on the _Dropbox Sign_ domain. Our accounts on _Dropbox Sign_ are configured to only accepts connections coming from alan.eu and alan.com domains
        - **USER SIGNATURE**: The frontend client displays the signature to the user, and the user signs all fields and confirms
        - **WEBHOOK NOTIFICATION**: _Dropbox Sign_ sends a webhook to notify us the document is fully signed
        - **PAYMENT METHOD CREATION**: This webhook triggers an event handler within the `PaymentMethodService`: it creates the payment method and saves the signed mandate (see `handle_hellosign_callback()`)

    !!! warning "Security Considerations"
        The signature request sent to _Dropbox Sign_ contains both personal and sensitive information:

        - IBAN of the debtor is visible on the PDF document and present in the request's metadata
        - SEPA mandate's unique reference is visible on the PDF document and present in the request's metadata
        - Address of the debtor is visible on the PDF document
        - Name of the debtor is visible on the PDF document
        - Name and Email address is present in the request's metadata

    Args:
        app_name: The application name for the signature request
        iban: IBAN of the bank account for the SEPA mandate
        profile_id: UUID of the profile requesting the signature

    Returns:
        A dictionary with the following fields:

            - sign_url (str): URL for embedding the signature iframe in frontend
            - signature_request_id (str): _Dropbox Sign_ request identifier for tracking
            - signature_id (str): Individual signature identifier within the request
            - sepa_unique_id (str): SEPA-compliant unique identifier for the mandate

    Raises:
        `PaymentMethodError.invalid_direct_debit_iban`: If the IBAN is not valid or not direct debit compliant
        `BaseErrorCode.missing_resource`: If the profile does not have an email (mandatory for signature on _Dropbox Sign_)
        `BaseErrorCode.missing_resource`: If the profile does not have an address (mandatory for SEPA mandate generation)

    Examples:
        ```python
        signature_request = (
            PaymentMethodService.create_sepa_mandate_signature_request(
                iban="DE89370400440532013000",
                profile_id=profile_id
            )
        )
        sign_url = signature_request["sign_url"]
        request_id = signature_request["signature_request_id"]

        # Once completed, we receive a webhook from _Dropbox Sign_
        event = server.listen()
        PaymentMethodService.handle_hellosign_callback(
            event=event,
        )
        ```
    """
    from components.payment_method.internal.business_logic.actions import (
        create_sepa_mandate_signature_request as create_sepa_mandate_signature_request_logic,
    )

    sign_url, signature_request_id, signature_id, sepa_unique_id = (
        create_sepa_mandate_signature_request_logic(
            iban=iban,
            profile_id=profile_id,
            app_name=app_name,
        )
    )

    return {
        "sign_url": sign_url,
        "signature_request_id": signature_request_id,
        "signature_id": signature_id,
        "sepa_unique_id": sepa_unique_id,
    }

get_billing_customer staticmethod

get_billing_customer(
    *,
    billing_customer_id: UUID,
    exclusively_use_global_repository: bool = False,
    raise_if_missing: Literal[True],
    session: Session | None = None
) -> BillingCustomer
get_billing_customer(
    *,
    billing_customer_id: UUID,
    exclusively_use_global_repository: bool = False,
    raise_if_missing: Literal[False],
    session: Session | None = None
) -> BillingCustomer | None
get_billing_customer(
    *,
    billing_customer_id: UUID,
    exclusively_use_global_repository: bool = False,
    session: Session | None = None
) -> BillingCustomer | None
get_billing_customer(
    *,
    profile_id: UUID,
    exclusively_use_global_repository: bool = False,
    raise_if_missing: Literal[True],
    session: Session | None = None
) -> BillingCustomer
get_billing_customer(
    *,
    profile_id: UUID,
    exclusively_use_global_repository: bool = False,
    raise_if_missing: Literal[False],
    session: Session | None = None
) -> BillingCustomer | None
get_billing_customer(
    *,
    profile_id: UUID,
    exclusively_use_global_repository: bool = False,
    session: Session | None = None
) -> BillingCustomer | None
get_billing_customer(
    *,
    payment_method_id: UUID,
    exclusively_use_global_repository: bool = False,
    raise_if_missing: Literal[True],
    session: Session | None = None
) -> BillingCustomer
get_billing_customer(
    *,
    payment_method_id: UUID,
    exclusively_use_global_repository: bool = False,
    raise_if_missing: Literal[False],
    session: Session | None = None
) -> BillingCustomer | None
get_billing_customer(
    *,
    payment_method_id: UUID,
    exclusively_use_global_repository: bool = False,
    session: Session | None = None
) -> BillingCustomer | None
get_billing_customer(
    *,
    billing_customer_id=None,
    profile_id=None,
    payment_method_id=None,
    exclusively_use_global_repository=False,
    raise_if_missing=False,
    session=None
)

Return a billing customer representing someone paying for Alan services.

This method accepts exactly one of billing_customer_id, profile_id, or payment_method_id. Its return type depends on the raise_if_missing parameter:

  • If raise_if_missing is specified, it returns BillingCustomer or raises if it does not exist
  • Otherwise, it returns BillingCustomer | None

This flexibility is reflected in the type system using overload.

Parameters:

Name Type Description Default
billing_customer_id UUID | None

Optional UUID of the billing customer to retrieve

None
profile_id UUID | None

Optional UUID of the profile associated with the billing customer to retrieve

None
payment_method_id UUID | None

Optional UUID of a payment method associated with the billing customer to retrieve

None
exclusively_use_global_repository bool

Optional boolean indicating whether to use global repository

False
raise_if_missing bool

If True, raises an exception when the customer is not found

False
session Session | None

Optional database session to use for the query

None

Returns:

Type Description
BillingCustomer | None

The billing customer if found, None if not found (unless raise_if_missing=True).

Raises:

Type Description
`BaseErrorCode.missing_resource`

If the customer does not exist and raise_if_missing is specified

Examples:

# Returns BillingCustomer | None
customer = PaymentMethodService.get_billing_customer(
    billing_customer_id=customer_id
)

# Returns BillingCustomer | None
customer = PaymentMethodService.get_billing_customer(
    profile_id=user.profile_id
)

# Returns BillingCustomer or raises
customer = PaymentMethodService.get_billing_customer(
    payment_method_id=pm.id,
    raise_if_missing=True
)
Source code in components/payment_method/public/api.py
@staticmethod
def get_billing_customer(
    *,
    billing_customer_id: "uuid.UUID | None" = None,
    profile_id: "uuid.UUID | None" = None,
    payment_method_id: "uuid.UUID | None" = None,
    exclusively_use_global_repository: bool = False,
    raise_if_missing: bool = False,
    session: "Session | None" = None,
) -> "BillingCustomer | None":
    """Return a billing customer representing someone paying for Alan services.

    This method accepts exactly one of `billing_customer_id`, `profile_id`, or `payment_method_id`.
    Its return type depends on the `raise_if_missing` parameter:

    - If `raise_if_missing` is specified, it returns `BillingCustomer` or raises if it does not exist
    - Otherwise, it returns `BillingCustomer | None`

    This flexibility is reflected in the type system using `overload`.

    Args:
        billing_customer_id: Optional UUID of the billing customer to retrieve
        profile_id: Optional UUID of the profile associated with the billing customer to retrieve
        payment_method_id: Optional UUID of a payment method associated with the billing customer to retrieve
        exclusively_use_global_repository: Optional boolean indicating whether to use global repository
        raise_if_missing: If True, raises an exception when the customer is not found
        session: Optional database session to use for the query

    Returns:
        The billing customer if found, None if not found (unless `raise_if_missing=True`).

    Raises:
        `BaseErrorCode.missing_resource`: If the customer does not exist and `raise_if_missing` is specified

    Examples:
        ```python
        # Returns BillingCustomer | None
        customer = PaymentMethodService.get_billing_customer(
            billing_customer_id=customer_id
        )

        # Returns BillingCustomer | None
        customer = PaymentMethodService.get_billing_customer(
            profile_id=user.profile_id
        )

        # Returns BillingCustomer or raises
        customer = PaymentMethodService.get_billing_customer(
            payment_method_id=pm.id,
            raise_if_missing=True
        )
        ```
    """
    billing_customer_repository = (
        GlobalBillingCustomerRepository(session=session)
        if exclusively_use_global_repository
        else _get_billing_customer_repository(session=session)
    )

    if billing_customer_id and not raise_if_missing:
        billing_customer = billing_customer_repository.get(billing_customer_id)
    elif billing_customer_id and raise_if_missing:
        billing_customer = (
            billing_customer_repository.get_or_raise_missing_resource(
                billing_customer_id
            )
        )
    elif profile_id and not raise_if_missing:
        billing_customer = billing_customer_repository.get_by_profile_id(profile_id)
    elif profile_id and raise_if_missing:
        billing_customer = (
            billing_customer_repository.get_by_profile_id_or_raise_missing_resource(
                profile_id
            )
        )
    elif payment_method_id and not raise_if_missing:
        billing_customer = billing_customer_repository.get_by_payment_method_id(
            payment_method_id
        )
    elif payment_method_id and raise_if_missing:
        billing_customer = billing_customer_repository.get_by_payment_method_id_or_raise_missing_resource(
            payment_method_id
        )
    else:
        raise ValueError("Should be unreachable")

    return billing_customer.to_public() if billing_customer else None

handle_hellosign_callback staticmethod

handle_hellosign_callback(*, event, session=None)

Completes the creation of a SEPA direct debit payment method from a Dropbox Sign signature_request_all_signed event.

Process Steps

This function executes the following steps, in order:

  • Retrieves payment method creation data from the event
  • Downloads the signed document from Dropbox Sign
  • Stores the signed mandate document in S3
  • Creates the SepaDirectDebitPaymentMethod
  • Creates a signed DebitMandate and attaches it to the payment method

Parameters:

Name Type Description Default
event HellosignEvent

Must be of type signature_request_all_signed, with signature_type == SignatureType.sepa_mandate

required
session Session | None

Database session to use, defaults to current session

None

Returns:

Type Description
tuple[BillingCustomer, SepaDirectDebitPaymentMethod]

The billing customer and the newly created payment method.

Raises:

Type Description
ValueError

If the request leading to the event does not originate from PaymentMethodService or if the event_type or signature_type do not match the expectations

`BaseErrorCode.missing_resource`

If required metadata is missing

Examples:

if (
    event.metadata.get("origin_component") == "PaymentMethodService"
    and event.signature_type == SignatureType.sepa_mandate
    and event.event_type == (
        HellosignEventType.signature_request_all_signed
    )
):
    customer, payment_method = (
        PaymentMethodService.handle_hellosign_callback(
            event=event,
        )
    )
Source code in components/payment_method/public/api.py
@staticmethod
def handle_hellosign_callback(
    *,
    event: "HellosignEvent",
    session: "Session | None" = None,
) -> "tuple[BillingCustomer, SepaDirectDebitPaymentMethod]":
    """Completes the creation of a SEPA direct debit payment method from a _Dropbox Sign_ `signature_request_all_signed` event.

    !!! info "Process Steps"
        This function executes the following steps, in order:

        - Retrieves payment method creation data from the event
        - Downloads the signed document from _Dropbox Sign_
        - Stores the signed mandate document in S3
        - Creates the `SepaDirectDebitPaymentMethod`
        - Creates a signed `DebitMandate` and attaches it to the payment method

    Args:
        event: Must be of type `signature_request_all_signed`, with `signature_type == SignatureType.sepa_mandate`
        session: Database session to use, defaults to current session

    Returns:
        The billing customer and the newly created payment method.

    Raises:
        ValueError: If the request leading to the event does not originate from `PaymentMethodService` or if the `event_type` or `signature_type` do not match the expectations
        `BaseErrorCode.missing_resource`: If required metadata is missing

    Examples:
        ```python
        if (
            event.metadata.get("origin_component") == "PaymentMethodService"
            and event.signature_type == SignatureType.sepa_mandate
            and event.event_type == (
                HellosignEventType.signature_request_all_signed
            )
        ):
            customer, payment_method = (
                PaymentMethodService.handle_hellosign_callback(
                    event=event,
                )
            )
        ```
    """
    from components.payment_method.internal.business_logic.actions import (
        create_sepa_direct_debit_payment_method_from_hellosign_callback,
    )
    from components.payment_method.public.dependencies import COMPONENT_NAME
    from shared.signed_document.enums.signature_type import SignatureType
    from shared.signed_document.hellosign_event_type import HellosignEventType

    if event.metadata.get("origin_component") != COMPONENT_NAME:
        raise ValueError(
            "PaymentMethodService cannot handle this event from Dropboxsign. PaymentMethodService is only allowed to process events originating from PaymentMethodService",
            dict(event=event),
        )

    if event.signature_type != SignatureType.sepa_mandate:
        raise ValueError(
            f"PaymentMethodService is only allowed to deal with SEPA mandates signature, it cannot handle {event.signature_type}",
            dict(event=event),
        )

    if event.event_type != HellosignEventType.signature_request_all_signed:
        raise ValueError(
            f"Unimplemented event handler: {event.event_type}", dict(event=event)
        )

    billing_customer_repository = _get_billing_customer_repository(session=session)

    billing_customer, pm = (
        create_sepa_direct_debit_payment_method_from_hellosign_callback(
            event=event,
            billing_customer_repository=billing_customer_repository,
        )
    )

    billing_customer_repository.session.commit()
    return billing_customer.to_public(), pm.to_public()

set_stripe_customer_id staticmethod

set_stripe_customer_id(
    *,
    profile_id,
    stripe_customer_id,
    session=None,
    commit=True
)

Set the Stripe customer ID of a specific billing customer.

Planned Deprecation

The only purpose for this function's existence is helping us migrate countries from existing local models to global modals. Ideally, the entire creation of the customer and the payment method should be encapsulated within the PaymentMethodService and such crude set operations should be forbidden because they would breach the ownership of the component.

This function will not be part of the final API of the component.

Parameters:

Name Type Description Default
profile_id UUID

UUID of the profile to set the Stripe customer ID for

required
stripe_customer_id str

The Stripe customer ID to attach

required
session Session | None

Optional database session to use

None
commit bool

Whether to commit the transaction (default: True)

True

Returns:

Type Description
BillingCustomer

The billing customer with the attached Stripe customer ID.

Raises:

Type Description
`BaseErrorCode.missing_resource`

If the billing customer does not exist for the profile_id

Examples:

customer = PaymentMethodService.set_stripe_customer_id(
    profile_id=user.profile_id,
    stripe_customer_id="cus_1234567890",
)
Source code in components/payment_method/public/api.py
@staticmethod
def set_stripe_customer_id(
    *,
    profile_id: "uuid.UUID",
    stripe_customer_id: str,
    session: "Session | None" = None,
    commit: bool = True,
) -> "BillingCustomer":
    """Set the Stripe customer ID of a specific billing customer.

    !!! warning "Planned Deprecation"
        The only purpose for this function's existence is helping us migrate
        countries from existing local models to global modals.
        Ideally, the entire creation of the customer and the payment method should
        be encapsulated within the `PaymentMethodService` and such crude set
        operations should be forbidden because they would breach the ownership of
        the component.

        This function will not be part of the final API of the component.

    Args:
        profile_id: UUID of the profile to set the Stripe customer ID for
        stripe_customer_id: The Stripe customer ID to attach
        session: Optional database session to use
        commit: Whether to commit the transaction (default: True)

    Returns:
        The billing customer with the attached Stripe customer ID.

    Raises:
        `BaseErrorCode.missing_resource`: If the billing customer does not exist for the `profile_id`

    Examples:
        ```python
        customer = PaymentMethodService.set_stripe_customer_id(
            profile_id=user.profile_id,
            stripe_customer_id="cus_1234567890",
        )
        ```
    """
    billing_customer_repository = _get_billing_customer_repository(session=session)

    billing_customer = (
        billing_customer_repository.get_by_profile_id_or_raise_missing_resource(
            profile_id=profile_id
        )
    )
    billing_customer.attach_stripe_customer_id(stripe_customer_id)

    billing_customer_repository.upsert(billing_customer)

    if commit:
        billing_customer_repository.session.commit()

    return billing_customer.to_public()

set_stripe_payment_method_id staticmethod

set_stripe_payment_method_id(
    *,
    payment_method_id,
    stripe_payment_method_id,
    session=None,
    commit=True
)

Set the Stripe payment method ID of a specific payment method.

Planned Deprecation

The only purpose for this function's existence is helping us migrate countries from existing local models to global modals. Ideally, the entire creation of the customer and the payment method should be encapsulated within the PaymentMethodService and such crude set operations should be forbidden because they would breach the ownership of the component.

This function will not be part of the final API of the component.

Parameters:

Name Type Description Default
payment_method_id UUID

UUID of the payment method to set the Stripe ID for

required
stripe_payment_method_id str

The Stripe payment method ID to attach

required
session Session | None

Optional database session to use

None
commit bool

Whether to commit the transaction (default: True)

True

Returns:

Type Description
BillingCustomer

The billing customer owning the payment method with attached Stripe payment method ID.

Raises:

Type Description
`BaseErrorCode.missing_resource`

If the billing customer does not exist for the payment_method_id

Examples:

customer = PaymentMethodService.set_stripe_payment_method_id(
    payment_method_id=some_uuid,
    stripe_payment_method_id="pm_1234567890"
)
Source code in components/payment_method/public/api.py
@staticmethod
def set_stripe_payment_method_id(
    *,
    payment_method_id: "uuid.UUID",
    stripe_payment_method_id: str,
    session: "Session | None" = None,
    commit: bool = True,
) -> "BillingCustomer":
    """Set the Stripe payment method ID of a specific payment method.

    !!! warning "Planned Deprecation"
        The only purpose for this function's existence is helping us migrate
        countries from existing local models to global modals.
        Ideally, the entire creation of the customer and the payment method should
        be encapsulated within the `PaymentMethodService` and such crude set
        operations should be forbidden because they would breach the ownership of
        the component.

        This function will not be part of the final API of the component.

    Args:
        payment_method_id: UUID of the payment method to set the Stripe ID for
        stripe_payment_method_id: The Stripe payment method ID to attach
        session: Optional database session to use
        commit: Whether to commit the transaction (default: True)

    Returns:
        The billing customer owning the payment method with attached Stripe payment method ID.

    Raises:
        `BaseErrorCode.missing_resource`: If the billing customer does not exist for the `payment_method_id`

    Examples:
        ```python
        customer = PaymentMethodService.set_stripe_payment_method_id(
            payment_method_id=some_uuid,
            stripe_payment_method_id="pm_1234567890"
        )
        ```
    """
    from shared.helpers.collections import first

    billing_customer_repository = _get_billing_customer_repository(session=session)

    billing_customer = billing_customer_repository.get_by_payment_method_id_or_raise_missing_resource(
        payment_method_id=payment_method_id
    )

    payment_method = first(
        billing_customer.payment_methods, lambda pm: pm.id == payment_method_id
    )
    payment_method.stripe_payment_method_id = stripe_payment_method_id

    billing_customer_repository.upsert(billing_customer)

    if commit:
        billing_customer_repository.session.commit()

    return billing_customer.to_public()

components.payment_method.public.commands

This component does not have any commands.

components.payment_method.public.dependencies

COMPONENT_NAME module-attribute

COMPONENT_NAME = 'payment_method'

Canonical name of the payment method component.

PaymentMethodDependency

Bases: ABC

Dependencies injected into the payment method component. Every component depending on the payment method component is responsible for injecting its own implementation of these dependencies.

get_billing_customer_repository

get_billing_customer_repository(session)

Returns the BillingCustomerRepository to use for the given session.

Source code in components/payment_method/public/dependencies.py
def get_billing_customer_repository(
    self,
    session: Session | None,
) -> BillingCustomerRepository:
    """Returns the BillingCustomerRepository to use for the given session."""
    return GlobalBillingCustomerRepository(session=session)

get_suggested_iban abstractmethod

get_suggested_iban(profile_id)

The suggested IBAN is used to pre-fill forms on frontends when creating a SepaDirectDebitPaymentMethod. In practice, we expect each country to inject a function that returns the settlement IBAN of the profile's owner.

Source code in components/payment_method/public/dependencies.py
@abstractmethod
def get_suggested_iban(
    self,
    profile_id: uuid.UUID,
) -> str | None:
    """The suggested IBAN is used to pre-fill forms on frontends when creating a
    SepaDirectDebitPaymentMethod.
    In practice, we expect each country to inject a function that returns the
    settlement IBAN of the profile's owner.
    """

get_app_dependency

get_app_dependency()

Function used to fetch the dependencies from the flask app.

Source code in components/payment_method/public/dependencies.py
def get_app_dependency() -> PaymentMethodDependency:
    """Function used to fetch the dependencies from the flask app."""
    from flask import current_app

    return cast("CustomFlask", current_app).get_component_dependency(COMPONENT_NAME)  # type: ignore[no-any-return]

set_app_dependency

set_app_dependency(dependency)

Function used to actually inject the dependency class in the component.

Source code in components/payment_method/public/dependencies.py
def set_app_dependency(dependency: PaymentMethodDependency) -> None:
    """Function used to actually inject the dependency class in the component."""
    from flask import current_app

    cast("CustomFlask", current_app).add_component_dependency(
        COMPONENT_NAME, dependency
    )

components.payment_method.public.entities

BillingCustomer dataclass

BillingCustomer(
    id,
    profile_id,
    sepa_debit_payment_methods,
    payment_cards,
    stripe_customer_id,
)

Bases: DataClassJsonMixin

A member or company paying for Alan services.

Temporary Single Payment Method Limitation

Currently, a BillingCustomer may only have one payment method, which is implicitly the active payment method. This limitation is temporary and multiple payment methods will be allowed in the future.

id instance-attribute

id

Database identifier.

payment_cards instance-attribute

payment_cards

List of payment card methods owned by this customer.

Not Implemented

Payment cards are not yet implemented. This field exists for future development and testing polymorphism of payment methods. Will be empty in current implementation.

profile_id instance-attribute

profile_id

Profile ID of the member corresponding to the BillingCustomer

sepa_debit_payment_methods instance-attribute

sepa_debit_payment_methods

List of SEPA direct debit payment methods owned by this customer.

Each payment method represents a bank account from which Alan can automatically withdraw payments via the SEPA direct debit system.

stripe_customer_id instance-attribute

stripe_customer_id

ID of the customer on Stripe.

Planned Deprecation

This property is exposed temporarily during the migration from local to global models. Ideally, all operations requiring this ID should be encapsulated within the PaymentMethodService or billing related services rather than exposing internal Stripe identifiers.

This field will be removed from the public API once the migration is complete.

BillingPaymentMethod dataclass

BillingPaymentMethod(
    id,
    method_type,
    display_name,
    is_chargeable,
    stripe_payment_method_id,
)

Base class representing a payment method used by a BillingCustomer to pay for Alan services.

display_name instance-attribute

display_name

A name chosen by the owner of the payment method, used for in-app display and communications.

id instance-attribute

id

Database identifier.

is_chargeable instance-attribute

is_chargeable

Indicates whether this payment method can be charged, with reason if not.

Examples:

# Payment method can be charged
is_chargeable = (True, None)

# Payment method cannot be charged due to missing mandate
is_chargeable = (False, UnchargeableReason.NO_SIGNED_MANDATE)

# Payment method cannot be charged due to expired mandate
is_chargeable = (False, UnchargeableReason.EXPIRED_MANDATE)

method_type instance-attribute

method_type

The type of payment method: card, SEPA direct debit, etc ...

stripe_payment_method_id instance-attribute

stripe_payment_method_id

ID of the payment method on Stripe.

Planned Deprecation

This property is exposed temporarily during the migration from local to global models. Ideally, all operations requiring this ID should be encapsulated within the PaymentMethodService rather than exposing internal Stripe identifiers.

This field will be removed from the public API once the migration is complete.

DebitMandate dataclass

DebitMandate(
    id,
    is_active,
    unique_reference,
    signed_at=optional_isodatetime_field(),
)

A document representing an authorisation to debit from a bank account. A debit mandate is attached to SepaDirectDebitPaymentMethod. The creditor is implicitely Marmot BE. The debtor is implicitely the BillingCustomer owning the DebitMandate.

id instance-attribute

id

Database identifier.

is_active instance-attribute

is_active

If is_active is False, the attached SepaDirectDebitPaymentMethod cannot be used for charging.

signed_at class-attribute instance-attribute

signed_at = optional_isodatetime_field()

The date and time the mandate was signed, provided by the document signature service.

unique_reference instance-attribute

unique_reference

Unique reference of the mandate. This is a reference written in the debit mandate document and is used by all parties involved to identify it (Alan, PSP, Bank).

PaymentCardPaymentMethod dataclass

PaymentCardPaymentMethod(
    id,
    method_type,
    display_name,
    is_chargeable,
    stripe_payment_method_id,
)

Bases: BillingPaymentMethod

Payment card payment method for credit/debit card transactions.

Not Implemented

This payment method type is not yet implemented. It exists only for testing polymorphism of payment methods and future development. Do not use in production code.

PaymentMethodType

Bases: AlanBaseEnum

All variants of payment methods supported by PaymentMethodService.

PAYMENT_CARD class-attribute instance-attribute

PAYMENT_CARD = 'payment_card'

SEPA_DIRECT_DEBIT class-attribute instance-attribute

SEPA_DIRECT_DEBIT = 'sepa_direct_debit'

from_payment_method_enum classmethod

from_payment_method_enum(payment_method)

Convert a PaymentMethod enum value (from core billing) to the corresponding PaymentMethodType enum value, compatible with PaymentMethodService.

Parameters:

Name Type Description Default
payment_method PaymentMethod

The core billing PaymentMethod enum value to convert

required

Returns:

Type Description
PaymentMethodType

The corresponding PaymentMethodType enum value

Raises:

Type Description
`BaseErrorCode.model_validation`

If the payment method is not supported by PaymentMethodService

Examples:

pm_type = PaymentMethodType.from_payment_method_enum(
    PaymentMethod.sepa_debit
)
print(pm_type) # PaymentMethodType.SEPA_DIRECT_DEBIT

pm_type = PaymentMethodType.from_payment_method_enum(
    PaymentMethod.payment_card
)
print(pm_type)  # PaymentMethodType.PAYMENT_CARD
Source code in components/payment_method/public/entities.py
@classmethod
def from_payment_method_enum(
    cls, payment_method: PaymentMethod
) -> "PaymentMethodType":
    """Convert a `PaymentMethod` enum value (from core billing) to the corresponding
    `PaymentMethodType` enum value, compatible with `PaymentMethodService`.

    Args:
        payment_method: The core billing `PaymentMethod` enum value to convert

    Returns:
        The corresponding `PaymentMethodType` enum value

    Raises:
        `BaseErrorCode.model_validation`: If the payment method is not supported by `PaymentMethodService`

    Examples:
        ```python
        pm_type = PaymentMethodType.from_payment_method_enum(
            PaymentMethod.sepa_debit
        )
        print(pm_type) # PaymentMethodType.SEPA_DIRECT_DEBIT

        pm_type = PaymentMethodType.from_payment_method_enum(
            PaymentMethod.payment_card
        )
        print(pm_type)  # PaymentMethodType.PAYMENT_CARD
        ```
    """
    match payment_method:
        case PaymentMethod.sepa_debit:
            return cls.SEPA_DIRECT_DEBIT
        case PaymentMethod.payment_card:
            return cls.PAYMENT_CARD
        case _:
            raise BaseErrorCode.model_validation(
                message=f"Unsupported payment method type: {payment_method}"
            )

to_payment_method_enum

to_payment_method_enum()

Convert a PaymentMethodType enum value to the corresponding PaymentMethod enum value (for core billing).

Returns:

Type Description
PaymentMethod

The equivalent core billing PaymentMethod enum value

Raises:

Type Description
`ValueError`

If this payment method type cannot be mapped

Examples:

pm_enum = PaymentMethodType.SEPA_DIRECT_DEBIT.to_payment_method_enum()
print(pm_enum)  # PaymentMethod.sepa_debit

pm_enum = PaymentMethodType.PAYMENT_CARD.to_payment_method_enum()
print(pm_enum)  # PaymentMethod.payment_card
Source code in components/payment_method/public/entities.py
def to_payment_method_enum(self) -> PaymentMethod:
    """Convert a `PaymentMethodType` enum value to the corresponding
    `PaymentMethod` enum value (for core billing).

    Returns:
        The equivalent core billing `PaymentMethod` enum value

    Raises:
        `ValueError`: If this payment method type cannot be mapped

    Examples:
        ```python
        pm_enum = PaymentMethodType.SEPA_DIRECT_DEBIT.to_payment_method_enum()
        print(pm_enum)  # PaymentMethod.sepa_debit

        pm_enum = PaymentMethodType.PAYMENT_CARD.to_payment_method_enum()
        print(pm_enum)  # PaymentMethod.payment_card
        ```
    """
    match self:
        case PaymentMethodType.SEPA_DIRECT_DEBIT:
            return PaymentMethod.sepa_debit
        case PaymentMethodType.PAYMENT_CARD:
            return PaymentMethod.payment_card

SepaDirectDebitPaymentMethod dataclass

SepaDirectDebitPaymentMethod(
    id,
    method_type,
    display_name,
    is_chargeable,
    stripe_payment_method_id,
    iban,
    debit_mandates,
)

Bases: BillingPaymentMethod

SEPA direct debit payment method for automated bank account withdrawals.

This payment method allows Alan to automatically withdraw payments from a customer's bank account using the SEPA direct debit system. Requires signed mandates to authorize debits from the specified IBAN.

debit_mandates instance-attribute

debit_mandates

List of signed mandates authorizing debits from the IBAN.

Each mandate, if signed, represents a legal authorization for Marmot BE (as creditor) to withdraw money from the debtor's bank account (owner of the payment method). Multiple mandates may exist, covering different purposes or simply because we accidentally sent too many documents to sign.

iban instance-attribute

iban

International Bank Account Number for the direct debit withdrawals.

Security Considerations

This contains sensitive financial information that must be handled with care. Do not transmit nor log this data carelessly. Ensure proper encryption and access controls when processing IBAN data.

UnchargeableReason

Bases: AlanBaseEnum

Reasons why a payment method might not be chargeable.

ARCHIVED_PAYMENT_METHOD class-attribute instance-attribute

ARCHIVED_PAYMENT_METHOD = 'archived_payment_method'

The payment method has been archived following a customer-initiated request.

EXPIRED_MANDATE class-attribute instance-attribute

EXPIRED_MANDATE = 'expired_mandate'

If a SepaDirectDebitPaymentMethod is unused during 36 month, the attached mandate automatically expires.

NO_SIGNED_MANDATE class-attribute instance-attribute

NO_SIGNED_MANDATE = 'no_signed_mandate'