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>
9.6 KiB
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
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
simplesoustfsa→ le titre disparaît ; comptepricedsousstock→ 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()écrasaiti18n_keyavec 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.
-
Nouvelle colonne
balance_accounts.vehicle_type— TEXT nullable, enveloppe fiscale, contrainte CHECK sur l'enum réduit courant :Code FR EN unregisteredNon-enregistré Non-registered tfsaCELI TFSA rrspREER RRSP rrifFERR RRIF fhsaCELIAPP FHSA respREEE RESP ⚠️
vehicle_type= enveloppe fiscale, jamais un véhicule automobile. Nullable : un compte chèque ou un wallet crypto n'a pas d'enveloppe (valeurNULL, et nonunregistered— un compte courant n'est pas un placement non-enregistré). -
Les catégories sont 5 classes d'actif pures : Liquidités, Fonds/FNB, Actions, Crypto, Autres. Les ex-catégories-véhicules
tfsa/rrspne sont plus des catégories — elles deviennent desvehicle_type. -
Deux migrations additives (jamais de modification d'une migration ≤ v11 — checksum SHA-384 sqlx) :
- v12 (additive) : ajoute
vehicle_type(+ CHECK) etbalance_categories.custom_label; backfillevehicle_type='tfsa'/'rrsp'pour les comptes des ex-enveloppes ; les comptescashrestentNULL. Inclut un backfill défensif du bug I : toute catégorie seed donti18n_keyavait été écrasé par du texte libre récupère ce texte danscustom_label, et soni18n_keyest restauré depuiskey. - v13 (reclassement, conditionnelle/idempotente) : re-rattache les comptes ex-
tfsa/rrspà la classe « Autres » (other), gardé parEXISTS(other) AND is_seed=1; désactive (is_active=0) les seedstfsa/rrspdevenus des véhicules. v12 stampevehicle_typeavant que v13 ne déplacebalance_category_id(ordre garanti par le versioning sqlx). consolidated_schema.sql(profils neufs) et les comptes starter (consolidated +STARTER_ACCOUNTSservice) sont synchronisés : CELI/REER pointent versother+ portent leurvehicle_type.
- v12 (additive) : ajoute
-
Renommage de catégorie via
custom_label(corrige le bug I) : l'affichage suit la règlecustom_label?.trim() || t(i18n_key, { defaultValue: key }), factorisée dans un helperrenderCategoryLabel. Le renommage écritcustom_labelet ne touche plus jamaisi18n_key— la traduction FR/EN reste intacte. -
Deux axes de lecture : le graphique empilé gagne un sous-toggle « Par classe d'actif » (défaut, = comportement existant) / « Par enveloppe » (
getSnapshotTotalsByVehicleAndDate, bucketCOALESCE(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_linesréférencentaccount_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_typelu 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_typecouvre 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 — racine (finding A), trajectoire additive en trois temps, recommandation sur l'ADR 0012
- ADR 0012 — modèle à deux niveaux, Rejected au profit du présent ADR
- ADR 0008 — Modified Dietz par compte (modèle plat préservé : le rendement reste par compte)
- ADR 0010 — FK RESTRICT sur transferts (contrainte inchangée)
- Issues #202 / #203 / #204 / #205 — implémentation Étape 1