rubis/docs/tech/stripe-trial-with-card.md
ordinarthur f9cba50b5e
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 1m30s
Build & Deploy API / build-and-deploy (push) Successful in 1m43s
Build & Deploy Web / build-and-deploy (push) Successful in 33s
feat(billing,landing): plan Free 2 factures + scaffold preuves sociales/SEO
Suite des chantiers structurants de landing-optimisations.md.

#5 — Plan Free : 5 → 2 factures actives (cf. ADR-023)
  - PLAN_CAPS.free.activeInvoicesLimit dans apps/api/app/services/billing.ts
  - Tests unitaires alignés (4 → 1, 5 → 2 cap, delta 3 → delta 2)
  - billing:scenario command : commentaires + valeur par défaut
  - PlanLimitBanner : copy dynamique via {limit} au lieu de "5" hardcodé
  - /parametres/abonnement : H1 + tile Free (3 mois → 14 jours, 5 → 2)
  - billing.test.tsx (fixtures + cas test)
  - landing copy : hero feature pill, Pricing tile, FinalCTA, CGV §5
  - CLAUDE.md pricing table

#7 — Scaffold <TrustedBy /> (preuve sociale)
  - Composant qui render null tant que copy.trustedBy.{logos,testimonials}
    sont vides — pas de placeholder bidon.
  - Structure data dans copy.ts avec commentaires sur les prérequis
    avant d'ajouter une entrée (accord signé, photo, citation chiffrée).
  - Section insérée juste avant <Pricing /> (cf. doc §4).

#8 — Plan articles SEO + brouillon article 1
  - docs/marketing/seo-articles.md : 5 articles ciblés, mots-clés,
    structure type, lead magnet, calendrier 5 semaines.
  - Article 1 ("Modèle d'email de relance facture impayée") en
    brouillon complet, prêt à valider via l'admin blog (apps/api).

#6 — Plan détaillé migration Stripe trial 14 j (code reporté)
  - docs/tech/stripe-trial-with-card.md : état actuel vs cible,
    architecture (Stripe Checkout + trial_period_days), modifs DB
    (trial_ends_at), API (start-trial + webhook trial_will_end),
    SPA (onboarding/billing), 3 emails transactionnels avec contenu
    intégral, risques + mitigations, plan d'exécution 2,5 j.
  - Implémentation reportée à une session focus avec accès Stripe
    test mode (cartes 3DS, webhook signing secret).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 10:38:52 +02:00

293 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Migration billing : essai 14 j Pro avec CB à l'inscription
> Version : 0.1 · Dernière maj : 2026-05-18
> Référence d'arbitrage : `docs/tech/landing-optimisations.md` §3 + §6
> Statut : **À implémenter** — pas encore en code. Ce doc décrit l'archi cible + plan d'exécution.
Document de chantier pour passer le funnel signup d'**aucun engagement** (Free 2 factures par défaut, Pro via Checkout au moment où l'usage dépasse la limite) à **essai 14 jours Pro avec carte bancaire à l'inscription** (Stripe `trial_period_days`, prélèvement automatique au J+14, possibilité d'annuler en un clic).
**Pourquoi** : un essai sans CB convertit 2 à 3 fois moins qu'un essai avec CB demandée (cf. études Profitwell, ChartMogul). Le segment cœur de Rubis (TPE-PME françaises sans crédit manager) a besoin d'être confronté à la décision payante dès qu'il a senti la valeur — sinon il oublie.
---
## État actuel (avant migration)
### Flow signup courant
1. `POST /api/v1/auth/signup``NewAccountController.store` crée Organization vide + provision les plans par défaut + crée User.
2. Émet une `AuthSession` (Bearer + refresh cookie).
3. **Aucun appel Stripe** à l'inscription. `org.plan = 'free'`, `stripeCustomerId = null`, `stripeSubscriptionId = null`, `gracePeriodEndsAt = null` (nouvelles orgs).
4. User est redirigé vers `/onboarding/compte``/onboarding/entreprise``/onboarding/signature`.
### Upgrade Pro plus tard
1. User va sur `/parametres/abonnement`, clique « Passer Pro ».
2. SPA appelle `POST /api/v1/billing/checkout` → Stripe Checkout `mode: 'subscription'`.
3. User paye via Checkout, est ramené sur `/parametres/abonnement?checkout=success`.
4. Webhook `customer.subscription.created``org.plan = 'pro'`, `stripeSubscriptionId` posé.
### Limites du modèle actuel
- Le segment cœur ne paye jamais : 2 factures Free (cf. ADR-023) suffisent pour beaucoup, et la friction d'aller chercher Checkout dans les Settings est forte.
- Pas de tunnel forcé : on perd les indécis qui se disent « j'essaie plus tard » et ne reviennent pas.
- L'orga historique (3 mois grace illimitée) habituait les users à un comportement opposé.
---
## État cible
### Flow signup cible
1. `POST /api/v1/auth/signup` — Organization + User créés comme aujourd'hui.
2. **Nouveau** : `POST /api/v1/billing/start-trial` (auth, appelé juste après signup) → crée Stripe Customer + Checkout Session avec `subscription_data.trial_period_days: 14` + `payment_method_collection: 'always'`.
3. SPA redirige vers l'URL Stripe Checkout.
4. User remplit sa CB sur Checkout, est ramené sur `/onboarding/compte`. La subscription est créée en `status: 'trialing'`, `org.plan = 'pro'`, `subscriptionStatus = 'trialing'`.
5. Onboarding standard se poursuit (compte → entreprise → signature).
6. À J+14, Stripe prélève automatiquement et passe la subscription en `status: 'active'`.
### Fallback « pas de CB »
Bouton secondaire sur le Checkout-redirect screen (apps/web) :
> **Pas de CB ? Démarrer en Free (2 factures)**
→ skip le Checkout, l'org reste `free` avec `gracePeriodEndsAt: null`. Cap immédiat à 2 factures. Récupère les ultra-réticents sans casser le funnel principal.
### Email J+12 (recap avant prélèvement)
Déclenché par le webhook Stripe `customer.subscription.trial_will_end` (Stripe l'émet automatiquement 3 jours avant `trial_end` = J+11 dans notre cas — assez proche du J+12 proposé par le doc). Si on veut exactement J+12, basculer sur un scheduler interne (BullMQ / cron node-cron) qui lit `trial_end - 2 days`.
Contenu : récap usage des 12 premiers jours (factures relancées, € récupérés, rubis gagnés) + rappel date prélèvement + lien annulation 1-clic vers Customer Portal.
---
## Architecture cible
### Modifications DB
| Champ | Avant | Après | Notes |
|---|---|---|---|
| `organizations.plan` | `'free'\|'pro'\|'business'` | `'free'\|'pro'\|'business'` | Inchangé. `'pro'` couvre trial + paid. |
| `organizations.subscription_status` | `'active'\|'past_due'\|'canceled'\|...` | + `'trialing'` | Stripe l'émet déjà, on le persiste tel quel. |
| `organizations.trial_ends_at` | n'existe pas | `timestamp with timezone NULL` | Date de fin d'essai (= Stripe `subscription.trial_end`). Sert au teaser UI sans rappel Stripe à chaque page. |
| `organizations.grace_period_ends_at` | conservé | conservé | Pour les orgs historiques. Nouvelles orgs : `null`. |
Migration : ajouter `trial_ends_at` ; backfill à null pour les orgs existantes (elles ne sont pas en trial).
### Modifications API
| Endpoint | État | Action |
|---|---|---|
| `POST /api/v1/billing/start-trial` | Nouveau | Crée Customer + Checkout Session avec `trial_period_days: 14`, return URL. |
| `POST /api/v1/billing/checkout` | Existant | Conserver — pour les users qui sont passés Free (fallback) et veulent upgrader plus tard. **Sans trial dans ce cas** (déjà eu son essai). |
| `POST /api/v1/billing/webhook` | Existant | Étendre pour gérer `customer.subscription.trial_will_end` → trigger email J+12. |
| `services/billing.ts` `canCreateInvoices` | Existant | Bypass quota si `subscription_status === 'trialing'` (Pro illimité pendant l'essai). |
### Modifications Frontend (apps/web)
1. **Nouveau** : `src/routes/onboarding/billing.tsx` — étape entre `/signup` et `/onboarding/compte`.
- Bouton primaire : « Démarrer l'essai 14 jours » → appelle `start-trial` → redirige vers Stripe Checkout.
- Bouton secondaire : « Pas de carte ? Commencer en Free (2 factures) » → skip, redirige `/onboarding/compte`.
2. **Modifier** : `src/main.tsx` ou le guard `_app` → forcer le passage par `/onboarding/billing` tant que `org.stripeCustomerId` est null **et** que l'org ne s'est pas explicitement déclarée Free (cookie ou flag DB `org.skippedTrial`).
3. **Modifier** : `src/components/billing/PlanLimitBanner.tsx` → afficher un mini-rappel pendant le trialing (« Essai Pro · X jours restants »).
4. **Modifier** : copy signup (`src/routes/signup.tsx`) → préciser « Carte demandée à l'étape suivante, non prélevée avant J+14 ».
5. **Modifier** : landing `Hero.tsx` / `FinalCTA.tsx` — pouvoir réactiver le sous-texte CTA « CB demandée, non prélevée avant J+14 » (actuellement masqué — cf. cecbddc).
### Modifications Emails (apps/api)
1. **Nouveau template React Email** : `emails/trial_recap.tsx` — recap J+12.
2. **Nouveau template** : `emails/trial_ended_success.tsx` — confirme le passage en payant.
3. **Nouveau template** : `emails/trial_ended_failed.tsx` — paiement échoué, fallback Free + lien CB.
Contenu détaillé des trois emails en annexe ↓.
---
## Risques et mitigations
| Risque | Impact | Mitigation |
|---|---|---|
| Le Stripe Checkout est hosté → on perd le contrôle UX à l'étape la plus stressante du tunnel | Conversion -10 à -20 % vs Elements custom | Accepter en V1, on rebascule sur Payment Element custom en V2 si la conversion plafonne. Checkout est plus rapide à shipper et fiable côté compliance. |
| Webhook `trial_will_end` peut être manqué (réseau, redéploiement) | Email J+12 raté → user surpris par le prélèvement | Doubler avec un job cron quotidien (`trial-recap-cron`) qui scanne les orgs avec `subscription_status = 'trialing'` ET `trial_ends_at` dans 1-3 jours et envoie l'email idempotemment (table `email_log` avec dédoublonnage). |
| Org historique (3 mois grace) avec orgs récentes mêlées dans la table | Migration backfill incorrecte | Le champ `grace_period_ends_at` reste intouché. Le nouveau flow ne pose `trial_ends_at` que sur les orgs qui passent par `start-trial`. Pas de conflit. |
| Stripe locale FR + 3D Secure | Friction supplémentaire sur certaines cartes | Locale `fr` déjà passée au Checkout, 3DS géré nativement par Stripe. Tester en mode test avec cartes 3DS `4000 0027 6000 3184`. |
| User ferme Stripe Checkout sans valider → state incohérent | Compte créé sans CB ni redirect onboarding | Côté webhook `checkout.session.expired` → ne rien faire. Côté UI, gérer le cas `?checkout=cancel` sur `/onboarding/billing` : afficher le fallback Free + bouton « réessayer avec CB ». |
| Migration des orgs en grace period (ils ne devraient pas voir l'écran billing) | UX confuse pour les early users | Le guard `_app` ne force `/onboarding/billing` que si `org.gracePeriodEndsAt` est null. Les orgs historiques continuent leur vie sans changement. |
---
## Plan d'exécution
### PR 1 — Backend (1 j)
1. Migration `add_trial_ends_at_to_organizations.ts`.
2. Endpoint `POST /api/v1/billing/start-trial` dans `billing_controller.ts`.
3. Étendre le webhook handler :
- `checkout.session.completed` → persister `trialEndsAt` depuis `subscription.trial_end`.
- `customer.subscription.trial_will_end` → trigger email recap (nouveau service).
- `customer.subscription.updated` (passage `trialing → active`) → email confirmation.
- `customer.subscription.updated` (passage `trialing → past_due`) → email fallback.
4. Bypass `canCreateInvoices` quand `subscription_status === 'trialing'`.
5. Tests unitaires (`tests/unit/billing.spec.ts`).
6. Cron de redondance `trial-recap-cron` (BullMQ ou node-cron quotidien).
### PR 2 — Frontend tunnel (0,5 j)
1. Route `src/routes/onboarding/billing.tsx`.
2. Guard `_app` : forcer `/onboarding/billing` si trial pas encore offert.
3. Banner « Essai Pro · X jours restants » dans `PlanLimitBanner.tsx`.
4. Copy signup + landing (réactiver le sous-texte CTA « CB demandée, non prélevée avant J+14 »).
### PR 3 — Emails (0,5 j)
1. Template `emails/trial_recap.tsx`.
2. Template `emails/trial_ended_success.tsx`.
3. Template `emails/trial_ended_failed.tsx`.
4. Tester via Resend / mode dev.
### PR 4 — Tests end-to-end (0,5 j)
1. Tester en mode test Stripe :
- Carte normale `4242 4242 4242 4242` → trial OK → prélèvement à J+14 OK.
- Carte 3DS `4000 0027 6000 3184` → 3DS OK → trial OK.
- Carte déclinée à J+14 `4000 0000 0000 0341` → email fallback Free OK.
2. Vérifier idempotence webhook (renvoyer 2× le même event).
Total : **2,5 j de dev**, alignés sur l'estimation initiale du doc landing-optimisations.md §6.
---
## Annexe — Contenu des emails transactionnels
### Email J+12 — Recap d'essai
**Sujet** : Plus que 2 jours d'essai — voilà ce que Rubis a déjà fait pour vous.
**Preheader** : X factures relancées, Y € récupérés, Z minutes libérées. Récap avant le prélèvement.
**Body** :
```
Bonjour {{prenom}},
12 jours que vous testez Rubis. Petit récap avant le prélèvement de
votre abonnement Pro dans 2 jours (le {{date_prelevement}}).
Ce que Rubis a fait pour vous depuis le {{date_signup}} :
◆ {{nb_factures_importees}} factures importées
◆ {{nb_relances_envoyees}} relances envoyées automatiquement
◆ {{euros_recuperes}} € encaissés
◆ {{nb_rubis_gagnes}} rubis ≈ {{heures_liberees}} de votre temps
que vous n'avez pas passé à relancer
[ICI un bloc visuel avec les 4 chiffres, gros, rubis sur crème]
Si tout est OK, vous n'avez rien à faire — votre essai bascule en Pro
mensuel à {{prix_pro_ttc}} TTC le {{date_prelevement}}. Vous gardez
toutes vos données, tous vos plans de relance, toutes vos relances en
cours.
Si vous voulez annuler, c'est en un clic depuis vos paramètres :
[Annuler mon essai →]
Vos relances en cours s'arrêteront le {{date_prelevement}} et vous
basculerez automatiquement sur le plan Free (2 factures actives).
Une question ? Répondez à ce mail, je le lis personnellement.
Belle journée,
Arthur — fondateur de Rubis
```
**Variables à interpoler** :
- `{{prenom}}``user.fullName.split(' ')[0]`
- `{{date_signup}}``org.createdAt` formaté FR
- `{{date_prelevement}}``org.trialEndsAt` formaté FR
- `{{prix_pro_ttc}}``19 €` ou `49 €` selon plan
- `{{nb_factures_importees}}` — count(invoices where org_id=… and created_at >= signup_date)
- `{{nb_relances_envoyees}}` — count(reminder_logs where status='sent' and org_id=…)
- `{{euros_recuperes}}` — sum(amount_cents) where status='paid' and paid_at >= signup_date
- `{{nb_rubis_gagnes}}` — org.rubisCount
- `{{heures_liberees}}` — formatRubisToHours(rubisCount)
### Email J+14 — Trial → active (succès)
**Sujet** : Bienvenue dans Rubis Pro — votre paiement est passé.
**Body** :
```
Bonjour {{prenom}},
Votre essai est terminé et le prélèvement de {{prix_pro_ttc}} a été
effectué avec succès. Vous êtes officiellement en Pro.
Pas de changement de votre côté — toutes vos factures, plans de
relance et automatisations continuent comme avant, sans aucune
interruption.
Trois choses utiles à connaître maintenant :
◆ Votre facture Stripe est disponible dans vos paramètres
[Voir mes factures Rubis →]
◆ Vous pouvez changer de carte ou de cycle (annuel = 2 mois
gratuits) en un clic
[Gérer mon abonnement →]
◆ Vous pouvez annuler à tout moment depuis le même écran. Aucune
question posée.
Merci de votre confiance.
Arthur — fondateur de Rubis
```
### Email J+14 — Trial → past_due (échec paiement)
**Sujet** : Votre essai Rubis a expiré — paiement à régulariser.
**Body** :
```
Bonjour {{prenom}},
Votre essai 14 jours est arrivé à son terme, mais le prélèvement de
{{prix_pro_ttc}} sur votre carte n'a pas pu aboutir
({{stripe_decline_code_humain}}).
Pas de panique : pendant 7 jours, votre compte reste en Pro et toutes
vos relances continuent comme avant. Il vous suffit de mettre à jour
votre carte pour rester sur Pro :
[Mettre à jour ma carte →]
Au-delà de 7 jours sans paiement valide, votre compte bascule
automatiquement sur le plan Free (2 factures actives). Vos données
restent intactes, mais les relances au-delà de 2 factures simultanées
sont mises en pause.
Une question ? Répondez à ce mail, je vous réponds dans la journée.
Belle journée,
Arthur — fondateur de Rubis
```
---
## Décisions ouvertes
À documenter en ADR au moment de l'implémentation :
1. **Tunnel forcé vs onboarding optionnel** : l'écran `/onboarding/billing` doit-il être bloquant (pas d'accès au reste sans choix CB ou Free) ou skippable (« plus tard ») ? Recommandation : bloquant, sinon on perd le bénéfice principal du tunnel.
2. **Stripe Checkout vs Payment Element custom** : tradeoff UX vs time-to-ship. Recommandation V1 : Checkout (compliance gratuite, locale FR native). V2 si la conversion plafonne : Payment Element intégré.
3. **Email J+12 timing exact** : Stripe émet `trial_will_end` à J+11 par défaut. On peut soit accepter (1 jour d'écart sans importance), soit overrider via cron interne pour avoir J+12 pile. Recommandation : accepter le J+11 Stripe (moins de complexité), reformulater le copy "Plus que 3 jours" au lieu de "Plus que 2 jours" si besoin.
4. **Org historique en grace** : on les laisse purger naturellement leurs 3 mois, ou on les force à passer par l'écran trial dès leur prochain login ? Recommandation : laisser purger — moins de churn brutal.
---
*Document maintenu en parallèle de `docs/tech/landing-optimisations.md`. Implémentation à faire dans une session dédiée avec accès Stripe test mode + clé webhook signing secret de test.*