feat(balance): per-security drill-down + latent gain (#216) #224
No reviewers
Labels
No labels
autopilot:pending-human
source:analyste
source:defenseur
source:human
source:medic
status:approved
status:blocked
status:in-progress
status:needs-clarification
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#224
Loading…
Reference in a new issue
No description provided.
Delete branch "issue-216-drilldown-gain"
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?
Resolves #216
Stacked on #223 (base
issue-214-ui-multi-titres).What
Surfaces unrealized (latent) gain on the EXISTING balance surfaces — no new visualization (decision 2026-06-04).
AccountLatestSnapshotgains accountkind+latest_snapshot_line_id. NewgetAccountLatentGainByLine(lineId)folds a line's holdings through the existingcomputeUnrealizedGain(reuses the book_cost=0 / NULL → N/A guard). New purerollupLatentGain(accounts)aggregates by asset class, by envelope (vehicle_type,nonebucket), and a grand total.useBalanceOverview): prefetches per-detailed-account latent gain in parallel (failures isolated), exposeslatentGainByAccount+latentGainRollup.BalanceAccountsTable: expandable detailed rows → per-security value + latent gain %; a latent-gain column (shown only when a detailed account has one); a summary block aggregating latent gain by asset class / envelope.BalanceOverviewCard: total latent gain line (hidden when no detailed accounts).Guards
N/A rendered (never divide-by-zero) when
book_costis NULL or 0; partial-% flag when some positions lack a cost basis.Scope
Native non-CAD currency display de-scoped this round (untestable while all securities are CAD). Modified Dietz return columns (#204) unchanged.
Tests
Focused unit tests for
getAccountLatentGainByLineandrollupLatentGain(grouping,noneenvelope, null-% / unknown book_cost, empty).npm run build+npm testgreen (618 passing).Note for #215
This PR also edits
BalanceAccountsTable. It adds: a leading chevron column-cell idiom in the name cell (drill-down toggle, gated ondrillable), a conditional latent-gain<th>/<td>, sub-rows after each main<tr>(rows now wrapped in aFragment), and acolSpancomputed as5 + (hasLatentGain?1:0) + (showReturns?4:0). The per-row actions menu (where #215 adds “Détailler en titres”) is untouched and still the last<td>.Generated autonomously by /autopilot run of 2026-06-06
Adversarial review — PR #224 (Link 6/9, issue #216)
Verdict: APPROVE ✅ — no must-fix. Reviewed the full diff (729+/7-, 8 files) and the head-branch sources for the dependencies (
computeUnrealizedGain,listHoldingsBySnapshotLine, migration v15,computeAccountReturn).Correctness — verified sound
rollupLatentGain: each bucket's%denominator isSUM(known book_cost)only;total_book_cost === 0 → total_gain_pct = null(N/A) at both bucket and grand-total level, faithfully mirroringcomputeUnrealizedGain's aggregate guard. No double-counting within a bucket —fold()accumulates each account once per map.has_unknown_book_costis OR-ed up. NULLvehicle_type → VEHICLE_NONE_BUCKET('none'). The 4 rollup tests' expected math (stock 0/200=0, crypto 10/40, tfsa 30/140, grand 10/240, null-% bucket) all check out against the implementation.kind === 'detailed' && latest_snapshot_line_id != null) then fetched viaPromise.all, each wrapped intry/catch → null. A single account's fetch failure is isolated — the overview still loads, that account just has no figure. Parallelism is safe (independent reads).lg.holdings— no extra round-trip. Chevron gated ondrillable = !!lg && lg.holdings.length > 0(simple/empty rows never expand).colSpan = 5 + (hasLatentGain?1:0) + (showReturns?4:0)is exact — confirmed against the 5 unconditional columns (name, category, latestValue, periodDelta, actions). Row key correctly moved to the wrapping<Fragment>.computeAccountReturnand the 4 return<th>/<td>appear only as unchanged context — no+/-on the return path.latest_snapshot_line_idscalar subquery is identical in shape (WHERE l.account_id=a.id ORDER BY s.snapshot_date DESC LIMIT 1) to the existinglatest_value/latest_snapshot_datesubqueries → picks the same winning row, mutually consistent.a.kind AS kindis valid (migration v15add kind ... CHECK (kind IN ('simple','detailed'))exists at this branch, inherited from the #223 base, not introduced here). Correlated-per-row, but matches the pre-existing pattern and the indexed columns it documents.latentGainByAccountentry even when the gain is 0 or all-unknown (renders+$0.00with the partial ⚠), so a user with securities never sees an empty surface.hasLatentGainanddrillableare derived from the same entry, so they can't disagree.LatentGainSummaryblock below the flat table (vs grouped sections) DOES satisfy “aggregated latent gain by asset class / envelope” — it aggregates on both axes viabyClass/byVehicle. The stated rationale (avoid collision with #215's grouped-sections rework) is sound. Not a gap..skip/.only/fit/xit; mocks are real (mockSelectfeedslistHoldingsBySnapshotLine, symbol carried through,has_unknown_book_costasserted). Genuine.Minor (non-blocking) — optional follow-ups
computeUnrealizedGain([])all-zeros entry, which flipshasLatentGaintrue and emits a$0.00bucket in byClass/byVehicle + a$0.00grand total. Purely cosmetic and an unusual state; could be tidied by skippingholdings.length === 0accounts from the rollup input.gain === 0renders as green+$0.00(treated as positive) consistently across the cell, summary, and overview total — cosmetic.Clean, well-scoped, reuses existing surfaces and the shared book_cost guard as intended. Shipping-ready on top of #223.