feat(categories): merge custom categories into the standard taxonomy #296

Merged
maximus merged 1 commit from issue-259-merge-custom-categories into main 2026-07-19 19:43:48 +00:00
Owner

Fixes #259

Problème

Les catégories personnalisées (absentes du seed v2) sans correspondance standard étaient affichées en texte brut dans l'assistant de migration, sans aucun choix, puis re-parentées d'office sous « Catégories personnalisées (migration) ». Aucun moyen de proposer une cible manuellement.

Solution

Le bloc préservé rend maintenant le même MappingRow que les lignes du seed. Choisir une feuille standard fusionne la catégorie custom : ses transactions, budgets, mots-clés et fournisseurs sont réassignés vers la feuille, et la custom est désactivée. Laisser le champ vide conserve le comportement précédent et ne bloque jamais le bouton « Suivant ».

Changements

  • Reducer (useCategoryMigration.ts) — RESOLVE_ROW résout dans plan.rows ET plan.preserved ; le garde-fou du bouton « Suivant » (unresolved) ne compte que les lignes du seed.
  • Writer (categoryMigrationService.ts) — helper partagé isResolvedTarget ; le mapping de réécriture inclut les préservées résolues ; le fourre-tout n'est créé que s'il reste une custom non fusionnée ; les customs fusionnées sont désactivées au lieu d'être re-parentées. Les boucles génériques (étapes 3-7) sont inchangées.
  • UI (StepSimulate.tsx) — bloc préservé en MappingRow (picker feuilles-seulement déjà en place).
  • i18n (FR/EN) + CHANGELOG (FR/EN).

Tests

  • Reducer : résolution d'une préservée + bump confidence ; le garde-fou reste sur les lignes du seed ; GO_NEXT avance malgré une custom non fusionnée.
  • Writer : réassignation vers la feuille choisie (params [leaf, custom] vérifiés sur tx/budgets/keywords/suppliers) ; soft-delete ; pas de fourre-tout vide quand tout est fusionné ; régression — fusion d'une custom parente avec enfant custom non résolu → enfant re-parenté sous le fourre-tout, parent désactivé, aucun orphelin.
  • 836 vitest verts, tsc + vite build propre. Aucune migration DB (v1→v16).

Hors scope

Signalés MINOR par le plan-checker, laissés à une éventuelle suite (l'issue demande de réutiliser MappingRow tel quel) : badge « À réviser » rouge sur une custom non fusionnée, et undo d'une fusion vers l'état « préservée ».

Fixes #259 ## Problème Les catégories personnalisées (absentes du seed v2) sans correspondance standard étaient affichées en texte brut dans l'assistant de migration, sans aucun choix, puis re-parentées d'office sous « Catégories personnalisées (migration) ». Aucun moyen de proposer une cible manuellement. ## Solution Le bloc préservé rend maintenant le même `MappingRow` que les lignes du seed. Choisir une feuille standard **fusionne** la catégorie custom : ses transactions, budgets, mots-clés et fournisseurs sont réassignés vers la feuille, et la custom est désactivée. Laisser le champ vide conserve le comportement précédent et ne bloque jamais le bouton « Suivant ». ## Changements - **Reducer** (`useCategoryMigration.ts`) — `RESOLVE_ROW` résout dans `plan.rows` ET `plan.preserved` ; le garde-fou du bouton « Suivant » (`unresolved`) ne compte que les lignes du seed. - **Writer** (`categoryMigrationService.ts`) — helper partagé `isResolvedTarget` ; le mapping de réécriture inclut les préservées résolues ; le fourre-tout n'est créé que s'il reste une custom non fusionnée ; les customs fusionnées sont désactivées au lieu d'être re-parentées. Les boucles génériques (étapes 3-7) sont inchangées. - **UI** (`StepSimulate.tsx`) — bloc préservé en `MappingRow` (picker feuilles-seulement déjà en place). - **i18n** (FR/EN) + **CHANGELOG** (FR/EN). ## Tests - Reducer : résolution d'une préservée + bump confidence ; le garde-fou reste sur les lignes du seed ; `GO_NEXT` avance malgré une custom non fusionnée. - Writer : réassignation vers la feuille choisie (params `[leaf, custom]` vérifiés sur tx/budgets/keywords/suppliers) ; soft-delete ; pas de fourre-tout vide quand tout est fusionné ; **régression** — fusion d'une custom parente avec enfant custom non résolu → enfant re-parenté sous le fourre-tout, parent désactivé, aucun orphelin. - 836 vitest verts, tsc + vite build propre. Aucune migration DB (v1→v16). ## Hors scope Signalés MINOR par le plan-checker, laissés à une éventuelle suite (l'issue demande de réutiliser `MappingRow` tel quel) : badge « À réviser » rouge sur une custom non fusionnée, et undo d'une fusion vers l'état « préservée ».
maximus added 1 commit 2026-07-18 22:07:10 +00:00
feat(categories): merge custom categories into the standard taxonomy (#259)
All checks were successful
PR Check / rust (pull_request) Successful in 22m15s
PR Check / frontend (pull_request) Successful in 2m29s
60d7f8ca49
Custom categories with no standard match were shown as read-only text in
the migration wizard and force-parented under a catch-all bucket. They now
get the same inline target picker as seeded rows: picking a standard leaf
merges the custom category — its transactions, budgets, keywords and
suppliers are reassigned to the leaf — and deactivates it. Leaving a custom
unmapped keeps the previous behaviour and never blocks the wizard.

- Reducer: RESOLVE_ROW resolves rows in both plan.rows and plan.preserved;
  the Next-button guard still counts seeded rows only.
- Writer: the rewrite mapping now includes resolved preserved rows; the
  catch-all parent is created only when a custom is left unmerged; merged
  customs are deactivated instead of re-parented (shared isResolvedTarget
  helper across the three sites).
- UI: the preserved block renders MappingRow instead of plain text.
- i18n (FR/EN) + CHANGELOG (FR/EN).

Tests: reducer (resolve a preserved custom, guard unchanged, GO_NEXT still
proceeds) + writer (reassign to the chosen leaf, soft-delete, no empty
parent when all merged, orphan-free merge of a custom parent with an
unresolved child). 836 vitest green, tsc + vite build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
maximus added the
status:review
label 2026-07-18 22:07:36 +00:00
Author
Owner

Revue /pr-review — APPROVE

Fusion des catégories personnalisées dans la taxonomie standard. Le câblage sur les 3 couches (reducer, writer, UI) est cohérent, la sémantique de fusion est correcte et le cas piège parent/enfant est couvert par un test de régression. Aucun problème bloquant.

Vérifié

  • Reducer (useCategoryMigration.ts) — RESOLVE_ROW applique la cible via applyTarget sur rows ET preserved (no-op sur le bucket qui ne contient pas la ligne, ids globalement uniques → pas de collision de clé). unresolved (garde du bouton « Suivant ») recalculé sur rows seulement → une custom laissée non fusionnée ne bloque jamais. 3 nouveaux tests ciblent exactement ces 3 invariants.
  • Writer (categoryMigrationService.ts) — mapping construit depuis [...plan.rows, ...plan.preserved] : les customs fusionnées passent par le même réécriture tx/budgets/mots-clés/fournisseurs (étapes 3-7). Le fourre-tout 2000 n'est créé que si hasUnmergedPreserved. L'étape 8 saute les fusionnées (isResolvedTarget → continue), l'étape 9 les désactive. Gestion de collision budget/keyword robuste (re-requête par itération), ordre de Map déterministe.
  • Orphelins — parent custom fusionné + enfant custom non fusionné : l'enfant est re-parenté sous 2000 (étape 8), le parent désactivé (étape 9), aucun dangling sous un parent mort. Test de régression présent et exact. Symétrique (parent non fusionné + enfant fusionné) sûr aussi : tout custom actif atterrit sous 2000.
  • UI (StepSimulate.tsx) — bloc préservé en MappingRow, picker feuilles-seulement (une custom ne peut fusionner que vers une feuille = sémantique correcte). TransactionPreviewPanel interroge par v2CategoryId uniquement → une ligne préservée désormais cliquable (cible nulle) est sûre. selectedRow gérait déjà plan.preserved.
  • i18n — clés reason.preserved / needsReview / chooseTarget / editTargetAria présentes ; preserved.txCount retirée et non référencée ailleurs ; preserved.title/body conservées et toujours utilisées ; FR + EN à jour.
  • Tests / hygiène — 8 nouveaux tests, les tests d'intégration existants (Flow 2, 3 customs non fusionnées) restent verts. CHANGELOG FR + EN sous [Unreleased]. Aucune migration DB. SQL 100 % paramétré, pas de secret. Commit conventionnel, Fixes #259.

Suggestions (non bloquantes)

  1. Une custom non fusionnée affiche un badge de confiance rouge « Aucune » (visuel identique à une ligne seed bloquante) alors qu'elle ne bloque pas « Suivant ». Le corps de PR défère explicitement le badge « À réviser » — noté pour la suite. Pas de contradiction numérique (bannière + stats comptent les lignes seed seulement).
  2. Le commentaire de l'étape 8 (« We touch only the top level of the custom tree… children follow naturally ») reste inexact : la boucle re-parente toutes les customs non fusionnées, aplatissant l'arbre sous 2000. Pré-existant, mais la PR touche ce bloc — l'occasion de corriger le commentaire.
## Revue /pr-review — APPROVE Fusion des catégories personnalisées dans la taxonomie standard. Le câblage sur les 3 couches (reducer, writer, UI) est cohérent, la sémantique de fusion est correcte et le cas piège parent/enfant est couvert par un test de régression. Aucun problème bloquant. ### Vérifié - **Reducer** (`useCategoryMigration.ts`) — `RESOLVE_ROW` applique la cible via `applyTarget` sur `rows` ET `preserved` (no-op sur le bucket qui ne contient pas la ligne, ids globalement uniques → pas de collision de clé). `unresolved` (garde du bouton « Suivant ») recalculé sur `rows` seulement → une custom laissée non fusionnée ne bloque jamais. 3 nouveaux tests ciblent exactement ces 3 invariants. - **Writer** (`categoryMigrationService.ts`) — `mapping` construit depuis `[...plan.rows, ...plan.preserved]` : les customs fusionnées passent par le même réécriture tx/budgets/mots-clés/fournisseurs (étapes 3-7). Le fourre-tout 2000 n'est créé que si `hasUnmergedPreserved`. L'étape 8 saute les fusionnées (`isResolvedTarget → continue`), l'étape 9 les désactive. Gestion de collision budget/keyword robuste (re-requête par itération), ordre de Map déterministe. - **Orphelins** — parent custom fusionné + enfant custom non fusionné : l'enfant est re-parenté sous 2000 (étape 8), le parent désactivé (étape 9), aucun dangling sous un parent mort. Test de régression présent et exact. Symétrique (parent non fusionné + enfant fusionné) sûr aussi : tout custom actif atterrit sous 2000. - **UI** (`StepSimulate.tsx`) — bloc préservé en `MappingRow`, picker feuilles-seulement (une custom ne peut fusionner que vers une feuille = sémantique correcte). `TransactionPreviewPanel` interroge par `v2CategoryId` uniquement → une ligne préservée désormais cliquable (cible nulle) est sûre. `selectedRow` gérait déjà `plan.preserved`. - **i18n** — clés `reason.preserved` / `needsReview` / `chooseTarget` / `editTargetAria` présentes ; `preserved.txCount` retirée et non référencée ailleurs ; `preserved.title`/`body` conservées et toujours utilisées ; FR + EN à jour. - **Tests / hygiène** — 8 nouveaux tests, les tests d'intégration existants (Flow 2, 3 customs non fusionnées) restent verts. CHANGELOG FR + EN sous [Unreleased]. Aucune migration DB. SQL 100 % paramétré, pas de secret. Commit conventionnel, `Fixes #259`. ### Suggestions (non bloquantes) 1. Une custom non fusionnée affiche un badge de confiance rouge « Aucune » (visuel identique à une ligne seed bloquante) alors qu'elle ne bloque pas « Suivant ». Le corps de PR défère explicitement le badge « À réviser » — noté pour la suite. Pas de contradiction numérique (bannière + stats comptent les lignes seed seulement). 2. Le commentaire de l'étape 8 (« We touch only the top level of the custom tree… children follow naturally ») reste inexact : la boucle re-parente **toutes** les customs non fusionnées, aplatissant l'arbre sous 2000. Pré-existant, mais la PR touche ce bloc — l'occasion de corriger le commentaire.
maximus merged commit 2314a64213 into main 2026-07-19 19:43:48 +00:00
maximus deleted branch issue-259-merge-custom-categories 2026-07-19 19:43:48 +00:00
Sign in to join this conversation.
No reviewers
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#296
No description provided.