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.
Sources
- Code :
projects/api/src/__new/modules/primary-purchase/ - Controller :
primary-purchase.controller.ts - Service :
primary-purchase.service.ts
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_confirmation → waiting_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_creation → confirmed, 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.