## Outcome Read-only review completed on branch `sprint12/truewealth-verified-import` at `caaf348`. The worktree remains clean; `git diff --check` passed. No files were created, modified, or committed. ## Key gaps found - Sprint documentation exists but remains source-gate oriented: - `docs/sprint12-truewealth-source-gate.md` - `docs/sprint12-truewealth-verified-import.md` - There is currently **no TrueWealth parser, router, API schema, dedicated service, or source-specific database table**. - Current TrueWealth support is manual-total-only: - UI: `frontend/src/pages/TrueWealthPage.vue` - API: `src/jarvis_finance/api/routers/positions.py:138-150` - schemas: `src/jarvis_finance/api/schemas/positions.py:250-268` - service: `src/jarvis_finance/services/manual_entry_service.py:121-167,1081-1117` - overview read model: `src/jarvis_finance/services/portfolio_service.py:13-26,47-75` - storage: `account_value_snapshots`, created in `src/jarvis_finance/storage/migrations.py:1520-1537` - Important defects in the existing manual path: - Preview IDs are random and Confirm neither verifies nor expires the supplied preview. - Deactivation exists only at account level, not valuation level, and has no Preview step. - `account_value_snapshots` has no lifecycle status, audit FK, immutability trigger, or no-delete protection. - `_latest_truewealth_value()` selects one latest value across heuristic account matches; it cannot model separate portfolios or official-versus-provisional precedence. - `TrueWealthPage.vue` chooses the first heuristic TrueWealth account rather than binding the supplied target explicitly. - Existing account recognition is label/type heuristic-based and is unsafe for canonical portfolio identity. ## Implementation map ### 1. Migration 44 Implement in: - `src/jarvis_finance/storage/migrations.py` - `src/jarvis_finance/storage/schema.py` - `tests/unit/test_schema.py` Add the four dedicated tables already contracted by the Sprint document: 1. **`truewealth_portfolios`** - `portfolio_id` PK - `account_id` FK to `accounts`, unique for this workflow - `source_reference_hash` - safe label, `portfolio_kind`, base currency - `is_active`, timestamps - Seed/bind the confirmed target `account_01017e97ddf6290b88daf548`; do not infer it from labels. - Ambiguous or conflicting mapping blocks Confirm. 2. **`truewealth_import_batches`** - batch ID, unique full SHA-256 - parser ID/version, source class/file type - sanitized filename digest, archive-safe reference - source date, portfolio count - Preview/confirmation identity fields matching the hardened ingestion pattern: `preview_id`, `confirmation_id UNIQUE`, `payload_hash`, `input_fingerprint` - `audit_id`, `confirmed_at`, `status CHECK(status='confirmed')` - update/delete immutability triggers. 3. **`truewealth_snapshots`** - immutable portfolio/batch FKs - authoritative `snapshot_date` - source total and currency - evidenced position sum and currency - separately reported cash/other value - reconciliation difference, tolerance/version/status - position count, completeness status and reason-code JSON - unique `(portfolio_id, batch_id)`; immutable/no-delete triggers. 4. **`truewealth_snapshot_positions`** - immutable parent FK - safe source-row reference - evidenced name/ISIN/ticker/type, quantity, price/currencies/value - evidence flags and nullable unknowns - unique row identity within snapshot; immutable/no-delete triggers. Extend **`account_value_snapshots`** for provisional lifecycle rather than inventing positions: - `valuation_kind` with legacy rows backfilled as `manual_provisional` - `is_active DEFAULT 1` - `confirmed_audit_id` - `deactivated_at`, `deactivation_audit_id`, optional `reactivated_at/reactivation_audit_id` - Protect value/date/account/source fields with a trigger; permit lifecycle-field changes only. - Add a no-delete trigger. - Index active manual values by `(account_id, is_active, valuation_date DESC, created_at DESC)`. The existing historic manual snapshot remains untouched and queryable. The official import does not update or delete it. ### 2. Dedicated parser and service Add: - `src/jarvis_finance/imports/truewealth_tax_statement.py` - `src/jarvis_finance/services/truewealth_service.py` - `src/jarvis_finance/api/schemas/truewealth.py` - `src/jarvis_finance/api/routers/truewealth.py` - register the router in `src/jarvis_finance/api/main.py` Reuse patterns from: - deterministic parser and bounded-file handling: `src/jarvis_finance/imports/postfinance_etrading.py` - hash-bound, expiring Preview and atomic Confirm: `src/jarvis_finance/services/portfolio_data.py:919-1055,1226-1361` - audit writer: `jarvis_finance.audit.log.record_audit_event` - strict Pydantic contracts: `src/jarvis_finance/api/schemas/portfolio_data.py` Do **not** route this document through generic PostFinance transaction/position ingestion. TrueWealth remains a source snapshot, not a reconstructed ledger. ### 3. Official PDF parser approach Safe structural inspection found: - seven-page PDF with an extractable text layer; - seven unique ISINs; - the expected valuation date is present; - many other date tokens occur; - security sections span multiple pages; - the supplied portfolio label is not reliably present as an exact extracted string. Therefore: - Use a pinned parser such as `truewealth_etax_pdf_v1`, preferably with a pure-Python PDF library added explicitly to dependencies. - Enforce size/page bounds, PDF signature, no encryption, maximum extracted text/word counts, and no OCR fallback. - Compute SHA-256 from original bytes before parsing. - Parse by **anchored document sections and coordinates**, not by unrestricted line regex: - identify the exact statement/document class; - extract the authoritative valuation date only from its labeled valuation-date field; - segment security blocks by validated ISIN; - extract only explicitly labeled closing quantity/value fields for that valuation date; - extract currency cash only from the explicit cash/summary section; - ignore transaction, income, tax and historical dated rows. - Swiss number parsing must deterministically handle apostrophe/space thousands separators, decimal comma/point, Unicode minus and null values. - Require exactly the evidenced securities and explicit currency cash from the supplied statement, while keeping the fixture anonymized. - Reconcile positions/cash against the permitted aggregate source total CHF 152845; do not create balancing rows. - Because source portfolio identity is not reliably parser-extractable, bind the parsed statement to the already confirmed target account through explicit Preview/Confirm mapping. Never infer from the filename or account-label similarity. - Archive/re-read outside Git at Confirm, verify the full digest, and reparse with the same parser version. ### 4. Precedence and double-count rules Implement one shared resolver in `truewealth_service.py`; use it from `/summary`, `/overview`, portfolio totals and the TrueWealth page. For each portfolio: 1. Find latest confirmed official snapshot by `(snapshot_date, confirmed_at)`. 2. Find latest active manual provisional valuation by `(valuation_date, created_at)`. 3. Choose: - official if no active manual exists; - manual if `manual_date > official_date`; - official if `official_date >= manual_date`; - manual if no official exists. 4. Portfolio contribution is exactly **one value**: - official source total when official wins; - otherwise official evidenced-position sum only if source total is absent and safely summable; - manual total when manual wins. 5. Never add: - official total plus official position sum; - manual total plus official total; - manual total plus stale official positions. 6. When manual wins: - stale official positions remain immutable and visible only as informational detail; - they contribute zero additional value; - return explicit mixed-as-of metadata: total as-of manual date, holdings as-of official date. 7. A later official snapshot with the same or newer date supersedes the manual in the current read model without mutating either record. 8. An older official snapshot remains history and cannot displace a newer active manual. 9. Deactivating the winning manual falls back to the next eligible manual or official snapshot; no deletion. This makes the existing historic manual value non-current immediately after the official import while preserving history. ### 5. API contract Recommended routes under `/api/truewealth`: **Reads** - `GET /summary` - `GET /portfolios` - `GET /snapshots?portfolio_id=&limit=&offset=` - `GET /snapshots/{snapshot_id}` - `GET /manual-valuations?portfolio_id=&limit=&offset=` **Official import** - `POST /imports/preview` - `POST /imports/confirm` **Manual provisional value** - `POST /manual-valuations/preview` - `POST /manual-valuations/confirm` - `POST /manual-valuations/{id}/deactivate/preview` - `POST /manual-valuations/{id}/deactivate/confirm` - To make deactivation genuinely reversible: `POST /manual-valuations/{id}/reactivate/preview|confirm` Preview responses should include chosen target portfolio, valuation date, value source, current winner, expected winner after Confirm, mixed-as-of consequence, reconciliation, digest prefix, expiry and warnings. Confirm must require the exact hash-bound, unexpired Preview and run valuation/batch/snapshot/positions/audit in one `BEGIN IMMEDIATE` transaction. Add all side-effect-free Preview paths to `READ_ONLY_POST_PATHS` in `src/jarvis_finance/api/main.py`; Confirm/deactivate/reactivate remain subject to `api/security.py`. ### 6. UI reuse Keep route: - `frontend/src/router/index.ts:41` — `/portfolio/truewealth` Refactor: - `frontend/src/pages/TrueWealthPage.vue` Add: - `frontend/src/api/truewealth.ts` - TrueWealth interfaces in `frontend/src/api/types.ts` or a dedicated type module. Reuse: - `LoadActionBar`, `LocalDataStatus`, `MoneyValue` - `frontend/src/components/ui/SectionCard.vue` - `KpiCard.vue`, `StatusBadge.vue`, `PrimeDataTable.vue` - `ConfirmDialog.vue` - responsive and reconciliation patterns from `frontend/src/components/portfolio-data/DataIngestionReconciliationPanel.vue` Required UI states: - verified current total/source/as-of; - provisional-current warning and mixed-as-of labels; - official holdings as-of separately from provisional total as-of; - reconciliation without summing both totals; - immutable official/manual history; - upload → Preview → Confirm; - provisional Preview → Confirm; - deactivate/reactivate confirmation and audit receipt; - no provider calls during normal rendering and no mobile horizontal overflow. ## Focused test matrix Create primarily: - `tests/unit/test_truewealth_tax_statement_parser.py` - `tests/unit/test_truewealth_verified_import.py` - `tests/unit/test_truewealth_precedence.py` - focused frontend acceptance additions following `test_vue_dashboard_v12_acceptance.py` Cover: 1. Exact anonymized PDF structure, seven evidenced securities plus explicit cash, Swiss decimals and multi-page blocks. 2. Wrong/encrypted/oversized/truncated PDF, missing/duplicate ISIN, ambiguous valuation date, missing total/cash, unsupported row. 3. Parser ignores historical transaction/tax rows and never reconstructs transactions, cost basis or look-through. 4. Preview causes zero DB/audit changes. 5. Confirm re-reads bytes, verifies digest/parser version, is atomic and audited. 6. Identical digest is idempotent; same filename with changed bytes is distinct. 7. Rollback leaves no portfolio/batch/snapshot/position/audit partials. 8. Source total versus position sum reconciliation; no balancing row and no total-plus-position double count. 9. Existing historic manual → official current. 10. Newer manual → manual current with official positions informational and mixed-as-of. 11. Same/newer official → official current. 12. Older official cannot displace newer manual. 13. Manual deactivate fallback, reactivation restoration, no delete. 14. Separate portfolio/account mapping conflicts block Confirm. 15. GET and Preview routes remain provider-free and side-effect-free. 16. OpenAPI/write-mode checks patterned after `test_portfolio_data_ingestion_reconciliation.py:324-350`. 17. PostFinance sentinel counts/data unchanged. 18. UI states, digest/raw-document privacy, audit receipt, laptop/iPad/mobile overflow. ## Issues encountered - `source-gate.md` named in the context does not exist at repository root; the actual file is `docs/sprint12-truewealth-source-gate.md`. - The installed Python environment has no PDF parsing package, though `pdftotext` is available. Production implementation must explicitly add and pin a parser dependency rather than relying silently on a host binary. - No raw document text, holdings, additional identifiers, or non-approved amounts were reported.