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