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>
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 utilisenamebrut. - 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.sqlavec le seed v1 complet (issu despike-archived/seed-standard/code/seed-proposal-v1.sql) et posercategories_schema_version='v1'par défaut - Créer
src/data/categoryTaxonomyV1.jsondérivé du SQL seed v1 - Ajouter les clés i18n FR et EN dans
src/i18n/locales/{fr,en}.jsonsouscategoriesSeed.* - Adapter le renderer CategoryTree/CategoryCombobox pour utiliser
i18n_keysi présent, fallback surname - 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.tsavecgetTaxonomyV1()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/standarddanssrc/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_dismisseddansuser_preferences - Intégrer au
Dashboard.tsx: affichée sicategories_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énère nom fichier
- 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.tsxavec 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.tsavecapplyMigration(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égoriessilast_categories_migration.timestamp< 90 jours et flagbanner_dismissed=false - Modale de confirmation
- Appel à
dataImportService.importFullProfile(path, { mode: 'replace' }) - Post-rollback : mettre à jour
categories_schema_version='v2'etlast_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.mdcouvrant 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 → verifysur 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
rollbackaprè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 :
CategoryTreeetCategoryComboboxrendent 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/standardaffiche 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 categoriesidentique).
Livraison 2
- Page
/paramètres/categories/migrerest 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.mdmis à 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 |