Modèle de données
Modèle de données
Source de vérité : docs/04-data-model.md. Le fichier prisma/schema.prisma doit en être le reflet exact. Toute modification passe par une PR avec migration Prisma associée.
docs/04-data-model.md ou directement app/pulse/prisma/schema.prisma.Principes généraux
Identifiants et timestamps
Tous les id sont des cuid() (type String). UUIDv7 réservé aux rares cas où le tri temporel natif est nécessaire.
Toute entité métier porte les trois champs suivants :
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organizationId String // isolation multi-tenant
Soft delete
Le soft delete (deletedAt DateTime?) est appliqué uniquement sur les entités à valeur légale ou auditée. Les référentiels et tables pivot utilisent le hard delete.
Entités avec soft delete : User, Organization, Asset, Contact, Establishment, Company, Opportunity, Quote, Order, Invoice, LeaseAffair, LeaseContract, Product.
Types de données
- Montants monétaires :
Decimal @db.Decimal(15, 2)— jamaisFloat. - Enums Prisma : préférés à
Stringpour toute valeur finie. - JSON : utilisé avec parcimonie, uniquement pour des structures vraiment dynamiques (paramètres, payload d'audit). Sinon, tables explicites.
Isolation multi-tenant
Chaque entité tenant-scoped filtre systématiquement par organizationId, lu depuis session.activeOrganizationId côté API — jamais depuis le corps de la requête.
Audit log
Table AuditLog append-only. Chaque mutation sensible y est tracée via auditLog(...) dans le service, jamais dans la route ni dans le repository.
model AuditLog {
id String @id @default(cuid())
organizationId String?
actorUserId String?
action String // ex: "asset.create", "lease.contract.unlock"
targetType String?
targetId String?
payload Json?
createdAt DateTime @default(now())
}
Vue d'ensemble des domaines
| Domaine | Entités clés |
|---|---|
| Identity | User, Organization, Member, Team, Role, Permission, ApiKey, Invitation |
| Annuaire | Company, Establishment, Person, Brand, Segment, Function |
| Catalogue | Product, ProductCategory, ProductAttribute, ProductVariant, ProductSellModality |
| CRM | Opportunity, OpportunityLine, EstablishmentProspect, ProspectBinder, ProspectAction, ProspectQuery, CallLog |
| Ventes & Facturation | Customer, Supplier, Quote, Order, Invoice, CreditNote, Payment, BillingEntity, SupplierInvoice |
| Location LLD/LCD | LeaseAffair, LeaseContract, LeaseFinancing, LeaseRentSchedule, Lender, Cession, LeaseAmendment, LeaseInsurance, LeaseMaintenance |
| Assets & Maintenance | Asset, AssetModel, AssetFamily, AssetLocation, AssetMovement, MaintenanceOperation, SparePartReference, StorageLocation, TransportRequest |
| Comptabilité analytique | AccountingCode, SupplierInvoiceAllocation, FixedAsset, FiscalYearClosing, AccountingProvision |
| Cotation REFI | ClientRating, FinancialStatement, FinancialStatementLine |
| Fichiers | File, FileLink, FileVersion, FileShare, StorageBucket |
| Collaboration | Comment, CommentReaction, CommentAttachment |
| Notifications | Notification, NotificationPreference, EmailQueue, SmsQueue, EmailTemplate |
| AI Kit | AiWorkflow, AiAgent, AiRun, AiPromptTemplate |
| Audit & Jobs | AuditLog, AccessLog, Job |
| E-invoicing | EInvoiceLifecycle, EInvoiceStatusEvent, EReportingSubmission |
| E-signature | SignatureRequest, SignatureSigner |
| Admin | ApplicationSetting, OrganizationSetting, ReleaseNote, Webhook |
Détail par domaine
Identity & Auth
Les tables User, Account, Session, Verification, TwoFactor, Organization, Member, Invitation sont en partie générées par Better Auth. Pulse les étend avec des champs métier sans rompre le contrat Better Auth.
Member (imposé par le plugin Organization de Better Auth), et non Membership. User.role et Member.role restent String pour la même raison.Points clés :
User.status:ACTIVE | INVITED | SUSPENDED | ARCHIVEDUserGlobalRole:SUPERADMIN_PROVIDER | ADMIN_PROVIDER | USERRole: peut être global (Pulse) ou org-scoped ;isSystem = true→ non éditable.Permission.code: formatdomain:action(ex:asset:create,lease.contract:unlock).ApiKey: stocke uniquement lehashedSecret(bcrypt), leprefix(8 chars) pour affichage, et lesscopes(tableau de permissions).
Annuaire
Hiérarchie : Company → Establishment → Person. L'identité légale (SIREN/SIRET) vit sur Company/Establishment, enrichie depuis Pappers ou INSEE.
Establishment.segmentId→Segment(relation 1-N, implémenté en version simplifiée sans tables M2M).Personpeut être rattaché à uneCompanyet/ou unEstablishment.Brand(enseigne) est une entité org-scoped distincte deCompany.
Catalogue
Product supporte plusieurs modalités de vente via ProductSellModality :
| Type | Usage |
|---|---|
ONE_SHOT | Vente unitaire |
SUBSCRIPTION | Abonnement mensuel / trimestriel / annuel |
LEASE | Location (LLD/LCD) |
SERVICE | Prestation à l'unité (intervention, heure, km) |
CRM
Opportunity.amountHtest dérivé (somme des lignes), jamais saisi à la main.weightedAmountHt = amountHt × probability / 100.EstablishmentProspect.establishmentIdestNOT NULL— un prospect existe toujours via un établissement de l'annuaire.OpportunityLossReason: liste fermée paramétrable par organisation, obligatoire à la clôtureCLOSED_LOST.CaMaturity:PIPE → WON → SECURED → INVOICED → REALIZED(maturité du CA pour le reporting).
Ventes & Facturation
BillingEntity, allouée sous advisory lock PostgreSQL. Elle est immuable une fois émise.BillingEntityporte les séquences de numérotation (invoiceSequence,quoteSequence, etc.) et les préfixes.Invoice.amountDueest stocké (pas calculé à la volée) pour permettre les filtresOVERDUEperformants.- La chaîne côté achats miroir la chaîne vente :
SupplierQuote → SupplierOrder → SupplierInvoice → SupplierPayment. SupplierInvoiceporte les dimensions analytiques (accountingCodeId,leaseContractId,fiscalYear,quarterTag…).
Location LLD/LCD
LeaseRentSchedule) est immuable après signature, sauf via avenant (LeaseAmendment). La cession (Cession) est irréversible une fois COMPLETED.Flux principal :
Opportunity → LeaseAffair → LenderApprovalRequest(s) → LeaseContract
↓
LeaseFinancing (PMT, taux, marges)
↓
LeaseRentSchedule (échéancier)
↓
Cession → Lender (bailleur final)
Points clés :
LeaseContract.isLocked = truepar défaut. Déverrouillage via permission spécialelease.contract:unlock(ADMIN uniquement), raison obligatoire, re-verrouillage automatique 24 h.LeaseFinancingstocke le taux client, le taux bailleur, la marge, et toutes les commissions (partenaire, agent, KDOS…).LenderPaymentSchedule: suivi des prélèvements bailleur (mandat de gestion / CB adossé).LeaseBankCommitment: encours des engagements bancaires par business unit et bailleur.LeaseInsuranceetLeaseMaintenance: gestion des services inclus dans le loyer, avec leurs propres échéanciers.
Assets & Maintenance
Asset.currentLocationId→AssetLocationactive. L'historique complet vit dansAssetLocation[].AssetServiceLink: QR code unique par asset (shortCode,publicUrl).MaintenanceOperation: typesPREVENTIVE | CURATIVE | INSPECTION | INSTALLATION | DECOMMISSIONING.StorageLocation: entrepôt/dépôt physique géolocalisé, peut être géré par unEstablishment.TransportRequest: multi-assets viaTransportRequestAsset(table pivot).
Comptabilité analytique
AccountingCode: plan comptable hiérarchique (parentCodeId), catégoriesIMMOBILISATION | PURCHASE | SERVICE | COMMISSION | INSURANCE | MAINTENANCE | FINANCIAL | BUYBACK | OTHER.SupplierInvoiceAllocation: ventilation analytique fine d'une facture fournisseur sur plusieurs comptes / contrats / exercices.FixedAsset: immobilisation comptable liée à unAssetphysique et/ou un contrat. Méthode d'amortissementLINEAR | DEGRESSIVE | NONE.FiscalYearClosing→AccountingProvision: gestion FNP, CCA, CAP, AAR, provisions pour risques.
Fichiers
StorageBucket: configuration du bucket S3 ou filesystem (dev).objectLockYearspour conformité légale (factures et contrats : 10 ans — Object Lock Compliance).FileLink: table pivot polymorphique reliant unFileà n'importe quelle entité (targetType,targetId). Permet la galerie multi-domaine sans FK nullable pour chaque modèle.- En production, les URL de fichiers sont signées et éphémères ; jamais de URL permanente exposée.
Index et contraintes
Index systématiques
Pour toute entité tenant-scoped :
@@index([organizationId, createdAt])
@@index([organizationId, deletedAt]) // si soft delete
Index spécifiques sur les colonnes filtrées fréquemment : status, dueDate, ownerUserId, stageId, etc.
Validations Zod (côté entrée)
| Champ | Validation |
|---|---|
email | .email() |
siren | Luhn + longueur 9 |
siret | Luhn + longueur 14 |
vatNumber | VIES si UE |
iban | Algorithme MOD-97 |
| Montants | Decimal(15, 2) — jamais Float |
Champs calculés vs stockés
| Cas | Stratégie |
|---|---|
| Totaux HT/TVA/TTC sur lignes et entêtes | Stockés (cohérence comptable + audit) |
weightedAmountHt opportunité | Stocké (perf agrégats pipeline) |
amountDue facture | Stocké (filtre "en retard" fréquent) |
Statut facture (PAID / OVERDUE / …) | Stocké + recalcul job nocturne |
| MTBF/MTTR par modèle d'asset | Calculé à la demande (vue matérialisée si volumétrie) |
Migrations zero-downtime
Protocole en 3 temps :
- Ajouter nullable — déployer la migration, le code existant est inchangé.
- Backfill — remplir les lignes existantes (script ou job one-shot).
- Appliquer la contrainte —
NOT NULLou suppression dans la release suivante.
Pour une suppression de colonne :
- Supprimer toutes les références dans le code (release N).
- Supprimer la colonne en base (release N+1).
Les migrations versionnées vivent sous app/pulse/prisma/migrations/. Ne jamais modifier une migration existante appliquée.
Seed
Le seed minimal (prisma/seed.ts) crée le super-admin Pulse, l'organisation démo, la matrice complète de permissions/rôles standard, les stages CRM, les motifs de perte, les modes de paiement et le plan comptable de référence. Un seed démo séparé (prisma/seed-demo.ts) ajoute des jeux de données réalistes (50 entreprises, 100 opportunités, 30 contrats LLD, 500 assets…) uniquement en environnement de développement.