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 > 0sur la balance gift ou withdrawable) : le solde Lemonway va se reconstituer. Sinon c'est une vraie anomalie →failed+ alerte Slackdebit-p2p-failed-insufficient-balance-no-pending-credits. - Toute autre erreur business sur un débit (wallet bloqué, inexistant…) →
failedimmédiat ; c'est le workerplayed-lemonway-p2pqui 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.