feat(balance): multi-security entry UI + SecurityPicker (#214) #223
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#223
Loading…
Reference in a new issue
No description provided.
Delete branch "issue-214-ui-multi-titres"
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 #214
Stacked on #213 (base branch
issue-213-reducer-dispatch).What
Turns the detailed account snapshot variant into the real per-title entry surface, building on the minimal sub-rows from #213.
SecurityPicker(new): autocomplete combobox over the existingbalance_securitiescatalogue + inline creation. Accepts any normalized symbol (UPPER/TRIM), no live ticker validation (price fetch is best-effort + separate). EmitsSecurityPick {symbol, asset_type, name, isNew}; a stock/crypto toggle sets the asset class when creating a new symbol (defaultstock). Built on theCategoryComboboxUI idiom (ARIA listbox, keyboard nav, click-outside). Pure helpersfilterSecurities/decideCreateOptionexported + unit-tested (no jsdom harness).SnapshotLineRowdetailed sub-rows: labeled columns [title (SecurityPicker), quantity, price (+ existingPriceFetchControl), value (qty x price, read-only), book_cost, live unrealized gain]. Account value = displayed SUM of positions. Simple accounts unchanged.useSnapshotEditor: newSET_HOLDING_SECURITYaction +setHoldingSecuritycallback (atomically sets symbol + asset_type + name, drops stale fetched-price attribution). Securities catalogue loaded inloadForDate, exposed asstate.securities(refreshes after a save that creates a security).balance.snapshot.detailed.*(col.*,picker.*, book cost, unrealized gain) — no hardcoded UI text.[Unreleased].Quality gate
npm run build(tsc + vite): greennpm test: green — 613 tests (+10 new inSecurityPicker.test.ts)Notes for downstream (#215 / #216)
SecurityPickerAPI:securities,value,assetType,onSelect(pick: SecurityPick).SecurityPick = {symbol, asset_type, name, isNew}.asset_typedefault on inline creation =stock(matchesmakeEmptyHolding); user-switchable via the create-row toggle.Generated autonomously by /autopilot run of 2026-06-06
Adversarial review — PR #223 (Link 5/9, issue #214)
Verdict: APPROVE. No must-fix issues. The pure helpers are correct, the stale-price drop is sound end-to-end, i18n parity is exact, and the tests genuinely cover the load-bearing logic. A few non-blocking notes below.
Scrutinized hardest — all pass
SecurityPicker logic — CORRECT.
decideCreateOptionnormalizes both the query and each catalogue symbol vianormalizeSecuritySymbol, so an exact (normalized) match returnsnull— no casing-duplicate path. TypingaaplwhenAAPLexists: filter surfacesAAPLto pick, no create option offered.filterSecuritiesis case-insensitive over symbol+name, preserves catalogue order, empty→whole list. The create symbol is the normalized form, so a picker-created security collapses onto the samebalance_securitiesrow as the v14/v16 migration path (UPPER(TRIM(...))).Stale-price drop — CORRECT, verified end-to-end.
SET_HOLDING_SECURITY(useSnapshotEditor.ts:355) sets symbol+asset_type+name and nullsprice_source/price_fetched_at.buildDetailedLines(useSnapshotEditor.ts:522-523) propagates those nulls into the savedSnapshotHoldingInput, and the service insertsprice_source ?? 'manual'. A price fetched for the old ticker is therefore not mis-attributed to the new one. Good — this was the highest-risk item.asset_type on create/pick — CORRECT. Default
'stock'matchesmakeEmptyHolding. Picking an existing security emitssec.asset_type(preserved, never forced to stock). The stock/crypto toggle only governs a newly-created symbol. Note (non-blocking):findOrCreateSecurity's UPSERT doesON CONFLICT DO UPDATE SET asset_type=$4on every save, so the holding's asset_type is always written back — but since picking carries the stored value, an untouched existing security round-trips unchanged. No corruption.PriceFetchControl reuse — CORRECT.
categoryKind="priced"is right: the control early-returnsnullfor any non-pricedkind, so passingpricedis what makes the fetch button render for detailed rows (additionally gated onuseIsPremium()).i18n — COMPLETE. FR + EN have identical key sets under
balance.snapshot.detailed.*(col.*,picker.*,bookCost*,latentGain*); everyt(...)key referenced inSecurityPicker.tsx/SnapshotLineRow.tsxis defined in both, including the dynamicpicker.assetType.{stock|crypto}. No hardcoded UI text. Extends #213's namespace without duplicating.CHANGELOG — accurate, no collision. This is a linear stack (#219→…→#223→#224→…→#227). #218 (#227) sits downstream of #223 and inherits this
[Unreleased]entry by fast-forward; its own job is ADR/guide lines, not re-adding the #214 entry. No add/add conflict.Tests — 10 real cases, no
.skip/.only.filterSecurities(6) +decideCreateOption(4) cover empty/whitespace, case-insensitive symbol+name match, order preservation, no-match, normalized create, exact-match→no-create, and partial-match→create. This is the right surface to test given no jsdom harness (mirrorsCategoryCombobox.test.ts).Non-blocking notes
SnapshotLineRow(detailedTotal, line ~91) isround(Σ raw qty×price), while the persisted aggregate isround(Σ round(qty×price))(buildDetailedLines). These can differ by one cent in pathological cases (round-then-sum vs sum-then-round). It's documented intent ("account value = displayed SUM"), the service is authoritative and re-validates exactly, and the magnitude is ≤1¢ — fine to ship, just flagging the known asymmetry. The per-row read-only value (computedValue, rawqty×price.toFixed(2)) shares the same harmless rounding boundary.balance.snapshot.detailed.symbolPlaceholderis no longer referenced anywhere (the plain-text symbol input it fed was replaced bypicker.placeholder). Harmless; tidy up in a later pass.it()); just noting an earlier read miscounted due todescribelines.Clean, well-scoped link in the stack. Ship it.