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 (script 01 refuse toute hausse vs le dernier
brick_prices), revente partielle d'un lot (propertyResalePartialSaleEvent), ou revente totale (prix tombe a 0). Un changement de brick price (script mensuel royalty) annule les deals marketplacependinguniquement, pas lesreserved(P2P en cours, bricks encore au vendeur). Script 01 refuse une revente royalty finale tant que le marketplace n'est pas deja ferme (isMarketplaceAllowed = false) ou qu'il reste un dealpending/reserved(voir Revente royalty / marketplace) - 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). Bascule pilotee par le feature flag DBENABLE_PROJECT_OWNER_COMPANY_INVOICING_API(tablefeature_flag, absent =false= workflows Bubble) — le flag gate les ecritures (creation de societe a la publication + emission de factures), jamais la lecture : l'espace-fi et le BO lisent Axonaut avec la memeAXONAUT_API_KEYen permanence, donc les deux chemins doivent pointer sur le meme espace Axonaut ;INVOICE_NINJA_API_KEYsuit la meme regle : lecture permanente (liste BO), ecritures derriere le flag — les deux cles sont requises en production (refinement env), sinon une cle absente se lirait « aucune facture » en BO. Tant que Bubble cree encore les societes (BRI-1659), le flag atruedoublonnerait project_owner_company_invoicing_ids: table de correspondance 1 ligne par projet ->axonautId+invoiceNinjaId(+siren). Colonne DBpropertyId(nommage legacy des FK versproperties), appeleeprojectIden code ;UNIQUE(propertyId),sirennon-unique (N projets d'une meme societe partagent 1 siren + memes IDs).axonautIdetinvoiceNinjaIdnullables, au moins un des deux pose (CHECK) : une societe Bubble peut n'avoir aucun compte Axonaut et le client Invoice Ninja n'est rempli par le provisioning que depuis BRI-1770 — les rows partielles sont completees au prochain run de la task, chaque provider garde par son propre id. Lecture unitaire Bubble (/invoicing-ids) = 404 quandaxonautIdest nul, son contrat n'expose que la societe Axonaut.companyBubbleIdnullable = 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 scriptbackfill-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 ; une societe Bubble sans compte Axonaut est desormais backfillable (seule celle sans aucun des deux ids est skippee + listee dans l'audit). Servie parGET /internal/project-owner/management-fees-invoicing/:period(bulk, middleware invoice-axonaut) etGET /internal/project-owner/:projectId/invoicing-ids(unitaire, Bubble) — cle communeproject-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 :withdrawableBalanceetgiftBalanceChange. Unwithdrawal_feeest lie auwithdrawalviareferenceId: le cancel BO d'un retraitwaiting(pas encore chez Lemonway) et le cron de money-out LW decline aussi le feewaitinglie - Withdrawable balance : solde retirable. Calcul :
current + pendingCredit - pendingDebit(confirmed + waiting credits - waiting debits). Lestopup_carden 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 (viaamountToUseFromGiftBalance)
Index module -> contexte¶
| Module | Contexte |
|---|---|
| primary-purchase, primary-automatic-funding | Investissement primaire — détail |
| property-funding, property-early-funding-access | Investissement primaire |
| property-creation, properties | Cycle de vie propriete |
| property-payment-schedule, property-repayment | Echeancier & Remboursements — détail |
| project-test-bank-debit | Prélèvement test 1€ (Lemonway SDD) — détail |
| 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, notary-auth, administration, feature-flag | Auth & Admin |
| notary | Espace notaire V1 — un seul notaire. NotaryAuthGuard protège les lectures projet ; aucun allowlist par notaire (multi-notaires hors V1). La liste expose les projets obligation/loan hors funding-failed et funding-ongoing (collecte en cours : pas d'acte). royalty n'ouvre droit à aucune hypothèque. repayment-finished reste, pour la mainlevée. Filtrables sur « traité » (notary_project.json.processed) ; ni la garantie ni le statut ne filtrent l'accès. Traité (processed) et rendez-vous (appointment) cohabitent dans le JSON de notary_project ; l'annuler conserve la ligne en passant appointment.scheduledAt à NULL. Les lectures par projet (détail, échéancier JSON, PDF nominal et prorogé) rejouent le même filtre SQL que la liste (inNotaryBoundaries) → 404 hors périmètre. GET /notary/projects/:id renvoie la ligne de liste (isTreated, garanties, contacts) plus scheduledAt et notary (étude, adresse, email, téléphone, depuis project_financing_request.json.notary d’une demande completed, null sinon) — une fiche rechargée ne dépend pas du cache liste. Il porte aussi legalRepresentative (name, email, phone optionnel) depuis special_purpose_vehicule.json.legalRepresentative, la personne envoyée à Lemonway. Un représentant absent fait échouer le GET. companyName reste le nom de cette société. La liste reprend contactEmail et contactPhone depuis ce même représentant légal. La fiche porte receivedByProjectOwnerAt et isReceivedByProjectOwnerAtLocked (vrai seulement si la date est déjà saisie et qu’un flux existe). PUT …/funding-received-by-project-owner-date est une commande (saisie / correction / null annule) qui porte aussi la transition reception-by-project-owner-pending → repayment-ongoing (rewind inverse à l'annulation), pour que le front n'enchaîne jamais d'appels. L'annulation droppe la clé JSONB (funding - 'receivedByProjectOwnerAt') : propertyFundingPg la type isoDate.optional(), qui rejette null et casserait chaque lecture suivante. Le notaire est auteur de l'acte, donc l'unicité de receivedByProjectOwnerAt ne vaut que côté admin/EF ; la date future reste refusée. Le même jour est refusé. Toute autre écriture (première saisie, autre jour, annulation) est refusée si un flux existe déjà (property_dividends, capital pending/paid, tirage approved, legacy ou transfer-to-echeancier-payment, referralPaidAt). « Traité » reste orthogonal : cette commande ne l'écrit jamais. L'échéancier notaire est un aperçu (lignes live + récap) ; le PDF imprime ce récap. Le récap affiche un taux annualisé (ratio Bubble non actuariel, libellé porteur — pas le TAEG actuariel) : coût total rapporté au montant financé, annualisé sur echeancierConfig.durationInMonths (durée en vigueur). Le PDF « avec prorogation » le recalcule avec la durée de prorogation du contrat ; le taux duringProrogation n'entre pas dans la formule. La fiducie entre en HT (fiducieFees.amountHt), absente elle compte pour 0. Pas de TVA ajoutée, pas de TRI, pas de persistance. Des honoraires de collecte absents retirent le taux annualisé, l'échéancier reste. Pas de JSON prorogé — seule la simulation PDF existe. Les hypothèques vivent dans operation.guarantee pour les deux variantes (guarantee déclaré sur obligV1 sans coverage) : une garantie n'a pas de seconde adresse, et le registre financement n'en modélise aucune (fees.guarantee = des frais). guarantee absent = inconnu, jamais « pas de garantie » — la source est l'acte signé. Sur l'historique V1, l'export ops (tableau Bubble, actes, et fiche projet pour les collectes 2023-24 sans ligne Bubble) fait foi : une case vide reste hors filtre ; une PPD n'est pas un rang 1 ; une promesse d'affectation n'est pas une hypothèque inscrite. Côté BO, GET /administration/project/:projectId/notary (AdminAuthGuard) sert la même vue sans le périmètre notaire — tous les projets — et y ajoute receivedByProjectOwnerAt (properties.funding.receivedByProjectOwnerAt). Contrats signés : proxy lecture-seule de AnalyseApi.getSignatureBundle sur l’id du projet, une demande de financement completed servant de garde ; pas de demande completed, 404 Analyse ou bundle sans documents → 200 { groups: [] } ; preview/download = URL présignée, pas de stream API. |
| product-highlight | What's New cloisonné par appScope (pdp / investors) — détail |
| company-internal-fundraising | Levée de fonds communautaire (campagne JSON+_view, presentationVideoUrl obligatoire sans _view, banking { beneficiary, iban, bic, bankName, address } obligatoire sans _view (IBAN/iban_zod, BIC/bic_zod ; GET investisseur banking required à la racine (RIB campagne, toujours présent, indépendant de peaPme), peaPme { id?, status, amount, bankTransferReference } si PEA waiting ou confirmed ; PATCH admin banking y compris clôturée, pas le PUT), documents { name, url, tag?, description? } ordre = array JSON, unicité tag/campagne, avant-première earlyAccessBeforeStartInHours, pause newInvestmentPausedAt, clôture ended, intention d'investissement JSON+_view waiting/confirmed/canceled (wallet → confirmed ; PEA et big-wire → waiting, confirmed + receivedAt au BO) + onBehalfOfCompany optionnel (compte individual investit pour une société, persisté au confirm, lu par le bulletin signé) + PEA-PME ≥ 100 k€ perso sans WT via POST …/purchase/pea-pme (peaPme.bankTransferReference BRICKS-PEA-XXXXXX, peaPme.receivedAt optionnel, pas onBehalfOfCompany) + big-wire ≥ 100 k€ perso sans WT via POST …/big-wire-intentions (bigWire.bankTransferReference BRICKS-VIR-XXXXXX, receivedAt optionnel posé au BO, plusieurs par investisseur, GET current bigWireIntentions (waiting + confirmed)) + bulletin PDF à la volée (POST …/purchase/draft-bulletin binaire ; POST …/draft-bulletin/url → { url }, PDF en Redis 5 min derrière un jeton, GET …/draft-bulletin/:token public inline, watermark PROJET, pas de persist en base, 409 pea-pme-already-exists ; GET …/campaigns/:campaignId/intentions/:investmentIntentionId/signed-bulletin JWT binaire ; POST …/intentions/:investmentIntentionId/signed-bulletin/url → { url }, PDF en Redis 5 min derrière un jeton, GET …/signed-bulletin/:token public inline ; bulletin définitif à la volée PEA-PME ou big-wire uniquement (wallet → 409 signed-bulletin-wallet-not-supported), signature électronique date seule (createdAt, Europe/Paris) Allura, 404 investment-intention-not-found ; RIB campaign.banking beneficiary/bankName/iban/bic ; aperçu admin POST …/campaigns/:id/bulletin-preview sans gate d'achat, signed pour le cas définitif), intention déclarative JSON+_view avant startedAt avec soft delete deletedAt sans revival, kinds WT company_internal_fundraise_purchase + _refund, rétractation 4j, reset dev-only après pause (refus si clôturée) des intentions déclaratives, des intentions et des WT associés, avec recalcul des soldes — la campagne et ses PDF restent ; config actionnariat table JSON-only (pas de _view), singleton, GET/PUT admin shareholding-config ; GET …/shareholding lit ces prix (graphique) et la plus-value ; fundraising2026 regroupe titlesAttributed et holdingDocumentIssued (YYYY-MM-DD ou null) et closedAt (ended.closedAt de la campagne 2026, startedAt Europe/Paris, y compris sans position), position 2022 optionnelle (sharesCount / 10 €, shareholderSince = premier investissement confirmé), individualFundraising2026 (compte normal : wallet confirmed, big-wire et PEA confirmed ou waiting, waitingAmount déjà dans value) et onBehalfOfCompanyFundraising2026 (groupé pays + SIREN, amount inclut le big-wire waiting), chacun omis si vide, historique WT séparé par personne, 3 dernières WT levée 2022+2026 refunds compris) — détail |
| home-news | Home news cloisonnée par appScope — GET public = news active du scope, sinon null |
| faq | FAQ (JSON+_view), une ligne par (appScope, page) — pages company_internal_shareholding | project | boosted_balance | company_internal_shareholding_2022, unicité SQL sur le couple. GET investisseur /investor/faqs/:page = items du scope investors, sinon null. HTML FR only (comme la FAQ campagne). Pas de create/delete |
| news-banner | Bannière info cloisonnée par appScope dans le json — GET public filtre enabled |
| 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. Surface BO : GET /administration/project-financing-request paginé serveur — détail ; surface BO « Sociétés » : GET /administration/project-owner-company, une ligne par project_owner_company — détail ; fiche brouillon : GET /administration/project-financing-request/:id, PATCH …/:id/presentation (draft only, nom + type) — détail ; fiche Société : GET /administration/project-owner-company/:id, PATCH …/:id/identity (tous statuts, null refusé hors draft → 409 company-invalid-for-status), PATCH …/:id/lei (hors draft, 409 lei-not-editable), PUT …/:id/representatives/:representativeId (identité PP + KYB, tous statuts, birthDate / residenceCountry à null refusés hors draft → 409 company-invalid-for-status) — détail ; documents admin des deux fiches : …/project-financing-request/:id/documents et …/project-owner-company/:id/documents (liste, POST multipart fichier + payload JSON — fiche Société : un lien par projet choisi —, PATCH type / rattachement, DELETE tant que les projets lecteurs sont en draft (une pièce pdp-onboarding retirée rouvre l'étape Documents validée) et, fiche Société, jamais un fichier sur plusieurs projets, Remplacer = supprimer + ajouter) — détail ; onglet RIB : PATCH …/project-owner-company/:id/banking (finalized seulement, iban / bic jamais effacés, bankName / accountHolder / label à null = retirés, 409 company-banking-not-editable sinon), vue + banking / linkedSpvIds (properties.spvId distincts des projets financés, nourrissent GET /administration/spv/:spvId/sdd-mandates). Une société porte N projets : le lien est project_financing_request."companyId" ; ses représentants vivent dans project_owner_company_representative (save App PDP = diff par id, absents archivés, jamais supprimés) ; push CRM à la création du brouillon (job graphile project-financing-request-creation, flag ENABLE_CRM_PDP_SYNC) |
| project-financing-request-user | Accès multi-utilisateurs à une demande de financement — project_financing_request_user_role est la seule source d'autorisation — détail ; surface BO « Utilisateurs espace financement » : administration/project-financing-request/users* — détail |
| project-financing-request-account-manager | Account manager d'une demande de financement — table project_financing_request_account_manager (6 AM repris de Bubble, uuid figés partagés avec le CRM, réglages d'assignation dans le json), assignation portée par project_financing_request.json.accountManagerId. Qui décide, en synchrone : à la création, computeAccountManagerForNewProject dans la transaction du dossier (règles : business/compute-account-manager) ; ensuite, le BO via PATCH /administration/project-financing-request/:id/account-manager. Qui propage, en asynchrone : à la création, la task project-financing-request-creation (CRM managed_by, puis Slack) ; sur un changement BO, la task project-financing-request-account-manager-assignment (CRM add-account-manager, Projet Analyse, Slack). Push CRM derrière ENABLE_CRM_PDP_SYNC. |
| project-conversation | Messagerie CRM des account managers (BO Projets) — l'API rend l'URL d'iframe /embed/conversations-all via le provider CrmApi (lib/providers/crm, env crm), userExternalId = admin.id (le CRM est aligné sur better_auth_user.id), zéro stockage — détail |
| project-financing-request-echeancier, project-financing-request-construction-budget, project-financing-request-news, project-financing-request-requests, project-financing-request-dashboard | Espace Financement post-financement (porteur) — échéancier, tirages travaux, actualités, requêtes, dashboard — détail |
| project-owner-company-invoicing | Facturation des frais de gestion porteur (Axonaut + Invoice Ninja), zéro stockage local — détail |
| asset-transfer, files-storage | Transferts d'actifs entre investisseurs (succession, cession) ; S3 public/private et upload d'images — détail |
| feedback | Feedback BO → Slack (@sos_captain sur chaque message, 🚨 si urgent, capture en thread), zéro stockage — détail |
Flux métier détaillés¶
Chaque module porte sa propre doc dans src/__new/modules/<module>/docs/ — c'est là que vivent
les cycles de vie et les règles de calcul (achat primaire, échéancier, prélèvement test,
versements, parrainage, anti-fraude, cashback, budget travaux…). Ce fichier reste une carte :
il dit où regarder, pas comment chaque flux est implémenté.
Les seuls parcours qui ne tiennent dans aucun module sont dans
docs/architecture-overview/api-cross-module-flows.md.