## Review outcome ### 1. Migros duplicate receipts **Defect confirmed in baseline:** receipt linking iterated over all Migros rows and could overwrite an earlier `duplicate_file` / `duplicate_source_row` disposition with `receipt_detail` or `review`. **Minimal fix** - In `src/jarvis_finance/services/household_import.py`, build `receipt_rows` only from non-duplicate rows: - exclude `duplicate_file` - exclude `duplicate_source_row` - exclude `duplicate_logical` A concurrent uncommitted edit already applies exactly this filter around lines 1390–1396. **Regression test** - Import a receipt, then preview the identical receipt again. - Assert: - disposition remains `duplicate_file` - duplicate count is 1 - no candidate, line-item, import-item, or Migros-link write occurs **Important test correction:** the concurrently added test also expects no second `household_import_batches` row, but current preview explicitly reports `import_batches: 1`. That requirement is broader than “linking must not overwrite duplicate disposition.” Either: - allow an audit-only batch and assert only economic/detail tables remain unchanged; or - deliberately change the contract to `import_batches: 0` and implement an all-duplicate confirm no-op. Do not leave the contradictory expectation (`expected_writes.import_batches == 1`, but batch count unchanged). --- ### 2. VISA `TransactionId` identity and legacy compatibility **Current implementation is already structurally correct** in `_normal_row()`: - With `TransactionId`, source identity is: `profile + account-reference hash + TransactionId` - Without `TransactionId`, it falls back to normalized legacy row content excluding mutable pending/status fields. - Card/account reference remains part of identity, so identical IDs on different cards do not collide. - Pending and booked versions with the same ID intentionally share identity. **Minimal change:** tests only unless conflicting payloads for one provider ID must fail closed. **Required tests** 1. Import a legacy generic VISA row, then preview a modern liability-format row with the same `TransactionId`, `CardId`, and economic transaction but additional `OriginalAmount`/`StateType` fields. Expect `duplicate_source_row`, not `possible_logical_duplicate`. 2. Same `TransactionId` on two different `CardId` values must not dedupe. 3. Blank `TransactionId` must retain content-based legacy identity. 4. Recommended safety test: same account and `TransactionId`, but conflicting date/amount/currency should be blocked or reviewed rather than silently suppressed. Current code treats it as a source duplicate because only the provider identity is compared. --- ### 3. Raiffeisen continuation rows **Implementation already works:** `parse_raiffeisen_physical_records()` right-aligns amount/balance/value-date fields and folds a blank-account continuation row into the preceding description before identity calculation. Runtime probe result: - 2 physical rows - 1 logical row - unquoted comma text and continuation were combined correctly - amount, balance, and value date remained correct **Minimal change:** tests only. **Required tests** 1. CSV with `Balance`, an unquoted comma inside `Text`, and a following blank-account continuation row: - `physical_row_count == 2` - `logical_row_count == 1` - combined description is exact - amount/balance/value date are unchanged 2. Household preview integration: - one row/candidate only - fingerprint and classification use the folded description 3. Leading orphan continuation row must not create a logical transaction. 4. Cover the legacy export without a `Balance` column. --- ### 4. TrueWealth grouped total, tax groups, and activities **Defects confirmed:** - `parse_truewealth_text()` treats every matching ISIN/position row across the document as a holding. - A security repeated in a tax-group section causes `TrueWealth positions are missing or duplicated`. - The source-total regex takes the first numeric-looking value within a broad text window; it is not anchored to an exact aggregate/group-total row. - The model has no activities collection, and `truewealth_service.py` persists only positions and cash. **Minimal parser fix** 1. Split parsing into explicit source sections: - portfolio holdings/cash section - tax-group/activity sections (`A`, `B`, `DA-1`, `USA`, etc.) 2. Parse holdings only from the portfolio section. Repeated ISINs in tax sections must never become additional positions. 3. Parse the provider total from the exact aggregate total row, or parse named group totals and reconcile their sum to that exact total. Never add both group totals and their component activity rows. 4. Add immutable `TrueWealthActivity` records with stable source references, minimally: - occurred date - ISIN/instrument when evidenced - activity type - gross amount - tax/withholding amount - net amount - currency 5. Bump `PARSER_VERSION`. **Persistence impact** - Activities require an append-only TrueWealth activity table or a carefully source-qualified projection into the generic transaction ledger. - Include activities in preview identity, audit counts, confirm writes, idempotency, and immutability controls. - They must not be added to snapshot value totals. **Core regression fixture** - Two holdings and cash. - The same ISIN repeated under at least two tax groups. - Several dividend/withholding activities. - Group subtotals plus one aggregate total. Assert: - each holding appears once - exact aggregate source total is selected - `positions + cash` reconciliation is exact/toleranced as intended - activity count and values are exact - tax-group subtotals and activities are not added to portfolio value - repeated parsing produces stable activity identities --- ### 5. PostFinance monthly account statements **Defect confirmed:** `_classify()` recognizes `account_statement`, but `_event_from_document()` returns `None`; therefore a monthly statement containing multiple bookings produces zero events. **Minimal fix** 1. Add `_account_statement_events(doc) -> tuple[PFEvent, ...]`, analogous to `_interest_events()`. 2. Parse physical rows into logical bookings: - booking date - value date - multiline description - debit or credit - currency - amount 3. Emit one event per logical booking, not one event per document. 4. Extend the event-building loop: - `_interest_events(doc)` - `_account_statement_events(doc)` - `_event_from_document(doc)` for single-event documents 5. Use row-level stable fingerprints including statement semantic identity and normalized row identity. **Double-count requirement** Account-statement rows may duplicate detailed trade/dividend/fee confirmations. Reconcile exact one-to-one business matches and prefer the detailed confirmation event. Do not blindly persist both. Ambiguous same-day/same-amount matches must remain explicit review/partial evidence rather than being silently collapsed. If statement-only generic cash bookings must be persisted, the current `postfinance_ledger_events.event_type` CHECK and `_project_event()` mapping need an explicit supported type such as `cash_booking`; do not mislabel unknown bookings as trades, dividends, or corporate actions. **Required tests** - One synthetic monthly account statement with at least three bookings and one multiline continuation. - Assert three events with correct dates, signs, amounts, and unique fingerprints. - Exact statement/confirmation overlap yields one economic event. - Two legitimate equal-amount bookings remain two events. - Malformed debit+credit or missing date/currency fails closed. - Reordered pages/continuation lines remain deterministic where source identity is unchanged. ## Verification performed - Synthetic Raiffeisen probe passed: continuation folding produced one correct logical row. - Synthetic TrueWealth repeated-tax-group probe reproduced the duplicate-position rejection. - Synthetic PostFinance account statement returned `None`, confirming missing multi-event parsing. - Targeted tests: five passed; the concurrent Migros replay test failed only because it expects no additional batch despite declaring one expected batch write. ## Workspace state - **Files modified by me:** none. - **Files created by me:** none. - Concurrent uncommitted changes were present in: - `src/jarvis_finance/services/household_import.py` - `tests/unit/test_household_import_v1_golden.py` - The repository-local `.venv` was absent, so verification used system `python3` with `PYTHONPATH=src`.