Dossier d'exemple, non retouché
Réservation pour coachs sportifs
Ce dossier a été écrit par Kodrudder. Nous l'affichons tel quel, avec ses hypothèses et les écarts que la relecture a trouvés.
Relevé de cette génération : 174 s, coût estimé 0,56 $.
- 15 règles métier
- 32 critères d'acceptation testables
- 8 tables SQL
- 20 routes d'API
- 25 tâches de développement
- 28 sources web
- 8 écarts trouvés par la relecture, corrigés dans le plan
À lire d'abord
Produit. Application web de réservation pour coachs sportifs indépendants : le coach publie disponibilités et tarifs, le client réserve et paie en ligne sans compte, puis reçoit un SMS de rappel J-1. Objectif : moins de no-shows et moins de relances manuelles.
Ce qui est décidé
- Marché France, EUR, interface en français, web responsive Europe/Paris (D-01, D-09).
- Paiement intégral via Stripe Checkout, versement direct au coach via Connect Express (D-02, D-03).
- Pas de compte client : email et mobile (06/07), lien de gestion par email (D-08).
- Annulation remboursée jusqu'à 24 h avant la séance, aucune commission au MVP (D-07, D-10).
- Rappel SMS Twilio J-1, strictement transactionnel, sans offre promotionnelle (D-05, D-06).
Ce qui reste une hypothèse à vérifier avant de coder
- Frais Stripe Connect et modèle de charge : qui absorbe les frais de remboursement (D-03, RM-09).
- Remboursement automatique impossible si le solde du coach est négatif : à tester sur compte test.
- SMS de service sans consentement (D-06) : validation juridique RGPD/CNIL avant production.
Corrections de la relecture
| Écart | Ce qui prévaut sur le texte des sections |
|---|---|
| EC-01 | Webhook tardif sur EXPIREE : PAYE puis REMBOURSE dans une seule transaction (RM-04) |
| EC-02 | rappel_sms créé à la confirmation si début > 24 h ; passé à annule à toute annulation |
| EC-03 | D-05 : 1 rappel + 1 SMS d'annulation coach éventuel, contenu transactionnel |
| EC-04 | Jeton de gestion = 256 bits partout (RM-13 aligné) |
| EC-05 | Blocage de date refusé si réservation EN_ATTENTE_PAIEMENT ou CONFIRMEE ce jour |
| EC-06 | Mobile 06/07 uniquement (RM-11 aligné) ; validation API avant insertion, 400 et non 500 |
| EC-07 | Ajouter GET /api/coach/seances, /disponibilites, /dates-bloquees |
| EC-08 | E-03 lit le statut via API-15 par jeton |
Comment utiliser ce dossier
- Contexte : donner à l'outil cette page, puis Cadrage, modèle de données (§4) et règles métier (RM), en précisant que les corrections EC-xx prévalent.
- Plan : lui faire suivre §6 dans l'ordre des jalons, de J0 à J3 (tranche « réserver et payer » P-01), une tâche T-xx à la fois.
- Validation : pour chaque tâche, exiger les CA liés et la « preuve de fin » (tests Vitest/Playwright, services externes mockés) avant de passer à la suivante.
Cadrage
4 liens vers des sources14 hypothèses marquées
Résumé produit
- Application web de réservation de créneaux pour coachs sportifs indépendants. Chaque coach publie ses disponibilités et ses tarifs.
- Le client réserve et paie en ligne. Il reçoit un rappel SMS avant la séance.
- Objectif : moins de no-shows et moins d'échanges manuels (FAIT : brief ; la cible chiffrée est en H dans les mesures).
Utilisateurs et résultat attendu
| ID | Utilisateur | Résultat attendu |
|---|---|---|
| U-01 | Coach indépendant | Remplir son agenda, être payé d'avance, ne plus relancer à la main |
| U-02 | Client du coach (particulier) | Réserver et payer en moins de 2 min sans créer de compte H, puis être rappelé avant la séance |
Décisions tranchées
| ID | Question | Décision retenue | Fait / H |
|---|---|---|---|
| D-01 | Marché | France, EUR, interface en français | H |
| D-02 | Paiement | Stripe Checkout, paiement intégral à la réservation. Les frais standard sont de 1,5 % + 0,25 € pour une carte EEE standard (stripe.com/en-fr/pricing). | FAIT (frais) / H (intégral) |
| D-03 | Versement au coach | Stripe Connect Express, pour que chaque coach soit payé directement. Les frais Connect sont à vérifier. | H |
| D-04 | Fournisseur SMS | Twilio, environ 0,08 $ par SMS vers la France (twilio.com) | FAIT (prix) / H (choix) |
| D-05 | Volume de SMS | 1 SMS de rappel 24 h avant la séance. La confirmation passe par email. Coût d'environ 0,08 $ par réservation. | H |
| D-06 | Cadre légal du SMS | Un rappel est un message de service, sans consentement spécifique (rendv.fr). Aucune offre promotionnelle dans le SMS, car un message mixte peut être requalifié en prospection (source). | FAIT |
| D-07 | Annulation | Remboursement intégral jusqu'à 24 h avant la séance, aucun remboursement ensuite | H |
| D-08 | Compte client | Pas de compte : email et mobile suffisent. Le lien de gestion de la réservation est envoyé par email. | H |
| D-09 | Plateforme | Web responsive, fuseau Europe/Paris, sans application native | H |
| D-10 | Commission | Aucune commission plateforme au MVP, abonnement à tester plus tard | H |
Périmètre MVP
| ID | Fonction |
|---|---|
| MVP-01 | Compte coach (email + mot de passe) et onboarding Stripe Connect |
| MVP-02 | Création de séances : durée, prix, lieu |
| MVP-03 | Disponibilités hebdomadaires récurrentes et blocage de dates |
| MVP-04 | Page publique du coach (lien partageable) avec choix du créneau |
| MVP-05 | Paiement en ligne et verrouillage du créneau (pas de double réservation) |
| MVP-06 | Email de confirmation et SMS de rappel J-1 |
| MVP-07 | Annulation par le client (règle D-07) et par le coach (remboursement automatique) |
| MVP-08 | Tableau de bord coach : liste des réservations et paiements |
Hors périmètre
| ID | Exclu | Preuve qui justifierait son ajout H |
|---|---|---|
| HP-01 | Synchronisation Google/Apple Calendar | Au moins 3 coachs sur 5 la réclament |
| HP-02 | Forfaits, abonnements et séances collectives | Demandes récurrentes en pilote |
| HP-03 | SMS marketing et relances | Consentement et base légale conçus au préalable |
| HP-04 | Application native, multi-langue, multi-devise | Usage mobile saturé ou demande hors France |
| HP-05 | Facturation et comptabilité avancées | Besoin déclaré par au moins 3 coachs |
Mesures de succès (H : à valider par le responsable produit)
| ID | Mesure | Cible |
|---|---|---|
| MS-01 | Coachs pilotes actifs (≥ 1 réservation payée) | ≥ 5 en 8 semaines |
| MS-02 | Conversion page coach vers paiement | ≥ 20 % |
| MS-03 | Taux de no-show des séances rappelées par SMS | Inférieur à la base déclarée par les coachs pilotes |
| MS-04 | Rappels SMS délivrés | ≥ 95 % |
| MS-05 | Temps de réservation de bout en bout | < 2 min |
Règles métier et critères d'acceptation
4 hypothèses marquées
Statuts (liste unique)
| Statut de réservation | Sens |
|---|---|
EN_ATTENTE_PAIEMENT | Créneau verrouillé temporairement, paiement non confirmé |
CONFIRMEE | Paiement reçu, séance planifiée |
ANNULEE_CLIENT | Annulée par le client |
ANNULEE_COACH | Annulée par le coach |
EXPIREE | Paiement non finalisé avant expiration du verrou |
TERMINEE | Heure de fin de séance dépassée |
| Statut de paiement | Sens |
|---|---|
AUCUN | Aucun paiement initié |
PAYE | Capturé |
REMBOURSE | Remboursement intégral émis |
ECHEC_REMBOURSEMENT | Remboursement refusé ou en erreur, à traiter manuellement |
Besoins
| ID | Besoin | MVP |
|---|---|---|
| BE-01 | Le coach crée un compte et se connecte (email + mot de passe) | MVP-01 |
| BE-02 | Le coach active ses paiements via l'onboarding Stripe Connect | MVP-01 |
| BE-03 | Le coach définit ses séances : durée, prix, lieu | MVP-02 |
| BE-04 | Le coach définit des disponibilités hebdomadaires et bloque des dates | MVP-03 |
| BE-05 | Le client consulte la page publique d'un coach et ses créneaux libres | MVP-04 |
| BE-06 | Le client réserve sans compte (nom, email, mobile) | MVP-04 |
| BE-07 | Un créneau ne peut être réservé que par un seul client | MVP-05 |
| BE-08 | Le client paie en ligne en une fois | MVP-05 |
| BE-09 | Le client reçoit un email de confirmation avec lien de gestion | MVP-06 |
| BE-10 | Le client reçoit un SMS de rappel 24 h avant la séance | MVP-06 |
| BE-11 | Le client annule via son lien, selon le délai | MVP-07 |
| BE-12 | Le coach annule une séance et le client est remboursé automatiquement | MVP-07 |
| BE-13 | Le coach consulte réservations et paiements | MVP-08 |
Règles métier
Valeurs par défaut en H. Toutes les heures sont en Europe/Paris (D-09).
| ID | Règle | Défaut H | Cas limites |
|---|---|---|---|
| RM-01 | Un créneau = (coach, début, durée). Il est réservable s'il est dans une disponibilité, hors date bloquée et sans réservation EN_ATTENTE_PAIEMENT ou CONFIRMEE chevauchante | Pas de chevauchement, pas de pause tampon | Deux séances de durées différentes qui se chevauchent : la seconde est refusée. Créneau passé : non réservable |
| RM-02 | Délai minimal de réservation avant le début | 2 h | Créneau à moins de 2 h : non proposé |
| RM-03 | Horizon maximal de réservation | 60 jours | Au-delà : non proposé |
| RM-04 | Verrou de paiement : la création passe la réservation en EN_ATTENTE_PAIEMENT et verrouille le créneau | 15 min. Expiration → EXPIREE, créneau libéré | Paiement confirmé après expiration : remboursement automatique et information du client. Abandon sur Stripe : EXPIREE à l'échéance |
| RM-05 | Concurrence : le verrou est atomique (contrainte d'unicité en base), la première écriture gagne | n/a | Deux requêtes simultanées : une seule réussit, l'autre reçoit « créneau indisponible » |
| RM-06 | Paiement : montant = prix de la séance, EUR, intégral. Passage à CONFIRMEE uniquement sur webhook Stripe signé et idempotent | Aucune commission (D-10) | Webhook en doublon : sans effet. Échec de carte : réservation reste EN_ATTENTE_PAIEMENT jusqu'à expiration. Montant reçu ≠ attendu : réservation non confirmée |
| RM-07 | Annulation client (D-07) via lien de gestion | Plus de 24 h avant le début : ANNULEE_CLIENT + remboursement intégral. À 24 h pile ou moins : annulation refusée | Séance déjà TERMINEE ou annulée : refus. Lien utilisé deux fois : idempotent |
| RM-08 | Annulation coach : toujours possible avant le début, remboursement intégral automatique | Notification email + SMS au client | Annulation avec remboursement en échec : ANNULEE_COACH, paiement ECHEC_REMBOURSEMENT, alerte au coach |
| RM-09 | Frais de remboursement : le client reçoit le montant intégral payé. Le surcoût éventuel des frais Stripe sur remboursement est à vérifier | Absorbé par le coach H | Coach sans solde Connect suffisant : remboursement à traiter manuellement |
| RM-10 | Rappel SMS : 1 SMS 24 h avant le début, uniquement pour une réservation CONFIRMEE (D-05, D-06). Contenu transactionnel sans offre promotionnelle | Envoi à début − 24 h | Réservation faite à moins de 24 h du début : pas de rappel, la confirmation par email suffit. Annulée avant l'envoi : aucun SMS. Mobile invalide ou échec Twilio : 1 nouvel essai à +10 min, puis echec journalisé, sans blocage de la réservation. Jamais d'envoi en doublon pour une même réservation |
| RM-11 | Validation du mobile : format français (+33 / 0X) normalisé E.164 ; email valide | Mobile et email obligatoires | Format invalide : réservation refusée avec message explicite |
| RM-12 | Modification de disponibilités : sans effet sur les réservations CONFIRMEE existantes. Un blocage de date sur une réservation existante est refusé tant que le coach ne l'annule pas (RM-08) | n/a | Suppression d'une séance du catalogue : les réservations existantes sont conservées |
| RM-13 | Droits : un coach ne voit et ne modifie que ses données. Le lien de gestion client contient un jeton aléatoire non devinable, limité à une réservation | Jeton ≥ 128 bits | Jeton invalide ou inconnu : 404 sans fuite d'information |
| RM-14 | Paiement activé : une séance n'est réservable que si l'onboarding Connect du coach est complet | n/a | Compte Connect restreint ou désactivé après coup : nouvelles réservations bloquées, existantes conservées |
| RM-15 | Passage à TERMINEE : automatique après l'heure de fin | n/a | Aucun remboursement possible après TERMINEE |
Table de décision : annulation et remboursement (RM-07, RM-08, RM-15)
| Initiateur | Statut courant | Délai avant le début | Résultat | Remboursement |
|---|---|---|---|---|
| Client | CONFIRMEE | > 24 h | ANNULEE_CLIENT | Intégral |
| Client | CONFIRMEE | ≤ 24 h | Refusé | Aucun |
| Client | EN_ATTENTE_PAIEMENT | tout | EXPIREE (libération du créneau) | Aucun paiement à rembourser |
| Client | TERMINEE, ANNULEE_*, EXPIREE | tout | Refusé | Aucun |
| Coach | CONFIRMEE | tout (avant début) | ANNULEE_COACH | Intégral automatique |
| Coach | EN_ATTENTE_PAIEMENT | tout | EXPIREE | Aucun paiement à rembourser |
| Coach | TERMINEE, ANNULEE_*, EXPIREE | tout | Refusé | Aucun |
Critères d'acceptation
| ID | Étant donné | Quand | Alors | BE | RM |
|---|---|---|---|---|---|
| CA-01 | Un coach a une disponibilité le lundi 10:00–12:00 et une séance de 60 min | Un client ouvre sa page publique | Les créneaux 10:00 et 11:00 sont proposés | BE-05 | RM-01 |
| CA-02 | Une date est bloquée par le coach | Un client consulte cette date | Aucun créneau n'est proposé | BE-04 | RM-01 |
| CA-03 | Un créneau dans 1 h | Un client consulte la page | Le créneau n'est pas proposé | BE-05 | RM-02 |
| CA-04 | Un créneau dans 61 jours | Un client consulte la page | Le créneau n'est pas proposé | BE-05 | RM-03 |
| CA-05 | Un créneau libre | Un client soumet nom, email et mobile valides | Réservation EN_ATTENTE_PAIEMENT, redirection vers Stripe Checkout, créneau verrouillé | BE-06, BE-08 | RM-04 |
| CA-06 | Un créneau libre | Deux requêtes de réservation arrivent simultanément | Une seule réussit, l'autre reçoit « créneau indisponible » | BE-07 | RM-05 |
| CA-07 | Une réservation EN_ATTENTE_PAIEMENT depuis 16 min | Le job d'expiration s'exécute | Statut EXPIREE, créneau à nouveau réservable | BE-07 | RM-04 |
| CA-08 | Une réservation EN_ATTENTE_PAIEMENT | Un webhook Stripe signé de paiement réussi arrive | Statut CONFIRMEE, paiement PAYE, email de confirmation envoyé | BE-08, BE-09 | RM-06 |
| CA-09 | Le même webhook déjà traité | Il est reçu à nouveau | Aucun changement d'état, aucun second email | BE-08 | RM-06 |
| CA-10 | Un webhook avec signature invalide | Il est reçu | Rejeté (400), aucun changement d'état | BE-08 | RM-06 |
| CA-11 | Une réservation EXPIREE | Un webhook de paiement réussi arrive | Paiement REMBOURSE automatiquement, réservation reste EXPIREE | BE-08 | RM-04 |
| CA-12 | Un paiement dont le montant ≠ prix attendu | Le webhook arrive | La réservation n'est pas confirmée et l'anomalie est journalisée | BE-08 | RM-06 |
| CA-13 | Une réservation CONFIRMEE à plus de 24 h | Le client annule via son lien | ANNULEE_CLIENT, paiement REMBOURSE intégral, créneau libéré | BE-11 | RM-07 |
| CA-14 | Une réservation CONFIRMEE à 24 h ou moins | Le client tente d'annuler | Refus avec message explicite, statut inchangé | BE-11 | RM-07 |
| CA-15 | Une réservation ANNULEE_CLIENT | Le client rouvre son lien d'annulation | Aucun second remboursement, message « déjà annulée » | BE-11 | RM-07 |
| CA-16 | Une réservation CONFIRMEE à 2 h | Le coach l'annule | ANNULEE_COACH, remboursement intégral, email et SMS au client | BE-12 | RM-08 |
| CA-17 | Le remboursement Stripe échoue | Le coach annule | ANNULEE_COACH, paiement ECHEC_REMBOURSEMENT, alerte visible au coach | BE-12 | RM-08 |
| CA-18 | Une réservation CONFIRMEE début à T | L'horloge atteint T − 24 h | Un SMS de rappel est envoyé, une seule fois | BE-10 | RM-10 |
| CA-19 | Une réservation créée à moins de 24 h du début | Elle passe en CONFIRMEE | Aucun SMS de rappel, email de confirmation envoyé | BE-10 | RM-10 |
| CA-20 | Une réservation annulée avant T − 24 h | L'horloge atteint T − 24 h | Aucun SMS | BE-10 | RM-10 |
| CA-21 | Twilio renvoie une erreur | Le rappel est envoyé | Un nouvel essai à +10 min, puis échec journalisé, réservation inchangée | BE-10 | RM-10 |
| CA-22 | Un SMS de rappel généré | Son contenu est inspecté | Il ne contient aucune offre promotionnelle (modèle fixe testé) | BE-10 | RM-10 |
| CA-23 | Un mobile « 12345 » | Le client soumet le formulaire | Refus avec message de format, aucune réservation créée | BE-06 | RM-11 |
| CA-24 | Un mobile « 06 12 34 56 78 » | Le client soumet le formulaire | Enregistré en +33612345678 | BE-06 | RM-11 |
| CA-25 | Une réservation CONFIRMEE sur une date | Le coach bloque cette date | Refus tant que la réservation n'est pas annulée | BE-04 | RM-12 |
| CA-26 | Deux coachs A et B | A demande une réservation de B | 403 ou 404, aucune donnée de B exposée | BE-13 | RM-13 |
| CA-27 | Un jeton de gestion inconnu | Le lien est ouvert | 404 sans information sur l'existence d'une réservation | BE-11 | RM-13 |
| CA-28 | Un coach sans onboarding Connect complet | Un client consulte sa page | Aucune séance réservable | BE-02 | RM-14 |
| CA-29 | Une réservation CONFIRMEE dont l'heure de fin est dépassée | Le job de clôture s'exécute | Statut TERMINEE, annulation refusée | BE-13 | RM-15 |
| CA-30 | Un coach connecté avec 3 réservations | Il ouvre son tableau de bord | Les 3 réservations s'affichent avec statut de réservation et de paiement | BE-13 | RM-13 |
| CA-31 | Un email déjà utilisé par un coach | Un second compte est créé avec cet email | Refus avec message d'erreur | BE-01 | — |
| CA-32 | Un coach crée une séance de 60 min à 50 € | Il l'enregistre | Elle apparaît sur sa page publique avec durée, prix et lieu | BE-03 | RM-14 |
Questions ouvertes (hypothèses actuelles)
| ID | Question | Hypothèse H |
|---|---|---|
| Q-01 | Qui supporte les frais Stripe non restitués lors d'un remboursement ? | Le coach (RM-09), à vérifier dans la tarification Stripe Connect |
| Q-02 | Faut-il un délai de grâce après l'heure de début pour une séance non honorée par le coach ? | Non, hors MVP |
Parcours et écrans
1 lien vers une source
Parcours principaux
P-01 — Client : réserver et payer (U-02, sans compte)
- Ouvre la page publique du coach (lien partagé) et choisit une séance.
- Choisit un jour puis un créneau libre (fuseau Europe/Paris affiché).
- Saisit nom, email et mobile ; validation en ligne du format (RM-11).
- Confirme : le créneau est verrouillé 15 min et le client est redirigé vers Stripe Checkout.
- Paie ; retour sur la page de confirmation (statut « en cours de validation » tant que le webhook n'est pas reçu).
- Reçoit l'email de confirmation avec le lien de gestion ; le SMS de rappel arrive 24 h avant la séance.
P-02 — Client : annuler (U-02)
- Ouvre le lien de gestion reçu par email.
- Consulte le détail de la réservation et la règle d'annulation (remboursement intégral si > 24 h).
- Clique « Annuler », confirme dans une boîte de dialogue.
- Voit le résultat : annulée et remboursée, ou refus motivé (≤ 24 h).
P-03 — Coach : démarrer (U-01)
- Crée son compte (email + mot de passe) et se connecte.
- Lance l'onboarding Stripe Connect et revient sur l'application.
- Crée une séance : durée, prix, lieu.
- Définit ses disponibilités hebdomadaires et bloque des dates.
- Copie son lien public et le partage.
P-04 — Coach : suivre et annuler (U-01)
- Ouvre le tableau de bord : liste des réservations avec statut de réservation et de paiement.
- Ouvre une réservation à venir et clique « Annuler la séance », avec confirmation.
- Vérifie le remboursement automatique, ou traite l'alerte
ECHEC_REMBOURSEMENT.
Écrans
| ID | But | Éléments clés | États à gérer | CA |
|---|---|---|---|---|
| E-01 | Page publique du coach | Nom, séances (durée, prix, lieu), calendrier, liste de créneaux | Chargement ; aucun créneau ; coach sans Connect (aucune séance réservable) ; créneau devenu indisponible ; erreur réseau | CA-01 à CA-04, CA-28, CA-32 |
| E-02 | Coordonnées du client | Récapitulatif séance, champs nom/email/mobile, bouton « Payer », compte à rebours du verrou | Champ invalide (message précis) ; envoi en cours (bouton désactivé) ; conflit « créneau indisponible » ; verrou expiré | CA-05, CA-06, CA-23, CA-24 |
| E-03 | Retour de paiement | Statut, récapitulatif, lien vers la gestion | En attente du webhook ; confirmée ; échec ou abandon (relancer) ; expirée (refaire le choix) | CA-07 à CA-12 |
| E-04 | Gestion de la réservation (client) | Détail, règle d'annulation, bouton « Annuler » | Annulable ; refus ≤ 24 h ; déjà annulée ; terminée ; jeton inconnu (404) ; erreur | CA-13 à CA-15, CA-27 |
| E-05 | Inscription / connexion coach | Email, mot de passe | Email déjà utilisé ; identifiants erronés ; envoi en cours | CA-31 |
| E-06 | Onboarding paiement | Bouton Stripe Connect, statut du compte | Non démarré ; en cours ; complet ; restreint ou désactivé | CA-28 |
| E-07 | Séances et disponibilités | Liste des séances, formulaire séance, grille hebdomadaire, dates bloquées, lien public à copier | Aucune séance ; validation du formulaire ; blocage refusé (réservation confirmée existante) ; enregistrement réussi | CA-02, CA-25, CA-32 |
| E-08 | Tableau de bord coach | Liste des réservations (date, client, statut, paiement), détail, bouton « Annuler la séance », alerte remboursement | Liste vide ; chargement ; ECHEC_REMBOURSEMENT mis en évidence ; confirmation d'annulation ; erreur | CA-16, CA-17, CA-26, CA-29, CA-30 |
Règles d'accessibilité et mobile
| ID | Règle |
|---|---|
| AM-01 | Conception mobile d'abord pour E-01 à E-04 ; cibles tactiles d'au moins 44 px ; aucun défilement horizontal. |
| AM-02 | Parcours complet au clavier avec focus visible ; la liste de créneaux est une suite de boutons, pas un calendrier piégeant le focus. |
| AM-03 | Chaque champ a une étiquette visible, un type adapté (email, tel) et une erreur en texte, liée au champ et annoncée aux lecteurs d'écran ; jamais la couleur seule. |
| AM-04 | Le statut (confirmée, annulée, échec remboursement) est exprimé en texte et pas seulement par une couleur. |
| AM-05 | Le compte à rebours du verrou est annoncé de façon non intrusive, sans mise à jour vocale à chaque seconde. |
| AM-06 | Les actions destructrices (annuler) exigent une confirmation avec l'issue financière indiquée (montant remboursé ou refus). |
Limite : ces règles sont à vérifier sur le rendu réel selon les critères WCAG 2.2 retenus (quickref). Une maquette ne démontre pas l'accessibilité du code final.
Architecture et données
4 hypothèses marquées
Stack retenue
| Couche | Choix | Raison | Alternative écartée |
|---|---|---|---|
| Front + back | Next.js (TypeScript), un seul dépôt | Pages publiques rapides, API et front dans un déploiement unique H | SPA + API séparée : surcoût pour un MVP |
| Base | PostgreSQL | Contraintes d'exclusion (RM-05), transactions, jobs sur SQL | SQLite/NoSQL : pas d'exclusion de chevauchement fiable |
| Paiement | Stripe Checkout + Connect Express (D-02, D-03) | Pas de données carte à gérer | Paiement sur place : no-shows |
| SMS | Twilio (D-04) | Fournisseur retenu en section 1 | Brevo SMS : non évalué |
| Resend H | API simple pour confirmations | SMTP maison | |
| Jobs | Table job_planifie + worker cron (1 min) H | Pas de broker à exploiter, planification en SQL | Redis/BullMQ : composant en plus |
| Hébergement | PaaS EU + Postgres managé, sauvegarde quotidienne H | Données en UE, exploitation légère | Kubernetes : disproportionné |
Composants
- Web/API : pages publiques, espace coach, endpoints de réservation et de gestion par jeton.
- Webhook Stripe : vérifie la signature, déduplique (
evenement_stripe), confirme ou rembourse (RM-06). - Worker : expire les verrous (RM-04), envoie les rappels (RM-10), clôture les séances (RM-15).
- Adaptateurs : Stripe, Twilio, email, derrière des interfaces simulables.
Modèle de données (PostgreSQL)
CREATE EXTENSION IF NOT EXISTS btree_gist; -- requis pour l'exclusion (RM-05)
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE coach (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
email text NOT NULL,
mot_de_passe_hash text NOT NULL,
nom_affiche text NOT NULL,
slug text NOT NULL, -- lien public partageable (MVP-04)
stripe_account_id text,
connect_complet boolean NOT NULL DEFAULT false, -- RM-14
cree_le timestamptz NOT NULL DEFAULT now(),
CONSTRAINT coach_slug_format CHECK (slug ~ '^[a-z0-9-]{3,50}$')
);
-- CA-31 : un email = un compte (insensible à la casse)
CREATE UNIQUE INDEX coach_email_uniq ON coach (lower(email));
CREATE UNIQUE INDEX coach_slug_uniq ON coach (slug);
CREATE TABLE seance_type (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
coach_id uuid NOT NULL REFERENCES coach(id) ON DELETE CASCADE,
titre text NOT NULL,
duree_minutes integer NOT NULL CHECK (duree_minutes BETWEEN 15 AND 240),
prix_cents integer NOT NULL CHECK (prix_cents > 0), -- RM-06 : EUR, intégral
lieu text NOT NULL,
actif boolean NOT NULL DEFAULT true, -- RM-12 : désactivation sans suppression
cree_le timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX seance_type_coach_idx ON seance_type (coach_id) WHERE actif;
-- Disponibilités hebdomadaires récurrentes (heure locale Europe/Paris, D-09)
CREATE TABLE disponibilite (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
coach_id uuid NOT NULL REFERENCES coach(id) ON DELETE CASCADE,
jour_semaine smallint NOT NULL CHECK (jour_semaine BETWEEN 1 AND 7), -- 1 = lundi
heure_debut time NOT NULL,
heure_fin time NOT NULL,
CONSTRAINT disponibilite_plage CHECK (heure_fin > heure_debut)
);
CREATE INDEX disponibilite_coach_idx ON disponibilite (coach_id, jour_semaine);
CREATE TABLE date_bloquee (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
coach_id uuid NOT NULL REFERENCES coach(id) ON DELETE CASCADE,
jour date NOT NULL,
UNIQUE (coach_id, jour)
);
-- RM-12 (refus du blocage si réservation CONFIRMEE ce jour) : contrôle applicatif, CA-25.
CREATE TABLE reservation (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
coach_id uuid NOT NULL REFERENCES coach(id),
seance_type_id uuid NOT NULL REFERENCES seance_type(id),
client_nom text NOT NULL,
client_email text NOT NULL,
client_mobile_e164 text NOT NULL,
debut timestamptz NOT NULL,
fin timestamptz NOT NULL,
prix_cents integer NOT NULL CHECK (prix_cents > 0), -- copie figée du prix
statut text NOT NULL DEFAULT 'EN_ATTENTE_PAIEMENT',
statut_paiement text NOT NULL DEFAULT 'AUCUN',
verrou_expire_le timestamptz, -- RM-04 : créée + 15 min
jeton_gestion text NOT NULL DEFAULT encode(gen_random_bytes(32), 'hex'), -- RM-13 : 256 bits
stripe_session_id text,
stripe_payment_intent_id text,
cree_le timestamptz NOT NULL DEFAULT now(),
mis_a_jour_le timestamptz NOT NULL DEFAULT now(),
annulee_le timestamptz,
CONSTRAINT reservation_statut_valide CHECK (statut IN
('EN_ATTENTE_PAIEMENT','CONFIRMEE','ANNULEE_CLIENT','ANNULEE_COACH','EXPIREE','TERMINEE')),
CONSTRAINT reservation_paiement_valide CHECK (statut_paiement IN
('AUCUN','PAYE','REMBOURSE','ECHEC_REMBOURSEMENT')),
CONSTRAINT reservation_plage CHECK (fin > debut),
-- RM-11 : mobile français normalisé E.164
CONSTRAINT reservation_mobile_e164 CHECK (client_mobile_e164 ~ '^\+33[67][0-9]{8}$'),
CONSTRAINT reservation_email_format CHECK (client_email ~ '^[^@\s]+@[^@\s]+\.[^@\s]+$'),
-- RM-04 : un verrou est obligatoire tant que le paiement est en attente
CONSTRAINT reservation_verrou_requis CHECK (statut <> 'EN_ATTENTE_PAIEMENT' OR verrou_expire_le IS NOT NULL),
-- RM-06 : CONFIRMEE implique un paiement capturé
CONSTRAINT reservation_confirmee_payee CHECK (statut <> 'CONFIRMEE' OR statut_paiement = 'PAYE'),
-- RM-01 + RM-05 : pas de chevauchement pour un même coach tant que la réservation
-- est EN_ATTENTE_PAIEMENT ou CONFIRMEE ; la première écriture gagne (CA-06).
CONSTRAINT reservation_pas_de_chevauchement EXCLUDE USING gist (
coach_id WITH =,
tstzrange(debut, fin, '[)') WITH &&
) WHERE (statut IN ('EN_ATTENTE_PAIEMENT','CONFIRMEE'))
);
CREATE UNIQUE INDEX reservation_jeton_uniq ON reservation (jeton_gestion); -- RM-13
CREATE UNIQUE INDEX reservation_session_uniq ON reservation (stripe_session_id)
WHERE stripe_session_id IS NOT NULL;
CREATE INDEX reservation_coach_debut_idx ON reservation (coach_id, debut DESC); -- tableau de bord (BE-13)
CREATE INDEX reservation_expiration_idx ON reservation (verrou_expire_le)
WHERE statut = 'EN_ATTENTE_PAIEMENT'; -- job RM-04
CREATE INDEX reservation_cloture_idx ON reservation (fin)
WHERE statut = 'CONFIRMEE'; -- job RM-15
-- Idempotence des webhooks Stripe (RM-06, CA-09)
CREATE TABLE evenement_stripe (
id_evenement text PRIMARY KEY, -- evt_... : doublon = INSERT en conflit, sans effet
type text NOT NULL,
recu_le timestamptz NOT NULL DEFAULT now(),
traite_le timestamptz
);
-- Rappels SMS : au plus un par réservation (RM-10, CA-18)
CREATE TABLE rappel_sms (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
reservation_id uuid NOT NULL UNIQUE REFERENCES reservation(id) ON DELETE CASCADE, -- jamais de doublon
envoi_prevu_le timestamptz NOT NULL, -- début - 24 h
statut text NOT NULL DEFAULT 'a_envoyer'
CHECK (statut IN ('a_envoyer','envoye','echec','annule')),
tentatives smallint NOT NULL DEFAULT 0 CHECK (tentatives BETWEEN 0 AND 2), -- 1 essai + 1 retry à +10 min
twilio_sid text,
derniere_erreur text,
envoye_le timestamptz
);
CREATE INDEX rappel_sms_a_envoyer_idx ON rappel_sms (envoi_prevu_le) WHERE statut = 'a_envoyer';
-- Journal des anomalies (RM-06 montant ≠ attendu, ECHEC_REMBOURSEMENT, échec SMS)
CREATE TABLE journal_anomalie (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
reservation_id uuid REFERENCES reservation(id) ON DELETE SET NULL,
type text NOT NULL,
detail jsonb NOT NULL DEFAULT '{}',
cree_le timestamptz NOT NULL DEFAULT now()
);
Garanties portées par la base
| Règle | Mécanisme |
|---|---|
| RM-01, RM-05 | EXCLUDE USING gist partiel sur EN_ATTENTE_PAIEMENT et CONFIRMEE |
| RM-04 | verrou_expire_le obligatoire, index dédié pour le job d'expiration |
| RM-06 | evenement_stripe (idempotence) et CONFIRMEE impossible sans PAYE |
| RM-10 | UNIQUE (reservation_id) sur rappel_sms |
| RM-11 | CHECK E.164 et email |
| RM-13 | Jeton 256 bits, index unique |
Contrôles applicatifs (hors base)
RM-02, RM-03 (délai min. 2 h, horizon 60 j), RM-07 et RM-08 (table de décision d'annulation), RM-12 (blocage de date) et RM-14 (connect_complet). Le job d'expiration libère le créneau en passant le statut à EXPIREE, ce qui sort la ligne de la contrainte d'exclusion.
API, intégrations et sécurité
5 hypothèses marquées
Conventions : JSON sur HTTPS ; erreurs { "code", "message" } ; heures ISO 8601 en UTC, affichées en Europe/Paris (D-09). Session coach par cookie HttpOnly, Secure, SameSite=Lax H. Accès client par jeton de gestion (RM-13), sans compte.
API
| ID | Méthode et chemin | Rôle autorisé | Entrée | Sortie | Erreurs | RM / CA |
|---|---|---|---|---|---|---|
| API-01 | POST /api/coach/inscription | Public | email, mot de passe, nom_affiche | 201 {id, slug} | 400 format ; 409 email déjà utilisé | BE-01 ; CA-31 |
| API-02 | POST /api/coach/connexion | Public | email, mot de passe | 200 + cookie de session | 401 identifiants erronés ; 429 | BE-01 |
| API-03 | POST /api/coach/connect/onboarding | Coach | — | 200 {url} (lien Stripe Connect) | 401 ; 502 Stripe | BE-02 ; RM-14, CA-28 |
| API-04 | GET /api/coach/connect/statut | Coach | — | 200 {connect_complet} | 401 | RM-14 ; CA-28 |
| API-05 | POST /api/coach/seances | Coach | titre, duree_minutes, prix_cents, lieu | 201 séance | 400 validation ; 401 | BE-03 ; RM-06, CA-32 |
| API-06 | PATCH /api/coach/seances/{id} | Coach propriétaire | champs modifiés ou actif=false | 200 séance | 400 ; 401 ; 404 | RM-12, RM-13 |
| API-07 | PUT /api/coach/disponibilites | Coach | liste {jour_semaine, heure_debut, heure_fin} | 200 liste | 400 plage invalide ; 401 | BE-04 ; RM-01, CA-01 |
| API-08 | POST /api/coach/dates-bloquees | Coach | jour | 201 | 401 ; 409 réservation CONFIRMEE ce jour | RM-12 ; CA-25, CA-02 |
| API-09 | DELETE /api/coach/dates-bloquees/{id} | Coach propriétaire | — | 204 | 401 ; 404 | RM-01 |
| API-10 | GET /api/coach/reservations | Coach | filtres statut, du, au | 200 liste (statut, statut_paiement, alerte) | 401 | BE-13 ; RM-13, CA-30, CA-26 |
| API-11 | POST /api/coach/reservations/{id}/annuler | Coach propriétaire | — | 200 {statut, statut_paiement} | 401 ; 404 ; 409 statut non annulable ou séance terminée | BE-12 ; RM-08, RM-15, CA-16, CA-17 |
| API-12 | GET /api/public/coachs/{slug} | Public | — | 200 coach + séances actives (vide si connect_complet faux) | 404 | BE-05 ; RM-14, CA-28, CA-32 |
| API-13 | GET /api/public/coachs/{slug}/creneaux | Public | seance_type_id, du, au | 200 liste de débuts libres | 400 hors horizon ; 404 | BE-05 ; RM-01 à RM-03, CA-01 à CA-04 |
| API-14 | POST /api/public/reservations | Public | seance_type_id, debut, nom, email, mobile | 201 {reservation_id, jeton_gestion, checkout_url} | 400 mobile/email invalide ; 409 créneau indisponible ; 422 coach sans Connect, créneau hors règles | BE-06, BE-07, BE-08 ; RM-04, RM-05, RM-11, RM-14, CA-05, CA-06, CA-23, CA-24 |
| API-15 | GET /api/public/reservations/{jeton} | Détenteur du jeton | — | 200 détail, annulable (booléen), règle d'annulation | 404 jeton inconnu, sans fuite | BE-11 ; RM-13, CA-27, CA-15 |
| API-16 | POST /api/public/reservations/{jeton}/annuler | Détenteur du jeton | — | 200 {statut, statut_paiement} | 404 ; 409 refus (≤ 24 h, terminée, déjà annulée) | BE-11 ; RM-07, RM-15, CA-13, CA-14, CA-15 |
| API-17 | POST /api/webhooks/stripe | Stripe (signature) | événement signé | 200 ; doublon : 200 sans effet | 400 signature invalide | BE-08, BE-09 ; RM-04, RM-06, CA-08 à CA-12 |
| JOB-01 | Worker, chaque minute | Interne | — | Réservations EN_ATTENTE_PAIEMENT échues → EXPIREE | — | RM-04 ; CA-07 |
| JOB-02 | Worker, chaque minute | Interne | — | Envoi des rappel_sms échus (réservation CONFIRMEE), retry à +10 min | — | BE-10 ; RM-10, CA-18 à CA-22 |
| JOB-03 | Worker, chaque minute | Interne | — | CONFIRMEE passée de fin → TERMINEE | — | RM-15 ; CA-29 |
Comportement structurant :
- API-14 : transaction unique.
INSERTdansreservation(la contraintereservation_pas_de_chevauchementtranche la concurrence, violation → 409), puis création de la session Checkout avecstripe_session_id. Si Stripe échoue, la ligne passe enEXPIREE. - API-17 :
INSERTdansevenement_stripe; conflit surid_evenement→ sans effet. Montant ≠prix_cents→ pas de confirmation, ligne dansjournal_anomalie. Réservation déjàEXPIREE→ remboursement automatique,statut_paiement = REMBOURSE. - API-11 / API-16 : mêmes transitions que la table de décision de la section 2 ; remboursement Stripe en échec →
ECHEC_REMBOURSEMENTet anomalie journalisée. - API-03 : l'événement Stripe
account.updated(reçu par API-17) met à jourconnect_complet.
Intégrations externes
| Service | Usage | Point de défaillance | Comportement de repli |
|---|---|---|---|
| Stripe Checkout | Paiement de la réservation | Indisponibilité à la création de session | Réservation EXPIREE, 502 au client, créneau libéré |
| Stripe webhooks | Confirmation, account.updated | Webhook perdu ou en retard | Stripe rejoue ; si le verrou expire avant, paiement tardif remboursé (CA-11) |
| Stripe Connect / remboursements | Onboarding, remboursements | Refus ou solde insuffisant (RM-09) | ECHEC_REMBOURSEMENT, alerte au coach, traitement manuel |
| Twilio | SMS de rappel et d'annulation coach | Erreur API, mobile invalide | 1 retry à +10 min, puis echec journalisé ; réservation inchangée (CA-21) |
| Resend H | Email de confirmation et lien de gestion | Erreur API | Retry par le worker ; lien de gestion aussi affiché sur E-03 |
| PostgreSQL | Données, verrou, jobs | Indisponibilité | 503 ; aucun état partiel grâce aux transactions |
Sécurité et données personnelles
- Authentification coach : mot de passe haché argon2id ou bcrypt (
mot_de_passe_hash), limitation des tentatives sur API-02, session expirante H. - Autorisation : toute requête coach filtre par
coach_idde la session ; accès hors périmètre → 404 (CA-26). Jeton de gestion à 256 bits, 404 uniforme si inconnu (CA-27). - Paiement : aucune donnée de carte chez nous (Stripe Checkout). Webhook accepté uniquement avec signature valide ; clés Stripe, Twilio et Resend en variables d'environnement, jamais dans le dépôt.
- Transport et entrées : HTTPS obligatoire, validation serveur de tous les champs (E.164, email), requêtes SQL paramétrées, limitation de débit sur API-13, API-14 et API-16 contre l'abus de verrous H.
- Données personnelles (RGPD) : client = nom, email, mobile, minimisés à ce qui sert la séance. Hébergement UE, journaux sans mobile ni email en clair. Durée de conservation H : 3 ans après la séance, puis anonymisation. SMS strictement transactionnels (D-05, D-06).
- Droits des personnes : procédure manuelle d'accès, rectification et effacement sur demande (MVP). Information et base légale (exécution du contrat) à afficher en E-02. À valider par le porteur du projet ; ce document n'est pas un avis juridique.
Plan de développement
1 hypothèse marquée
Hypothèses de plan H : un seul dépôt Next.js + PostgreSQL ; Stripe, Twilio et Resend sont isolés derrière des interfaces simulables (mocks en test) ; tests : Vitest (unitaire et intégration sur Postgres de test) et Playwright (bout en bout). La tranche verticale prioritaire est P-01 (réserver et payer) : elle est livrée dès le jalon J3. Chaque tâche suppose les corrections EC-xx intégrées (marquées « corrige EC-xx »).
Décisions de plan issues de la relecture
| Écart | Décision appliquée dans le plan |
|---|---|
| EC-01 | Webhook tardif sur EXPIREE : PAYE puis REMBOURSE dans une même transaction (T-12) |
| EC-02 | Insertion de rappel_sms à la confirmation (T-11) ; passage à annule aux annulations (T-15, T-17) |
| EC-03 | SMS d'annulation coach conservé ; D-05 devient « 1 rappel + 1 SMS d'annulation coach éventuel », contenu transactionnel (T-17) |
| EC-04 | Jeton = 256 bits partout ; RM-13 aligné (T-02) |
| EC-05 | Blocage de date refusé si réservation EN_ATTENTE_PAIEMENT ou CONFIRMEE ce jour ; en plus, la confirmation Stripe d'une réservation sur date bloquée n'est pas empêchée car le créneau est verrouillé avant (T-08, T-12) |
| EC-06 | Mobile = 06/07 uniquement ; RM-11 aligné ; validation API avant insertion, 400 et non 500 (T-09) |
| EC-07 | Ajout des GET coach séances, disponibilités, dates bloquées (T-06, T-07, T-08) |
| EC-08 | E-03 utilise API-15 par jeton (T-13) |
J0 — Socle (dépôt, base, outillage)
| ID | Objectif | Modules ou fichiers | Dépend de | CA | Preuve de fin | Piège à éviter |
|---|---|---|---|---|---|---|
| T-01 | Initialiser Next.js (TS), lint, Vitest, Playwright, variables d'environnement typées, CI | package.json, src/lib/env.ts, .github/workflows/ci.yml | — | — | npm run lint && npm test verts en CI | Ne jamais committer de clé ; échouer au démarrage si une variable manque |
| T-02 | Migrations : schéma §4 complet, avec jeton 256 bits (corrige EC-04) et CHECK mobile ^\+33[67][0-9]{8}$ | db/migrations/001_init.sql | T-01 | CA-06, CA-31 | Test d'intégration : insertion chevauchante rejetée par EXCLUDE ; email en double rejeté | Activer btree_gist avant la contrainte ; tester la contrainte sur base réelle, pas en mémoire |
| T-03 | Adaptateurs simulables Stripe, Twilio, Resend, Horloge (interfaces et mocks) | src/adapters/*.ts | T-01 | — | Tests unitaires avec mocks ; horloge injectable | Sans horloge injectable, les tests 24 h et 15 min sont impossibles |
J1 — Compte coach et paiements activés
| ID | Objectif | Modules ou fichiers | Dépend de | CA | Preuve de fin | Piège à éviter |
|---|---|---|---|---|---|---|
| T-04 | API-01/02 : inscription, connexion, session cookie HttpOnly, hash argon2id, limitation des tentatives | src/app/api/coach/inscription, connexion, src/lib/auth.ts | T-02 | CA-31 | Test : second compte avec le même email (casse différente) → 409 | Comparer l'email en lower() ; ne pas révéler si l'email existe à la connexion |
| T-05 | API-03/04 : onboarding Connect Express, statut ; account.updated met à jour connect_complet (traité dans T-12) | src/app/api/coach/connect/* | T-04, T-03 | CA-28 | Test mock Stripe : lien renvoyé ; statut reflète connect_complet | Ne jamais déduire « complet » du retour navigateur, seulement du webhook |
| T-06 | Écrans E-05 et E-06 (inscription/connexion, onboarding) | src/app/(coach)/connexion, onboarding | T-04, T-05 | CA-31 | Playwright : création de compte puis affichage du statut Connect | Messages d'erreur liés aux champs (AM-03) |
J2 — Séances et disponibilités
| ID | Objectif | Modules ou fichiers | Dépend de | CA | Preuve de fin | Piège à éviter |
|---|---|---|---|---|---|---|
| T-07 | API-05/06 + GET /api/coach/seances (corrige EC-07) : création, modification, désactivation (actif=false) | src/app/api/coach/seances | T-04 | CA-32 | Test : séance 60 min à 5000 cents créée, relue par GET ; accès à la séance d'un autre coach → 404 | Filtrer toujours par coach_id de session ; prix en centimes entiers |
| T-08 | API-07/08/09 + GET /api/coach/disponibilites et GET /api/coach/dates-bloquees (corrige EC-07) ; blocage refusé si réservation EN_ATTENTE_PAIEMENT ou CONFIRMEE ce jour (corrige EC-05) | src/app/api/coach/disponibilites, dates-bloquees | T-04 | CA-02, CA-25 | Test : blocage sur jour avec réservation CONFIRMEE → 409 ; idem avec EN_ATTENTE_PAIEMENT | Calculer « le jour » en Europe/Paris, pas en UTC |
| T-09 | Module de calcul des créneaux : disponibilités, dates bloquées, durée, chevauchements, délai min 2 h, horizon 60 j | src/domain/creneaux.ts | T-02, T-03 | CA-01, CA-02, CA-03, CA-04 | Tests unitaires CA-01 à CA-04 avec horloge figée | Passage heure d'été/hiver : tester un lundi de changement d'heure |
| T-10 | Écran E-07 (séances, disponibilités, dates bloquées, lien public copiable) | src/app/(coach)/seances | T-07, T-08 | CA-02, CA-25, CA-32 | Playwright : création séance et disponibilité, blocage refusé affiché | Afficher l'erreur 409 en texte clair (AM-04) |
J3 — Tranche verticale : réserver et payer (P-01)
| ID | Objectif | Modules ou fichiers | Dépend de | CA | Preuve de fin | Piège à éviter |
|---|---|---|---|---|---|---|
| T-11 | API-12/13 : page publique et créneaux ; séances masquées si connect_complet faux | src/app/api/public/coachs/[slug] | T-09, T-05 | CA-01 à CA-04, CA-28, CA-32 | Tests API : coach sans Connect → liste de séances vide | Ne pas exposer d'email ni d'id interne coach |
| T-12 | API-14 : validation entrées (email, mobile 06/07 normalisé E.164, 400 avant insertion, corrige EC-06), INSERT réservation avec verrou 15 min, session Checkout ; violation d'exclusion → 409 ; échec Stripe → EXPIREE | src/app/api/public/reservations, src/domain/reservation.ts, src/lib/telephone.ts | T-11, T-03 | CA-05, CA-06, CA-23, CA-24 | Test concurrence : 2 requêtes parallèles → 1 × 201, 1 × 409 ; « 12345 » → 400 ; « 06 12 34 56 78 » → +33612345678 | Une transaction ; ne pas pré-vérifier la disponibilité puis insérer (course) : la contrainte tranche |
| T-13 | API-17 webhook Stripe : signature, idempotence evenement_stripe, CONFIRMEE si montant égal, anomalie sinon ; à la confirmation, insertion de rappel_sms si début − now > 24 h (corrige EC-02) ; webhook tardif sur EXPIREE : PAYE puis REMBOURSE dans la même transaction (corrige EC-01) ; account.updated → connect_complet | src/app/api/webhooks/stripe, src/domain/paiement.ts | T-12 | CA-08 à CA-12, CA-19 | Tests : doublon sans effet, signature invalide → 400, montant ≠ → pas de confirmation et ligne journal_anomalie, paiement tardif → EXPIREE + REMBOURSE | Lire le corps brut pour la signature ; INSERT dans evenement_stripe et traitement dans la même transaction |
| T-14 | JOB-01 : worker d'expiration des verrous (cron 1 min) | src/worker/expiration.ts | T-12 | CA-07 | Test avec horloge : réservation à 16 min → EXPIREE, créneau à nouveau réservable | Ne toucher que EN_ATTENTE_PAIEMENT ; requête idempotente |
| T-15 | Écrans E-01, E-02, E-03 (E-03 lit API-15 par jeton, corrige EC-08), compte à rebours, états d'erreur | src/app/c/[slug], reserver, retour | T-11, T-12, T-13 | CA-05, CA-06, CA-23, CA-24, CA-07 | Playwright : parcours P-01 complet avec Stripe simulé, du créneau à « confirmée » | Page de retour : statut « en cours de validation » tant que le webhook n'est pas reçu |
J4 — Notifications et gestion client
| ID | Objectif | Modules ou fichiers | Dépend de | CA | Preuve de fin | Piège à éviter |
|---|---|---|---|---|---|---|
| T-16 | Email de confirmation avec lien de gestion (envoi après confirmation, retry par le worker) | src/adapters/email.ts, src/domain/notifications.ts | T-13 | CA-08, CA-09 | Test : 1 email à la confirmation, 0 sur webhook doublon | Envoyer après commit de la transaction, pas dedans |
| T-17 | JOB-02 : envoi des rappels SMS, 1 retry à +10 min puis echec ; modèle de message fixe sans promotion ; jamais de doublon | src/worker/rappels.ts, src/domain/sms.ts | T-13, T-03 | CA-18 à CA-22 | Tests : 1 SMS à T − 24 h ; erreur Twilio → retry puis echec, réservation inchangée ; contenu = modèle fixe | UNIQUE(reservation_id) + mise à jour atomique du statut avant envoi pour éviter le double envoi |
| T-18 | API-15/16 : détail et annulation client par jeton, table de décision ; remboursement intégral ; rappel_sms.statut='annule' à l'annulation (corrige EC-02) ; 404 uniforme si jeton inconnu | src/app/api/public/reservations/[jeton] | T-13 | CA-13 à CA-15, CA-27 | Tests : > 24 h → ANNULEE_CLIENT + REMBOURSE ; ≤ 24 h → 409 ; 2ᵉ appel idempotent sans second remboursement ; jeton inconnu → 404 | Clé d'idempotence Stripe sur le remboursement ; frontière exacte à 24 h pile = refus |
| T-19 | Écran E-04 (gestion client, confirmation d'annulation avec issue financière, AM-06) | src/app/g/[jeton] | T-18 | CA-13 à CA-15, CA-27 | Playwright : annulation depuis le lien, puis réouverture → « déjà annulée » | Boîte de dialogue accessible au clavier |
J5 — Espace coach : suivi et annulation
| ID | Objectif | Modules ou fichiers | Dépend de | CA | Preuve de fin | Piège à éviter |
|---|---|---|---|---|---|---|
| T-20 | API-10/11 : liste des réservations et annulation coach, remboursement automatique ; ECHEC_REMBOURSEMENT + anomalie + alerte ; rappel_sms → annule ; email + SMS d'annulation coach au client, contenu transactionnel, SMS documenté dans D-05 (corrige EC-03) | src/app/api/coach/reservations | T-18, T-17 | CA-16, CA-17, CA-26, CA-30 | Tests : annulation à 2 h → ANNULEE_COACH + REMBOURSE + email et SMS ; échec Stripe → ECHEC_REMBOURSEMENT ; réservation d'un autre coach → 404 | Ne pas bloquer l'annulation si le remboursement échoue ; ne pas rembourser une réservation EN_ATTENTE_PAIEMENT (→ EXPIREE) |
| T-21 | JOB-03 : clôture CONFIRMEE → TERMINEE ; refus d'annulation ensuite | src/worker/cloture.ts | T-18, T-20 | CA-29 | Test horloge : après fin → TERMINEE, annulation → 409 | Comparer sur fin, pas sur debut |
| T-22 | Écran E-08 (tableau de bord, alerte ECHEC_REMBOURSEMENT, annulation avec confirmation) | src/app/(coach)/tableau-de-bord | T-20, T-21 | CA-16, CA-17, CA-26, CA-29, CA-30 | Playwright : 3 réservations affichées avec statuts en texte ; annulation et alerte visibles | Statuts en texte, pas seulement en couleur (AM-04) |
J6 — Durcissement et livraison
| ID | Objectif | Modules ou fichiers | Dépend de | CA | Preuve de fin | Piège à éviter |
|---|---|---|---|---|---|---|
| T-23 | Limitation de débit (API-02, 13, 14, 16), en-têtes de sécurité, journaux sans mobile ni email en clair | src/middleware.ts, src/lib/log.ts | T-12, T-18 | CA-26, CA-27 | Test : rafale sur API-14 → 429 ; inspection des logs sans donnée personnelle | Limiter aussi par IP sur les routes publiques |
| T-24 | Tests de bout en bout sur les parcours P-01 à P-04 et mention RGPD en E-02 | e2e/*.spec.ts, E-02 | T-15, T-19, T-22 | CA-05 à CA-30 | npx playwright test vert en CI | Mocks Stripe/Twilio : ne jamais appeler les services réels en CI |
| T-25 | README : installation, variables d'environnement, lancement des workers, limites connues et hypothèses à vérifier (frais de remboursement Connect, solde insuffisant, qualification juridique des SMS) | README.md | T-24 | — | Installation à froid réussie en suivant le README | Lister les limites telles quelles, sans les masquer |
Couverture des critères d'acceptation
| CA | Tâche(s) |
|---|---|
| CA-01 | T-09, T-11 |
| CA-02 | T-08, T-09, T-10 |
| CA-03 | T-09, T-11 |
| CA-04 | T-09, T-11 |
| CA-05 | T-12, T-15, T-24 |
| CA-06 | T-02, T-12, T-15 |
| CA-07 | T-14, T-15 |
| CA-08 | T-13, T-16 |
| CA-09 | T-13, T-16 |
| CA-10 | T-13 |
| CA-11 | T-13 |
| CA-12 | T-13 |
| CA-13 | T-18, T-19 |
| CA-14 | T-18, T-19 |
| CA-15 | T-18, T-19 |
| CA-16 | T-20, T-22 |
| CA-17 | T-20, T-22 |
| CA-18 | T-17 |
| CA-19 | T-13, T-17 |
| CA-20 | T-17, T-18 |
| CA-21 | T-17 |
| CA-22 | T-17 |
| CA-23 | T-12, T-15 |
| CA-24 | T-12, T-15 |
| CA-25 | T-08, T-10 |
| CA-26 | T-20, T-22, T-23 |
| CA-27 | T-18, T-19, T-23 |
| CA-28 | T-05, T-11 |
| CA-29 | T-21, T-22 |
| CA-30 | T-20, T-22 |
| CA-31 | T-02, T-04, T-06 |
| CA-32 | T-07, T-10, T-11 |
Les 32 CA sont couverts. Les T-13 et T-17 s'appuient sur rappel_sms créé à la confirmation (EC-02) ; l'ordre T-13 → T-17 → T-18 → T-20 respecte les dépendances.
Relecture indépendante
Écarts
| ID | Gravité | Où | Problème | Correction |
|---|---|---|---|---|
| EC-01 | Haute | §4 reservation_confirmee_payee vs RM-06 / CA-08 | Contrainte CONFIRMEE ⇒ PAYE. Mais RM-07/RM-08 passent en ANNULEE_* avec REMBOURSE : cohérent. En revanche TERMINEE n'a pas de contrainte, et surtout un remboursement automatique sur EXPIREE (RM-04/CA-11) est incohérent avec RM-06 « réservation expirée = créneau libéré » : le client payé tardivement ne voit pas son créneau garanti. Ce cas est traité, mais le paiement PAYE transitoire avant REMBOURSE n'est pas défini. | Préciser en RM-04 : webhook tardif → paiement PAYE puis REMBOURSE dans la même transaction. |
| EC-02 | Haute | RM-10 / §4 rappel_sms / JOB-02 | rappel_sms n'est jamais créé ni annulé dans les parcours : aucune règle ni API ne dit qui insère la ligne (à la confirmation, API-17) ni qui passe annule (annulations RM-07/RM-08). CA-19 (< 24 h : pas de rappel) suppose de ne pas insérer. | Ajouter à API-17 : à la confirmation, insérer rappel_sms si début − now() > 24 h. Ajouter à API-11/API-16 : passer rappel_sms.statut = 'annule'. |
| EC-03 | Moyenne | RM-08 / §5 intégrations | RM-08 prévoit « email + SMS au client » à l'annulation coach, mais aucun SMS d'annulation n'est dans D-05 (1 SMS par réservation), RM-10, ni les CA (CA-16 le cite). Coût et cadre D-06 non traités. | Mettre à jour D-05 : « 1 rappel + 1 SMS d'annulation coach éventuel » et garder son contenu transactionnel, ou retirer le SMS de RM-08/CA-16. |
| EC-04 | Moyenne | RM-13 vs §4 | RM-13 dit « jeton ≥ 128 bits », §4/§5 en fournissent 256 bits. Écart mineur de formulation. | Aligner RM-13 sur « 256 bits » (ou « ≥ 128 » partout). |
| EC-05 | Moyenne | RM-12 / CA-25 vs API-08 | RM-12 : blocage refusé si réservation CONFIRMEE ; la réservation EN_ATTENTE_PAIEMENT ce jour n'est pas couverte et peut devenir CONFIRMEE sur une date bloquée. | Étendre à EN_ATTENTE_PAIEMENT ou filtrer les créneaux/confirmations sur dates bloquées. |
| EC-06 | Moyenne | §4 CHECK reservation_mobile_e164 vs RM-11 | Regex \+33[67]… n'accepte que les mobiles 06/07 ; RM-11 dit « +33 / 0X » (tout format français). CA-24 passe, mais un 01–05, 08–09 est accepté par RM-11 et rejeté en base (500 au lieu de 400). | Aligner RM-11 sur « mobile français 06/07 » et valider côté API avant insertion. |
| EC-07 | Faible | §3 E-01/E-07 vs API | Aucun endpoint de lecture pour E-07 (liste séances, disponibilités, dates bloquées du coach) ni de suppression de disponibilité/séance ; GET coach manquants. | Ajouter GET /api/coach/seances, GET /api/coach/disponibilites, GET /api/coach/dates-bloquees. |
| EC-08 | Faible | §3 E-03 vs API | E-03 affiche le statut « en attente du webhook » mais API-15 est la seule lecture et est présentée pour E-04. | Indiquer que E-03 utilise API-15 par jeton. |
Hypothèses à vérifier en premier
- Frais de remboursement Stripe Connect (D-03, RM-09) : le coach les absorbe, avec risque de solde insuffisant et de remboursements bloqués ; vérifier la tarification Connect et le modèle de charge (destination vs direct) avant de coder.
- Remboursement automatique Stripe Connect sans solde coach : si le compte du coach est négatif, le remboursement peut échouer systématiquement ; tester sur compte test.
- SMS de service sans consentement (D-06) : la qualification « message de service » repose sur des sources de blogs ; faire valider par un juriste (RGPD/CNIL) avant la mise en production.
Sources web consultées
28 liens vers des sources
- Taux de commission carte bancaire : guide complet (2026) - Shopify France
- Les frais Stripe
- Stripe fees 2026: what a payment really costs
- Frais Stripe : calculateur, net reçu et prix
- Frais Stripe 2026 : combien coûte chaque vente
- Pricing & Fees
- Stripe ou Mollie en France : les frais dépendent du paiement
- Stripe vs PayPal : quel prestataire de paiement en 2026
- Stripe ou PayPal en France : le coût selon le panier - Quasa.io
- Twilio SMS pricing in France: cost per message and monthly totals
- SMS Pricing in France for Text Messaging
- https://www.twilio.com/content/dam/twilio-com/pric...
- Twilio SMS Pricing 2026: $0.0083 a Text Plus Carrier Fees
- Twilio Pricing 2026: Plans, Costs & Hidden Fees Explained - Twilio
- Twilio WhatsApp Pricing 2026: $0.005 a Message Plus Meta Fees
- DroidSender
- DroidSender
- Best SMS APIs for France: Compare Pricing, Delivery Rates, and Integration Options
- Twilio SMS Pricing in 2026: What You Actually Pay
- Prospection commerciale email ou SMS : client, prospect, B2B, que faire apres le rappel CNIL ? - Maître Reda Kohen, avocat en droit immobilier et droit des affaires à Paris
- Gestion des SMS de Rappel de Rendez-vous en France : Conformité au RGPD
- SMS publicitaires malgré STOP, spams signalés au 33700 sans effet : contester, saisir la CNIL et obtenir réparation (2026) - Maître Reda Kohen, avocat en droit immobilier et droit des affaires à Paris
- TéLéPHONIE É d i t i o n 2 0 1 2
- SMS de rappel et de confirmation de rendez-vous : modèles prêts à copier
- La CNIL dit enfin ce que doit être la prospection commerciale faite par SMS ou e-mail pour être légale
- Rappel de rendez-vous par SMS : quoi écrire, quand envoyer, ce qui est permis
- Garage : quels messages clients envoyer, à quelle fréquence, et ce que dit la CNIL sur les SMS
- Prospection par e-mail et SMS : décryptage du nouveau guide de la CNIL pour rester en conformité