rubis/docs/flow.md
ordinarthur e449b708f3 docs(invoices): édition native + ADR-025 + roadmap Factur-X (Phase 5)
Documente la feature ajoutée en V1.1 dans toute la doc cadre :

- **CLAUDE.md** : "Pure-player relance" nuancé en "La relance reste
  l'âme du produit", extension douce assumée. Périmètre V1/IN
  enrichi avec l'éditeur de factures. Glossaire enrichi (facture
  native, numéro de séquence, snapshot, Factur-X). Stack : ajout
  @react-pdf/renderer + pointeurs vers pdf-templates et les routes
  /parametres/facturation et /factures/nouvelle.
- **docs/produit.md** : nouvelle section 4.2bis "Édition native des
  factures" — scope V1.1 minimal, snapshots immuables, numérotation
  strict séquentielle, roadmap Factur-X V1.5 / PDP V2.
- **docs/flow.md** : nouvelle section 11bis (3 sources d'une facture,
  flow utilisateur de création, génération PDF, numérotation,
  snapshots, cas limites). Tableau "Ce que Rubis ne fait PAS" mis à
  jour (édition oui mais pas devis/avoirs/Factur-X V1).
- **docs/decisions.md** : ADR-025 "Édition native des factures +
  roadmap Factur-X" (rationale extension douce, choix techniques
  notables, alternatives écartées).
- **docs/tech/architecture.md** : section 6.1bis (flow technique
  édition native, points d'attention numérotation atomique + lazy
  PDF regenerate), ajout @react-pdf à la stack, routes /native +
  /preview-pdf + /invoice-themes + /invoice-settings documentées.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 03:18:11 +02:00

572 lines
32 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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-30j"
├── description: "Cadence sobre, ton qui monte progressivement."
└── steps[]
├── PlanStep { offsetDays: 3, tone: "amical", subject, body, requiresManualValidation: false }
├── PlanStep { offsetDays: 10, tone: "courtois", subject, body, requiresManualValidation: false }
└── PlanStep { offsetDays: 25, tone: "ferme", subject, body, requiresManualValidation: true }
mise en demeure → bouton manuel,
jamais d'envoi auto
Tons disponibles : `amical | courtois | ferme | mise_en_demeure`
(cf. `apps/api/app/services/default_plans.ts:18`).
```
### 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.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 `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 `pending` `paid` 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) :
```bash
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é )
### 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.
---
## 11bis. Édition native des factures (V1.1)
> Pour le rationale, voir ADR-025. Ici on documente le flow.
### 11bis.1 Trois sources d'une facture dans Rubis
| Source | Origine | Drapeau DB | PDF | Snapshots |
|---|---|---|---|---|
| **OCR** | Upload drag-and-drop, extraction Mindee/Document AI | `is_native = false` | Fichier source uploadé (`pdf_storage_key`) | aucun |
| **Saisie manuelle** | `ManualInvoiceDialog`, 6 champs | `is_native = false` | `pdf_storage_key = null` (pas de fichier) | aucun |
| **Native (V1.1)** | Éditeur `/factures/nouvelle` | `is_native = true` | Généré côté serveur, stocké MinIO | `client_snapshot` + `issuer_snapshot` figés |
Les statuts (`pending`, `in_relance`, `paid`…) et le cycle de vie (cf. §3) sont **identiques** pour les 3 sources. La distinction est UX / présentation : une facture native peut être ré-éditée (avec re-génération PDF) tant qu'elle n'est pas dans un relance déclenchée ; une OCR ne peut pas.
### 11bis.2 Création (flow utilisateur)
1. `/factures` clic "Créer une facture" (bouton primaire à côté de "Importer").
2. `/factures/nouvelle` : split-view. À gauche, formulaire ; à droite, iframe PDF live (debounce 500 ms via `POST /api/v1/invoices/preview-pdf`).
3. L'utilisateur saisit : client (combobox autocomplete), dates (émission + délai échéance calculée), plan de relance (optionnel), thème + accent, lignes (désignation/qté/PU/TVA), notes pied de page.
4. Le serveur recalcule HT/TVA/TTC à chaque preview (jamais confiance au client). Le front affiche aussi un total live en local pour feedback instantané (mêmes règles d'arrondi : `Math.round` par ligne).
5. Deux boutons en footer :
- **Enregistrer en brouillon** `POST /invoices/native` avec `draft: true`. Statut `pending`, `sequence_number = null`, `numero = "BROUILLON-XXXX"`. Pas de check-in programmé.
- **Émettre la facture** `POST /invoices/native` avec `draft: false`. Alloue le prochain numéro de la séquence (`sequence_number = N`, `numero = "<prefix>0042"`) en transaction avec verrou `FOR UPDATE` sur la ligne `organizations`. Programme le check-in si un plan est associé.
### 11bis.3 Génération PDF
- Templates dans `apps/api/app/pdf-templates/` (4 fichiers TSX + `common.tsx` + `index.tsx` dispatcher).
- Pipeline : `renderInvoiceToBuffer(themeSlug, props)` `@react-pdf/renderer.renderToBuffer` `uploadBuffer(buf, 'invoice-pdf', orgId)` `invoices/<orgId>/<uuid>.pdf` sur MinIO.
- Snapshot `client_snapshot` lu en priorité, fallback sur le client live pour la preview (qui n'a pas encore figé les snapshots).
- Lazy regenerate : si `GET /invoices/:id/pdf` reçoit une facture native sans `pdf_storage_key` (génération échouée au store), on retente à la volée et on persiste.
### 11bis.4 Numérotation (point sensible légalement)
- Compteur dans `organizations.invoice_settings.numeroNextSeq` (JSONB).
- Allocation : `SELECT invoice_settings FROM organizations WHERE id = $1 FOR UPDATE` lit le compteur, increment, écrit `invoice_settings = jsonb_set(...)`. Garantit l'unicité même sous concurrence (deux onglets, deux jobs).
- Brouillons : `numero` éphémère type `BROUILLON-A1B2C3D4`, `sequence_number = null`. Aucun risque de gap si l'utilisateur supprime un brouillon (la séquence n'a pas été allouée).
- Unicité : `UNIQUE (organization_id, numero)` + `UNIQUE (organization_id, sequence_number)` (partial, autorise plusieurs `NULL`).
- Override initial : l'utilisateur peut définir `numeroNextSeq = 42` dans les settings une fois (pour reprendre une séquence existante d'un autre outil). Au-delà, c'est auto-incrémenté.
### 11bis.5 Snapshots (immutabilité)
À l'émission d'une facture native, deux JSONB sont gelés :
- `client_snapshot` : nom, email, contact, SIREN/SIRET, TVA intra, adresse structurée du client tel qu'il était.
- `issuer_snapshot` : identité émetteur tel qu'elle était dans `invoice_settings.issuer`.
Si plus tard le client déménage ou si l'org modifie son SIRET, **les factures déjà émises restent identiques** au PDF stocké. Cf. ADR-025 pour le rationale (preuve comptable).
### 11bis.6 Cas limites
- **Plan limite Free atteint** (5 factures actives) : `POST /invoices/native` renvoie 402 `plan_limit_reached` même règle que la saisie OCR/manuelle.
- **Lignes vides** : le SPA bloque le submit si une ligne n'a pas de description ou si `unitPriceCents < 0`.
- **Génération PDF échoue post-store** : on log, on continue (la facture est créée), `pdf_storage_key = null`. Le prochain `GET /:id/pdf` regénère lazy.
- **Aperçu live et client inexistant** : le SPA ne POST pas tant qu'on n'a pas de `clientId` (combobox doit avoir sélectionné une fiche).
- **Race condition sur la séquence** : verrou row-level sur `organizations` sérialise les `storeNative` concurrents pour la même org. Pas de gaps possibles hors brouillons supprimés (et les brouillons ne consomment pas la séquence).
## 12. Ce que Rubis ne fait PAS (rappel)
| Hors-scope | Pourquoi |
|---|---|
| ~~Émettre des factures~~ édité V1.1 : on émet maintenant des factures natives (cf. §11bis et ADR-025), mais on n'est toujours pas un outil de facturation complet (pas d'avoirs, pas de devis, pas d'acomptes en V1.1). | Extension douce, pas pivot. |
| Devis, avoirs (credit notes), acomptes, facturation récurrente | V2+. La V1.1 reste minimale (factures simples uniquement). |
| 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. |
| Émission Factur-X / transmission via PDP (réforme 2026-2027) | V1.5 (Factur-X natif) puis V2 (PDP partenaire si demande client) cf. ADR-025. |
---
*Dernière maj : 2026-05-07. Maintenu par Arthur + Claude.*