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