docs: flow.md — cycle de vie facture, statuts, surfaces UI, check-in deep dive
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 22s
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 22s
Nouvelle doc orientée comportement produit : explique précisément ce que
fait Rubis du point de vue user-lambda. Pour les arch/tech → architecture.md.
Pour la spec features → produit.md.
Sections :
1. Modèle mental
2. Glossaire (rubis, plan, étape, confirmation, mise en demeure, DSO, LME)
3. Cycle de vie d'une facture (6 statuts + diagramme transitions ASCII +
détails par transition avec effets en cascade)
4. Surfaces UI où l'user agit (modale check-in, email check-in, fiche
facture, slide-over démo) — avec différences mobile/desktop
5. Mécanique de confirmation deep-dive (le coeur du produit)
6. Plans de relance (structure, plans pré-fournis, wizard custom, vars)
7. Mode démo (flag, fork point unique, horloge virtuelle)
8. KPIs & calculs (rubis, encaissé, DSO, pipeline)
9. Edge cases & règles
10. Métriques produit à instrumenter
11. Ce que Rubis ne fait PAS
CLAUDE.md mis à jour pour pointer vers cette doc dans la liste des
documents associés.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
52e78b66e9
commit
d410ae014e
@ -137,7 +137,8 @@ Voir `/docs/decisions.md` pour le log complet avec rationale.
|
||||
| `/landing/favicon.{svg,ico,png}` | Set complet de favicons + apple-touch-icon |
|
||||
| `/landing/site.webmanifest` | Manifest PWA (theme `#9F1239`, background `#FAF7F2`) |
|
||||
| `/landing/assets/logo.png` | Logo Rubis original (généré, source pour les favicons) |
|
||||
| `/docs/produit.md` | Spec produit détaillée (features, flows, IN/OUT V1) |
|
||||
| `/docs/produit.md` | Spec produit haut niveau (features, IN/OUT V1, pricing) |
|
||||
| `/docs/flow.md` | **Comportement produit deep-dive** : cycle de vie d'une facture, statuts + transitions, surfaces UI, mécanique de confirmation (check-in), mode démo, edge cases |
|
||||
| `/docs/marque.md` | Référence marque écrite (palette, typo, voix, do/don't) |
|
||||
| `/docs/decisions.md` | Log de décisions avec rationale (format ADR-light) |
|
||||
| `/docs/wireframes-mvp.html` | Wireframes low-fi des 13 écrans MVP |
|
||||
|
||||
418
docs/flow.md
Normal file
418
docs/flow.md
Normal file
@ -0,0 +1,418 @@
|
||||
# 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`.
|
||||
- **Note V1** : dans la prod actuelle, le statut DB de l'invoice reste techniquement `pending` jusqu'à ce que l'user réponde — c'est le seed démo qui force `awaiting_user_confirmation` pour pré-peupler des cas. À aligner V1.5 (le job `send_checkin_job` devrait push le statut).
|
||||
|
||||
#### `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 envoie l'email à l'user, marque la task `sent`, mais **ne change PAS le statut de l'invoice côté prod** (TODO V1.5 — bascule en `awaiting_user_confirmation` quand 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 `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.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. 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.*
|
||||
Loading…
x
Reference in New Issue
Block a user