## Sprint 20C backend implementation map **Grounding:** inspected worktree `/home/agent/.hermes/worktrees/FinanceManager-sprint20c` at exact base `0e2cd9f7291e941050a21e52b0c56d4f59ea6b7a`. Schema is currently **49**. ### 1. PostFinance component valuations **Current gap** - `postfinance_service.confirm_postfinance_import()` persists the authoritative components in: - `postfinance_snapshots.securities_chf` - `postfinance_snapshots.cash_chf` - `postfinance_snapshot_positions` - `postfinance_snapshot_cash` - But its legacy `account_value_snapshots` projection writes `total_chf` against the depot account (`postfinance_service.py:838-849`). - Daily valuation currently gets generic cash only from `cash_account_snapshots` via `portfolio_analytics._latest_cash_entries()`. The official import path writes `cash_balances`, not `cash_account_snapshots`. - Therefore canonical performance needs explicit component account values: - depot account = securities component - E-Trading cash account = cash component - never add the PostFinance control total as a third performance value. **Minimal implementation** - `src/jarvis_finance/services/portfolio_analytics.py` - Promote `_store_valuation_snapshot()` to a reusable internal/public helper, or add `store_account_valuation_snapshot()`. - Extend `_latest_cash_entries()` to use `latest_official_postfinance_cash()` for the confirmed `etrading_cash` role, replacing—not adding to—generic cash rows for that account. - Keep `run_daily_market_valuation()` writing separate depot and settlement-cash account snapshots. - `src/jarvis_finance/services/postfinance_service.py` - In `confirm_postfinance_import()`, append two `portfolio_valuation_snapshots` at `snapshot.valuation_at`: - `etrading_depot → securities_total_chf` - `etrading_cash → cash_total_chf` - Preserve the existing `account_value_snapshots` total as a reconciliation control; the canonical rows take precedence in `portfolio_performance._load_valuations()`. - Include component snapshot IDs/counts in the existing import audit. - Existing confirmed PostFinance snapshots will not be repaired merely by changing future imports. Add a bounded, fingerprinted component-projection Preview→Confirm operation—preferably in: - `performance_activation.preview_postfinance_component_projection()` - `performance_activation.confirm_postfinance_component_projection()` - It should project only immutable `postfinance_snapshots`, append versions, reject changed inputs, and audit confirmation. ### 2. PostFinance cashflow classification **Current defect** `postfinance_service._canonical_transaction()` writes: - buy/sell as `activity_kind="trade"` - dividend/interest/fee as `activity_kind="external_cashflow"` But `portfolio_performance.ACTIVITY_MAP` recognizes neither `trade` nor `external_cashflow`. Consequently these confirmed immutable transactions become unsupported and can trigger `cashflow_classification_incomplete`. Dividends, interest and fees must be **performance/null flows**, not investor external cashflows. **Minimal implementation** - `src/jarvis_finance/services/postfinance_service.py` - Change `_canonical_transaction()` to persist exact kinds: - buy → `buy` - sell → `sell` - dividend → `dividend` - interest → `interest` - fee → `fee` - transfer → `internal_transfer` - FX conversion → supported non-external `fx` - `src/jarvis_finance/services/portfolio_performance.py` - Extend `ACTIVITY_MAP` for `fx` as a supported non-external activity. - In `_activity_from_row()`, add a compatibility adapter for immutable `source_type='postfinance_official_import'`: derive semantics from `transaction_type` when legacy `activity_kind` is `trade` or `external_cashflow`. - In `_load_activities()`, preserve scope-boundary behavior: - transfer wholly inside selected scope → nullflow/internal - E-Finance ↔ E-Trading where only E-Trading is selected → external deposit/withdrawal - For legacy PostFinance transfer rows, derive direction from immutable `postfinance_ledger_events.direction`; existing canonical transaction amounts are not reliably signed and cannot be updated because official rows are trigger-protected. ### 3. True Wealth external-flow/nullflow confirmation **Current gap** - `truewealth_service.py` supports official/manual valuations but no flow capture. - `performance_cashflow_coverage` can attest a complete period, and `transactions` already supports `external_deposit`/`external_withdrawal`. - Sprint 20B activation can mark coverage complete without storing actual True Wealth flows or an explicit no-flow decision. **Minimal Preview→Confirm→Audit contract** - `src/jarvis_finance/services/truewealth_service.py` - Add: - `preview_truewealth_cashflow_classification()` - `confirm_truewealth_cashflow_classification()` - Request contract: - period start/end - evidence reference/note - mode: `external_flows` or `no_external_flows` - flow rows for external mode: date, deposit/withdrawal, positive amount, currency, stored FX where non-CHF - Preview must be side-effect free and return: - account binding - normalized flow totals/counts - valuation-boundary blockers - input revision/fingerprint - expected transaction/coverage/audit writes - Confirm must: - re-preview and reject drift - use `BEGIN IMMEDIATE` - append deterministic `transactions` rows for real flows - write **no zero-value transaction** for nullflow - record complete `performance_cashflow_coverage` for the evidenced period - append one confirmation audit containing mode, period, counts, fingerprint and transaction IDs - be idempotent by confirmation ID/request binding. - `src/jarvis_finance/api/schemas/truewealth.py` - Add strict Pydantic flow row, preview and confirm request models (`extra="forbid"`, `Literal[True]`, bounded note/reference). - `src/jarvis_finance/api/routers/truewealth.py` - Add: - `POST /truewealth/cashflows/preview` - `POST /truewealth/cashflows/confirm` - `src/jarvis_finance/api/main.py` - Add only `/api/truewealth/cashflows/preview` to `READ_ONLY_POST_PATHS`; confirm remains protected by write-mode security. - `src/jarvis_finance/services/performance_activation.py` - Tighten True Wealth activation so `managed_total_value` requires an already confirmed flow/nullflow contract covering the requested period, rather than the boolean attestation alone. ### Migration decision **No migration required; keep `MIGRATION_VERSION == 49`.** Existing tables contain all required fields: - `portfolio_valuation_snapshots`: component account values, versioning and supersession - `transactions`: activity kind, amount, currency, FX, source lineage and confirmation state - `performance_cashflow_coverage`: evidenced period coverage - `audit_log`: confirmation identity and request-bound audit payload Do not add a special “nullflow transaction”; represent nullflow through audited coverage evidence. ### Key synthetic tests Prefer a focused new `tests/unit/test_sprint20c_component_valuations_cashflows.py`, plus narrow regressions in existing tests: 1. Official PostFinance confirmation writes exactly two canonical account component valuations; their sum equals `total_chf`. 2. Depot never receives an additive canonical total valuation. 3. Daily valuation uses official settlement cash with an `as_of` cutoff and does not double-count stale generic cash. 4. Component projection is read-only on preview, stale-safe, atomic, idempotent and append-only. 5. Legacy PostFinance buy/sell/dividend/interest/fee rows no longer produce `cashflow_classification_incomplete`. 6. Dividend/interest/fee and FX are not external cashflows. 7. Fully internal transfers remain nullflow; a transfer crossing selected-scope boundary becomes the correctly signed external flow. 8. True Wealth flow preview performs zero writes; confirm appends transactions, coverage and one audit. 9. True Wealth nullflow confirm appends coverage/audit but zero transactions. 10. Reject negative/zero/non-finite amounts, missing non-CHF FX, account mismatch, input drift and confirmation-ID reuse. 11. A True Wealth external flow without a valuation on its TTWROR boundary remains explicitly blocked/unavailable. 12. API write-mode and OpenAPI contracts cover the new endpoints. 13. Assert `MIGRATION_VERSION == 49`. ### Blockers / cautions - No Sprint 20C specification exists in the repository; the map is derived from the requested scope and current Sprint 20/20B contracts. - There are two PostFinance ingestion lineages (`postfinance_service.py` official bundle path and the older `portfolio_data.py` ingestion path). Sprint 20C should target the routed official bundle service and avoid creating a third projection path. - Existing official PostFinance transaction rows are immutable; historical semantic repair must occur in the performance read adapter, not by updating them. - True Wealth performance cannot become complete from flow confirmation alone: TTWROR still requires confirmed valuations on every external-flow boundary. - No True Wealth flow-file parser currently exists. Unless a source format is separately specified, Sprint 20C should accept normalized, explicitly confirmed entries/nullflow evidence only. **Files modified:** none. Read-only analysis only; worktree remained clean and `git diff --check` passed.