# Playbook E2E manuel — Essai 14 j Pro avec CB (Stripe test mode)

> Version : 0.1 · Dernière maj : 2026-05-18
> Référence : `docs/tech/stripe-trial-with-card.md`

Validation manuelle bout en bout du tunnel essai 14 j avant le go-live. À exécuter UNE FOIS avant le premier déploiement prod du chantier billing, puis à re-jouer après chaque modif des handlers webhook.

**Pré-requis non triviaux à connaître** :

- Compte Stripe en mode **test** (Dashboard → "Viewing test data" en haut à droite — toggle bien sur "TEST").
- **Stripe CLI** installé : `brew install stripe/stripe-cli/stripe` (sinon `stripe.com/docs/stripe-cli`).
- Redis local (BullMQ) : `docker compose -f docker-compose.dev.yml up redis -d` depuis la racine du repo.
- Postgres local + base `rubis` migrée : `pnpm --filter @rubis/api migration:run`.

Les tests **automatisés** couvrent déjà tous les cas applicatifs (cf. `apps/api/tests/unit/stripe_billing.spec.ts` + `apps/api/tests/functional/billing_trial.spec.ts`). Ce playbook ajoute la validation des cas que seul Stripe peut produire : **3DS challenge réel**, **prélèvement effectif à J+14**, **passage `trialing → active` natif**.

---

## Setup ponctuel (une seule fois)

### 1. Récupérer la clé webhook test

```bash
stripe login                    # ouvre browser, lie ton compte Stripe au CLI
stripe listen --forward-to http://localhost:3333/api/v1/billing/webhook
```

Le CLI affiche un `whsec_xxxx` qu'il faut copier dans ton `.env` local :

```env
STRIPE_SECRET_KEY=sk_test_...        # depuis Dashboard → Developers → API keys
STRIPE_WEBHOOK_SECRET=whsec_xxxx     # depuis l'output stripe listen
WEB_URL=http://localhost:5173
LANDING_URL=http://localhost:5174
```

**Laisse `stripe listen` tourner** dans un terminal séparé pendant tout le test — c'est lui qui forward les webhooks Stripe → ta machine.

### 2. Vérifier les Prices Stripe test

```bash
pnpm --filter @rubis/api exec node ace stripe:setup
```

Crée les 4 Prices avec les lookup keys `rubis_pro_monthly`, `rubis_pro_yearly`, `rubis_business_monthly`, `rubis_business_yearly`. Vérifie dans Dashboard → Products que les 4 sont actifs.

### 3. Démarrer l'app

3 terminaux :

```bash
# Terminal 1 — Stripe webhook forwarder (laisser tourner)
stripe listen --forward-to http://localhost:3333/api/v1/billing/webhook

# Terminal 2 — API + SPA + landing
pnpm dev

# Terminal 3 — interactions avec Stripe (test clock, cards)
```

---

## Scénario 1 — Happy path : essai → prélèvement → actif

**Objectif** : valider qu'un signup avec carte 3DS passe en trial, puis bascule automatiquement en `active` à l'expiration.

### Étapes

1. **Signup** sur http://localhost:5173/signup avec un email frais (`alice+test@rubis.test`).
2. **Onboarding billing** — naviguer manuellement vers http://localhost:5173/onboarding/billing (l'écran n'est pas encore forcé dans le flow).
3. Cliquer **"Démarrer mon essai 14 jours"**.
4. Stripe Checkout s'ouvre. Saisir la carte 3DS test :
    - Numéro : `4000 0027 6000 3184`
    - Date : n'importe quelle date future
    - CVC : `123`
    - Code postal : `75001`
5. **3DS challenge** apparaît (Stripe simule la modale auth banque). Cliquer **"Complete authentication"** dans la modale.
6. Stripe redirige vers `http://localhost:5173/onboarding/compte?trial=started&session_id=cs_xxx`.

### Vérifications immédiates

- [ ] **DB org** : `SELECT plan, subscription_status, trial_ends_at FROM organizations WHERE id = '<org_id>';`
    - `plan = 'pro'`
    - `subscription_status = 'trialing'`
    - `trial_ends_at` ≈ now + 14 jours
- [ ] **App SPA** : la bannière "Essai Pro · 14 jours restants" s'affiche en haut de `/factures`.
- [ ] **Stripe Dashboard → Customers** : le customer est créé avec metadata `organization_id`.
- [ ] **Stripe Dashboard → Subscriptions** : la subscription est en `trialing` jusqu'au `trial_end`.

### Fast-forward avec Stripe Test Clock

Pour valider la suite (J+11 recap + J+14 prélèvement) sans attendre 14 jours réels :

```bash
# 1. Récupère le customer_id et le subscription_id côté Dashboard
CUSTOMER_ID="cus_xxx"
SUBSCRIPTION_ID="sub_xxx"

# 2. Crée un test clock à T0 (now)
stripe test_helpers test_clocks create \
  --frozen-time "$(date +%s)" \
  --name "rubis-trial-e2e"

# Copie le CLOCK_ID retourné. Puis attache le customer au test clock :
stripe customers update "$CUSTOMER_ID" \
  --test-clock "$CLOCK_ID"
```

⚠️ **Important** : il faut **recommencer le signup** depuis zéro avec le customer attaché au test clock (Stripe interdit d'attacher un clock à un customer existant). Solution : créer le customer **AVANT** le signup avec le test clock attaché, puis hack temporairement `ensureStripeCustomer` pour le réutiliser, ou utiliser un endpoint admin de test (à créer en V2 si on en fait beaucoup).

Pour ce playbook V1, l'option pragmatique :

```bash
# Advance le clock à J+11 (3 jours avant trial_end)
TARGET_J11=$(( $(date +%s) + 11 * 24 * 3600 ))
stripe test_helpers test_clocks advance "$CLOCK_ID" --frozen-time "$TARGET_J11"

# Stripe émet alors customer.subscription.trial_will_end qui arrive sur
# notre webhook via stripe listen. Vérifie le terminal 1 :
#   [200] POST http://localhost:3333/api/v1/billing/webhook [evt_xxx]
```

### Vérifications J+11

- [ ] **Terminal 1** (`stripe listen`) affiche un POST `customer.subscription.trial_will_end` → 200.
- [ ] **DB BullMQ** (Redis) : le job `trial-recap:<sub_id>` apparaît dans la queue. Vérifier via `redis-cli`:
    ```bash
    redis-cli KEYS 'bull:trial-recap:*'
    ```
- [ ] **Mailpit** (http://localhost:8025 si `MAIL_DRIVER=smtp`) : un mail "Plus que 3 jours d'essai" arrive avec :
    - Sujet : `Plus que 3 jours d'essai · récap avant prélèvement`
    - Stats : factures importées, relances envoyées, € récupérés, rubis
    - Bouton "Gérer mon abonnement" → URL Customer Portal Stripe

### Fast-forward à J+14

```bash
TARGET_J14=$(( $(date +%s) + 14 * 24 * 3600 ))
stripe test_helpers test_clocks advance "$CLOCK_ID" --frozen-time "$TARGET_J14"
```

### Vérifications J+14 — happy path

- [ ] **Terminal 1** : webhook `invoice.created` + `invoice.paid` + `customer.subscription.updated` (status `trialing → active`).
- [ ] **DB org** : `subscription_status = 'active'`.
- [ ] **App SPA** : la bannière "Essai Pro" disparaît, l'écran `/parametres/abonnement` affiche "Rubis Pro · Mensuel actif".
- [ ] **Stripe Dashboard → Payments** : un Payment Intent réussi à 19 €.

---

## Scénario 2 — Carte refusée au prélèvement

**Objectif** : valider qu'une CB qui passe l'auth 3DS mais qui échoue au prélèvement à J+14 fait passer l'org en `past_due`.

### Étapes

1. Signup neuf (`bob+failed@rubis.test`).
2. **Onboarding billing → Démarrer essai**.
3. Stripe Checkout : utiliser la carte **decline-on-charge** :
    - Numéro : `4000 0000 0000 0341`
    - (Cette carte accepte le SetupIntent + 3DS mais REFUSE le PaymentIntent au moment du prélèvement.)
4. Compléter le 3DS challenge.
5. Vérifier que l'org est bien `trialing` (comme scénario 1 jusqu'ici).
6. Avancer le test clock à J+14 (voir scénario 1).

### Vérifications

- [ ] **Terminal 1** : webhook `invoice.payment_failed` reçu.
- [ ] **DB org** : `subscription_status = 'past_due'`, `plan` reste `'pro'` (smart retries Stripe pendant 7 jours).
- [ ] **App SPA** : bandeau "Paiement échoué — mettez à jour votre carte" sur `/parametres/abonnement` (à implémenter — actuellement le UI affiche juste le status sans appel à l'action explicite, c'est OK pour V1).
- [ ] Si on attend 7 jours supplémentaires (test clock advance), `customer.subscription.deleted` arrive → org bascule en `free` avec `trial_ends_at` conservé en historique.

---

## Scénario 3 — Annulation depuis Customer Portal pendant l'essai

**Objectif** : valider qu'un user qui clique "Annuler mon abonnement" pendant l'essai garde son accès jusqu'à `trial_end` puis bascule en Free.

### Étapes

1. Signup + essai démarré comme scénario 1.
2. Aller sur `/parametres/abonnement` → cliquer **"Gérer mon abonnement"**.
3. Stripe Customer Portal s'ouvre. Cliquer **"Cancel subscription"**.
4. Choisir "At end of trial period" (option par défaut pendant un trial).

### Vérifications

- [ ] Webhook `customer.subscription.updated` reçu avec `cancel_at_period_end: true`.
- [ ] **DB org** : `cancel_at_period_end = true`, `subscription_status` reste `trialing`.
- [ ] **App SPA** : `/parametres/abonnement` affiche "Annulé · accès jusqu'au DD/MM" + bouton "Réactiver".
- [ ] Advance test clock à J+14 → webhook `customer.subscription.deleted` → DB : `plan='free'`, `subscription_status='canceled'`, `trial_ends_at` toujours posé (historique).

---

## Scénario 4 — Re-trial bloqué (idempotence garde-fou)

**Objectif** : vérifier que l'API renvoie 409 quand un user qui a déjà eu son essai tente d'en démarrer un second.

### Étapes

1. Reprendre un compte qui a déjà eu un trial (le user de scénario 1, par exemple).
2. Côté frontend, naviguer à nouveau sur `/onboarding/billing` et cliquer "Démarrer mon essai 14 jours".

### Vérifications

- [ ] **Network tab** : `POST /api/v1/billing/start-trial` → **409 Conflict** avec `error.code = 'trial_already_consumed'`.
- [ ] **SPA** : redirige vers `/parametres/abonnement` avec toast info "Essai déjà utilisé".
- [ ] **DB org** : aucune modification.

---

## Scénario 5 — Fallback "pas de CB"

**Objectif** : valider que l'user qui clique "Commencer en Free" peut continuer sans CB.

### Étapes

1. Signup neuf (`carol+free@rubis.test`).
2. `/onboarding/billing` → cliquer **"Pas de carte ? Commencer en Free (2 factures)"**.
3. L'app continue sur `/onboarding/compte` sans appel Stripe.

### Vérifications

- [ ] **DB org** : `plan='free'`, `stripe_customer_id=null`, `trial_ends_at=null`.
- [ ] **App** : limite Free 2 factures s'applique immédiatement (importer une 3e facture → 422 `free_limit_active_invoices`).
- [ ] **Network** : aucun appel `/billing/start-trial`.

---

## Cleanup post-tests

```bash
# Supprimer les test clocks créés
stripe test_helpers test_clocks list
stripe test_helpers test_clocks delete <CLOCK_ID>

# Supprimer les customers test (Dashboard → Customers → filtrer par metadata.test=true)
```

---

## Cas qui restent à automatiser (V2)

- **Playwright headless** : drive le Checkout iframe + 3DS modal. Faisable mais coûteux à maintenir vs gain réel — la chaîne `applicative` est déjà couverte par 60 tests unitaires + 16 tests E2E HTTP. Le seul gap restant est l'**UI Stripe** elle-même, qu'on ne contrôle pas et qui change régulièrement côté Stripe.
- **Test clock automation** : encapsuler le advance + assertion dans un helper japa réutilisable. Pertinent si on multiplie les scénarios timing.

Le scope V1 est : tests E2E auto sur **notre code**, playbook manuel pour **les surfaces Stripe**. Le ratio sécurité/effort est bon.
