rubis/docs/tech/stripe-trial-e2e-playbook.md
ordinarthur 094c26059f
All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m18s
test(billing): tests E2E HTTP du tunnel essai 14 j + playbook Stripe test mode
Ajoute 16 tests E2E qui hit les vraies routes `/api/v1/billing/*` à
travers le middleware auth, les validators et la persistance DB.
Complémentaire des 60 tests unitaires sur les services.

Suites couvertes :
  - POST /start-trial : 200 happy path, customer Stripe réutilisé,
    409 trial déjà consommé (2 garde-fous), 401 sans Bearer
  - GET  /subscription : expose inTrial + trialEndsAt, garde-fou
    trial_ends_at passé
  - POST /webhook : checkout.completed, subscription.updated trialing→active,
    trial_will_end → enqueue recap (avec spy), payment_failed → past_due,
    subscription.deleted → free + trial_ends_at conservé
  - Idempotence : 2× le même event = même état final
  - Event type inconnu → 200 silencieux (pas de DB write)
  - 400 si stripe-signature absent / signature invalide

Helpers de test :
  - installFullStripeMock(opts) → mock complet : customers, prices,
    checkout, billingPortal, subscriptions, webhooks. Avec
    passThroughWebhook qui bypass la vérif signature pour tester
    le routing applicatif sans signer manuellement chaque payload.

env.test : STRIPE_SECRET_KEY + STRIPE_WEBHOOK_SECRET dummy +
WEB_URL/LANDING_URL.

Documentation : docs/tech/stripe-trial-e2e-playbook.md — playbook
manuel pour valider en mode Stripe test (5 scénarios : happy path
3DS, carte refusée au prélèvement, annulation Customer Portal,
re-trial bloqué, fallback Free). Utilise Stripe Test Clocks pour
fast-forward sans attendre 14 jours réels.

Total après ce commit : 76 tests sur la chaîne billing (60 unit + 16 E2E).
Les cas Stripe-side (3DS UI réel, prélèvement effectif J+14) restent
à valider manuellement via le playbook avant le go-live.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 14:01:16 +02:00

11 KiB

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

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 :

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

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 :

# 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 :

# 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 :

# 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:
    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

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-trial409 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

# 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.