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>
15 KiB
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
POST /api/v1/auth/signup—NewAccountController.storecrée Organization vide + provision les plans par défaut + crée User.- Émet une
AuthSession(Bearer + refresh cookie). - Aucun appel Stripe à l'inscription.
org.plan = 'free',stripeCustomerId = null,stripeSubscriptionId = null,gracePeriodEndsAt = null(nouvelles orgs). - User est redirigé vers
/onboarding/compte→/onboarding/entreprise→/onboarding/signature.
Upgrade Pro plus tard
- User va sur
/parametres/abonnement, clique « Passer Pro ». - SPA appelle
POST /api/v1/billing/checkout→ Stripe Checkoutmode: 'subscription'. - User paye via Checkout, est ramené sur
/parametres/abonnement?checkout=success. - Webhook
customer.subscription.created→org.plan = 'pro',stripeSubscriptionIdposé.
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
POST /api/v1/auth/signup— Organization + User créés comme aujourd'hui.- Nouveau :
POST /api/v1/billing/start-trial(auth, appelé juste après signup) → crée Stripe Customer + Checkout Session avecsubscription_data.trial_period_days: 14+payment_method_collection: 'always'. - SPA redirige vers l'URL Stripe Checkout.
- User remplit sa CB sur Checkout, est ramené sur
/onboarding/compte. La subscription est créée enstatus: 'trialing',org.plan = 'pro',subscriptionStatus = 'trialing'. - Onboarding standard se poursuit (compte → entreprise → signature).
- À 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)
- Nouveau :
src/routes/onboarding/billing.tsx— étape entre/signupet/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.
- Bouton primaire : « Démarrer l'essai 14 jours » → appelle
- Modifier :
src/main.tsxou le guard_app→ forcer le passage par/onboarding/billingtant queorg.stripeCustomerIdest null et que l'org ne s'est pas explicitement déclarée Free (cookie ou flag DBorg.skippedTrial). - Modifier :
src/components/billing/PlanLimitBanner.tsx→ afficher un mini-rappel pendant le trialing (« Essai Pro · X jours restants »). - Modifier : copy signup (
src/routes/signup.tsx) → préciser « Carte demandée à l'étape suivante, non prélevée avant J+14 ». - 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)
- Nouveau template React Email :
emails/trial_recap.tsx— recap J+12. - Nouveau template :
emails/trial_ended_success.tsx— confirme le passage en payant. - 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)
- Migration
add_trial_ends_at_to_organizations.ts. - Endpoint
POST /api/v1/billing/start-trialdansbilling_controller.ts. - Étendre le webhook handler :
checkout.session.completed→ persistertrialEndsAtdepuissubscription.trial_end.customer.subscription.trial_will_end→ trigger email recap (nouveau service).customer.subscription.updated(passagetrialing → active) → email confirmation.customer.subscription.updated(passagetrialing → past_due) → email fallback.
- Bypass
canCreateInvoicesquandsubscription_status === 'trialing'. - Tests unitaires (
tests/unit/billing.spec.ts). - Cron de redondance
trial-recap-cron(BullMQ ou node-cron quotidien).
PR 2 — Frontend tunnel (0,5 j)
- Route
src/routes/onboarding/billing.tsx. - Guard
_app: forcer/onboarding/billingsi trial pas encore offert. - Banner « Essai Pro · X jours restants » dans
PlanLimitBanner.tsx. - 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)
- Template
emails/trial_recap.tsx. - Template
emails/trial_ended_success.tsx. - Template
emails/trial_ended_failed.tsx. - Tester via Resend / mode dev.
PR 4 — Tests end-to-end (0,5 j)
- 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.
- Carte normale
- 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.createdAtformaté FR{{date_prelevement}}—org.trialEndsAtformaté FR{{prix_pro_ttc}}—19 €ou49 €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 :
- Tunnel forcé vs onboarding optionnel : l'écran
/onboarding/billingdoit-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. - 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é.
- 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. - 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.