Simpl-Resultat/spec-plan-refonte-seed-categories-ipc.md
le king fu 0e2078088a docs: add spec decisions and plan for categories IPC seed refactor
Source of truth for milestone spec-refonte-seed-categories-ipc
(issues #115-#123).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 15:35:15 -04:00

23 KiB

Spec Plan — Refonte du seed de catégories vers IPC Statistique Canada

Date: 2026-04-19 Projet: simpl-resultat Statut: Draft Slug: refonte-seed-categories-ipc Decisions: spec-decisions-refonte-seed-categories-ipc.md

Design

UX / Interface

Livraison 1 — Découverte (Option E)

Bannière dashboard (première ouverture post-MAJ)

  • Position : haut du dashboard, dismissable
  • Texte : "Découvrez la nouvelle structure standard des catégories — inspirée de Statistique Canada"
  • CTA : Voir le guide → navigue vers /paramètres/categories/standard
  • Une fois dismiss, ne réapparaît plus (flag user_preferences.categories_v1_banner_dismissed = true)

Page /paramètres/categories/standard (lecture seule)

  • Bloc pédagogique en haut : pourquoi cette structure, lien vers source Statistique Canada
  • Arbre navigable avec expand/collapse des branches
  • Hover sur catégorie : tooltip avec description + exemples de fournisseurs (ex: "Metro, IGA, Maxi, Loblaws...")
  • Compteur global : "9 catégories racines, ~40 sous-catégories, ~90 feuilles"
  • Bouton recherche full-text
  • Bouton export PDF
  • Aucune action destructive — lecture pure

Entrée Paramètres

  • Dans Paramètres > Gestion des catégories, ajouter lien Explorer la structure standard

Livraison 2 — Migration (Option B)

Page 3 étapes séquentielles à /paramètres/categories/migrer :

Étape 1 — Découvrir (reprend la page de Livraison 1 en lecture)

  • CTA : Continuer vers l'aperçu de migration

Étape 2 — Simuler (dry-run)

  • Résumé impact : X catégories, Y transactions, Z règles, W budgets
  • Table 3 colonnes : Actuelle | Correspondance | v1 proposée
    • Badges confiance 🟢/🟡/🟠/🔴
    • Panneau latéral cliquable par ligne : liste des transactions affectées + possibilité de changer la cible
  • Compteur "N décisions à prendre" + bouton suivant désactivé tant que toutes les 🟠 ne sont pas résolues
  • Les choix sont persistés en mémoire (pas encore BDD)

Étape 3 — Consentir

  • Checklist explicite : "Je comprends que cette opération modifie mes catégories / Une sauvegarde sera créée avant tout changement / Je peux rétablir à tout moment"
  • Bouton Créer la sauvegarde et migrer désactivé tant que checklist non cochée
  • Pendant exécution : loader avec 4 étapes affichées (backup créé → vérifié → migration SQL → commit)
  • Écran succès avec chemin du fichier SREF + CTA Aller au tableau de bord
  • Écran échec backup : abort complet, aucune écriture, message clair + options (changer dossier / réessayer / annuler)

Bannière post-migration (Paramètres > Catégories, 90 jours)

  • "Migration appliquée le . Sauvegarde : "
  • Bouton Rétablir la sauvegarde → modale confirmation → importFullProfile() en mode replace
  • Bouton Ne plus afficher → flag dans user_preferences.categories_migration_banner_dismissed

Données

Nouvelle migration SQL v8 (additive)

Nom : v8__category_schema_version.sql

Ajoute :

  • Colonne categories.i18n_key TEXT NULL — clé i18n technique pour les catégories seedées (ex: seed.categories.alimentation.epicerie). NULL pour les catégories custom → le renderer utilise name brut.
  • Entrée dans user_preferences : categories_schema_version = 'v1' ou 'v2' (détermine quelle taxonomie le profil utilise).

Important : cette migration n'écrase pas le seed v2 des profils existants. Elle ajoute juste la colonne i18n_key (NULL par défaut) et pose categories_schema_version='v2' pour tous les profils existants.

consolidated_schema.sql mis à jour

  • Contient le seed v1 complet (issu de spike-archived/seed-standard/code/seed-proposal-v1.sql)
  • Pose categories_schema_version='v1' par défaut
  • Les i18n_key sont peuplées au seed

Clés i18n ajoutées

Dans src/i18n/locales/fr.json et en.json, nouveau namespace categoriesSeed :

{
  "categoriesSeed": {
    "revenus": { "root": "Revenus", "emploi": { "root": "Emploi", "paie": "Paie régulière", ... } },
    "alimentation": { "root": "Alimentation", "epicerie": { "root": "Épicerie & marché", "reguliere": "Épicerie régulière", ... } },
    ...
  }
}

Les i18n_key dans la BDD pointent vers ces clés : ex categoriesSeed.alimentation.epicerie.reguliere.

Rien de neuf en schéma v2 : pas de colonnes frequency ni essentiality

Ces attributs sont hors scope (cf. spec-decisions).

Architecture

Composants React (nouveaux)

Fichier Rôle
src/pages/CategoriesStandardGuidePage.tsx Page read-only de Livraison 1
src/pages/CategoriesMigrationPage.tsx Page 3-étapes de Livraison 2 (routeur interne par étape)
src/components/categories-migration/StepDiscover.tsx Étape 1
src/components/categories-migration/StepSimulate.tsx Étape 2 avec table 3 colonnes
src/components/categories-migration/StepConsent.tsx Étape 3 avec checklist et loader
src/components/categories-migration/MappingRow.tsx Ligne de table avec badge confiance + panneau latéral
src/components/categories-migration/TransactionPreviewPanel.tsx Panneau latéral montrant transactions impactées
src/components/dashboard/CategoriesV1DiscoveryBanner.tsx Bannière dashboard one-shot
src/components/settings/CategoriesMigrationBackupBanner.tsx Bannière post-migration (90j)

Services (nouveaux)

Fichier Rôle
src/services/categoryTaxonomyService.ts Source de vérité de la taxonomie v1 (structure hardcodée en TS, utilisée par StepDiscover + StepSimulate pour afficher l'arbre cible). Import depuis JSON bundle.
src/services/categoryMappingService.ts Calcule le mapping v2→v1 avec badge de confiance. Implémente l'algo 4-passes (keyword → supplier → défaut → revue). Retourne une structure MigrationPlan en mémoire, sans écriture BDD.
src/services/categoryMigrationService.ts Orchestre la migration : prend un MigrationPlan + BackupResult validé, exécute la transaction SQL atomique (INSERT catégories v1 → UPDATE transactions/budgets/keywords → DELETE catégories v2 non mappées → création parent "Catégories personnalisées (migration)" si custom détectées).
src/services/categoryBackupService.ts Wrapper autour de dataExportService pour le flow pre-migration : crée un fichier SREF nommé <ProfileName>_avant-migration-<ISO8601>.sref dans ~/Documents/Simpl-Resultat/backups/, vérifie l'intégrité (read-back + checksum), retourne BackupResult ou lève une erreur claire.

Hooks (nouveaux)

Fichier Rôle
src/hooks/useCategoryTaxonomy.ts Charge la taxonomie v1 depuis le service (useMemo).
src/hooks/useCategoryMigration.ts useReducer pour l'état de la page de migration (étape courante, mapping plan, backup result, erreurs).

Fichier JSON de taxonomie

src/data/categoryTaxonomyV1.json — structure hiérarchique de la taxonomie v1 utilisée par categoryTaxonomyService.ts. Régénéré depuis spec-plan-*/code/seed-proposal-v1.sql (source de vérité = le SQL, le JSON est dérivé pour l'UI).

Flow d'intégration

Utilisateur clique "Créer la sauvegarde et migrer"
    ↓
useCategoryMigration dispatches START_MIGRATION
    ↓
categoryBackupService.createAndVerify()
    → Tauri: pick_save_file + write_export_file + read_import_file
    → Si échec : dispatch BACKUP_FAILED, abort, aucune écriture BDD
    → Si succès : dispatch BACKUP_VERIFIED, retourne BackupResult
    ↓
categoryMigrationService.applyMigration(plan, backup)
    → BEGIN TRANSACTION
    → INSERT catégories v1 (IDs 1000+, i18n_key peuplées, is_inputable calculé)
    → UPDATE transactions SET category_id = <mapping[old_id]>
    → UPDATE budgets SET category_id = <mapping[old_id]>
    → UPDATE keywords SET category_id = <mapping[old_id]>
    → Si custom détectées : INSERT parent "Catégories personnalisées (migration)" + re-parent
    → DELETE FROM categories WHERE id IN (<v2 non mappées, non custom>)
    → UPDATE user_preferences SET value='v1' WHERE key='categories_schema_version'
    → INSERT INTO user_preferences (key='last_categories_migration', value=<JSON métadonnées>)
    → COMMIT
    → Si erreur : ROLLBACK, backup reste disponible pour rétablissement
    ↓
dispatch MIGRATION_SUCCESS, affiche écran succès

Plan de travail

Issue 1 — Seed v1 + i18n keys pour nouveaux profils [type:task]

Dépendances : aucune

  • Ajouter migration SQL v8 : colonne categories.i18n_key TEXT NULL + user_preferences('categories_schema_version', 'v2') pour profils existants
  • Mettre à jour consolidated_schema.sql avec le seed v1 complet (issu de spike-archived/seed-standard/code/seed-proposal-v1.sql) et poser categories_schema_version='v1' par défaut
  • Créer src/data/categoryTaxonomyV1.json dérivé du SQL seed v1
  • Ajouter les clés i18n FR et EN dans src/i18n/locales/{fr,en}.json sous categoriesSeed.*
  • Adapter le renderer CategoryTree/CategoryCombobox pour utiliser i18n_key si présent, fallback sur name
  • Tests : création d'un nouveau profil → vérifier que le seed v1 est appliqué, que categories_schema_version='v1', et que les noms s'affichent traduits FR/EN

Issue 2 — Service categoryTaxonomyService (source taxonomie v1 en TS) [type:task]

Dépendances : Issue 1

  • Créer src/services/categoryTaxonomyService.ts avec getTaxonomyV1() retournant l'arbre typé depuis le JSON
  • Créer src/hooks/useCategoryTaxonomy.ts
  • Exposer des helpers : findByPath(path), getLeaves(), getParentById(id)

Issue 3 — Page "Guide des catégories standard" (Livraison 1) [type:feature]

Dépendances : Issue 2

  • Créer route /paramètres/categories/standard dans src/App.tsx
  • Créer src/pages/CategoriesStandardGuidePage.tsx
  • Implémenter l'arbre navigable avec expand/collapse, tooltips, compteur global
  • Bouton recherche full-text
  • Bouton export PDF (via window.print() avec feuille style dédiée, ou lib PDF léger)
  • Ajouter lien dans src/components/settings/CategoriesCard.tsx (ou équivalent)

Issue 4 — Bannière dashboard one-shot + découverte [type:feature]

Dépendances : Issue 3

  • Créer src/components/dashboard/CategoriesV1DiscoveryBanner.tsx
  • Ajouter flag categories_v1_banner_dismissed dans user_preferences
  • Intégrer au Dashboard.tsx : affichée si categories_schema_version='v2' AND flag non-dismiss
  • CTA dismissable vers la page Guide

Issue 5 — Service categoryMappingService (algo ventillage 4 passes) [type:task]

Dépendances : Issue 2

  • Créer src/services/categoryMappingService.ts
  • Implémenter l'algo 4 passes (keyword → supplier → défaut → revue)
  • Types : MigrationPlan, MappingRow, ConfidenceBadge
  • Fonction computeMigrationPlan(profileData): MigrationPlan — pure, sans effet de bord BDD
  • Mapping table encodée depuis spike-archived/seed-standard/code/mapping-old-to-new.md
  • Détection des catégories custom (non présentes dans le seed v2)

Issue 6 — Service categoryBackupService + wrapper SREF pre-migration [type:task]

Dépendances : aucune (peut aller en parallèle avec Issue 5)

  • Créer src/services/categoryBackupService.ts
  • Fonction createPreMigrationBackup(profile): Promise<BackupResult> :
    • Génère nom fichier <ProfileName>_avant-migration-<ISO8601>.sref
    • Emplacement par défaut ~/Documents/Simpl-Resultat/backups/
    • Appelle dataExportService.performExport('transactions_with_categories', 'json', password)
    • Écrit via write_export_file (commande Tauri existante)
    • Vérifie intégrité via read_import_file + checksum SHA-256
    • Retourne BackupResult { path, size, checksum, verifiedAt } ou throw
  • Gérer erreurs : espace disque, permissions, chiffrement si profil a un PIN

Issue 7 — Page de migration 3-étapes (Livraison 2) [type:feature]

Dépendances : Issue 5, Issue 6

  • Créer route /paramètres/categories/migrer
  • Créer src/pages/CategoriesMigrationPage.tsx avec routeur interne (step 1/2/3)
  • Créer src/components/categories-migration/ avec StepDiscover, StepSimulate, StepConsent, MappingRow, TransactionPreviewPanel
  • Créer src/hooks/useCategoryMigration.ts (useReducer)
  • Créer src/services/categoryMigrationService.ts avec applyMigration(plan, backup) :
    • Transaction SQL atomique (BEGIN/COMMIT/ROLLBACK)
    • INSERT v1 + UPDATE transactions/budgets/keywords + création parent custom + DELETE v2 non mappées
    • Journal dans user_preferences.last_categories_migration
  • Écran succès/échec avec chemin backup et options de rollback

Issue 8 — Bouton "Rétablir la sauvegarde" (90 jours) [type:feature]

Dépendances : Issue 6, Issue 7

  • Créer src/components/settings/CategoriesMigrationBackupBanner.tsx
  • Afficher dans Paramètres > Catégories si last_categories_migration.timestamp < 90 jours et flag banner_dismissed=false
  • Modale de confirmation
  • Appel à dataImportService.importFullProfile(path, { mode: 'replace' })
  • Post-rollback : mettre à jour categories_schema_version='v2' et last_categories_migration.reverted_at

Issue 9 — Tests complets [type:test]

Dépendances : Issues 1, 2, 5, 6, 7

  • Tests unitaires categoryMappingService (algo 4 passes, badges confiance, détection custom)
  • Tests unitaires categoryBackupService (création, vérification, erreurs)
  • Tests intégration : flow complet plan → backup → migrate → verify → rollback
  • Tests régression : transactions/budgets/keywords préservés post-migration avec IDs remappés
  • Tests régression : fixtures paramétrées (ancien seed v2 ET nouveau seed v1) sur budget, transactions, splits, auto-catégorisation
  • QA manuelle : checklist dans docs/qa-refonte-seed-categories-ipc.md couvrant les 3 étapes UI, les cas nominal/échec/rollback

Ordre d'exécution

Livraison 1 (E read-only + seed nouveaux profils):
  Issue 1 → Issue 2 → Issue 3 → Issue 4

Livraison 2 (B migration profils existants):
  Issue 2 → Issue 5 ─┐
                     ├→ Issue 7 → Issue 8
  Issue 6 ───────────┘

Tests:
  Issues 1,2,5,6,7 → Issue 9

Livraison 1 = Issues 1-4 (PR #1). Livraison 2 = Issues 5-9 (PR #2 ou series).

Fichiers concernés

Fichier Action Raison
src-tauri/src/lib.rs Modifier Ajouter migration v8 (colonne i18n_key + pref categories_schema_version)
src-tauri/src/database/migrations/v8__category_schema_version.sql Créer Migration additive
src-tauri/src/database/consolidated_schema.sql Modifier Seed v1 complet pour nouveaux profils
src/i18n/locales/fr.json Modifier Nouveau namespace categoriesSeed + clés UI migration
src/i18n/locales/en.json Modifier Nouveau namespace categoriesSeed + clés UI migration
src/data/categoryTaxonomyV1.json Créer Dérivé du SQL seed v1
src/services/categoryTaxonomyService.ts Créer Source taxonomie v1 côté TS
src/services/categoryMappingService.ts Créer Algo 4 passes
src/services/categoryBackupService.ts Créer Wrapper SREF pre-migration
src/services/categoryMigrationService.ts Créer Orchestration migration SQL atomique
src/hooks/useCategoryTaxonomy.ts Créer
src/hooks/useCategoryMigration.ts Créer useReducer état page migration
src/pages/CategoriesStandardGuidePage.tsx Créer Livraison 1
src/pages/CategoriesMigrationPage.tsx Créer Livraison 2
src/components/categories-migration/* Créer 5 composants (step 1/2/3 + mapping row + preview panel)
src/components/dashboard/CategoriesV1DiscoveryBanner.tsx Créer Bannière one-shot
src/components/settings/CategoriesMigrationBackupBanner.tsx Créer Bannière post-migration 90j
src/components/settings/CategoriesCard.tsx Modifier Ajouter lien vers page Guide + page Migrer
src/pages/Dashboard.tsx Modifier Intégrer la bannière découverte
src/App.tsx Modifier Nouvelles routes
src/components/categories/CategoryTree.tsx Modifier Support i18n_key fallback name
src/components/categories/CategoryCombobox.tsx Modifier Idem
docs/architecture.md Modifier Documenter nouveaux services, pages, migration v8
docs/adr/NNNN-refonte-seed-categories-ipc.md Créer ADR pour le choix IPC + pattern prévisualisation-consentement
docs/qa-refonte-seed-categories-ipc.md Créer Checklist QA manuelle
CHANGELOG.md Modifier Entrée sous [Unreleased] — Added/Changed
CHANGELOG.fr.md Modifier Idem FR

Plan de tests

Tests unitaires

  • categoryMappingService.computeMigrationPlan() : chaque règle de mapping v2→v1 (18 haute / 12 moyenne / 3 basse / 1 aucune) retourne le bon badge et la bonne cible.
  • Algo 4 passes :
    • Pass 1 (keyword match) avec diverses combinaisons
    • Pass 2 (supplier propagation)
    • Pass 3 (fallback défaut)
    • Pass 4 (flag "à réviser")
  • Détection des catégories custom (absentes du seed v2) → bucket preserved.
  • Détection des splits (ex: Transport en commun 28 v2 → Bus 1521 OR Train 1522 v1).
  • categoryBackupService.createPreMigrationBackup() avec mocks Tauri :
    • Succès normal : retourne BackupResult valide
    • Échec write_export_file : throw erreur "Impossible de créer la sauvegarde"
    • Échec integrity check : throw erreur "Sauvegarde corrompue"
    • Profil avec PIN : chiffrement appliqué

Tests d'intégration

  • Flow complet plan → backup → migrate → verify sur profil fixture v2 réaliste :
    • Catégories v2 mappées correctement
    • Transactions re-liées aux nouvelles catégories v1
    • Keywords re-liés
    • Budgets re-liés
    • Catégories custom regroupées sous "Catégories personnalisées (migration)"
  • Flow rollback après migration : import SREF restaure l'état v2 exact (transactions, keywords, budgets, categories).
  • Échec backup → abort → aucune écriture BDD (profil v2 intact).
  • Échec migration SQL → ROLLBACK → profil v2 intact, backup reste disponible.

Tests de régression

Fixtures paramétrées v2 ET v1 pour couvrir :

  • Auto-catégorisation (categorizationService.applyKeywordToTransaction)
  • Budgets mensuels et agrégation parent/enfant (budgetService.getBudgetVsActual)
  • Splits de transactions sur catégories multiples (transactionService.splitTransaction)
  • Import CSV avec matching supplier/keyword
  • Export/Import SREF (pas de régression sur le format)
  • UI : CategoryTree et CategoryCombobox rendent correctement v2 et v1

Critères d'acceptation

Livraison 1

  • Tout nouveau profil créé après la MAJ a le seed v1 appliqué (vérifié par SELECT sur la BDD d'un fresh profile).
  • La bannière dashboard s'affiche sur les profils v2 existants au premier lancement post-MAJ.
  • La bannière disparaît après dismiss et ne réapparaît plus (persistant).
  • La page /paramètres/categories/standard affiche correctement l'arbre complet v1 avec FR/EN.
  • Recherche full-text trouve les catégories par nom ou par mot-clé associé.
  • Export PDF produit un document lisible de la taxonomie.
  • Aucun changement aux données des profils v2 existants (test : avant/après MAJ, SELECT * FROM categories identique).

Livraison 2

  • Page /paramètres/categories/migrer est accessible depuis la page Guide et depuis Paramètres.
  • Étape 2 affiche le bon badge de confiance pour chaque catégorie (validation sur fixture).
  • Toutes les catégories 🟠 "split requis" bloquent l'avancement tant que non résolues.
  • Backup SREF est créé et vérifié AVANT toute écriture BDD.
  • Échec backup → abort → aucune écriture BDD (profil v2 intact).
  • Migration succès → transactions, budgets, keywords tous re-liés correctement.
  • Catégories custom préservées sous "Catégories personnalisées (migration)".
  • Bannière post-migration visible pendant 90 jours, dismissable.
  • Bouton Rétablir la sauvegarde fonctionne : restaure exactement l'état v2.

Global

  • Tous les tests unitaires et intégration passent (npm test, cargo test).
  • Type-check clean (npm run build).
  • CHANGELOG mis à jour FR et EN.
  • docs/architecture.md mis à jour.
  • ADR rédigé.
  • QA manuelle exécutée selon checklist docs/qa-refonte-seed-categories-ipc.md.

Edge cases et risques

Cas Mitigation
Profil v2 avec 0 catégorie custom → mapping simple Testé par fixture minimale
Profil v2 avec ≥50 catégories custom (utilisateur power) UI pagine la liste dans l'étape 2 ; parent "Catégories personnalisées (migration)" absorbe tout
Utilisateur abandonne étape 2 en plein milieu Aucune écriture BDD, le plan en mémoire est perdu — OK, aucun effet secondaire
Utilisateur abandonne étape 3 après checklist cochée mais avant backup Bouton Annuler abort propre, aucune écriture
Espace disque insuffisant pour backup categoryBackupService lève erreur claire → UI montre écran d'échec avec options "changer dossier / réessayer / annuler"
Migration SQL échoue au milieu ROLLBACK automatique, backup reste disponible, UI affiche erreur + invite à rétablir
Utilisateur lance 2 instances de Simpl'Résultat en parallèle pendant la migration SQLite lock naturel ; la 2e instance attend ; bas risque (app desktop mono-fenêtre en pratique)
Profil protégé par PIN : backup doit être chiffré categoryBackupService récupère le PIN depuis le ProfileContext et passe en password à write_export_file
Utilisateur renomme/déplace le fichier SREF après migration Le bouton Rétablir affiche un file picker si le chemin enregistré n'est plus valide
Déjà-migré : utilisateur re-lance la page de migration Détection categories_schema_version='v1' → message "Votre profil utilise déjà la taxonomie v1" avec option "revoir la sauvegarde" uniquement
Utilisateur veut migrer APRÈS les 90 jours (bannière disparue) Entrée permanente dans Paramètres > Catégories reste disponible → bouton Explorer / Migrer
Clé i18n manquante (typo dans le JSON) Fallback sur le nom brut de la catégorie — pas de crash
Seed v1 manque une feuille qu'un utilisateur a en v2 (ex: "Projets") Mapping badge 🔴 + prompt obligatoire étape 2, ou préservé en custom
Compatibilité forward : une future v2 du seed (refinement v1.1, v1.2) categories_schema_version permet de détecter et ajouter des colonnes plus tard. Pattern réutilisable.
Performance : 139 catégories + 100+ keywords au seed pour un nouveau profil < 200 ms sur SSD moderne, négligeable
Performance : migration de 5000 transactions Transaction unique, < 2 s sur SSD moderne, loader visible pendant ce temps