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 trois 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 le Canada et les USA,
regionCode(province ou état, code à 2 lettres) 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). La session Checkout n'applique pas de plafond. Les 10 000 € restent sur Lemonway CB, la carte cadeau, la réservation d'achat de bricks (POST /primary-purchase) et le top-up (front). La levée ne filtre pas ce plafond.referer— 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). 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. Checkout peut renvoyer des champs extra (Flow SDK) : createPaymentSessionResponseValidator est un intersection avec un dictionary(string, unknown) pour les conserver.
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 le Canada ou les USA |
checkout.create_payment_session.error |
400 | Erreur retournée par l'API Checkout.com lors de la création de session |
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 |
Code région manquant (onboarding)¶
Lors de la sauvegarde d'une adresse d'onboarding pour le Canada ou les USA, si regionCode reste absent après sanitization, InvestorOnboardingService logue une erreur et envoie une alerte Slack via CheckoutSlackService.sendMissingCheckoutRegionCodeAlert :
| Champ alerte | Valeur |
|---|---|
| Canal Slack (prod) | #checkout-double-paiement |
| Canal Slack (hors prod) | devChannelNames[environment] (internal-development-alerts, internal-staging-alerts, internal-local-alerts) |
| Mention | @sos_captain (<!subteam^S05BL6SCWMD>) |
| Action attendue | Retrouver le code province (CAN) ou état (USA) à partir de l'adresse et l'ajouter sur le customer-profile en BO — sans ce champ, le customer ne peut pas payer par carte (Checkout). Une suggestion Mapbox indicative peut être incluse dans l'alerte (à valider manuellement) |
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 après commit via CheckoutSlackService.sendDoublePaymentAlert :
| Champ alerte | Valeur |
|---|---|
| Canal Slack (prod) | #checkout-double-paiement |
| Canal Slack (hors prod) | devChannelNames[environment] (internal-development-alerts, internal-staging-alerts, internal-local-alerts) |
| Paiement conservé | incomingCheckoutPaymentId — écrase le contexte WT (last-write-wins) |
| Paiement à rembourser | existingCheckoutPaymentId — lien cliquable vers le dashboard Checkout (CHECKOUT_DASHBOARD_URL) |
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.
POST /checkout/webhook/dispute-received¶
Webhook appelé par Checkout.com quand une dispute (chargeback carte) est reçue (dispute_received). À ce stade Checkout a reçu la notification du réseau et est encore en revue : l'API n'écrit rien en base. Elle envoie uniquement une alerte Slack pour que le support aille consulter la dispute sur le dashboard Checkout.
Le payload officiel (dispute_received) ne contient pas d'URL dashboard. _links.self pointe vers l'API workflow de l'événement, pas vers le Hub. La dispute se consulte sur la fiche du paiement. Le lien Slack reprend le même pattern que le double paiement : {CHECKOUT_DASHBOARD_URL}/payments/all-payments/payment/{payment_id}.
L'id de dispute (dsp_…) est dans le message.
À configurer côté Checkout : workflow / webhook dispute_received vers cet endpoint, même x-api-key que payment-approved / payment-declined.
Pré-conditions¶
- Header
x-api-keyvalide (ApiAuthService.assertApiKeyInHeaders('checkout'))
Request body¶
Validé via CheckoutWebhook.disputeReceivedBodySchema (zod, checkout-webhook.model.ts).
{
"type": "dispute_received",
"data": {
"id": "dsp_xxx",
"category": "fraudulent",
"amount": 8150,
"currency": "EUR",
"reason_code": "10.4",
"payment_id": "pay_xxx",
"payment_reference": "<walletTransactionId>",
"payment_method": "VISA"
}
}
Le validator ne parse que les champs utilisés pour Slack. data.id (dispute) et data.payment_id sont requis. Le reste du payload Checkout est ignoré.
data.category est l'enum API Checkout : canceled_recurring, credit_not_issued, fraudulent, general, incorrect_amount, duplicate, not_as_described, product_service_not_received (dispute reasons). Une valeur hors de cette liste (ou une catégorie ajoutée plus tard par Checkout) fait échouer le validator en 400, donc pas d'alerte Slack.
Response¶
201 Created sans corps en cas de succès.
Alerte Slack¶
| Champ alerte | Valeur |
|---|---|
| Canal Slack (prod) | #checkout-double-paiement |
| Canal Slack (hors prod) | devChannelNames[environment] (internal-development-alerts, internal-staging-alerts, internal-local-alerts) |
| Mention | @sos_captain (<!subteam^S05BL6SCWMD>) |
| Action attendue | Consulter la dispute sur Checkout (fiche du paiement) |
| Lien | Fiche du paiement {CHECKOUT_DASHBOARD_URL}/payments/all-payments/payment/{payment_id} |
Erreurs¶
| Code | Statut | Cause |
|---|---|---|
Unauthorized |
401 | API key absente ou invalide |
Cron sync-checkout-topup-report¶
Graphile task sync-checkout-topup-report (0 */6 * * *) — rapproche les top-ups carte Checkout avec les rapports financiers Checkout (FinancialActionsByPayout).
Source : sync-checkout-topup-report.cron-task.ts.
| Paramètre | Valeur |
|---|---|
| Lookback | 7 jours calendaires (CHECKOUT_REPORT_LOOKBACK_DAYS = 7, createdAfter = startOfDay(now − 7d)) — filet si un rapport quotidien a été manqué |
| Filtre | Rapports FinancialActionsByPayout non encore présents en DB (checkout_financial_report) |
| Ordre | created_on croissant — les rapports les plus anciens non traités passent en premier |
| Effet | Télécharge le CSV, crée les TOPUP_CHECKOUT manquants / aligne les captures, puis marque le rapport traité |
En échec de fetch Checkout → alerte Slack (InternalAlertSlackService) + throw Graphile pour retry. Runbook jobs : skill sos-graphile-jobs.
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