Aller au contenu

Primary Purchase — Vue d'ensemble

Le module primary-purchase orchestre l'achat de bricks (parts d'une propriété) par un investor.

Quel que soit le point d'entrée — achat direct, réservation par carte, réservation admin ou auto-invest — l'achat est créé en statut waiting_for_assignation : les bricks sont assignées avant tout paiement par carte. Des workers asynchrones font ensuite avancer l'achat jusqu'à la création d'un P2P Lemonway (transfert investor → SPV de la propriété) puis la génération du contrat PDF d'obligation.

Flux global

flowchart TD
    Investor([POST /primary-purchase]):::endpoint
    Admin([POST /administration/bricks/reservation]):::endpoint
    AutoInvest([Tâche graphile project-automatic-funding]):::endpoint

    BalanceCheck{Balance suffisante ?}
    PurchaseBricks["purchaseBricks — WT d'achat débitée immédiatement"]:::action
    ReserveBricks["reserveBricks — aucune WT, réservation attachée"]:::action

    Investor --> BalanceCheck
    BalanceCheck -- "Oui" --> PurchaseBricks
    BalanceCheck -- "Non (flag réservation)" --> ReserveBricks
    Admin --> ReserveBricks
    AutoInvest --> PurchaseBricks

    PurchaseBricks --> WaitAssign[waiting_for_assignation]:::status
    ReserveBricks --> WaitAssign

    WaitAssign -- "Bricks assignées — achat direct" --> WaitP2P[waiting_for_p2p_creation]:::status
    WaitAssign -- "Bricks assignées — réservation" --> WaitResConf[waiting_for_reservation_confirmation]:::status
    WaitAssign -- "Bricks assignées — auto-invest" --> WaitAuto[waiting_for_auto_invest_investor_confirmation]:::status
    WaitAssign -. "Pas assez de bricks : funding bloqué + decline" .-> DeclinedNoBricks["declined — no-bricks-left"]:::failure

    WaitResConf -- "Carte confirmée (webhook Lemonway) ou admin : WT d'achat créée" --> WaitP2P
    WaitResConf -. "expireAt dépassé (10 min carte / 10 j admin)" .-> DeclinedExpired["declined — expired"]:::failure

    WaitAuto -- "Investor confirme" --> WaitP2P
    WaitAuto -. "Investor annule" .-> Refunded([refunded]):::neutral

    WaitP2P -- "P2P pending attaché à la WT" --> WaitContract[waiting_for_contract_creation]:::status
    WaitContract -- "P2P joué et réussi : contrat PDF + rewards" --> Confirmed([confirmed]):::success
    WaitContract -. "P2P échoué : refund auto" .-> Refunded

    Confirmed -. "Rétractation ≤ J+4 / admin" .-> Refunded

    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
    classDef neutral fill:#6b7280,color:#fff,stroke:#374151

Endpoints du module

Verbe Route Handler Description
POST /primary-purchase primaryPurchase Lance un achat. Branche entre achat immédiat (balance suffisante) et réservation par carte (sinon).
DELETE /primary-purchase/reservation/:reservationId cancelReservation Force l'expiration immédiate d'une réservation (expireAt = now).

Les deux endpoints sont protégés par JwtAuthGuard + UserRoleGuard avec rôle CUSTOMER, et exigent que l'investor ait complété son onboarding et ses transaction rights. Détail requêtes/réponses : voir la page endpoints.

Endpoints connexes

Le cycle de vie complet fait intervenir des endpoints portés par d'autres controllers :

Verbe Route Rôle dans le flux
POST /customers/lemonway/payment-intent/card (type bricks-reservation, aussi en variante saved-card) Crée la WT carte Lemonway du money-in et attache son id à la réservation (waiting_for_card_confirmation). Chemin par défaut (flag Checkout OFF).
POST /checkout/create-payment-session (bricksReservationId optionnel) Session Checkout ; webhook payment_approved crée TOPUP_CHECKOUT WAITING (utilisable) et attache la WT à la réservation. Derrière ENABLE_CHECKOUT_CARD_BRICKS_PURCHASE.
POST /wallet-transactions/:walletTransactionId/refund Rétractation investor (≤ J+4). Voir Remboursement.
POST /investor/investment-plan/purchase/confirm L'investor confirme un achat auto-invest (waiting_for_auto_invest_investor_confirmationwaiting_for_p2p_creation).
POST /investor/investment-plan/purchase/cancel L'investor annule un achat auto-invest → refund.
POST /administration/bricks/reservation Crée une réservation admin (expiration +10 jours, sous-statut waiting_for_admin_confirmation).
PATCH /administration/bricks/reservation Confirme (confirmPrimaryPurchaseReservation) ou annule (action: cancel → expiration immédiate) une réservation admin.
POST /administration/transactions/:transactionId/refund Refund admin d'une WT PRIMARY_PURCHASE. Voir Remboursement.

Acteurs externes

Acteur Rôle Quand
Lemonway Webhook money-in qui confirme la WT carte (réservation) ; exécution des P2P (achat investor → SPV, refund SPV → investor) Réservation, waiting_for_p2p_creationconfirmed, refund
Property SPV Reçoit le crédit P2P (en tant que creditAccountId Lemonway) Exécution du P2P d'achat
Brick Pool Pool de bricks disponibles à assigner pour la propriété waiting_for_assignation
Redis Compteur de bricks vendues par propriété (+ compteur auto-invest) : incrément à la création, décrément au decline/refund. Voir Réconciliation des compteurs pour le mécanisme de correction près de la clôture. Tout le cycle
CustomerIO Émission de l'événement primary_purchase À la confirmation
Referral Rewards Génère bonus referee/referrer si applicable À la confirmation
Slack Alerte en cas de sur-vente détectée (blockPropertyFunding) Assignation en échec

Workers asynchrones

Les transitions sont portées par des process workers dédiés (projects/api/worker/), chacun activé par un flag d'environnement worker.is* au bootstrap NestJS.

Timeout ≠ cadence

Les workers tournent dans safeInfiniteLoop : la boucle repart immédiatement tant qu'il y a des jobs et ne dort que 500 ms quand il n'y en a plus. La valeur indiquée ci-dessous est le timeout d'une itération, pas un intervalle de polling.

Worker Rôle Timeout d'itération Verrouillage
PurchaseWaitingForBricksAssignation Assigne les bricks et route vers le statut suivant ; sur-vente → funding bloqué + decline 10 s Batch 1000 FOR UPDATE SKIP LOCKED
PurchaseWaitingForP2PCreation Attache un P2P pending à la WT d'achat 10 s Batch 1000 FOR UPDATE OF … SKIP LOCKED
ReservationWaitingForConfirmation Crée la WT d'achat quand la WT carte est confirmée (ou réservation admin confirmée) 30 s Lecture des ids sans verrou, puis verrou par purchase (pLimit(10))
ReservationWaitingForExpiration Décline les achats dont la réservation a expiré 30 s Lecture des ids sans verrou, puis verrou par purchase (séquentiel)
PendingLemonwayP2P Joue les P2P pending chez Lemonway (hors module) 60 s Par P2P
PlayedLemonwayP2P Réagit aux P2P joués : succès → confirmPrimaryPurchase (contrat, rewards, event) ; échec → refund automatique (hors module) 60 s Par P2P

Toutes les transitions s'exécutent dans safePgTransaction ; les compteurs Redis sont compensés via setOnRollback si la transaction Postgres échoue.

Pour aller plus loin

  • Statuts & transitions — détail des 8 statuts du purchase et des 5 statuts de la réservation imbriquée, avec state diagram complet.
  • Remboursement — éligibilité, déclencheurs et mécanique du refund.
  • Réconciliation des compteurs de funding — pourquoi et comment la task nightly aligne Redis sur la vérité PG près de la clôture des collectes (dual-write, stabilisation, blocage, correction).
  • Endpoints — requêtes, réponses et erreurs du controller.