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 versTenant). - 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 @updatedAtpartout. - IDs :
cuid()par défaut (sauf pourReservation.icalToken,Tenant.icalToken= uuid v4).
Enums (36)
| Enum | Valeurs | Usage |
|---|---|---|
Role | CLIENT / TENANT_ADMIN / SUPER_ADMIN | User & ClientAccount discriminator implicite |
MediaType | IMAGE / VIDEO | Media.type |
CanalType | WHATSAPP / TELEPHONE / INSTAGRAM / TELEGRAM / SNAPCHAT / EMAIL / LIEN / PANIER / DEVIS | TenantCanal.type |
NiveauAcces | PUBLIC / CONNEXION / SUR_DEMANDE / INVITATION | Tenant.niveauAcces |
StatutAcces | EN_ATTENTE / APPROUVE / REFUSE / INVITE | AccesVitrine.statut |
StatutCommande | EN_ATTENTE / CONFIRMEE / EN_PREPARATION / EXPEDIE / LIVRE / ANNULE | Commande.statut |
StatutPaiement | EN_ATTENTE / PAYE / REFUSE / REMBOURSE | Commande.statutPaiement |
ModePaiementCommande | ORANGE_MONEY / MOOV_MONEY / WAVE / AIRTEL_MONEY / CASH / VIREMENT | Commande.modePaiement |
StatutPanier | ACTIF / CONVERTI / ABANDONNE | Panier.statut |
StatutProduit | DRAFT / PUBLISHED / ARCHIVED | Produit.statut |
Disponibilite | IMMEDIATE / SUR_COMMANDE / A_PARTIR_DE / SUR_DEVIS | Produit.disponibilite (séparé de StatutProduit, DEC-114) |
AlbumType | PHOTOS / PRESTATIONS / ... | MediaAlbum.type |
SectionBlocType | TEXT / IMAGE / HERO / ... | SectionBloc.type |
PageType | STATIQUE / APPLICATIVE | VitrinePage.type (Boutique/Services = APPLICATIVE non persistées) |
SnapshotStatut | DRAFT / PUBLISHED | VitrineSnapshot.statut |
ModeContact | WHATSAPP / SNAPCHAT / INSTAGRAM / TELEGRAM / SMS / EMAIL / DEVIS / RESERVATION | Prestation.modeContact (DEC-197) |
ModeConfirmationRDV | AUTO / MANUEL | ConfigReservationPrestation.modeConfirmation (DEC-198) |
TypeAcompte | AUCUN / POURCENTAGE / MONTANT_FIXE | ConfigReservationPrestation.typeAcompte |
StatutReservation | EN_ATTENTE / CONFIRME / ANNULE | Reservation.statut |
TypeReservation | PRESTATION / TABLE / EVENEMENT | Reservation.type |
StatutPaiementRDV | EN_ATTENTE / PAYE / REMBOURSE | Reservation.statutPaiement |
ModePaiementRDV | CASH / ORANGE_MONEY / MOOV_MONEY / VIREMENT / PAYDUNYA | Reservation.modePaiement |
JourSemaine | LUN / MAR / MER / JEU / VEN / SAM / DIM | CreneauDisponible.jour |
ModeCommandeRepas | SUR_PLACE / A_RECUPERER / LIVRAISON | CommandeRepas.mode |
StatutCommandeRepas | RECUE / EN_PREPARATION / PRETE / EN_LIVRAISON / LIVREE / ANNULEE | CommandeRepas.statut |
NotifEventType | COMMANDE_CREEE / STATUT_CHANGE / PAIEMENT_VALIDE / RDV_CONFIRME / ACCES_APPROUVE / STORY_PUBLIEE / BROADCAST_GERANT / ARRIVAGE / ... | NotifEvent.type |
NotifCanal | PUSH / EMAIL / SMS / WHATSAPP / IN_APP | NotifDelivery.canal |
NotifStatut | PENDING / SENT / DELIVERED / FAILED / SKIPPED | NotifDelivery.statut |
AudienceSegment | ALL / ABONNES / FAVORIS / ... | BroadcastCampagne.audience |
TypeAbonnement | TENANT / VITRINE / PRODUIT / CLIENT | Abonnement.cibleType (polymorphique) |
AuteurMessage | CLIENT / TENANT / SYSTEM | Message.auteur |
ReviewTarget | TENANT / PRODUIT / PRESTATION | Review.cibleType |
ReviewStatut | EN_ATTENTE / PUBLIE / MODERE | Review.statut |
ReferralStatut | INVITE / CONVERTI / EXPIRE | Referral.statut |
CashbackType | GAGNE / UTILISE | CashbackTransaction.type |
StoryMediaType | IMAGE / VIDEO | VitrineStory.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 services20260419173016_unify_media_albums_section_blocs— Builder unifié20260420230204_add_snapshot_preview_token_autosave— Snapshot autosave20260507044342_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 seedTenant kady est préservé intact par les re-runs (seul ConfigPaiement + ConfigAcces ajoutées si manquantes). Idempotent.