Aller au contenu

Checkout

Contrôleur HTTP du flux de money-in par carte via Checkout.com. Source : checkout.controller.ts.

Pour le flux métier complet (cycle de vie des WalletTransaction, rapprochement bancaire des wire-ins, P2P Lemonway), voir le service checkout.service.ts.

Un endpoint expose la création de session de paiement (auth investisseur + rôle CUSTOMER). Les deux autres sont des webhooks appelés par Checkout.com et sécurisés par API key (x-api-key header).

POST /checkout/create-payment-session

Crée une session de paiement Checkout.com pour un montant donné. Renvoie le secret/token nécessaires au SDK front pour ouvrir la modale de paiement carte.

Le payload Checkout est construit côté serveur depuis le profil investisseur (buildCheckoutPaymentSessionPayload) — adresse, type (individual / corporate), date de naissance, téléphone, etc. Une WalletTransactionId est générée à l'avance (reference envoyée à Checkout) pour pouvoir corréler ensuite le webhook avec la WT à créer.

Pré-conditions

  • Session investisseur valide (JwtAuthGuard)
  • Rôle CUSTOMER (UserRoleGuard)
  • Droits transactionnels actifs : Customer.assertTransactionRights(['all'])
  • Profil investisseur (CustomerProfileRepository.findOneOrFail) présent — sinon 500
  • Pour les pays nécessitant un code région Checkout (US, CA, etc.), regionCode doit être renseigné sur le profil

Request body

{
  "amount": 5000,
  "referer": "https://app.bricks.co/topup",
  "searchParams": "utm_source=email",
  "bricksReservationId": "019abc..."
}

Pas d’union discriminée côté wire — kind n’existe pas dans le body, pour rester compatible avec les clients mobile déjà en store (top-up sans champ extra). La distinction top-up / bricks purchase se fait par présence de bricksReservationId.

  • amount — montant en cents (Cents), plafonné à businessRules.moneyIn.card.maxAmountInCents (10 000 €), comme Lemonway CB
  • referer — URL absolue du front qui ouvre la session (optionnel)
  • searchParams — query string transmise à Checkout (optionnel)
  • bricksReservationId — optionnel. Si présent : exige le flag ENABLE_CHECKOUT_CARD_BRICKS_PURCHASE, ownership/status validés, et amount doit couvrir au minimum le money-in requis (purchase.value − amountToUseFromGiftBalance − availableWithdrawable). Un montant plus élevé est accepté (ex. wallet non utilisé côté front), toujours ≤ plafond CB. Au payment_approved, setCardInForBricksReservation attache la WT. Lemonway CB reste le chemin par défaut côté front tant que le flag est OFF. Les URLs de retour portent checkout_flow=bricks_purchase (via searchParams front) pour rouvrir PurchaseResultModal (même UI succès que Lemonway).

Validé via createPaymentSessionBodyValidator (depuis @bricks-common/api-communication, paymentSession.ts).

Response

{
  "id": "ps_xxx",
  "payment_session_secret": "sk_xxx",
  "payment_session_token": "tok_xxx",
  "_links": {
    "self": { "href": "https://api.checkout.com/payment-sessions/ps_xxx" }
  }
}

Réponse passée telle quelle depuis Checkout (validée par createPaymentSessionResponseValidator).

Erreurs

Code Statut Cause
customer.rights.account-is-being-verified 403 Compte Lemonway bloqué ou droits insuffisants
customer.rights.account-not-completed 403 Identité investisseur non vérifiée
checkout.customer.state_required 400 Code région manquant pour un pays qui le requiert (US, CA, etc.)
checkout.create_payment_session.error 400 Erreur retournée par l'API Checkout.com lors de la création de session
amount-too-high 400 amount > businessRules.moneyIn.card.maxAmountInCents (10 000 €)
checkout.bricks-purchase-not-enabled 400 bricksReservationId présent alors que ENABLE_CHECKOUT_CARD_BRICKS_PURCHASE est OFF (ou minVersion non respectée via header x-version)
checkout.bricks-reservation-amount-too-low 400 amount < money-in minimum pour la réservation bricks
primary-purchase-not-found / wrong-investor / wrong-status / no-reservation 400 bricksReservationId invalide pour l'investisseur
session_expired 401 Session investisseur invalide

POST /checkout/webhook/payment-approved

Webhook appelé par Checkout.com quand un paiement carte est approuvé. Crée (ou met à jour) la WalletTransaction de type TOPUP_CHECKOUT au statut WAITING côté Bricks. Le P2P Lemonway sera ensuite créé via le rapprochement wire-in (cf. processCheckoutWireIn).

Gère le cas Apple Pay où plusieurs tentatives peuvent partager la même session (et donc la même reference/wtId) : la WT existante est repassée en WAITING avec le nouveau checkoutPaymentId.

Si udf2 (réservation bricks) est présent, setCardIn est tenté après le commit de la WT. Un échec d'attach ne rollback pas le TOPUP_CHECKOUT (Checkout a déjà capturé) — log + alerte Slack ; retry / ops manuelle.

Double paiement (même session, deux captures)

Si une WT TOPUP_CHECKOUT existe déjà avec un checkoutPaymentId différent du webhook entrant (et n'est pas DECLINED), le service logue un warning et envoie une alerte Slack dédiée via CheckoutSlackService.sendDoublePaymentAlert :

Champ alerte Valeur
Canal Slack #checkout-double-paiement (channelNameToId['checkout-double-paiement'])
Paiement conservé incomingCheckoutPaymentId — écrase le contexte WT (last-write-wins)
Paiement à rembourser existingCheckoutPaymentId — lien cliquable vers le dashboard Checkout

Runbook support : skill sos-diagnostic-double-checkout.

Pré-conditions

  • Header x-api-key valide (ApiAuthService.assertApiKeyInHeaders('checkout'))

Request body

Validé via CheckoutWebhook.paymentApprovedValidator (checkout-webhook.model.ts).

{
  "id": "evt_xxx",
  "type": "payment_approved",
  "version": "1.0.40",
  "created_on": "2026-05-07T12:34:56Z",
  "data": {
    "id": "pay_xxx",
    "action_id": "act_xxx",
    "reference": "<walletTransactionId>",
    "amount": 5000,
    "currency": "EUR",
    "customer": { "id": "cus_xxx", "email": "..." },
    "metadata": { "udf1": "<customer-uuid>" },
    "source": { "card_wallet_type": "card", "scheme": "Visa", "last_4": "4242" },
    "risk": { "flagged": false, "score": 2 },
    "..."
  }
}

metadata.udf1 doit contenir l'UUID du customer (sinon le service retourne customer-id-not-found).

Response

200 OK sans corps en cas de succès.

Erreurs

Code Statut Cause
Unauthorized 401 API key absente ou invalide
customer-id-not-found 500 metadata.udf1 manquant dans le payload Checkout
wt-not-checkout 500 Une WT existe déjà avec cette reference mais n'est pas de type TOPUP_CHECKOUT (intégrité données)

Toute erreur du service est loggée et alertée sur Slack via InternalAlertSlackService.alert avant de remonter en 500.

POST /checkout/webhook/payment-declined

Webhook appelé par Checkout.com quand un paiement carte est refusé. Crée une WalletTransaction de type TOPUP_CHECKOUT au statut DECLINED (si aucune WT n'existe encore pour cette reference).

Gère le retry sur la même session (timeout / Apple Pay) : - WT déjà WAITING (ou autre non-DECLINED) avec le même checkoutPaymentId → erreur wt-not-declined - WT non-DECLINED avec un autre checkoutPaymentId → no-op (decline stale après un approve) - Pas d'expiry forcée de la réservation bricks (udf2) : un decline n'est pas terminal, un payment_approved peut encore arriver. La réservation expire via son expireAt naturel (front 5 min / back 10 min).

Pré-conditions

  • Header x-api-key valide (ApiAuthService.assertApiKeyInHeaders('checkout'))

Request body

Validé via CheckoutWebhook.paymentDeclinedValidator (checkout-webhook.model.ts).

Même schéma que payment-approved avec type: "payment_declined".

Response

200 OK sans corps en cas de succès.

Erreurs

Code Statut Cause
Unauthorized 401 API key absente ou invalide
customer-id-not-found 500 metadata.udf1 manquant dans le payload Checkout
wt-not-checkout 500 Une WT existe déjà avec cette reference mais n'est pas de type TOPUP_CHECKOUT
wt-not-declined 500 WT existante avec même checkoutPaymentId n'est pas en DECLINED

Toute erreur du service est loggée et alertée sur Slack via InternalAlertSlackService.alert avant de remonter en 500.

Liens