Aller au contenu

Domain Map - Bricks

Bricks est une plateforme d'investissement immobilier fractionnaire. Les investisseurs achetent des "briques" (parts) de biens immobiliers et percoivent des revenus (coupons ou royalties).

Nommage canonique des concepts de domaine (brick, investor, property, project, money-in…) : voir naming-conventions.mdc §7.

Glossaire

  • Brique : part d'un bien immobilier. Prix initial 1 000 centimes (10 EUR) au primaire pour tous les types de contrat. Le prix evolue ensuite differemment : pour les obligations/loans il diminue a chaque remboursement capital (jusqu'a 0 au final). Pour les royalties, le prix est une valeur d'expertise mise a jour mensuellement via un script manuel (Excel fourni par un operateur) -- 3 cas : evolution normale du prix, revente partielle d'un lot (propertyResalePartialSaleEvent), ou revente totale (prix tombe a 0). Un changement de brick price annule tous les deals marketplace en cours sur la propriete
  • Types de contrat : obligation (pret obligataire avec coupons + remboursement capital), loan (pret, meme logique qu'obligation), royalty (revenus locatifs, pas de remboursement capital, prix brique variable)
  • Funding : phase de collecte de fonds pour un bien. Le bien doit atteindre un seuil minimum pour que le funding soit un succes
  • SPV (Special Purpose Vehicle) : entite juridique portant un bien, avec son propre compte Lemonway. Un SPV par bien. Aussi appelee societe porteuse (ProjectOwnerCompany, rename en cours -- BRI-1501)
  • Axonaut / Invoice Ninja : prestataires de facturation externes des frais de gestion. Chaque societe porteuse y a une identite : axonautId (societe Axonaut) + invoiceNinjaId (client Invoice Ninja). Clients natifs crees dans le monorepo par l'epic BRI-1651 (avant : joignables seulement via Bubble)
  • project_owner_company_invoicing_ids : table de correspondance 1 ligne par projet -> axonautId + invoiceNinjaId (+ siren). Colonne DB propertyId (nommage legacy des FK vers properties), appelee projectId en code ; UNIQUE(propertyId), siren non-unique (N projets d'une meme societe partagent 1 siren + memes IDs). invoiceNinjaId nullable (Invoice Ninja optionnel / en fin de vie ; axonautId toujours present). companyBubbleId nullable = id du record societe dans Bubble (tracabilite exports Bubble ; absent pour les societes creees nativement post-cutover). Lookup direct projet -> facturation (BRI-1653, sans passer par le join SPV/siren). Peuplee depuis les exports Bubble par le script backfill-invoicing-ids-from-bubble (BRI-1652) : CSV property->societe joint a CSV societe->siren+IDs ; idempotent, ne comble que les champs NULL, n'ecrase jamais. Servie par GET /internal/project-owner/management-fees-invoicing/:period (bulk, middleware invoice-axonaut) et GET /internal/project-owner/:projectId/invoicing-ids (unitaire, Bubble) — cle commune project-owner-invoicing
  • P2P : transfert entre deux comptes Lemonway (pas entre wallets applicatifs). Utilise pour : achat primaire (investor -> SPV), revenus (SPV -> investor), fees (SPV -> Bricks)
  • Early access : acces anticipe au funding pour investisseurs eligibles via code d'acces
  • Echeancier : calendrier de remboursement d'un bien obligation/loan. Definit les mensualites, remboursements capital, et frais de gestion pour chaque mois
  • Sequestre (interets) : montant pre-collecte lors du funding, utilise pour couvrir les premieres mensualites. Diminue progressivement jusqu'a epuisement
  • Coupon (revenue_obligation_coupon) : revenu mensuel verse aux investisseurs d'une obligation/loan, correspond aux interets de l'echeance. Seul type de WT imposable (prelevements)
  • Royalty (revenue_royalty) : revenu locatif reverse aux investisseurs d'un bien royalty
  • WT (Wallet Transaction) : mouvement financier d'un investisseur. Statuts : waiting -> confirmed | declined. Chaque WT a deux composantes : withdrawableBalance et giftBalanceChange
  • Withdrawable balance : solde retirable. Calcul : current + pendingCredit - pendingDebit (confirmed + waiting credits - waiting debits). Les topup_card en waiting sont exclus des pending credits
  • Gift balance : solde provenant de gift cards ou boosted balance. Meme calcul que withdrawable mais sur giftBalanceChange. Non retirable directement, mais transferable vers withdrawable (gift_balance_to_main_balance_transfer). Utilise en priorite lors d'un achat primaire (via amountToUseFromGiftBalance)

Index module -> contexte

Module Contexte
primary-purchase, primary-automatic-funding Investissement primaire
property-funding, property-early-funding-access Investissement primaire
property-creation, properties Cycle de vie propriete
property-payment-schedule, property-repayment Echeancier & Remboursements
property-construction-budget, property-charts, property-deletion Gestion biens (CRUD)
investor-withdraw-request Anti-fraude retrait
money-in, lemonway-account, lemonway-withdraw Paiements
lemonway-p2p, special-purpose-vehicule Worker P2P & SPV
investor-referral, referrals, gift-card Referral & Gift Cards
portfolio, investor-boosted-balance, wallet-transactions Portefeuille
investor-onboarding, investor-kyc-lemonway-form, investor-deletion Investisseur
investor-taxation, investor-financial-document Fiscalite
app-config Config applicative temps-reel (cashback, etc.)
customer-auth, pdp-auth, admin-auth, administration, feature-flag Auth & Admin
project-owner, project-owner-presentation, project-internal-note Porteurs de projet
project-owner-company-invoicing, internal-project-owner Facturation frais de gestion des societes porteuses (table de correspondance Axonaut/Invoice Ninja + annuaire /internal)
project-financing-request Espace Financement (porteur — demande de financement, presentation, ProjectOwnerCompany, S3 upload). Renamed from espace-fi May 2026; borrower company table project_owner_company (BRI-1501)
project-financing-request-user Acces multi-utilisateurs a une demande de financement (BRI-1807) : project_financing_request_user_rights est la SEULE source d'autorisation (3 roles, soft delete pour retirer/re-inviter), project_financing_request_user_invitation porte le token d'invitation. Extrait de project-financing-request ; arete bidirectionnelle assumee avec lui — le guard et la creation de projet consomment ce module, et son controller importe le meme guard (infra partagee des 15 routes du financement)
project-owner-project-echeancier, project-owner-project-construction-budget, project-owner-project-news, project-owner-project-requests Espace Financement post-financement (porteur — echeancier, tirages travaux, actualites, requetes). News & requests = proxys live gestion-des-defauts, zero stockage DB. Requests = votes (1 demande = 1 vote) : GET agrege les pages upstream + summary, statuts traduits au bord (draft/open→waiting, closed→accepted, cancelled→declined — approximation v1, un vote refuse au vote reste closed upstream) ; POST create-draft (prefixe upstream /api/votes/external), relecture Bricks 72h avant publication au vote
project-owner-company-invoicing Facturation frais de gestion (porteur) — table de correspondance (cf. glossaire) + GET factures par echeance (BRI-1656) : provider lib/providers/axonaut/ (1 seule requete company-filtered, page en HEADER, quota journalier serre — jamais de boucle), matching local sur la description (({projectId}) + « Periode concernee »), avoirs exclus par total negatif, renvoie l'URL publique Axonaut, zero stockage
asset-transfer, files-storage Transferts

Investissement primaire

Modules : primary-purchase, primary-automatic-funding, property-funding, property-early-funding-access

3 flows d'achat distincts, tous commencent par waiting_for_assignation :

1. Achat classique (wallet) : Debit wallet immediat a la creation. waiting_for_assignation -> waiting_for_p2p_creation -> waiting_for_contract_creation -> confirmed

2. Reservation (carte) : Pas de debit wallet. Le customer reserve des briques et paie par carte. waiting_for_assignation -> waiting_for_reservation_confirmation -> paiement carte -> waiting_for_p2p_creation -> waiting_for_contract_creation -> confirmed Expiration : 10 min (carte) ou 10 jours (admin). Max 3 reservations en attente par investisseur. Variante admin : waiting_for_admin_confirmation au lieu de waiting_for_card_initiation. Dual path CB : Lemonway (TOPUP_CARD confirmed) par defaut ; Checkout (TOPUP_CHECKOUT waiting apres payment_approved, utilisable via pendingCredit) derriere ENABLE_CHECKOUT_CARD_BRICKS_PURCHASE. Les deux coexistent.

3. Investissement automatique : Le systeme achete selon le plan d'investissement. Debit wallet immediat, mais l'investor doit confirmer ou annuler. waiting_for_assignation -> waiting_for_auto_invest_investor_confirmation -> confirmation/annulation -> waiting_for_p2p_creation -> waiting_for_contract_creation -> confirmed Vagues : early access (1h avant funding) ou normales (10min apres display + J+1 16h). Distribution round-robin si plus de demandes que de briques disponibles. Eligibilite par vague : un investisseur est exclu des qu'une ligne primary_purchase existe deja sur le bien (meme remboursee). Relance admin manuelle (POST .../funding/trigger-automatic-investment) avec includeInvestorsWithCanceledAutoInvest: true : exclut seulement les investisseurs avec un achat confirmed ou en cours (waiting_* sur primary_purchase) ; refunded et declined ne bloquent pas. Trigger manuel sans garde early-access cote endpoint ni worker (decision ops). Job Graphile dedupe par project-automatic-funding-manual-{propertyId} : erreur si deja en queue.

Echecs possibles (tous flows) : declined (expired, no-bricks-left) ou refunded

Subtilites : - Quota 70% par investisseur : calcule sur le total de briques du bien, pas sur les briques restantes - Retractation 4 jours : calcule en end-of-day timezone Europe/Paris, converti en UTC - L'assignation des briques est une etape separee (batch) : les purchases sont creees sans briques, puis un job assigne les briques libres aux purchases en attente - Oversell bricks physiques : le worker d'assignation pose funding.blocked = { reason: 'bricks-oversell', at } (idempotent + alerte Slack) et decline l'exces en no-bricks-left (WT declined, pas de refund WT ; Redis non decremente sur ces declines) - Tant que funding.blocked est set (bricks-oversell ou counter-reconciliation) : mutations d'achat refusees ; watchers expiration/confirmation exclus via SQL ; assignation continue (drain). Affichage 100 % Redis = overlay lecture quand blocked - Cron funding-counter-reconciliation : block → drain waiting_for_assignation → stabilize → Redis=min(PG, capacity) → unblock (refuse si PG > capacity). Detail : funding-counter-reconciliation.md - Si le nombre total de briques achetees depasse le total apres assignation (race condition), la transaction est rollback - context.refund.at sur la WT d'achat = date de remboursement effectif, pas de demande : il reste null tant que le P2P de remboursement n'est pas confirme (le front affiche alors "en cours de restitution"). Tout nouveau kind de refund doit donc passer par PrimaryPurchaseRefundService.handleSucceededLemonwayP2PRefund dans le handler de succes P2P — sinon la ligne reste "en cours" a vie

Workflow funding : notStarted -> inProgress -> maxEndDateReached -> closed (success | failed)

Subtilites cloture funding : - Success requiert que le refund window du dernier achat soit expire (pas de tous les achats) - Apres cloture success, les refunds customer sont bloques meme si encore dans la fenetre de retractation - Seuls les refunds admin avec raison propertyNotFinanced restent possibles apres cloture - Si aucun achat n'existe, la cloture success est bloquee (pas de latestRefundableUntilDate)

Cycle de vie d'une propriete

Modules : property-creation, properties, property-funding, property-payment-schedule, property-repayment

draft -> published -> funding -> cloture -> echeancier -> remboursements -> fin

Subtilites par transition :

Publication (draft -> published) : - Cree physiquement les briques en base (pas juste un changement de statut) - Schedule les jobs d'investissement automatique (atomique avec la publication) - Prerequis : SPV Lemonway valide, funding dans 2h minimum, au moins 2 images

Cloture funding -> flux financiers post-funding : Les transferts de fees sont des operations manuelles separees, pas automatiques : - Funding fees : SPV -> compte origination Bricks (P2P) - Fiducie fees : SPV -> compte principal Bricks (P2P) - Fonds au porteur : SPV -> compte bancaire porteur (withdraw, pas P2P)

Max transferable au porteur = montant finance - funding fees TTC - fiducie fees TTC - sequestre - budget construction. Plusieurs transferts partiels autorises, cumul tracke.

Statut projet (derive, pas stocke) : - funding-ongoing : funding pas encore cloture - funding-failed : funding cloture en echec - reception-by-project-owner-pending : funding success + fonds pas encore recus par le PDP (receivedByProjectOwnerAt null) - repayment-ongoing : funding success + fonds recus par PDP + pas de remboursement final - repayment-finished : remboursement final execute (finalRepaymentAt set)

La date de reception des fonds par le PDP (receivedByProjectOwnerAt) est set par l'Espace Financement via l'API quand le projet passe en statut running. Elle ne peut pas etre dans le futur et ne peut etre set qu'une seule fois. Elle est prerequise pour approuver les tirages travaux (construction budget).

Echeancier

Module : property-payment-schedule

Un echeancier par bien (obligation/loan), configure apres funding success.

Logique de calcul : - Interets calcules jour par jour (30 jours/mois comptable), le capital restant evolue a chaque evenement - Mensualite = interets + remboursement capital - Sequestre d'interets : montant pre-collecte deduit des mensualites. Peut reduire amountDue a zero (echeance sans paiement du) - amountDue = max(0, mensualite + penalites - sequestre restant). remainingToBePaid = amountDue - deja paye

Evenements qui recomputent l'echeancier a partir de leur date (en preservant les echeances passees) : - capitalLoaned : nouvelle tranche versee (augmente capital restant) - capitalRepaymentPlanned / capitalRepaymentUnplanned : remboursement (diminue capital restant) - prorogation : prolongation de duree (doit etre > duree actuelle) - interestRateChange, vatRateChange, decreaseInterestSequestre

Subtilites : - Prorogation contractuelle prevue (contractSpecifications.durationInMonths.prorogation = duree totale apres prorogation, strictement > nominale) mais pas encore appliquee a l'echeancier : simulation sans ecriture DB via GET /administration/espace-financement/project/:projectId/echeancier/prorogation-simulation (espace-financement-ext) et GET /administration/property/:projectId/payment-schedule/prorogation-simulation (BO) ; 400 prorogation-not-planned si absente, 400 prorogation-duration-should-be-greater-than-nominal-duration si prorogation <= nominal. Si deja appliquee, renvoie l'echeancier courant - GET .../echeancier/start-and-end-date (espace-financement-ext) expose endDateWithProrogation : date du dernier capitalRepaymentPlanned apres simulation de prorogation (null si prorogation absente / simulation impossible) - Penalites de retard : poussees par gestion-des-defauts (webhook POST /administration/gestion-defauts/echeancier/penalties), stockees en jsonb penalty {total, forInvestors, forBricks, discount?} sur l'echeance. Seule la part forInvestors entre dans amountDue ; forBricks n'est pas encaisse via l'echeancier — reclame en une fois au solde de tout compte (somme des forBricks sur le dernier virement) - Le meme webhook porte le statut projet (delay | default | null) : il ecrit financialStatus (etat courant, recalcule a la regularisation) et leve hasBeenInDelayOrDefaultOnce, jamais efface a la regularisation — c'est ce flag qui maintient l'acces au suivi detaille apres retour a la normale, financialStatus distinguant deja retard et defaut. Historique anterieur irrecuperable (financialStatus ecrase sur place) : backfill via export gestion-des-defauts (scripts/property/backfill-has-been-in-delay-or-default-once.script.ts) - L'echeancier PDP (project-owner-project-echeancier) expose penalty (total + les deux parts) quand total > 0 — affiche dans l'Espace Financement avec le detail du calcul - L'echeancier PDP expose mensualite (theorique) et amountDue (du post-sequestre), et derive en vue mensualiteStatus: 'coveredByInterestSequestre' (amountDue = 0 && mensualite > 0 && interestSequestreRemaining > 0) — jamais persiste, le noPayment en base reste surcharge. La jauge "echeances restantes" compte les couvertes dans le total mais pas dans le restant. Detection dupliquee front-side dans le BO (deriveEcheanceDisplayStatus, le contrat admin reste brut) — changer les deux ensemble - Edit BO du contract budget (totalFundEscrow/totalConstructionBudget, action admin property.edit-contract-budget) : sequestre d'interets gele des qu'un echeancier existe (pas de recompute, choix conservateur) ; sinon baisse interdite apres le premier virement PDP, hausse conditionnee au solde Lemonway. Budget travaux indicatif, toujours editable - Remboursement anticipe (capitalRepaymentUnplanned) interdit si echeances passees impayees - Le remboursement anticipe ne peut pas depasser le capital restant a rembourser - Frais de gestion mensuels transferes separement des revenus investisseurs (SPV -> compte Bricks) - Frais de gestion executes seulement si la mensualite n'est pas impayee (inclut noPayment quand le sequestre couvre tout) - TVA sur frais de gestion depend du pays du SPV, et peut changer via evenement vatRateChange - Cloture anticipee : les frais de gestion du dernier mois contractuel sont proratas au jour pres (pas factures pour le mois complet)

Versements revenus & remboursement capital

Modules : property-payment-schedule, property-repayment

Versement revenus (coupons obligation) : - Decouple du paiement porteur : le porteur paie via virement, un admin assigne le virement a une echeance, puis un script manuel (avec fichier Excel) verse les revenus aux investisseurs - Le script calcule : interets / nombre de briques = montant par investisseur - Le versement n'est pas automatise : un operateur execute le script manuellement avec les donnees de paiement - Prelevements fiscaux appliques lors du versement

Remboursement capital : - Payout cree en pending, execute seulement le lendemain (createdAt < NOW()::date) - Si montant < 1ct par brique : payout differe, inclus dans le remboursement final - Remboursement final : calcul exact par investisseur (brickCount * brickPrice - dejaRembourse), pas de floor par brique (evite les erreurs d'arrondi) - Remboursement partiel : Math.floor(montantParBrique * brickCount) (arrondi a l'inferieur) - Le prix de la brique diminue a chaque remboursement partiel : max(0, Math.round((lastBrickPrice * brickCount - repaymentAmount) / brickCount)) - Remboursement final : prix brique set a 0 directement (pas de formule), finalRepaymentAt set sur la propriete - Detection final : totalDejaRembourse + montant = montantFinance

Worker P2P & Withdraws

Modules : lemonway-p2p, lemonway-withdraw

Regle metier : les debits d'un meme compte client sont strictement ordonnances. Withdraws et P2P partagent le meme classement -- si un customer a un withdraw + un P2P en attente, seul le plus ancien est joue. Les comptes SPV/techniques ne sont pas soumis a cette contrainte (parallelisme autorise).

Subtilites retry/fail : - Balance insuffisante + debit + pending credits existent → retry 1h (attendre que le credit arrive) - Balance insuffisante + debit + pas de pending credits → failed + alerte (ne devrait pas arriver) - Credit qui echoue (business error) → toujours retry (les credits ne doivent jamais echouer definitivement) - duplicate_reference → traite comme erreur technique (retry), avec verification d'idempotence cote Lemonway. Cas reel : deploiement entre l'appel Lemonway et la sauvegarde en base - p2p_limit_reached_for_receiver → retry (limite temporaire cote Lemonway)

Referral & Gift Cards

Modules : investor-referral, referrals, gift-card

Referral - subtilites : - Le lien parrain est cree automatiquement a la validation KYC (pas a l'inscription) - Le code parrain est utilisable X jours apres validation KYC (post-KYC). Avant KYC : pas de deadline - Campagnes avec priorites : Birthday Bricks gagne si sa valeur est >= Classic/Premium (ties en faveur de Birthday Bricks). Classic est teste avant Premium. Premium utilise 365j de durabilite au lieu de la duree du lien - Le cap annuel bloque les rewards au moment du paiement, pas a la creation - L'unblocking par invoice doit matcher exactement le cumul des montants bloques (pas de partiel) - Le compte company peut etre referrer mais ne recoit pas de reward - Referrer supprime → pas de reward referrer cree. Les rewards existants ne sont pas annules - Refund d'un achat confirme → annulation de tous les rewards (tous statuts) associes a ce purchaseWtId. Ne s'applique que si purchase.status === 'confirmed'

Referral payout automatique (cron referral-primary-purchase-auto-payout) : - Cron quotidien a 07:00 UTC qui paie les parrainages des projets eligibles, sans intervention admin (1h avant le cron de remboursement de capital investor-payout-capital-repayment a 08:00 UTC). Il n'existe plus de route admin pour declencher un payout manuel : si le cron rencontre un probleme, on le corrige et le rattrapage se fait au prochain run (les paiements de parrainage ne sont pas urgents) - Source de verite "deja traite" : properties."referralPaidAt", set dans la meme transaction que les WT refer_referrer / refer_referee. Toujours set apres traitement par le cron, meme s'il n'y avait rien a verser (cas no-op : early return avec log, notification Slack referralPayoutNoOp, skip customer.io) — semantique "0 paye reste paye". Backfille en migration pour les projets ayant deja recu un payout via la route admin historique - Eligibilite : funding.ended.type = 'success', financialStatusdisbursementAllowedStatuses (repayment-ongoing, repayment-delay, repayment-default, repayment-finished), referralPaidAt IS NULL, et soit (1) au moins une ligne property_dividends avec MIN(createdAt) >= 7 jours (premier versement revenus investisseur effectif, independamment du sequestre), soit (2) aucune ligne property_dividends et funding.ended.at <= NOW() - 60 jours (businessRules.referral.primaryAutoPayoutDaysAfterFundingSuccessWithoutFirstRevenue). Pas de filtre EXISTS pending reward : un projet eligible sans reward a verser est process une fois pour flag referralPaidAt, jamais re-pris ensuite - Ordre : oldest-first par funding.propertyPublishDate (date de publication du projet, pas la date de reception des fonds par le porteur) - Cap journalier : maximum 2 projets payes par jour calendaire Paris (Europe/Paris), compte derive de COUNT(*) FROM properties WHERE ("referralPaidAt" AT TIME ZONE 'Europe/Paris')::date = (NOW() AT TIME ZONE 'Europe/Paris')::date. Une retry Graphile dans la meme journee ne peut pas depasser le cap (les projets deja payes sont filtres par la query d'eligibilite, et leur referralPaidAt compte vers le cap) - Retry : property-not-found et referral-payout-not-allowed sont skip (perte d'eligibilite entre fetch et lock). referral-payout-provision-transfer-failed (Lemonway pre-commit) ou throw inattendu avant commit → throw final pour que Graphile retry. Les echecs post-commit (Customer.io, Slack final) sont best-effort dans le service : le payout reste reussi, alerte Slack sur le thread, pas de retry Graphile (le projet a deja referralPaidAt set) - Provision Lemonway synchrone : transferFundsFromMainAccountToReferralSubaccount reste dans la meme transaction DB qu'avant (cf. ReferralPayoutService.triggerPayoutOfReferralsForProject). Idempotence garantie par attemptP2PAtLemonway qui detecte les references dupliquees (referral-rewards-${businessId}-${projectId.slice(0, 8)}, le businessId rend la ref lisible cote Lemonway) - Recap Slack post-run (#suivi-versement-parrainage, id C0850H45JTD) : query getEligibleProjectsForAutomaticReferralPayout au debut ; payout sur slice(0, cap restant), recap eligibles en attente sur slice(cap) du meme snapshot. Query dediee getProjectsApproachingReferralPayoutWithoutFirstRevenue : sans property_dividends, fenetre J+50–J+59 (primaryAutoPayoutRecapDaysWithoutFirstRevenueMin / primaryAutoPayoutDaysAfterFundingSuccessWithoutFirstRevenue, borne haute exclusive), referralPaidAt IS NULL — disjointe des eligibles J+60+ fallback. Message envoye si au moins une liste non vide. Skip recap si cap journalier deja atteint en debut de run

Gift card - subtilites : - Le refund d'une gift card claimee doit d'abord debiter le giftBalance du claimer (P2P claimer -> compte d'attente), puis refund le donneur en withdrawableBalance - Le claim applique le code parrain du donneur au claimer (si eligible : pas de referrer existant, dans la fenetre de durabilite) - L'email est schedule en heure francaise (emailTriggerFrenchHour en Europe/Paris), converti en UTC - L'expiration est calculee depuis triggerDate (pas depuis l'envoi de l'email) - Le gift balance est separe du withdrawable balance (credit gift, pas withdrawable) - La ligne gift_card a deux ecrivains concurrents : le claim (status/claimer*) et le worker PDF (documentState). Les deux ecrivent le document entier, ce qui n'est sur que parce que chacun lit sous FOR UPDATE et ecrit dans la meme transaction — le worker PDF la tient pendant la generation + S3 + email, donc plusieurs secondes. Le claim lit en for update skip locked et n'attend donc jamais derriere lui : 0 ligne = code inconnu ou ligne prise, desambigue par une relecture sans verrou (gift-card-temporarily-locked vs gift-card-not-found). Codes d'erreur du claim en kebab sans point : le front les utilise directement comme cle i18n et . y est un separateur de niveaux. Sortir l'I/O de la transaction imposera de relire la ligne sous verrou avant d'ecrire - confirmGiftCardCredit renvoie Err('gift-card.not-found-for-credit-wt') si la carte ne pointe plus vers la WT de credit. La WT reste waiting et bloque la file du customer (1 WT par customer dans played-lemonway-p2p) : code d'erreur loggue a chaque iteration, detection par monitor Datadog, resolution manuelle

Anti-fraude retrait

Module : investor-withdraw-request

Deux couches de protection sur les retraits investisseur, evaluees par FraudCheckV2 (active) :

Regle composite (AND) : accountAgeDays < 60 AND confirmedInvestmentCount = 0 AND cardTopupCount >= 1 - Age du compte : jours depuis customers.createdAt - Investissements confirmes : primary_purchase confirmed dont la fenetre de retractation est expiree - Topups carte : topup_card + topup_checkout (tout statut, les declined comptent comme tentative suspecte)

BIC blackliste (OR independant) : si le BIC de l'IBAN commence par un des 7 prefixes neobanques exotiques (DBLKFR22, BZENLT22, INTFBGSFXXX, CFTEMTM1, EVIULT2V, SUMUIE22, SUPULT22)

Decision : blocked = matchesFraudPattern || isBlacklistedBic

Bypasses : - executedBy === 'admin' → jamais bloque - Investisseur dans la whitelist (investor_withdraw_whitelist) → jamais bloque. Le fraudCheck est quand meme enregistre - Le full audit payload (version, config, metriques, decision avec flags individuels) est persiste sur chaque tentative de retrait dans investor_withdraw_requests

Quand bloque : pas de WT de retrait, erreur HTTP 403 withdrawal.blocked-by-fraud-rule, row inseree avec outcome: 'blocked' Quand autorise : WT creee, row inseree avec outcome: 'allowed' + walletTransactionId

Voir investor-withdraw-request/docs/ pour l'analyse d'impact, les templates de requetes, et la reference de donnees.

Boosted balance rate

Module : investor-boosted-balance + businessRules.boostedBalance (@bricks-common/api-communication)

  • Campagnes horodatées en Europe/Paris : launch_offer (4% depuis sept. 2025), puis rate_reduced_may_2026 (2,4% depuis le 29/05/2026 00:00 Paris)
  • GET /investor/boosted-balance/view expose rate.value (pourcentage) et inEffectSince de la campagne active ; disclaimer optionnel via boostedBalanceDisclaimer (actuellement undefined)
  • Gains journaliers calculés sur le solde wallet total ; claim vers gift balance. Feature flag ENABLE_BOOSTED_BALANCE_GAINS_PAYOUT peut forcer 0%

App Config & Cashback

Module : app-config

Endpoint leger (GET /app-config, JWT required) appele une fois au demarrage de l'app. Retourne la config temps-dependante :

Cashback : campagnes promotionnelles appliquees sur les achats primaires pendant une periode. - Source de verite : businessRules.cashback dans @bricks-common/api-communication - Convention : une seule campagne active a un instant T - getActiveCampaignAt(date) : retourne la premiere campagne dont date est dans [startDate, endDate] (inclusif, timezone Europe/Paris) - Reponse : { activeCashback?: { operationName, percentage, startDate, endDate } }

Application backend : lors d'un achat primaire, primary-purchase.service evalue getActiveCampaignAt(purchaseDate). Si actif, le cashback est attache au contexte de la WT : amount = round(purchaseValue * percentage / 100) en centimes + percentage + operationName

Integration front : - Mobile : AppConfigProvider gate (bloque le rendu si pending), prefetch au boot via runMandatoryLoader. CashbackBadge sur les cartes projet, CashbackBanner sur l'ecran d'achat - Web : RTK Query lazy + Redux slice appConfig. Badge sur les cartes projet, banner dans le modal d'achat

Checkout webhook enrichment

Module : money-in/checkout

Les webhooks Checkout.com (payment_approved / payment_declined) capturent desormais des donnees supplementaires sur chaque paiement, stockees dans wallet_transactions.context (JSONB) :

Donnee Source webhook Champ context
Risk risk.flagged, risk.score context.risk
Response summary response_summary context.responseSummary
Card issuer source.card_type, card_category, issuer, issuer_country, bin context.card.cardType, .cardCategory, .issuer, .issuerCountry, .bin
Client IP payment_ip context.paymentIp

mapCardFromCheckoutSource normalise les champs carte (snake_case webhook → camelCase context). Tous les champs supplementaires sont optionnels ; si Checkout les omet, ils restent undefined.

Construction budget -- flux bypass

Module : property-construction-budget

L'approbation d'une demande de tirage travaux supporte deux modes :

Nominal (approve-withdraw) : cree un retrait Lemonway via LemonwayWithdrawService (SPV → compte bancaire porteur). Persiste lemonwayWithdrawId + ibanId

Bypass (approve-no-withdraw) : approuve sans creer de retrait Lemonway. Le mouvement de fonds est gere hors API (virement manuel). Persiste lemonwayWithdrawBypassed: true

Les deux modes sont exclusifs (XOR enforce par un refinement sur le validator). Les deux requierent que funding.receivedByProjectOwnerAt soit renseigné (le PDP a reçu les fonds).


Workflows cross-modules

Parcours investissement primaire : money-in (credit wallet) -> primary-purchase (reserve briques) -> assignation briques (batch) -> lemonway-p2p (debit wallet -> SPV) -> investor-financial-document (contrat PDF) -> portfolio

Parcours revenus obligation : Porteur paie (virement) -> admin assigne a echeance -> script mensuel verse revenus -> lemonway-p2p (SPV -> investisseurs) -> investor-taxation (prelevements) -> wallet-transactions

Parcours remboursement capital : Admin cree payout pending -> cron J+1 execute -> lemonway-p2p (SPV -> investisseurs) -> brick price mis a jour -> si final : finalRepaymentAt set

Parcours retrait : Customer demande retrait -> lemonway-withdraw (wallet -> compte bancaire). Ordonnance avec les P2P du meme compte.