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>
293 lines
15 KiB
Markdown
293 lines
15 KiB
Markdown
# 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.*
|