Aller au contenu

Statuts & transitions

Le P2P est une discriminated union sur status à trois états, définie dans le domain (lemonway-p2p.ts). Les transitions sont des fonctions pures du domain (succeed, retry, failForLemonwayP2PBusinessError), appelées exclusivement par le state-watcher pending-p2p-to-play — jamais par les endpoints HTTP.

State diagram

stateDiagram-v2
    direction LR

    [*] --> pending: createPendingP2P<br/>(service métier ou worker)

    pending --> succeeded: POST /p2p OK<br/>ou référence déjà jouée chez LW
    pending --> pending: erreur technique (backoff exponentiel)<br/>erreur business sur un crédit<br/>solde insuffisant avec crédits en attente
    pending --> failed: autre erreur business sur un débit<br/>solde insuffisant sans crédit en attente

    failed --> pending: FailedLemonwayP2P.retry<br/>(withholding tax auto, gift card gelée, sinon script manuel)

    succeeded --> [*]
Statut Type domain Champs spécifiques Signification
pending PendingLemonwayP2P attemptAfter pilote l'éligibilité À jouer (ou rejouer) chez Lemonway dès que attemptAfter est passé
succeeded SucceededLemonwayP2P lemonwayP2PId L'argent a bougé chez Lemonway — état terminal
failed FailedLemonwayP2P businessErrorType? Échec métier définitif — déclenche refund/decline côté métier, sauf retry automatique (withholding tax, gift card gelée)

Chaque transition empile une entrée dans attempts[] (date, erreur brute, attemptAfter courant) : l'historique complet d'un P2P se lit directement dans le JSON.

failed n'est pas tout à fait terminal

FailedLemonwayP2P.retry remet un P2P failed en pending. Utilisé automatiquement pour les WITHHOLDING_TAX (re-tentative à J+1) et pour les GIFT_CARD_PURCHASE dont le donneur vient d'être gelé (re-jeu au dégel), voir Runtime ; et manuellement (script ponctuel) pour débloquer un P2P après correction de la cause.

Classification des erreurs Lemonway

Quand POST /p2p échoue, le service classe l'erreur en deux familles :

  • technical_error — transitoire (panne, timeout, erreur interne Lemonway) : on retentera, sans limite de tentatives.
  • business_error — cause métier identifiée par un code Lemonway :
Code LW Cause businessErrorType
110, 187 Solde du wallet débité insuffisant debit_account_balance_not_sufficient
111, 167 Wallet émetteur bloqué / statut invalide debit_account_blocked
146 Statut de wallet incorrect debit_or_credit_account_blocked
168 Wallet récepteur bloqué credit_account_blocked
137 Type de wallet non autorisé pour le P2P p2p_not_allowed
147 Wallet introuvable non_existent_payment_account
autre code — unknown

Deux codes sont volontairement reclassés en erreur technique (donc retry indéfini) :

  • 348 — référence dupliquée : le P2P existe probablement déjà chez Lemonway ; un retry ultérieur le retrouvera via GET /p2p (voir l'idempotence).
  • 101 — plafond P2P du wallet récepteur atteint : on réessaie jusqu'à ce que ça passe.

Décision après une erreur

La décision retry / fail dépend du sens du mouvement (totalValue_view < 0 = débit du customer) — codée dans le state-watcher :

flowchart TD
    Err{Type d'erreur} -- technical_error --> RetryT[retry → pending<br/>backoff exponentiel]:::status
    Err -- business_error --> Sens{Débit ou crédit ?}
    Sens -- crédit --> RetryB[retry → pending<br/>backoff par paliers]:::status
    Sens -- débit --> Insuf{Solde insuffisant ?}
    Insuf -- non --> Fail[fail → failed]:::failure
    Insuf -- oui --> PC{Crédits en attente<br/>sur la balance ?}
    PC -- oui --> RetryB
    PC -- non --> FailAlert[fail → failed<br/>+ alerte Slack]:::failure

    classDef status fill:#fef3c7,color:#78350f,stroke:#d97706
    classDef failure fill:#ef4444,color:#fff,stroke:#b91c1c

Les principes :

  • Un crédit ne fail jamais. Créditer un investisseur finira toujours par fonctionner (au pire quand son wallet sera débloqué) ; échouer définitivement un crédit signifierait perdre de l'argent dû.
  • Un débit pour solde insuffisant est retenté seulement si des crédits sont en attente (pendingCredit > 0 sur la balance gift ou withdrawable) : le solde Lemonway va se reconstituer. Sinon c'est une vraie anomalie → failed + alerte Slack debit-p2p-failed-insufficient-balance-no-pending-credits.
  • Toute autre erreur business sur un débit (wallet bloqué, inexistant…) → failed immédiat ; c'est le worker played-lemonway-p2p qui décidera de la conséquence métier (refund, decline, freeze du customer — voir Runtime).

Stratégies de backoff

Calculées par le domain pending-lemonway-p2p.ts, persistées dans attemptAfter :

Famille Formule Concrètement
Erreur technique 5 min × 2^min(attemptNumber, 7) 10 min, 20 min, 40 min… plafonné à ~10 h 40 par tentative
Erreur business paliers selon l'ancienneté de la 1ʳᵉ tentative : < 3 j → 3 h ; 3 à 10 j → 24 h ; > 10 j → 72 h + jitter ±10 % pour étaler les vagues de retries partageant le même attemptedAt

Au-delà de 5 tentatives, chaque nouvel échec technique est loggé en error pour déclencher le monitor Datadog (P2P coincé).

Effet sur la wallet transaction porteuse

La WT reste en waiting pendant toute la vie du P2P. C'est le worker played-lemonway-p2p qui la fait avancer une fois le P2P joué :

P2P final WT Par
succeeded confirmed (+ effets métier : confirmation d'achat, crédit de balance…) handleSucceededLemonwayP2P
failed declined ou refund selon le kind ; waiting conservé pour withholding tax et gift card gelée handleFailedLemonwayP2P

Le détail kind par kind est dans Runtime — conséquences métier.