feat(migration catégories) : mapper manuellement une catégorie custom vers la taxonomie standard (fusion) #259
Labels
No labels
autopilot:pending-human
source:analyste
source:defenseur
source:human
source:medic
status:approved
status:blocked
status:in-progress
status:needs-clarification
status:needs-fix
status:ready
status:review
status:triage
type:bug
type:feature
type:infra
type:refactor
type:schema
type:security
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: maximus/Simpl-Resultat#259
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Contexte
Lors de la migration vers la hiérarchie de catégories standard,
computeMigrationPlan(categoryMappingService.ts:330-357) range les catégories du profil dans deux seaux, et un seul est éditable :plan.rows— les catégories du seed standard. Elles passent par les 4 passes d'appariement (mot-clé, fournisseur, défaut, review) et sont rendues parMappingRow, qui porte un type-ahead éditable sur chaque ligne, résolue ou non (livré en #246/#252).plan.preserved— les catégories custom de l'utilisateur (id absent du seed v2). Elles sont poussées avecv1TargetId: nulletreason: "preserved"sans jamais être soumises au moteur d'appariement, puis affichées parStepSimulate.tsx:202-216comme de simples<li>en texte brut : nom + nombre de transactions. Aucun picker, aucune interaction.Leur sort est scellé sans consultation : le writer (
categoryMigrationService.ts:320-331) les re-parente d'office sous une catégorie fourre-tout « Catégories personnalisées (migration) ».Elles sont d'ailleurs absentes du reste de l'écran :
stats.totaletunresolvedne comptent queplan.rows.C'est le problème signalé : les catégories sans appariement n'offrent aucun choix pour proposer une catégorie manuellement.
Demande
Permettre de choisir manuellement une cible pour chaque catégorie custom, avec une sémantique de fusion (tranchée avec Max le 2026-07-12) :
C'est exactement le comportement déjà appliqué aux lignes de
plan.rows. Ne rien choisir conserve le comportement actuel (la custom est préservée sous « Catégories personnalisées (migration) ») — une custom non mappée reste un choix légitime et ne doit pas bloquer le bouton « Suivant ».Hors périmètre : le « re-parentage » (garder la custom vivante mais la ranger sous un parent standard choisi) a été explicitement écarté.
Travail à faire
<li>en texte du bloc préservé (StepSimulate.tsx:202-216) par desMappingRow, réutilisable tel quel (il prend déjàrow,targetCategories,resolveTarget,onResolve).RESOLVE_ROW(useCategoryMigration.ts:119-145) pour retrouver la ligne dansplan.preservedégalement. Garderunresolvedcalculé surplan.rowsuniquement, sinon le bouton « Suivant » se retrouve bloqué tant que toutes les customs ne sont pas mappées (régression).categoryMigrationService.ts) — 4 retouches chirurgicales :buildMappingFromRows([...plan.rows, ...plan.preserved]— le helper filtre déjà lesv1TargetId === null).plan.preserved.some(p => p.v1TargetId === null)), sinon on crée un parent vide.is_active = 0) les customs fusionnées, comme l'étape 9 le fait déjà pour les lignes du seed.categoriesSeed.migration.simulate.*(le bloc préservé change de discours : il ne dit plus « on les garde », il propose un choix).[Unreleased]dansCHANGELOG.mdetCHANGELOG.fr.md.Les étapes 3 à 7 du writer (transactions,
budget_entries,budget_template_entries,keywords,suppliers) bouclent surmapping.entries()et sont génériques — elles n'ont pas à être touchées : alimenter la Map suffit à faire fonctionner la fusion.Rien à changer non plus dans
categoryMappingService.ts(les préservées gardent leur forme, elles reçoivent juste une cible), ni dansCategoryCombobox/taxonomyToComboboxCategories(le catalogue feuilles-seulement est exactement ce qu'il faut pour une fusion).Fichiers concernés
src/components/categories-migration/StepSimulate.tsx— bloc préservé à rendre interactifsrc/hooks/useCategoryMigration.ts—RESOLVE_ROWne voit queplan.rowssrc/services/categoryMigrationService.ts— les 4 retouches du writersrc/i18n/locales/fr.json,src/i18n/locales/en.jsonCHANGELOG.md,CHANGELOG.fr.mdSurface de test
Base existante, déjà consciente du sujet (toutes ces suites mentionnent
preserved) :src/services/categoryMappingService.test.ts(607 l., 26 tests)src/services/categoryMigrationService.test.ts(366 l., 34 tests)src/hooks/useCategoryMigration.test.ts— reducerRESOLVE_ROWsrc/components/categories-migration/migrationTargets.test.tsLes tests affirmant que les préservées sont re-parentées devront être ajustés (ce ne sera plus vrai pour celles qui sont fusionnées).
Régression à écrire explicitement : fusionner une catégorie custom parente qui a des enfants custom. La liste
preservedest plate (parents et enfants y figurent comme entrées séparées), donc l'enfant non résolu retombe sous le fourre-tout via l'étape 8 pendant que le parent est désactivé — pas d'orphelin. Le comportement est sûr par construction, mais aucun test ne le garantit aujourd'hui.Contrainte d'infra : ni jsdom ni @testing-library dans le repo — aucun composant React n'est rendable en test. La logique nouvelle doit vivre dans le reducer et le writer, qui sont testables.
Critères d'acceptation
Complexité estimée
Medium — le lourd (combobox, catalogue de feuilles,
MappingRow, machinerie de réécriture SQL) existe déjà et est générique. Le travail est du câblage sur trois couches, plus les tests.Correction de périmètre (analyse 2026-07-12)
Le titre et le corps d'origine de cette issue étaient factuellement faux. Ils ont été rédigés à chaud le 2026-07-05 pendant le run v0.12.0 et transposaient le signalement initial par analogie, sans lecture du code.
Ce que l'issue décrivait (faux) : un mapping automatique des comptes standards vers les comptes existants dans le
StarterAccountsModal(module Bilan), avec une case désactivée en l'absence de similaire.Ce que le code fait réellement : aucune notion de mapping n'existe dans ce flux. La seule interaction par ligne est une case à cocher « créer ce compte : oui / non », et elle est désactivée quand une collision EST détectée (
StarterAccountsModal.tsx:160,disabled={isCollision || submitting}) — soit la polarité inverse. Le commentairel.8cité (« the matching checkbox is disabled ») désignait « la case correspondante », pas « la case de matching ».Le vrai problème signalé (confirmé avec Max) est dans la migration des catégories vers la hiérarchie standard : les catégories pour lesquelles il n'y a pas d'appariement n'offrent aucun choix pour proposer une catégorie manuellement. Le corps de l'issue est réécrit en conséquence.
Corps d'origine (archive)
Contexte
Le
StarterAccountsModalpropose des comptes standards (Compte chèque, CELI, REER, Compte non-enregistré) et les associe automatiquement aux comptes existants du profil. Quand aucun similaire n'est auto-identifié, la case de matching est désactivée / non-cochable (cf.StarterAccountsModal.tsxl.8) — l'utilisateur ne peut pas mapper manuellement.Demande
Permettre de mapper un compte standard vers un compte existant même pour ceux sans similaire auto-identifié : offrir un sélecteur manuel (type-ahead / dropdown des comptes existants) au lieu du seul match automatique.
Fichiers
src/components/balance/StarterAccountsModal.tsx— UI de matching (case désactivée quand pas de similaire)src/services/balance.service.ts—proposeStarterAccounts(si le mapping affecte la proposition)Note
Analogue au picker de cible éditable de la migration catégories (#246/#252 : cible modifiable sur chaque ligne, pas seulement les « à réviser »).
/analyzerecommandé pour cadrer l'UX.feat(bilan) : mapper un compte standard vers un compte existant même sans similaire auto-identifiéto feat(migration catégories) : mapper manuellement une catégorie custom vers la taxonomie standard (fusion)