# 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