feat(migration catégories) : mapper manuellement une catégorie custom vers la taxonomie standard (fusion) #259

Open
opened 2026-07-06 00:29:16 +00:00 by maximus · 1 comment
Owner

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 par MappingRow, 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 avec v1TargetId: null et reason: "preserved" sans jamais être soumises au moteur d'appariement, puis affichées par StepSimulate.tsx:202-216 comme 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.total et unresolved ne comptent que plan.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) :

Choisir une feuille de la taxonomie standard réassigne les transactions (et budgets, mots-clés, fournisseurs) de la catégorie custom vers cette feuille, et la catégorie custom disparaît.

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

  • UI — remplacer les <li> en texte du bloc préservé (StepSimulate.tsx:202-216) par des MappingRow, réutilisable tel quel (il prend déjà row, targetCategories, resolveTarget, onResolve).
  • Reducer — étendre RESOLVE_ROW (useCategoryMigration.ts:119-145) pour retrouver la ligne dans plan.preserved également. Garder unresolved calculé sur plan.rows uniquement, sinon le bouton « Suivant » se retrouve bloqué tant que toutes les customs ne sont pas mappées (régression).
  • Writer (categoryMigrationService.ts) — 4 retouches chirurgicales :
    • l.193 : passer les préservées résolues à buildMappingFromRows ([...plan.rows, ...plan.preserved] — le helper filtre déjà les v1TargetId === null).
    • l.199 : ne créer le fourre-tout que s'il reste au moins une custom non mappée (plan.preserved.some(p => p.v1TargetId === null)), sinon on crée un parent vide.
    • l.323-331 : ne re-parenter sous le fourre-tout que les préservées non résolues.
    • l.339-350 : désactiver (is_active = 0) les customs fusionnées, comme l'étape 9 le fait déjà pour les lignes du seed.
  • i18n — clés FR + EN sous categoriesSeed.migration.simulate.* (le bloc préservé change de discours : il ne dit plus « on les garde », il propose un choix).
  • CHANGELOG — entrée sous [Unreleased] dans CHANGELOG.md et CHANGELOG.fr.md.

Les étapes 3 à 7 du writer (transactions, budget_entries, budget_template_entries, keywords, suppliers) bouclent sur mapping.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 dans CategoryCombobox / 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 interactif
  • src/hooks/useCategoryMigration.tsRESOLVE_ROW ne voit que plan.rows
  • src/services/categoryMigrationService.ts — les 4 retouches du writer
  • src/i18n/locales/fr.json, src/i18n/locales/en.json
  • CHANGELOG.md, CHANGELOG.fr.md

Surface 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 — reducer RESOLVE_ROW
  • src/components/categories-migration/migrationTargets.test.ts

Les 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 preserved est 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

  • Chaque catégorie custom offre un type-ahead des feuilles de la taxonomie standard
  • Choisir une feuille réassigne transactions, budgets, mots-clés et fournisseurs vers elle, et désactive la catégorie custom
  • Ne rien choisir conserve le comportement actuel (rangée sous « Catégories personnalisées (migration) »)
  • Le fourre-tout n'est pas créé si toutes les customs ont été fusionnées
  • Le bouton « Suivant » n'est pas bloqué par une custom laissée non mappée
  • Test de régression : fusion d'une custom parente ayant des enfants custom

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.

## 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 par `MappingRow`, 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 avec `v1TargetId: null` et `reason: "preserved"` **sans jamais être soumises au moteur d'appariement**, puis affichées par `StepSimulate.tsx:202-216` comme 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.total` et `unresolved` ne comptent que `plan.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) : > Choisir une feuille de la taxonomie standard réassigne les transactions (et budgets, mots-clés, fournisseurs) de la catégorie custom vers cette feuille, et la catégorie custom disparaît. 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 - [ ] **UI** — remplacer les `<li>` en texte du bloc préservé (`StepSimulate.tsx:202-216`) par des `MappingRow`, réutilisable tel quel (il prend déjà `row`, `targetCategories`, `resolveTarget`, `onResolve`). - [ ] **Reducer** — étendre `RESOLVE_ROW` (`useCategoryMigration.ts:119-145`) pour retrouver la ligne dans `plan.preserved` également. **Garder `unresolved` calculé sur `plan.rows` uniquement**, sinon le bouton « Suivant » se retrouve bloqué tant que toutes les customs ne sont pas mappées (régression). - [ ] **Writer** (`categoryMigrationService.ts`) — 4 retouches chirurgicales : - l.193 : passer les préservées résolues à `buildMappingFromRows` (`[...plan.rows, ...plan.preserved]` — le helper filtre déjà les `v1TargetId === null`). - l.199 : ne créer le fourre-tout que s'il reste au moins une custom **non** mappée (`plan.preserved.some(p => p.v1TargetId === null)`), sinon on crée un parent vide. - l.323-331 : ne re-parenter sous le fourre-tout que les préservées **non résolues**. - l.339-350 : désactiver (`is_active = 0`) les customs **fusionnées**, comme l'étape 9 le fait déjà pour les lignes du seed. - [ ] **i18n** — clés FR + EN sous `categoriesSeed.migration.simulate.*` (le bloc préservé change de discours : il ne dit plus « on les garde », il propose un choix). - [ ] **CHANGELOG** — entrée sous `[Unreleased]` dans `CHANGELOG.md` et `CHANGELOG.fr.md`. Les étapes 3 à 7 du writer (transactions, `budget_entries`, `budget_template_entries`, `keywords`, `suppliers`) bouclent sur `mapping.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 dans `CategoryCombobox` / `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 interactif - `src/hooks/useCategoryMigration.ts` — `RESOLVE_ROW` ne voit que `plan.rows` - `src/services/categoryMigrationService.ts` — les 4 retouches du writer - `src/i18n/locales/fr.json`, `src/i18n/locales/en.json` - `CHANGELOG.md`, `CHANGELOG.fr.md` ## Surface 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` — reducer `RESOLVE_ROW` - `src/components/categories-migration/migrationTargets.test.ts` Les 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 `preserved` est 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 - [ ] Chaque catégorie custom offre un type-ahead des feuilles de la taxonomie standard - [ ] Choisir une feuille réassigne transactions, budgets, mots-clés et fournisseurs vers elle, et désactive la catégorie custom - [ ] Ne rien choisir conserve le comportement actuel (rangée sous « Catégories personnalisées (migration) ») - [ ] Le fourre-tout n'est pas créé si toutes les customs ont été fusionnées - [ ] Le bouton « Suivant » n'est pas bloqué par une custom laissée non mappée - [ ] Test de régression : fusion d'une custom parente ayant des enfants custom ## 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.
maximus added the
status:triage
type:feature
source:human
labels 2026-07-06 00:29:16 +00:00
Author
Owner

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 commentaire l.8 cité (« 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 StarterAccountsModal propose 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.tsx l.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.tsproposeStarterAccounts (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 »). /analyze recommandé pour cadrer l'UX.

## 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 commentaire `l.8` cité (« 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. --- <details> <summary>Corps d'origine (archive)</summary> ## Contexte Le `StarterAccountsModal` propose 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.tsx` l.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 »). `/analyze` recommandé pour cadrer l'UX. </details>
maximus changed title from 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) 2026-07-13 00:00:48 +00:00
maximus added
status:ready
and removed
status:triage
labels 2026-07-13 00:00:58 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: maximus/Simpl-Resultat#259
No description provided.