Données·10 min de lecture·1,936 mots

Modèles de données (Prisma)

Data Models — Backend Prisma

72 modèles, 36 enums, 73 migrations. Source de vérité : app/prisma/schema.prisma.

Conventions

  • Multi-tenancy : tous les modèles métier portent tenantId String (FK vers Tenant).
  • Cascade : delete tenant → cascade vers tous ses enfants.
  • Soft delete : non utilisé V1 (sauf cas explicites). Préférence pour delete dur + audit log.
  • Timestamps : createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt partout.
  • IDs : cuid() par défaut (sauf pour Reservation.icalToken, Tenant.icalToken = uuid v4).

Enums (36)

EnumValeursUsage
RoleCLIENT / TENANT_ADMIN / SUPER_ADMINUser & ClientAccount discriminator implicite
MediaTypeIMAGE / VIDEOMedia.type
CanalTypeWHATSAPP / TELEPHONE / INSTAGRAM / TELEGRAM / SNAPCHAT / EMAIL / LIEN / PANIER / DEVISTenantCanal.type
NiveauAccesPUBLIC / CONNEXION / SUR_DEMANDE / INVITATIONTenant.niveauAcces
StatutAccesEN_ATTENTE / APPROUVE / REFUSE / INVITEAccesVitrine.statut
StatutCommandeEN_ATTENTE / CONFIRMEE / EN_PREPARATION / EXPEDIE / LIVRE / ANNULECommande.statut
StatutPaiementEN_ATTENTE / PAYE / REFUSE / REMBOURSECommande.statutPaiement
ModePaiementCommandeORANGE_MONEY / MOOV_MONEY / WAVE / AIRTEL_MONEY / CASH / VIREMENTCommande.modePaiement
StatutPanierACTIF / CONVERTI / ABANDONNEPanier.statut
StatutProduitDRAFT / PUBLISHED / ARCHIVEDProduit.statut
DisponibiliteIMMEDIATE / SUR_COMMANDE / A_PARTIR_DE / SUR_DEVISProduit.disponibilite (séparé de StatutProduit, DEC-114)
AlbumTypePHOTOS / PRESTATIONS / ...MediaAlbum.type
SectionBlocTypeTEXT / IMAGE / HERO / ...SectionBloc.type
PageTypeSTATIQUE / APPLICATIVEVitrinePage.type (Boutique/Services = APPLICATIVE non persistées)
SnapshotStatutDRAFT / PUBLISHEDVitrineSnapshot.statut
ModeContactWHATSAPP / SNAPCHAT / INSTAGRAM / TELEGRAM / SMS / EMAIL / DEVIS / RESERVATIONPrestation.modeContact (DEC-197)
ModeConfirmationRDVAUTO / MANUELConfigReservationPrestation.modeConfirmation (DEC-198)
TypeAcompteAUCUN / POURCENTAGE / MONTANT_FIXEConfigReservationPrestation.typeAcompte
StatutReservationEN_ATTENTE / CONFIRME / ANNULEReservation.statut
TypeReservationPRESTATION / TABLE / EVENEMENTReservation.type
StatutPaiementRDVEN_ATTENTE / PAYE / REMBOURSEReservation.statutPaiement
ModePaiementRDVCASH / ORANGE_MONEY / MOOV_MONEY / VIREMENT / PAYDUNYAReservation.modePaiement
JourSemaineLUN / MAR / MER / JEU / VEN / SAM / DIMCreneauDisponible.jour
ModeCommandeRepasSUR_PLACE / A_RECUPERER / LIVRAISONCommandeRepas.mode
StatutCommandeRepasRECUE / EN_PREPARATION / PRETE / EN_LIVRAISON / LIVREE / ANNULEECommandeRepas.statut
NotifEventTypeCOMMANDE_CREEE / STATUT_CHANGE / PAIEMENT_VALIDE / RDV_CONFIRME / ACCES_APPROUVE / STORY_PUBLIEE / BROADCAST_GERANT / ARRIVAGE / ...NotifEvent.type
NotifCanalPUSH / EMAIL / SMS / WHATSAPP / IN_APPNotifDelivery.canal
NotifStatutPENDING / SENT / DELIVERED / FAILED / SKIPPEDNotifDelivery.statut
AudienceSegmentALL / ABONNES / FAVORIS / ...BroadcastCampagne.audience
TypeAbonnementTENANT / VITRINE / PRODUIT / CLIENTAbonnement.cibleType (polymorphique)
AuteurMessageCLIENT / TENANT / SYSTEMMessage.auteur
ReviewTargetTENANT / PRODUIT / PRESTATIONReview.cibleType
ReviewStatutEN_ATTENTE / PUBLIE / MODEREReview.statut
ReferralStatutINVITE / CONVERTI / EXPIREReferral.statut
CashbackTypeGAGNE / UTILISECashbackTransaction.type
StoryMediaTypeIMAGE / VIDEOVitrineStory.mediaType

Modèles — vue d'ensemble par famille

Tenant + Accès (10)

Tenant {
  id, subdomain @unique, nom, description, ville, pays, marketId
  niveauAcces NiveauAcces @default(PUBLIC)
  themeCouleur, logoUrl, banniereUrl
  whatsapp (legacy, fallback TenantCanal[])
  vitrineActive, actif
  icalToken @unique
  metaDescription, metaImage
  modulesActifs (relation TenantModule[])
  canaux TenantCanal[]
  categoriesWari (M2M via TenantCategorieWari)
  configAcces ConfigAccesVitrine?
  configPaiement ConfigPaiementGerant?
  configRestaurant ConfigRestaurant?
}

Market { id, code @unique, nom, devise, indicatifTel, fuseauHoraire }

TenantModule { id, tenantId, nom (catalogue/services/rdv/restaurant/evenements/stories), actif }

TenantCanal { id, tenantId, type CanalType, valeur, actif, ordre, @@unique([tenantId, type]) }

TenantCategorieWari { tenantId, categorieWariId, @@unique([tenantId, categorieWariId]) }

ConfigAccesVitrine {
  tenantId @unique, FK CASCADE
  nomsVisibles, photosVisibles, prixVisibles, descriptionsVisibles, stockVisible, coordonneesVisibles
  panierSansCompte, commandeSansCompte, reservationSansCompte, contactSansCompte
  (10 booléens DEFAULT_CONFIG_ACCES)
}

ConfigPaiementGerant {
  tenantId @unique, FK CASCADE
  6 toggles modes + 10 champs numéro/nom + 6 champs instructions
}

AccesVitrine {
  id, tenantId, clientAccountId, statut StatutAcces
  produitId?, prestationId?, motif?
  @@unique([tenantId, clientId])
  + FK CASCADE clientAccount + onDelete CASCADE
}

Identité (4)

User {
  id, email @unique, passwordHash (bcrypt), nom, prenom, role Role
  tenantId? (null pour SUPER_ADMIN)
  verifiedAt, lastLoginAt
}

ClientAccount {
  id, email?, phone?, nom, prenom, dateNaissance?, sexe?
  adresse?, ville?, pays?, devise?, langue?
  originTenantId? (premier tenant qui a fait la création)
  CHECK XOR : 1 des 2 (email OU phone) requis
  + relations : commandes, reservations, accesVitrines, conversations, abonnements, pushTokens, wishlists
}

MagicToken { id, identifier, token @unique, expiresAt, usedAt? }

PushToken {
  id, token @unique, platform (ios/android)
  userId? | clientAccountId?  -- CHECK CONSTRAINT XOR (BUG-124 fix)
  createdAt, lastSeenAt
}

Catalogue produits (10)

Categorie { id, nom, slug, tenantId, parentId? (hiérarchie 3 niveaux), ordre }
Produit {
  id, tenantId, nom, slug, description, prix, prixPromo?, stock
  statut StatutProduit, disponibilite Disponibilite (DEC-114)
  dateDisponibilite?, delaiCommandeJours?
  marque?, sku?, modeleId?
  specs (relation ProduitSpec[])
  variantes (relation VarianteProduit[])
  medias (relation Media[] via mediaId / MediaAlbumMembership)
  categories (M2M via ProduitCategorie)
}
ProduitCategorie { produitId, categorieId, @@unique }

Media {
  id, url (MinIO), tenantId, type MediaType, ordre, alt
  produitId? | prestationId? | platId? | varianteProduitId? | variantePrestationId?
  (polymorphique souple via FK optionnelles)
}

ModeleProduit { id, tenantId, nom, attributs ModeleAttribut[] }
ModeleAttribut { id, modeleId, nom (axe), ordre, valeursSuggérees String[] }

VarianteProduit {
  id, produitId, attributs Json (Record<string,string>), prix?, stock, sku?
  // PAS de champ nom/valeur — DEC-159
}
VariantePrestation { id, prestationId, attributs Json, prix?, stock?, duree? }

ModeleSpec / ProduitSpec { spécifications techniques key/value par produit }

Services / Prestations (5)

CategorieService { id, nom, slug, tenantId }
Prestation {
  id, tenantId, nom, slug, description, modeContact ModeContact
  prixMin?, prixMax? (5 cas affichage : Sur devis / À partir de / Jusqu'à / Prix fixe / Range)
  duree? (string libre, DEC-176)
  videoUrl?
  medias, variantes, categories
}
PrestationCategorie { @@unique pivot }

RDV / Réservations (4) — WP-221

ConfigReservationPrestation {
  prestationId @unique, accepteReservation Bool
  modeConfirmation ModeConfirmationRDV
  typeAcompte TypeAcompte, valeurAcompte?
  modesPaiementAcceptes ModePaiementRDV[]
  dureeMinutes, delaiMinReservationHeures, delaiAnnulationHeures
}
CreneauDisponible { id, tenantId, prestationId?, jour JourSemaine, heureDebut, heureFin, actif }
PlageIndisponible { id, tenantId, prestationId?, dateDebut, dateFin, motif? }
Reservation {
  id, tenantId, clientAccountId, prestationId?, evenementId?
  type TypeReservation, statut StatutReservation
  dateDebut, dateFin, dureeMinutes
  modeContact ModeContact, modePaiement ModePaiementRDV?, statutPaiement StatutPaiementRDV
  montantTotal, montantAcompte
  noteClient?, noteGerant?, motifAnnulation?
  icalToken @unique @default(uuid())
  confirmedAt?, cancelledAt?
}

Restauration (7) — WP-228

ConfigRestaurant {
  tenantId @unique, accepteSurPlace/Recuperer/Livraison Bool
  fraisLivraison, montantMinLivraison, tempsPrepEstime
  horaires Json (7 jours), accepteReservationTable Bool
}
MenuSection { id, tenantId, nom, ordre, actif }
Plat {
  id, tenantId, sectionId?, nom, description, prix
  disponible, photoUrl?, ordre
  options OptionPlat[], medias Media[] (via platId)
}
OptionPlat { id, platId, nom (Taille/Sans...), obligatoire, choixUnique, ordre, valeurs Json }
TableRestaurant { id, tenantId, numero, capacite?, qrToken @unique, actif }
CommandeRepas {
  id, tenantId, clientAccountId?, mode ModeCommandeRepas, statut StatutCommandeRepas
  tableId?, numeroTable?, phoneClient?
  total, fraisLivraison, montantMin
  adresseLivraison?, paiementMode?, refPaiement?
  lignes LigneCommandeRepas[]
}
LigneCommandeRepas { id, commandeRepasId, platId?, snapshot Json (nom/prix/options), quantite }

Commerce (8)

Client {
  id, tenantId, email?, phone?, nom, prenom, ville?, dateInvitation?
  // entité PAR tenant — distincte de ClientAccount (compte global)
}
Commande {
  id, tenantId, clientId, clientAccountId? (FK directe DEC-186 / BUG-126)
  statut StatutCommande, statutPaiement StatutPaiement
  total, modePaiement ModePaiementCommande?, refPaiement?, instructionsPaiement? (snapshot)
  noteInterne?, addresseLivraison?
  lignes CommandeLigne[]
}
CommandeLigne {
  id, commandeId, produitId?, varianteId?
  snapshot Json (nom/variante/prix), quantite, prixUnitaire
}
Panier { id, tenantId, clientAccountId?, statut StatutPanier, expiresAt }
PanierLigne { id, panierId, produitId?, varianteId?, quantite, prixUnitaire, tenantSubdomain? }
Wishlist { id, clientAccountId, produitId, addedAt }

Vitrine builder (5)

VitrineSection { id, tenantId, nom, ordre, type, actif, blocs SectionBloc[] }
SectionBloc { id, sectionId, type SectionBlocType, contenu Json, ordre }
VitrinePage { id, tenantId, slug, titre, type PageType, ordre, statut, snapshots VitrineSnapshot[] }
VitrineSnapshot { id, pageId, statut SnapshotStatut, data Json, previewToken?, autoSavedAt }
VitrineTemplate { id, nom, categoriesWari[], data Json, public Bool }
MediaAlbum + MediaAlbumMembership { albums de médias unifiés (DEC unifié WP-072) }

CategorieWari (catalogue marketplace) (2)

CategorieWari {
  id, slug @unique, nom, icon, parentId? (12 racines + 67 sous-cat)
  ordre, niveau
}
TenantCategorieWari { tenantId, categorieWariId (M2M, max 3 mobile / max 6 web) }

Notification Engine (4)

Abonnement {
  id, clientAccountId?, userId?
  cibleType TypeAbonnement (TENANT|VITRINE|PRODUIT|CLIENT)
  cibleId String (id du tenant/vitrine/produit/client suivi)
  canauxPreferes NotifCanal[]
}

NotifEvent {
  id, type NotifEventType, payload Json
  tenantId?, produitId?, prestationId?, commandeId?, reservationId?, storyId?
  createdAt
}

NotifDelivery {
  id, eventId, abonnementId?, destinataireId (userId or clientAccountId), destinataireType
  canal NotifCanal, statut NotifStatut
  sentAt?, deliveredAt?, errorMessage?
}

BroadcastCampagne {
  id, tenantId, titre, message, audience AudienceSegment
  scheduledAt, sentAt, stats Json
}

Stories (2)

VitrineStory {
  id, tenantId, mediaType StoryMediaType, mediaUrl
  caption?, publishedAt, expiresAt (24h par défaut)
  vues, abonnesPushed
}
VitrineStoryView { id, storyId, clientAccountId, viewedAt, @@unique }

Messagerie / Reviews / Reseaux / Ops (10)

Conversation { id, tenantId, clientAccountId, dernierMessage }
Message { id, conversationId, auteur AuteurMessage, contenu, mediaUrl?, sentAt }
TenantQuickReply { id, tenantId, label, contenu, ordre }
Review { id, cibleType ReviewTarget, cibleId, clientAccountId, note, commentaire, statut ReviewStatut }
Referral { id, parrainAccountId, filleulAccountId, statut ReferralStatut, recompense }
CashbackTransaction { id, clientAccountId, type CashbackType, montant, motif, commandeId? }
Collection { id, tenantId, nom, slug } + CollectionItem { collectionId, produitId, ordre }
Arrivage { id, tenantId, titre, message, scheduledAt, sentAt }
Evenement { id, tenantId, nom, dateDebut, dateFin, lieu, capacite, prixMin?, prixMax? }
Tag { id, tenantId, label, couleur } + ProduitTag / PrestationTag pivots
SearchQuery { id, query, marketId?, tenantId?, resultsCount, executedAt }
AuditLog { id, userId?, action, target, payload Json, createdAt }
AdminPrefs { userId @unique, sidebarPinned, theme, ... }

Indexes critiques (post P0 hardening)

8 indexes ajoutés dans la passe sécu 2026-05-20 (perf + audit) :

CREATE INDEX idx_commande_tenant_status ON commande(tenantId, statut);
CREATE INDEX idx_reservation_tenant_date ON reservation(tenantId, dateDebut);
CREATE INDEX idx_acces_vitrine_tenant_statut ON acces_vitrine(tenantId, statut);
CREATE INDEX idx_push_token_user ON push_tokens(userId);
CREATE INDEX idx_push_token_client ON push_tokens(clientAccountId);
CREATE INDEX idx_audit_log_user_action ON audit_log(userId, action, createdAt);
CREATE INDEX idx_notif_delivery_destinataire ON notif_delivery(destinataireId, destinataireType, statut);
CREATE INDEX idx_search_query_market_executed ON search_query(marketId, executedAt);

Contraintes CHECK (post P0 hardening)

  • push_tokens_xor_owner — XOR userId/clientAccountId (BUG-124)
  • client_account_xor_identifier — XOR email/phone (au moins 1 des 2)

Migrations

73 migrations dans app/prisma/migrations/, du 20260406135758_init (avril) au dernier déploiement Mai 2026.

Les plus structurantes :

  • 20260419023304_add_prestations — Module services
  • 20260419173016_unify_media_albums_section_blocs — Builder unifié
  • 20260420230204_add_snapshot_preview_token_autosave — Snapshot autosave
  • 20260507044342_add_module_rdv_reservations — Module RDV (WP-221)
  • 20260510_module_restauration — Module restau (WP-228)
  • 20260508_add_config_acces_vitrine — Config accès (WP-224 ét.2)
  • 20260508_paiement_commande_ussd — ConfigPaiementGerant (WP-225)
  • 20260509_security_constraints — XOR + FK CASCADE PushToken/AccesVitrine (WP-227)

Migration policy

  • Toujours créer une migration Prisma (pas db push) pour la prod.
  • Après chaque migration : docker exec -it superapp_nextjs npx prisma generate (sinon types out-of-sync).
  • Rebuild obligatoire ensuite : docker compose build nextjs && docker compose up -d nextjs (Turbopack ne hot-reload pas les routes API dans ce setup Docker).

Seed

prisma/seed.ts — fixtures idempotentes 3 profiles (minimal / demo / full). Profile demo (default) : 3 tenants (kady PUBLIC, salon-ouaga PUBLIC services+rdv, vip-club SUR_DEMANDE, private-collection INVITATION) + 3 ClientAccounts + ConfigPaiement Cash+Orange + ConfigAcces defaults + 25 produits images Unsplash + 7 prestations + créneaux LUN-VEN 09-17 + 2 commandes + 2 réservations + 2 AccesVitrine.

DATABASE_URL=$DATABASE_URL WARI_SEED_PROFILE=demo npx prisma db seed

Tenant kady est préservé intact par les re-runs (seul ConfigPaiement + ConfigAcces ajoutées si manquantes). Idempotent.