diff --git a/README.md b/README.md
index 6877ffe..0a1bceb 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,92 @@
# expria-frontend
+Interface utilisateur d'Expria — Coach IA de préparation au TCF Canada.
+Plateforme SaaS francophone ciblant les candidats à l'immigration Express Entry
+(Algérie, Maroc, Cameroun).
+
+## Stack technique
+
+- **React 18** + **Vite** — frontend pur, pas de logique serveur
+- **TypeScript** — typage strict
+- **Tailwind CSS** — styles utilitaires
+- **Supabase JS** — authentification côté client uniquement
+
+## Documents de référence
+
+Lire ces fichiers avant toute modification :
+
+| Fichier | Contenu |
+|---|---|
+| `docs/DEVELOPMENT_PRINCIPLES.md` | Cycle de travail, règles, conventions — **obligatoire** |
+| `docs/ARCHITECTURE.md` | Stack, structure, flux de données |
+| `docs/PLANS_TARIFAIRES.md` | Plans Free / Standard / Premium et permissions |
+| `docs/PARCOURS_UTILISATEURS.md` | Parcours exact de chaque type d'utilisateur |
+| `docs/GOLDEN_DATASET.md` | Tests manuels anti-régression |
+
+## Structure des dossiers
+
+```
+src/
+├── api/ # Fonctions d'appel API (une par domaine)
+├── components/ # Composants React réutilisables
+│ ├── ui/ # Composants génériques (Button, Modal, Badge)
+│ ├── simulation/ # Composants spécifiques aux simulations
+│ ├── rapport/ # Composants d'affichage des rapports
+│ ├── dashboard/ # Composants du dashboard utilisateur
+│ └── paywall/ # Composants de blocage / upgrade
+├── pages/ # Pages de l'application (une par route)
+├── hooks/ # Hooks React personnalisés
+├── lib/
+│ ├── access.ts # Source de vérité des permissions par plan ⚠️
+│ ├── supabase.ts # Client Supabase (auth uniquement)
+│ └── constants.ts # Constantes globales
+└── types/ # Types TypeScript partagés
+```
+
+> ⚠️ `src/lib/access.ts` est la source de vérité unique des permissions par plan.
+> Ce fichier doit rester identique à `src/lib/access.ts` dans `expria-backend`.
+> Toute modification des plans tarifaires met ce fichier à jour en premier.
+
+## Variables d'environnement
+
+Créer un fichier `.env` à la racine (ne jamais committer) :
+
+```
+VITE_API_URL=http://localhost:3000 # URL backend en développement
+VITE_SUPABASE_URL=https://xxx.supabase.co
+VITE_SUPABASE_ANON_KEY=xxx # Clé publique uniquement
+```
+
+> ❌ Aucune clé privée ne doit apparaître dans ce fichier ou dans le code frontend.
+
+## Commandes
+
+```bash
+# Installer les dépendances
+npm install
+
+# Démarrer en développement
+npm run dev
+
+# Builder pour la production
+npm run build
+
+# Prévisualiser le build
+npm run preview
+```
+
+## Déploiement
+
+Plateforme : **Cloudflare Pages**
+Déploiement : manuel via CLI
+
+```bash
+npm run build
+npx wrangler pages deploy dist --project-name=expria
+```
+
+Domaine : `expria.app`
+
+## Dépôt lié
+
+Backend : `https://codeberg.org/Hermann_Kitio/expria-backend`
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
new file mode 100644
index 0000000..c72453e
--- /dev/null
+++ b/docs/ARCHITECTURE.md
@@ -0,0 +1,556 @@
+# ARCHITECTURE.md — Expria / Coach TCF Canada
+
+> **Document de référence — Version 1.1**
+> Ce document décrit l'architecture technique complète du projet.
+> Toute décision architecturale majeure doit être documentée ici avant d'être implémentée.
+> À lire conjointement avec PLANS_TARIFAIRES.md et PARCOURS_UTILISATEURS.md.
+
+---
+
+## 1. Vue d'ensemble
+
+Expria est une application web SaaS de coaching TCF Canada, construite sur une architecture
+frontend / backend séparés, avec une pièce dédiée pour la fonctionnalité temps réel (T2 EO live).
+
+```
+Utilisateur (navigateur)
+ ↓
+┌─────────────────────────────┐
+│ FRONTEND │
+│ React + Vite │
+│ Cloudflare Pages (gratuit) │
+└─────────────┬───────────────┘
+ │ appels API REST
+┌─────────────▼───────────────┐
+│ BACKEND │
+│ Hono.js (Node.js) │
+│ Render (Frankfurt) │
+│ — toutes les routes API │
+│ — WebSocket proxy T2 EO │
+└──────┬──────────────┬───────┘
+ │ │
+┌──────▼──────┐ ┌────▼────────────────┐
+│ Supabase │ │ APIs externes │
+│ PostgreSQL │ │ — DeepSeek (EE/EO) │
+│ Auth │ │ — Gemini (audio) │
+│ Storage │ │ — Stripe (paiement)│
+└─────────────┘ └─────────────────────┘
+```
+
+---
+
+## 2. Décisions architecturales
+
+### Pourquoi frontend séparé (React + Vite) et non Next.js
+
+Next.js est un framework full-stack qui mélange frontend et backend dans le même projet.
+Ce mélange a été la principale source de chaos dans la version précédente d'Expria :
+régressions fréquentes, difficultés à tester, couplage fort entre affichage et logique métier.
+
+React + Vite est un frontend pur — il ne fait qu'afficher et appeler des API.
+Il ne connaît pas Supabase, ne connaît pas Gemini, ne connaît pas Stripe.
+Il reçoit des données depuis le backend et les affiche. C'est tout.
+
+### Pourquoi Hono.js pour le backend
+
+Hono est un framework backend moderne, léger, TypeScript natif, conçu pour Node.js.
+Il offre une structure claire de routes, middlewares, et contrôleurs — similaire à Express
+mais plus moderne et mieux typé. Il tourne sur Render sans configuration particulière.
+
+### Pourquoi Render pour le backend (et non Cloudflare Workers)
+
+La T2 EO live nécessite une connexion WebSocket persistante entre le serveur et Gemini Live API,
+pouvant durer plusieurs minutes. Cloudflare Workers impose une durée d'exécution maximale
+incompatible avec cette contrainte. Render supporte nativement les connexions WebSocket longue durée.
+
+### Pourquoi Cloudflare Pages pour le frontend
+
+Le frontend est un ensemble de fichiers statiques (HTML, CSS, JS). Cloudflare Pages les distribue
+depuis un CDN mondial — latence minimale pour les utilisateurs en Algérie, Maroc, Cameroun.
+Tier gratuit, déploiement automatique depuis GitHub.
+
+### Pourquoi Supabase est conservé
+
+Supabase fournit trois services critiques déjà en production :
+- Authentification complète (email, OAuth Google/Apple, sessions JWT)
+- Base de données PostgreSQL avec Row Level Security
+- Stockage de fichiers (enregistrements audio EO)
+
+Remplacer Supabase par une Render DB nue demanderait de recoder l'authentification
+et les politiques de sécurité de zéro — un coût disproportionné sans bénéfice.
+
+---
+
+## 3. Structure des dépôts
+
+Deux dépôts GitHub séparés :
+
+```
+expria-frontend/ → React + Vite
+expria-backend/ → Hono.js
+```
+
+Supabase est géré via son propre dashboard et CLI (migrations SQL versionnées).
+
+### Pourquoi deux dépôts séparés
+
+- Claude Code travaille sur un périmètre clair : frontend OU backend, jamais les deux en même temps
+- Une modification frontend ne peut pas casser le backend par accident
+- Déploiements indépendants : on peut déployer le frontend sans toucher au backend
+
+---
+
+## 4. Structure des dossiers
+
+### Frontend — expria-frontend/
+
+```
+expria-frontend/
+├── public/ # Assets statiques (favicon, images)
+├── src/
+│ ├── api/ # Fonctions d'appel API (une par domaine)
+│ │ ├── auth.ts # Appels auth (login, logout, register)
+│ │ ├── simulations.ts # Appels simulations (créer, récupérer)
+│ │ ├── corrections.ts # Appels corrections (rapport, score)
+│ │ ├── plans.ts # Appels plans (upgrade, status)
+│ │ └── stripe.ts # Appels paiement (checkout, prorata)
+│ ├── components/ # Composants React réutilisables
+│ │ ├── ui/ # Composants génériques (Button, Modal, Badge)
+│ │ ├── simulation/ # Composants spécifiques aux simulations
+│ │ ├── rapport/ # Composants d'affichage des rapports
+│ │ ├── dashboard/ # Composants du dashboard utilisateur
+│ │ └── paywall/ # Composants de blocage / upgrade
+│ ├── pages/ # Pages de l'application (une par route)
+│ │ ├── Home.tsx # Page d'accueil (visiteur non connecté)
+│ │ ├── Login.tsx # Connexion / inscription
+│ │ ├── Dashboard.tsx # Dashboard utilisateur (tous plans)
+│ │ ├── Simulation.tsx # Interface de simulation (EE + EO)
+│ │ ├── Rapport.tsx # Affichage d'un rapport de correction
+│ │ ├── Methodologie.tsx # Page méthodologie
+│ │ ├── Tarifs.tsx # Page tarifaire
+│ │ └── T2Live.tsx # Interface T2 EO live (Premium uniquement)
+│ ├── hooks/ # Hooks React personnalisés
+│ │ ├── useAuth.ts # Gestion de l'authentification
+│ │ ├── usePlan.ts # Lecture du plan actuel + permissions
+│ │ ├── useSimulation.ts # Logique de simulation
+│ │ └── useT2Live.ts # Connexion WebSocket T2 live
+│ ├── lib/
+│ │ ├── access.ts # Source de vérité des permissions par plan
+│ │ ├── supabase.ts # Client Supabase (auth uniquement côté frontend)
+│ │ └── constants.ts # Constantes globales (URLs, config)
+│ ├── types/ # Types TypeScript partagés
+│ │ ├── plans.ts # Types Plan, Permission
+│ │ ├── simulation.ts # Types Simulation, Tache, Production
+│ │ └── rapport.ts # Types Rapport, Critere, Score
+│ ├── App.tsx # Router principal
+│ └── main.tsx # Point d'entrée
+├── .env.example # Variables d'environnement (sans valeurs)
+├── vite.config.ts
+├── tsconfig.json
+└── package.json
+```
+
+### Backend — expria-backend/
+
+```
+expria-backend/
+├── src/
+│ ├── routes/ # Définition des routes API (une par domaine)
+│ │ ├── auth.ts # POST /auth/verify-token
+│ │ ├── simulations.ts # POST /simulations, GET /simulations/:id
+│ │ ├── corrections.ts # POST /corrections/ee, POST /corrections/eo
+│ │ ├── plans.ts # GET /plans/status, POST /plans/upgrade
+│ │ ├── stripe.ts # POST /stripe/checkout, POST /stripe/webhook
+│ │ └── t2live.ts # WebSocket /t2/live (T2 EO proxy Gemini)
+│ ├── controllers/ # Logique métier (une par domaine)
+│ │ ├── simulationController.ts
+│ │ ├── correctionController.ts
+│ │ ├── planController.ts
+│ │ ├── stripeController.ts
+│ │ └── t2LiveController.ts
+│ ├── middleware/ # Middlewares Hono
+│ │ ├── auth.ts # Vérification JWT Supabase
+│ │ ├── plan.ts # Vérification des permissions par plan
+│ │ └── rateLimit.ts # Limitation des appels API
+│ ├── lib/
+│ │ ├── supabase.ts # Client Supabase admin (service role)
+│ │ ├── deepseek.ts # Client DeepSeek (corrections EE/EO)
+│ │ ├── gemini.ts # Client Gemini (transcription audio)
+│ │ ├── stripe.ts # Client Stripe
+│ │ └── access.ts # Copie de la source de vérité des plans
+│ ├── types/ # Types TypeScript partagés
+│ └── index.ts # Point d'entrée Hono
+├── .env.example
+├── tsconfig.json
+└── package.json
+```
+
+---
+
+## 5. Tables Supabase
+
+### Table : profiles
+
+Créée automatiquement à l'inscription. Liée à `auth.users`.
+
+```sql
+CREATE TABLE profiles (
+ id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
+ email TEXT NOT NULL,
+ plan TEXT NOT NULL DEFAULT 'free'
+ CHECK (plan IN ('free', 'standard', 'premium')),
+ simulations_used INTEGER NOT NULL DEFAULT 0,
+ stripe_customer_id TEXT,
+ stripe_subscription_id TEXT,
+ plan_expires_at TIMESTAMPTZ,
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ updated_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- Trigger : mise à jour automatique de updated_at
+CREATE OR REPLACE FUNCTION update_updated_at()
+RETURNS TRIGGER AS $$
+BEGIN NEW.updated_at = NOW(); RETURN NEW; END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER profiles_updated_at
+BEFORE UPDATE ON profiles
+FOR EACH ROW EXECUTE FUNCTION update_updated_at();
+
+-- RLS : chaque utilisateur ne voit que son propre profil
+ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
+
+CREATE POLICY "Utilisateur voit son profil"
+ON profiles FOR SELECT
+USING (auth.uid() = id);
+
+CREATE POLICY "Utilisateur modifie son profil"
+ON profiles FOR UPDATE
+USING (auth.uid() = id);
+```
+
+### Table : productions
+
+Enregistrement de chaque simulation soumise pour correction.
+
+```sql
+CREATE TABLE productions (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
+ tache TEXT NOT NULL
+ CHECK (tache IN ('EE_T1','EE_T2','EE_T3','EO_T1','EO_T3','EO_T2_LIVE')),
+ mode TEXT NOT NULL
+ CHECK (mode IN ('entrainement', 'examen')),
+ contenu TEXT, -- texte pour EE, transcript pour EO
+ audio_url TEXT, -- URL Supabase Storage (EO uniquement)
+ score NUMERIC(4,1), -- score /20
+ nclc INTEGER, -- niveau NCLC estimé
+ rapport JSONB, -- rapport complet (critères, erreurs, tips)
+ created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- Index pour les requêtes fréquentes
+CREATE INDEX productions_user_id_idx ON productions(user_id);
+CREATE INDEX productions_created_at_idx ON productions(created_at DESC);
+
+-- RLS
+ALTER TABLE productions ENABLE ROW LEVEL SECURITY;
+
+CREATE POLICY "Utilisateur voit ses productions"
+ON productions FOR SELECT
+USING (auth.uid() = user_id);
+
+CREATE POLICY "Utilisateur crée ses productions"
+ON productions FOR INSERT
+WITH CHECK (auth.uid() = user_id);
+```
+
+### Table : pattern_analyses
+
+Résultats de l'analyse des patterns (Premium — 5 dernières productions).
+
+```sql
+CREATE TABLE pattern_analyses (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
+ productions_ids UUID[] NOT NULL, -- IDs des 5 productions analysées
+ patterns JSONB NOT NULL, -- erreurs récurrentes détectées
+ exercises JSONB NOT NULL, -- exercices long terme générés
+ preparation_index INTEGER, -- indice de préparation 0-100
+ created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+ALTER TABLE pattern_analyses ENABLE ROW LEVEL SECURITY;
+
+CREATE POLICY "Utilisateur voit ses analyses"
+ON pattern_analyses FOR SELECT
+USING (auth.uid() = user_id);
+```
+
+---
+
+## 6. Routes API backend
+
+### Authentification
+```
+POST /auth/verify-token Vérifie le JWT Supabase, retourne le profil + plan
+```
+
+### Simulations
+```
+POST /simulations Crée une simulation, vérifie les quotas selon le plan
+GET /simulations/:id Récupère une simulation par ID
+GET /simulations Liste les simulations de l'utilisateur connecté
+```
+
+### Corrections
+```
+POST /corrections/ee Soumet une production EE pour correction (DeepSeek)
+POST /corrections/eo Soumet une production EO pour correction (Gemini)
+```
+
+### Plans
+```
+GET /plans/status Retourne le plan actuel + permissions de l'utilisateur
+POST /plans/upgrade Crée une session Stripe Checkout (nouveau abonnement)
+POST /plans/upgrade-prorata Upgrade en cours d'abonnement (prorata Stripe)
+```
+
+### Stripe
+```
+POST /stripe/checkout Crée une Checkout Session Stripe
+POST /stripe/webhook Reçoit les events Stripe (checkout, invoice, deleted)
+```
+
+### T2 EO Live
+```
+WS /t2/live WebSocket — proxy Gemini Live API (Premium uniquement)
+```
+
+---
+
+## 7. Flux de données clés
+
+### Flux : Simulation EE (mode entraînement)
+
+```
+1. Frontend → POST /simulations (créer la simulation, vérifier quota)
+2. Backend → Supabase profiles (vérifier plan + simulations_used)
+3. Backend → POST /corrections/ee (envoyer à DeepSeek)
+4. DeepSeek → Backend (rapport JSON)
+5. Backend → Supabase productions (enregistrer production + rapport)
+6. Backend → Frontend (retourner le rapport)
+7. Frontend → Afficher selon le plan (rapport complet ou flouté)
+```
+
+### Flux : Paiement nouveau abonnement
+
+```
+1. Frontend → POST /stripe/checkout (créer Checkout Session)
+2. Backend → Stripe API (session avec price_id)
+3. Stripe → Frontend (redirect vers page paiement Stripe)
+4. Utilisateur paie
+5. Stripe → POST /stripe/webhook (checkout.session.completed)
+6. Backend → Supabase profiles (plan = 'standard' ou 'premium')
+7. Frontend → Redirect dashboard (plan mis à jour)
+```
+
+### Flux : Upgrade Standard → Premium (prorata)
+
+```
+1. Frontend → POST /plans/upgrade-prorata
+2. Backend → Stripe API (subscription.update + preview invoice)
+3. Backend → Frontend (montant prorata calculé)
+4. Frontend → Confirmation utilisateur (affiche le montant exact)
+5. Frontend → POST /plans/upgrade-prorata (confirmation)
+6. Backend → Stripe API (subscription.update confirmé)
+7. Stripe → POST /stripe/webhook (invoice.paid)
+8. Backend → Supabase profiles (plan = 'premium')
+9. Frontend → Dashboard Premium (accès immédiat)
+```
+
+### Flux : T2 EO Live (WebSocket)
+
+```
+1. Frontend → WS /t2/live (connexion WebSocket, JWT vérifié)
+2. Backend → Vérification plan (premium uniquement)
+3. Backend → WS Gemini Live API (ouverture connexion avec GEMINI_API_KEY)
+4. Frontend → Backend → Gemini (audio candidat en temps réel)
+5. Gemini → Backend → Frontend (audio examinateur IA en temps réel)
+6. Fin dialogue
+7. Backend → POST Gemini REST (évaluation de la session)
+8. Backend → Supabase productions (enregistrement production + rapport)
+9. Backend → Frontend (rapport complet)
+```
+
+---
+
+## 8. Variables d'environnement
+
+### Frontend (.env)
+```
+VITE_API_URL=https://api.expria.app # URL du backend Render
+VITE_SUPABASE_URL=https://xxx.supabase.co
+VITE_SUPABASE_ANON_KEY=xxx # Clé publique uniquement
+```
+
+### Backend (.env)
+```
+# Supabase
+SUPABASE_URL=https://xxx.supabase.co
+SUPABASE_SERVICE_ROLE_KEY=xxx # Clé privée — ne jamais exposer
+
+# APIs
+DEEPSEEK_API_KEY=xxx
+GEMINI_API_KEY=xxx # Ne jamais exposer côté frontend
+
+# Stripe
+STRIPE_SECRET_KEY=xxx # Clé privée Stripe
+STRIPE_WEBHOOK_SECRET=xxx # Secret webhook Stripe
+STRIPE_PRICE_STANDARD=price_xxx # price_id plan Standard
+STRIPE_PRICE_PREMIUM=price_xxx # price_id plan Premium
+
+# Config
+PORT=3000
+APP_URL=https://expria.app
+API_URL=https://api.expria.app
+NODE_ENV=production
+```
+
+> **Règle absolue :** aucune clé privée (SUPABASE_SERVICE_ROLE_KEY, GEMINI_API_KEY,
+> STRIPE_SECRET_KEY) ne doit jamais se retrouver dans le frontend ou dans un commit Git.
+
+---
+
+## 9. Déploiement
+
+### Contexte — Contrainte d'hébergement Git
+
+GitHub et GitLab appliquent les sanctions américaines OFAC qui restreignent l'accès
+aux résidents de Crimée. Ces plateformes ne sont pas utilisables de façon fiable
+pour ce projet.
+
+**Solution actuelle (Phase 1 — MVP) :**
+Codeberg (plateforme européenne, Allemagne) pour l'hébergement Git privé.
+Render ne supporte pas l'auto-deploy depuis Codeberg — le déploiement est donc manuel.
+
+**Évolution prévue (Phase 2 — après premiers revenus) :**
+Migration vers un VPS Hetzner (3,29€/mois) avec Coolify.
+Coolify supporte l'auto-deploy depuis n'importe quel serveur Git privé (Codeberg, Gitea).
+Cette migration supprime la dépendance à Render et restaure l'auto-deploy complet.
+
+---
+
+### Hébergement Git — Codeberg (Phase 1)
+
+- Plateforme : codeberg.org (Allemagne — hors juridiction américaine)
+- Dépôts : privés
+- Dépôt frontend : `https://codeberg.org/Hermann_Kitio/expria-frontend`
+- Dépôt backend : `https://codeberg.org/Hermann_Kitio/expria-backend`
+- Dépôt archive (ancienne version) : `https://codeberg.org/Hermann_Kitio/Expria`
+- Limitation : pas d'auto-deploy natif vers Render
+
+### Frontend — Cloudflare Pages
+
+- Source : dépôt Codeberg `expria-frontend`
+- Build command : `npm run build`
+- Output directory : `dist`
+- Domaine : `expria.app` (DNS pointé depuis Vercel vers Cloudflare Pages)
+- Déploiement : **manuel via Cloudflare Pages CLI**
+
+```bash
+# Commande de déploiement frontend
+npm run build
+npx wrangler pages deploy dist --project-name=expria
+```
+
+### Backend — Render
+
+- Source : dépôt Codeberg `expria-backend`
+- Type : Web Service (Node.js)
+- Région : Frankfurt (EU) — proximité utilisateurs Afrique du Nord
+- Build command : `npm run build`
+- Start command : `npm start`
+- Domaine : `api.expria.app`
+- WebSocket : activé nativement sur Render
+- Déploiement : **manuel via Render CLI ou dashboard**
+
+```bash
+# Commande de déploiement backend
+# Option 1 : via Render CLI
+render deploy
+
+# Option 2 : via dashboard Render
+# → Manual Deploy → Deploy latest commit
+```
+
+### Base de données — Supabase
+
+- Région : Frankfurt (EU)
+- Migrations : versionnées dans `supabase/migrations/`
+- Déploiement : `supabase db push` (manuel, après validation)
+
+### Procédure de déploiement complète (Phase 1)
+
+```
+1. Tester localement (npm run test — tous les tests verts)
+2. Rejouer le Golden Dataset
+3. Commit + push sur Codeberg (branche main)
+4. Déployer le backend : render deploy (ou dashboard Render)
+5. Déployer le frontend : npm run build && npx wrangler pages deploy dist
+6. Vérifier les URLs de production (expria.app + api.expria.app)
+7. Rejouer le Smoke Test (Groupe Z du Golden Dataset)
+```
+
+### Évolution Phase 2 — VPS Hetzner + Coolify
+
+Quand Expria génère ses premiers revenus, migrer vers :
+
+```
+Codeberg (Git privé — inchangé)
+ ↓ auto-deploy via webhook
+Coolify sur VPS Hetzner CAX11 (3,29€/mois)
+ — remplace Render pour le backend
+ — auto-deploy natif depuis Codeberg
+ — Docker, SSL automatique, logs intégrés
+ ↓
+Supabase (inchangé)
+```
+
+Avantages de la Phase 2 :
+- Auto-deploy restauré (push → déploiement automatique)
+- Coût réduit (3,29€/mois vs Render Starter)
+- Aucune dépendance à une plateforme américaine pour le backend
+- Cloudflare Pages reste pour le frontend (gratuit, CDN mondial)
+
+---
+
+## 10. Règles de développement
+
+### Règle 1 — Séparation stricte
+Le frontend ne contient aucune logique métier.
+Il appelle le backend et affiche ce qu'il reçoit.
+Toute vérification de plan, de quota, de droit d'accès se fait côté backend.
+
+### Règle 2 — Source de vérité unique des plans
+`lib/access.ts` existe dans les deux dépôts (frontend et backend).
+Le fichier doit être identique dans les deux.
+Toute modification des plans tarifaires met à jour ce fichier en premier,
+dans les deux dépôts, avant tout autre changement de code.
+
+### Règle 3 — Jamais plus de 3 fichiers touchés par session Claude
+Si une modification nécessite de toucher plus de 3 fichiers,
+elle doit être découpée en plusieurs sessions avec validation intermédiaire.
+
+### Règle 4 — Plan avant code
+Claude Code ne commence jamais à coder sans avoir d'abord produit
+un plan détaillé (fichiers impactés, risques, étapes).
+Le plan est validé par Hermann avant l'exécution.
+
+### Règle 5 — Tests manuels après chaque session
+Après chaque session Claude Code, rejouer le golden dataset
+(voir GOLDEN_DATASET.md) avant de passer à la session suivante.
+
+### Règle 6 — Variables d'environnement
+Aucune valeur de variable d'environnement n'est jamais écrite en dur dans le code.
+Toujours lire depuis `process.env` (backend) ou `import.meta.env` (frontend).
diff --git a/docs/DEVELOPMENT_PRINCIPLES.md b/docs/DEVELOPMENT_PRINCIPLES.md
new file mode 100644
index 0000000..263399c
--- /dev/null
+++ b/docs/DEVELOPMENT_PRINCIPLES.md
@@ -0,0 +1,422 @@
+# DEVELOPMENT_PRINCIPLES.md — Expria / Coach TCF Canada
+
+> **Document de référence — Version 1.0**
+> Ce document définit les règles que Claude Code doit lire et respecter
+> à chaque session de développement, sans exception.
+> Il est conçu pour un projet maintenu par un fondateur non-technique
+> assisté par l'IA.
+
+---
+
+## INSTRUCTION OBLIGATOIRE POUR CLAUDE CODE
+
+**Avant d'écrire la moindre ligne de code, tu dois :**
+
+1. Lire ce fichier en entier
+2. Lire ARCHITECTURE.md
+3. Lire PLANS_TARIFAIRES.md
+4. Annoncer : "Documents lus. Voici mon plan pour cette session."
+5. Produire un plan détaillé et attendre la validation
+
+**Tu ne passes à l'implémentation que quand Hermann dit "GO".**
+
+---
+
+## 1. Le cycle de travail obligatoire
+
+Chaque modification, petite ou grande, suit ce cycle sans exception :
+
+```
+ÉTAPE 1 — ANALYSE
+ Lire les fichiers concernés
+ Identifier tous les fichiers qui seront impactés
+ Identifier les risques de régression
+
+ÉTAPE 2 — PLAN
+ Produire un plan détaillé :
+ — Fichiers modifiés (liste exhaustive)
+ — Fichiers créés (liste exhaustive)
+ — Fichiers supprimés (liste exhaustive)
+ — Risques identifiés
+ — Étapes dans l'ordre
+ Attendre la validation de Hermann
+
+ÉTAPE 3 — IMPLÉMENTATION
+ Exécuter le plan validé
+ Ne pas s'écarter du plan sans signaler le changement
+ Maximum 3 fichiers touchés par étape
+
+ÉTAPE 4 — VÉRIFICATION
+ Lancer npm run test (backend)
+ Annoncer le résultat : "X tests passés, 0 échecs"
+ Si un test échoue : STOP — corriger avant de continuer
+
+ÉTAPE 5 — RÉSUMÉ
+ Lister exactement ce qui a été modifié
+ Indiquer les tests manuels à rejouer (référence au Golden Dataset)
+```
+
+---
+
+## 2. Règles absolues — ne jamais enfreindre
+
+### Règle A — Plan avant code
+Claude Code ne commence jamais à coder sans plan validé.
+Même pour une modification "simple" d'une ligne.
+La simplicité apparente est trompeuse dans un projet avec des permissions par plan.
+
+### Règle B — Maximum 3 fichiers par étape
+Si une tâche nécessite de modifier plus de 3 fichiers,
+elle est découpée en plusieurs étapes avec validation intermédiaire.
+Chaque étape est testée avant de passer à la suivante.
+
+### Règle C — Tests verts avant de continuer
+Après chaque étape d'implémentation :
+`npm run test` doit retourner 0 échec.
+Si un test échoue, on corrige avant de passer à l'étape suivante.
+On ne livre jamais une étape avec des tests rouges.
+
+### Règle D — Jamais de logique de plan en dur dans le code
+Toutes les vérifications de permissions lisent depuis `lib/access.ts`.
+Exemple interdit :
+```typescript
+// ❌ JAMAIS
+if (user.plan === 'premium') { ... }
+if (user.plan !== 'free') { ... }
+```
+Exemple correct :
+```typescript
+// ✅ TOUJOURS
+const perms = getPlanPermissions(user.plan)
+if (perms.exam_mode) { ... }
+```
+
+### Règle E — Jamais de clé privée dans le frontend
+Les variables suivantes n'existent que dans le backend :
+- `SUPABASE_SERVICE_ROLE_KEY`
+- `GEMINI_API_KEY`
+- `DEEPSEEK_API_KEY`
+- `STRIPE_SECRET_KEY`
+- `STRIPE_WEBHOOK_SECRET`
+
+Le frontend n'a accès qu'à :
+- `VITE_SUPABASE_ANON_KEY` (clé publique Supabase)
+- `VITE_API_URL` (URL du backend)
+
+### Règle F — Jamais de modification de la base de données sans migration SQL
+Toute modification du schéma Supabase passe par un fichier de migration
+dans `supabase/migrations/`.
+Jamais de modification directe dans le dashboard Supabase sans fichier de migration correspondant.
+
+### Règle G — Jamais de suppression de données utilisateur
+Aucune opération `DELETE` sur les tables `productions` ou `profiles`
+sauf dans les scripts de test explicitement balisés.
+En cas de doute : archiver, ne pas supprimer.
+
+### Règle H — Signaler tout écart par rapport au plan
+Si pendant l'implémentation Claude Code réalise que le plan doit être modifié,
+il STOP, signale le changement, explique pourquoi, et attend une nouvelle validation.
+Il ne prend jamais de décision architecturale de sa propre initiative.
+
+---
+
+## 3. Structure du code — conventions
+
+### Nommage des fichiers
+```
+frontend/src/
+ pages/ → PascalCase.tsx (Dashboard.tsx, Simulation.tsx)
+ components/ → PascalCase.tsx (RapportCard.tsx, PaywallModal.tsx)
+ hooks/ → camelCase.ts (useAuth.ts, usePlan.ts)
+ api/ → camelCase.ts (simulations.ts, corrections.ts)
+ lib/ → camelCase.ts (access.ts, supabase.ts)
+ types/ → camelCase.ts (plans.ts, simulation.ts)
+
+backend/src/
+ routes/ → camelCase.ts (simulations.ts, stripe.ts)
+ controllers/ → camelCase.ts (simulationController.ts)
+ middleware/ → camelCase.ts (auth.ts, plan.ts)
+ lib/ → camelCase.ts (access.ts, deepseek.ts)
+ __tests__/ → camelCase.test.ts (canUserSimulate.test.ts)
+```
+
+### Nommage des variables et fonctions
+```typescript
+// Variables : camelCase
+const userPlan = 'premium'
+const simulationsUsed = 5
+
+// Fonctions : camelCase, verbe en premier
+function getUserPlan(userId: string) { }
+function canUserSimulate(profile: Profile) { }
+function updateUserPlan(userId: string, plan: Plan) { }
+
+// Types et interfaces : PascalCase
+type Plan = 'free' | 'standard' | 'premium'
+interface UserProfile { }
+interface SimulationResult { }
+
+// Constantes globales : SCREAMING_SNAKE_CASE
+const MAX_FREE_SIMULATIONS = 5
+const PLANS = { free: {...}, standard: {...}, premium: {...} }
+```
+
+### Structure d'une route Hono (backend)
+```typescript
+// Pattern obligatoire pour toutes les routes
+app.post('/simulations', authMiddleware, planMiddleware('simulation'), async (c) => {
+ // 1. Récupérer et valider les données entrantes
+ const body = await c.req.json()
+
+ // 2. Logique métier (déléguer au controller)
+ const result = await simulationController.create(body, c.get('user'))
+
+ // 3. Retourner la réponse
+ return c.json(result, 201)
+})
+```
+
+### Structure d'un composant React (frontend)
+```typescript
+// Pattern obligatoire pour tous les composants
+interface Props {
+ // Toujours typer les props
+}
+
+export function NomDuComposant({ prop1, prop2 }: Props) {
+ // 1. Hooks en premier
+ const { user } = useAuth()
+ const perms = usePlan()
+
+ // 2. Handlers
+ const handleClick = () => { }
+
+ // 3. Rendu conditionnel selon le plan
+ if (!perms.dashboard) return
+
+ // 4. JSX
+ return (
+
...
+ )
+}
+```
+
+---
+
+## 4. Gestion des erreurs
+
+### Backend — toujours retourner une erreur structurée
+```typescript
+// ✅ Format d'erreur standard
+return c.json({
+ error: true,
+ code: 'QUOTA_REACHED',
+ message: 'Quota de simulations atteint pour ce plan',
+}, 403)
+
+// ❌ Jamais
+return c.json({ error: 'error' }, 500)
+throw new Error('Something went wrong')
+```
+
+### Codes d'erreur standardisés
+```
+AUTH_REQUIRED → 401 Pas de JWT ou JWT invalide
+PLAN_INSUFFICIENT → 403 Feature non disponible pour ce plan
+QUOTA_REACHED → 403 Quota de simulations épuisé
+INVALID_PLAN → 400 Valeur de plan inconnue
+SIMULATION_NOT_FOUND → 404 Simulation inexistante ou non accessible
+STRIPE_WEBHOOK_INVALID → 400 Signature webhook invalide
+INTERNAL_ERROR → 500 Erreur serveur inattendue
+```
+
+### Frontend — toujours gérer les erreurs API
+```typescript
+// ✅ Pattern obligatoire pour tout appel API
+try {
+ const result = await api.simulations.create(data)
+ // succès
+} catch (error) {
+ if (error.code === 'QUOTA_REACHED') {
+ // afficher modal upgrade
+ } else if (error.code === 'PLAN_INSUFFICIENT') {
+ // afficher paywall
+ } else {
+ // afficher message d'erreur générique
+ }
+}
+```
+
+---
+
+## 5. Sécurité — vérifications obligatoires
+
+### Toute route backend qui touche à des données utilisateur doit :
+
+```typescript
+// 1. Vérifier l'authentification
+app.use(authMiddleware) // vérifie le JWT Supabase
+
+// 2. Vérifier le plan si nécessaire
+app.use(planMiddleware('exam_mode')) // vérifie la permission
+
+// 3. Vérifier que la ressource appartient à l'utilisateur
+const simulation = await db.getSimulation(id)
+if (simulation.user_id !== currentUser.id) {
+ return c.json({ error: true, code: 'AUTH_REQUIRED' }, 401)
+}
+```
+
+### La vérification des permissions se fait TOUJOURS côté backend
+Le frontend peut masquer des boutons selon le plan — c'est de l'UX.
+Mais la vérification réelle se fait dans le middleware backend.
+Un utilisateur malveillant peut appeler l'API directement sans passer par le frontend.
+
+---
+
+## 6. Communication avec Hermann
+
+### Format du plan (avant implémentation)
+```
+📋 PLAN — [nom de la tâche]
+
+Fichiers modifiés :
+ - src/routes/simulations.ts (ajouter la vérification de quota)
+ - src/lib/access.ts (ajouter la fonction canUserSimulate)
+
+Fichiers créés :
+ - src/lib/__tests__/canUserSimulate.test.ts
+
+Fichiers supprimés :
+ - aucun
+
+Risques identifiés :
+ - La modification de access.ts peut affecter toutes les routes
+ qui utilisent getPlanPermissions → vérifier après modification
+
+Étapes :
+ 1. Créer la fonction canUserSimulate dans access.ts
+ 2. Écrire le test canUserSimulate.test.ts
+ 3. Ajouter la vérification dans la route POST /simulations
+
+En attente de validation avant de commencer.
+```
+
+### Format du résumé (après implémentation)
+```
+✅ RÉSUMÉ — [nom de la tâche]
+
+Modifié :
+ - src/routes/simulations.ts : ajout vérification quota ligne 42
+ - src/lib/access.ts : ajout fonction canUserSimulate
+
+Créé :
+ - src/lib/__tests__/canUserSimulate.test.ts (7 tests)
+
+Tests : 41/41 passés ✅
+
+Tests manuels à rejouer :
+ - Golden Dataset Groupe 2 (B2, B7) — quota Free
+ - Golden Dataset Groupe 3 (C2) — illimité Standard
+```
+
+### Quand Claude Code doit s'arrêter et demander
+- Un test automatisé échoue après modification
+- La tâche nécessite de modifier plus de 3 fichiers par étape
+- Une décision architecturale non documentée est requise
+- Une ambiguïté sur le comportement attendu selon un plan tarifaire
+- Une clé privée serait nécessaire côté frontend
+
+---
+
+## 7. Checklist de démarrage de session
+
+Avant chaque session Claude Code, vérifier :
+
+```
+[ ] Les 4 documents de référence ont été lus
+ (DEVELOPMENT_PRINCIPLES.md, ARCHITECTURE.md,
+ PLANS_TARIFAIRES.md, PARCOURS_UTILISATEURS.md)
+
+[ ] L'environnement de test est dans l'état attendu
+ (voir TEST_ENVIRONMENT.md — script de réinitialisation)
+
+[ ] Les tests automatisés sont tous verts
+ (npm run test → 0 échec)
+
+[ ] Un commit Git propre existe avant de commencer
+ (état de repli en cas de régression)
+
+[ ] La tâche de la session est clairement définie
+ (une seule tâche par session)
+```
+
+---
+
+## 8. Checklist de fin de session
+
+Avant de clôturer chaque session Claude Code :
+
+```
+[ ] Les tests automatisés sont tous verts
+ (npm run test → 0 échec)
+
+[ ] Le résumé de session a été produit
+ (fichiers modifiés, tests passés)
+
+[ ] Les tests manuels du Golden Dataset ont été rejoués
+ (groupes concernés par les modifications)
+
+[ ] Un commit Git a été fait avec un message clair
+ Format : "feat: [description]" ou "fix: [description]"
+ Exemples :
+ "feat: ajout vérification quota simulations free"
+ "fix: correction rapport flouté plan découverte"
+ "refactor: extraction logique permissions dans access.ts"
+
+[ ] DEVELOPMENT_PRINCIPLES.md et ARCHITECTURE.md
+ sont à jour si une décision a changé
+```
+
+---
+
+## 9. Messages d'erreur utilisateur — ton et format
+
+Les messages affichés à l'utilisateur doivent être :
+- En français
+- Clairs et non techniques
+- Orientés action (que faire ensuite)
+- Non condescendants
+
+```
+// ✅ Bons messages
+"Vous avez utilisé vos 5 simulations gratuites.
+ Passez en Standard pour continuer votre préparation."
+
+"Le mode Examen est réservé au plan Premium.
+ Passez en Premium pour vous entraîner en conditions réelles."
+
+"Une erreur est survenue. Veuillez réessayer dans quelques instants."
+
+// ❌ Mauvais messages
+"Erreur 403 : quota_reached"
+"Vous n'êtes pas autorisé à effectuer cette action."
+"Internal server error"
+```
+
+---
+
+## 10. Ce que Claude Code ne doit jamais faire
+
+```
+❌ Modifier lib/access.ts sans signaler que TOUTES les permissions sont impactées
+❌ Ajouter une dépendance npm sans demander la validation
+❌ Modifier le schéma Supabase directement dans le dashboard
+❌ Écrire une logique de plan en dur (if plan === 'premium')
+❌ Exposer une clé privée dans le frontend
+❌ Supprimer des données utilisateur (productions, profiles)
+❌ Modifier plus de 3 fichiers sans validation intermédiaire
+❌ Passer à l'étape suivante si un test est rouge
+❌ Prendre une décision architecturale sans la documenter
+❌ Coder sans plan validé, même pour "juste une petite modification"
+```
diff --git a/docs/GOLDEN_DATASET.md b/docs/GOLDEN_DATASET.md
new file mode 100644
index 0000000..e9c6cb4
--- /dev/null
+++ b/docs/GOLDEN_DATASET.md
@@ -0,0 +1,178 @@
+# GOLDEN_DATASET.md — Expria / Coach TCF Canada
+
+> **Document de référence — Version 1.0**
+> Ce fichier contient les tests manuels à rejouer après CHAQUE session Claude Code,
+> avant de passer à la session suivante ou de déployer en production.
+> Un seul test en rouge = la modification est refusée, on revient en arrière.
+
+---
+
+## Principe d'utilisation
+
+1. Avant chaque session Claude Code : sauvegarder le code (commit Git)
+2. Après chaque session Claude Code : rejouer TOUS les tests du groupe concerné
+3. Si un test échoue : ne pas continuer, identifier la régression, corriger d'abord
+4. En cas de doute : rejouer le dataset complet (section 7)
+
+**Comptes de test à créer dans Supabase avant de commencer :**
+
+| Compte | Plan | Usage |
+|---|---|---|
+| test.free@expria.local | free | Tester les parcours Free |
+| test.standard@expria.local | standard | Tester les parcours Standard |
+| test.premium@expria.local | premium | Tester les parcours Premium |
+| test.quota@expria.local | free (5/5 simulations utilisées) | Tester le blocage quota |
+
+> Ces comptes sont créés via script SQL dans Supabase (ne pas les créer manuellement).
+> Voir `supabase/seeds/test_accounts.sql`.
+
+---
+
+## Groupe 1 — Authentification
+
+Ces tests vérifient que l'accès à l'application fonctionne correctement.
+
+| # | Test | Compte | Résultat attendu | ✅ / ❌ |
+|---|---|---|---|---|
+| A1 | Inscription avec email valide | Nouveau compte | Compte créé, plan = "free", redirection dashboard | |
+| A2 | Connexion avec email + mot de passe valides | test.free | Dashboard Free affiché, compteur simulations visible | |
+| A3 | Connexion avec mot de passe incorrect | test.free | Message d'erreur, pas de redirection | |
+| A4 | Déconnexion | test.free | Redirection page d'accueil, session détruite | |
+| A5 | Accès direct à /dashboard sans être connecté | — | Redirection vers /login | |
+
+---
+
+## Groupe 2 — Plan Free (Découverte)
+
+Ces tests vérifient le parcours complet d'un utilisateur Free.
+
+| # | Test | Compte | Résultat attendu | ✅ / ❌ |
+|---|---|---|---|---|
+| B1 | Dashboard Free affiché après connexion | test.free | Compteur "X/5 simulations", aperçu flouté du dashboard Premium visible | |
+| B2 | Lancer une simulation EE Tâche 1 | test.free (quota < 5) | Interface de production affichée, pas de tips | |
+| B3 | Soumettre une production EE | test.free (quota < 5) | Rapport affiché : score + NCLC visibles, critères floutés avec cadenas | |
+| B4 | Vérifier le rapport flouté | test.free | Au moins 1 élément flouté avec mention "Disponible en Standard" | |
+| B5 | Lancer une simulation EO Tâche 1 | test.free (quota < 5) | Interface d'enregistrement audio affichée | |
+| B6 | Cliquer sur EO Tâche 2 live | test.free | Cadenas affiché + message "Exclusivité Premium" | |
+| B7 | Tenter une 6e simulation | test.quota | Modal de blocage avec boutons "Standard" et "Premium" | |
+| B8 | Cliquer "Plus tard" dans le modal de blocage | test.quota | Modal fermé, pas de redirection | |
+| B9 | Cliquer "Mode Examen" | test.free | Cadenas + message "Exclusivité Premium" | |
+
+---
+
+## Groupe 3 — Plan Standard
+
+Ces tests vérifient le parcours complet d'un utilisateur Standard.
+
+| # | Test | Compte | Résultat attendu | ✅ / ❌ |
+|---|---|---|---|---|
+| C1 | Dashboard Standard après connexion | test.standard | Historique visible, bouton "Choisir une tâche" actif, mode examen verrouillé | |
+| C2 | Lancer une simulation EE sans limite | test.standard | Simulation accessible sans vérification de quota | |
+| C3 | Toggle "Suggestions d'idées" activé | test.standard | Suggestions d'idées visibles pendant la simulation | |
+| C4 | Toggle "Mode focus" activé | test.standard | Tips masqués pendant la simulation | |
+| C5 | Soumettre une production EE | test.standard | Rapport complet : score, critères, erreurs, modèle, exercices — rien flouté | |
+| C6 | Vérifier l'enregistrement dans le dashboard | test.standard | Production apparaît dans l'historique avec date, tâche, score | |
+| C7 | Cliquer sur une production dans l'historique | test.standard | Rapport complet de cette production affiché | |
+| C8 | Cliquer sur "Mode Examen" | test.standard | Message "Réservé au plan Premium" + bouton upgrade | |
+| C9 | Cliquer sur "EO Tâche 2 live" | test.standard | Cadenas + message "Exclusivité Premium" | |
+| C10 | Indice de préparation après 5 productions | test.standard (5 prod.) | Section indice visible avec score et message interprétatif | |
+
+---
+
+## Groupe 4 — Plan Premium
+
+Ces tests vérifient le parcours complet d'un utilisateur Premium.
+
+| # | Test | Compte | Résultat attendu | ✅ / ❌ |
+|---|---|---|---|---|
+| D1 | Dashboard Premium après connexion | test.premium | Historique, indice, bouton "Lancer un examen" actif, T2 live accessible | |
+| D2 | Accéder à EO Tâche 2 live | test.premium | Page de préparation T2 affichée, bouton "Démarrer le dialogue" | |
+| D3 | Démarrer le dialogue T2 | test.premium | L'IA prend la parole en premier, audio reçu et joué | |
+| D4 | Répondre en audio (T2) | test.premium | L'IA réagit après la réponse du candidat | |
+| D5 | Fin de dialogue T2 | test.premium | Rapport complet affiché, production enregistrée avec tag "T2 Live" | |
+| D6 | Lancer mode Examen EE | test.premium | Page d'avertissement affichée avant démarrage | |
+| D7 | Confirmer le lancement Examen EE | test.premium | 3 tâches visibles, timer 60:00 démarré, inarrêtable | |
+| D8 | Vérifier le blocage à T=0 (Examen EE) | test.premium | Zone de texte figée, message "Temps écoulé", envoi automatique | |
+| D9 | Lancer mode Examen EO | test.premium | Timer 12:00 démarré, enregistrement actif | |
+| D10 | Analyse des patterns (5+ productions) | test.premium | Section "Mon profil" affiche erreurs récurrentes classées par type | |
+| D11 | Exercices long terme générés | test.premium | Exercices distincts de ceux du rapport individuel | |
+
+---
+
+## Groupe 5 — Paiements et changements de plan
+
+Ces tests vérifient que le système de paiement fonctionne sans régression.
+
+> ⚠️ Ces tests utilisent les cartes de test Stripe.
+> Carte valide : `4242 4242 4242 4242` — expiry : n'importe quelle date future — CVC : n'importe lequel.
+> Carte refusée : `4000 0000 0000 0002`
+
+| # | Test | Compte | Résultat attendu | ✅ / ❌ |
+|---|---|---|---|---|
+| E1 | Upgrade Free → Standard | test.free | Redirection Stripe, paiement, retour dashboard Standard, plan = "standard" | |
+| E2 | Upgrade Free → Premium | test.free | Redirection Stripe, paiement, retour dashboard Premium, plan = "premium" | |
+| E3 | Upgrade Standard → Premium (prorata) | test.standard | Montant prorata affiché avant confirmation, accès Premium après paiement | |
+| E4 | Paiement refusé | test.free | Message d'erreur Stripe, plan inchangé, pas de régression | |
+| E5 | Accès immédiat après paiement | test.free | Dashboard du nouveau plan affiché sans délai ni reconnexion | |
+| E6 | Plan mis à jour dans Supabase | test.free | Colonne `plan` dans la table `profiles` reflète le nouveau plan | |
+
+---
+
+## Groupe 6 — Sécurité et permissions
+
+Ces tests vérifient qu'un utilisateur ne peut pas accéder à ce qu'il ne devrait pas.
+
+| # | Test | Compte | Résultat attendu | ✅ / ❌ |
+|---|---|---|---|---|
+| F1 | Accès direct URL /t2-live sans être Premium | test.standard | Redirection ou message "Accès réservé au Premium" | |
+| F2 | Appel API POST /corrections/ee sans JWT | — (non connecté) | Réponse 401 Unauthorized | |
+| F3 | Appel API GET /simulations d'un autre utilisateur | test.free | Réponse 403 ou liste vide (RLS Supabase) | |
+| F4 | Tentative de lancer examen via API (plan standard) | test.standard | Réponse 403 — plan insuffisant | |
+| F5 | Variable GEMINI_API_KEY visible dans le frontend | — | Introuvable dans le code source JavaScript chargé par le navigateur | |
+
+---
+
+## Groupe 7 — Dataset complet (smoke test)
+
+À rejouer avant chaque déploiement en production.
+Ce sont les 10 scénarios les plus critiques, un par type de parcours.
+
+| # | Test | Description rapide |
+|---|---|---|
+| Z1 | Inscription + simulation Free | Nouvel utilisateur → simulation → rapport flouté |
+| Z2 | Blocage quota Free | 6e simulation → modal de blocage |
+| Z3 | Simulation Standard complète | Login → simulation → rapport complet → dashboard |
+| Z4 | Mode examen bloqué en Standard | Bouton mode examen → message upgrade |
+| Z5 | T2 live Premium | Login → T2 live → dialogue → rapport |
+| Z6 | Mode examen EE complet | Lancement → timer → T=0 → envoi auto → rapport |
+| Z7 | Paiement Free → Standard | Checkout Stripe → webhook → dashboard Standard |
+| Z8 | Prorata Standard → Premium | Montant affiché → confirmation → accès Premium |
+| Z9 | Sécurité JWT | Appel API sans token → 401 |
+| Z10 | Déconnexion + accès protégé | Déconnexion → accès /dashboard → redirection /login |
+
+---
+
+## Procédure en cas d'échec
+
+```
+Test échoue
+ ↓
+1. Noter le numéro du test et le comportement observé
+2. NE PAS continuer la session Claude Code
+3. Identifier le fichier modifié qui a causé la régression
+4. Revenir au commit Git précédent (git revert ou git checkout)
+5. Analyser la cause avec Claude (chat, pas code)
+6. Reformuler le prompt en ajoutant la contrainte manquante
+7. Relancer la session Claude Code
+8. Rejouer le groupe de tests concerné
+```
+
+---
+
+## Historique des sessions
+
+> Remplir après chaque session Claude Code.
+
+| Date | Session | Tests rejoués | Résultat | Notes |
+|---|---|---|---|---|
+| — | — | — | — | — |
diff --git a/docs/PARCOURS_UTILISATEURS.md b/docs/PARCOURS_UTILISATEURS.md
new file mode 100644
index 0000000..6613dd0
--- /dev/null
+++ b/docs/PARCOURS_UTILISATEURS.md
@@ -0,0 +1,440 @@
+# PARCOURS_UTILISATEURS.md — Expria / Coach TCF Canada
+
+> **Document de référence — Version 1.1**
+> Ce document décrit le parcours exact de chaque type d'utilisateur, depuis la page d'accueil.
+> À lire conjointement avec PLANS_TARIFAIRES.md.
+
+---
+
+## 1. Visiteur non connecté
+
+```
+Arrive sur la page d'accueil
+ ↓
+Sections visibles :
+ — Proposition de valeur principale
+ (ce qu'est Expria, pour qui, pourquoi ça marche)
+ — Aperçu des 6 tâches (EE + EO) avec description courte de chacune
+ — Section "Comment ça marche"
+ (3 étapes : tu t'entraînes → tu reçois un rapport → tu progresses)
+ — Témoignages / résultats candidats
+ — Section tarifaire (3 plans visibles, comparatif des features)
+ — CTA principal : "Commencer gratuitement"
+ — CTA secondaire : "Voir les offres"
+```
+
+---
+
+## 2. Parcours — Plan Découverte (Free)
+
+### Inscription
+
+```
+Clique sur "Commencer gratuitement"
+ ↓
+Page d'inscription :
+ — Email + mot de passe
+ — OU connexion Google / Apple
+ ↓
+Compte créé → rôle assigné : "free" dans Supabase
+ ↓
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+DASHBOARD FREE
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+Contenu affiché :
+ — Message de bienvenue + explication du plan Découverte
+ — Compteur : "0/5 simulations utilisées"
+ — Accès au mode entraînement uniquement
+ — Aperçu flouté du dashboard complet (historique, indice de préparation)
+ avec mention "Disponible en Standard"
+ — Bouton "Choisir une tâche"
+```
+
+### Simulation (dans la limite des 5 à vie)
+
+```
+Clique sur "Choisir une tâche"
+ ↓
+Sélection de la tâche :
+ — EE Tâche 1 ✅
+ — EE Tâche 2 ✅
+ — EE Tâche 3 ✅
+ — EO Tâche 1 ✅
+ — EO Tâche 3 ✅
+ — EO Tâche 2 (live) ❌ → cadenas + "Exclusivité Premium"
+ ↓
+Vérification backend : simulations_used < 5 ?
+ — OUI → accès à la simulation
+ — NON → modal de blocage (voir section "Quota atteint")
+ ↓
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+SIMULATION — Mode entraînement
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+ — Interface de production
+ (zone de texte pour EE / enregistrement audio pour EO)
+ — Pas de tips accessibles
+ ↓
+Envoi pour correction
+ ↓
+Compteur mis à jour : simulations_used + 1
+ ↓
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+RAPPORT FREE
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+Affiché :
+ — Score global /20 ✅
+ — Niveau NCLC estimé ✅
+ — Feedback court (2-3 lignes) ✅
+
+Flouté avec cadenas :
+ — Détail par critère → "Disponible en Standard" 🔒
+ — Explication des erreurs → "Disponible en Standard" 🔒
+ — Production modèle → 1 phrase visible + "Voir plus en Standard" 🔒
+ — Suggestions d'idées → 1 phrase visible + "Voir plus en Standard" 🔒
+ — Exercices personnalisés → titre visible + "Disponible en Standard" 🔒
+ ↓
+Bandeau discret en bas du rapport :
+ "Vous avez utilisé X/5 simulations gratuites.
+ Débloquez le rapport complet et l'entraînement illimité."
+ [Voir les offres]
+```
+
+### Quota atteint (6e tentative)
+
+```
+Tentative de lancer une nouvelle simulation
+ ↓
+Modal de blocage :
+ "Vous avez utilisé vos 5 simulations gratuites.
+ Pour continuer votre préparation, choisissez un plan."
+ [Passer en Standard] [Passer en Premium] [Plus tard]
+```
+
+---
+
+## 3. Parcours — Plan Standard
+
+### Abonnement
+
+```
+Arrive sur la page tarifaire
+(depuis la page d'accueil, ou depuis le modal de blocage du plan Free)
+ ↓
+Clique sur "Choisir Standard"
+ ↓
+Stripe Checkout — 19,90€ / 4 semaines
+ ↓
+Paiement réussi
+ ↓
+Webhook Stripe : checkout.session.completed
+ → plan mis à jour : "standard" dans Supabase
+ → redirection vers dashboard Standard
+ ↓
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+DASHBOARD STANDARD
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+Contenu affiché :
+ — Message de bienvenue + confirmation d'accès
+ — Historique des productions (vide au départ)
+ — Indice de préparation (activé après 5 productions)
+ — Accès mode entraînement ✅
+ — Accès mode examen ❌ → cadenas + "Exclusivité Premium"
+ — Bouton "Choisir une tâche"
+```
+
+### Simulation (illimitée)
+
+```
+Clique sur "Choisir une tâche"
+ ↓
+Sélection de la tâche :
+ — EE Tâche 1 ✅
+ — EE Tâche 2 ✅
+ — EE Tâche 3 ✅
+ — EO Tâche 1 ✅
+ — EO Tâche 3 ✅
+ — EO Tâche 2 (live) ❌ → cadenas + "Exclusivité Premium"
+ ↓
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+SIMULATION — Mode entraînement
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+Options activables avant de commencer :
+ — Toggle "Afficher les suggestions d'idées" ✅
+ — Toggle "Mode focus" (masquer les tips — simuler les conditions réelles) ✅
+ ↓
+Production :
+ — Zone de texte (EE) / Enregistrement audio (EO)
+ — Si tips activés : suggestions d'idées disponibles sans quitter la page
+ ↓
+Envoi pour correction
+ ↓
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+RAPPORT STANDARD (complet)
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+Affiché en totalité :
+ — Score /20 + NCLC ✅
+ — Détail par critère ✅
+ — Explication des erreurs détectées ✅
+ — Production modèle basée sur la copie ✅
+ — Exercices personnalisés liés à CETTE production ✅
+ ↓
+Production enregistrée automatiquement dans le dashboard
+ ↓
+Bandeau discret (max 1 fois toutes les 3 sessions) :
+ "Passez en Premium pour vous entraîner en conditions réelles
+ et simuler le dialogue avec l'examinateur."
+ [En savoir plus]
+```
+
+### Dashboard — Consultation
+
+```
+Dashboard Standard
+ ↓
+Liste de toutes les productions (date, tâche, score, NCLC)
+ ↓
+Clique sur une production
+ → Rapport complet correspondant affiché
+ ↓
+Après 5 productions enregistrées :
+ — Indice de préparation affiché ✅
+ (score dynamique basé sur progression + régularité)
+```
+
+### Tentative d'accès aux features Premium
+
+```
+Clique sur "Mode Examen" ou "EO Tâche 2 live"
+ ↓
+Message :
+ "Le mode Examen et la simulation live de la Tâche 2
+ sont réservés au plan Premium."
+ [Passer en Premium] [Pas maintenant]
+```
+
+### Upgrade Standard → Premium en cours d'abonnement (prorata)
+
+```
+Clique sur "Passer en Premium"
+ ↓
+Page d'upgrade — Stripe calcule le prorata en temps réel
+ ↓
+Affichage du montant exact avant confirmation :
+ "Il vous reste X jours sur votre abonnement Standard.
+ Vous paierez [montant calculé]€ aujourd'hui
+ pour accéder au plan Premium jusqu'au [date de fin actuelle]."
+ [Confirmer] [Annuler]
+ ↓
+Confirmation → Stripe débite le montant du prorata
+ ↓
+Webhook Stripe : invoice.paid
+ → plan mis à jour : "premium" dans Supabase
+ → accès Premium immédiat, sans reconnexion
+ ↓
+Dashboard Premium affiché
+ — Message : "Bienvenue en Premium. Votre accès est actif jusqu'au [date]."
+```
+
+> **Règle :** L'utilisateur ne paie jamais le plein tarif Premium si il était déjà abonné Standard.
+> Stripe crédite les jours non consommés du Standard et facture uniquement la différence.
+
+### Expiration de l'abonnement Standard
+
+```
+Webhook Stripe : customer.subscription.deleted
+ → plan remis à "free" dans Supabase
+ ↓
+À la prochaine connexion :
+ — Dashboard Free affiché
+ — Message :
+ "Votre abonnement Standard a expiré.
+ Vos productions sont conservées.
+ Renouvelez pour y accéder à nouveau."
+ — Productions : conservées en base
+ (non accessibles en lecture tant que plan = "free")
+ — Compteur simulations free :
+ reprend là où il s'était arrêté avant l'abonnement
+```
+
+---
+
+## 4. Parcours — Plan Premium
+
+### Abonnement
+
+```
+Arrive sur la page tarifaire
+(depuis la page d'accueil, le dashboard Free, ou le dashboard Standard)
+ ↓
+Clique sur "Choisir Premium" ou "Passer en Premium"
+ ↓
+Stripe Checkout — 39,90€ / 4 semaines
+ ↓
+Paiement réussi
+ ↓
+Webhook Stripe : checkout.session.completed
+ → plan mis à jour : "premium" dans Supabase
+ → redirection vers dashboard Premium
+ ↓
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+DASHBOARD PREMIUM
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+Contenu affiché :
+ — Historique complet des productions
+ — Indice de préparation (actif dès 5 productions)
+ — Analyse des patterns (active dès 5 productions)
+ — Accès mode entraînement ✅
+ — Accès mode examen ✅
+ — Accès EO Tâche 2 live ✅
+ — Bouton "Choisir une tâche"
+ — Bouton "Lancer un examen"
+```
+
+### Simulation entraînement — EO Tâche 2 live
+
+```
+Choisit EO Tâche 2 — Interaction live
+ ↓
+Page de préparation :
+ — Explication du déroulé
+ (l'IA joue le rôle de l'examinateur)
+ — Consigne de la tâche affichée
+ — Bouton "Démarrer le dialogue"
+ ↓
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+SIMULATION LIVE — T2 Expression Orale
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+ — L'IA ouvre le dialogue (première prise de parole de l'examinateur)
+ — Le candidat répond en audio en temps réel
+ — L'IA adapte ses relances selon les réponses du candidat
+ — Durée libre en mode entraînement (pas de timer sur cette tâche)
+ ↓
+Fin du dialogue (candidat ou IA clôture)
+ ↓
+Rapport complet généré (même structure que les autres tâches) ✅
+ ↓
+Production enregistrée dans le dashboard avec tag "T2 Live"
+```
+
+### Mode Examen
+
+```
+Clique sur "Lancer un examen"
+ ↓
+Choix de l'épreuve :
+ — Expression Écrite (60 min)
+ — Expression Orale (12 min)
+ ↓
+Page d'avertissement :
+ "Une fois lancé, le timer ne peut pas être arrêté.
+ Votre production sera envoyée automatiquement
+ à l'expiration du temps, même si vous n'avez pas terminé."
+ [Je comprends — Lancer l'examen] [Annuler]
+```
+
+**Si Expression Écrite :**
+
+```
+ ↓
+3 tâches affichées simultanément sur la même page
+Timer 60:00 visible en permanence (coin supérieur)
+Zone de texte active
+ ↓
+À T=0 :
+ — Zone de saisie figée (readOnly) ⛔
+ — Envoi automatique déclenché ✅
+ — Message : "Temps écoulé. Votre production a été envoyée."
+ ↓
+Rapport complet affiché ✅
+Production enregistrée avec tag "Mode Examen"
+```
+
+**Si Expression Orale :**
+
+```
+ ↓
+Tâches enchaînées (T1 → T3 → T2)
+Timer global 12:00 visible
+Enregistrement audio actif
+ ↓
+À T=0 :
+ — Enregistrement stoppé ⛔
+ — Envoi automatique déclenché ✅
+ — Message : "Temps écoulé. Votre production a été envoyée."
+ ↓
+Rapport complet affiché ✅
+Production enregistrée avec tag "Mode Examen"
+```
+
+### Analyse des patterns
+
+```
+Dashboard → section "Mon profil de préparation"
+ ↓
+Condition : 5 productions enregistrées minimum
+ ↓
+SI condition remplie :
+ ↓
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+ANALYSE DES PATTERNS
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+Analyse automatique des 5 dernières productions
+ ↓
+Affichage des erreurs récurrentes classées par type :
+ — Structurelles (organisation des idées, cohérence)
+ — Lexicales (répétitions, registre, richesse)
+ — Morphosyntaxiques (accords, conjugaison, syntaxe)
+ ↓
+Distinction visuelle :
+ — Erreur ponctuelle (1 occurrence) → signalée
+ — Pattern répété (3+ occurrences sur 5 productions) → mis en évidence
+ ↓
+Exercices long terme générés :
+ — Spécifiques aux patterns détectés
+ — Distincts des exercices du rapport individuel
+ — Renouvelés automatiquement à chaque nouvelle analyse
+ ↓
+Indice de préparation :
+ — Score dynamique 0 → 100
+ — Basé sur : moyenne des scores récents + régularité + courbe de progression
+ — Messages interprétatifs :
+ · < 40 : "Continuez à vous entraîner régulièrement"
+ · 40–70 : "Bonne progression — visez NCLC 7-8"
+ · > 70 : "Vous êtes en bonne voie pour NCLC 9+"
+```
+
+### Expiration de l'abonnement Premium
+
+```
+Webhook Stripe : customer.subscription.deleted
+ → plan remis à "free" dans Supabase
+ ↓
+À la prochaine connexion :
+ — Dashboard Free affiché
+ — Message :
+ "Votre abonnement Premium a expiré.
+ Toutes vos productions sont conservées.
+ Renouvelez pour retrouver l'accès complet."
+ — Productions : conservées en base
+ — Accès mode examen, T2 live, patterns : coupé immédiatement
+ — Compteur simulations free : reprend là où il s'était arrêté
+```
+
+---
+
+## 5. Matrice des upgrades / downgrades
+
+| Depuis → Vers | Action | Montant facturé | Délai | Données |
+|---|---|---|---|---|
+| Free → Standard | Stripe Checkout | 19,90€ | Immédiat après webhook | Conservées |
+| Free → Premium | Stripe Checkout | 39,90€ | Immédiat après webhook | Conservées |
+| Standard → Premium | Prorata Stripe | Différence au prorata | Immédiat après webhook | Conservées |
+| Premium → Standard | Résiliation + nouvel abonnement | 19,90€ | Immédiat après webhook | Conservées |
+| Premium → Free | Résiliation | — | Immédiat après webhook | Conservées |
+| Standard → Free | Résiliation | — | Immédiat après webhook | Conservées |
+
+> **Règle absolue :** les productions ne sont jamais supprimées, quel que soit le changement de plan.
+> L'accès aux features change. Les données restent.
+
+### Détail du prorata Standard → Premium
+Stripe crédite automatiquement les jours non consommés du plan Standard et facture les jours restants au tarif Premium. L'utilisateur voit le montant exact avant de confirmer. Aucun calcul manuel requis côté code — comportement natif de Stripe via `subscription.update()` avec `proration_behavior: 'always_invoice'`.
diff --git a/docs/PLANS_TARIFAIRES.md b/docs/PLANS_TARIFAIRES.md
new file mode 100644
index 0000000..c5febeb
--- /dev/null
+++ b/docs/PLANS_TARIFAIRES.md
@@ -0,0 +1,314 @@
+# PLANS_TARIFAIRES.md — Expria / Coach TCF Canada
+
+> **Document de référence — Version 1.1**
+> Toute modification des plans tarifaires doit être reflétée dans ce fichier en premier.
+> Tout le code, tout prompt à Claude, tout débat sur une feature part d'ici.
+
+---
+
+## 1. Vue d'ensemble
+
+| Feature | Découverte (Free) | Standard | Premium |
+|---|---|---|---|
+| Mode entraînement | ✅ | ✅ | ✅ |
+| Simulations | 5 à vie | Illimitées | Illimitées |
+| EE — Tâches 1, 2, 3 | ✅ | ✅ | ✅ |
+| EO — Tâche 1 et 3 | ✅ | ✅ | ✅ |
+| EO — Tâche 2 live | ❌ | ❌ | ✅ |
+| Rapport basique (score + NCLC) | ✅ | ✅ | ✅ |
+| Rapport détaillé (critères + erreurs) | 🔒 flouté | ✅ | ✅ |
+| Tips (idées, modèle, exercices/production) | 🔒 tronqués | ✅ | ✅ |
+| Historique + dashboard | ❌ | ✅ | ✅ |
+| Mode examen | ❌ | ❌ | ✅ |
+| Analyse patterns (5 dernières prod.) | ❌ | ❌ | ✅ |
+| Exercices long terme | ❌ | ❌ | ✅ |
+| Indice de préparation | ❌ | ❌ | ✅ |
+
+---
+
+## 2. Détail par plan
+
+---
+
+### 🟢 PLAN DÉCOUVERTE — Gratuit
+
+**Rôle :** Acquisition / frustration contrôlée.
+Laisser l'utilisateur goûter le produit et voir que la correction existe, sans lui permettre de construire un entraînement sérieux sur le plan gratuit.
+
+#### Accès
+- Mode entraînement uniquement
+- **5 simulations à vie** (toutes tâches confondues, compteur global, ne se remet jamais à zéro)
+
+#### Expression Écrite
+- Tâches 1, 2, 3 disponibles ✅
+
+#### Expression Orale
+- Tâche 1 et Tâche 3 disponibles ✅
+- Tâche 2 (interaction live) ❌ — cadenas + mention "Exclusivité Premium"
+
+#### Correction
+- Score global /20 ✅
+- Niveau NCLC estimé ✅
+- Feedback court (2-3 lignes) ✅
+- Détail par critère → **flouté** + "Disponible en Standard" 🔒
+- Explication des erreurs → **floutée** 🔒
+
+#### Tips (entraînement)
+- Suggestions d'idées/arguments → 1 phrase visible + "Voir plus en Standard" 🔒
+- Production modèle → 1 phrase visible + "Voir plus en Standard" 🔒
+- Exercices personnalisés → titre visible + "Disponible en Standard" 🔒
+
+#### Données utilisateur
+- Pas d'historique ❌
+- Pas de dashboard ❌
+- Aperçu flouté du dashboard complet avec mention "Disponible en Standard"
+
+#### Mode examen
+- Non disponible ❌
+
+#### Logique backend
+```typescript
+free: {
+ simulations_lifetime: 5,
+ oral_t2_live: false,
+ detailed_report: false, // visible mais flouté côté frontend
+ tips: false, // visible mais tronqué côté frontend
+ dashboard: false,
+ exam_mode: false,
+ pattern_analysis: false,
+ preparation_index: false,
+}
+```
+
+---
+
+### 🔵 PLAN STANDARD — 19,90€ / 4 semaines
+
+**Rôle :** Progression sérieuse.
+Débloquer l'entraînement complet avec correction détaillée et suivi des productions.
+
+#### Accès
+- Mode entraînement illimité ✅
+- Simulations illimitées ✅
+
+#### Expression Écrite
+- Tâches 1, 2, 3 disponibles ✅
+
+#### Expression Orale
+- Tâche 1 et Tâche 3 disponibles ✅
+- Tâche 2 (interaction live) ❌ — cadenas + mention "Exclusivité Premium"
+
+#### Correction
+- Score global /20 ✅
+- Niveau NCLC estimé ✅
+- Détail par critère ✅
+- Explication des erreurs détectées ✅
+
+#### Tips (entraînement)
+- Suggestions d'idées/arguments ✅ (activables via toggle avant la simulation)
+- Production modèle basée sur la copie ✅
+- Exercices personnalisés liés à CETTE production ✅
+- Mode focus disponible (masquer les tips pour simuler les conditions réelles) ✅
+
+#### Données utilisateur
+- Historique complet des productions ✅
+- Dashboard (consultation des copies et rapports passés) ✅
+- Indice de préparation ✅ (activé après 5 productions)
+
+#### Mode examen
+- Non disponible ❌ — cadenas + mention "Exclusivité Premium"
+
+#### Analyse avancée
+- Analyse des patterns multi-productions ❌
+- Exercices long terme ❌
+
+#### Logique backend
+```typescript
+standard: {
+ simulations_lifetime: null, // illimité
+ oral_t2_live: false,
+ detailed_report: true,
+ tips: true,
+ dashboard: true,
+ exam_mode: false,
+ pattern_analysis: false,
+ preparation_index: false,
+}
+```
+
+---
+
+### 🟣 PLAN PREMIUM — 39,90€ / 4 semaines
+
+**Rôle :** Réussite — viser NCLC 9+.
+Tout Standard, plus les outils de simulation réelle et d'intelligence long terme.
+
+#### Accès
+- Tout le plan Standard ✅
+- Mode examen ✅
+- EO Tâche 2 live ✅
+- Analyse avancée multi-productions ✅
+
+#### 🔥 Mode Examen (feature majeure)
+
+**Expression Écrite :**
+- 3 tâches affichées simultanément
+- Timer 60 minutes — inarrêtable ⛔
+- Zone de saisie figée (readOnly) à T=0 ⛔
+- Envoi automatique déclenché à T=0 ✅
+
+**Expression Orale :**
+- 3 tâches enchaînées
+- Timer global 12 minutes — inarrêtable ⛔
+- Enregistrement stoppé et envoi automatique à T=0 ✅
+
+Productions enregistrées dans le dashboard avec tag **"Mode Examen"**.
+
+#### 🔥 EO Tâche 2 — Simulation live (différenciateur clé)
+- Interaction dynamique en temps réel avec l'examinateur IA
+- Le candidat répond en audio
+- L'IA adapte ses relances selon les réponses du candidat
+- Rapport complet généré à la fin du dialogue
+- Productions enregistrées avec tag **"T2 Live"**
+
+> ❗ Exclusivité Premium — aucun concurrent ne propose cette feature.
+
+#### 🔥 Analyse des patterns (intelligence long terme)
+- Analyse automatique des 5 dernières productions
+- Détection des erreurs récurrentes classées par type :
+ - Structurelles (organisation, cohérence)
+ - Lexicales (répétitions, registre, richesse)
+ - Morphosyntaxiques (accords, conjugaison, syntaxe)
+- Distinction : erreur ponctuelle vs pattern répété (3+ occurrences sur 5 productions)
+- Exercices long terme générés sur la base des patterns détectés
+- **Distinction clé :** ces exercices sont différents des exercices du rapport individuel (qui sont spécifiques à une seule production)
+
+#### 🔥 Indice de préparation
+- Score dynamique 0 → 100
+- Basé sur : moyenne des scores récents + régularité des sessions + courbe de progression
+- Messages interprétatifs :
+ - < 40 : "Continuez à vous entraîner régulièrement"
+ - 40–70 : "Bonne progression — visez NCLC 7-8"
+ - \> 70 : "Vous êtes en bonne voie pour NCLC 9+"
+
+#### Logique backend
+```typescript
+premium: {
+ simulations_lifetime: null,
+ oral_t2_live: true,
+ detailed_report: true,
+ tips: true,
+ dashboard: true,
+ exam_mode: true,
+ pattern_analysis: true,
+ preparation_index: true,
+}
+```
+
+---
+
+## 3. Source de vérité unique (lib/access.ts)
+
+```typescript
+export const PLANS = {
+ free: {
+ simulations_lifetime: 5,
+ oral_t2_live: false,
+ detailed_report: false,
+ tips: false,
+ dashboard: false,
+ exam_mode: false,
+ pattern_analysis: false,
+ preparation_index: false,
+ },
+ standard: {
+ simulations_lifetime: null,
+ oral_t2_live: false,
+ detailed_report: true,
+ tips: true,
+ dashboard: true,
+ exam_mode: false,
+ pattern_analysis: false,
+ preparation_index: false,
+ },
+ premium: {
+ simulations_lifetime: null,
+ oral_t2_live: true,
+ detailed_report: true,
+ tips: true,
+ dashboard: true,
+ exam_mode: true,
+ pattern_analysis: true,
+ preparation_index: true,
+ },
+}
+```
+
+> Tout le code lit depuis cet objet. Aucune condition de plan n'est écrite en dur ailleurs dans le code.
+
+---
+
+## 4. Règles transversales
+
+### Upgrades possibles
+- Découverte → Standard
+- Découverte → Premium (accès direct, sans passer par Standard)
+- Standard → Premium
+
+Dans tous les cas : immédiat après confirmation du webhook Stripe, sans délai, sans reconnexion requise.
+
+### Upgrade en cours d'abonnement — Prorata automatique (Stripe)
+
+Quand un utilisateur Standard souhaite passer en Premium avant la fin de sa période, il ne paie **pas le plein tarif Premium**. Stripe calcule automatiquement le prorata :
+
+- Les jours restants du plan Standard sont **crédités**
+- Les jours restants sont **refacturés au tarif Premium**
+- L'utilisateur paie uniquement la **différence de valeur pour le temps restant**
+
+**Exemple :**
+Un utilisateur a payé 19,90€ pour 4 semaines. Il upgrade après 2 semaines.
+Stripe crédite ~10€ (2 semaines non consommées de Standard) et facture ~20€ (2 semaines au tarif Premium).
+→ Il paie ~10€, pas 39,90€.
+
+**Ce que cela implique côté implémentation :**
+- Utiliser `subscription.update()` dans l'API Stripe avec `proration_behavior: 'always_invoice'`
+- Stripe génère automatiquement une facture de prorata et débite le montant exact
+- Le webhook `invoice.paid` confirme le paiement → mettre à jour le plan à `"premium"` dans Supabase
+
+**Ce que l'utilisateur voit dans l'interface :**
+Avant de confirmer l'upgrade, afficher le montant exact calculé par Stripe :
+> "Vous paierez [montant calculé]€ aujourd'hui pour accéder au plan Premium jusqu'au [date de fin]."
+> [Confirmer] [Annuler]
+
+Pas de surprise, pas de frustration.
+
+### Downgrades / Résiliations
+- Premium → Standard
+- Premium → Découverte (résiliation sans remplacement)
+- Standard → Découverte (résiliation sans remplacement)
+
+### Conservation des données
+Les productions sont toujours conservées en base, quel que soit le changement de plan. Seul l'accès aux features change. L'utilisateur ne perd jamais ses données.
+
+### Compteur de simulations Free
+- Global (EE + EO confondus)
+- Ne se remet jamais à zéro
+- Une simulation = un envoi pour correction
+- Si l'utilisateur a consommé 3/5 simulations avant de s'abonner, puis résilie, il lui reste 2 simulations gratuites
+
+### Expiration d'abonnement
+- Déclenchée par le webhook Stripe : `customer.subscription.deleted`
+- Rôle remis à `"free"` dans Supabase
+- Productions conservées, accès features coupé immédiatement
+- Message affiché à la prochaine connexion : "Votre abonnement a expiré. Vos productions sont conservées. Renouvelez pour retrouver l'accès complet."
+
+### Stripe — Produits à créer (mode Test d'abord)
+| Plan | Produit Stripe | Prix |
+|---|---|---|
+| Standard | "Expria Standard" | 19,90€ / 4 semaines |
+| Premium | "Expria Premium" | 39,90€ / 4 semaines |
+
+Webhooks à écouter :
+- `checkout.session.completed` → mettre à jour le plan dans Supabase (nouveau abonnement)
+- `invoice.paid` → mettre à jour le plan dans Supabase (upgrade avec prorata)
+- `customer.subscription.deleted` → remettre le plan à "free"
diff --git a/docs/TESTS_AUTOMATISES.md b/docs/TESTS_AUTOMATISES.md
new file mode 100644
index 0000000..c4661d3
--- /dev/null
+++ b/docs/TESTS_AUTOMATISES.md
@@ -0,0 +1,528 @@
+# TESTS_AUTOMATISES.md — Expria / Coach TCF Canada
+
+> **Document de référence — Version 1.0**
+> Ce document contient les tests Vitest automatisés à implémenter dans le backend.
+> Ces tests s'exécutent en quelques secondes et détectent les régressions invisibles
+> que le Golden Dataset ne peut pas attraper par des tests manuels.
+
+---
+
+## 1. Principe
+
+**Ces 6 fonctions sont critiques.** Si l'une d'elles casse, toute l'application tombe :
+
+| Fonction | Rôle | Conséquence si cassée |
+|---|---|---|
+| `canUserSimulate` | Vérifie quota + plan avant simulation | N'importe qui peut simuler sans limite |
+| `getPlanPermissions` | Retourne les permissions d'un plan | Mauvaises features affichées / refusées |
+| `checkFeatureAccess` | Vérifie l'accès à une feature spécifique | Features Premium accessibles en Free |
+| `updateUserPlan` | Met à jour le plan dans Supabase | Paiement reçu mais accès non débloqué |
+| `verifyStripeWebhook` | Valide la signature Stripe | N'importe qui peut déclencher un webhook |
+| `calculateProrata` | Calcule le montant d'upgrade | Mauvais montant affiché à l'utilisateur |
+
+---
+
+## 2. Installation
+
+Dans le dépôt `expria-backend`, installer Vitest :
+
+```bash
+npm install --save-dev vitest @vitest/coverage-v8
+```
+
+Ajouter dans `package.json` :
+
+```json
+{
+ "scripts": {
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "test:coverage": "vitest run --coverage"
+ }
+}
+```
+
+Créer le fichier `vitest.config.ts` :
+
+```typescript
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ environment: 'node',
+ globals: true,
+ coverage: {
+ reporter: ['text', 'html'],
+ include: ['src/lib/**', 'src/controllers/**'],
+ },
+ },
+})
+```
+
+---
+
+## 3. Tests — canUserSimulate
+
+**Fichier :** `src/lib/__tests__/canUserSimulate.test.ts`
+
+```typescript
+import { describe, it, expect } from 'vitest'
+import { canUserSimulate } from '../access'
+
+describe('canUserSimulate', () => {
+
+ // Plan FREE — dans les limites
+ it('autorise un utilisateur free avec 0 simulation utilisée', () => {
+ const result = canUserSimulate({ plan: 'free', simulations_used: 0 })
+ expect(result.allowed).toBe(true)
+ })
+
+ it('autorise un utilisateur free avec 4 simulations utilisées', () => {
+ const result = canUserSimulate({ plan: 'free', simulations_used: 4 })
+ expect(result.allowed).toBe(true)
+ })
+
+ // Plan FREE — quota atteint
+ it('bloque un utilisateur free avec 5 simulations utilisées', () => {
+ const result = canUserSimulate({ plan: 'free', simulations_used: 5 })
+ expect(result.allowed).toBe(false)
+ expect(result.reason).toBe('quota_reached')
+ })
+
+ it('bloque un utilisateur free avec plus de 5 simulations (sécurité)', () => {
+ const result = canUserSimulate({ plan: 'free', simulations_used: 99 })
+ expect(result.allowed).toBe(false)
+ expect(result.reason).toBe('quota_reached')
+ })
+
+ // Plan STANDARD — illimité
+ it('autorise toujours un utilisateur standard', () => {
+ const result = canUserSimulate({ plan: 'standard', simulations_used: 999 })
+ expect(result.allowed).toBe(true)
+ })
+
+ // Plan PREMIUM — illimité
+ it('autorise toujours un utilisateur premium', () => {
+ const result = canUserSimulate({ plan: 'premium', simulations_used: 999 })
+ expect(result.allowed).toBe(true)
+ })
+
+ // Plan inconnu — sécurité défensive
+ it('bloque un plan inconnu', () => {
+ const result = canUserSimulate({ plan: 'unknown' as any, simulations_used: 0 })
+ expect(result.allowed).toBe(false)
+ expect(result.reason).toBe('invalid_plan')
+ })
+
+})
+```
+
+---
+
+## 4. Tests — getPlanPermissions
+
+**Fichier :** `src/lib/__tests__/getPlanPermissions.test.ts`
+
+```typescript
+import { describe, it, expect } from 'vitest'
+import { getPlanPermissions } from '../access'
+
+describe('getPlanPermissions', () => {
+
+ describe('Plan FREE', () => {
+ it('retourne les bonnes permissions pour free', () => {
+ const perms = getPlanPermissions('free')
+ expect(perms.simulations_lifetime).toBe(5)
+ expect(perms.oral_t2_live).toBe(false)
+ expect(perms.detailed_report).toBe(false)
+ expect(perms.tips).toBe(false)
+ expect(perms.dashboard).toBe(false)
+ expect(perms.exam_mode).toBe(false)
+ expect(perms.pattern_analysis).toBe(false)
+ expect(perms.preparation_index).toBe(false)
+ })
+ })
+
+ describe('Plan STANDARD', () => {
+ it('retourne les bonnes permissions pour standard', () => {
+ const perms = getPlanPermissions('standard')
+ expect(perms.simulations_lifetime).toBeNull()
+ expect(perms.oral_t2_live).toBe(false)
+ expect(perms.detailed_report).toBe(true)
+ expect(perms.tips).toBe(true)
+ expect(perms.dashboard).toBe(true)
+ expect(perms.exam_mode).toBe(false) // Standard n'a PAS le mode examen
+ expect(perms.pattern_analysis).toBe(false)
+ expect(perms.preparation_index).toBe(false)
+ })
+ })
+
+ describe('Plan PREMIUM', () => {
+ it('retourne les bonnes permissions pour premium', () => {
+ const perms = getPlanPermissions('premium')
+ expect(perms.simulations_lifetime).toBeNull()
+ expect(perms.oral_t2_live).toBe(true)
+ expect(perms.detailed_report).toBe(true)
+ expect(perms.tips).toBe(true)
+ expect(perms.dashboard).toBe(true)
+ expect(perms.exam_mode).toBe(true)
+ expect(perms.pattern_analysis).toBe(true)
+ expect(perms.preparation_index).toBe(true)
+ })
+ })
+
+ it('lève une erreur pour un plan inconnu', () => {
+ expect(() => getPlanPermissions('unknown' as any)).toThrow()
+ })
+
+})
+```
+
+---
+
+## 5. Tests — checkFeatureAccess
+
+**Fichier :** `src/lib/__tests__/checkFeatureAccess.test.ts`
+
+```typescript
+import { describe, it, expect } from 'vitest'
+import { checkFeatureAccess } from '../access'
+
+describe('checkFeatureAccess', () => {
+
+ // Features accessibles en FREE
+ it('free peut accéder au rapport basique', () => {
+ expect(checkFeatureAccess('free', 'basic_report')).toBe(true)
+ })
+
+ // Features BLOQUÉES en FREE
+ it('free ne peut pas accéder au rapport détaillé', () => {
+ expect(checkFeatureAccess('free', 'detailed_report')).toBe(false)
+ })
+
+ it('free ne peut pas accéder au mode examen', () => {
+ expect(checkFeatureAccess('free', 'exam_mode')).toBe(false)
+ })
+
+ it('free ne peut pas accéder à la T2 live', () => {
+ expect(checkFeatureAccess('free', 'oral_t2_live')).toBe(false)
+ })
+
+ it('free ne peut pas accéder au dashboard', () => {
+ expect(checkFeatureAccess('free', 'dashboard')).toBe(false)
+ })
+
+ // Features STANDARD
+ it('standard peut accéder au rapport détaillé', () => {
+ expect(checkFeatureAccess('standard', 'detailed_report')).toBe(true)
+ })
+
+ it('standard peut accéder au dashboard', () => {
+ expect(checkFeatureAccess('standard', 'dashboard')).toBe(true)
+ })
+
+ it('standard ne peut PAS accéder au mode examen', () => {
+ expect(checkFeatureAccess('standard', 'exam_mode')).toBe(false)
+ })
+
+ it('standard ne peut PAS accéder à la T2 live', () => {
+ expect(checkFeatureAccess('standard', 'oral_t2_live')).toBe(false)
+ })
+
+ it('standard ne peut PAS accéder à l\'analyse des patterns', () => {
+ expect(checkFeatureAccess('standard', 'pattern_analysis')).toBe(false)
+ })
+
+ // Features PREMIUM
+ it('premium peut accéder au mode examen', () => {
+ expect(checkFeatureAccess('premium', 'exam_mode')).toBe(true)
+ })
+
+ it('premium peut accéder à la T2 live', () => {
+ expect(checkFeatureAccess('premium', 'oral_t2_live')).toBe(true)
+ })
+
+ it('premium peut accéder à l\'analyse des patterns', () => {
+ expect(checkFeatureAccess('premium', 'pattern_analysis')).toBe(true)
+ })
+
+ it('premium peut accéder à l\'indice de préparation', () => {
+ expect(checkFeatureAccess('premium', 'preparation_index')).toBe(true)
+ })
+
+})
+```
+
+---
+
+## 6. Tests — updateUserPlan
+
+**Fichier :** `src/lib/__tests__/updateUserPlan.test.ts`
+
+> Ces tests utilisent un mock de Supabase — pas de vraie base de données.
+
+```typescript
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { updateUserPlan } from '../planController'
+
+// Mock du client Supabase
+vi.mock('../supabase', () => ({
+ supabase: {
+ from: vi.fn(() => ({
+ update: vi.fn(() => ({
+ eq: vi.fn(() => ({
+ select: vi.fn(() => ({
+ single: vi.fn(() => ({
+ data: { id: 'test-user-id', plan: 'standard' },
+ error: null,
+ })),
+ })),
+ })),
+ })),
+ })),
+ },
+}))
+
+describe('updateUserPlan', () => {
+
+ it('met à jour le plan vers standard', async () => {
+ const result = await updateUserPlan('test-user-id', 'standard')
+ expect(result.success).toBe(true)
+ expect(result.plan).toBe('standard')
+ })
+
+ it('met à jour le plan vers premium', async () => {
+ const result = await updateUserPlan('test-user-id', 'premium')
+ expect(result.success).toBe(true)
+ })
+
+ it('refuse une valeur de plan invalide', async () => {
+ await expect(
+ updateUserPlan('test-user-id', 'super_premium' as any)
+ ).rejects.toThrow('Plan invalide')
+ })
+
+ it('refuse un userId vide', async () => {
+ await expect(
+ updateUserPlan('', 'standard')
+ ).rejects.toThrow('userId requis')
+ })
+
+})
+```
+
+---
+
+## 7. Tests — verifyStripeWebhook
+
+**Fichier :** `src/lib/__tests__/verifyStripeWebhook.test.ts`
+
+```typescript
+import { describe, it, expect, vi } from 'vitest'
+import { verifyStripeWebhook } from '../stripe'
+
+// Mock de la librairie Stripe
+vi.mock('stripe', () => ({
+ default: vi.fn(() => ({
+ webhooks: {
+ constructEvent: vi.fn((payload, signature, secret) => {
+ if (signature === 'valid_signature') {
+ return {
+ type: 'checkout.session.completed',
+ data: { object: { client_reference_id: 'user-123' } },
+ }
+ }
+ throw new Error('No signatures found matching the expected signature')
+ }),
+ },
+ })),
+}))
+
+describe('verifyStripeWebhook', () => {
+
+ it('valide un webhook avec une signature correcte', () => {
+ const result = verifyStripeWebhook(
+ Buffer.from('payload'),
+ 'valid_signature',
+ 'whsec_test_secret'
+ )
+ expect(result.valid).toBe(true)
+ expect(result.event?.type).toBe('checkout.session.completed')
+ })
+
+ it('rejette un webhook avec une signature incorrecte', () => {
+ const result = verifyStripeWebhook(
+ Buffer.from('payload'),
+ 'invalid_signature',
+ 'whsec_test_secret'
+ )
+ expect(result.valid).toBe(false)
+ expect(result.error).toBeDefined()
+ })
+
+ it('rejette un payload vide', () => {
+ const result = verifyStripeWebhook(
+ Buffer.from(''),
+ 'valid_signature',
+ 'whsec_test_secret'
+ )
+ expect(result.valid).toBe(false)
+ })
+
+ it('rejette une signature vide', () => {
+ const result = verifyStripeWebhook(
+ Buffer.from('payload'),
+ '',
+ 'whsec_test_secret'
+ )
+ expect(result.valid).toBe(false)
+ })
+
+})
+```
+
+---
+
+## 8. Tests — calculateProrata
+
+**Fichier :** `src/lib/__tests__/calculateProrata.test.ts`
+
+```typescript
+import { describe, it, expect } from 'vitest'
+import { calculateProrata } from '../stripe'
+
+describe('calculateProrata', () => {
+
+ it('calcule le prorata correct pour un upgrade à mi-période', () => {
+ // Standard = 19.90€ / 28 jours, 14 jours restants
+ // Premium = 39.90€ / 28 jours, 14 jours restants
+ // Crédit Standard = 19.90 * (14/28) = 9.95€
+ // Coût Premium = 39.90 * (14/28) = 19.95€
+ // À payer = 19.95 - 9.95 = 10.00€
+ const result = calculateProrata({
+ currentPlanPrice: 19.90,
+ newPlanPrice: 39.90,
+ totalDays: 28,
+ daysRemaining: 14,
+ })
+ expect(result.amount).toBeCloseTo(10.00, 1)
+ })
+
+ it('retourne 0 si les plans ont le même prix', () => {
+ const result = calculateProrata({
+ currentPlanPrice: 19.90,
+ newPlanPrice: 19.90,
+ totalDays: 28,
+ daysRemaining: 14,
+ })
+ expect(result.amount).toBeCloseTo(0, 1)
+ })
+
+ it('retourne le plein tarif si aucun jour n\'a été consommé', () => {
+ const result = calculateProrata({
+ currentPlanPrice: 19.90,
+ newPlanPrice: 39.90,
+ totalDays: 28,
+ daysRemaining: 28,
+ })
+ expect(result.amount).toBeCloseTo(20.00, 1)
+ })
+
+ it('retourne un montant minimal si 1 seul jour reste', () => {
+ const result = calculateProrata({
+ currentPlanPrice: 19.90,
+ newPlanPrice: 39.90,
+ totalDays: 28,
+ daysRemaining: 1,
+ })
+ expect(result.amount).toBeGreaterThan(0)
+ expect(result.amount).toBeLessThan(5)
+ })
+
+ it('refuse des valeurs négatives', () => {
+ expect(() => calculateProrata({
+ currentPlanPrice: -1,
+ newPlanPrice: 39.90,
+ totalDays: 28,
+ daysRemaining: 14,
+ })).toThrow()
+ })
+
+ it('refuse daysRemaining > totalDays', () => {
+ expect(() => calculateProrata({
+ currentPlanPrice: 19.90,
+ newPlanPrice: 39.90,
+ totalDays: 28,
+ daysRemaining: 30,
+ })).toThrow()
+ })
+
+})
+```
+
+---
+
+## 9. Lancer les tests
+
+```bash
+# Dans expria-backend/
+
+# Lancer tous les tests une fois
+npm run test
+
+# Lancer en mode watch (relance automatiquement à chaque modification)
+npm run test:watch
+
+# Générer un rapport de couverture
+npm run test:coverage
+```
+
+**Résultat attendu (tous les tests au vert) :**
+```
+✓ canUserSimulate (7 tests)
+✓ getPlanPermissions (7 tests)
+✓ checkFeatureAccess (13 tests)
+✓ updateUserPlan (4 tests)
+✓ verifyStripeWebhook (4 tests)
+✓ calculateProrata (6 tests)
+
+Test Files 6 passed (6)
+Tests 41 passed (41)
+Duration ~1.2s
+```
+
+---
+
+## 10. Règle d'utilisation avec Claude Code
+
+**Avant chaque session Claude Code qui touche au backend :**
+```bash
+npm run test
+# Tous les tests doivent être verts avant de commencer
+```
+
+**Après chaque session Claude Code qui touche au backend :**
+```bash
+npm run test
+# Si un test passe au rouge → régression détectée → ne pas continuer
+```
+
+**Dans le prompt à Claude Code, toujours inclure :**
+> "Après chaque modification, lance `npm run test`.
+> Si un test échoue, corrige la régression avant de passer à l'étape suivante.
+> Ne me montre le résultat final que quand tous les tests sont verts."
+
+---
+
+## 11. Quand ajouter de nouveaux tests
+
+Ajouter un test automatisé chaque fois que Claude Code crée une nouvelle fonction qui :
+- Vérifie un droit d'accès
+- Modifie des données en base
+- Calcule un montant ou un score
+- Appelle une API externe
+
+**Ne pas tester :**
+- Les composants d'affichage (trop fragiles, trop coûteux à maintenir)
+- Les routes HTTP directement (c'est le rôle du Golden Dataset)
+- La logique Supabase interne (c'est leur responsabilité)
diff --git a/docs/TEST_ENVIRONMENT.md b/docs/TEST_ENVIRONMENT.md
new file mode 100644
index 0000000..1f6d4f0
--- /dev/null
+++ b/docs/TEST_ENVIRONMENT.md
@@ -0,0 +1,365 @@
+# TEST_ENVIRONMENT.md — Expria / Coach TCF Canada
+
+> **Document de référence — Version 1.0**
+> Ce document décrit comment créer et réinitialiser l'environnement de test.
+> Les comptes de test permettent de rejouer tous les parcours utilisateur
+> sans créer de vrais abonnements ni passer par Stripe.
+
+---
+
+## 1. Principe
+
+L'environnement de test est une configuration connue et reproductible de la base de données.
+Il consiste en 4 comptes Supabase préconfigurés, un par situation critique.
+
+**Règles absolues :**
+- Ces comptes n'existent que dans l'environnement de développement / staging
+- Jamais en production
+- Les emails se terminent par `@expria.local` — bloqués à l'inscription dans le code
+- Les mots de passe sont documentés ici — ne jamais les utiliser pour de vrais comptes
+
+---
+
+## 2. Les 4 comptes de test
+
+| Compte | Plan | simulations_used | Cas testé |
+|---|---|---|---|
+| test.free@expria.local | free | 0 | Parcours Free normal |
+| test.standard@expria.local | standard | 12 | Parcours Standard complet |
+| test.premium@expria.local | premium | 28 | Parcours Premium complet |
+| test.quota@expria.local | free | 5 | Blocage quota Free |
+
+**Mot de passe pour tous les comptes de test :** `Expria2025!test`
+
+---
+
+## 3. Script de création — à exécuter dans Supabase SQL Editor
+
+> ⚠️ À exécuter UNE SEULE FOIS dans l'environnement de développement.
+> Ne jamais exécuter en production.
+
+```sql
+-- =============================================================
+-- EXPRIA — Création des comptes de test
+-- Environnement : développement / staging uniquement
+-- =============================================================
+
+-- Étape 1 : Créer les utilisateurs dans auth.users
+-- (Supabase gère le hash du mot de passe automatiquement)
+
+INSERT INTO auth.users (
+ id,
+ email,
+ encrypted_password,
+ email_confirmed_at,
+ created_at,
+ updated_at,
+ raw_app_meta_data,
+ raw_user_meta_data,
+ is_super_admin,
+ role
+) VALUES
+ (
+ '00000000-0000-0000-0000-000000000001',
+ 'test.free@expria.local',
+ crypt('Expria2025!test', gen_salt('bf')),
+ NOW(), NOW(), NOW(),
+ '{"provider":"email","providers":["email"]}',
+ '{}', false, 'authenticated'
+ ),
+ (
+ '00000000-0000-0000-0000-000000000002',
+ 'test.standard@expria.local',
+ crypt('Expria2025!test', gen_salt('bf')),
+ NOW(), NOW(), NOW(),
+ '{"provider":"email","providers":["email"]}',
+ '{}', false, 'authenticated'
+ ),
+ (
+ '00000000-0000-0000-0000-000000000003',
+ 'test.premium@expria.local',
+ crypt('Expria2025!test', gen_salt('bf')),
+ NOW(), NOW(), NOW(),
+ '{"provider":"email","providers":["email"]}',
+ '{}', false, 'authenticated'
+ ),
+ (
+ '00000000-0000-0000-0000-000000000004',
+ 'test.quota@expria.local',
+ crypt('Expria2025!test', gen_salt('bf')),
+ NOW(), NOW(), NOW(),
+ '{"provider":"email","providers":["email"]}',
+ '{}', false, 'authenticated'
+ )
+ON CONFLICT (id) DO NOTHING;
+
+-- Étape 2 : Créer les profils dans la table profiles
+INSERT INTO profiles (
+ id,
+ email,
+ plan,
+ simulations_used,
+ stripe_customer_id,
+ stripe_subscription_id,
+ plan_expires_at,
+ created_at,
+ updated_at
+) VALUES
+ (
+ '00000000-0000-0000-0000-000000000001',
+ 'test.free@expria.local',
+ 'free', 0, NULL, NULL, NULL,
+ NOW(), NOW()
+ ),
+ (
+ '00000000-0000-0000-0000-000000000002',
+ 'test.standard@expria.local',
+ 'standard', 12, 'cus_test_standard', 'sub_test_standard',
+ NOW() + INTERVAL '14 days',
+ NOW(), NOW()
+ ),
+ (
+ '00000000-0000-0000-0000-000000000003',
+ 'test.premium@expria.local',
+ 'premium', 28, 'cus_test_premium', 'sub_test_premium',
+ NOW() + INTERVAL '21 days',
+ NOW(), NOW()
+ ),
+ (
+ '00000000-0000-0000-0000-000000000004',
+ 'test.quota@expria.local',
+ 'free', 5, NULL, NULL, NULL,
+ NOW(), NOW()
+ )
+ON CONFLICT (id) DO UPDATE SET
+ plan = EXCLUDED.plan,
+ simulations_used = EXCLUDED.simulations_used,
+ stripe_customer_id = EXCLUDED.stripe_customer_id,
+ stripe_subscription_id = EXCLUDED.stripe_subscription_id,
+ plan_expires_at = EXCLUDED.plan_expires_at,
+ updated_at = NOW();
+
+-- Étape 3 : Insérer des productions de test pour les comptes Standard et Premium
+-- (nécessaire pour tester le dashboard, l'historique, et l'analyse des patterns)
+
+INSERT INTO productions (
+ id, user_id, tache, mode, contenu, score, nclc, rapport, created_at
+) VALUES
+ -- 5 productions pour test.standard (active l'indice de préparation)
+ (gen_random_uuid(), '00000000-0000-0000-0000-000000000002',
+ 'EE_T1', 'entrainement',
+ 'Texte de production test EE T1 — compte standard',
+ 14.5, 8,
+ '{"criteres":[{"nom":"Cohérence","score":3},{"nom":"Lexique","score":4}],"erreurs":["Connecteurs logiques insuffisants"],"exercices":["Exercice connecteurs"]}',
+ NOW() - INTERVAL '10 days'),
+ (gen_random_uuid(), '00000000-0000-0000-0000-000000000002',
+ 'EE_T2', 'entrainement',
+ 'Texte de production test EE T2 — compte standard',
+ 15.0, 8,
+ '{"criteres":[{"nom":"Cohérence","score":4},{"nom":"Lexique","score":3}],"erreurs":["Vocabulaire limité"],"exercices":["Exercice vocabulaire"]}',
+ NOW() - INTERVAL '8 days'),
+ (gen_random_uuid(), '00000000-0000-0000-0000-000000000002',
+ 'EE_T3', 'entrainement',
+ 'Texte de production test EE T3 — compte standard',
+ 13.5, 7,
+ '{"criteres":[{"nom":"Cohérence","score":3},{"nom":"Lexique","score":3}],"erreurs":["Structure argumentative faible"],"exercices":["Exercice argumentation"]}',
+ NOW() - INTERVAL '6 days'),
+ (gen_random_uuid(), '00000000-0000-0000-0000-000000000002',
+ 'EO_T1', 'entrainement',
+ NULL, 14.0, 8,
+ '{"criteres":[{"nom":"Phonologie","score":4},{"nom":"Lexique","score":3}],"erreurs":["Liaisons manquantes"],"exercices":["Exercice liaisons"]}',
+ NOW() - INTERVAL '4 days'),
+ (gen_random_uuid(), '00000000-0000-0000-0000-000000000002',
+ 'EO_T3', 'entrainement',
+ NULL, 15.5, 9,
+ '{"criteres":[{"nom":"Phonologie","score":4},{"nom":"Lexique","score":4}],"erreurs":[],"exercices":[]}',
+ NOW() - INTERVAL '2 days'),
+
+ -- 7 productions pour test.premium (active patterns + indice)
+ (gen_random_uuid(), '00000000-0000-0000-0000-000000000003',
+ 'EE_T1', 'entrainement',
+ 'Texte production EE T1 — premium',
+ 16.0, 9,
+ '{"criteres":[{"nom":"Cohérence","score":4},{"nom":"Lexique","score":4}],"erreurs":["Connecteurs logiques"],"exercices":["Connecteurs"]}',
+ NOW() - INTERVAL '20 days'),
+ (gen_random_uuid(), '00000000-0000-0000-0000-000000000003',
+ 'EE_T2', 'entrainement',
+ 'Texte production EE T2 — premium',
+ 15.5, 9,
+ '{"criteres":[{"nom":"Cohérence","score":4},{"nom":"Lexique","score":4}],"erreurs":["Connecteurs logiques"],"exercices":["Connecteurs"]}',
+ NOW() - INTERVAL '16 days'),
+ (gen_random_uuid(), '00000000-0000-0000-0000-000000000003',
+ 'EE_T3', 'examen',
+ 'Texte production EE T3 — premium examen',
+ 17.0, 10,
+ '{"criteres":[{"nom":"Cohérence","score":5},{"nom":"Lexique","score":4}],"erreurs":[],"exercices":[]}',
+ NOW() - INTERVAL '12 days'),
+ (gen_random_uuid(), '00000000-0000-0000-0000-000000000003',
+ 'EO_T1', 'entrainement',
+ NULL, 16.5, 9,
+ '{"criteres":[{"nom":"Phonologie","score":4},{"nom":"Lexique","score":4}],"erreurs":["Connecteurs logiques"],"exercices":["Connecteurs oraux"]}',
+ NOW() - INTERVAL '9 days'),
+ (gen_random_uuid(), '00000000-0000-0000-0000-000000000003',
+ 'EO_T2_LIVE', 'entrainement',
+ NULL, 15.0, 8,
+ '{"criteres":[{"nom":"Interaction","score":4},{"nom":"Phonologie","score":3}],"erreurs":["Hésitations fréquentes"],"exercices":["Fluidité orale"]}',
+ NOW() - INTERVAL '6 days'),
+ (gen_random_uuid(), '00000000-0000-0000-0000-000000000003',
+ 'EO_T3', 'entrainement',
+ NULL, 16.0, 9,
+ '{"criteres":[{"nom":"Phonologie","score":4},{"nom":"Lexique","score":4}],"erreurs":["Connecteurs logiques"],"exercices":["Connecteurs oraux"]}',
+ NOW() - INTERVAL '3 days'),
+ (gen_random_uuid(), '00000000-0000-0000-0000-000000000003',
+ 'EE_T1', 'examen',
+ 'Texte production EE T1 examen — premium',
+ 17.5, 10,
+ '{"criteres":[{"nom":"Cohérence","score":5},{"nom":"Lexique","score":4}],"erreurs":[],"exercices":[]}',
+ NOW() - INTERVAL '1 day')
+ON CONFLICT DO NOTHING;
+```
+
+---
+
+## 4. Script de vérification — confirmer que les comptes existent
+
+```sql
+-- Vérifier les profils créés
+SELECT
+ id,
+ email,
+ plan,
+ simulations_used,
+ plan_expires_at
+FROM profiles
+WHERE email LIKE '%@expria.local'
+ORDER BY email;
+
+-- Vérifier les productions créées
+SELECT
+ p.email,
+ prod.tache,
+ prod.mode,
+ prod.score,
+ prod.created_at
+FROM productions prod
+JOIN profiles p ON p.id = prod.user_id
+WHERE p.email LIKE '%@expria.local'
+ORDER BY p.email, prod.created_at;
+```
+
+**Résultat attendu :** 4 profils, 12 productions au total.
+
+---
+
+## 5. Script de réinitialisation — remettre l'environnement à zéro
+
+> À utiliser quand les comptes de test ont été modifiés par des sessions de test
+> et qu'on veut repartir d'un état propre.
+
+```sql
+-- Supprimer les productions de test
+DELETE FROM productions
+WHERE user_id IN (
+ SELECT id FROM profiles WHERE email LIKE '%@expria.local'
+);
+
+-- Remettre les profils à leur état initial
+UPDATE profiles SET
+ plan = 'free',
+ simulations_used = 0,
+ stripe_customer_id = NULL,
+ stripe_subscription_id = NULL,
+ plan_expires_at = NULL,
+ updated_at = NOW()
+WHERE email = 'test.free@expria.local';
+
+UPDATE profiles SET
+ plan = 'standard',
+ simulations_used = 12,
+ stripe_customer_id = 'cus_test_standard',
+ stripe_subscription_id = 'sub_test_standard',
+ plan_expires_at = NOW() + INTERVAL '14 days',
+ updated_at = NOW()
+WHERE email = 'test.standard@expria.local';
+
+UPDATE profiles SET
+ plan = 'premium',
+ simulations_used = 28,
+ stripe_customer_id = 'cus_test_premium',
+ stripe_subscription_id = 'sub_test_premium',
+ plan_expires_at = NOW() + INTERVAL '21 days',
+ updated_at = NOW()
+WHERE email = 'test.premium@expria.local';
+
+UPDATE profiles SET
+ plan = 'free',
+ simulations_used = 5,
+ stripe_customer_id = NULL,
+ stripe_subscription_id = NULL,
+ plan_expires_at = NULL,
+ updated_at = NOW()
+WHERE email = 'test.quota@expria.local';
+
+-- Réinsérer les productions (copier-coller le bloc INSERT de la section 3)
+```
+
+---
+
+## 6. Bloquer les inscriptions @expria.local en production
+
+Ajouter cette validation dans le backend (middleware d'inscription) :
+
+```typescript
+// src/middleware/auth.ts — backend Hono
+
+const BLOCKED_EMAIL_DOMAINS = ['@expria.local']
+
+export function validateEmail(email: string): boolean {
+ const isBlocked = BLOCKED_EMAIL_DOMAINS.some(domain =>
+ email.toLowerCase().endsWith(domain)
+ )
+ if (isBlocked) return false
+ return true
+}
+```
+
+Et dans la route d'inscription :
+
+```typescript
+// src/routes/auth.ts
+
+app.post('/auth/register', async (c) => {
+ const { email, password } = await c.req.json()
+
+ if (!validateEmail(email)) {
+ return c.json({ error: 'Email non autorisé' }, 400)
+ }
+ // ... suite de l'inscription
+})
+```
+
+---
+
+## 7. Procédure complète — première mise en place
+
+```
+Étape 1 : Ouvrir Supabase Dashboard → SQL Editor
+Étape 2 : Copier-coller le script de la section 3
+Étape 3 : Exécuter
+Étape 4 : Copier-coller le script de vérification (section 4)
+Étape 5 : Vérifier : 4 profils + 12 productions affichés
+Étape 6 : Tester une connexion avec test.free@expria.local
+ dans l'application (mot de passe : Expria2025!test)
+Étape 7 : Vérifier que le dashboard Free s'affiche correctement
+```
+
+---
+
+## 8. Procédure — avant chaque session Golden Dataset
+
+```
+Étape 1 : Exécuter le script de réinitialisation (section 5)
+Étape 2 : Exécuter le script de vérification (section 4)
+Étape 3 : Confirmer que les 4 profils sont dans l'état attendu
+Étape 4 : Lancer les tests du Golden Dataset
+```