Aller au contenu

Machines à états

Tout le comportement de paiement est gouverné par deux machines à états : celle d’un leg (une jambe) et celle d’une session. Le statut de la session est déduit de l’état de ses legs.

  • Logique des legs : api/src/domain/session/leg-status.ts
  • Logique des sessions : api/src/domain/session/session-status.ts

Une carte cadeau est débitée directement (pas de phase « hold » séparée) : un débit réussi la place en CAPTURED. L’annulation et le remboursement passent par un état transitoire.

stateDiagram-v2
  [*] --> PENDING
  PENDING --> CAPTURED: débit réussi
  PENDING --> DEBIT_FAILED: débit refusé
  CAPTURED --> CANCELING: rollback demandé
  CAPTURED --> REFUNDING: remboursement demandé
  CANCELING --> CANCELED
  CANCELING --> CANCEL_FAILED: refus du fournisseur
  CANCEL_FAILED --> CANCELING: nouvel essai
  CANCEL_FAILED --> CANCELED: réglé hors E-Pay
  REFUNDING --> REFUNDED
  REFUNDING --> REFUND_FAILED: refus du fournisseur
  REFUND_FAILED --> REFUNDING: nouvel essai
  REFUND_FAILED --> REFUNDED: réglé hors E-Pay
  DEBIT_FAILED --> [*]
  CANCELED --> [*]
  REFUNDED --> [*]

Un PSP suit un cycle autorisation (hold) → capture. Entre les deux, un hold peut être relâché (void) lors d’un rollback. Le 3DS introduit un état d’attente d’action client.

stateDiagram-v2
  [*] --> PENDING
  PENDING --> HELD: autorisation réussie
  PENDING --> HOLD_FAILED: autorisation refusée
  HELD --> CAPTURED: capture réussie
  HELD --> CAPTURE_FAILED: capture refusée
  HELD --> RELEASING: rollback (void du hold)
  RELEASING --> RELEASED
  CAPTURED --> REFUNDING: remboursement
  CAPTURED --> REFUNDED: remboursé chez le PSP (dashboard)
  REFUNDING --> REFUNDED
  REFUNDING --> REFUND_FAILED: refus du PSP
  REFUNDED --> REFUND_FAILED: échec annoncé après coup
  REFUND_FAILED --> REFUNDING: nouvel essai
  REFUND_FAILED --> REFUNDED: réglé hors E-Pay
  HOLD_FAILED --> [*]
  CAPTURE_FAILED --> [*]
  RELEASED --> [*]
  REFUNDED --> [*]

Les états terminaux d’un leg sont : DEBIT_FAILED, HOLD_FAILED, CAPTURE_FAILED, CANCELED, RELEASED, REFUNDED. Une fois là, plus aucune transition n’est possible.

Une session naît en CREATED, collecte ses legs, puis est traitée. Son statut « avancé » (COMPLETED, ROLLING_BACK, FAILED, remboursements) est déduit de l’état des legs.

stateDiagram-v2
  [*] --> CREATED
  CREATED --> COLLECTING: ajout de legs
  COLLECTING --> PROCESSING: submit (montant restant = 0)
  CREATED --> PROCESSING: submit
  PROCESSING --> COMPLETED: tous les legs actifs CAPTURED
  PROCESSING --> COLLECTING: autorisation carte refusée (reopenForRetry, cadeaux conservés)
  PROCESSING --> ROLLING_BACK: échec de capture
  ROLLING_BACK --> FAILED: compensation terminée (plus rien en vol)
  COMPLETED --> PARTIALLY_REFUNDED: un leg remboursé
  COMPLETED --> FULLY_REFUNDED: tous les legs remboursés
  PARTIALLY_REFUNDED --> FULLY_REFUNDED
  CREATED --> EXPIRED: TTL dépassé
  COLLECTING --> EXPIRED: TTL dépassé
  FAILED --> [*]
  FULLY_REFUNDED --> [*]
  EXPIRED --> [*]

deriveSessionStatus applique, en simplifié, cette cascade de règles sur l’ensemble des legs :

  1. EXPIRED est collant : les compensations qui aboutissent après l’expiration ne ressuscitent pas la session
  2. Un leg en cours de rollback (CANCELING/RELEASING) → ROLLING_BACK
  3. Session ROLLING_BACK sans plus rien en vol → FAILED (convergence de la compensation)
  4. Tous les legs encore porteurs d’argent REFUNDED → FULLY_REFUNDED
  5. Au moins un leg REFUNDED/REFUNDING/REFUND_FAILED, ou un leg encaissé remboursé en partie chez le PSP → PARTIALLY_REFUNDED
  6. Depuis PROCESSING, tous les legs CAPTURED → COMPLETED
  7. Sinon → statut courant conservé

Un refus d’autorisation ne passe PAS par cette cascade : le worker appelle explicitement reopenForRetry — la session repasse en COLLECTING, cartes cadeaux conservées (déjà débitées, elles restent acquises), et le leg carte refusé sera remplacé par setPspLeg au prochain essai. Les legs inactifs (HOLD_FAILED, CANCELED…) ne comptent plus dans la somme au submit ni pour conclure la session.

Ces transitions ne se produisent pas « en direct » dans la requête HTTP : elles sont appliquées par l’orchestrateur (process-outbox-entry.ts) au fil du traitement asynchrone. Voir Outbox & orchestration puis L’orchestrateur.

Source de vérité métier complémentaire : doc/site/content/state-machines.mdx.