Pricing V1 :
- Free : 5 factures actives, 1 user, 3 mois de grâce illimité au signup
- Pro : 19 €/mois ou 190 €/an, factures illimitées, 1 user
- Business : 49 €/mois ou 490 €/an, illimité + 5 sièges (V2 multi-users)
+ reply-from-user-email (V2)
Backend :
- Migration : plan, grace_period_ends_at, stripe_customer_id,
stripe_subscription_id, subscription_status, billing_cycle,
current_period_end sur `organizations`. Backfill grace_period auto.
- `app/services/billing.ts` : PLAN_CAPS, countActiveInvoices,
canCreateInvoices (enforce post-grace), getOrgSubscriptionState.
- `app/services/stripe.ts` : client lazy + lookup_keys stables.
- `app/controllers/billing_controller.ts` :
• GET /billing/subscription → state pour l'UI
• POST /billing/checkout → crée une Checkout Session
• POST /billing/portal → Customer Portal Session
• POST /billing/webhook (public) → handle 4 events Stripe
(checkout.completed, subscription.updated/deleted, invoice.payment_failed)
- `commands/stripe_setup.ts` : `node ace stripe:setup` crée Products +
Prices (idempotent via lookup_key).
- Enforcement 402 `plan_limit_reached` sur :
• POST /invoices (saisie manuelle)
• POST /invoices/import-batch/:id/drafts/:draftId/validate (OCR)
Frontend :
- `lib/billing.ts` : useSubscription, useStartCheckout, useOpenPortal,
useIsAtFreeLimit.
- `routes/_app/parametres_.abonnement.tsx` : page comparaison plans
avec toggle mensuel/annuel, current plan + portail Stripe, CTA upgrade
qui redirige vers Checkout hostée.
- `routes/_app/parametres.tsx` : nouvelle section "Abonnement" qui
affiche le plan courant + lien vers la page abonnement.
- `components/billing/PlanLimitBanner.tsx` : banner sur /factures qui
s'adapte selon période (grâce / approche / atteinte).
- Toast dédié 402 sur la validation OCR avec action "Passer Pro".
Doc :
- flow.md : nouvelle section §11 "Pricing & enforcement" qui couvre
plans, grâce, webhook flow, Customer Portal, env vars.
Setup dev :
1. STRIPE_SECRET_KEY (sk_test_...) dans apps/api/.env
2. `stripe listen --forward-to localhost:3333/api/v1/billing/webhook`
→ copier whsec_... → STRIPE_WEBHOOK_SECRET
3. `node ace stripe:setup` une fois pour créer Products+Prices
4. Tester via /parametres/abonnement → checkout en mode test Stripe
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
26 KiB
Flow produit — Rubis Sur l'Ongle
Cette doc décrit comment Rubis se comporte côté user-lambda : quel statut une facture prend, quand elle change d'état, qui voit quoi quand. Pour la spec produit haut-niveau (cible, pricing, IN/OUT V1) →
produit.md. Pour les décisions architecturales →decisions.md.
1. Modèle mental en une phrase
Une facture passe par un nombre limité d'états bien définis, et toute transition est soit déclenchée par l'utilisateur (mark paid, "non impayée"), soit par le scheduler (envoi d'un mail, fin de plan). Aucune transition silencieuse n'envoie de mail au client final sans validation humaine — c'est la promesse centrale de la marque.
2. Glossaire produit
| Terme | Définition |
|---|---|
| Rubis | Unité de gamification. 1 rubis = 10 minutes libérées = 1 relance que l'user n'a pas eu à faire à la main. Crédité sur l'org à chaque mark-paid. |
| Plan de relance | Cadence d'emails programmés (ex. J+3, J+10, J+25) avec un ton et un contenu par étape. Une facture est associée à 0 ou 1 plan. |
Étape (PlanStep) |
Un email programmé dans un plan. Possède : un offsetDays (relatif à la dueDate), un ton, un sujet, un body, et un flag requiresManualValidation (pour la mise en demeure). |
| Confirmation | (Anciennement "check-in"). Mécanique cœur du produit : Rubis demande à l'user "cette facture a-t-elle été payée ?" avant d'envoyer la prochaine relance. Voir §5. |
| Mise en demeure | Étape ferme du plan, avec mention LME explicite. Toujours sous validation manuelle via modale de confirmation, jamais auto. |
| DSO | Days Sales Outstanding — délai moyen entre l'émission d'une facture et son paiement. Métrique secondaire. |
| LME | Loi de Modernisation de l'Économie (2008). Plafonne les délais de paiement à 60j (ou 45j fin de mois). Encadre la rédaction des mises en demeure. |
3. Cycle de vie d'une facture
3.1 Les 6 états
Une facture est toujours dans exactement un des états suivants (Invoice.status) :
| État | Sens produit | Visible dans le filtre |
|---|---|---|
pending |
Facture fraîchement importée, pas encore relancée. Attente du premier check-in (qui partira à dueDate). |
"À relancer" |
awaiting_user_confirmation |
Le check-in a été envoyé à l'user, on attend qu'il réponde "payée ?" ou "toujours impayée ?". Le scheduler ne fait rien tant que l'user n'a pas répondu. | "À valider" |
in_relance |
L'user a confirmé l'impayé → le plan tourne. Au moins une relance a été ou est sur le point d'être envoyée au client. | "En relance" |
paid |
Facture réglée. paidAt populé, +1 rubis crédité, futures relances cancelled. |
"Encaissées" |
litigation |
Facture contestée par le client (refus, désaccord, longue impayé). Pas de relance auto, action manuelle requise (huissier, recommandé, médiation). | "Litige" |
cancelled |
Facture annulée (avoir, doublon, erreur). Sortie du portefeuille actif, conservée en historique. | (pas dans les chips, accessible "Toutes") |
3.2 Diagramme de transitions
┌─────────────────────────┐
│ Import / saisie │
│ manuelle │
└────────────┬────────────┘
↓
┌────────┐
│ pending │
└───┬─────┘
│
Le scheduler envoie le check-in
à dueDate (= jour de l'échéance)
↓
┌─────────────────────────────────────────┐
│ awaiting_user_confirmation │ ←──── état pivot
└────┬────────────────┬────────────────┬──┘ (UI insiste ici :
│ │ │ modale au login,
Réponse "Oui"│ │ Réponse │ bouton fiche
(payée) │ │ "Non" │ facture)
↓ ↓ ↓
┌────────┐ ┌────────────┐ (dismiss → reste
│ paid │ │ in_relance │ en awaiting jusqu'à
└────────┘ └─────┬──────┘ reprise par l'user)
│
│ scheduler envoie chaque
│ étape à `dueDate + offset`
│
│ user clique "Marquer encaissée"
│ depuis la fiche facture ↓
↓
┌────────┐
│ paid │
└────────┘
Transitions manuelles (par l'user, depuis la fiche facture) :
• [tout état actif] → litigation (TODO V1.5 — UI à câbler)
• [tout état] → cancelled (TODO V1.5 — UI à câbler)
• paid → (pas de retour, sauf bug — il y a un endpoint mark-unpaid V2 prévu)
3.3 Détails par transition
pending → awaiting_user_confirmation
- Qui déclenche : le scheduler
CheckinTask, automatiquement, quanddueDateest atteinte. - Effet : un email check-in part à l'user (pas au client) avec 2 liens Oui (payée) / Non (toujours impayée). La
CheckinTaskest marquéesent. - Note V1 : dans la prod actuelle, le statut DB de l'invoice reste techniquement
pendingjusqu'à ce que l'user réponde — c'est le seed démo qui forceawaiting_user_confirmationpour pré-peupler des cas. À aligner V1.5 (le jobsend_checkin_jobdevrait push le statut).
awaiting_user_confirmation → paid (réponse "Oui")
- Qui déclenche : l'user, via 4 surfaces possibles :
- Modale in-app au login (
InAppCheckinModal) — bouton Oui — la facture est payée - Lien
paiddans l'email check-in - Fiche facture — bouton Marquer encaissée
- Slide-over en mode démo (
DemoEmailSlide)
- Modale in-app au login (
- Effet en cascade :
Invoice.status = 'paid',Invoice.paidAt = nowInvoice.rubisEarned += 1,Organization.rubisCount += 1- Toutes les
RelanceTaskscheduledfutures →cancelled(jobs BullMQ removed) CheckinTask.status = 'answered',answer = 'paid',answeredAt = now- Activity event
invoice_paid
awaiting_user_confirmation → in_relance (réponse "Non")
- Qui déclenche : l'user, via les mêmes 4 surfaces.
- Effet :
Invoice.status = 'in_relance'CheckinTask.status = 'answered',answer = 'still_pending'scheduleRelancesForInvoice()est appelé :- Récupère le plan associé à la facture
- Pour chaque step : crée une
RelanceTaskavecsendAt = invoice.dueDate + step.offsetDays - Si certains
sendAtsont déjà passés (cas d'une facture en retard), la 1re étape éligible est calée ànow + 1minpuis les suivantes gardent l'écart prévu (cf.catchUpAnchordansrelance_scheduler.ts) - Enqueue chaque task dans BullMQ avec
delay = sendAt - now
- Activity event
relance_sent
in_relance → paid (mark paid manuel)
- Qui déclenche : l'user via le bouton Marquer encaissée sur la fiche facture (typiquement après avoir reçu un virement).
- Effet : identique au "Oui" depuis check-in (mark paid + cancel relances + bonus rubis).
in_relance → litigation (V1.5)
- Pas encore câblé en UI. Endpoint backend prévu :
POST /invoices/:id/litigatequi :- Status →
litigation - Cancel les relances futures
- (Optionnel) génère un brouillon de mise en demeure si pas déjà envoyé
- Status →
* → cancelled (V1.5)
- Toute facture sortie du flux normal (annulée, doublon).
- Pas de comptage dans le CA, pas dans les graphes.
4. Surfaces UI — où l'user agit
4.1 Modale check-in au login (InAppCheckinModal)
Quand : à chaque ouverture de l'app si pending non vide (factures awaiting_user_confirmation).
Comportement :
- Affiche toujours
queue[0]— l'invoice la plus ancienne en attente - L'user a 4 options :
- Oui → mark paid (cf. transition ci-dessus)
- Non → schedule relances + status → in_relance
- Plus tard → skip cette facture pour la session (set local en mémoire), passe à
queue[1] - X (close) → ferme la modale pour ce moment seulement
- Si l'user ferme et revient sur l'onglet (
focus/visibilitychange),dismissedest reset àfalse→ la modale re-pop si pending est encore non-vide. TanStack Query refetch sur focus en bonus. - Pas de persistance
sessionStorage: le user qui dismiss accède toujours aux factures via/factures?status=awaiting_user_confirmation(chip "À valider").
Mobile vs desktop :
- Mobile : bottom-sheet (slide-from-bottom, drag handle, safe-area-inset-bottom)
- Desktop : modale centrée, max 520px
4.2 Email check-in
Quand : envoyé automatiquement par le scheduler à dueDate de chaque facture (sauf si déjà payée ou cancelled).
Contenu :
- À : l'utilisateur Rubis (pas le client final)
- Reply-to : l'utilisateur lui-même (au cas où il répondrait)
- Sujet : "Facture F-XXXX — payée par CLIENT_NAME ?"
- Body avec 2 liens :
https://app.rubis.../api/v1/checkin/:token/paid→ respondPaid → redirect SPA?checkin=paidhttps://app.rubis.../api/v1/checkin/:token/pending→ respondPending → redirect SPA?checkin=pending
- Token signé (32 bytes random base64url, hash SHA-256 stocké en DB), TTL 24h.
Sécurité :
- Si le token est invalide / inconnu / expiré → redirect SPA
?checkin=invalid|expired - Si l'invoice a été payée entre-temps →
?checkin=already_answered - Pas de réponse possible 2× (idempotent côté serveur).
4.3 Fiche facture (/factures/:id)
Header avec 2 boutons d'action quand status ∈ {pending, awaiting_user_confirmation, in_relance} :
| Bouton | Visible si | Effet |
|---|---|---|
| Marquer encaissée | tout sauf paid/cancelled/litigation | Mark paid (transition vers paid) |
| Relancer maintenant | pending ou awaiting_user_confirmation |
Lance les relances (transition vers in_relance) |
Le 2e bouton est désactivé pour in_relance (le plan tourne déjà, pas de re-trigger possible côté UI V1).
Sur la timeline de la fiche, on affiche tous les events :
invoice_imported(toujours, àissueDate)relance_sent(1 par relance déjà envoyée)invoice_paid(si payée)
Et toutes les RelanceTask du plan, avec leur statut visuel :
- Envoyée après votre confirmation (
task.status = 'sent') - Confirmation avant envoi (
task.status = 'scheduled') — wording uniforme, rappelle qu'aucun mail ne part sans validation - Annulée — facture encaissée (
task.status = 'cancelled')
Au-dessus de la timeline : un bandeau rassurant "Aucune relance ne part sans votre validation. Avant chaque envoi, Rubis vous demande si la facture a été réglée — vous gardez la main."
4.4 Slide-over démo (DemoEmailSlide)
Quand : en mode démo, à chaque event qui fire (relance ou check-in dû). L'horloge virtuelle est en pause tant que l'user n'a pas acquitté.
Comportement :
- Étape 1 ("ask") : même question qu'en réel, mêmes 2 boutons Oui/Non
- "Oui" → mark paid + écran "Encaissée"
- "Non" → écran preview de l'email qui aurait été envoyé (pas envoyé pour de vrai en démo, capturé en BDD via
demo_captured_emails) - "Continuer la démo" → reprend l'horloge
Mobile : bottom-sheet (idem modale check-in). Desktop : slide-over droit (h-screen, max 520px).
5. Le mécanisme de confirmation (deep dive)
5.1 Pourquoi cette mécanique
C'est la promesse centrale de Rubis : aucune relance ne part sans que l'user ait dit "oui, vraie impayée, lance".
Sans ça, le SaaS ferait peur (peur de relancer un client qui vient de payer). Avec ça, le user contrôle, et Rubis automatise tout le reste.
5.2 Architecture des CheckinTask
- Un
CheckinTaskest créé à la création de l'invoice (sauf sipendingfuture) — programmé pourdueDate. - Au moment où le job tourne (queue
checkins), il envoie l'email à l'user, marque la tasksent, mais ne change PAS le statut de l'invoice côté prod (TODO V1.5 — bascule enawaiting_user_confirmationquand l'email est envoyé). - L'user a 24h (TTL) pour cliquer un des 2 liens email. Au-delà, la task expire (status
expired) — elle ne refire pas, mais l'user peut toujours répondre via la modale in-app ou la fiche.
5.3 Architecture des RelanceTask
- Créées uniquement quand l'user répond "Non" au check-in (ou clique "Relancer maintenant"). Pas avant.
- Une
RelanceTaskcorrespond à unPlanStep× uneInvoice. Status :scheduled: en attente de firesent: email envoyé OKcancelled: annulée (facture mark paid, statut litigation, etc.)failed: échec d'envoi définitif (5 retry exponentiels)
- Le job BullMQ
relance-{taskId}fire àtask.sendAt. Idempotent : si la task n'est plusscheduled, no-op.
5.4 Les 4 surfaces qui peuvent répondre au check-in
Toutes appellent in fine la même logique métier (mêmes mutations DB, mêmes effets) :
| Surface | Endpoint | Auth |
|---|---|---|
Email lien paid |
GET /api/v1/checkin/:token/paid |
Token URL |
Email lien pending |
GET /api/v1/checkin/:token/pending |
Token URL |
| Modale in-app — Oui | POST /api/v1/checkin/inapp/:invoiceId/paid |
Bearer auth |
| Modale in-app — Non | POST /api/v1/checkin/inapp/:invoiceId/pending |
Bearer auth |
| Fiche facture — Marquer encaissée | POST /api/v1/invoices/:id/mark-paid |
Bearer auth |
| Fiche facture — Relancer maintenant | POST /api/v1/checkin/inapp/:invoiceId/pending |
Bearer auth |
| Démo — Oui | POST /api/v1/invoices/:id/mark-paid |
Bearer auth |
6. Plans de relance
6.1 Structure
Plan
├── name: "Standard B2B"
├── slug: "standard-b2b"
├── description: "Plan équilibré pour la majorité des factures"
└── steps[]
├── PlanStep { offsetDays: 3, tone: "amical", subject, body, requiresManualValidation: false }
├── PlanStep { offsetDays: 10, tone: "ferme", subject, body, requiresManualValidation: false }
└── PlanStep { offsetDays: 25, tone: "stricte", subject, body, requiresManualValidation: true }
↑
mise en demeure → bouton manuel,
jamais d'envoi auto
6.2 Plans pré-fournis
| Plan | Cadence | Cible |
|---|---|---|
| Standard B2B | J+3, J+10, J+25 (mise en demeure brouillon) | Majorité des factures |
| Rapide | J+1, J+5, J+10 — ton ferme | Cash flow tendu |
| Patient | J+15, J+30, J+60 — ton doux | Clients VIP, partenaires |
| Ferme | J+0, J+5, J+10, J+15 — ton ferme dès le départ | Grosses factures, clients à risque |
6.3 Création custom
Wizard 4 étapes (/plans/nouveau) :
- Identité : nom, description, slug auto
- Cadence : éditeur calendrier visuel pour placer les J+X
- Messages : pour chaque étape, choisir ton + écrire/IA-générer le contenu (avec variables
{{client.name}},{{numero}}, etc.) - Récap : preview avant création
6.4 Variables dans les templates
Disponibles dans subject et body des steps :
{{client.name}},{{client.email}},{{client.contactFirstName}},{{client.contactLastName}}{{user.fullName}},{{user.companyName}}{{numero}},{{amount}},{{dueDate}},{{issueDate}}{{daysLate}}(jours de retard arrondis, démo-aware viaclock.now()){{signature}}(de l'user, posée en /parametres)
Render via une fonction de substitution simple (pas Mustache section, pas de logique conditionnelle).
7. Mode démo
7.1 Flag d'activation
Organization.demoMode = true (toggle dans /parametres). Tant que ce flag est false, rien dans l'app ne dévie de la prod.
7.2 Effets côté prod
Une seule branche dans le code prod, dans mail_dispatcher.ts → captureEmailIfDemo() :
- Si
org.demoMode = true→ l'email n'est PAS envoyé via SMTP/Resend, capturé en BDD (demo_captured_emails) - Sinon → envoi normal
Tout le reste (idempotence, status update, rubis bump, BullMQ, scheduling) tourne identique à la prod.
7.3 Horloge virtuelle
Organization.virtualNow (timestamp). En mode démo, toutes les fonctions qui lisent l'heure passent par clock.now(orgId) qui :
- En prod (demoMode=false) →
DateTime.utc() - En démo →
org.virtualNow(cache 250ms pour éviter le spam DB)
Conséquence : on peut faire avancer l'horloge à 1x/2x/5x via le hook useDemoTick côté SPA (rAF loop + sync backend toutes les 250ms). À chaque event qui fire (relance dûe, check-in dû), l'horloge se met en pause et le slide-over s'ouvre.
7.4 Sortir du mode démo
/demo/end — l'org repasse en demoMode=false. La boîte capturée reste consultable (on ne wipe pas).
8. KPIs et calculs
8.1 Compteur rubis
- Crédité +1 à chaque
invoice_paid(mark paid). Sur l'invoice (rubisEarned) ET sur l'org (rubisCount). - "Rubis ce mois" = somme des
rubisEarneddes factures payées avecpaidAt >= startOfMonth(now). - Conversion 1 rubis = 10 minutes libérées affichée partout (ex: 18 rubis ≈ 3h libérées).
8.2 Encaissé
Somme des amountTtcCents des factures status = 'paid' :
- Sur le mois (KPI dashboard)
- Sur N derniers mois (chart Insights, bucket mensuel ou hebdo selon range)
- Delta vs mois précédent (KPI dashboard)
8.3 DSO (Days Sales Outstanding)
Pour chaque facture payée : daysToPayment = paidAt - issueDate.
DSO du mois = moyenne arithmétique sur les factures paid_at >= startOfMonth(now).
8.4 Pipeline (chart Insights)
Donut + légende avec count + montant TTC par statut :
- pending, awaiting_user_confirmation, in_relance, litigation, paid
cancelledexclus (bruit).
9. Edge cases & règles à connaître
9.1 Plan changé sur facture en cours
Si l'user change le plan d'une facture qui est in_relance :
- Les
RelanceTaskscheduleddu précédent plan sontcancelled(jobs BullMQ removed). - Les nouvelles tasks sont créées selon le nouveau plan.
- Les tasks
sentrestent intactes (historique).
9.2 Facture marquée payée pendant qu'une relance est en queue
Le worker BullMQ check invoice.status === 'paid' au moment de fire :
- Si oui → cancel la task, no-op.
- Sinon → envoi normal.
C'est ce qui évite l'envoi d'une relance après mark paid.
9.3 Idempotence du check-in
Si l'user clique 2× le lien /checkin/:token/paid :
- 1er clic :
task.status = 'answered', redirect SPA avec succès. - 2e clic : task déjà answered → redirect
?checkin=already_answered(toast "Cette confirmation avait déjà été traitée").
9.4 Re-pop modale au refocus
L'user qui dismiss la modale puis bascule sur Slack et revient → modale re-pop si pending non vide (cf. §4.1). Pas de spam si l'user reste sur l'onglet.
9.5 Mode démo et signature
L'User.signature (posée en /parametres) est interpolée dans tous les templates {{signature}}. En démo, mêmes signatures que prod — la capture montre exactement ce qui partirait en réel.
10. Métriques produit à instrumenter
(reprise + précisions vs produit.md §9)
- Taux de réponse au check-in : % des CheckinTask
answeredvs totalsent. Cible : >70%. Si bas → l'email check-in n'est pas lu, à itérer (subject, timing). - Latence de réponse : médiane du
answeredAt - sentAt. Cible : <24h. - Ratio Oui/Non au check-in : si trop de "Oui" → on relance des factures déjà payées hors plateforme (signal pour pousser banking V2). Si trop de "Non" → cadence cohérente.
- Conversion
pending→paidsans relance : combien de factures sont marquées payées avant la 1re relance. Indicateur de bonne hygiène utilisateur. - DSO moyen pré/post Rubis : à mesurer via export historique vs progression mensuelle.
- % factures qui passent en
litigation: doit rester <2%. Au-delà, c'est que les plans sont trop agressifs.
11. Pricing & enforcement
11.1 Plans
| Plan | Prix mensuel | Prix annuel | Limite factures actives | Sièges | V2 |
|---|---|---|---|---|---|
| Free | 0 € | — | 5 (après période de grâce 3 mois) | 1 | — |
| Pro | 19 € | 190 € (-17%) | illimitées | 1 | — |
| Business | 49 € | 490 € (-17%) | illimitées | 5 (V2 multi-users) | reply-from-user-email, SMS |
"Facture active" = statut ∈ {pending, awaiting_user_confirmation, in_relance, litigation}. Les paid et cancelled ne consomment pas de slot.
11.2 Période de grâce 3 mois
À la création de l'org : gracePeriodEndsAt = createdAt + 3 mois. Pendant cette fenêtre, le plan Free est illimité (l'user teste full power). Au-delà :
- Si
activeInvoicesCount ≤ 5→ reste Free, fonctionne normalement - Si
activeInvoicesCount > 5→ import bloqué (HTTP 402plan_limit_reached) jusqu'à upgrade
L'API qui enforce : canCreateInvoices(organizationId, delta) dans app/services/billing.ts. Appelée par :
POST /invoices/import-batch/:id/drafts/:draftId/validate(validation OCR)POST /invoices(saisie manuelle)
Les uploads de PDFs ne sont PAS bloqués (le user peut empiler des drafts), seule la validation qui crée l'Invoice finale check la limite.
11.3 Stripe — flow technique
Setup initial (1 fois par compte Stripe — test ou prod) :
node ace stripe:setup
Crée 2 Products (Pro, Business) + 4 Prices (mensuel + annuel pour chacun) avec lookup_key stable (rubis_pro_monthly, rubis_pro_yearly, rubis_business_monthly, rubis_business_yearly). Idempotent.
Flow utilisateur — upgrade Free → Pro :
- Click "Passer Pro" sur
/parametres/abonnement - SPA appelle
POST /api/v1/billing/checkout { plan, cycle } - Backend crée un Stripe Customer (si pas déjà) + une Stripe Checkout Session, retourne
{ url } - SPA redirect vers Stripe (UI hostée, 3DS géré)
- User paye → Stripe redirect
success_url = /parametres/abonnement?checkout=success - En parallèle : Stripe envoie
checkout.session.completedau webhook → org passe en plan Pro
Webhook (POST /api/v1/billing/webhook, public, signature vérifiée) :
checkout.session.completed→ premier paiement OK, set plan + subscriptionIdcustomer.subscription.updated→ renouvellement / change de plan / mise à jour statuscustomer.subscription.deleted→ annulation effective → fallbackfreeinvoice.payment_failed→ statuspast_due(UI rappelle l'user)
Le webhook est idempotent : Stripe peut re-livrer plusieurs fois le même event, on traite chaque fois en read-then-write sans assumer 1-shot.
Customer Portal (gestion CB / annulation) :
- Click "Gérer" sur
/parametres/abonnement - SPA appelle
POST /api/v1/billing/portal - Backend crée une Stripe Billing Portal Session, retourne
{ url } - SPA redirect vers Stripe (CB, factures, annulation, tout est géré là)
11.4 Champs DB (organizations)
| Colonne | Type | Sens |
|---|---|---|
plan |
'free' | 'pro' | 'business' |
Plan courant. Default free. |
grace_period_ends_at |
timestamp | created_at + 3 mois. NULL après upgrade. |
stripe_customer_id |
string | Set au 1er checkout, jamais réécrit. |
stripe_subscription_id |
string | Refresh à chaque webhook subscription. |
subscription_status |
string | active, trialing, past_due, canceled, incomplete, unpaid (mirroring Stripe). |
billing_cycle |
'monthly' | 'yearly' |
Pour l'UI. |
current_period_end |
timestamp | "Prochaine facture le X". |
11.5 UI surfaces billing
/parametres/abonnement— comparaison Free/Pro/Business + toggle mensuel/annuel + plan courant + bouton portail/parametres(la page principale) — section "Abonnement" qui montre le plan courant + lien "Gérer l'abonnement"/factures—<PlanLimitBanner>:- Pendant grâce : rappel discret "illimité jusqu'au DD/MM/YYYY"
- Approche limite (ratio ≥ 80%) : avertissement avant blocage
- Limite atteinte : banner rouge bloquant + CTA "Passer Pro"
- Toast 402 sur la validation OCR : message + bouton action "Passer Pro" qui navigue vers /parametres/abonnement
11.6 Variables d'environnement
STRIPE_SECRET_KEY=sk_test_... (ou sk_live_... en prod)
STRIPE_WEBHOOK_SECRET=whsec_... (signature du endpoint)
En dev local, exposer le webhook via stripe listen --forward-to localhost:3333/api/v1/billing/webhook (Stripe CLI requise) — la CLI affiche le whsec_... à mettre en env.
12. Ce que Rubis ne fait PAS (rappel)
| Hors-scope | Pourquoi |
|---|---|
| Émettre des factures | On n'est pas un Henrri-bis. On relance ce qui sort d'ailleurs. |
| Réconciliation banking auto | V2+. V1 = check-in email. |
| Relancer par SMS | V2 (réservé plan le plus cher). |
| Multi-utilisateurs | V2 (plans payants seulement). |
| CRM / pipeline commercial | On reste pure-player relance. |
| Recouvrement contentieux | Hors-scope définitif. La mise en demeure est le seuil. Au-delà, c'est huissier. |
Dernière maj : 2026-05-07. Maintenu par Arthur + Claude.