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.),
regionCodedoit ê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 CBreferer— URL absolue du front qui ouvre la session (optionnel)searchParams— query string transmise à Checkout (optionnel)bricksReservationId— optionnel. Si présent : exige le flagENABLE_CHECKOUT_CARD_BRICKS_PURCHASE, ownership/status validés, etamountdoit 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. Aupayment_approved,setCardInForBricksReservationattache la WT. Lemonway CB reste le chemin par défaut côté front tant que le flag est OFF. Les URLs de retour portentcheckout_flow=bricks_purchase(viasearchParamsfront) pour rouvrirPurchaseResultModal(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-keyvalide (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-keyvalide (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¶
- Service :
checkout.service.ts - Modèle webhook :
checkout-webhook.model.ts - Builder payload :
build-checkout-payment-session-payload.ts - Validators body :
paymentSession.ts - Auth API key :
api-auth.service.ts