Retour aux projets
WebPlanificationCas d'étude

EventFlow — SaaS de mise en relation événementielle

Marketplace qui met en relation clients et prestataires événementiels : recherche par domaine, cahiers des charges, distribution de tâches et paiements en séquestre.

Plateforme SaaS où les clients publient leurs événements et cherchent des prestataires (traiteurs, sonorisateurs, photographes, décors…) par domaine et disponibilité. Le client rédige un cahier des charges, distribue les tâches aux prestataires retenus, et paie via un compte séquestre : les fonds sont bloqués par la plateforme puis libérés à la validation des livrables.

Stack technique

Nuxt  · framework full-stackTypeScript  · langageNode.js  · runtimePostgreSQL  · base de données principaleDocker  · déploiement

Feuille de route

  1. 1

    Cadrage

    Fait1/1 tâches terminées

    Contexte, problème à résoudre, objectifs et périmètre du projet

    30 sept. 2026

  2. 2

    Cahier des charges

    Fait2/2 tâches terminées

    Exigences fonctionnelles et non fonctionnelles, user stories

    Livrables : user stories (13 US, parcours recherche/réservation), règles de gestion du compte séquestre (18 règles, cycle de vie escrow, transactions, litiges), référentiel des domaines et catégories (7 catégories MVP, règles de disponibilité/recherche). Questions ouvertes à trancher : PSP cible pour le séquestre, taux de commission, périmètre géographique MVP.

    30 sept. 2026 30 sept. 2026

  3. 3

    Maquettes

    En cours0/2 tâches terminées

    Wireframes et maquettes UI des écrans clés

    Wireframes papier validés. Artefact de wireframes low-fi descriptifs produit (recherche, fiche prestataire, dashboard événement avec jauge d'escrow) — sert de base à la passe Figma (charte zinc/gris). Tâches Figma ouvertes, échéances 14 et 21 novembre.

    30 sept. 2026

  4. 4

    Conception technique

    Fait3/3 tâches terminées

    Architecture système, design de la base de données (ERD) et spécification de l'API

    3 artefacts produits : ERD PostgreSQL (16+ entités, enums d'état, triggers de transition d'escrow, ledger en double partie, index de recherche), spec API REST OpenAPI 3.1 (recherche, offres, escrow, paiements/webhooks, tâches/libération, litiges), ADR-001 (Mobile Money + virement au MVP avec séquestre comptable et job de réconciliation quotidien, migration vers PSP cantonnement au lancement commercial). Reste à trancher avant dev : choix de l'agrégateur PSP, taux de commission.

    30 sept. 2026 30 sept. 2026

  5. 5

    Développement

    À faire0/5 tâches terminées

    Implémentation itérative des fonctionnalités

  6. 6

    Tests

    À faire0/2 tâches terminées

    Tests unitaires, tests d'intégration et recette

  7. 7

    Mise en ligne

    À faire0/1 tâches terminées

    Déploiement, CI/CD, monitoring

  8. 8

    Maintenance

    À faire

    Corrections, évolutions, veille technique

Artefacts de conception

Diagrammes d'architecture

Diagramme d architecture — EventFlow

Architecture

[ Nuxt 3 SPA/SSR ]──HTTP──▶[ API Nuxt server (Nitro) ]──▶[ PostgreSQL ]
        │                            │                        ▲
        │                            ├──▶[ Service séquestre ]──┘ (transactions 2 phases)
        │                            ├──▶[ Notifications (email/push) ]
        └── auth Better Auth ────────┘
  • Front + API dans un seul runtime Nuxt/Nitro : moins de services à opérer pour un MVP.
  • Service séquestre = module applicatif (pas un process séparé) : chaque mouvement de fonds est une ligne escrow_transactions avec machine à états funded → in_escrow → released|refunded|disputed.
  • Règle d or : aucun fonds ne bouge hors d une transaction SQL unique qui écrit le mouvement + l audit.
  • Notifications découplées via file interne (retry), jamais dans la transaction de paiement.

User stories

User stories — EventFlow

User stories

Client

  • En tant que client, je recherche un prestataire par domaine, ville et date, afin de trouver rapidement qui est disponible.
  • En tant que client, je rédige un cahier des charges structuré (budget, livrables, date) pour que les prestataires sachent exactement quoi faire.
  • En tant que client, je dépose les fonds sur un compte séquestre pour ne payer le prestataire qu à la validation des livrables.
  • En tant que client, je distribue les tâches entre plusieurs prestataires et suis leur avancement.

Prestataire

  • En tant que prestataire, je publie mon profil par domaine (traiteur, sono, photo, déco…) avec tarif et zone d intervention.
  • En tant que prestataire, je consulte les cahiers des charges ouverts et soumet une offre.
  • En tant que prestataire, je vois les fonds séquestrés avant de commencer, pour travailler en confiance.

Plateforme

  • En tant qu admin, je médie les litiges et décide du remboursement ou de la libération des fonds.

Maquettes

Maquettes — spécification des écrans (SCR-01…SCR-06)

Maquettes — spécification des écrans

Conformément au gabarit : chaque écran est une spécification d implémentation (besoins primaires/secondaires, widgets, positions, animations). Tokens = voir l artefact design_system.

SCR-01 — Recherche de prestataires

  • Objectif primaire : trouver un prestataire disponible pour une date et un domaine donnés.
  • Entrée / sortie : arrive de la home → va vers SCR-02 (fiche prestataire).

Besoins primaires

  • Filtres domaine/ville/date → barre de filtres sticky top + drawer mobile
  • Liste des résultats → grille 3 colonnes (desktop) sous la barre de filtres

Besoins secondaires

  • Tri (note, tarif) → select en fin de barre de filtres
  • Sauvegarder un prestataire → icône bookmark sur la carte (connecté uniquement)

Widgets

WidgetTypePositionContenu / donnéesÉtats
Barre de filtresform (selects + date + bouton)top bar stickyevent_categories, villesskeleton pendant chargement
Carte prestatairecard clickablegrille contenuphoto, nom, badge domaine, note, tarif, dispovide → empty state « Aucun prestataire » + bouton élargir les filtres
Toasttoastbas-droiteconfirmation « ajouté aux favoris »—

Interactions et animations

InteractionDéclencheurAnimationRetour visuel
Appliquer filtresclic / changement de selectfade-in des résultats 200 msURL mise à jour (partageable)
Ouvrir filtres mobileclic bouton filtredrawer slide-in droite 200 msoverlay dimmed
Hover cartehovertranslateY -2px + shadow modal (150 ms)curseur pointer

Règles d affichage

  • Badge « Vérifié » si verified=true ; note masquée si < 3 avis ; page = 20 résultats, pagination infinie par bouton « Charger plus ».

Responsive

  • mobile : 1 colonne, filtres en drawer ; tablette : 2 colonnes.

Design system

  • chips de filtres (radius-full), badges sémantiques, --color-primary pour le CTA.

SCR-02 — Fiche prestataire

  • Objectif primaire : décider d inviter ce prestataire sur un événement.
  • Entrée / sortie : depuis SCR-01 → CTA « Inviter sur mon événement » → sélecteur d événement (modal).

Besoins primaires

  • Portfolio → galerie 4 colonnes (lightbox)
  • Tarifs et disponibilités → encart sticky à droite (desktop)
  • CTA d invitation → bouton primary en tête d encart

Besoins secondaires

  • Avis clients → liste paginée en bas, note globale en en-tête
  • Partage → icône ghost dans le header

Widgets

WidgetTypePositionContenuÉtats
En-tête profilheader cardhautphoto 96px, nom, badge domaine, note, badge « Vérifié »—
Encart réservationcard stickycolonne droitetarif horaire, prochaine dispo, CTAerreur → désactivé + message si compte client absent
Galeriegrid + lightboxcontenu gauchedocuments kind=portfolio vérifiésvide → « Portfolio en cours de constitution »
Liste avislistbasreviews (note, commentaire, date)vide → « Pas encore d avis »

Interactions et animations

InteractionDéclencheurAnimationRetour
Ouvrir lightboxclic imagefade 150 ms + zoom 1.02Échap flèches = navigation
Inviterclic CTAmodal scale .97→1 (150 ms)toast succès + badge « invité »

Règles d affichage

  • CTA masqué si le visiteur est le prestataire lui-même ; badge KYC cliquable = détail de vérification (modal).

Responsive

  • mobile : encart réservation devient bloc sticky bas (CTA toujours visible).

SCR-03 — Cahier des charges pas-à-pas

  • Objectif primaire : publier un brief complet et incitatif en < 5 minutes.
  • Entrée / sortie : depuis le tableau de bord client → publication → SCR-04.

Besoins primaires

  • Étapes (contexte → livrables → budget → date) → stepper horizontal top
  • Sauvegarde brouillon → auto-save 3 s après frappe + bouton manuel

Besoins secondaires

  • Aides contextuelles → icône info par champ (tooltip)
  • Modèles de livrables par catégorie → suggestions de chips cliquables

Widgets

WidgetTypePositionContenuÉtats
Steppertabs horizontalestop4 étapes, étape courante surlignéeerreurs de validation par étape
Formulaire dynamiqueformcontenuchamps de l étape couranteerreurs inline après blur
Budgetinput number + sliderétape 3min/max, devise XOF figéeavertissement si hors fourchette marché
Barre d autosavebadge discrettop bar« Enregistré il y a X s »en cours → spinner 16px

Interactions et animations

InteractionDéclencheurAnimationRetour
Changement d étapeclic étape validée / bouton Suivantslide horizontal 200 msvalidation bloquante avec scroll vers l erreur
Auto-save3 s d inactivitépulse discret du badgerien si succès ; toast erreur si échec réseau

Règles d affichage

  • RG-05 : impossible de publier si budget_max < budget_min ; le bouton « Publier » reste disabled avec raison explicite.

Responsive

  • mobile : stepper vertical compact (points), une question par écran.

SCR-04 — Tableau de bord événement

  • Objectif primaire : suivre prestataires, tâches et fonds d un événement en un coup d œil.
  • Entrée / sortie : hub client → détail booking (SCR-05) via une carte.

Besoins primaires

  • Jauge de fonds (séquestré / libéré / remboursé) → donut en tête
  • Liste des prestataires engagés → cartes horizontales avec avancement des tâches

Besoins secondaires

  • Timeline de l événement → rail vertical à droite
  • Actions rapides (inviter, publier un brief) → boutons outline dans l en-tête

Widgets

WidgetTypePositionContenuÉtats
Donut séquestrecharten-tête gauchefunded / in_escrow / released / refundedchargement → skeleton donut
Carte bookingcard clickableliste centraleprestataire, montant, barre de tâches x/y, badge statutlitige → bordure error + badge
Rail timelinelist verticalecolonne droitejalons de l événement—

Interactions et animations

InteractionDéclencheurAnimationRetour
Progression tâcheschangement de statut d une tâchebarre animée width 300 ms ease-outtoast à 100 % : « Prêt à valider la prestation »
Ouvrir bookingclic cartenavigation normale—

Règles d affichage

  • RG-14 : booking disputé → CTA de libération remplacé par « Litige en cours » (non cliquable + lien vers le litige).

Responsive

  • mobile : donut pleine largeur, timeline repliée en bas.

SCR-05 — Paiement séquestre (détail booking)

  • Objectif primaire : approvisionner puis libérer les fonds en comprenant chaque état.
  • Entrée / sortie : depuis SCR-04 → retour SCR-04.

Besoins primaires

  • Machine à états visible → stepper des 4 états (funded → in_escrow → released | refunded) avec état courant
  • Approvisionnement → CTA primary + récapitulatif du montant et de la commission 7 % (RG-20)
  • Libération → CTA danger-outline + confirmation modale explicite

Besoins secondaires

  • Historique des mouvements → table (type, montant, date, référence) depuis escrow_ledger
  • Aide contextuelle « Pourquoi les fonds sont bloqués » → alert info repliable

Widgets

WidgetTypePositionContenuÉtats
Stepper séquestretabstopétats + datesdisputed → états gelés grisés
Récap montantcardsous le stepperamount, commission, net prestataire—
Table ledgertablebasmouvements immuablesvide → « Aucun mouvement »
Confirmation libérationmodaloverlayavertissement irréversible + montant—

Interactions et animations

InteractionDéclencheurAnimationRetour
Approvisionnerclic CTAbouton loading (spinner)succès → toast + stepper avance (slide 200 ms)
Libérerclic + confirmation modal—toast succès ; funds marqués released ; écran lecture seule

Règles d affichage

  • RG-17 : CTA « Libérer » absent tant que booking ≠ completed ou fenêtre de 48 h non expirée — à la place, alert info avec la date d ouverture.
  • RG-18 : pas de bouton « Rembourser » côté client — uniquement via litige.

Responsive

  • mobile : stepper en liste verticale, table ledger scrollable horizontalement.

SCR-06 — Litige

  • Objectif primaire : permettre aux deux parties de présenter les faits et de connaître la décision.
  • Entrée / sortie : depuis SCR-04/05 → résolution → retour SCR-04.

Besoins primaires

  • Fil de discussion → list chronologique (client, prestataire, admin différenciés)
  • Motif d ouverture → alert en tête (immuable)

Besoins secondaires

  • Pièces justificatives → galerie de documents liés
  • Décision admin (rôle admin) → bloc dédié release/refund/split avec montants

Widgets

WidgetTypePositionContenuÉtats
Bandeau statutalerttopopen / resolved + issueresolved → verticale figée
Fil de messageslistcentremessages + piècesvide → impossible (RG-26 exige un motif)
Formulaire décisionform (admin)bas (admin)release / refund / split + montantsvalidation croisée : split ⇒ 2 montants

Interactions et animations

InteractionDéclencheurAnimationRetour
Nouveau messageenvoiapparition slide-up 200 msscroll auto vers le bas
Résoudreclic admin + confirmation—bandeau passe à la couleur de l issue + toast

Règles d affichage

  • RG-14 : pendant open, toutes les actions financières du booking sont gelées (répercuté sur SCR-04/05).

Responsive

  • mobile : identique, formulaire de décision en drawer.

Cahier des charges

Cahier des charges — EventFlow

Cahier des charges

Acteurs

  • Client : organise un événement, publie son besoin
  • Prestataire : propose ses services par domaine (traiteur, sono, photo, déco…)
  • Plateforme : anime la marketplace, gère le séquestre

Fonctionnalités clés

  1. Recherche de prestataires par domaine, localité, disponibilité et note
  2. Publication d'un cahier des charges structuré (budget, date, livrables)
  3. Répartition des tâches entre prestataires avec échéances
  4. Paiement en séquestre : les fonds sont bloqués à la commande et libérés à la validation des livrables
  5. Litiges : médiation par la plateforme

Contraintes

  • Conformité réglementaire des fonds séquestrés
  • Notifications temps réel (devis, validation, litige)

Systèmes de design

Design system — EventFlow

Design system — EventFlow

Source de vérité visuelle du produit. Toute valeur brute (couleur, taille) hors token est un défaut du livrable — un intégrateur humain ou un agent IA génère du HTML/CSS conforme en lisant uniquement ce document.

1. Tokens

Couleurs

TokenValeurUsageContraste
--color-primary#d97706 (amber-600)actions principales, liens actifs, jauge de séquestre4.6:1 sur surface — AA
--color-primary-hover#b45309hover des actions principales—
--color-surface#fafaf9 (stone-50)fond de page—
--color-surface-raised#ffffffcartes, modales, tableaux—
--color-success#059669fonds libérés, validation, badge « vérifié »AA
--color-warning#d97706en attente d approbation, litige ouvert légerAA
--color-error#dc2626erreurs, litige, remboursementAA
--color-info#0284c7information, états neutres actifsAA
--color-text#1c1917 (stone-900)texte principal14:1
--color-text-muted#57534e (stone-600)texte secondaire, labels7:1
--color-border#e7e5e4 (stone-200)séparateurs, bordures d input—

Typographie

TokenValeurUsage
--font-headingPlus Jakarta Sans, 700titres (h1 2rem / h2 1.5rem / h3 1.125rem)
--font-bodyInter, 400, interligne 1.6corps
--text-xs 12px · --text-sm 14px · --text-base 16px · --text-lg 18px · --text-xl 20pxéchelle ×1.125 arrondiehiérarchie complète
--font-monoJetBrains Monomontants, références de transactions

Espacements et rayons

  • Échelle : --space-1 4px → --space-12 96px (×1.5 par palier). Règle : padding de carte = --space-4, gap de grille = --space-6, section = --space-10.
  • Rayons : --radius-sm 6px (inputs, badges) · --radius-md 10px (boutons, toasts) · --radius-xl 16px (cartes, modales) · --radius-full (avatars, pills).

Icônes

  • Bibliothèque : Lucide · taille : 20px (16px inline) · épaisseur : 1.5px.
  • Règle : icône toujours accompagnée d un libellé, sauf actions universelles (recherche, fermer).

Ombres et z-index

  • --shadow-card : 0 1px 3px rgba(28,25,23,.08) · --shadow-modal : 0 10px 40px rgba(28,25,23,.16).
  • z-index : contenu 0, header sticky 20, drawer 30, toast 40, modal 50, lightbox 60.

2. Composants

ComposantVariantsStatesTokens consommésComportement
Buttonprimary / outline / ghost / dangerdefault, hover, active, focus (ring 2px primary), disabled, loading (spinner 16px + libellé conservé)primary, radius-md, space-2/4navigation = <a> stylé ; action = <button> ; danger uniquement pour suppression/litige
Toastinfo / success / warning / errorapparition (slide-up 200 ms ease-out), auto-dismiss 5 s (10 s si erreur), action « Annuler » 5 ssuccess/warning/error/info, surface-raised, radius-mdposition bas-droite, empilement vertical max 3
Alertidem toastsinline statique (non volant)ideminline dans les formulaires et pages ; toast pour les actions ponctuelles
Input / Select / Textareadefault, with-icon, error, disabledfocus (ring primary), validation live après blurborder, radius-sm, space-2/4message d erreur lié par aria-describedby ; label TOUJOURS au-dessus
Modalcentered / fullscreen-mobileouverture (overlay fade 150 ms + scale .97→1), focus trap, Échap = fermershadow-modal, radius-xlfermeture = confirmation si formulaire sale
Drawerright (panneaux filtres)idem modalidemsur mobile = plein écran
Cardsimple / clickable / selectedhover (shadow-card → modal + translateY -2px, 150 ms)surface-raised, radius-xlclickable = whole-card link
Badgeneutral / success / warning / error / infostatiquecouleurs sémantiquesstatuts : booking, séquestre, KYC
Tabsunderlineactif (trait 2px primary, animation slide 200 ms)primary, text-mutedlazy-content
Tablesortable, selectableheader sticky, zebra off, hover rowborder, surfacetri = aria-sort
Skeletonshimmerchargement (pulse 1.5 s)borderplaceholder au ratio exact du contenu

3. Règles transverses

  • Mode sombre : même nommage, valeurs inversées (surface #1c1917, text #fafaf9) via data-theme="dark" — les composants ne connaissent que les tokens.
  • Accessibilité : contrastes AA vérifiés (table ci-dessus), focus visible partout (ring), cibles tactiles ≥ 44×44 px, labels explicites — jamais de placeholder-only.
  • Animations : 150–200 ms, ease-out uniquement, respect de prefers-reduced-motion (tout devient instantané).
  • CSS : utilitaires Tailwind + variables CSS pour les tokens ; interdits : styles inline pour les règles d affichage, valeurs de couleur hors token.

Spécifications API

Spec API — conventions et routes de référence

Spécification d API — conventions et routes de référence

Conformément au gabarit : chaque route du catalogue (37 routes) suit les blocs ci-dessous. Les exemples parsables (JSON réels) sont obligatoires — un agent IA doit pouvoir générer client, handler et tests sans poser de question.

Conventions transverses (applicables à TOUTES les routes)

Forme d erreur normalisée

{ "statusCode": 409, "message": "Transition interdite : booking in_progress", "details": [] }
CodeQuand
400requête mal formée
401non authentifié
402fonds insuffisants / paiement requis
403authentifié mais non autorisé (mauvais rôle ou mauvaise appartenance)
404ressource inexistante ou hors périmètre de l appelant
409conflit d état (machine à états, unicité)
422validation de payload

Pagination

?page=1&limit=20 → { "data": [...], "total": 137, "page": 1, "limit": 20 } — limit plafonné à 100.

Dates et montants

ISO 8601 UTC pour les dates · montants en XOF entier (pas de décimales).

Route de référence 1 — POST /api/v1/briefs//offers

  • Auth : Bearer · Rôles : prestataire vérifié (RG-02) · RG liées : RG-04, RG-06, RG-07

Query params

(none)

Body

champtyperequisvalidation
amountintegeroui> 0, ≤ 1 000 000 000
delivery_daysintegernon1–365
messagestringnon≤ 2000 caractères

Réponse 201

{ "id": "uuid", "briefId": "uuid", "amount": 450000, "deliveryDays": 14, "message": "Traiteur 300 couverts", "status": "pending", "createdAt": "2026-09-30T10:00:00.000Z" }

Erreurs

codedéclencheur
403prestataire non vérifié (RG-02) ou brief non publié (RG-04)
409offre pending déjà existante sur ce brief (RG-06)
422amount ≤ 0 (RG-07) ou payload invalide

Route de référence 2 — POST /api/v1/bookings//escrow

  • Auth : Bearer · Rôles : client propriétaire · RG liées : RG-11, RG-15, RG-19, RG-21, RG-30

Query params

nomtyperequisdéfautcontraintes
idempotencyKeystringnon—≤ 64 car., clé de réplay (RG-21)

Body

champtyperequisvalidation
providerRefstringouiréférence de la transaction externe (mobile money / virement)
amountintegeroui= booking.agreedAmount exactement (RG-15)

Réponse 201

{ "id": "uuid", "bookingId": "uuid", "amount": 450000, "currency": "XOF", "status": "in_escrow", "fundedAt": "2026-09-30T10:00:00.000Z" }

Erreurs

codedéclencheur
402montant reçu ≠ agreedAmount (RG-15)
403appelant ≠ client du booking
409séquestre déjà in_escrow ou booking disputé (RG-14)

Exemple requête

{ "providerRef": "OMM-20260930-88123", "amount": 450000 }

(Réjoué avec le même idempotencyKey → 201 avec la même ressource, zéro second mouvement dans escrow_ledger.)

Règles de gestion

Règles de gestion — RG-01 à RG-31

Règles de gestion — EventFlow

Règles métier numérotées applicables aux données (ERD) et à l'API. Chaque règle est vérifiable par un test.

Comptes et rôles

  • RG-01 — Un compte users porte exactement un rôle à la fois (client, prestataire, admin) ; un client peut devenir prestataire en créant un provider_profiles, jamais l'inverse.
  • RG-02 — Un prestataire ne peut soumettre une offre que si provider_profiles.verified = true (KYC validé par un admin).
  • RG-03 — La note (provider_profiles.rating) est la moyenne arithmétique des reviews.rating du prestataire ; elle est recalculée à chaque écriture d'avis, jamais saisie à la main.

Cahier des charges et offres

  • RG-04 — Un briefs suit draft → published → closed : seul un brief published accepte des offres et invitations.
  • RG-05 — À la publication d'un brief, budget_max doit être ≥ budget_min > 0.
  • RG-06 — Un prestataire ne peut avoir qu'une seule offre pending par brief (unicité (brief_id, provider_id) sur les offres actives).
  • RG-07 — Le montant d'une offre est strictement positif et exprimé en XOF, sans décimale.
  • RG-08 — Accepter une offre : crée le bookings (montant figé = offers.amount), passe l'offre à accepted, rejette toutes les autres offres du brief, et clôt le brief (closed). L'opération est atomique.
  • RG-09 — Une offre peut être retirée (withdrawn) tant qu'elle n'est pas acceptée ; un brief closed n'accepte plus ni offre ni retrait.

Cycle de réservation

  • RG-10 — Cycle d'un booking : pending → confirmed → in_progress → completed | disputed. Toute transition hors de ce graphe est rejetée (409).
  • RG-11 — Un booking ne peut passer confirmed que si son séquestre est in_escrow (fonds reçus). Pas de fonds ⇒ pas d'engagement ferme.
  • RG-12 — Seul le prestataire peut start et complete ; seul le client peut confirmer après approvisionnement.
  • RG-13 — complete est refusé tant qu'il existe des booking_tasks ≠ fait (409), sauf levée explicite par le client.
  • RG-14 — Un booking disputed gèle toute transition financière jusqu'à la résolution du litige.

Séquestre (le cœur réglementaire)

  • RG-15 — Le séquestre est unique par booking (escrow_transactions.booking_id unique) ; le montant séquestré est exactement le montant du booking.
  • RG-16 — Machine à états du séquestre : funded → in_escrow → released | refunded, avec gel disputed. Toute autre transition est interdite.
  • RG-17 — release n'est possible que si le booking est completed et que la fenêtre de rétractation de 48 h est expirée (ou waiver explicite du client).
  • RG-18 — refund n'est possible que sur décision d'un litige (resolved_refund ou resolved_split), par un admin — jamais à la demande d'une seule partie.
  • RG-19 — Tout mouvement de fonds écrit une ligne immuable dans escrow_ledger (append-only : pas d'UPDATE/DELETE), avec balance_after contrôlable par re-calcul.
  • RG-20 — La commission plateforme (7 %) est prélevée à la libération : le release écrit un mouvement commission distinct du mouvement release au prestataire.
  • RG-21 — Les mouvements de fonds sont idempotents : un même release rejoué (retry réseau) ne crée pas un second mouvement (clé d'idempotence sur la requête).
  • RG-22 — Le solde séquestre global de la plateforme (somme des in_escrow) doit toujours être réconciliable avec le compte de recouvrement externe — contrôle quotidien.

Retraits (payouts)

  • RG-23 — Un prestataire ne peut retirer que des fonds released : sum(payouts en cours) ≤ solde libéré - commission.
  • RG-24 — Un retrait nécessite verified = true et un canal de destination confirmé (OTP).
  • RG-25 — Un payout suit requested → processing → paid | failed ; un payout failed remet le montant dans le solde disponible.

Litiges

  • RG-26 — Un litige ne peut être ouvert que sur un booking confirmed, in_progress ou completed, et un seul litige actif à la fois.
  • RG-27 — Trois issues possibles, tranchées par un admin : resolved_release (prestataire), resolved_refund (client), resolved_split (répartition négociée, montants fixés par l'admin).
  • RG-28 — La décision du litige déclenche le mouvement de fonds correspondant (RG-17/18) et notifie les deux parties ; le fil dispute_messages est clos à la résolution.

Avis, notifications, documents

  • RG-29 — Un avis n'est possible que sur un booking completed, par le client, un seul avis par booking.
  • RG-30 — Les notifications sont écrites après commit de la transaction métier (jamais dans la même transaction qu'un mouvement de fonds) : un échec de notification ne bloque jamais un paiement.
  • RG-31 — Les documents KYC ne sont jamais publics ; seuls les portfolios vérifiés sont exposés via l'API publique.

Schéma de données

users

Comptes : clients, prestataires et admins (Better Auth)

id PKuuid
email text

Identifiant de connexion Better Auth, unique

non null

full_name text
role enum(client, prestataire, admin)

Rôle fonctionnel : client, prestataire ou admin — vérifié côté serveur à chaque route

non null

phone text
created_at timestamp

provider_profiles

Profil prestataire : domaine, zone, tarif, note

id PKuuid
user_id FKuuid

→ users

domain enum(traiteur, sonorisation, photo, decoration, autre)

Domaine principal — doit exister dans event_categories

non null

city varchar
description text
hourly_rate numeric

Tarif indicatif horaire (XOF)

rating numeric

Moyenne calculée des reviews, mise à jour asynchrone à l'écriture d'un avis

verified boolean

KYC validé par un admin — requis pour recevoir des payouts

events

Événement organisé par un client

id PKuuid
client_id FKuuid

→ users

title varchar

non null

event_date date
budget numeric
city varchar
status enum(brouillon, planifie, termine)

briefs

Cahier des charges publié par le client

id PKuuid
event_id FKuuid

→ events

requirements text

non null

deliverables jsonb
budget_min numeric
budget_max numeric
status enum(draft, published, closed)

draft → published (visible aux prestataires) → closed (offre acceptée ou expiration)

bookings

Engagement d un prestataire sur un cahier des charges

id PKuuid
event_id FKuuid

→ events

brief_id FKuuid

→ briefs

provider_id FKuuid

→ provider_profiles

agreed_amount numeric

Montant accepté = montant de l'offre retenue, figé à la création

non null

status enum(pending, confirmed, in_progress, completed, disputed)

pending → confirmed (fonds séquestrés) → in_progress → completed | disputed — transitions validées par le service séquestre

booking_tasks

Tâches distribuées au prestataire dans un booking

id PKuuid
booking_id FKuuid

→ bookings

title varchar

non null

description text
due_date date
status enum(a_faire, en_cours, fait)

escrow_transactions

Fonds séquestrés d un booking (machine à états)

id PKuuid
booking_id FKuuid

→ bookings

amount numeric

Montant total séquestré = agreed_amount du booking

non null

currency char(3)

= XOF

status enum(funded, in_escrow, released, refunded, disputed)

funded → in_escrow (fonds reçus) → released (validation) | refunded (litige) | disputed (gel)

non null

funded_at timestamp
released_at timestamp
payout_ref text

Référence du versement externe (mobile money)

reviews

Avis laissé après un booking terminé

id PKuuid
booking_id FKuuid

→ bookings

author_id FKuuid

→ users

rating integer

non null

comment text

event_categories

Référentiel des domaines de prestataires (traiteur, sonorisation, photo, déco…). Alimente les filtres de recherche et les profils.

id PKuuid

Identifiant de la catégorie

slug varchar

Clé URL unique, ex: traiteur

non null

label varchar

Libellé affiché, ex: Traiteur & cuisine

non null

icon varchar

Nom d'icône du design system

position integer

= 0

Ordre d'affichage dans les filtres

provider_availabilities

Créneaux de disponibilité déclarés par un prestataire — sert au filtre « disponible le … » de la recherche.

id PKuuid

Identifiant du créneau

provider_id FKuuid

→ provider_profiles

Prestataire concerné

start_at timestamp

Début du créneau de disponibilité

non null

end_at timestamp

Fin du créneau

non null

kind enum(available, busy, tentative)

= available

État du créneau

brief_invitations

Invitation d'un prestataire sur un cahier des charges publié — le client peut cibler des profils plutôt que publier dans l'anonymat.

id PKuuid

Identifiant de l'invitation

brief_id FKuuid

→ briefs

Cahier des charges ciblé

provider_id FKuuid

→ provider_profiles

Prestataire invité

status enum(sent, viewed, declined)

= sent

Cycle de vie de l'invitation

sent_at timestamp

Horodatage de l'envoi

offers

Devis/remise d'offre d'un prestataire sur un cahier des charges. Une seule offre active par couple (prestataire, brief).

id PKuuid

Identifiant de l'offre

brief_id FKuuid

→ briefs

Cahier des charges visé

provider_id FKuuid

→ provider_profiles

Prestataire offreur

amount numeric

Montant total proposé (XOF)

non null

delivery_days integer

Délai de réalisation annoncé (jours)

message text

Argumentaire du prestataire

status enum(pending, accepted, rejected, withdrawn)

= pending

État de l'offre

created_at timestamp

Horodatage de la soumission

escrow_ledger

Journal en append-only de tous les mouvements de fonds du séquestre — un booking = plusieurs lignes immuables.

id PKuuid

Identifiant du mouvement

escrow_id FKuuid

→ escrow_transactions

Compte séquestre concerné

entry_type enum(fund, release, refund, commission, payout)

Nature du mouvement

non null

amount numeric

Montant du mouvement (toujours positif, signé par le type)

non null

balance_after numeric

Solde séquestre après le mouvement — re-calculable, stocké pour l'audit

non null

reference text

Référence externe (transaction mobile money…)

created_at timestamp

Horodatage immuable

payouts

Demandes de retrait des prestataires après libération des fonds.

id PKuuid

Identifiant du retrait

provider_id FKuuid

→ provider_profiles

Bénéficiaire

amount numeric

Montant demandé

non null

status enum(requested, processing, paid, failed)

= requested

Cycle de traitement

method enum(mobile_money, bank_transfer)

Canal de versement

destination varchar

Destination masquée (numéro/IBAN)

requested_at timestamp

Date de la demande

paid_at timestamp

Date de versement effectif

disputes

Litige ouvert par une partie sur un booking — gèle la libération des fonds jusqu'à la décision admin.

id PKuuid

Identifiant du litige

booking_id FKuuid

→ bookings

Réservation disputée

opened_by FKuuid

→ users

Auteur de l'ouverture

reason text

Motif détaillé

non null

status enum(open, resolved_release, resolved_refund, resolved_split)

= open

Issue du litige

resolved_by FKuuid

→ users

Admin ayant tranché

resolved_at timestamp

Horodatage de la décision

dispute_messages

Fil de discussion d'un litige (client, prestataire, admin).

id PKuuid

Identifiant du message

dispute_id FKuuid

→ disputes

Litige concerné

author_id FKuuid

→ users

Auteur du message

content text

Contenu du message

non null

created_at timestamp

Horodatage

notifications

Notifications in-app découplées de la transaction de paiement (file interne).

id PKuuid

Identifiant de la notification

user_id FKuuid

→ users

Destinataire

type varchar

Clé de template : offer_received, escrow_funded, dispute_opened…

non null

payload jsonb

Données du template (ids, montants…)

read_at timestamp

Null = non lue

created_at timestamp

Horodatage

documents

Pièces jointes vérifiées : portfolio prestataire, preuves de livraison, factures.

id PKuuid

Identifiant du document

owner_id FKuuid

→ users

Propriétaire du document

booking_id FKuuid

→ bookings

Réservation liée (si justificatif de livraison)

kind enum(portfolio, kyc, delivery_proof, invoice)

Nature du document

file_url text

URL de stockage object

non null

verified boolean

= false

Validé par un admin

Documentation API

GET/api/v1/providers200, 400

Recherche de prestataires (domaine, ville, date, budget) triée par note — SELECT filtré + tri rating

GET/api/v1/providers/{id}200, 404

Fiche détaillée d un prestataire avec avis

POST/api/v1/eventsAuth requise201, 401, 422

Créer un événement (client authentifié)

POST/api/v1/events/{id}/briefAuth requise201, 403, 422

Publier le cahier des charges de l événement

POST/api/v1/bookingsAuth requise201, 409

Engager un prestataire sur un brief (statut pending) — Crée booking + brief fermé

POST/api/v1/bookings/{id}/tasksAuth requise201, 403

Ajouter une tâche au prestataire

POST/api/v1/bookings/{id}/escrowAuth requise201, 402, 409

Approvisionner le séquestre (funds) — escrow_transactions.status = in_escrow

POST/api/v1/bookings/{id}/releaseAuth requise200, 403, 409

Libérer les fonds après validation des livrables — status = released

POST/api/v1/bookings/{id}/disputeAuth requise201, 409

Ouvrir un litige (bloque la libération) — status = disputed

POST/api/v1/bookings/{id}/reviewsAuth requise201, 403

Déposer un avis après un booking terminé

GET/api/v1/categories200

Liste des domaines de prestataires (référentiel des filtres)

GET/api/v1/providers/meAuth requise200, 404

Profil prestataire de l'utilisateur connecté

PATCH/api/v1/providers/meAuth requise200, 403, 422

Mettre à jour son profil prestataire (domaine, tarif, description)

GET/api/v1/providers/me/availabilityAuth requise200

Liste de ses créneaux de disponibilité

POST/api/v1/providers/me/availabilityAuth requise201, 422

Déclarer un créneau de disponibilité

Exemple de requête

{"start_at":"2027-01-10T08:00:00Z","end_at":"2027-01-10T20:00:00Z","kind":"available"}

Exemple de réponse

{"id":"uuid","kind":"available"}
POST/api/v1/providers/{id}/verifyAuth requise200, 403

Valider le KYC d'un prestataire (admin)

POST/api/v1/events/{id}/brief/invitationsAuth requise201, 403, 422

Inviter des prestataires ciblés sur un cahier des charges

Exemple de requête

{"provider_ids":["uuid1","uuid2"]}

Exemple de réponse

{"created":2}
GET/api/v1/briefs/{id}/offersAuth requise200, 403

Offres reçues sur un cahier des charges (client)

POST/api/v1/briefs/{id}/offersAuth requise201, 403, 409, 422

Soumettre une offre sur un brief (prestataire vérifié)

Exemple de requête

{"amount":450000,"delivery_days":14,"message":"Traiteur 300 couverts, menu joint"}

Exemple de réponse

{"id":"uuid","status":"pending"}
POST/api/v1/offers/{id}/withdrawAuth requise200, 403, 409

Retirer son offre tant qu'elle n'est pas acceptée

POST/api/v1/offers/{id}/acceptAuth requise201, 403, 409

Accepter une offre : crée le booking et rejette les autres offres

Exemple de réponse

{"booking_id":"uuid","rejected_offers":3}
PATCH/api/v1/bookings/{id}/confirmAuth requise200, 403, 409

Confirmer le booking après approvisionnement du séquestre

PATCH/api/v1/bookings/{id}/startAuth requise200, 403, 409

Démarrer la prestation (prestataire)

PATCH/api/v1/bookings/{id}/completeAuth requise200, 403, 409

Déclarer la prestation terminée (toutes tâches faites)

PATCH/api/v1/booking-tasks/{id}Auth requise200, 403

Mettre à jour une tâche (statut, échéance)

POST/api/v1/bookings/{id}/tasks/reorderAuth requise200, 422

Réordonner les tâches du booking

GET/api/v1/bookings/{id}/escrow/historyAuth requise200, 403

Journal complet des mouvements de fonds du séquestre

Exemple de réponse

[{"entry_type":"fund","amount":450000,"balance_after":450000}]
POST/api/v1/escrow/{id}/refundAuth requise200, 403, 409

Rembourser le client — réservé à la résolution d'un litige (admin)

GET/api/v1/payoutsAuth requise200

Historique des retraits du prestataire connecté

POST/api/v1/payoutsAuth requise201, 402, 403

Demander un retrait des fonds libérés (KYC requis)

Exemple de requête

{"amount":300000,"method":"mobile_money","destination":"+22507…"}

Exemple de réponse

{"id":"uuid","status":"requested"}
POST/api/v1/disputesAuth requise201, 409, 422

Ouvrir un litige sur un booking (gèle le séquestre)

Exemple de requête

{"booking_id":"uuid","reason":"Livrable incomplet : 80 sur 300 couverts"}

Exemple de réponse

{"id":"uuid","status":"open"}
POST/api/v1/disputes/{id}/messagesAuth requise201, 403

Ajouter un message au fil du litige

POST/api/v1/disputes/{id}/resolveAuth requise200, 403, 409

Trancher un litige : release / refund / split (admin)

Exemple de requête

{"outcome":"split","client_amount":150000,"provider_amount":300000}

Exemple de réponse

{"status":"resolved_split"}
GET/api/v1/me/notificationsAuth requise200

Notifications de l'utilisateur connecté

PATCH/api/v1/me/notifications/readAuth requise200

Marquer des notifications comme lues

Exemple de requête

{"ids":["uuid1"]}
GET/api/v1/me/bookingsAuth requise200

Mes réservations (client ou prestataire selon le rôle)

GET/api/v1/me/earningsAuth requise200

Gains du prestataire : libérés, séquestrés, en attente de retrait

Exemple de réponse

{"released":900000,"in_escrow":450000,"pending_payout":300000}

Décisions d'architecture

Séquestre interne en 2 phases (approvisionner puis libérer) plutôt que paiement direct

Décidé le 30 sept. 2026
Contexte

Les deux parties doivent être protégées : le prestataire veut la garantie des fonds, le client ne veut payer que des livrables validés. Les passerelles type Stripe Connect imposent des contraintes KYC et de pays peu adaptées au marché cible.

Options envisagées
  • Paiement direct au prestataire (aucune protection client)
  • Séquestre via passerelle (Stripe Connect / Paystack) : fiable mais KYC + couverture pays limitée
  • Séquestre interne : modèle de fonds sur notre Postgres, machine à états explicite
Décision

Séquestre interne en 2 phases : le client approvisionne (funds), les fonds restent in_escrow jusqu à la validation des livrables (release) ou d un litige (refund). Chaque transition est une transaction SQL unique avec piste d audit.

Conséquences

La plateforme porte une responsabilité de conservation des fonds (réglementaire) — intégration KYC prévue au roadmap ; retrait des fonds manuel au début ; un webhook payout automatisera la libération plus tard.

Recherche prestataires sur Postgres (trigram + filtres) plutôt qu un moteur dédié

Décidé le 30 sept. 2026
Contexte

La recherche est centrée sur des filtres structurés (domaine, ville, date, budget) avec un classement simple par note, pas sur de la recherche plein texte floue.

Options envisagées
  • Meilisearch / Elasticsearch : excellents en full-text, mais un service de plus à opérer et synchroniser
  • Postgres avec index GIN trigram + requêtes filtrées
Décision

Postgres seul pour le MVP : index GIN trigram sur nom/bio, filtres exacts sur domaine/ville, tri par note et disponibilité.

Conséquences

Si la recherche devient un besoin produit (recherche naturelle, suggestions), ajouter Meilisearch plus tard — le point d intégration est isolé dans le module de recherche.

Jalons

  1. MVP marketplace + séquestre

    À faire

    Recherche, cahier des charges, tâches et paiement séquestre opérationnels

    31 janv. 2027

  2. Beta fermée — 50 événements réels

    À faire

    Premiers clients et prestataires réels, séquestre utilisé en production restreinte

    28 févr. 2027

  3. Lancement commercial

    À faire

    Ouverture publique, KYC intégrés, payouts automatisés

    30 avr. 2027