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

9.6 KiB
Raw Permalink Blame History

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 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_idhistorique 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 — 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