feat(billing): TD-13 webhook idempotency + Stripe Customer Portal + doc cleanup
- Table stripe_webhook_events + helpers isEventProcessed/markEventProcessed - POST /stripe/customer-portal (auth + stripe_customer_id check) - ARCHITECTURE-backend.md: suppression POST /plans/upgrade (duplication doc) - TD-13 fermé dans TECH_DEBT-backend.md - Tests: 261 → 278 verts (+17)
This commit is contained in:
parent
ec0598d122
commit
6671bac347
10 changed files with 891 additions and 324 deletions
|
|
@ -72,6 +72,7 @@ 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)
|
||||
|
|
@ -159,8 +160,8 @@ expria-backend/
|
|||
│ │ ├── 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
|
||||
│ │ ├── plans.ts # GET /plans/status, POST /plans/upgrade-prorata
|
||||
│ │ ├── stripe.ts # POST /stripe/checkout, /stripe/customer-portal, /stripe/webhook
|
||||
│ │ └── t2live.ts # WebSocket /t2/live (T2 EO proxy Gemini)
|
||||
│ ├── controllers/ # Logique métier (une par domaine)
|
||||
│ │ ├── simulationController.ts
|
||||
|
|
@ -292,11 +293,13 @@ 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
|
||||
|
|
@ -304,25 +307,29 @@ GET /simulations Liste les simulations de l'utilisateur connec
|
|||
```
|
||||
|
||||
### 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)
|
||||
POST /plans/upgrade-prorata Upgrade en cours d'abonnement (prorata Stripe — preview du montant)
|
||||
```
|
||||
|
||||
### Stripe
|
||||
|
||||
```
|
||||
POST /stripe/checkout Crée une Checkout Session Stripe
|
||||
POST /stripe/webhook Reçoit les events Stripe (checkout, invoice, deleted)
|
||||
POST /stripe/checkout Crée une Checkout Session Stripe (nouveau abonnement)
|
||||
POST /stripe/customer-portal Crée une Billing Portal Session (gestion abonnement self-service)
|
||||
POST /stripe/webhook Reçoit les events Stripe (checkout, invoice, deleted) — idempotent (TD-13 résolu Sprint 5a)
|
||||
```
|
||||
|
||||
### T2 EO Live
|
||||
|
||||
```
|
||||
WS /t2/live WebSocket — proxy Gemini Live API (Premium uniquement)
|
||||
```
|
||||
|
|
@ -388,6 +395,7 @@ WS /t2/live WebSocket — proxy Gemini Live API (Premium
|
|||
## 8. Variables d'environnement
|
||||
|
||||
### Frontend (.env)
|
||||
|
||||
```
|
||||
VITE_API_URL=https://api.expria.app # URL du backend Render
|
||||
VITE_SUPABASE_URL=https://xxx.supabase.co
|
||||
|
|
@ -395,6 +403,7 @@ VITE_SUPABASE_ANON_KEY=xxx # Clé publique uniquement
|
|||
```
|
||||
|
||||
### Backend (.env)
|
||||
|
||||
```
|
||||
# Supabase
|
||||
SUPABASE_URL=https://xxx.supabase.co
|
||||
|
|
@ -481,29 +490,35 @@ npx wrangler pages deploy dist --project-name=expria
|
|||
## 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).
|
||||
|
|
|
|||
|
|
@ -6,6 +6,34 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/).
|
|||
|
||||
---
|
||||
|
||||
## [Unreleased] — 2026-04-26 — Sprint 5a — Backend billing cleanup
|
||||
|
||||
### Added
|
||||
|
||||
- `supabase/migrations/007_sprint_5a_stripe_webhook_events.sql` — table `stripe_webhook_events(id TEXT PRIMARY KEY, processed_at TIMESTAMPTZ NOT NULL DEFAULT NOW())` + index sur `processed_at`. Idempotente (`CREATE TABLE IF NOT EXISTS`).
|
||||
- `src/lib/stripeWebhookEvents.ts` — helpers `isEventProcessed` / `markEventProcessed` (insert idempotent, conflit unique `23505` avalé silencieusement).
|
||||
- `src/lib/__tests__/stripeWebhookEvents.test.ts` — 8 tests (lecture, écriture, edge cases vide/erreur DB).
|
||||
- `src/lib/__tests__/createBillingPortalSession.test.ts` — 4 tests (succès, customerId vide, returnUrl vide, URL Stripe vide).
|
||||
- `POST /stripe/customer-portal` — endpoint authentifié qui crée une Stripe Billing Portal Session (gestion abonnement self-service) et redirige l'utilisateur. 400 `NO_ACTIVE_SUBSCRIPTION` si pas de `stripe_customer_id` ; return_url = `${APP_URL}/dashboard`.
|
||||
|
||||
### Changed
|
||||
|
||||
- `POST /stripe/webhook` — déduplication explicite des events Stripe (TD-13 résolu) : check `isEventProcessed(event.id)` avant traitement → early return `200 { received: true, replayed: true }` ; `markEventProcessed` après succès uniquement (pas si exception, pour permettre rejeu Stripe).
|
||||
- `src/lib/stripe.ts` — nouvelle fonction `createBillingPortalSession({ customerId, returnUrl })` (mirror de `createCheckoutSession`).
|
||||
- `src/routes/__tests__/stripe.test.ts` — 5 nouveaux tests (2 idempotency webhook + 3 customer-portal route).
|
||||
- `docs/ARCHITECTURE-backend.md` — §3 commentaire `plans.ts` corrigé (`POST /plans/upgrade-prorata` au lieu de `POST /plans/upgrade` qui n'existait pas) ; §6 retrait de la ligne dupliquée `POST /plans/upgrade` (la création d'abonnement passe par `POST /stripe/checkout`) ; §6 ajout `POST /stripe/customer-portal`.
|
||||
|
||||
### Resolved
|
||||
|
||||
- **TD-13 🔴 → Résolu** : Webhook Stripe idempotent (table `stripe_webhook_events` + helpers + wiring route + 10 tests).
|
||||
|
||||
### Notes
|
||||
|
||||
- Tests : 261 → 278 verts (+17).
|
||||
- Aucun changement frontend dans ce sprint — Sprint 5b (frontend billing) à venir.
|
||||
|
||||
---
|
||||
|
||||
## [Unreleased] — 2026-04-26 — Sprint 4.8 — Phonologie EO
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -97,15 +97,14 @@
|
|||
### TD-13 — Webhook Stripe non idempotent
|
||||
|
||||
**Priorité :** 🔴 Critique
|
||||
**Statut :** Ouvert — à faire avant mise en production
|
||||
**Description :** Stripe peut livrer un même event webhook deux fois (retries réseau, rejeu manuel depuis le dashboard). La route `POST /stripe/webhook` traite chaque réception sans dédoublonnage. En pratique, les opérations `updateUserPlan` et `updateUserStripeInfo` sont idempotentes par nature (même résultat en cas de double appel), mais si de la logique non idempotente est ajoutée plus tard (ex: compteur, envoi d'email, crédit utilisateur), un double traitement causerait un bug.
|
||||
**À faire :**
|
||||
**Statut :** Résolu — Sprint 5a (2026-04-26)
|
||||
**Description :** Stripe peut livrer un même event webhook deux fois (retries réseau, rejeu manuel depuis le dashboard). La route `POST /stripe/webhook` traite désormais chaque réception via une déduplication explicite : check `stripe_webhook_events(id)` avant traitement, INSERT après succès.
|
||||
**Résolution Sprint 5a :**
|
||||
|
||||
- Créer une table `stripe_webhook_events(id TEXT PRIMARY KEY, processed_at TIMESTAMPTZ)`
|
||||
- Avant traitement, vérifier si `event.id` est déjà en base → si oui, retourner 200 sans rien faire
|
||||
- Après traitement, insérer l'`event.id` dans la table
|
||||
**Session concernée :** Stripe (POST /stripe/webhook)
|
||||
**Condition de résolution :** Avant la mise en production publique.
|
||||
- Migration `supabase/migrations/007_sprint_5a_stripe_webhook_events.sql` — table `stripe_webhook_events(id TEXT PRIMARY KEY, processed_at TIMESTAMPTZ NOT NULL DEFAULT NOW())` + index sur `processed_at`.
|
||||
- Helper `src/lib/stripeWebhookEvents.ts` — `isEventProcessed` / `markEventProcessed` (insert idempotent, conflit unique avalé silencieusement).
|
||||
- `src/routes/stripe.ts` — early return `200 { received: true, replayed: true }` si l'event est déjà journalisé ; `markEventProcessed(event.id)` après traitement réussi (pas si exception, pour permettre rejeu Stripe).
|
||||
- 8 tests unitaires + 2 tests d'intégration (`isEventProcessed`/`markEventProcessed` + comportement route).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -258,3 +257,4 @@ Gate de qualité actuel : npm run test.
|
|||
| TD-16 | Bucket Storage abandonné | 2026-04-25 | Sprint 4b — Deepgram direct |
|
||||
| TD-17 | Limite audio in-memory caduque | 2026-04-25 | Sprint 4b |
|
||||
| TD-18 | RLS Storage caduque | 2026-04-25 | Sprint 4b |
|
||||
| TD-13 | Webhook Stripe idempotent | 2026-04-26 | Sprint 5a |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue