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

15 KiB
Raw Blame History

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/signupNewAccountController.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.createdorg.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.