Modèle de données

Conventions Prisma, vue d'ensemble des domaines, règles d'index et de migration zero-downtime du schéma Pulse ERP.

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.

Cette page est une synthèse orientée développeur. Pour les schémas complets champ par champ, consultez 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) — jamais Float.
  • Enums Prisma : préférés à String pour 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

DomaineEntités clés
IdentityUser, Organization, Member, Team, Role, Permission, ApiKey, Invitation
AnnuaireCompany, Establishment, Person, Brand, Segment, Function
CatalogueProduct, ProductCategory, ProductAttribute, ProductVariant, ProductSellModality
CRMOpportunity, OpportunityLine, EstablishmentProspect, ProspectBinder, ProspectAction, ProspectQuery, CallLog
Ventes & FacturationCustomer, Supplier, Quote, Order, Invoice, CreditNote, Payment, BillingEntity, SupplierInvoice
Location LLD/LCDLeaseAffair, LeaseContract, LeaseFinancing, LeaseRentSchedule, Lender, Cession, LeaseAmendment, LeaseInsurance, LeaseMaintenance
Assets & MaintenanceAsset, AssetModel, AssetFamily, AssetLocation, AssetMovement, MaintenanceOperation, SparePartReference, StorageLocation, TransportRequest
Comptabilité analytiqueAccountingCode, SupplierInvoiceAllocation, FixedAsset, FiscalYearClosing, AccountingProvision
Cotation REFIClientRating, FinancialStatement, FinancialStatementLine
FichiersFile, FileLink, FileVersion, FileShare, StorageBucket
CollaborationComment, CommentReaction, CommentAttachment
NotificationsNotification, NotificationPreference, EmailQueue, SmsQueue, EmailTemplate
AI KitAiWorkflow, AiAgent, AiRun, AiPromptTemplate
Audit & JobsAuditLog, AccessLog, Job
E-invoicingEInvoiceLifecycle, EInvoiceStatusEvent, EReportingSubmission
E-signatureSignatureRequest, SignatureSigner
AdminApplicationSetting, 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.

Le modèle d'appartenance s'appelle 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 | ARCHIVED
  • UserGlobalRole : SUPERADMIN_PROVIDER | ADMIN_PROVIDER | USER
  • Role : peut être global (Pulse) ou org-scoped ; isSystem = true → non éditable.
  • Permission.code : format domain:action (ex: asset:create, lease.contract:unlock).
  • ApiKey : stocke uniquement le hashedSecret (bcrypt), le prefix (8 chars) pour affichage, et les scopes (tableau de permissions).

Annuaire

Hiérarchie : CompanyEstablishmentPerson. L'identité légale (SIREN/SIRET) vit sur Company/Establishment, enrichie depuis Pappers ou INSEE.

  • Establishment.segmentIdSegment (relation 1-N, implémenté en version simplifiée sans tables M2M).
  • Person peut être rattaché à une Company et/ou un Establishment.
  • Brand (enseigne) est une entité org-scoped distincte de Company.

Catalogue

Product supporte plusieurs modalités de vente via ProductSellModality :

TypeUsage
ONE_SHOTVente unitaire
SUBSCRIPTIONAbonnement mensuel / trimestriel / annuel
LEASELocation (LLD/LCD)
SERVICEPrestation à l'unité (intervention, heure, km)

CRM

  • Opportunity.amountHt est dérivé (somme des lignes), jamais saisi à la main. weightedAmountHt = amountHt × probability / 100.
  • EstablishmentProspect.establishmentId est NOT NULL — un prospect existe toujours via un établissement de l'annuaire.
  • OpportunityLossReason : liste fermée paramétrable par organisation, obligatoire à la clôture CLOSED_LOST.
  • CaMaturity : PIPE → WON → SECURED → INVOICED → REALIZED (maturité du CA pour le reporting).

Ventes & Facturation

La numérotation des factures est une séquence sans trou par BillingEntity, allouée sous advisory lock PostgreSQL. Elle est immuable une fois émise.
  • BillingEntity porte les séquences de numérotation (invoiceSequence, quoteSequence, etc.) et les préfixes.
  • Invoice.amountDue est stocké (pas calculé à la volée) pour permettre les filtres OVERDUE performants.
  • La chaîne côté achats miroir la chaîne vente : SupplierQuote → SupplierOrder → SupplierInvoice → SupplierPayment.
  • SupplierInvoice porte les dimensions analytiques (accountingCodeId, leaseContractId, fiscalYear, quarterTag…).

Location LLD/LCD

Le tableau d'amortissement (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 = true par défaut. Déverrouillage via permission spéciale lease.contract:unlock (ADMIN uniquement), raison obligatoire, re-verrouillage automatique 24 h.
  • LeaseFinancing stocke 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.
  • LeaseInsurance et LeaseMaintenance : gestion des services inclus dans le loyer, avec leurs propres échéanciers.

Assets & Maintenance

  • Asset.currentLocationIdAssetLocation active. L'historique complet vit dans AssetLocation[].
  • AssetServiceLink : QR code unique par asset (shortCode, publicUrl).
  • MaintenanceOperation : types PREVENTIVE | CURATIVE | INSPECTION | INSTALLATION | DECOMMISSIONING.
  • StorageLocation : entrepôt/dépôt physique géolocalisé, peut être géré par un Establishment.
  • TransportRequest : multi-assets via TransportRequestAsset (table pivot).

Comptabilité analytique

  • AccountingCode : plan comptable hiérarchique (parentCodeId), catégories IMMOBILISATION | 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 à un Asset physique et/ou un contrat. Méthode d'amortissement LINEAR | DEGRESSIVE | NONE.
  • FiscalYearClosingAccountingProvision : gestion FNP, CCA, CAP, AAR, provisions pour risques.

Fichiers

  • StorageBucket : configuration du bucket S3 ou filesystem (dev). objectLockYears pour conformité légale (factures et contrats : 10 ans — Object Lock Compliance).
  • FileLink : table pivot polymorphique reliant un File à 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)

ChampValidation
email.email()
sirenLuhn + longueur 9
siretLuhn + longueur 14
vatNumberVIES si UE
ibanAlgorithme MOD-97
MontantsDecimal(15, 2) — jamais Float

Champs calculés vs stockés

CasStratégie
Totaux HT/TVA/TTC sur lignes et entêtesStockés (cohérence comptable + audit)
weightedAmountHt opportunitéStocké (perf agrégats pipeline)
amountDue factureStocké (filtre "en retard" fréquent)
Statut facture (PAID / OVERDUE / …)Stocké + recalcul job nocturne
MTBF/MTTR par modèle d'assetCalculé à la demande (vue matérialisée si volumétrie)

Migrations zero-downtime

Ne jamais supprimer une colonne ou la rendre NOT NULL en une seule migration sur une base en production.

Protocole en 3 temps :

  1. Ajouter nullable — déployer la migration, le code existant est inchangé.
  2. Backfill — remplir les lignes existantes (script ou job one-shot).
  3. Appliquer la contrainteNOT NULL ou suppression dans la release suivante.

Pour une suppression de colonne :

  1. Supprimer toutes les références dans le code (release N).
  2. 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.