# Elsa — Documentation technique

Plateforme PHP/MySQL de gestion d'un studio de fitness/yoga : planning, réservations,
abonnements, invitations, cartes-cadeaux, push notifications, questionnaire de santé.

> **Objectif de ce document** : permettre à un nouveau développeur de comprendre, déployer et maintenir l'application sans contexte préalable.

---

## Table des matières

1. [Vue d'ensemble](#1-vue-densemble)
2. [Stack technique & dépendances](#2-stack-technique--dépendances)
3. [Architecture des fichiers](#3-architecture-des-fichiers)
4. [Authentification & rôles](#4-authentification--rôles)
5. [Base de données](#5-base-de-données)
6. [Auto-migrations](#6-auto-migrations)
7. [Settings (clé/valeur configurables)](#7-settings)
8. [Modules fonctionnels](#8-modules-fonctionnels)
9. [Cron jobs](#9-cron-jobs)
10. [Sécurité](#10-sécurité)
11. [Déploiement](#11-déploiement)
12. [Branding & charte graphique](#12-branding--charte-graphique)
13. [Troubleshooting](#13-troubleshooting)
14. [Glossaire métier](#14-glossaire-métier)

---

## 1. Vue d'ensemble

**Elsa** est une application web mono-tenant (un seul studio) avec trois espaces :

| Espace | URL | Rôle | Authentification |
|---|---|---|---|
| Public (widget) | `/widget/` | Affichage du planning, réservation, achat | Optionnelle |
| Public (cartes-cadeaux) | `/giftcard.php` | Achat & utilisation de carte-cadeau | Optionnelle (login requis pour utiliser) |
| Espace client | `/client/` | Réservations, achats, profil | Login obligatoire |
| Espace professeur | `/teacher/` | Liste d'appel, séance découverte, notes | Login obligatoire |
| Espace administration | `/admin/` | Gestion complète | Login obligatoire |

Le widget public peut être **embarqué via iframe** sur un site externe (`assets/js/widget-embed.js`).

---

## 2. Stack technique & dépendances

**Backend** :
- PHP 8.1+ procédural (PDO MySQL)
- MySQL/MariaDB 10+
- Composer pour les dépendances tierces

**Frontend** :
- HTML/CSS/JS vanilla (pas de framework)
- Font Awesome 6 (CDN)
- Stripe.js pour paiements CB
- Service Worker `sw.js` + manifest PWA

**Dépendances Composer (`composer.json`)** :
- `minishlink/web-push` — push notifications VAPID
- `setasign/fpdi` + `setasign/fpdf` — génération PDF (cartes-cadeaux)
- `chillerlan/php-qrcode` — QR code dans les PDF

**APIs externes** :
- Stripe (paiements CB)
- GoCardless (prélèvements SEPA pour abonnements)

**Pas de framework** : tout le code est en PHP procédural avec includes. Les fichiers sont organisés par rôle (admin/, client/, teacher/, widget/) et chacun gère son propre routing via paramètres GET et actions POST.

---

## 3. Architecture des fichiers

```
/
├── admin/                      Backoffice administrateur
│   ├── index.php               Dashboard
│   ├── clients.php             Liste & recherche clients
│   ├── client_detail.php       Fiche client (achats, historique, tags, questionnaire)
│   ├── contacts.php            Invités (contacts non-clients) + export CSV
│   ├── bookings.php            Grille planning hebdo + modal inscrits + push + transfert
│   ├── schedule.php            Configuration créneaux hebdomadaires + cours ponctuels
│   ├── exceptions.php          Fermetures & modifications du planning
│   ├── class_types.php         Types de cours + coût en crédits par offre
│   ├── teachers.php            Profs + grille de rémunération
│   ├── studios.php             Lieux
│   ├── offers.php              Formules (carnet, abo, découverte, unité)
│   ├── payments.php            Historique paiements
│   ├── push.php                Configuration VAPID + envoi notifications
│   ├── analytics.php           Stats : inscrits/présents, par cours/prof/lieu/créneau
│   ├── billing.php             Précalcul facturation profs mensuelle + export CSV
│   ├── questionnaires.php      Questionnaires santé : à traiter + historique
│   ├── giftcards.php           Cartes-cadeaux : liste + template PDF + coords
│   └── settings.php            Paramètres globaux (logo, infos, Stripe, GC, widget)
│
├── client/                     Espace membre
│   ├── index.php               Tableau de bord, réservations, achats, profil
│   ├── cart.php                Panier multi-formules + carte-cadeau
│   ├── invite.php              Inviter un(e) ami(e) à un cours
│   ├── renew.php               Renouveler/changer de formule
│   ├── questionnaire.php       Questionnaire de santé (CERFA dématérialisé)
│   ├── paysms.php              Page de paiement séance découverte (lien email)
│   ├── waitlist_confirm.php    Confirmer une place libérée en liste d'attente
│   ├── gc_callback.php         Callback GoCardless (signature mandat SEPA)
│   ├── push_subscription.php   Endpoint d'enregistrement push
│   └── login.php / logout.php
│
├── teacher/                    Espace professeur
│   ├── index.php               Liste cours + liste d'appel + découverte + notes + push
│   ├── login.php / logout.php
│
├── widget/                     Widget public embarquable
│   └── index.php               Planning, login/register, achat, réservation, choix formule
│
├── giftcard.php                Page publique cartes-cadeaux (achat + utilisation)
│
├── includes/                   Code partagé
│   ├── config.php              Constantes (BASE_URL, URLs, DB credentials)
│   ├── database.php            Singleton PDO
│   ├── helpers.php             ★ Cœur fonctionnel : auth, CSRF, sessions, crédits,
│   │                             auto-migrations, tags, branding, push, questionnaire…
│   ├── emails.php              Classe EmailTemplate (envoi + templates HTML)
│   ├── gocardless.php          Wrapper API GoCardless
│   ├── push_sender.php         Wrapper WebPush
│   └── giftcard_pdf.php        Génération PDF carte-cadeau (FPDI + QR)
│
├── cron/                       Tâches planifiées
│   ├── check_payments.php      GoCardless : vérif paiements + renouvellement crédits
│   └── prospect_lifecycle.php  Welcome J+30min + relance 72h + J+7 invité + relance
│                                questionnaire annuelle + rappel J-1 découverte + avis Google
│
├── templates/                  Templates partagés
│   └── admin_layout.php        Layout admin (sidebar + topbar + content)
│
├── assets/                     Statiques
│   ├── css/app.css             Feuille de styles principale (vars CSS branding)
│   ├── js/
│   │   ├── app.js              JS commun (modales, confirms…)
│   │   ├── pwa.js              Service worker logic
│   │   └── widget-embed.js     Loader iframe pour intégration externe
│   ├── icons/                  Icônes PWA (192, 512)
│   └── uploads/                Logos studio + templates PDF + PDF cartes-cadeaux générés
│
├── vendor/                     Dépendances Composer
├── manifest.php                Manifest PWA dynamique
├── sw.js                       Service worker
├── install.php                 Script d'installation initial (création DB)
└── composer.json
```

**Convention de routing** : pas de framework MVC, chaque fichier PHP gère ses propres paramètres GET (`?action=…`, `?edit=…`, `?ajax=…`) et POST. Les requêtes AJAX retournent du JSON via `jsonResponse()`. Les pages classiques redirigent avec `redirect()`.

---

## 4. Authentification & rôles

**3 rôles** : `admin`, `teacher`, `client`. Chaque rôle a sa propre table (`admins`, `teachers`, `clients`) et son propre login.

**Session PHP** : `$_SESSION['user_id']`, `$_SESSION['user_role']`, `$_SESSION['user_name']`.

**Helpers (includes/helpers.php)** :
- `requireAuth('admin'|'teacher'|'client')` — redirige vers login si pas connecté avec le bon rôle
- `isLoggedIn(string $role)` — booléen
- `currentUserId(): ?int`
- `loginUser($id, $role, $name)` — régénère l'ID de session
- `logout()`

**CSRF** : toutes les requêtes POST passent par `verifyCsrf()` qui compare `$_POST[CSRF_TOKEN_NAME]` avec `$_SESSION[CSRF_TOKEN_NAME]`. Helpers : `csrfField()`, `generateCsrf()`.

**Mot de passe** : bcrypt via `password_hash()` / `password_verify()`. Cost paramétrable via `HASH_COST` (config.php).

---

## 5. Base de données

**Tables principales** (cf. `nkwx1081_elsa.sql` pour le schéma initial, mais privilégier les auto-migrations dans `helpers.php` pour les ajouts récents).

### Tables core

| Table | Description |
|---|---|
| `admins` | Administrateurs (login + nom) |
| `teachers` | Professeurs (login + nom + email + grille de rémunération via `teacher_billing_rules`) |
| `clients` | Membres inscrits + `has_used_discovery`, `first_purchase_done`, `google_review_email_sent_at` |
| `studios` | Lieux où ont lieu les cours |
| `class_types` | Types de cours (yoga vinyasa, pilates…) avec couleur, capacité par défaut, waitlist |
| `class_type_studio_capacity` | Capacité override par (cours, lieu) |
| `weekly_schedule` | Créneaux récurrents (jour de la semaine + heure) |
| `class_sessions` | Sessions individuelles (instances datées) — `weekly_schedule_id` NULL si **cours ponctuel** (`is_special=1`) + `teacher_notes` |

### Réservations & crédits

| Table | Description |
|---|---|
| `bookings` | Une réservation = (client OU contact OU découverte anonyme) sur une `class_session` |
| `client_purchases` | Achats de formules (un par formule, lié à une `offer`) — porte les crédits restants |
| `credit_history` | Log des mouvements de crédits (débit/crédit) |
| `cart_items` | Panier multi-formules en cours |
| `guest_invitations` | Lien (host_client, contact, session) pour les invitations |
| `waitlist_tokens` | Tokens uniques de confirmation de place libérée |
| `contacts` | Personnes ayant été invitées (non encore clientes) |

### Paiements

| Table | Description |
|---|---|
| `payments` | Tous les paiements (CB, prélèvement, espèces, chèque, carte-cadeau) |
| `upcoming_payments` | Prélèvements GoCardless planifiés/échoués |
| `pending_gc_flows` | Flows GoCardless en attente de signature mandat SEPA |
| `discovery_payment_tokens` | Tokens uniques pour paiement séance découverte par email |
| `payment_alerts` | Anomalies de paiement à traiter par l'admin |

### Offres

| Table | Description |
|---|---|
| `offers` | Catalogue : type (discovery/single/pack/subscription), prix, crédits, durée, **unlimited_guest_invitations** (formule duo) |
| `class_type_offers` | Quelles offres paient quels cours + **`credit_cost`** (coût en crédits variable par paire) |

### Features récentes (Lots 1-12)

| Table | Description | Lot |
|---|---|---|
| `prospect_emails` | Suivi du cycle de vie d'un prospect (welcome J+30min, promo 72h, J+7 invité, conversion) | 5 |
| `health_questionnaires` | Questionnaires CERFA 15699-01 signés (9 réponses, code signature, IP, expires_at) | 6 |
| `teacher_billing_rules` | Grille de rémunération (teacher, class_type) — horaire OU forfait par paliers <9/≥9 prés. | 11 |
| `gift_cards` | Cartes-cadeaux (code unique, montant, solde restant, expires_at 6 mois) | 12 |
| `gift_card_redemptions` | Log d'utilisation partielle d'une carte-cadeau | 12 |
| `coupons` / `coupon_redemptions` | Coupons de réduction + log d'utilisation | F |
| `password_resets` | Jetons de réinitialisation/création de mot de passe (3 réalms, validité indéfinie) | I |
| `push_subscriptions` | Endpoints push web par client | — |
| `push_notifications_log` | Historique des envois push | — |
| `schedule_exceptions` | Fermetures (closure) ou modifications planning, optionnellement par studio | — |
| `settings` | Stockage clé/valeur pour tous les paramètres (cf. §7) | — |

### Colonnes ajoutées (via auto-migration)

- `bookings.contact_id`, `is_guest_booking`, `is_anticipation`, `discovery_payment_method`, `discovery_contact_name`, `discovery_contact_email`, `welcome_first_email_sent_at`, `discovery_reminder_sent_at`
- `class_sessions.is_special`, `teacher_notes`, `teacher_notes_updated_at`
- `class_type_offers.credit_cost` (défaut 1)
- `offers.unlimited_guest_invitations`
- `clients.google_review_email_sent_at`
- `discovery_payment_tokens.contact_email`

---

## 6. Auto-migrations

Pour éviter au déployeur de jouer du SQL manuel, **`includes/helpers.php`** contient deux fonctions auto-migrantes appelées à chaque chargement (idempotentes) :

- `_autoMigrateBookings()` — colonnes manquantes sur `bookings`, tables manquantes (`waitlist_tokens`, `contacts`, `guest_invitations`, `prospect_emails`, `gift_cards`, `gift_card_redemptions`, `coupons`, `coupon_redemptions`, `teacher_billing_rules`, `health_questionnaires`, `password_resets`), colonnes `is_special` / `teacher_notes` / `credit_cost` / `unlimited_guest_invitations`.
- `_autoMigratePush()` — crée la table `push_subscriptions` si absente, à partir d'un fichier `push_migration.sql`.

Ces fonctions s'exécutent une seule fois par requête HTTP (via flag `static $done`).

⚠️ **Limitation** : si le user MySQL n'a pas le privilège `ALTER` / `CREATE`, ces migrations échouent **silencieusement** (try/catch). Vérifier les privilèges en cas de problème.

Pour migrer manuellement (recommandé pour audit) : voir le récap SQL dans le message du repo `DOCS.md` ou inspecter `helpers.php` → bloc `_autoMigrateBookings`.

---

## 7. Settings

Table clé/valeur `settings` lue/écrite via `getSetting($key, $default)` / `setSetting($key, $value)`.

### Paramètres généraux (admin/settings.php → Général)

| Clé | Défaut | Description |
|---|---|---|
| `studio_name` | "Studio Zen" | Nom affiché partout (fallback si pas de logo) |
| `logo_dark_bg_path` | "" | Chemin relatif vers le logo pour fond sombre |
| `logo_light_bg_path` | "" | Chemin relatif vers le logo pour fond clair |
| `cancellation_hours_refund` | 24 | Délai (h) avant cours pour annulation avec remboursement |
| `cancellation_hours_no_refund` | 6 | Délai (h) avant cours pour annulation sans remboursement |
| `credit_low_threshold` | 2 | Seuil "crédits faibles" (alerte client + picto prof) |
| `expiry_warning_days` | 7 | Délai (j) d'alerte avant expiration formule |
| `guest_free_per_month` | 2 | Quota d'invitations gratuites par mois (bypass par formule duo) |
| `guest_price` | 15.00 | Tarif invité au-delà du quota |
| `google_review_url` | "" | URL fiche Google My Business pour collecte d'avis |
| `google_review_threshold` | 5 | Nb de cours présents avant envoi de l'email avis |
| `home_content` | "" | HTML personnalisé page d'accueil client |
| `booking_custom_message` | "" | HTML personnalisé sous "Réserver un cours" |

### Stripe (admin/settings.php → Stripe)

- `stripe_public_key`
- `stripe_secret_key`

### GoCardless (admin/settings.php → GoCardless)

- `gocardless_access_token`
- `gocardless_environment` (`sandbox`/`live`)

### Push notifications (admin/push.php)

- `vapid_public_key`
- `vapid_private_key`
- `admin_email` (pour le subject VAPID)

### Cartes-cadeaux (admin/giftcards.php)

- `giftcard_pdf_template` — chemin du PDF Canva uploadé
- `giftcard_pdf_coords` — JSON des coordonnées des placeholders dans le PDF

---

## 8. Modules fonctionnels

### Lot 1 — Quick wins UI/UX
- **Téléphone** : indicatifs +262/+33 alignés (Réunion + France)
- **Vue inscription admin** étendue à 30j
- **booking.php** : invités + séances découverte visibles dans le modal
- **Séance découverte par email** (au lieu de SMS) avec lien Stripe
- **Pictos liste d'appel** prof : ⚠️ crédits faibles, ⏳ formule expirant
- **Exceptions "modification"** traitées comme fermetures (planning + check résa)
- **Email confirmation** à l'invité

### Lot 2 — Logos light/dark
- Upload de 2 versions du logo (fond clair / fond sombre) via `admin/settings.php`
- Helpers `getStudioLogoUrl($variant)` et `studioBrandHtml($variant, $imgHeight)`
- Remplacement de "🧘 Studio Zen" partout (headers, login, emails, paysms, PWA modal)
- Format accepté : PNG, JPG, SVG, WEBP — 2 Mo max

### Lot 3 — Tags clients & push ciblé
- **Tags** auto sur fiche client (cours réservés + lieux fréquentés, avec compteur)
- **Push targeting** étendu : `all`, `client:N`, `class_type:N`, `studio:N`, `session:N`
- **Push aux inscrits d'une séance** depuis le modal de `admin/bookings.php` (pré-rempli)
- **Export CSV** des invités non-clients dans `admin/contacts.php`

### Lot 4 — Widget & formules
- **Pop-up de réservation widget** : choix de la formule à débiter si ≥ 2 formules actives
- **Formule duo** : checkbox `unlimited_guest_invitations` sur offer → invitations illimitées sans débit de crédit
- **Widget embed filtrable** par `data-studio-id` et `data-class-type-id`
- Builder dans `admin/settings.php` → Widget → Vue filtrée

### Lot 5 — Cycle prospect + analytics
- Table `prospect_emails` + cron `prospect_lifecycle.php`
- Email **J+30min après 1er cours** avec offre 1 crédit offert valable 30 jours (souscrit dans les 72h)
- Email **relance dernier jour** de la promo 72h
- Email **J+7 après invitation** (si pas devenu client)
- Application automatique de la promo au panier (1 crédit bonus 30 jours)
- Page **`admin/analytics.php`** : filtres date/prof/lieu/cours, agrégations

### Lot 6 — Questionnaire santé CERFA 15699-01
- Table `health_questionnaires` + 9 questions exactes du QS-Sport
- Flow client : 9 réponses Oui/Non → code 6 chiffres email → signature (horodatage + IP)
- Validité 1 an, blocage des bookings/cart/invite/renew sans questionnaire valide
- Admin : page dédiée avec onglets "À traiter" (manquants + certificat requis) et "Historique"
- Badge rouge dans la sidebar admin
- Relance annuelle via cron (5j avant expiration)

### Lot 7 — Quick wins & charte graphique
- **Charte** : `#18594e` / `#5e9289` / `#c09175` propagée dans CSS + emails + hardcoded
- **Surlignage achats expirant ≤ 30j** (avec crédits > 0) sur fiche client
- **Places restantes affichées** dans "Mes cours" + masquage du bouton "Inviter" si complet
- **Push prof aux inscrits** depuis sa vue (bloc dépliable)

### Lot 8 — Cron emails/push complémentaires
- **Push doublé** à la libération d'une place liste d'attente (en plus du mail)
- Helper `sendPushToClient($clientId, $title, $body, $url, $tag)` réutilisable
- **Rappel J-1 séance découverte** (mail + push si client enregistré)
- **Collecte avis Google** après X cours présents (1 envoi unique, tracé via `clients.google_review_email_sent_at`)

### Lot 9 — Planning & cours ponctuels
- **Cours ponctuel** (`is_special=1`, `weekly_schedule_id=NULL`) : créé directement dans `class_sessions`
- **Annulation stricte** : aucun remboursement quel que soit le délai
- **Transfert admin** : changer le `client_id` d'une booking ponctuelle (depuis le modal)
- **Pictos planning** : ⭐ ponctuel, 📝 note prof
- **Carrousel** mise en avant côté client (entre message custom et sélecteur studio)
- **Notes prof auto-save** (debounce 800ms) + remontée admin dans le modal

### Lot 10 — Crédit variable & analytics présents
- **`class_type_offers.credit_cost`** : configurable par paire (cours, offre) dans `admin/class_types.php`
- `deductCredit()` et `refundCredit()` adaptés multi-crédits
- **Analytics** enrichi : colonnes Présents + bloc "Par créneau récurrent" (jour/heure/lieu/cours)

### Lot 11 — Facturation profs
- Table `teacher_billing_rules` (teacher, class_type, billing_type, taux/forfaits)
- Grille de rémunération dans `admin/teachers.php` (édition d'un prof)
- Page **`admin/billing.php`** : précalcul mensuel avec récap par prof + détail séance
- **Export CSV** intégré

### Lot 12 — Cartes-cadeaux
- Page publique `/giftcard.php` (landing + achat Stripe + utilisation par code)
- Génération PDF avec template Canva uploadé + overlay (FPDI) + QR code (chillerlan)
- Fallback PDF basique si template absent
- Admin : liste + upload template + édition des coordonnées des placeholders
- Validité 6 mois, utilisable en plusieurs fois (solde résiduel)
- Application au checkout `client/cart.php` (déduction automatique, paiement résiduel via Stripe)

### Lot F — Coupons de réduction
- Tables `coupons` + `coupon_redemptions` (auto-migration dans `_autoMigrateBookings()`)
- Admin : `admin/coupons.php` (lien sidebar « Coupons ») — CRUD : code, type (% ou montant fixe), valeur, montant minimum, nombre max d'utilisations (total), utilisations par client, période de validité, activation
- Helpers (`includes/helpers.php`) : `getCouponByCode()`, `computeCouponDiscount()`, `evaluateCoupon()` (valide actif/dates/quotas/min/par-client + calcule la remise), `redeemCoupon()`
- Application aux points de paiement :
  - `client/cart.php` (panier multi-offres) : champ « Code promo », coupon en session `active_coupon_code`, remise appliquée **avant** la carte-cadeau puis Stripe
  - `client/renew.php` : code promo en paiement comptant uniquement (réduit le 1er débit carte ; ignoré pour le paiement en plusieurs fois). Garde-fou minimum Stripe 0,50 €
  - `widget/index.php` (API `purchase`) : champ « Code promo » dans le modal, validé côté serveur avant l'achat
- Un coupon est une **remise** (pas un instrument de paiement) : journalisé dans `coupon_redemptions`, jamais inséré comme `payment` (n'inflate pas le CA)

### Lot I — Mot de passe oublié (admin / clients / professeurs)
- Table `password_resets` (auto-migration) : `user_type` ENUM('admin','client','teacher'), `user_id`, `token` UNIQUE, `purpose` ENUM('reset','create'), `used_at`. **Validité indéfinie** (le jeton vaut tant qu'il n'est pas consommé) ; un seul lien actif par compte (les anciens sont neutralisés à la création d'un nouveau)
- Helpers (`includes/helpers.php`) : `createPasswordResetToken()`, `getValidPasswordReset()`, `consumePasswordReset()`, `setUserPassword()` (met à jour `password_hash` dans la bonne table), `findUserByEmail()`
- Email : `EmailTemplate::passwordReset($firstName,$resetUrl,$purpose)` (variante `reset` ou `create`)
- Pages **mutualisées** à la racine : `forgot_password.php?role=admin|client|teacher` (réponse anti-énumération) et `reset_password.php?token=…`
- Lien « Mot de passe oublié ? » ajouté sur `admin/login.php`, `client/login.php`, `teacher/login.php`

### Lot H — Séance découverte (inscription par l'admin)
- Page `admin/discovery.php` (lien sidebar « Séance découverte ») : formulaire nom/prénom/email/téléphone + moyen de règlement (espèces / chèque / CB) + choix du créneau (séances à venir sous 30 j)
- Au submit : création (ou réutilisation) du compte client (`has_used_discovery=1`) → réservation découverte (`booked_by='admin'`, `credit_deducted=0`) → email de confirmation (`discoveryConfirmation`) → si nouveau compte, email d'activation/création de mot de passe (jeton Lot I, purpose `create`)
- **CB** : crée un `discovery_payment_tokens` + envoie le lien `client/paysms.php?token=…` (`discoveryPaymentLink`). Le paiement est enregistré `succeeded` par paysms.php après règlement — aucune ligne `payment` pré-créée
- **Espèces / chèque** : insère un `payments` (`payment_method='manual'`, **`status='pending'`**, libellé précisant « espèces »/« chèque »). Donc **exclu des statistiques** (le CA filtre `status='succeeded'`)
- Validation du règlement : action `mark_paid` dans `admin/payments.php` ET `admin/client_detail.php` → passe `pending` → `succeeded` (+ `payment_date=NOW()`). Les lignes `pending` sont **surlignées en rouge** dans les deux pages, avec bouton « Valider »

---

## 9. Cron jobs

### `cron/check_payments.php`

À planifier **toutes les heures** :
```
0 * * * * php /chemin/elsa/cron/check_payments.php >> /var/log/elsa-cron.log 2>&1
```

Vérifie les paiements GoCardless en `processing`, détecte les échecs, suspend les abonnements, renouvelle les crédits mensuels, expire les achats périmés.

### `cron/prospect_lifecycle.php`

À planifier **toutes les 15 minutes** :
```
*/15 * * * * php /chemin/elsa/cron/prospect_lifecycle.php >> /var/log/elsa-cron.log 2>&1
```

Exécute 6 étapes successives :
1. **Welcome J+30min** après le 1er cours (séance découverte ou invité)
2. **Relance promo 72h** (envoyée 24h avant expiration)
3. **J+7 invité** non-client
4. **Relance annuelle questionnaire santé** (350-360j post-signature ou 7j post-inscription)
5. **Rappel J-1 séance découverte** (mail + push)
6. **Avis Google** après seuil X cours présents

**Sécurité** : avant la mise en prod, modifier la valeur `CHANGEZ_CETTE_CLE_SECRETE` dans le fichier (clé pour exécution via HTTP en fallback CLI).

---

## 10. Sécurité

- **CSRF** : tous les POST critiques sont protégés (`verifyCsrf()`). Le token est régénéré à la connexion.
- **Sessions** : `session_regenerate_id(true)` au login pour éviter le session fixation.
- **XSS** : helper `e()` (alias `htmlspecialchars`) appliqué à tout output dynamique.
- **SQL Injection** : PDO prepared statements partout. **Aucun** `$_GET` ou `$_POST` interpolé en SQL.
- **Mots de passe** : bcrypt cost configurable (`HASH_COST` dans config.php).
- **Cookies** : les sessions PHP utilisent `httponly` et `secure` (si HTTPS).
- **.htaccess** : protection du dossier `includes/` (`RewriteRule ^includes/ - [F,L]`), headers de sécurité (X-Content-Type-Options, X-XSS-Protection).
- **VAPID** : clés générées via OpenSSL EC (prime256v1).
- **Stripe** : montants vérifiés côté serveur après confirmation client (re-check du PaymentIntent).
- **GoCardless** : tokens session uniques + callback validés.

⚠️ **À durcir éventuellement** :
- Headers CSP / HSTS (à ajouter dans `.htaccess`)
- Rate limiting login
- 2FA admin

---

## 11. Déploiement

### Prérequis
- PHP 8.1+ (extensions : pdo_mysql, mbstring, openssl, curl, gd, fileinfo)
- MySQL/MariaDB 10+
- Composer 2+
- Apache avec mod_rewrite ET mod_headers
- Privilèges DB `ALTER` et `CREATE TABLE` pour le user MySQL (auto-migrations)
- Cron (pour les tâches planifiées)
- SMTP fonctionnel (la fonction PHP `mail()` est utilisée)

### Étapes

1. **Cloner / uploader** les fichiers sur le serveur.
2. **Installer Composer deps** :
   ```bash
   composer install --no-dev --optimize-autoloader
   ```
3. **Créer la base de données** + un user MySQL avec privilèges complets sur cette base.
4. **Configurer** `includes/config.php` :
   - `DB_HOST`, `DB_NAME`, `DB_USER`, `DB_PASS`
   - `BASE_URL`, `ADMIN_URL`, `CLIENT_URL`, `TEACHER_URL`, `WIDGET_URL`
   - `HASH_COST` (recommandé : 12)
   - `CURRENCY_SYMBOL`, `CSRF_TOKEN_NAME`
5. **Importer** `nkwx1081_elsa.sql` dans la base (schéma initial). Les colonnes/tables ajoutées ultérieurement seront créées automatiquement au premier chargement (auto-migrations).
6. **Permissions** : rendre `assets/uploads/` writable par PHP (`chmod 775` ou similaire), idem pour `assets/uploads/giftcards/`.
7. **Charger une page** (ex. `/admin/login.php`) → vérifier absence d'erreur 500 (= signe d'auto-migration en échec).
8. **Configurer le premier admin** : insérer manuellement en SQL ou utiliser `install.php` (si encore présent).
9. **Configurer les settings** depuis `admin/settings.php` :
   - Nom du studio + logos
   - Clés Stripe (test puis prod)
   - GoCardless si abonnements SEPA
   - VAPID push (générer depuis `admin/push.php`)
10. **Planifier les cron** (voir §9).
11. **Tester un parcours complet** : inscription widget → questionnaire santé → réservation → invitation → annulation.

---

## 12. Branding & charte graphique

**Couleurs primaires** définies dans `assets/css/app.css` (variables CSS) :

| Variable | Valeur | Usage |
|---|---|---|
| `--color-primary` | `#18594e` | Vert foncé principal (boutons, titres, headers) |
| `--color-primary-light` | `#5e9289` | Vert sauge (dégradés, accents) |
| `--color-primary-dark` | `#0f3d35` | Vert très foncé (login bg gradient) |
| `--color-primary-bg` | `#e6efed` | Fond très clair de la couleur primaire |
| `--color-accent` | `#c09175` | Beige cuivré (CTA secondaires, formule duo, alertes douces) |
| `--color-accent-light` | `#dec1a5` | Beige clair |
| `--color-accent-bg` | `#f8efe5` | Fond très clair de l'accent |

**Pour changer la charte** : modifier ces 7 variables dans `app.css`. Les emails (`includes/emails.php`) ont **deux constantes en dur** : `$primaryColor = '#18594e'` et `$accentColor = '#c09175'` — à modifier en parallèle.

**Logo** : uploadé via admin/settings.php → onglet Général. Deux versions (fond clair / fond sombre). Fallback `🧘 NomStudio` si aucun logo.

**Police** : DM Sans (body) + Playfair Display (titres) — déclarées dans `app.css`, à charger via Google Fonts si non chargées par le HTML.

---

## 13. Troubleshooting

### Page blanche / erreur 500
- Vérifier les logs Apache/PHP (`error_log` dans le dossier du fichier ou logs serveur).
- Cause fréquente : auto-migration en échec → DB user sans privilège ALTER.
- Cause fréquente : composer non installé → page giftcard.php ou push échouent.

### Auto-migrations ne s'exécutent pas
- Vérifier privilèges MySQL du user app
- Charger n'importe quelle page incluant `helpers.php` (ex. `/client/login.php`)
- Si toujours bloqué, jouer manuellement les `CREATE TABLE` / `ALTER TABLE` extraits de `helpers.php` (fonction `_autoMigrateBookings`)

### Push notifications ne partent pas
- Vérifier `composer install` effectué (lib `minishlink/web-push`)
- Vérifier clés VAPID configurées (`admin/push.php`)
- Vérifier que `admin_email` est dans `settings` (utilisé pour le subject VAPID)
- Vérifier les logs PHP — `[PushSender] HTTP …` indique le code retour

### Emails ne partent pas
- La fonction PHP `mail()` est utilisée → vérifier qu'un SMTP est configuré côté serveur (Postfix / sendmail / relais)
- Vérifier les spams côté destinataire
- Pour debug, ajouter un log dans `EmailTemplate::send()` avant le `mail()`

### PDF carte-cadeau vide ou incorrect
- Composer installé ? (`setasign/fpdi` + `setasign/fpdf` + `chillerlan/php-qrcode`)
- Template PDF compatible FPDI ? (PDF 1.4, pas chiffré — Canva exporte parfois en PDF 1.6+ qui nécessite FPDI Premium)
- Coords correctes dans `admin/giftcards.php` ? Calibrer avec une carte de test.

### Carte-cadeau non appliquée au checkout
- Le code doit être validé via `/giftcard.php?action=redeem` ou `?gift=CODE` sur cart.php
- Vérifier `$_SESSION['active_gift_card_code']` (PHP session active)
- Vérifier que la carte n'est pas expirée (>6 mois) ou à solde 0

### Questionnaire santé bloque tout le monde
- L'auto-migration de la table `health_questionnaires` a-t-elle réussi ?
- En cas de blocage urgent : commenter temporairement les `requireValidHealthQuestionnaire();` dans les fichiers client/

### Cron ne s'exécute pas
- Vérifier la crontab (`crontab -l` pour le user qui héberge l'app)
- Vérifier le chemin PHP CLI (`which php`)
- Tester manuellement : `php /chemin/elsa/cron/prospect_lifecycle.php` → doit afficher des logs

---

## 14. Glossaire métier

- **Séance / class_session** : un cours instancié à une date+heure précise.
- **Créneau / weekly_schedule** : un template de cours récurrent (ex. tous les lundis 9h).
- **Cours ponctuel / is_special** : une séance créée hors créneau récurrent (one-shot).
- **Type de cours / class_type** : la catégorie (Yoga Vinyasa, Pilates Intermédiaire…).
- **Offre / offer** : une formule à vendre (carnet 10, abo mensuel, séance unitaire, découverte).
- **Achat / client_purchase** : ce que le client a acheté (instance d'offre avec crédits restants).
- **Crédit** : 1 réservation = X crédits débités du client_purchase (`class_type_offers.credit_cost`).
- **Découverte** : 1er cours offert/discount, géré via `clients.has_used_discovery` et `offers.type='discovery'`.
- **Invitation** : un client membre invite un contact à un cours (gratuit si quota restant, sinon CB).
- **Formule duo** : offre avec `unlimited_guest_invitations=1` → invitations illimitées sans débit.
- **Liste d'attente / waitlisted** : booking sur un cours complet, notifié email+push si place libérée.
- **Anticipation** : abonné qui réserve avant que son crédit mensuel n'arrive → marqué `is_anticipation=1`, le crédit sera ajusté au renouvellement.
- **Prospect** : email recensé dans `prospect_emails` (séance découverte ou invité), suivi pour la conversion.
- **CERFA 15699-01** : questionnaire de santé officiel français (QS-Sport, 9 questions).

---

**Version doc** : maintenue à jour jusqu'au Lot 13 + Lots F (coupons), H (séance découverte admin) et I (mot de passe oublié).
**Contact d'origine** : Vincent Verdier — propriétaire/dev d'origine.

> 💌 Si tu reprends ce projet : commence par lire ce doc en entier, puis lance les cron en local pour observer leur comportement. La meilleure entrée en matière est probablement de créer un faux client, simuler une séance attended, et voir le cycle prospect se dérouler.
