Repository navigation
Redesign customer pages (Fees, Lots, Funding) and extend backend for Portfolio, Orders, Profile - #25
Merged
Badri-Rajendran merged 8 commits intoSep 7, 2026
Conversation
The dev server default (5000) collides with macOS AirPlay Receiver; every feature this session runs the backend on 5001 instead.
…st disclosure Real Stripe Elements card entry replaces the placeholder payment form. New HighWaterMarkCard and BillingPeriodProgress make the accrual mechanics legible instead of a bare number. DunningBanner states the past-due amount and next step plainly rather than a generic failure message. No progress-toward-fee estimate was added — the accrual period is disclosed, never projected ahead of the actual charge. ChargeHistory/AccrualSummary lost their own loading/error branches in favor of page-level states.
…disclosure Extend GET /lots with unrealized gain/loss, wash-sale-adjustment disclosure, and short/long-term holding-period classification (open lots only). New LotsSummaryService backs the page's totals; HoldingPeriodBadge, ConsumptionHistory (per-consumption sale detail), and LotFilterToolbar replace the previous flat, undifferentiated table. Fixes a shared Table.css bug: a wide table's content-based width forced the whole page to scroll horizontally instead of just the table.
…e page Extend GET /funding/cash-summary with outstanding_receivable, deposit_cap_per_transaction, deposit_cap_per_day, and deposited_today (FR-6's bounced-deposit disclosure and real deposit-cap visibility were previously invisible to the customer). Add GET /funding/history returning per-deposit/withdrawal rows with real settlement status (pending/confirmed/failed) -- a bounced deposit is one row with failure_reason: "ach_return", never a separate correction-entry row. New shared deposited_on() helper (services/identity/_shared.py) so the cap-enforcement path and this new disclosure path can never diverge on "deposited today". Frontend: OutstandingBalanceBanner, DepositLimits, BankLinkCard, FundingHistoryTable, SettlementStatusPill are new; CashSummary becomes presentational, fixing a double GET /cash-summary fetch per page load; form errors now reach Input's aria-invalid state; new useFunding hook replaces useCashPolicy with a refresh() path that updates balance/history in place after a submit instead of blanking back to skeletons.
GET /api/v1/portfolios/holdings: real per-security holdings -- units, market value, current vs. target weight, drift -- reusing DriftEvaluationService rather than recomputing weights. A security held but dropped from the model still appears with target_weight_pct: 0; a partial valuation evaluation is reported as completeness: "partial", never silently returned as an empty holdings list. Previously this data existed only behind the adviser/admin-only GET /admin/rebalance/<customer_id>. GET /api/v1/portfolios/performance: a live, as-of-now dated-value series sourced from sub_period_return (the only table that stores dated portfolio values), range as a strict allowlist (1m/3m/6m/1y/all), never a free-form date pair. Deliberately distinct from GET /statements' as-published series -- see ADR 26 for why the two are never merged or presented as interchangeable.
POST /api/v1/orders/<id>/cancel requests broker-side cancellation without locally mutating order status -- the authoritative canceled transition still arrives only via the existing trade-updates websocket (ADR 22), folded by OrderProjectionService. A fill racing an accepted cancel request still lands the order on filled, never canceled, with no special-casing needed since request_cancel never wrote to order.status in the first place. Adds BrokerPort.cancel_order, implemented for both AlpacaBrokerAdapter and FakeBrokerAdapter. Cancellable only from submitted/accepted/ partially_filled; idempotent while cancellable, 409 once terminal. @Audited("orders.cancel_requested"). Also: order detail now includes per-fill quantity/price on each event, and the tax lots a buy opened / a sell consumed (already FK-enforced, not previously on the wire). A generic 403 on order-submission ineligibility is replaced with a specific, customer-safe reason code via an allowlist -- the raw internal reason, and any KYC-provider-specific detail, never reaches the wire. Fixed a test bug found during review: a manual mid-test customer delete violated CustomerCashLock's FK (no ON DELETE CASCADE); replaced with a direct unit-level test of the code-mapping function, since a real HTTP request can never reach that reason -- an authenticated session already implies a live Customer row.
The platform's first stored human-identity PII: display_name, phone, mailing_address as nullable columns on customer (nullable because every pre-existing row predates them). Real identity data continues to live only in Stripe Identity's hosted KYC flow (ADR 9) -- this adds no SSN, DOB, or government ID storage. GET/PATCH /api/v1/profile are self-scoped only: the customer is resolved entirely from the session, with no customer_id parameter of any kind to check or get wrong. PATCH is @Audited("identity.profile.update") with the raw values kept out of the audit payload (a SHA-256 payload hash only). Validation errors reference field names only, never the submitted value. phone is validated E.164; display_name/mailing_address get length bounds and reject empty/whitespace-only input.
Summarizes the Fees, Tax Lots, Funding, Portfolio, Orders, and Profile work in CHANGELOG.md; indexes ADRs 25-27 in docs/decisions/README.md.
Badri-Rajendran
added a commit
that referenced
this pull request
Sep 7, 2026
…on_scope fix(ci): CI has been red since PR #25 -- test_orders.py's session-scoped table fixture
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Bundles the customer-facing page redesign programme run this session, front-to-back:
in a live browser against the real stack (real Stripe/Plaid Sandbox).
ADR) — frontend redesign for these three is tracked as follow-on work.
Problem
Several customer pages had a real, working backend but a thin or placeholder frontend (no
loading/empty/error states, stale data after a submit, accessibility gaps), and several pages were
missing backend capability entirely that the product needs: real per-security holdings (customers
could only see a model's target weights, never what they actually held), a performance
time-series, order cancellation, and any stored customer profile data beyond an email address.
What changed
Fees — real Stripe Elements card entry,
HighWaterMarkCard/BillingPeriodProgressmaking feeaccrual mechanics legible, an honest
DunningBannerfor a past-due charge.Tax Lots —
GET /lotsextended with unrealized gain/loss, wash-sale disclosure, andshort/long-term holding-period classification; new
LotsSummaryService; fixed a sharedTable.cssbug forcing the whole page to scroll horizontally on a wide table.Funding —
GET /funding/cash-summaryextended withoutstanding_receivable,deposit_cap_per_transaction,deposit_cap_per_day,deposited_today; newGET /funding/historywith real per-deposit settlement status. Frontend fixes a doubleGET /cash-summaryfetch and a form-error accessibility bug (aria-invalidnever fired).Portfolio (ADR 26) —
GET /api/v1/portfolios/holdings(real per-security holdings, reusingDriftEvaluationService) andGET /api/v1/portfolios/performance(a live, as-of-now dated-valueseries from
sub_period_return,rangeas an allowlisted enum — explicitly never merged withGET /statements's as-published series).Orders (ADR 25) —
POST /api/v1/orders/<id>/cancel: a broker cancel request, never a localstatus mutation. The order only ever transitions to
canceledvia the existing trade-updateswebsocket (ADR 22); a fill racing an accepted cancel request still lands the order on
filled.Also: per-fill quantity/price on order events, the tax lots each fill opened/consumed, and a
specific customer-safe reason code replacing a generic 403 on ineligible submission.
Profile (ADR 27) — the platform's first stored customer PII (display name, phone, mailing
address), self-scoped
GET/PATCH /api/v1/profile,@auditedon write with raw values kept outof the audit payload.
Validation evidence
uv run ruff check app tests(76 pre-existing baseline errors, confirmed identical onunmodified
origin/mainvia a worktree diff — zero new),uv run mypy --strict app(2pre-existing baseline errors, unrelated file),
uv run lint-imports(5 kept, 0 broken).Orders (48, including a fill-races-cancel regression test), Profile (25, including a self-scoping
isolation test and an audit-payload redaction test) — all passing. One real test bug found and
fixed during review (an FK-violating manual customer delete in
test_orders.py, replaced with aunit-level test of the actual unreachable-via-HTTP code path).
npm run lint,npm run buildboth clean. Fees/Lots/Funding each independentlyverified in a live Playwright pass against the real running stack (real Postgres, real
Plaid/Stripe Sandbox) — golden path, empty state, error state, 375px, dark theme.
instance.
Security / DB / API impact
Stringcolumns oncustomer(Profile). No otherschema changes; every other new endpoint reads existing tables.
GET /funding/history,GET /portfolios/holdings,GET /portfolios/performance,POST /orders/<id>/cancel,GET/PATCH /profile), allsession-scoped and rate-limited;
Orders/BrokerPortgainscancel_order, implemented for boththe real Alpaca adapter and the test double.
storage/access/redaction decisions. Order cancellation never locally mutates authoritative state
(ADR 25) — the broker-confirmed websocket event remains the only source of truth. Order
submission's generic 403 is replaced with an allowlisted, customer-safe reason code — the raw
internal reason and any KYC-provider detail never reach the wire.
Follow-on (not in this PR)