Commit graph

330 commits

Author SHA1 Message Date
le king fu
4d5a0e2e3b docs(architecture): document balance domain (tables, services, hooks, commands, routes)
Update docs/architecture.md to reflect the Bilan feature:
- BDD: 5 new tables + 7 indexes + CHECK + FK invariants (CAD lock,
  kind invariants, ON DELETE RESTRICT on transaction_id)
- Migrations: v8 + v9 added to history
- Services: balance.service.ts with its 4 logical sections
- Hooks: useBalanceAccounts / useSnapshotEditor / useBalanceOverview
- Commands: compute_account_return (1 new) + Phase 5 fetch_price stub
- Routing: /balance, /balance/snapshot, /balance/accounts
- Components: balance/ folder added in tree

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 17:06:30 -04:00
le king fu
5274e51907 chore: CHANGELOG entry for cross-cutting tests
All checks were successful
PR Check / rust (push) Successful in 22m44s
PR Check / frontend (push) Successful in 2m23s
Bilingual entry under [Unreleased] documenting the integration test
suite added for Issue #144: end-to-end happy path, currency lock,
priced-kind tolerance safety, computeAccountReturn wiring, three Rust
migration-on-seeded-DB scenarios, and the source-level non-regression
test on the inlined transfer icon.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:54:04 -04:00
le king fu
5a54d37de5 test(transactions): add non-regression test for inline transfer icon
Source-level structural test on `TransactionTable.tsx` to lock down the
inlined transfer icon contract introduced in #142. Without RTL or jsdom
in the dev-deps, the test reads the component source and asserts:

  - the icon is gated by `linkedTransfersByTxId?.has(row.id)`,
  - optional-chaining short-circuits cleanly when the prop is omitted
    (zero-impact on pre-#142 callers),
  - the prop is declared OPTIONAL on the component interface,
  - the `Link2` glyph comes from lucide-react,
  - tooltip + aria-label go through `transactions.transferIcon.*` i18n
    keys,
  - the row's description cell layout (truncate span + title) stays
    shared between linked and non-linked rows.

Catches the specific regression vectors: someone removing the gate,
renaming the prop, or breaking the optional-chaining pattern that
guarantees the page renders identically when no transfers are linked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:53:59 -04:00
le king fu
50fe0ab1ac test(balance): add migration v9 integration on seeded DB
Three new Rust integration tests applied at the bottom of `lib.rs`'s
`#[cfg(test)] mod tests`. They exercise the realistic upgrade path: a v1
profile DB with imported transactions + categories already there gets the
v9 migration applied on top.

`migration_v9_preserves_existing_transactions_on_seeded_db` asserts no
row loss / data mutation after the migration runs. Spot-checks one
amount preserved verbatim and that the v9 seeded categories coexist with
the v1 categories table.

`integration_link_unlink_transfer_roundtrip_on_seeded_db` walks link →
joined-view read → blocked deletion (FK RESTRICT) → unlink → allowed
deletion → orphan-row sanity check. Covers the FK chain end-to-end on
real (non-stub) transaction ids.

`integration_modified_dietz_inputs_read_back_correctly_on_seeded_db`
mirrors the exact SQL used by `balance_commands.rs::read_value_at_or_before`
and `read_cash_flows`, asserting the snapshot-endpoint lookups and the
period-bounded JOINed cash flows return the expected shapes when run
against a seeded v1+v9 DB.

`integration_v9_preserves_v1_categories_and_keywords` verifies the
`categories.id` and `balance_categories.id` namespaces are independent
(same numeric id allowed on each table without collision).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:53:50 -04:00
le king fu
9adfb85d84 test(balance): add cross-cutting integration tests
End-to-end happy path through the full Bilan stack: account → priced
category → priced snapshot → linked transfer → return. Drives every
service against the existing in-memory FakeDb harness used by
category-migration tests so SQL shape (table names + parameters) can be
asserted alongside service outputs.

Currency lock: USD / EUR / GBP / JPY / AUD all rejected up-front by the
service with a typed `currency_unsupported` code, no DB hit. The CAD
default is verified to land in the INSERT params explicitly.

Priced-kind safety: a snapshot save with one out-of-tolerance line must
NOT clear pre-existing lines (the DELETE is gated behind the validation
loop). A drift just within ε is accepted unchanged.

computeAccountReturn wiring: malformed dates are rejected client-side
without invoking the Rust command; missing active profile yields a typed
`transfer_active_profile_unknown`; partial-period payloads are forwarded
unchanged (null fields preserved).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:53:36 -04:00
le king fu
ca275821bc feat(balance): i18n + CHANGELOG for returns/transfers
All checks were successful
PR Check / rust (push) Successful in 22m37s
PR Check / frontend (push) Successful in 2m30s
Issue #142 / Bilan #4 — translations and changelog entries.

i18n (FR + EN):
- `balance.returns.partialTooltip`, `balance.returns.noTransfersWarning`
- `balance.accountsTable.return3m/return1y/sinceCreation/unadjusted`
  (label + tooltip variants)
- `balance.transfers.linkAction` + `balance.transfers.direction.{in,out}`
- `balance.transfers.modal.*` (every modal label, including the
  partial-failure summary and the per-row direction toggle)
- `balance.transfers.errors.*` (5 new typed error codes)
- `balance.evolution.transferIn/transferOut` (chart label)
- `transactions.transferIcon.tooltip/ariaLabel`

CHANGELOG (English source + French translation):
- New entry under `[Unreleased]` summarising the Modified Dietz
  formula, the per-account return columns (3M / 1A / since-inception
  + unadjusted), the link-transfers modal, the transactions-page
  inline icon, the typed FK error on bulk-delete paths, and the
  vertical reference markers on the evolution chart. References #142.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:39:06 -04:00
le king fu
faa09614a3 feat(balance): add transfer markers on evolution chart
Issue #142 / Bilan #4 — vertical reference lines for tagged transfers.

`BalanceEvolutionChart.tsx` accepts a new optional prop
`transferMarkers?: BalanceAccountTransferWithTransaction[]`. For every
marker whose `transaction_date` matches a date already on the X axis,
the chart renders a `<ReferenceLine>` (Recharts) — green for `in`
(capital added), red for `out` (capital removed). The marker is drawn
in both `line` and `stacked` modes; in line mode an inline label
("In" / "Out") sits at the top-right of the marker so the user can
identify the direction without hovering.

Markers whose date is between two snapshot ticks are filtered out
(Recharts categorical axis silently drops unknown ticks; preferred
over an off-axis bug). A future improvement is to switch the X axis
to a numeric/time scale so markers can land anywhere — out of scope
here per the autopilot prompt's "least invasive" guideline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:38:55 -04:00
le king fu
0e996a5aa1 feat(transactions): inline transfer icon + FK error message
Issue #142 / Bilan #4 — non-regressive transfer awareness in the
transactions table + clean error mapping on bulk delete.

- `TransactionTable.tsx`: optional new prop
  `linkedTransfersByTxId?: Map<txId, links[]>`. When supplied, a small
  `<Link2>` icon appears next to the description for every linked
  transaction; tooltip lists the account name(s) and direction(s).
  Without the prop, the table renders byte-for-byte identical to
  before — preserves the spec's non-regression invariant.
- `TransactionsPage.tsx`: loads the linked-transfers map once on mount
  via `listAllLinkedTransfersForTooltip()` (one batch SELECT) and
  threads it through to the table. Failure to load the map degrades
  gracefully to an empty map (icon simply doesn't appear).
- `transactionService.ts`: new `deleteTransaction(id)` helper +
  `TransactionLinkedToBalanceError` (typed FK guard). Pre-checks
  `balance_account_transfers` before attempting the DELETE so the
  error carries the offending account names; falls back to the FK
  pattern matcher if a race linked the transaction between the
  SELECT and the DELETE.
- `importedFileService.ts`: both bulk delete paths
  (`deleteImportWithTransactions`, `deleteAllImportsWithTransactions`)
  now pre-check for any linked transfer and surface the same typed
  error before they would explode on FK RESTRICT. The pre-check has
  a `LIMIT 50` safety cap on the global path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:38:46 -04:00
le king fu
a45e5c3cd0 feat(balance): add LinkTransfersModal + return columns in accounts table
Issue #142 / Bilan #4 — UI for transfer linking + per-account returns.

- New `LinkTransfersModal.tsx`: portal modal with date-range / category /
  free-text filters, multi-select with auto-proposed direction (`in` for
  negative bank amounts, `out` for positive — flippable per row).
  Submits via sequential `linkTransfer` calls; reports per-row failures
  inline (most common case: `transfer_already_linked` on a re-submit).
- `BalanceAccountsTable.tsx`: 4 new columns rendered side-by-side —
  3M / 1A / Since-inception (Modified Dietz via `compute_account_return`)
  + Unadjusted (`(V_end - V_start) / V_start`). Returns load lazily
  after mount via `Promise.all` over (account × horizon); per-cell
  failure leaves the slot at "—" without blocking the rest of the
  table. The actions menu gains a *Link transfers* item that bubbles
  the request up to the parent page. New props:
  `sinceCreationDate` (anchors the since-inception horizon) and
  `onLinkTransfers` (modal opener).
- `BalancePage.tsx`: hosts the new modal, loads the categories list
  once on mount for the filter dropdown, fetches the union of
  `listAccountTransfers` per account so the chart can render markers,
  and threads the earliest snapshot date down to the table. Reload
  is triggered after the modal reports at least one successful link.
- `balance.service.ts`: dropped the unused `BalanceAccountTransfer`
  import to satisfy `tsc --noUnusedLocals`.

`npm run build` clean. `npm test` → 429 passed. Manual sanity check:
the table renders "…" placeholders during the per-row return load,
then resolves to either a percentage or a "—" with the partial
tooltip when the underlying snapshot endpoint is missing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:38:24 -04:00
le king fu
dafdd4ce17 feat(balance): add returns + transfers section to balance.service
Issue #142 / Bilan #4 — TS bridge for the Modified Dietz command + plain
CRUD for transfer linking.

Types (`src/shared/types/index.ts`):
- `BalanceTransferDirection` ('in' | 'out')
- `BalanceAccountTransfer` (raw row) +
  `BalanceAccountTransferWithTransaction` (joined view)
- `AccountReturn` (mirrors the Rust struct, ready to receive the invoke
  payload as-is)

Service (`src/services/balance.service.ts`):
- `computeAccountReturn(accountId, periodStart, periodEnd)`: resolves the
  active profile's `db_filename` from `loadProfiles()` and calls the
  `compute_account_return` Tauri command.
- `linkTransfer(accountId, transactionId, direction, notes?)`: INSERT
  with duplicate guard (typed `transfer_already_linked` error instead of
  raw SQL UNIQUE failure).
- `unlinkTransfer(accountId, transactionId)`: DELETE with
  `transfer_not_linked` guard for stale-UI calls.
- `listAccountTransfers(accountId, dateRange?)`: joined SELECT for
  modal/list rendering.
- `listLinkedTransactionIds()`: returns a `Set<number>` for the
  transaction icon (one query, in-memory `.has()` lookups thereafter).
- `listAllLinkedTransfersForTooltip()`: returns
  `Map<transactionId, links[]>` for tooltip rendering.
- `suggestTransferDirection(amount)`: pure helper for the modal — maps
  negative bank amounts to 'in', positive to 'out'.
- `isLinkedTransactionFkError(error)`: detects the canonical SQLite "FK
  constraint failed" text so `transactionService.deleteTransaction` can
  surface a clear i18n message.
- 5 new error codes added to `BalanceErrorCode`.

Tests (`balance.service.test.ts`): 22 new vitest cases bringing the file
to 85 passed. Mocks `@tauri-apps/api/core` `invoke` and
`./profileService` `loadProfiles`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:27:16 -04:00
le king fu
23ff8466c0 fix(balance): use transactions.date column (not transaction_date)
The schema's transactions table uses `date` (see schema.sql:67), not
`transaction_date`. Compile-checked the column name was correct.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:24:13 -04:00
le king fu
0381dd48bb feat(balance): add compute_account_return Tauri command
Issue #142 / Bilan #4 — server-side Modified Dietz wrapper.

- New `src-tauri/src/commands/balance_commands.rs` with single command
  `compute_account_return(db_filename, account_id, period_start, period_end)`:
  - Opens the active profile DB via `rusqlite::Connection::open(app_data_dir
    / db_filename)` — matches `repair_migrations` / `delete_profile_db`.
  - Reads `value_start` (latest snapshot ≤ period_start) + `value_end`
    (latest snapshot ≤ period_end) via correlated SELECT.
  - Reads cash flows via JOIN `balance_account_transfers` ⨝
    `transactions` filtered by `transaction_date BETWEEN`. Sign applied
    per direction (`in` → +, `out` → −).
  - Calls `return_calculator::modified_dietz`, returns typed
    `AccountReturn`.
- Registered in `commands/mod.rs` (pub use) and in `lib.rs`'
  `tauri::generate_handler!` array.

`cargo check` clean. `cargo test --lib` → 54 passed (including the 7
return_calculator + 7 migration_v9 tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:23:14 -04:00
le king fu
c9cdb5a891 feat(balance): add chrono dep + Modified Dietz return_calculator with tests
Issue #142 / Bilan #4 — TDD step 1.

- Added `chrono = "0.4"` (default-features off, `serde` + `std` features)
  to `src-tauri/Cargo.toml` for day-precision date arithmetic.
- New private module `src-tauri/src/commands/return_calculator.rs`:
  - `pub(crate) fn modified_dietz(value_start, value_end, cash_flows,
    period_start, period_end) -> AccountReturn`
  - `AccountReturn { value_start, value_end, net_contributions, return_pct,
    annualized_pct, is_partial, has_no_transfers_warning }` (Serialize)
  - Edge cases handled: missing start/end snapshot (`is_partial = true`,
    `return_pct = None`), no transfers (collapses to simple return + warn
    flag), zero-length period (skips annualization), V_start = 0 with first
    flow > 0 (account-created mid-period), depleted-then-refilled (no
    panic, finite output).
- 7 co-located TDD tests covering nominal + every edge case above.
- Module declared `pub(crate)` in `commands/mod.rs` (kept out of the
  wildcard re-export — only `balance_commands.rs` will consume it).

`cargo test --lib commands::return_calculator` → 7 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:21:37 -04:00
le king fu
1e261ae2ea feat(balance): i18n + CHANGELOG for /balance page
All checks were successful
PR Check / rust (push) Successful in 22m24s
PR Check / frontend (push) Successful in 2m22s
FR + EN translations under:
- nav.balance — sidebar label
- balance.overview.* — page title, latest total, Δ% vs previous,
  staleness warning, new-snapshot CTA, accounts table headers,
  empty/no-snapshot states
- balance.period.* — 3M / 6M / 1A / 3A / all selector labels
- balance.chart.* — empty state, mode legend, line / stacked
  toggle labels
- balance.sidebar — entry label (mirrors nav.balance)

CHANGELOG entry under [Unreleased] / Added documenting the new
page, period selector, evolution chart modes, accounts table,
sidebar entry, the four service helpers, and the new hook.

Refs: #141

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:08:10 -04:00
le king fu
83ac484a22 feat(balance): add sidebar Bilan entry
Insert a new "Bilan" / "Balance sheet" entry in NAV_ITEMS pointing
at /balance with the Wallet lucide-react icon. Position: between
Reports and Settings, matching the autopilot prompt instruction
and the spec-plan-bilan v2 ordering.

Sidebar.tsx imports Wallet and registers it in iconMap.

Refs: #141

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:07:13 -04:00
le king fu
ffefa90fd0 feat(balance): add BalancePage with chart + accounts table
Three new components composed under a new BalancePage at /balance:

- BalanceOverviewCard — latest aggregate net worth, Δ% vs the
  previous chronological snapshot (rendered as "—" when only
  one snapshot exists), 60-day staleness warning, and a
  "+ Nouveau snapshot" CTA pointing at /balance/snapshot.

- BalanceEvolutionChart — Recharts-based line / stacked-area
  toggle. Line mode plots SUM(value) per snapshot_date with a
  single primary-coloured stroke. Stacked mode transposes the
  byCategory series into one Area per category_key with a
  fixed 10-color palette indexed deterministically. Tooltip
  formats CAD via Intl.NumberFormat.

- BalanceAccountsTable — one row per active account with name,
  category label, latest value, and Δ% over the active period
  (latest_value vs the period anchor). Returns columns
  (3M / 1Y / since-creation / unadjusted) reserved for #142
  with a TODO marker. Action menu includes a disabled "Detail"
  placeholder + functional "Archive" wired through reload().

BalancePage composes the three with an inline period selector
(3M / 6M / 1A / 3A / Tout) and chart-mode toggle, both styled
as segmented controls. State flows through useBalanceOverview.

Route /balance registered before /balance/accounts in App.tsx.

Refs: #141

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:07:04 -04:00
le king fu
202b008bc9 feat(balance): add useBalanceOverview hook
Scoped useReducer hook backing BalancePage. Tracks:
- period (3M / 6M / 1A / 3A / all) — defaults to 1A
- chartMode (line / stacked) — defaults to line
- evolutionTotals + evolutionByCategory + accountsLatest +
  accountsPeriodAnchor (parallel-fetched on mount and on every
  period change via Promise.all)
- isLoading + error

Exposes computeBalanceDateRange(period, today) as a pure helper
so the date math is unit-testable without mocking time. Anchors
on `today` rather than the latest snapshot — keeps the chart's
right edge stable as the user enters new snapshots.

Refs: #141

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:06:38 -04:00
le king fu
396310aa74 feat(balance): add timeseries aggregator helpers + tests
Add four service helpers used by the upcoming `/balance` overview:

- getSnapshotTotalsByDate(range?) — SUM(value) GROUP BY snapshot_date
  with an optional inclusive [from, to] range. LEFT JOIN preserves
  empty snapshots as zero rows so the chart shows continuity.
- getSnapshotTotalsByCategoryAndDate(range?) — same aggregation broken
  down by balance_categories.key, returned as one row per snapshot
  date with a `byCategory` map. Powers the stacked-area variant.
- getAccountsLatestSnapshot() — one row per active account with the
  value of its most-recent snapshot line (NULL when none exists).
  Filters archived accounts via WHERE is_active = 1 AND archived_at
  IS NULL, matches the listBalanceAccounts default.
- getAccountsPeriodAnchor(range) — earliest snapshot_date >= from
  per account, with the value at that date — the anchor used to
  compute the per-account Δ% column on the accounts table.

Tests cover empty DB, single/multi snapshot, archived exclusion via
SQL inspection, date-range params (from-only, both bounds, open).

Refs: #141

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:06:23 -04:00
le king fu
80c0a97841 feat(balance): i18n + CHANGELOG for priced kind
All checks were successful
PR Check / rust (push) Successful in 22m31s
PR Check / frontend (push) Successful in 2m21s
- Adds keys under balance.category.kind, balance.category.form.kindLabel
  / kindHint*, balance.category.actions.deleteHasAccountsHint,
  balance.category.error.has_accounts, balance.account.form
  .symbolRequiredForPriced, balance.snapshot.priced.* (FR + EN).
- Extends balance.errors.* with the four new typed codes:
  snapshot_priced_quantity_required,
  snapshot_priced_unit_price_required,
  snapshot_priced_value_mismatch,
  snapshot_simple_must_be_scalar.
- CHANGELOG entries (FR + EN) under [Unreleased].

Refs #140

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 15:02:18 -04:00
le king fu
5bc7fe80b1 feat(balance): improve category deletion UX with linked-accounts message
- AccountsPage Categories tab now uses the new AccountForm 'category'
  variant for creation (with kind selector).
- Delete button is disabled when the category has linked accounts;
  the disabled tooltip surfaces the count.
- Clicking the delete button on a category with linked accounts now
  shows a dismissable error banner listing up to the first 3 names
  (with ellipsis when more) so the user knows exactly which accounts
  to archive first. The service-level FK RESTRICT remains the
  ultimate guard.

Refs #140

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 15:01:44 -04:00
le king fu
6288a3fe23 feat(balance): support priced kind in AccountForm + SnapshotLineRow
- AccountForm now exposes a 'category' variant with a kind selector
  (simple | priced); the legacy 'account' variant is unchanged
  modulo the new symbol-required-for-priced UI guard.
- SnapshotLineRow dispatches on account.category_kind:
  * simple variant unchanged from #146
  * priced variant: quantity + unit_price inputs + read-only
    computed value rendered live (qty × price, 2 decimals) +
    [Manuel] attribution tag
- useSnapshotEditor extends state with pricedValues map, exposes
  setLineQuantity / setLineUnitPrice handlers, prefill copies
  quantity but leaves unit_price blank (per spec-decisions row),
  save() builds mixed simple+priced batches.
- SnapshotEditor + SnapshotEditPage thread the new priced state.
- Total line at the top of SnapshotEditPage now sums simple + priced
  contributions live as the user types.

Refs #140

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 15:01:38 -04:00
le king fu
db5bffbdcf feat(balance): add priced-kind validation to service + tests
- Export validateLineKindInvariants helper for both 'simple' and 'priced'
  account kinds; surfaces typed BalanceServiceError codes.
- Extend SnapshotLineInput with optional account_kind / quantity /
  unit_price (default 'simple' to preserve #146 callers).
- upsertSnapshotLines now validates kind invariants ahead of the SQL
  CHECK and persists priced lines with non-NULL qty / unit_price.
- Tolerance constant PRICED_VALUE_TOLERANCE = 0.01 absorbs FP drift.
- 14 new unit tests covering simple invariants, priced invariants,
  tolerance edge cases, and mixed batches.

Refs #140

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:55:20 -04:00
le king fu
8f5cc71707 feat(balance): add i18n keys + CHANGELOG entry for snapshot editor
All checks were successful
PR Check / rust (push) Successful in 21m48s
PR Check / frontend (push) Successful in 2m19s
i18n FR/EN under balance.snapshot.* — page (titles, date label and
immutability notice, total, prefill, save/create/delete buttons),
editor (empty state), line (placeholder + a11y label), delete
(double-confirm modal copy). Five new error codes added to
balance.errors.* (snapshot_date_required, snapshot_date_taken,
snapshot_not_found, snapshot_value_invalid, snapshot_priced_unsupported).

Adds common.back so the SnapshotEditPage back arrow has a localized title.

CHANGELOG entries for #146 under [Unreleased] in both EN and FR.

Refs #146

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:49:41 -04:00
le king fu
fdc6cc6c38 feat(balance): add useSnapshotEditor hook + SnapshotEditPage + components
New scoped useReducer hook covering the full single-snapshot lifecycle —
LOAD_FOR_DATE / SET_LINE_VALUE / SAVE / DELETE / PREFILL_FROM_PREVIOUS /
RESET — with the following semantics:
- 'new' mode (?date= absent or no snapshot at that date) creates the row
  at save time only, so abandoning the form does not leave an empty
  snapshot behind;
- 'edit' mode loads existing lines + prefills the values map;
- prefillFromPrevious copies simple-kind values from the most recent
  earlier snapshot (priced branch is a no-op + TODO Issue #140);
- save() flips 'new' -> 'edit' on success and updates the URL ?date=
  so refresh keeps the user in edit mode;
- snapshotDate is immutable in edit mode (UI guard, matches spec).

New SnapshotEditPage at /balance/snapshot:
- date picker (native input type=date — matches the AdjustmentForm /
  TransactionFilterBar / PeriodSelector pattern, no new dep)
- per-category groups of accounts with one value field each
- prefill button (disabled when no earlier snapshot exists, with
  tooltip explaining why)
- delete button with double-confirmation modal that requires retyping
  the snapshot date before the destructive action enables.

New SnapshotEditor (groups by category sort_order) and SnapshotLineRow
(simple variant — single value field per account) components.

Route /balance/snapshot wired in App.tsx.

Refs #146

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:49:33 -04:00
le king fu
afc338b564 feat(balance): extend balance.service with snapshots + lines (simple kind)
Adds the snapshots + lines section of the balance service for Issue #146
(Bilan #1b). Simple-kind only — quantity / unit_price are forced to NULL
both at the SQL CHECK level (already in v9) and at the service level
(`upsertSnapshotLines` validates ahead of time). Priced-kind upsert lands
in #140.

New service exports:
- listSnapshots / getSnapshotByDate / getSnapshotById / getPreviousSnapshot
- createSnapshot (throws snapshot_date_taken when UNIQUE per date violated
  so the UI can redirect to edit mode)
- updateSnapshot / deleteSnapshot (cascade lines via FK)
- listLinesBySnapshot / upsertSnapshotLines (rewrite-all strategy)

New BalanceErrorCode entries: snapshot_date_required, snapshot_date_taken,
snapshot_not_found, snapshot_value_invalid, snapshot_priced_unsupported.

New shared types: BalanceSnapshot, BalanceSnapshotLine.

22 new vitest cases cover: invalid-date guards, unique-per-date violation,
simple-kind null invariant on inserts, NaN/Infinity rejection,
clear+rewrite line semantics, getPreviousSnapshot strict-before ordering.

Refs #146

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:49:19 -04:00
le king fu
4c71eaca2d feat(balance): add i18n keys + CHANGELOG entry
All checks were successful
PR Check / rust (push) Successful in 22m25s
PR Check / frontend (push) Successful in 2m27s
PR Check / rust (pull_request) Successful in 22m44s
PR Check / frontend (pull_request) Successful in 2m21s
Adds the FR/EN translation namespace `balance.*` covering:
- balance.accountsPage.* (page chrome, tab labels, empty states)
- balance.account.* (table fields, status badges, action labels,
  full account form copy with priced/simple-aware hints)
- balance.category.* (intro text, table fields, kind labels, origin
  labels, action prompts, simple-only creation form, seeded labels
  for the 7 standard categories)
- balance.errors.* (one entry per BalanceErrorCode union member)

CHANGELOG.md and CHANGELOG.fr.md both gain a single entry under
`[Unreleased] / Added` summarising the schema migration v9, the new
balance.service CRUD section and the AccountsPage tabs.

Refs #138

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:38:09 -04:00
le king fu
fccc8e4fa2 feat(balance): add useBalanceAccounts hook + AccountsPage + AccountForm
Wires the AccountsPage end-to-end with a new scoped useReducer hook,
the page itself (accessible at /balance/accounts) and the account form.

useBalanceAccounts (src/hooks/useBalanceAccounts.ts):
- Loads accounts (excludes archived by default) + categories in parallel
- Surfaces typed errors from balance.service via state.errorCode so the
  UI can localize them (e.g. seed protection, currency rejection)
- CRUD operations on both domains: addAccount/editAccount/archive/
  unarchiveAccount + addCategory/editCategory/removeCategory

AccountsPage (src/pages/AccountsPage.tsx):
- Two tabs: Comptes + Catégories
- Accounts tab: archive toggle, table of (name, category, symbol,
  currency, status), inline edit/archive/restore
- Categories tab: full list of seeded + user categories. Add new
  simple-kind category (priced creation lands in #140). Rename via
  inline prompt; delete disabled on seeded rows. Errors surfaced via
  i18n keys keyed on BalanceErrorCode.

AccountForm (src/components/balance/AccountForm.tsx):
- Variant=account only (category variant lands in #140)
- Auto-detects priced category to hint the symbol field
- Full FR/EN coverage of labels and validation messages

Per spec-plan-bilan.md v2 the sidebar entry "Bilan" is intentionally
not added in this issue — it lands in #141 (Bilan #3) when the
/balance overview becomes navigable. Until then the route is reachable
directly via URL.

Refs #138

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:37:30 -04:00
le king fu
58d3c86336 feat(balance): add balance.service CRUD section + tests
Adds the TypeScript service layer for the Bilan feature, scoped to
Issue #138 (Bilan #1a) — categories + accounts CRUD only. Snapshots,
snapshot lines, transfers and price-fetching land in subsequent issues.

The service uses `getDb()` + tauri-plugin-sql directly per project
convention (96 occurrences across 15 services). No new Tauri commands
introduced — the only future Rust commands are `compute_account_return`
(Issue #142) and `fetch_price` (Issue #144).

API surface:
- listBalanceCategories / getBalanceCategory / createBalanceCategory /
  updateBalanceCategory / deleteBalanceCategory (with seed + has-accounts
  guards)
- listBalanceAccounts (excludes archived by default) / getBalanceAccount
  / createBalanceAccount (CAD-only at MVP) / updateBalanceAccount /
  archiveBalanceAccount / unarchiveBalanceAccount (soft delete)

Typed errors via BalanceServiceError + BalanceErrorCode union so the UI
can render distinct i18n messages. Domain types added under
`src/shared/types/index.ts`: BalanceCategoryKind, BalanceCategory,
BalanceAccount, BalanceAccountWithCategory, BALANCE_CURRENCY_CAD.

19 vitest cases cover: ordering, kind validation, seed protection,
linked-account guard, currency rejection, missing-category lookup,
soft delete + restore round-trip, symbol/notes normalization.

Refs #138

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:33:39 -04:00
le king fu
a6787adef0 feat(balance): add migration v9 schema (5 tables, 7 indexes, seed)
Adds the SQL foundation for the Bilan (balance sheet) feature:

- 5 new tables: balance_categories, balance_accounts, balance_snapshots,
  balance_snapshot_lines, balance_account_transfers
- 7 indexes (category, active partial, snapshot, accounts x2, transaction,
  snapshot_date)
- Seed of 7 standard categories (5 simple + 2 priced) marked is_seed=1
- CHECK(currency = 'CAD') on balance_accounts (MVP — v2 lifts the constraint
  with a multi-currency rate table)
- CHECK kind invariants on balance_snapshot_lines (quantity/unit_price both
  NULL OR both NOT NULL)
- FK transaction_id ON DELETE RESTRICT to preserve reproducibility of
  Modified Dietz returns calculated on past periods

Migration v9 is added inline to the lib.rs Vec<Migration> via a new
constant database::BALANCE_SCHEMA backed by balance_schema.sql. The
schema is mirrored in consolidated_schema.sql so brand-new profiles
get the feature preinstalled without replaying v9.

13 new co-located rusqlite tests validate the migration on a fresh
in-memory DB: schema applies cleanly, 7 categories seeded with correct
kinds, CHECK rejects invalid currency/kind/direction, UNIQUE rejects
duplicate snapshot_date / (snapshot_id,account_id) / (transaction_id,
account_id), FK CASCADE on snapshot delete, FK RESTRICT on transaction
delete and on category with linked accounts, seed idempotent on replay.

Refs #138

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:31:50 -04:00
1f506fb171 Merge pull request 'fix(reports): render category combobox in hierarchical DFS order (#126)' (#134) from issue-126-category-combobox-presentation into main 2026-04-22 01:09:35 +00:00
le king fu
871768593d fix(reports): render category combobox in hierarchical DFS order (#126)
All checks were successful
PR Check / rust (push) Successful in 22m7s
PR Check / frontend (push) Successful in 2m19s
PR Check / rust (pull_request) Successful in 21m37s
PR Check / frontend (pull_request) Successful in 2m14s
The by-category report combobox (`/reports/category`) was showing its full
category list with scrambled indentation — parents from one sub-tree
interleaved with children from another. Root cause: `getAllCategoriesWithCounts`
returns rows via `ORDER BY sort_order, name`, which is a *global* sort; two
different roots with sort_order=1 would be followed by their respective
children in the same bucket, mixing depths together.

Add a pure `sortHierarchical(categories, resolveName)` helper in
`CategoryCombobox.tsx` that rebuilds the display order as a DFS walk of the
tree: each parent is emitted immediately followed by its descendants, with
siblings within a group sorted by `sort_order` then localized display name.
Orphans (parent filtered out or missing) are appended at the end so nothing
disappears. The helper runs client-side inside the combobox's `useMemo`, so
the fix is scoped to this component and doesn't affect other consumers of
`getAllCategoriesWithCounts`. Filtering on the input query remains unchanged.

Covered by 7 unit tests on the helper (empty list, single root, the exact
bug-reproducing scrambled case, sort_order + name tiebreak, 3-level
hierarchy, orphans, idempotence).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 20:58:53 -04:00
le king fu
1c9eebb78c chore: release v0.8.4
All checks were successful
Release / build-and-release (push) Successful in 24m1s
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 19:32:44 -04:00
95f708c8fd Merge pull request 'test(categories): complete test coverage for migration flow (#123)' (#133) from issue-123-complete-tests into main 2026-04-21 23:28:08 +00:00
le king fu
12d1877870 test(categories): complete test coverage for migration flow (#123)
All checks were successful
PR Check / rust (push) Successful in 22m48s
PR Check / frontend (push) Successful in 2m20s
PR Check / rust (pull_request) Successful in 22m51s
PR Check / frontend (pull_request) Successful in 2m21s
Adds unit + integration + regression tests and a QA checklist for the
v2→v1 seed migration feature.

- Fixtures: src/__fixtures__/profiles.ts (makeV2Profile, makeV1Profile,
  makeV2ProfileWithCustom) with realistic categories, keywords,
  suppliers, transactions, budgets.
- Unit: categoryMappingService (100 cases covering every DEFAULT_MAPPINGS
  entry, 4-pass priority, splits, preserved/custom detection),
  categoryBackupService (23 cases — Tauri mocks: success, write error,
  integrity check, PIN-encrypted profile), categoryMigrationService (16
  cases — BEGIN/COMMIT/ROLLBACK flow, backup-missing abort, journaling,
  custom parent creation).
- Integration: full plan→backup→migrate→verify flow; rollback via SREF
  import; backup failure → no DB write; migration SQL failure → ROLLBACK
  + intact state.
- Regression: parameterised v2/v1 fixtures covering auto-categorisation,
  budget aggregation, splits preservation.
- Docs: docs/qa-refonte-seed-categories-ipc.md — manual checklist for UX,
  system errors, encrypted profile, custom preservation, 90-day banner,
  restore flow.

331 vitest tests pass (up from 193 baseline).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 19:25:13 -04:00
4c7f3d09e1 Merge pull request 'feat(categories): restore backup banner and permanent restore action (#122)' (#132) from issue-122-restore-backup-banner into main 2026-04-21 01:52:08 +00:00
le king fu
0132e6e164 feat(categories): add restore backup banner and permanent restore action (#122)
All checks were successful
PR Check / rust (push) Successful in 21m45s
PR Check / frontend (push) Successful in 2m17s
PR Check / rust (pull_request) Successful in 21m1s
PR Check / frontend (pull_request) Successful in 2m13s
Surfaces the pre-migration SREF backup to the user so they can roll back a
category migration without digging into the filesystem:

- 90-day dismissable banner at the top of Settings > Categories pointing to
  the automatic backup (hidden once reverted, once dismissed, or past 90d).
- Permanent "Restore a backup" entry in Settings > Categories, available as
  long as a migration journal exists (even past the 90-day window).
- Confirmation modal with two-step consent, red Restore button, fallback
  file picker when the recorded path is missing, PIN prompt for encrypted
  SREF files, full-page reload on success.

Internals:
- New `categoryRestoreService` wrapping `read_import_file` +
  `importTransactionsWithCategories` with stable error codes
  (file_missing, read_failed, parse_failed, wrong_envelope_type,
  needs_password, wrong_password, import_failed).
- New `file_exists` Tauri command for the pre-flight presence check.
- On success: `categories_schema_version=v2` + merge `reverted_at` into
  `last_categories_migration`.
- Pure `shouldShowBanner` / `isWithinBannerWindow` helpers with tests.
- FR/EN i18n keys under `settings.categoriesCard.restore*`.
- CHANGELOG entries in both locales.

Closes #122
2026-04-20 21:47:43 -04:00
b9734acd93 Merge pull request 'feat(categories): 3-step migration page + categoryMigrationService (#121)' (#131) from issue-121-categories-migration-page into main 2026-04-21 01:33:55 +00:00
le king fu
0646875327 feat(categories): add 3-step migration page + categoryMigrationService (#121)
All checks were successful
PR Check / rust (push) Successful in 21m39s
PR Check / frontend (push) Successful in 2m21s
PR Check / rust (pull_request) Successful in 21m49s
PR Check / frontend (pull_request) Successful in 2m15s
New user-facing 3-step migration flow at /settings/categories/migrate that
allows legacy v2 profiles to opt in to the v1 IPC taxonomy.

Step 1 Discover — read-only taxonomy tree (reuses CategoryTaxonomyTree from
Livraison 1, #117).
Step 2 Simulate — 3-column dry-run table with confidence badges (high /
medium / low / needs-review), transaction preview side panel, inline target
picker for unresolved rows. The "next" button is blocked until every row is
resolved.
Step 3 Consent — checklist + optional PIN field for PIN-protected profiles +
4-step loader (backup created / verified / SQL applied / committed).

Success and error screens surface the SREF backup path and the counts of
rows migrated. Errors never leave the profile in a partial state — the new
categoryMigrationService wraps the entire SQL writeover in a
BEGIN/COMMIT/ROLLBACK atomic transaction and aborts up-front if the backup
is not present / verified.

New code:
- src/services/categoryMigrationService.ts — applyMigration(plan, backup)
  atomic writer (INSERT v1 → UPDATE transactions/budgets/budget_templates/
  keywords/suppliers → reparent preserved customs → deactivate v2 seed →
  bump categories_schema_version=v1 → journal last_categories_migration).
- src/hooks/useCategoryMigration.ts — useReducer state machine
  (discover → simulate → consent → running → success | error).
- src/hooks/useCategoryMigration.test.ts — 13 pure reducer tests.
- src/components/categories-migration/{StepDiscover,StepSimulate,StepConsent,
  MappingRow,TransactionPreviewPanel}.tsx — UI per the mockup.
- src/pages/CategoriesMigrationPage.tsx — wrapper with internal router,
  stepper, backup/migrate orchestration, success/error screens.

Tweaks:
- src/App.tsx — new /settings/categories/migrate route.
- src/components/settings/CategoriesCard.tsx — additional card surfacing
  the migrate entry for v2 profiles only.
- src/i18n/locales/{fr,en}.json — categoriesSeed.migration.* namespace
  (page / stepper / 3 steps / running / success / error / backup error codes).
- CHANGELOG.{md,fr.md} — [Unreleased] / Added entry.

Scope limits respected: no SQL migration modified, no new migration added,
no unit tests of the applyMigration writer (covered by #123 in wave 4),
no restore-backup button (#122 in wave 4), no post-migration banner (#122).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 21:31:21 -04:00
084b621506 Merge pull request 'feat(categories): dashboard v1 discovery banner (#118)' (#130) from issue-118-dashboard-discovery-banner into main 2026-04-21 01:16:20 +00:00
le king fu
0ded5a1ac6 feat(categories): add dashboard v1 discovery banner (#118)
All checks were successful
PR Check / rust (push) Successful in 21m42s
PR Check / frontend (push) Successful in 2m14s
PR Check / rust (pull_request) Successful in 21m50s
PR Check / frontend (pull_request) Successful in 2m17s
Adds a non-destructive, dismissable banner on the Dashboard that invites
profiles still on the legacy v2 category seed to discover the new v1 IPC
taxonomy. The banner links to the standard categories guide page
(/settings/categories/standard, shipped in #117).

Visibility rules:
- Only rendered when `categories_schema_version='v2'` in `user_preferences`.
- Hidden once the user dismisses it — dismissal is persisted in the same
  `user_preferences` table under the key `categories_v1_banner_dismissed`
  (value '1'), so the banner never reappears across app restarts.
- New v1-seeded profiles never see it.

No DB schema change: reuses the existing key/value `user_preferences`
table via the existing `getPreference`/`setPreference` helpers. No
migration added.

i18n keys under `dashboard.categoriesBanner.*` (FR + EN).
Changelog entry added under [Unreleased] in both CHANGELOG.md and
CHANGELOG.fr.md.
2026-04-20 21:11:56 -04:00
1640a73499 Merge pull request 'feat(categories): categoryMappingService 4-pass algo (#119)' (#128) from issue-119-category-mapping-service into main 2026-04-21 01:07:16 +00:00
115f707823 Merge pull request 'feat(categories): categories standard guide page (#117)' (#129) from issue-117-categories-standard-guide into main 2026-04-21 01:07:05 +00:00
le king fu
defa63a063 feat(categories): add categories standard guide page (#117)
All checks were successful
PR Check / rust (push) Successful in 21m42s
PR Check / frontend (push) Successful in 2m17s
PR Check / rust (pull_request) Successful in 22m37s
PR Check / frontend (pull_request) Successful in 2m12s
Adds a read-only Settings subpage at /settings/categories/standard that
exposes the full v1 IPC category taxonomy:

- Recursive tree with per-root expand/collapse (chevron buttons), clickable
  only via the disclosure caret — no destructive actions anywhere on the
  page.
- Live counter banner: roots / subcategories / leaves / total, computed
  from the bundled categoryTaxonomyV1 JSON.
- Accent- and case-insensitive full-text search over translated names;
  matching nodes keep their ancestor chain visible, non-matching branches
  are pruned from the visible tree.
- Hover tooltips (native `title`) showing i18n_key, type (income /
  expense / transfer — translated) and numeric id of each node — useful
  for power-users cross-referencing the consolidated schema.
- Export as PDF button that triggers window.print(); a dedicated
  @media print rule in styles.css forces every branch to render fully
  expanded during printing regardless of the on-screen collapse state,
  and hides the toolbar / back-link.
- All labels resolve via t(node.i18n_key, { defaultValue: node.name })
  to be forward-compatible with future user-created taxonomy rows that
  have no i18n_key.

Also:
- New CategoriesCard in Settings that links to the page (FolderTree
  icon, consistent with the userGuide / changelog card pattern).
- i18n keys added under categoriesSeed.guidePage.* and
  settings.categoriesCard.* (FR + EN).
- CHANGELOG.md + CHANGELOG.fr.md updated under [Unreleased] / Added.

Route uses the English-style `/settings/categories/standard` to match
the rest of the app (/settings, /categories, /changelog, ...). The
original spec mentions a French-accented path but the existing router
is English-only; documenting here so reviewers can see the decision.

No SQL migration, no schema change, no write to the database — this
is strictly a read-only view on the TS-side taxonomy bundle.

Type-check clean (tsc --noEmit), 148/148 vitest tests pass, vite build
succeeds.
2026-04-20 21:02:38 -04:00
le king fu
be3cda1556 feat(categories): add categoryMappingService (4-pass algo) (#119)
All checks were successful
PR Check / rust (push) Successful in 22m33s
PR Check / frontend (push) Successful in 2m18s
PR Check / rust (pull_request) Successful in 21m36s
PR Check / frontend (pull_request) Successful in 2m13s
Pure function that computes a v2 → v1 category migration plan from a
snapshot of the profile data. The 4-pass algorithm (keyword → supplier
propagation → default fallback → needs review) produces a MigrationPlan
with confidence badges (high/medium/low/none) per row, exposes split
targets for categories that ventilate across multiple v1 leaves (e.g.
Transport en commun → Autobus + Train), and preserves user-custom
categories in a dedicated bucket for later placement under
"Catégories personnalisées (migration)".

- Mapping tables encoded from
  .spikes/archived/seed-standard/code/mapping-old-to-new.md
- No DB I/O: the caller hands us categories, keywords, transactions and
  optional suppliers; the service stays testable and side-effect-free.
- 20 unit tests cover every pass, custom preservation, split exposure,
  stats aggregation and pass priority.

Prepares the ground for #121 (migration writer UI).
2026-04-20 21:01:55 -04:00
b8fa089c5f Merge pull request 'feat(categories): categoryTaxonomyService + useCategoryTaxonomy (#116)' (#127) from issue-116-category-taxonomy-service into main 2026-04-21 00:54:58 +00:00
le king fu
742aa9ec3c feat(categories): add categoryTaxonomyService + useCategoryTaxonomy hook (#116)
All checks were successful
PR Check / rust (push) Successful in 22m44s
PR Check / frontend (push) Successful in 2m17s
PR Check / rust (pull_request) Successful in 22m4s
PR Check / frontend (pull_request) Successful in 2m15s
Source of truth for the v1 IPC taxonomy on the TS side. Loads the bundled
JSON, exposes typed helpers (findById, findByPath, getLeaves, getParentById)
used by the upcoming Guide (#117) and Migration (#121) pages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:53:15 -04:00
le king fu
b37be36ddc chore(claude): add project rules and release skill, ignore local state
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:44:49 -04:00
le king fu
4912ae39b0 docs: add WIP specs for OAuth keychain, monetisation, reports, and web
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:41:00 -04:00
f3af3d7c1b Merge pull request 'feat(categories): v1 IPC seed + i18n keys + migration v8 (#115)' (#125) from issue-115-seed-v1-i18n into main 2026-04-19 20:51:16 +00:00
63feebefc8 Merge pull request 'feat(categories): categoryBackupService pre-migration SREF wrapper (#120)' (#124) from issue-120-category-backup-service into main 2026-04-19 20:48:51 +00:00