feat(balance): detail-account wizard (pivot date) (#215) #225

Merged
maximus merged 1 commit from issue-215-detail-wizard into main 2026-06-10 01:07:59 +00:00
Owner

Resolves #215

Stacked on #224.

Light confirmation modal (DetailAccountWizard) flipping a simple balance account to detailed entry mode. Toggle-only per the 2026-06-04 plan-overnight decision: sets kind='detailed' + detailed_since = today (local civil day, YYYY-MM-DD) via updateBalanceAccount. No title capture — per-security holdings are entered at the next normal snapshot (validateDetailedSnapshot requires them from the pivot on).

  • Entry point: Détailler en titres action in the per-row actions menu of BalanceAccountsTable, shown only for kind === 'simple' rows (replaces the disabled Détail / coming soon placeholder).
  • Past aggregated history (snapshots < detailed_since) stays frozen read-only.
  • One-way: the #212 service backstop rejects detailed → simple once holdings exist; the UI exposes no inverse action. The wizard surfaces the typed error via balance.detailWizard.errors.account_kind_detailed_has_holdings.
  • Exports a pure buildDetailToggleInput() helper covered by 3 focused unit tests (no jsdom harness in this project).
  • FR/EN i18n under balance.detailWizard.*; removed the now-dead balance.overview.detailAction / detailComingSoon keys.
  • Gates: npm run build green, npm test 621 passing.

Generated autonomously by /autopilot run of 2026-06-06

Resolves #215 Stacked on #224. Light confirmation modal (`DetailAccountWizard`) flipping a **simple** balance account to **detailed** entry mode. Toggle-only per the 2026-06-04 plan-overnight decision: sets `kind='detailed'` + `detailed_since = today` (local civil day, YYYY-MM-DD) via `updateBalanceAccount`. No title capture — per-security holdings are entered at the next normal snapshot (`validateDetailedSnapshot` requires them from the pivot on). - Entry point: `Détailler en titres` action in the per-row actions menu of `BalanceAccountsTable`, shown only for `kind === 'simple'` rows (replaces the disabled `Détail / coming soon` placeholder). - Past aggregated history (snapshots `< detailed_since`) stays frozen read-only. - One-way: the #212 service backstop rejects `detailed → simple` once holdings exist; the UI exposes no inverse action. The wizard surfaces the typed error via `balance.detailWizard.errors.account_kind_detailed_has_holdings`. - Exports a pure `buildDetailToggleInput()` helper covered by 3 focused unit tests (no jsdom harness in this project). - FR/EN i18n under `balance.detailWizard.*`; removed the now-dead `balance.overview.detailAction` / `detailComingSoon` keys. - Gates: `npm run build` green, `npm test` 621 passing. Generated autonomously by /autopilot run of 2026-06-06
maximus added the
status:review
autopilot:pending-human
labels 2026-06-06 17:56:10 +00:00
maximus added 1 commit 2026-06-06 17:56:11 +00:00
Adds a light confirmation modal (DetailAccountWizard) that flips a simple
balance account to detailed entry mode: sets kind='detailed' and
detailed_since = today (local civil day, YYYY-MM-DD) via updateBalanceAccount.
Toggle-only — no title capture; per-security holdings are entered at the next
normal snapshot, where validateDetailedSnapshot requires them from the pivot on.

Entry point: a 'Détailler en titres' action in the per-row actions menu of
BalanceAccountsTable, shown only for kind==='simple' rows (replaces the disabled
'Détail / coming soon' placeholder). Past aggregated history stays frozen
read-only. The flip is one-way: the #212 service backstop rejects
detailed -> simple once holdings exist, and the UI exposes no inverse action.

Exports buildDetailToggleInput() as a pure helper for a focused unit test
(project has no jsdom harness). FR/EN i18n under balance.detailWizard.*; removed
the now-dead balance.overview.detailAction / detailComingSoon keys.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
Owner

Verdict: APPROVE — no must-fix issues.

Stacked on issue-216-drilldown-gain; base→head diff confirms the table change is surgical and kind/detailed_since/validateDetailedSnapshot/#212 guard all come from the lower stack (migration v15), not invented here.

Toggle correctness ✔

  • buildDetailToggleInput(today){ kind: 'detailed', detailed_since: localISO(today) }. localISO uses local civil accessors (getFullYear/getMonth()+1/getDate(), zero-padded) — no UTC, no off-by-one. Test new Date(2026, 5, 6, 12, 30)"2026-06-06" is constructed in local time, so the assertion is self-consistent.
  • Format matches the stored shape: normalizeSnapshotDate only validates ^\d{4}-\d{2}-\d{2}$ and returns the string verbatim — YYYY-MM-DD from localISO passes through unchanged. Round-trips cleanly into balance_accounts.detailed_since.

Pivot semantics ✔

  • detailed_since is the authoritative pivot persisted on the account. validateDetailedSnapshot returns early when holdings.length === 0 (pre-pivot aggregated lines tolerated read-only); holdings are required only at/after the pivot via the snapshot editor. Toggle-only / no-title-capture is correctly honored.

Gating ✔

  • Action rendered iff onDetailAccount && acc.kind === 'simple'. Already-detailed rows never show it → no double-toggle from the UI. Even if re-fired, updateBalanceAccount is idempotent on an already-detailed account (benign). simple → detailed is always allowed by the service (the guard only fires on detailed → simple with holdings).

detailed → simple (one-way) ✔

  • No "simplify"/revert affordance anywhere in src/ (grep clean — the only kind:'simple' hits are AccountForm creation default, category kinds, and the #212 guard's own tests). Backed by the service backstop account_kind_detailed_has_holdings (COUNT over balance_snapshot_holdings ⋈ lines). Defense-in-depth confirmed.

Refetch ✔

  • onDetailed → void reload() (BalancePage), wizard then onClose()detailTarget=null unmounts the modal. Row re-renders with kind='detailed'; the menu item self-hides. No stale state. Confirm button disabled={submitting} blocks double-submit in-flight.

Dead i18n key removal ✔

  • balance.overview.detailAction / detailComingSoon removed from both locales; full-tree content grep across 246 src files finds zero remaining refs. New balance.detailWizard.* tree is symmetric FR/EN (10 keys each, no empty values). common.close/common.cancel present both locales.

#216 collision ✔ (no break)

  • base(#216)→head(#215) diff touches only the dead placeholder <button> inside the actions-menu <div> plus prop wiring + ListTree import. The #216 restructure — Fragment key={account_id}, chevron, colSpan latent-gain drill-down <td> (lines ~456–645) — is untouched. Row structure / colSpan intact.

i18n copy ✔

  • Confirmation surfaces all three required notions in both locales: frozen history (pointFrozen), next-snapshot entry (pointNextSnapshot), irreversibility (irreversible), plus the pivot date (pointPivot). No hardcoded text — every string goes through t(). Typed-error path interpolates errors.${e.code} with defaultValue: e.message fallback (unknown code degrades gracefully, never leaks a raw key).

Tests ✔

  • 3 genuine buildDetailToggleInput unit tests, no .skip/.only/.todo/xit/fit. They cover: midday→date, zero-padding single-digit month/day, and the no-downgrade/no-envelope-mutation invariant (asserts exactly ['detailed_since','kind'] keys). Reasonable given the project has no jsdom harness (matches SecurityPicker.test.ts convention).

Non-blocking suggestion

  • Pivot display vs committed date can disagree across midnight. pivot shown in the modal is localISO(new Date()) computed at render, while the committed detailed_since is localISO(new Date()) recomputed at confirm (inside handleConfirm). If the modal stays open across a midnight boundary, the user sees day N but commits day N+1. Extreme edge case; resolving the pivot once (e.g. useState(() => localISO(new Date()))) and reusing it in both the copy and the confirm payload would make the displayed and persisted date provably identical.

Files: DetailAccountWizard.tsx (+163), DetailAccountWizard.test.ts (+35), BalanceAccountsTable.tsx (+21/-8), BalancePage.tsx (+17), fr.json/en.json (+15/-3 each). Build green / 621 tests passing per PR body.

## Adversarial review — PR #225 (Issue #215, Link 7/9) **Verdict: APPROVE** ✅ — no must-fix issues. Stacked on `issue-216-drilldown-gain`; base→head diff confirms the table change is surgical and `kind`/`detailed_since`/`validateDetailedSnapshot`/#212 guard all come from the lower stack (migration v15), not invented here. ### Toggle correctness ✔ - `buildDetailToggleInput(today)` → `{ kind: 'detailed', detailed_since: localISO(today) }`. `localISO` uses **local civil accessors** (`getFullYear`/`getMonth()+1`/`getDate()`, zero-padded) — no UTC, no off-by-one. Test `new Date(2026, 5, 6, 12, 30)` → `"2026-06-06"` is constructed in local time, so the assertion is self-consistent. - Format matches the stored shape: `normalizeSnapshotDate` only validates `^\d{4}-\d{2}-\d{2}$` and returns the string verbatim — `YYYY-MM-DD` from `localISO` passes through unchanged. Round-trips cleanly into `balance_accounts.detailed_since`. ### Pivot semantics ✔ - `detailed_since` is the authoritative pivot persisted on the account. `validateDetailedSnapshot` returns early when `holdings.length === 0` (pre-pivot aggregated lines tolerated read-only); holdings are required only at/after the pivot via the snapshot editor. Toggle-only / no-title-capture is correctly honored. ### Gating ✔ - Action rendered iff `onDetailAccount && acc.kind === 'simple'`. Already-detailed rows never show it → no double-toggle from the UI. Even if re-fired, `updateBalanceAccount` is idempotent on an already-detailed account (benign). `simple → detailed` is always allowed by the service (the guard only fires on `detailed → simple` with holdings). ### detailed → simple (one-way) ✔ - No "simplify"/revert affordance anywhere in `src/` (grep clean — the only `kind:'simple'` hits are AccountForm creation default, category kinds, and the #212 guard's own tests). Backed by the service backstop `account_kind_detailed_has_holdings` (COUNT over `balance_snapshot_holdings ⋈ lines`). Defense-in-depth confirmed. ### Refetch ✔ - `onDetailed → void reload()` (BalancePage), wizard then `onClose()` → `detailTarget=null` unmounts the modal. Row re-renders with `kind='detailed'`; the menu item self-hides. No stale state. Confirm button `disabled={submitting}` blocks double-submit in-flight. ### Dead i18n key removal ✔ - `balance.overview.detailAction` / `detailComingSoon` removed from **both** locales; full-tree content grep across 246 src files finds **zero** remaining refs. New `balance.detailWizard.*` tree is **symmetric** FR/EN (10 keys each, no empty values). `common.close`/`common.cancel` present both locales. ### #216 collision ✔ (no break) - base(#216)→head(#215) diff touches **only** the dead placeholder `<button>` inside the actions-menu `<div>` plus prop wiring + `ListTree` import. The #216 restructure — `Fragment key={account_id}`, chevron, `colSpan` latent-gain drill-down `<td>` (lines ~456–645) — is **untouched**. Row structure / colSpan intact. ### i18n copy ✔ - Confirmation surfaces all three required notions in both locales: frozen history (`pointFrozen`), next-snapshot entry (`pointNextSnapshot`), irreversibility (`irreversible`), plus the pivot date (`pointPivot`). No hardcoded text — every string goes through `t()`. Typed-error path interpolates `errors.${e.code}` with `defaultValue: e.message` fallback (unknown code degrades gracefully, never leaks a raw key). ### Tests ✔ - 3 genuine `buildDetailToggleInput` unit tests, no `.skip`/`.only`/`.todo`/`xit`/`fit`. They cover: midday→date, zero-padding single-digit month/day, and the no-downgrade/no-envelope-mutation invariant (asserts exactly `['detailed_since','kind']` keys). Reasonable given the project has no jsdom harness (matches `SecurityPicker.test.ts` convention). ### Non-blocking suggestion - **Pivot display vs committed date can disagree across midnight.** `pivot` shown in the modal is `localISO(new Date())` computed at *render*, while the committed `detailed_since` is `localISO(new Date())` recomputed at *confirm* (inside `handleConfirm`). If the modal stays open across a midnight boundary, the user sees day N but commits day N+1. Extreme edge case; resolving the pivot once (e.g. `useState(() => localISO(new Date()))`) and reusing it in both the copy and the confirm payload would make the displayed and persisted date provably identical. Files: `DetailAccountWizard.tsx` (+163), `DetailAccountWizard.test.ts` (+35), `BalanceAccountsTable.tsx` (+21/-8), `BalancePage.tsx` (+17), `fr.json`/`en.json` (+15/-3 each). Build green / 621 tests passing per PR body.
maximus changed target branch from issue-216-drilldown-gain to main 2026-06-10 01:07:57 +00:00
maximus merged commit c8a6f74a1d into main 2026-06-10 01:07:59 +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#225
No description provided.