Simpl-Resultat/spec-decisions-feature-gating.md
le king fu 99ba147906 docs(spec): feature-gating decisions + plan + milestone spec-feature-gating (#297-#302)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 17:20:50 -04:00

80 lines
6.3 KiB
Markdown

# Spec Decisions — Gating des fonctionnalités par édition de licence
> Date: 2026-07-19
> Projet: simpl-resultat
> Statut: Draft
> Slug: feature-gating
## Contexte
Simpl'Résultat n'applique aujourd'hui **aucun gating de fonctionnalités** : un utilisateur Free a accès à tout, sauf la récupération des cours (seul gate dur, vérifié côté serveur via maximus-api). L'infrastructure de licence est pourtant déjà là — 3 éditions signées Ed25519 (`free`/`base`/`premium`), table `FEATURE_TIERS`, champ `features[]` signé — mais seuls 3 call-sites de gate existent (auto-update, laissé ouvert ; cours, Premium). Max veut définir un vrai périmètre d'abonnement en verrouillant des modules selon le tier.
## Objectif
Appliquer une matrice d'entitlements Free/Base/Premium à travers l'app : chaque module hors du tier de l'utilisateur reste **visible mais verrouillé**, avec un écran d'upsell. Le gating est piloté par un hook central côté React, adossé à l'édition de licence déjà vérifiée, avec un override par-licence optionnel. Aucune donnée n'est supprimée — le durcissement bloque l'accès, il ne détruit rien.
## Scope
### IN
- Matrice d'entitlements Free/Base/Premium (ci-dessous).
- Hook central `useEntitlement(feature)` + garde de navigation (Sidebar) et de route.
- Écran/zone d'upsell « verrouillé » (cadenas, CTA upgrade), i18n FR/EN.
- Enrichir `FEATURE_TIERS` (Rust) + câbler l'override par-licence (`features[]` consulté par `is_feature_allowed`).
- Gating **par onglet** des rapports (`/reports/*`) : Free = Tendance uniquement.
- Absorption du re-gate auto-update (#271) : l'auto-update devient Base+.
- Durcissement sec **non destructif** : blocage d'accès immédiat, données conservées, récupérables par upgrade.
### OUT (explicitement exclu)
- Le champ `product` explicite (#270) — reste dans spec-paiements.
- Le go-live Stripe / le flux d'achat en ligne (l'upsell pointe vers l'achat, dont l'URL localisée relève de #270).
- Un vrai système de codes d'invitation (l'émission de clés reste manuelle via l'endpoint admin `generate`).
- Tout gate « dur » server-enforced au-delà de l'existant (cours) — le gating local est un soft paywall assumé (GPL).
- Un 4e tier « admin » : l'accès total de Max = une licence Premium auto-émise.
- Mode lecture-seule des features verrouillées (écarté : durcissement sec, pas de distinction voir/éditer).
## Matrice d'entitlements
| Module | Free | Base | Premium |
|---|:--:|:--:|:--:|
| Dashboard | ✅ | ✅ | ✅ |
| Import CSV | ✅ | ✅ | ✅ |
| Transactions | ✅ | ✅ | ✅ |
| Catégories (+ guide, migration) | ✅ | ✅ | ✅ |
| Rapport **Tendance** | ✅ | ✅ | ✅ |
| Autres rapports (Highlights, Comparaison, Catégorie, Cartes) | ❌ | ✅ | ✅ |
| Budget | ❌ | ✅ | ✅ |
| Multi-profils | ❌ | ✅ | ✅ |
| Auto-update | ❌ | ✅ | ✅ |
| **Bilan complet** (patrimoine + cours) | ❌ | ❌ | ✅ |
| Export/Import chiffré | ✅ | ✅ | ✅ |
| Changelog / Docs | ✅ | ✅ | ✅ |
Admin (Max) = licence **Premium** auto-émise (superset).
## Décisions prises
| Question | Décision | Raison |
|---|---|---|
| UX du gate (feature hors tier) | **Verrouillé + upsell** : entrée visible avec cadenas ; clic → écran « Passez à Base/Premium » | Meilleure conversion — l'utilisateur voit ce qu'il rate |
| Données existantes lors du durcissement | **Durcissement sec non destructif** : blocage d'accès (vue + édition) immédiat, mais **données conservées**, jamais supprimées, récupérables par upgrade | Simple à coder (pas de mode lecture-seule) ; ne détruit aucune donnée financière (clarifié par Max) |
| Modèle d'entitlements | **Statique + override licence** : map `tier → features` en dur, PLUS `license.features[]` consulté en additif (`allowed = feature ∈ tier OU feature ∈ license.features`) | Matrice centralisée et simple, + débloque le champ `features[]` déjà signé pour des licences spéciales sans créer de tier |
| Coordination spec-paiements | **Absorber #271** (re-gate auto-update) ici ; **#270** (`product`) reste séparé | L'auto-update est dans la matrice → gaté ici, #271 redondant ; `product`/URL d'achat = go-live, hors scope |
| Enforcement | **UI-only** (gardes React : nav + route). `current_edition()` (Rust) reste la source de vérité lue ; les commandes Tauri ne refusent pas (sauf l'existant : cours server-enforced) | Cohérent avec le soft-paywall GPL assumé — un gate Rust serait aussi retirable par un fork, sans gain de sécurité |
| Écran d'upsell | **Composant générique paramétré** (feature + tier requis), i18n FR/EN ; CTA « Obtenir Base/Premium » (lien d'achat, dépend de #270) + « J'ai déjà une clé » → Réglages → Licence | Un seul composant réutilisable, pas un écran par feature |
| Admin (accès total de Max) | **Licence Premium auto-émise** (endpoint admin `generate`), pas de 4e édition ni mode dev | Premium = superset ; réutilise l'infra existante, zéro code neuf |
| Free multi-profils existants | **Profil actif conservé** ; profils supplémentaires verrouillés → upsell (données conservées) | Cohérent avec durcissement sec + conservation des données |
## Contraintes
- **GPL-3.0 + privacy-first** : le gating local est un soft paywall **assumé** (contournable par fork). Seules les features adossées à maximus-api (les cours, aujourd'hui) sont dures. Aucune donnée financière ne quitte l'appareil.
- **Séquencement** : le gating est livrable et testable **indépendamment de Stripe** (pas encore en live) via des clés admin-émises. Corollaire : sans ce chantier, distribuer des clés n'a aucun effet visible (tout est déjà ouvert).
## Références
| Source | Pertinence |
|---|---|
| `spec-monetisation.md` | Modèle des 3 tiers, contraintes GPL / soft-paywall, format de licence |
| `src-tauri/src/commands/entitlements.rs` | `FEATURE_TIERS` + `is_feature_allowed` — table à enrichir, override `features[]` à câbler |
| `src-tauri/src/commands/license_commands.rs` | `current_edition()` (source de vérité de l'édition) + `LicenseClaims.features` (champ signé à consulter) |
| `src/services/licenseService.ts`, `src/hooks/useLicense.ts`, `src/hooks/useIsPremium.ts` | Base du futur hook `useEntitlement` |
| `src/App.tsx` (routes `/reports/*`), `src/components/layout/Sidebar` | Points de garde nav + route |