feat(categories): 3-step migration page + categoryMigrationService (#121) #131
No reviewers
Labels
No labels
source:analyste
source:defenseur
source:human
source:medic
status:approved
status:blocked
status:in-progress
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#131
Loading…
Reference in a new issue
No description provided.
Delete branch "issue-121-categories-migration-page"
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?
Fixes #121
Summary
Livraison 2 of the IPC category refactor. This PR adds the 3-step category migration page at
/settings/categories/migratewith a guided flow (Discover -> Simulate -> Consent) and the atomiccategoryMigrationService.applyMigration(plan, backup)that rewrites a v2 profile to the v1 IPC taxonomy inside a single SQL transaction.Depends on #115 (seed v1 + i18n keys + migration v8), #116 (categoryTaxonomyService), #117 (guide page + CategoryTaxonomyTree), #118 (dashboard banner), #119 (categoryMappingService), #120 (categoryBackupService) — all merged.
What's in
StepDiscover.tsx): reuses theCategoryTaxonomyTreecomponent from #117 so the look matches the standalone guide page. Pure reading.StepSimulate.tsx+MappingRow.tsx+TransactionPreviewPanel.tsx): 3-column dry-run table with confidence badges (🟢 high / 🔵 medium / 🟠 low / 🔴 none), a side panel listing up to 50 affected transactions per selected row, and an inline<select>picker for rows that need manual resolution. The Continue button is blocked untilstate.unresolved === 0.StepConsent.tsx): checklist of 3 acknowledgements + a PIN field for protected profiles. The confirm button runscreatePreMigrationBackupthenapplyMigrationand updates a 4-step loader stage-by-stage. Success / error screens surface the backup path + migration counters.categoryMigrationService.ts):BEGIN.Catégories personnalisées (migration)parent if needed.INSERT OR IGNOREevery v1 taxonomy node (safe to re-run after a crash).UPDATE transactions/budget_entries/budget_template_entries/keywords/suppliers.category_idper theMigrationPlanmapping; for the two budget tables and for keywords weDELETEcolliding rows first to respect theUNIQUEconstraints.is_active=0).categories_schema_version='v1'and journal the run inuser_preferences.last_categories_migration.COMMIT— on any throw,ROLLBACKand return aMigrationOutcomewith the error.useCategoryMigration.ts): auseReducercovering the 5 real steps pluserror, with 11 actions. Pure — no service calls inside the reducer, 13 unit tests cover the transitions.categoriesSeed.migration.*(page / stepper / 3 steps / running loader / success / error / backup error codes).[Unreleased] / Added.Scope limits (explicit)
i18n_keycolumn).Test plan
npx tsc --noEmitclean.npx vitest run— 181 tests pass, including 13 new reducer tests.npm run buildclean (tsc + vite).cargo checkclean.throwin devtools) → verify profile unchanged.Self-review — APPROVED
I reviewed the diff through the four lenses requested (security, correctness, quality, data). Summary below.
Security
StepConsentis kept in React state only, passed tocreatePreMigrationBackupaspasswordand never logged. It reaches the Rust side through the existingwrite_export_filecommand (same path as the regular SREF export), which already handles AES-256-GCM with Argon2id. No new cleartext-password surface.db.executeincategoryMigrationServiceuses positional parameters.applyMigrationvalidates theBackupResultstructure (path + checksum non-empty) before opening the transaction. A caller that fakes a backup object would fail the guard — it cannot skip to the writer.user_preferences.last_categories_migration.Correctness — transaction atomicity
BEGIN ... COMMITblock. Any throw in thetrytriggersROLLBACKin thecatchandoutcome.succeeded = false. The rollback itself is wrapped in a try/catch so it never leaks a separate error to the caller.INSERT OR IGNOREfor the v1 taxonomy nodes makes a partial / retried run idempotent even if a previous attempt inserted some rows before erroring out.UNIQUEcollisions onbudget_entries (category_id, year, month),budget_template_entries (template_id, category_id)andkeywords (keyword, category_id)are prevented by deleting the v2 colliding row BEFORE the UPDATE. This was the single correctness risk I identified — the UPDATE would otherwise fail mid-transaction and rollback the whole run.ROLLBACKleavescategories_schema_versionat'v2'(the update happens inside the txn), so a failed run keeps the profile correctly flagged as v2.GO_NEXTfromsimulateis a no-op whilestate.unresolved > 0. Covered byuseCategoryMigration.test.ts— the test suite passes.Data integrity
i18n_keycolumn from #115/v8 is sufficient. Respects the project rule.Catégories personnalisées (migration)parent (id 2000, outside both v1 range 1000-1999 and v2 range < 1000). They are NOT deactivated — only seeded v2 ids are.suppliers.category_idis rewritten too (even though the issue only mentioned transactions / budgets / keywords). Without it, suppliers whose category pointed at a v2 seed category would be left dangling once we deactivate the v2 seed. Net: the supplier -> v1 category relationship is preserved across the migration.is_active=0(soft delete), notDELETE FROM. This keeps historical FKs intact even in edge cases I might have missed, and matches the pattern used bycategoryService.deactivateCategory.Quality
categoryMigrationService.applyMigrationis a single top-level function with a clear step-by-step structure and a doc comment explaining the ordering and why each step is where it is.SET_OUTCOMEwithsucceeded=false+FAIL) is covered.categoriesSeed.migration.*, plussettings.categoriesCard.migrate{Title,Description}anderror.backup.*per theBackupError.codeenum.StageLinesubcomponent; stages advance from the page, not from inside the service, so the loader reflects what the page actually did.Build / CI
npx tsc --noEmitclean.npx vitest run— 181 tests pass (13 new reducer tests).npm run buildclean (tsc + vite).cargo checkclean.Scope limits — verified
applyMigrationwriter (#123).I recommend APPROVE and merge once CI confirms green.