# 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](./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` : ```json { "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é `_avant-migration-.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 = → UPDATE budgets SET category_id = → UPDATE keywords SET category_id = → Si custom détectées : INSERT parent "Catégories personnalisées (migration)" + re-parent → DELETE FROM categories WHERE id IN () → UPDATE user_preferences SET value='v1' WHERE key='categories_schema_version' → INSERT INTO user_preferences (key='last_categories_migration', value=) → 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` : - Génère nom fichier `_avant-migration-.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 |