rubis/docs/flow.md
ordinarthur ab75f1f979
All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m6s
fix(checkin): bump invoice.status pending → awaiting_user_confirmation
Bug V1 documenté dans flow.md mais jamais corrigé : le job send_checkin_job
envoyait l'email + marquait la CheckinTask `sent`, mais ne touchait pas le
statut de la facture. Conséquence : l'user reçoit le mail check-in dans sa
boîte mais la modale in-app au refresh ne l'affiche pas (la modale liste
uniquement les `awaiting_user_confirmation` côté DB).

Fix : après l'envoi mail OK et le mark CheckinTask=sent, on bump
`Invoice.status = 'awaiting_user_confirmation'` SI elle est encore
en `pending`. Pas de bump si entre temps :
  - mark-paid (status=paid)
  - litigation/cancelled (transitions manuelles)
  - in_relance (impossible mais safe)

Doc flow.md mise à jour pour refléter le nouveau comportement (effets
de la transition pending → awaiting + déprécation de la note "TODO V1.5").

Pour les factures existantes en prod qui ont déjà reçu le mail mais
restent en `pending` (cas pré-fix) : backfill manuel via SQL :

  UPDATE invoices SET status = 'awaiting_user_confirmation'
  WHERE status = 'pending'
    AND id IN (
      SELECT invoice_id FROM checkin_tasks WHERE status = 'sent'
    );

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 17:42:52 +02:00

26 KiB
Raw Blame History

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, quand dueDate est atteinte.
  • Effet :
    • Un email check-in part à l'user (pas au client) avec 2 liens Oui (payée) / Non (toujours impayée)
    • La CheckinTask est marquée sent
    • L'Invoice.status passe de pending à awaiting_user_confirmation (uniquement si encore en pending — pas de bump si la facture a été marquée payée ou autre entre temps)
    • Côté SPA : la modale check-in usePendingCheckins voit désormais cette facture et l'affiche au prochain login / refocus

awaiting_user_confirmation → paid (réponse "Oui")

  • Qui déclenche : l'user, via 4 surfaces possibles :
    1. Modale in-app au login (InAppCheckinModal) — bouton Oui — la facture est payée
    2. Lien paid dans l'email check-in
    3. Fiche facture — bouton Marquer encaissée
    4. Slide-over en mode démo (DemoEmailSlide)
  • Effet en cascade :
    • Invoice.status = 'paid', Invoice.paidAt = now
    • Invoice.rubisEarned += 1, Organization.rubisCount += 1
    • Toutes les RelanceTask scheduled futures → 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 RelanceTask avec sendAt = invoice.dueDate + step.offsetDays
      • Si certains sendAt sont déjà passés (cas d'une facture en retard), la 1re étape éligible est calée à now + 1min puis les suivantes gardent l'écart prévu (cf. catchUpAnchor dans relance_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/litigate qui :
    • Status → litigation
    • Cancel les relances futures
    • (Optionnel) génère un brouillon de mise en demeure si pas déjà envoyé

* → 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), dismissed est 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=paid
    • https://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 CheckinTask est créé à la création de l'invoice (sauf si pending future) — programmé pour dueDate.
  • Au moment où le job tourne (queue checkins), il :
    1. Envoie l'email à l'user
    2. Marque la CheckinTask.status = 'sent'
    3. Bump l'Invoice.status en awaiting_user_confirmation si encore pending (la modale in-app la voit alors)
  • 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 (le statut reste awaiting_user_confirmation tant qu'il n'a pas répondu).

5.3 Architecture des RelanceTask

  • Créées uniquement quand l'user répond "Non" au check-in (ou clique "Relancer maintenant"). Pas avant.
  • Une RelanceTask correspond à un PlanStep × une Invoice. Status :
    • scheduled : en attente de fire
    • sent : email envoyé OK
    • cancelled : 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 plus scheduled, 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) :

  1. Identité : nom, description, slug auto
  2. Cadence : éditeur calendrier visuel pour placer les J+X
  3. Messages : pour chaque étape, choisir ton + écrire/IA-générer le contenu (avec variables {{client.name}}, {{numero}}, etc.)
  4. 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 via clock.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.tscaptureEmailIfDemo() :

  • 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 rubisEarned des factures payées avec paidAt >= 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
  • cancelled exclus (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 RelanceTask scheduled du précédent plan sont cancelled (jobs BullMQ removed).
  • Les nouvelles tasks sont créées selon le nouveau plan.
  • Les tasks sent restent 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 answered vs total sent. 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 pendingpaid sans 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 402 plan_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 :

  1. Click "Passer Pro" sur /parametres/abonnement
  2. SPA appelle POST /api/v1/billing/checkout { plan, cycle }
  3. Backend crée un Stripe Customer (si pas déjà) + une Stripe Checkout Session, retourne { url }
  4. SPA redirect vers Stripe (UI hostée, 3DS géré)
  5. User paye → Stripe redirect success_url = /parametres/abonnement?checkout=success
  6. En parallèle : Stripe envoie checkout.session.completed au 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 + subscriptionId
  • customer.subscription.updated → renouvellement / change de plan / mise à jour status
  • customer.subscription.deleted → annulation effective → fallback free
  • invoice.payment_failed → status past_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) :

  1. Click "Gérer" sur /parametres/abonnement
  2. SPA appelle POST /api/v1/billing/portal
  3. Backend crée une Stripe Billing Portal Session, retourne { url }
  4. 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.