Simpl-Resultat/docs/adr/0014-balance-vehicule-attribut.md
le king fu ebc709a277
All checks were successful
PR Check / rust (pull_request) Successful in 22m24s
PR Check / frontend (pull_request) Successful in 2m22s
docs(balance): ADR 0014 + reject 0012 + guide + changelog (#205)
Document Étape 1 of the balance audit (vehicle_type axis), already shipped
in #202/#203/#204:
- ADR 0014 (Accepted): fiscal envelope is an account attribute, the category
  is a pure asset class; Étape 2 (per-security detail) explicitly out of scope.
- ADR 0012 marked Rejected (never accepted, not Superseded) + pointer to 0014.
- User guide (markdown + in-app docs.balance i18n FR/EN): optional fiscal
  envelope, the two chart axes, type renaming, and the historical-reclass note.
- CHANGELOG.md + CHANGELOG.fr.md [Unreleased]: Added (envelope field, envelope
  axis, collapsible returns) + Changed (asset-class category, CELI/REER reclass,
  rename no longer alters translation, historical-reclass note).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 21:15:06 -04:00

104 lines
9.6 KiB
Markdown
Raw 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.

# ADR 0014 — Bilan : le véhicule fiscal est un attribut du compte, la classe d'actif est la catégorie
- Status: **Accepted**
- Date: 2026-06-01
- Rejette: ADR 0012 (modèle à deux niveaux véhicule × composition — voir « Alternatives » ci-dessous)
- Issues: #202 (couche données), #203 (UI saisie), #204 (UI suivi), #205 (cet ADR + doc)
- Audit source: [docs/audit-bilan-2026-05.md](../audit-bilan-2026-05.md)
## Contexte
L'audit critique de la page Bilan (`docs/audit-bilan-2026-05.md`, revue CPA + UX, 2026-05-31) a identifié une **racine unique** (finding A) derrière la plupart des frictions du module : le modèle est *plat*. Une seule « catégorie » (`balance_categories`) encodait simultanément **deux axes orthogonaux** :
- le **véhicule fiscal** / l'enveloppe : CELI, REER, non-enregistré… ;
- la **classe d'actif** : liquidités, fonds/FNB, actions, crypto, autres.
Les 7 catégories seedées par la migration v9 — `cash · tfsa · rrsp · fund · other · stock · crypto` — mélangeaient donc des frères/sœurs de natures incomparables. Conséquences directes mesurées par l'audit :
- **Cas québécois inexprimables** : suivre des actions *dans* un CELI force un choix dégradé (compte `simple` sous `tfsa` → le titre disparaît ; compte `priced` sous `stock` → l'abri CELI disparaît).
- **Empilé « par catégorie » illisible** (finding G) : ni « répartition par classe d'actif », ni « répartition par enveloppe » — un axe bâtard.
- **Bug i18n latent** (finding I) : renommer une catégorie via `window.prompt()` écrasait `i18n_key` avec du texte libre, cassant la traduction FR/EN — une promesse-socle du projet.
L'audit recommandait une **trajectoire additive en trois temps** (Étape 0 quick wins, déjà livrée en #201 ; Étape 1 séparer l'axe véhicule ; Étape 2 détail par titre) explicitement opposée au big-bang de l'ADR 0012. Le présent ADR documente la décision prise pour l'**Étape 1**.
### Pourquoi additif (et pas le modèle deux-tables de l'ADR 0012)
L'ADR 0012 proposait deux tables `balance_vehicles` × `balance_compositions` et une ligne de snapshot devenant un triplet `(vehicle_id, composition_id, value)`. C'est une réécriture quasi totale de `/balance` (grille de saisie 2D imposée à tous, dédoublement des agrégateurs, migration de données massive) pour un grain qui reste la **classe d'actif** — pas le **titre individuel** que l'investisseur attend réellement (audit : « l'ADR 0012 résout le mauvais grain »). L'approche retenue ajoute deux colonnes nullables, ne touche aucun compte simple existant, et garde la grille de saisie à une dimension.
## Décision
**Le véhicule fiscal devient un attribut du compte ; la catégorie devient une pure classe d'actif.**
1. **Nouvelle colonne `balance_accounts.vehicle_type`** — TEXT nullable, enveloppe fiscale, contrainte CHECK sur l'enum réduit courant :
| Code | FR | EN |
|---|---|---|
| `unregistered` | Non-enregistré | Non-registered |
| `tfsa` | CELI | TFSA |
| `rrsp` | REER | RRSP |
| `rrif` | FERR | RRIF |
| `fhsa` | CELIAPP | FHSA |
| `resp` | REEE | RESP |
⚠️ `vehicle_type` = enveloppe **fiscale**, jamais un véhicule automobile. Nullable : un compte chèque ou un wallet crypto n'a pas d'enveloppe (valeur `NULL`, et non `unregistered` — un compte courant n'est pas un placement non-enregistré).
2. **Les catégories sont 5 classes d'actif pures** : Liquidités, Fonds/FNB, Actions, Crypto, Autres. Les ex-catégories-véhicules `tfsa`/`rrsp` ne sont plus des catégories — elles deviennent des `vehicle_type`.
3. **Deux migrations additives** (jamais de modification d'une migration ≤ v11 — checksum SHA-384 sqlx) :
- **v12 (additive)** : ajoute `vehicle_type` (+ CHECK) et `balance_categories.custom_label` ; backfille `vehicle_type='tfsa'`/`'rrsp'` pour les comptes des ex-enveloppes ; les comptes `cash` restent `NULL`. Inclut un **backfill défensif du bug I** : toute catégorie seed dont `i18n_key` avait été écrasé par du texte libre récupère ce texte dans `custom_label`, et son `i18n_key` est restauré depuis `key`.
- **v13 (reclassement, conditionnelle/idempotente)** : re-rattache les comptes ex-`tfsa`/`rrsp` à la classe « Autres » (`other`), gardé par `EXISTS(other) AND is_seed=1` ; désactive (`is_active=0`) les seeds `tfsa`/`rrsp` devenus des véhicules. v12 stampe `vehicle_type` **avant** que v13 ne déplace `balance_category_id` (ordre garanti par le versioning sqlx).
- `consolidated_schema.sql` (profils neufs) et les comptes starter (consolidated + `STARTER_ACCOUNTS` service) sont synchronisés : CELI/REER pointent vers `other` + portent leur `vehicle_type`.
4. **Renommage de catégorie via `custom_label`** (corrige le bug I) : l'affichage suit la règle `custom_label?.trim() || t(i18n_key, { defaultValue: key })`, factorisée dans un helper `renderCategoryLabel`. Le renommage écrit `custom_label` et **ne touche plus jamais `i18n_key`** — la traduction FR/EN reste intacte.
5. **Deux axes de lecture** : le graphique empilé gagne un sous-toggle « Par classe d'actif » (défaut, = comportement existant) / « Par enveloppe » (`getSnapshotTotalsByVehicleAndDate`, bucket `COALESCE(vehicle_type,'none')`). Le tableau des comptes replie ses colonnes de rendement par défaut, avec un toggle dont l'état est persisté (`user_preferences.balance_show_returns`).
### Hors scope (Étape 2 — explicitement exclue de cette décision)
Le **détail par titre** reste hors scope et n'est *pas* tranché par cet ADR :
- pas de table `balance_securities` (symbole normalisé, devise, asset_type) ;
- pas de holdings/positions par titre sous un compte ;
- pas de `book_cost` / PRU (distinction apport vs gain latent) ;
- pas de migration du `kind` (`simple`/`priced`) de la catégorie vers le compte, donc pas encore d'assistant « détailler un compte agrégé en titres » sans rupture d'historique ;
- pas d'import CSV de cours local, pas de multi-devise (reste CAD), pas d'agrégation du rendement par véhicule.
L'Étape 2 fera l'objet d'un ADR distinct quand le besoin sera confirmé (gating de l'audit : trajectoire additive, pas de big-bang). L'Étape 1 ne **ferme aucune** de ces portes — elle pose l'axe véhicule sans présumer du grain titre.
## Alternatives considérées
- **ADR 0012 — modèle à deux niveaux `balance_vehicles` × `balance_compositions`** (rejeté, voir ci-dessus) : surdimensionné, grille 2D imposée, migration massive, grain « classe d'actif » et non « titre ».
- **Tagging multi-axes libre** (alternative A de l'ADR 0012) : aucune contrainte sur les combinaisons, rapports « actions dans CELI » coûteux, vocabulaire divergent entre profils.
- **Sous-comptes (`parent_id`)** (alternative B de l'ADR 0012) : invariant somme parent = somme enfants à maintenir, snapshots dédoublés sans gain expressif.
- **Statu quo** (modèle plat enrichi de catégories user `tfsa_stock`…) : explosion N×M de la taxonomie, friction documentée croissante.
L'attribut nullable sur le compte gagne sur les quatre : migration triviale (2 colonnes), zéro impact sur les comptes simples existants, deux groupements naturels (`GROUP BY category` / `GROUP BY vehicle_type`), et aucune porte fermée pour l'Étape 2.
## Consequences
### Positives
- **Cas québécois exprimables** : « combien dans mon CELI ? » se lit par `vehicle_type`, indépendamment de la classe d'actif détenue.
- **Empilé assaini** (finding G) : deux axes distincts et explicites, défaut « par classe d'actif » (zéro surprise vs l'existant).
- **Bug i18n corrigé** (finding I) : le renommage n'altère plus la traduction ; le backfill défensif v12 répare les profils déjà cassés.
- **Migration sûre** : purement additive, idempotente, gardée ; les comptes simples ne bougent jamais ; `snapshot_lines` référencent `account_id`**historique des valeurs préservé**.
- **Tableau dégonflé** (finding F) : rendements repliés par défaut, état persisté — moins de bruit pour le grand public.
### Négatives / risques actés
- **Historique re-affiché sous « Autres »** : l'axe « par classe d'actif » est recalculé sur la **catégorie courante** du compte. Un snapshot pré-migration d'un compte ex-CELI/REER apparaît donc désormais sous « Autres » (et non plus « CELI »/« REER »). **Comportement attendu**, documenté au CHANGELOG et au guide. La lecture par enveloppe, elle, retrouve bien le CELI/REER via `vehicle_type`.
- **Pas de migration Down** : v12/v13 sont idempotentes et conditionnelles ; toute correction passe par une v14 (jamais d'édition rétroactive).
- **Comptes ex-CELI/REER contenant de vraies actions** : restent un montant agrégé en « Autres » + leur `vehicle_type`. Le détail par titre est l'Étape 2 — non débloqué ici.
- **`vehicle_type` lu comme automobile** : risque d'ambiguïté de nommage (un agent d'exploration a déjà halluciné « car/truck »). Mitigé par le CHECK explicite, le commentaire de migration et la table d'enum ci-dessus.
### Neutre
- L'enum `vehicle_type` couvre le set courant (6 valeurs). Ajouter CRI, RPDB, etc. plus tard = une nouvelle migration qui élargit le CHECK (jamais une édition de v12).
## Liens
- [Audit Bilan 2026-05](../audit-bilan-2026-05.md) — racine (finding A), trajectoire additive en trois temps, recommandation sur l'ADR 0012
- [ADR 0012](0012-balance-two-level-model.md) — modèle à deux niveaux, **Rejected** au profit du présent ADR
- [ADR 0008](0008-modified-dietz-pour-rendement.md) — Modified Dietz par compte (modèle plat préservé : le rendement reste par compte)
- [ADR 0010](0010-fk-restrict-balance-transfers.md) — FK RESTRICT sur transferts (contrainte inchangée)
- Issues #202 / #203 / #204 / #205 — implémentation Étape 1