Aller au contenu

Lemonway P2P — Vue d'ensemble

Un P2P est un transfert d'argent wallet à wallet chez Lemonway (notre PSP) : débit d'un wallet, crédit d'un autre. C'est la brique de base de tous les mouvements d'argent internes à la plateforme — achat de bricks (investisseur → SPV), topup par carte (compte technique → investisseur), récompense de parrainage, gift card, remboursement…

Côté API, un P2P n'est pas une entité autonome : c'est un objet JSON porté par une wallet transaction (colonne lemonwayP2P de la table wallet_transactions). La wallet transaction décrit le mouvement métier ; le P2P décrit son exécution réelle chez Lemonway.

Sources

Le pipeline en trois temps

Le cycle de vie d'un P2P est entièrement asynchrone, porté par trois workers qui communiquent uniquement via Postgres :

flowchart LR
    Services[Services métier<br/>+ worker bricks-purchase-p2p]:::action
    Pending[("WT waiting<br/>P2P pending")]:::status
    LW{POST /p2p<br/>Lemonway}:::endpoint
    Succeeded[("P2P succeeded")]:::success
    Failed[("P2P failed")]:::failure
    Done[WT confirmed<br/>confirmations métier]:::success
    Refund[WT declined<br/>refund / decline métier]:::failure

    Services -- createPendingP2P --> Pending
    Pending -- worker<br/>pending-lemonway-p2p --> LW
    LW -- succès --> Succeeded
    LW -. erreur transitoire<br/>retry + backoff .-> Pending
    LW -- erreur définitive --> Failed
    Succeeded -- worker<br/>played-lemonway-p2p --> Done
    Failed -- worker<br/>played-lemonway-p2p --> Refund

    classDef endpoint fill:#4f46e5,color:#fff,stroke:#3730a3
    classDef action fill:#0ea5e9,color:#fff,stroke:#0369a1
    classDef status fill:#fef3c7,color:#78350f,stroke:#d97706
    classDef success fill:#10b981,color:#fff,stroke:#047857
    classDef failure fill:#ef4444,color:#fff,stroke:#b91c1c
  1. Création — un service métier attache un P2P pending à sa wallet transaction (WalletTransaction.createPendingP2P). Pour les achats de bricks, c'est le worker bricks-purchase-p2p qui le fait en batch.
  2. Exécution — le worker pending-lemonway-p2p sélectionne les P2P pending éligibles (en respectant l'ordre des débits par compte) et les joue chez Lemonway. Succès → succeeded, erreur transitoire → retry avec backoff, erreur définitive → failed.
  3. Conséquences — le worker played-lemonway-p2p réconcilie : il prend les wallet transactions waiting dont le P2P est joué et déclenche la suite métier (confirmation d'achat, remboursement, decline…), puis passe la WT en confirmed ou declined.

Détail des statuts dans Statuts & transitions, détail des workers dans Runtime — workers & exécution.

Modèle de données

Le P2P est une discriminated union sur status (lemonway-p2p.ts), stockée en jsonb sur wallet_transactions.lemonwayP2P (migration 058-wt-p2p-column-and-json-index.sql) :

Champ Type Rôle
status pending | succeeded | failed Statut d'exécution chez Lemonway
debitAccountId / creditAccountId string Wallets Lemonway débité / crédité (id customer ou compte technique)
amount Cents Montant en centimes, de bout en bout (aucune conversion)
comment string Libellé visible chez Lemonway
walletTransactionId WalletTransactionId WT porteuse — sert aussi de référence d'idempotence chez Lemonway
attemptNumber / attemptAfter PositiveInteger / Date Compteur de tentatives et date de prochaine éligibilité
attempts Array<{ attemptedAt, rawError?, attemptAfter }> Historique complet des tentatives (debug)
lemonwayP2PId PositiveInteger Si succeeded : id de la transaction chez Lemonway (recopié dans wallet_transactions.lemonwayTransactionId)
businessErrorType enum Si failed : cause métier (voir Statuts & transitions)

La factory WalletTransaction.createPendingP2P initialise status: 'pending', attemptNumber: 1, attemptAfter: now. Le montant par défaut est |withdrawableBalance| + |giftBalance| de la WT.

Qui crée des P2P — les flux métier

Flux WT kind Débit → Crédit
Achat primaire de bricks (primary-purchase) PRIMARY_PURCHASE investisseur → SPV de la propriété
Remboursement d'achat PRIMARY_PURCHASE_REFUND SPV → investisseur
Topup par carte (checkout) TOPUP_CHECKOUT Bricks-Checkout.com → investisseur
Achat de gift card GIFT_CARD_PURCHASE acheteur → gift_card_waiting_to_be_claimed
Réclamation de gift card GIFT_CARD_CREDIT gift_card_waiting_to_be_claimed → bénéficiaire
Remboursement de gift card (admin) GIFT_CARD_REFUND gift_card_waiting_to_be_claimed → acheteur
Récompense de parrainage REWARD_REFEREE / REWARD_REFERRER Referrals → investisseur
Gain de solde boosté BOOSTED_BALANCE_GAIN SoldeBooste → investisseur
Remboursement de retenue à la source (admin) REFUND_WITHHOLDING_TAX CollecteDGFIP → investisseur
Suppression de compte (reprise du solde gift) ADJUSTMENT_NEG investisseur → compte principal Bricks

D'autres kinds transitent aussi par le pipeline (BUYING_DEAL marketplace, REVENUE_OBLIGATION_COUPON, WITHHOLDING_TAX, CREDIT_FOR_WALLET_ASSET_TRANSFER…) — la liste de référence des conséquences par kind est dans Runtime.

Les comptes non-customer (Bricks-Checkout.com, Referrals, etc.) sont des wallets techniques déclarés dans businessRules.

Usage hors pipeline

L'échéancier projet (back-office) joue aussi des P2P, mais directement via attemptP2PAtLemonway avec une référence project-ptba-…, sans wallet transaction ni workers — voir property-payment-schedule.controller.ts.

Exemple de bout en bout — achat de bricks

sequenceDiagram
  autonumber
  participant PP as primary-purchase
  participant W1 as bricks-purchase-p2p (worker)
  participant W2 as pending-lemonway-p2p (worker)
  participant LW as Lemonway
  participant W3 as played-lemonway-p2p (worker)

  Note over PP: achat en waiting_for_p2p_creation
  W1->>PP: setP2PForPurchases (batch ≤ 1000)
  W1->>W1: WT.lemonwayP2P = pending<br/>achat → waiting_for_contract_creation
  W2->>W2: sélection priorisée + lock (SKIP LOCKED)
  W2->>LW: POST /p2p (reference = wtId)
  LW-->>W2: transaction.id
  W2->>W2: P2P → succeeded<br/>WT.lemonwayTransactionId = id
  W3->>W3: détecte WT waiting + P2P succeeded
  W3->>PP: confirmPrimaryPurchase
  W3->>W3: WT → confirmed

Si le P2P échoue définitivement (failed), le worker played-lemonway-p2p déclenche à l'inverse le remboursement de l'achat (refundPrimaryPurchase, reason p2pFailed).

Pour aller plus loin