Output:
Chunk ID: 692333
Wall time: 0.0000 seconds
Process exited with code 1
Original token count: 249
Output:
   235	    *,
   236	    action: str,
   237	    entity_type: str,
   238	    entity_id: str,
   239	    details: dict[str, Any],
   240	    note: str = "",
   241	    quality: str = "ok",
   242	) -> str:
   243	    audit_id = _id("audit")
   244	    now = _now()
   245	    conn.execute(
   246	        """INSERT INTO audit_log(
   247	             audit_id,timestamp,source,action,entity_type,entity_id,old_values_json,new_values_json,
   248	             user_text_note,confirmed,confirmation_timestamp,auto_parsed,parse_confidence,created_by,
   249	             quality_status,created_at)
   250	           VALUES(?,?,'truewealth_sprint12',?,?,?,NULL,?,?,1,?,0,NULL,'user',?,?)""",
   251	        (audit_id, now, action, entity_type, entity_id, json.dumps(details, sort_keys=True), note, now, quality, now),
   252	    )
   253	    return audit_id
   254
   255
[Errno 30] Read-only file system: 'src/jarvis_finance/services/__pycache__/truewealth_service.cpython-311.pyc.138998938632240'


nl -ba src/jarvis_finance/services/truewealth_service.py | sed -n '256,270p'


Output:
Chunk ID: 23c729
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 282
Output:
   256	def confirm_truewealth_import(conn: Connection, request: dict[str, Any]) -> dict[str, Any]:
   257	    if request.get("confirm") is not True:
   258	        raise ValueError("Explicit TrueWealth import confirmation is required")
   259	    preview = preview_truewealth_import(conn, request)
   260	    if request.get("preview_id") != preview["preview_id"] or request.get("confirmation_id") != preview["confirmation_id"]:
   261	        raise ValueError("TrueWealth import preview is stale or changed")
   262	    if preview["duplicate"]:
   263	        row = conn.execute(
   264	            "SELECT batch_id,audit_id FROM truewealth_import_batches WHERE file_sha256=?",
   265	            (preview["file_sha256"],),
   266	        ).fetchone()
   267	        return {"status": "confirmed", "batch_id": row["batch_id"], "audit_id": row["audit_id"], "idempotent": True}
   268	    if preview["conflict"]:
   269	        raise ValueError("A different official TrueWealth source already exists for this snapshot date")
   270	    account_id = _validate_account(conn, str(request.get("account_id") or "").strip() or None)


git diff -- src/jarvis_finance/storage/migrations.py src/jarvis_finance/api/main.py src/jarvis_finance/services/portfolio_data.py src/jarvis_finance/services/portfolio_performance.py src/jarvis_finance/services/portfolio_service.py src/jarvis_finance/services/reconciliation_snapshot.py scripts/ci_portfolio_phase3_gate.py pyproject.toml requirements-ci.txt


Output:
Chunk ID: cfd27c
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 4753
Output:
diff --git a/pyproject.toml b/pyproject.toml
index ea33360..a3f1f33 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,7 +7,7 @@ name = "jarvis-finance"
 version = "0.1.0"
 description = "JARVIS Finance System MVP - code, docs and synthetic fixtures only"
 requires-python = ">=3.11"
-dependencies = []
+dependencies = ["pypdf==6.14.2"]

 [project.optional-dependencies]
 dev = ["pytest>=8", "ruff>=0.5"]
diff --git a/requirements-ci.txt b/requirements-ci.txt
index 6f593ae..375d951 100644
--- a/requirements-ci.txt
+++ b/requirements-ci.txt
@@ -5,6 +5,7 @@ openpyxl==3.1.5
 pandas==3.0.3
 plotly==6.7.0
 pydantic==2.13.4
+pypdf==6.14.2
 pytest==9.0.2
 ruff==0.15.13
 streamlit==1.57.0
diff --git a/scripts/ci_portfolio_phase3_gate.py b/scripts/ci_portfolio_phase3_gate.py
index 22ab947..b91f367 100644
--- a/scripts/ci_portfolio_phase3_gate.py
+++ b/scripts/ci_portfolio_phase3_gate.py
@@ -15,7 +15,7 @@ from jarvis_finance.config.settings import load_settings
 from jarvis_finance.storage.migrations import apply_migrations, get_schema_version

 SPRINT5_COMMIT = "480d7a1b72a950e864c4bb021d3f58af8f8c15f9"
-EXPECTED_SCHEMA = 43
+EXPECTED_SCHEMA = 44

 def _assert_tmp_path(path: Path) -> Path:
@@ -159,8 +159,8 @@ def main() -> None:
     sprint5.close()

     print("phase3_migration_gate=PASS")
-    print("empty_schema=43 integrity=ok ingestion_and_analysis_rows=0")
-    print(f"sprint5_schema=40_to_43 integrity=ok seed_digest={digest_before}")
+    print(f"empty_schema={EXPECTED_SCHEMA} integrity=ok ingestion_and_analysis_rows=0")
+    print(f"sprint5_schema=40_to_{EXPECTED_SCHEMA} integrity=ok seed_digest={digest_before}")
     print("settings_guard=PASS production_paths_rejected=true")

diff --git a/src/jarvis_finance/api/main.py b/src/jarvis_finance/api/main.py
index 069007f..5336423 100644
--- a/src/jarvis_finance/api/main.py
+++ b/src/jarvis_finance/api/main.py
@@ -7,7 +7,7 @@ from fastapi import FastAPI, Request
 from fastapi.middleware.cors import CORSMiddleware
 from fastapi.responses import JSONResponse

-from jarvis_finance.api.routers import budget, cash, crypto, crypto_trader, equity, health, market, overview, positions, reports, system
+from jarvis_finance.api.routers import budget, cash, crypto, crypto_trader, equity, health, market, overview, positions, reports, system, truewealth
 from jarvis_finance.api.security import WRITE_METHODS, is_write_request_allowed, resolve_write_mode

 LOCAL_ORIGINS = [
@@ -18,7 +18,12 @@ LOCAL_ORIGINS = [
     "http://localhost:8503",
     "http://127.0.0.1:8503",
 ]
-READ_ONLY_POST_PATHS = {"/api/portfolio/ingestion/preview", "/api/portfolio/policy/preview"}
+READ_ONLY_POST_PATHS = {
+    "/api/portfolio/ingestion/preview",
+    "/api/portfolio/policy/preview",
+    "/api/truewealth/imports/preview",
+    "/api/truewealth/manual-values/preview",
+}

 def _is_safe_local_origin(origin: str) -> bool:
@@ -68,7 +73,7 @@ def create_app(*, write_mode: str | None = None) -> FastAPI:
         return await call_next(request)

     app.state.write_mode = active_write_mode
-    for router in (health.router, overview.router, crypto.router, crypto_trader.router, equity.router, cash.router, reports.router, positions.router, budget.router, system.router, market.router):
+    for router in (health.router, overview.router, crypto.router, crypto_trader.router, equity.router, cash.router, reports.router, positions.router, budget.router, system.router, market.router, truewealth.router):
         app.include_router(router, prefix="/api")
     return app

diff --git a/src/jarvis_finance/services/portfolio_data.py b/src/jarvis_finance/services/portfolio_data.py
index 230f4cb..7a5762e 100644
--- a/src/jarvis_finance/services/portfolio_data.py
+++ b/src/jarvis_finance/services/portfolio_data.py
@@ -1596,8 +1596,11 @@ def build_portfolio_reconciliation(
         cash_value = _stored_decimal(cash["amount_chf"], "Cashbestand") if cash else None
         cash_date = str(cash["balance_date"]) if cash else None
         legacy = conn.execute(
-            """SELECT total_value_chf,valuation_date,source_type FROM account_value_snapshots WHERE account_id=? AND valuation_date<=? AND created_at<=?
-               ORDER BY valuation_date DESC,created_at DESC,snapshot_id DESC LIMIT 1""", (account, as_of, cutoff)
+            """SELECT total_value_chf,valuation_date,source_type FROM account_value_snapshots
+               WHERE account_id=? AND valuation_date<=? AND created_at<=? AND COALESCE(is_active,1)=1
+               ORDER BY valuation_date DESC,
+                        CASE source_type WHEN 'truewealth_official_import' THEN 3 WHEN 'truewealth_manual_provisional' THEN 2 ELSE 1 END DESC,
+                        COALESCE(valuation_at,created_at) DESC,created_at DESC,snapshot_id DESC LIMIT 1""", (account, as_of, cutoff)
         ).fetchone()
         reported_total = _stored_decimal(legacy["total_value_chf"], "Kontototal") if legacy else None
         total_date = str(legacy["valuation_date"]) if legacy else None
diff --git a/src/jarvis_finance/services/portfolio_performance.py b/src/jarvis_finance/services/portfolio_performance.py
index 3539bd8..10a4744 100644
--- a/src/jarvis_finance/services/portfolio_performance.py
+++ b/src/jarvis_finance/services/portfolio_performance.py
@@ -228,13 +228,15 @@ def _load_valuations(
           SELECT s.*,
                  ROW_NUMBER() OVER (
                    PARTITION BY account_id, valuation_date
-                   ORDER BY created_at DESC, snapshot_id DESC
+                   ORDER BY CASE source_type WHEN 'truewealth_official_import' THEN 3 WHEN 'truewealth_manual_provisional' THEN 2 ELSE 1 END DESC,
+                            COALESCE(valuation_at,created_at) DESC, created_at DESC, snapshot_id DESC
                  ) AS rn
           FROM account_value_snapshots s
           WHERE account_id IN ({placeholders})
             AND valuation_date BETWEEN ? AND ?
             AND created_at <= ?
-            AND updated_at IS NULL
+            AND COALESCE(is_active,1)=1
+            AND source_type <> 'truewealth_manual_provisional'
         )
         SELECT * FROM ranked WHERE rn=1 ORDER BY valuation_date, account_id
         """,
diff --git a/src/jarvis_finance/services/portfolio_service.py b/src/jarvis_finance/services/portfolio_service.py
index 74344d1..b270e8c 100644
--- a/src/jarvis_finance/services/portfolio_service.py
+++ b/src/jarvis_finance/services/portfolio_service.py
@@ -18,8 +18,11 @@ def _latest_truewealth_value(conn: Connection) -> Decimal:
         JOIN accounts a ON a.account_id = avs.account_id
         JOIN platforms p ON p.platform_id = a.platform_id
         WHERE a.is_active = 1
+          AND COALESCE(avs.is_active,1) = 1
           AND (lower(p.name || ' ' || a.account_name) LIKE '%true%wealth%' OR lower(a.account_type) IN ('managed_portfolio','robo_portfolio'))
-        ORDER BY avs.valuation_date DESC, avs.created_at DESC
+        ORDER BY avs.valuation_date DESC,
+                 CASE avs.source_type WHEN 'truewealth_official_import' THEN 3 WHEN 'truewealth_manual_provisional' THEN 2 ELSE 1 END DESC,
+                 COALESCE(avs.valuation_at,avs.created_at) DESC, avs.created_at DESC
         LIMIT 1
         """
     ).fetchone()
diff --git a/src/jarvis_finance/services/reconciliation_snapshot.py b/src/jarvis_finance/services/reconciliation_snapshot.py
index 086b2cb..8a76ece 100644
--- a/src/jarvis_finance/services/reconciliation_snapshot.py
+++ b/src/jarvis_finance/services/reconciliation_snapshot.py
@@ -201,7 +201,10 @@ def list_snapshot_metadata(conn: Connection, *, now: datetime | None = None) ->
         conn,
         """SELECT account_id, valuation_date, created_at, source_type, currency, total_value_chf, quality_status
            FROM account_value_snapshots
-           ORDER BY account_id, valuation_date DESC, created_at DESC, snapshot_id DESC""",
+           WHERE COALESCE(is_active,1)=1
+           ORDER BY account_id, valuation_date DESC,
+                    CASE source_type WHEN 'truewealth_official_import' THEN 3 WHEN 'truewealth_manual_provisional' THEN 2 ELSE 1 END DESC,
+                    COALESCE(valuation_at,created_at) DESC, created_at DESC, snapshot_id DESC""",
     )
     latest_accounts: set[str] = set()
     for row in valuations:
diff --git a/src/jarvis_finance/storage/migrations.py b/src/jarvis_finance/storage/migrations.py
index db02415..41183b7 100644
--- a/src/jarvis_finance/storage/migrations.py
+++ b/src/jarvis_finance/storage/migrations.py
@@ -6,8 +6,8 @@ from sqlite3 import Connection

 from .schema import INITIAL_SCHEMA_SQL

-MIGRATION_VERSION = 43
-MIGRATION_NAME = "043_postfinance_baseline_mapping_audit_v1"
+MIGRATION_VERSION = 44
+MIGRATION_NAME = "044_truewealth_verified_snapshot_v1"

 INSTRUMENT_OPTIONAL_COLUMNS = {
     "position_category": "TEXT",
@@ -1925,6 +1925,173 @@ def _create_daily_market_analytics_tables(conn: Connection) -> None:
     )

+def _create_truewealth_verified_snapshot_v1(conn: Connection) -> None:
+    _add_missing_columns(
+        conn,
+        "account_value_snapshots",
+        {
+            "valuation_at": "TEXT",
+            "source_reference": "TEXT",
+            "is_active": "INTEGER NOT NULL DEFAULT 1",
+            "deactivated_at": "TEXT",
+            "deactivation_reason": "TEXT",
+        },
+    )
+    conn.executescript(
+        """
+        CREATE TABLE IF NOT EXISTS truewealth_portfolios (
+            portfolio_id TEXT PRIMARY KEY,
+            account_id TEXT NOT NULL UNIQUE REFERENCES accounts(account_id),
+            source_reference_hash TEXT NOT NULL,
+            label TEXT NOT NULL,
+            portfolio_kind TEXT CHECK(portfolio_kind IN ('free_assets','pillar_3a','child','other')),
+            base_currency TEXT NOT NULL DEFAULT 'CHF',
+            is_active INTEGER NOT NULL DEFAULT 1 CHECK(is_active IN (0,1)),
+            created_at TEXT NOT NULL,
+            updated_at TEXT
+        );
+
+        CREATE TABLE IF NOT EXISTS truewealth_import_batches (
+            batch_id TEXT PRIMARY KEY,
+            portfolio_id TEXT NOT NULL REFERENCES truewealth_portfolios(portfolio_id),
+            account_id TEXT NOT NULL REFERENCES accounts(account_id),
+            file_sha256 TEXT NOT NULL UNIQUE CHECK(length(file_sha256)=64),
+            filename_sha256 TEXT NOT NULL CHECK(length(filename_sha256)=64),
+            source_file_type TEXT NOT NULL CHECK(source_file_type='application/pdf'),
+            parser_id TEXT NOT NULL,
+            parser_version TEXT NOT NULL,
+            provenance TEXT NOT NULL CHECK(provenance='truewealth_customer_export'),
+            snapshot_date TEXT NOT NULL,
+            page_count INTEGER NOT NULL CHECK(page_count > 0),
+            archive_reference TEXT NOT NULL,
+            status TEXT NOT NULL CHECK(status='confirmed'),
+            audit_id TEXT NOT NULL,
+            confirmed_at TEXT NOT NULL,
+            confirmed_by TEXT NOT NULL DEFAULT 'user'
+        );
+        CREATE INDEX IF NOT EXISTS idx_truewealth_batches_portfolio_date
+            ON truewealth_import_batches(portfolio_id, snapshot_date DESC, confirmed_at DESC);
+
+        CREATE TABLE IF NOT EXISTS truewealth_snapshots (
+            snapshot_id TEXT PRIMARY KEY,
+            portfolio_id TEXT NOT NULL REFERENCES truewealth_portfolios(portfolio_id),
+            account_id TEXT NOT NULL REFERENCES accounts(account_id),
+            batch_id TEXT NOT NULL UNIQUE REFERENCES truewealth_import_batches(batch_id),
+            snapshot_date TEXT NOT NULL,
+            source_total_chf TEXT NOT NULL,
+            securities_total_chf TEXT NOT NULL,
+            cash_total_chf TEXT NOT NULL,
+            components_total_chf TEXT NOT NULL,
+            reconciliation_difference_chf TEXT NOT NULL,
+            reconciliation_tolerance_chf TEXT NOT NULL DEFAULT '1.00',
+            reconciliation_status TEXT NOT NULL CHECK(reconciliation_status IN ('matched','within_tolerance','mismatch')),
+            position_count INTEGER NOT NULL,
+            cash_count INTEGER NOT NULL,
+            completeness_status TEXT NOT NULL CHECK(completeness_status IN ('complete','partial','blocked')),
+            reason_codes_json TEXT NOT NULL DEFAULT '[]'
+                CHECK(json_valid(reason_codes_json) AND json_type(reason_codes_json)='array'),
+            created_at TEXT NOT NULL,
+            UNIQUE(portfolio_id, snapshot_date, batch_id)
+        );
+        CREATE INDEX IF NOT EXISTS idx_truewealth_snapshot_date
+          ON truewealth_snapshots(portfolio_id, snapshot_date DESC);
+        CREATE UNIQUE INDEX IF NOT EXISTS ux_truewealth_snapshot_portfolio_date
+          ON truewealth_snapshots(portfolio_id, snapshot_date);
+        CREATE TABLE IF NOT EXISTS truewealth_snapshot_positions (
+            snapshot_position_id TEXT PRIMARY KEY,
+            snapshot_id TEXT NOT NULL REFERENCES truewealth_snapshots(snapshot_id),
+            source_row_reference TEXT NOT NULL,
+            instrument_name TEXT NOT NULL,
+            isin TEXT NOT NULL,
+            asset_type TEXT,
+            quantity TEXT NOT NULL,
+            price_currency TEXT NOT NULL,
+            source_price TEXT NOT NULL,
+            source_value_chf TEXT NOT NULL,
+            source_evidence_json TEXT NOT NULL DEFAULT '{}'
+                CHECK(json_valid(source_evidence_json) AND json_type(source_evidence_json)='object'),
+            created_at TEXT NOT NULL,
+            UNIQUE(snapshot_id, source_row_reference),
+            UNIQUE(snapshot_id, isin)
+        );
+        CREATE INDEX IF NOT EXISTS idx_truewealth_positions_snapshot
+            ON truewealth_snapshot_positions(snapshot_id, isin);
+
+        CREATE TABLE IF NOT EXISTS truewealth_snapshot_cash (
+            snapshot_cash_id TEXT PRIMARY KEY,
+            snapshot_id TEXT NOT NULL REFERENCES truewealth_snapshots(snapshot_id),
+            source_row_reference TEXT NOT NULL,
+            currency TEXT NOT NULL,
+            amount_original TEXT NOT NULL,
+            fx_rate_to_chf TEXT,
+            source_value_chf TEXT NOT NULL,
+            source_evidence_json TEXT NOT NULL DEFAULT '{}'
+                CHECK(json_valid(source_evidence_json) AND json_type(source_evidence_json)='object'),
+            created_at TEXT NOT NULL,
+            UNIQUE(snapshot_id, source_row_reference),
+            UNIQUE(snapshot_id, currency)
+        );
+        CREATE INDEX IF NOT EXISTS idx_truewealth_cash_snapshot
+            ON truewealth_snapshot_cash(snapshot_id, currency);
+
+        CREATE UNIQUE INDEX IF NOT EXISTS ux_account_value_truewealth_official_reference
+            ON account_value_snapshots(source_reference)
+            WHERE source_type='truewealth_official_import' AND source_reference IS NOT NULL;
+
+        CREATE TRIGGER IF NOT EXISTS truewealth_account_values_no_delete
+        BEFORE DELETE ON account_value_snapshots
+        WHEN OLD.source_type IN ('truewealth_official_import','truewealth_manual_provisional')
+        BEGIN SELECT RAISE(ABORT, 'truewealth account valuations cannot be deleted'); END;
+        CREATE TRIGGER IF NOT EXISTS truewealth_official_account_values_immutable
+        BEFORE UPDATE ON account_value_snapshots
+        WHEN OLD.source_type='truewealth_official_import'
+        BEGIN SELECT RAISE(ABORT, 'official truewealth account valuations are immutable'); END;
+        CREATE TRIGGER IF NOT EXISTS truewealth_manual_value_fields_immutable
+        BEFORE UPDATE ON account_value_snapshots
+        WHEN OLD.source_type='truewealth_manual_provisional' AND (
+            NEW.snapshot_id IS NOT OLD.snapshot_id OR NEW.account_id IS NOT OLD.account_id OR
+            NEW.valuation_date IS NOT OLD.valuation_date OR NEW.total_value_chf IS NOT OLD.total_value_chf OR
+            NEW.currency IS NOT OLD.currency OR NEW.source_type IS NOT OLD.source_type OR
+            NEW.quality_status IS NOT OLD.quality_status OR NEW.notes IS NOT OLD.notes OR
+            NEW.created_at IS NOT OLD.created_at OR NEW.valuation_at IS NOT OLD.valuation_at OR
+            NEW.source_reference IS NOT OLD.source_reference
+        )
+        BEGIN SELECT RAISE(ABORT, 'manual truewealth valuation fields are immutable'); END;
+
+        CREATE TRIGGER IF NOT EXISTS truewealth_batches_immutable_update
+        BEFORE UPDATE ON truewealth_import_batches
+        BEGIN SELECT RAISE(ABORT, 'truewealth import batches are immutable'); END;
+        CREATE TRIGGER IF NOT EXISTS truewealth_batches_no_delete
+        BEFORE DELETE ON truewealth_import_batches
+        BEGIN SELECT RAISE(ABORT, 'truewealth import batches cannot be deleted'); END;
+        CREATE TRIGGER IF NOT EXISTS truewealth_snapshots_immutable_update
+        BEFORE UPDATE ON truewealth_snapshots
+        BEGIN SELECT RAISE(ABORT, 'truewealth snapshots are immutable'); END;
+        CREATE TRIGGER IF NOT EXISTS truewealth_snapshots_no_delete
+        BEFORE DELETE ON truewealth_snapshots
+        BEGIN SELECT RAISE(ABORT, 'truewealth snapshots cannot be deleted'); END;
+        CREATE TRIGGER IF NOT EXISTS truewealth_positions_immutable_update
+        BEFORE UPDATE ON truewealth_snapshot_positions
+        BEGIN SELECT RAISE(ABORT, 'truewealth positions are immutable'); END;
+        CREATE TRIGGER IF NOT EXISTS truewealth_positions_no_delete
+        BEFORE DELETE ON truewealth_snapshot_positions
+        BEGIN SELECT RAISE(ABORT, 'truewealth positions cannot be deleted'); END;
+        CREATE TRIGGER IF NOT EXISTS truewealth_cash_immutable_update
+        BEFORE UPDATE ON truewealth_snapshot_cash
+        BEGIN SELECT RAISE(ABORT, 'truewealth cash is immutable'); END;
+        CREATE TRIGGER IF NOT EXISTS truewealth_cash_no_delete
+        BEFORE DELETE ON truewealth_snapshot_cash
+        BEGIN SELECT RAISE(ABORT, 'truewealth cash cannot be deleted'); END;
+        CREATE TRIGGER IF NOT EXISTS truewealth_audit_immutable_update
+        BEFORE UPDATE ON audit_log WHEN OLD.entity_type='truewealth_import_batch'
+        BEGIN SELECT RAISE(ABORT, 'truewealth import audit is immutable'); END;
+        CREATE TRIGGER IF NOT EXISTS truewealth_audit_no_delete
+        BEFORE DELETE ON audit_log WHEN OLD.entity_type='truewealth_import_batch'
+        BEGIN SELECT RAISE(ABORT, 'truewealth import audit cannot be deleted'); END;
+        """
+    )
+
+
 def _apply_compat_migrations(conn: Connection) -> None:
     _add_missing_instrument_columns(conn)
     for table, text_columns in TEXT_AFFINITY_COLUMNS.items():
@@ -1955,6 +2122,7 @@ def _apply_compat_migrations(conn: Connection) -> None:
     _create_portfolio_ingestion_reconciliation_tables(conn)
     _create_daily_market_analytics_tables(conn)
     _create_postfinance_baseline_mapping_audit_v1(conn)
+    _create_truewealth_verified_snapshot_v1(conn)
     _create_grocery_optimizer_v1_tables(conn)
     _add_grocery_price_provider_v1_columns(conn)
     _create_grocery_matching_learning_v2_tables(conn)


sed -n '1,320p' src/jarvis_finance/imports/truewealth_tax_statement.py


Output:
Chunk ID: 362413
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2117
Output:
from __future__ import annotations

from dataclasses import dataclass
from decimal import Decimal, InvalidOperation
from hashlib import sha256
from io import BytesIO
import re

PARSER_ID = "truewealth_tax_statement"
PARSER_VERSION = "1"
MAX_PDF_BYTES = 5 * 1024 * 1024
_DATE = r"(?P<day>\d{2})\.(?P<month>\d{2})\.(?P<year>\d{4})"
_DATE_PLAIN = r"\d{2}\.\d{2}\.\d{4}"
_ISIN = re.compile(r"^\s*\d+\s+([A-Z]{2}[A-Z0-9]{9}[0-9])\s+-\s+(.+?)\s*$")
_POSITION = re.compile(
    rf"^\s*{_DATE}\s+Bestand\s*/\s*Steuerwert\s*/\s*Ertrag\s+"
    r"([\d’'.,]+)\s+([A-Z]{3})\s+([\d’'.,]+)\s+([\d’'.,]+)(?:\s|$)"
)
_CASH_LABEL = re.compile(r"^\s*(CHF|EUR|GBP|USD)\s+\(")
_CASH_VALUE = re.compile(
    rf"^\s*{_DATE}\s+Steuerwert\s*/\s*Ertrag\s+([A-Z]{{3}})\s+(.+)$"
)

@dataclass(frozen=True)
class TrueWealthPosition:
    source_ref: str
    name: str
    isin: str
    quantity: Decimal
    currency: str
    source_price: Decimal
    source_value_chf: Decimal

@dataclass(frozen=True)
class TrueWealthCash:
    source_ref: str
    currency: str
    amount_original: Decimal
    fx_rate_to_chf: Decimal | None
    value_chf: Decimal

@dataclass(frozen=True)
class TrueWealthStatement:
    statement_date: str
    period_from: str
    period_to: str
    source_total_chf: Decimal
    positions: tuple[TrueWealthPosition, ...]
    cash: tuple[TrueWealthCash, ...]
    file_sha256: str
    page_count: int

    @property
    def securities_total_chf(self) -> Decimal:
        return sum((row.source_value_chf for row in self.positions), Decimal("0"))

    @property
    def cash_total_chf(self) -> Decimal:
        return sum((row.value_chf for row in self.cash), Decimal("0"))

    @property
    def components_total_chf(self) -> Decimal:
        return self.securities_total_chf + self.cash_total_chf

def _iso(day: str, month: str, year: str) -> str:
    return f"{year}-{month}-{day}"

def _decimal(value: str) -> Decimal:
    normalized = value.replace("’", "").replace("'", "").replace(" ", "")
    if normalized.count(",") == 1 and "." not in normalized:
        normalized = normalized.replace(",", ".")
    try:
        return Decimal(normalized)
    except InvalidOperation as exc:
        raise ValueError("TrueWealth statement contains an invalid numeric value") from exc

def _extract_pdf_text(raw: bytes) -> tuple[str, int]:
    if not raw or len(raw) > MAX_PDF_BYTES or not raw.startswith(b"%PDF-"):
        raise ValueError("TrueWealth source must be a bounded PDF")
    try:
        from pypdf import PdfReader

        reader = PdfReader(BytesIO(raw), strict=True)
        if reader.is_encrypted:
            raise ValueError("Encrypted TrueWealth PDFs are not supported")
        pages = [page.extract_text(extraction_mode="layout") or "" for page in reader.pages]
    except ValueError:
        raise
    except Exception as exc:
        raise ValueError("TrueWealth PDF could not be read") from exc
    if not pages or not all(page.strip() for page in pages[:6]):
        raise ValueError("TrueWealth PDF does not contain the expected text layer")
    return "\n\f\n".join(pages), len(pages)

def parse_truewealth_text(text: str, *, file_sha256: str = "synthetic", page_count: int = 7) -> TrueWealthStatement:
    if "True Wealth AG" not in text or "Steuerauszug in CHF" not in text:
        raise ValueError("PDF is not an evidenced TrueWealth tax statement")
    date_match = re.search(rf"Steuerauszug\s+in\s+CHF\s+{_DATE}", text)
    period_match = re.search(rf"Periode\s+{_DATE_PLAIN}\s*-\s*{_DATE_PLAIN}", text)
    if not date_match or not period_match:
        raise ValueError("TrueWealth statement date or period is missing")
    statement_date = _iso(date_match.group("day"), date_match.group("month"), date_match.group("year"))
    period_dates = re.findall(r"\d{2}\.\d{2}\.\d{4}", period_match.group(0))
    period_from = "-".join(reversed(period_dates[0].split(".")))
    period_to = "-".join(reversed(period_dates[1].split(".")))
    if statement_date != period_to:
        raise ValueError("TrueWealth statement date conflicts with the period end")

    total_match = re.search(
        rf"Total\s+Steuerwert\s+der[\s\S]{{0,500}}?Werte\s+am\s+{re.escape(period_dates[1])}[\s\S]{{0,350}}?\n\s*([\d’']+(?:[.,]\d+)?)\s+",
        text,
    )
    if not total_match:
        raise ValueError("TrueWealth source total is missing")
    source_total = _decimal(total_match.group(1))

    positions: list[TrueWealthPosition] = []
    cash: list[TrueWealthCash] = []
    current_isin: str | None = None
    current_name: list[str] = []
    current_cash_currency: str | None = None
    for raw_line in text.splitlines():
        line = raw_line.rstrip()
        cash_label = _CASH_LABEL.match(line)
        if cash_label:
            current_cash_currency = cash_label.group(1)
            continue
        cash_value = _CASH_VALUE.match(line)
        if cash_value and current_cash_currency:
            currency = cash_value.group(4)
            if currency != current_cash_currency:
                raise ValueError("TrueWealth cash currency is inconsistent")
            numbers = re.findall(r"[\d’'.,]+", cash_value.group(5))
            required = 2 if currency == "CHF" else 3
            if len(numbers) < required:
                raise ValueError("TrueWealth cash value is incomplete")
            amount = _decimal(numbers[0])
            optional_fx = None if currency == "CHF" else _decimal(numbers[1])
            value_chf = _decimal(numbers[1] if currency == "CHF" else numbers[2])
            cash.append(
                TrueWealthCash(
                    source_ref=f"cash:{currency}",
                    currency=currency,
                    amount_original=amount,
                    fx_rate_to_chf=optional_fx,
                    value_chf=value_chf,
                )
            )
            current_cash_currency = None
            continue

        start = _ISIN.match(line)
        if start:
            current_isin = start.group(1)
            current_name = [start.group(2).strip()]
            continue
        if current_isin and line.strip() and not re.match(r"^\s*\d{2}\.\d{2}\.\d{4}", line):
            if not any(token in line for token in ("True Wealth AG", "Kunde", "Kdnr.", "Periode", "Erstellt am", "Kanton", "Valoren-Nr.", "Datum", "ISIN", "Depot ", "USA", "Werte mit Anrechnung")):
                current_name.append(line.strip())
            continue
        position = _POSITION.match(line)
        if position and current_isin:
            row_date = _iso(position.group("day"), position.group("month"), position.group("year"))
            if row_date != statement_date:
                raise ValueError("TrueWealth position date conflicts with statement date")
            positions.append(
                TrueWealthPosition(
                    source_ref=f"position:{current_isin}",
                    name=" ".join(current_name),
                    isin=current_isin,
                    quantity=_decimal(position.group(4)),
                    currency=position.group(5),
                    source_price=_decimal(position.group(6)),
                    source_value_chf=_decimal(position.group(7)),
                )
            )
            current_isin = None
            current_name = []

    if len(positions) < 1 or len({row.isin for row in positions}) != len(positions):
        raise ValueError("TrueWealth positions are missing or duplicated")
    if not cash:
        raise ValueError("TrueWealth cash rows are missing")
    if any(row.currency != "CHF" for row in positions):
        raise ValueError("TrueWealth tax-statement position values must be reported in CHF")
    difference = source_total - (sum((row.source_value_chf for row in positions), Decimal("0")) + sum((row.value_chf for row in cash), Decimal("0")))
    if abs(difference) > Decimal("1.00"):
        raise ValueError("TrueWealth components do not reconcile to the source total")
    return TrueWealthStatement(
        statement_date=statement_date,
        period_from=period_from,
        period_to=period_to,
        source_total_chf=source_total,
        positions=tuple(positions),
        cash=tuple(cash),
        file_sha256=file_sha256,
        page_count=page_count,
    )

def parse_truewealth_tax_statement(raw: bytes) -> TrueWealthStatement:
    text, pages = _extract_pdf_text(raw)
    return parse_truewealth_text(text, file_sha256=sha256(raw).hexdigest(), page_count=pages)


sed -n '1,260p' src/jarvis_finance/api/routers/truewealth.py && sed -n '1,260p' src/jarvis_finance/api/schemas/truewealth.py && sed -n '1,260p' frontend/src/api/truewealth.ts


Output:
Chunk ID: 3bf9c1
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1919
Output:
from __future__ import annotations

from fastapi import APIRouter, Depends, HTTPException

from jarvis_finance.api.dependencies import get_db
from jarvis_finance.api.schemas.truewealth import (
    TrueWealthImportConfirmRequest,
    TrueWealthImportPreviewRequest,
    TrueWealthManualConfirmRequest,
    TrueWealthManualPreviewRequest,
    TrueWealthManualStatusRequest,
)
from jarvis_finance.services.truewealth_service import (
    confirm_manual_truewealth_value,
    confirm_truewealth_import,
    get_truewealth_summary,
    preview_manual_truewealth_value,
    preview_truewealth_import,
    set_manual_truewealth_value_active,
)

router = APIRouter(prefix="/truewealth", tags=["truewealth"])

def _run(callable_, *args):
    try:
        return callable_(*args)
    except ValueError as exc:
        raise HTTPException(status_code=422, detail=str(exc)) from exc

def _safe_import_preview(payload: dict) -> dict:
    return {
        key: value
        for key, value in payload.items()
        if key not in {"file_sha256", "existing_batch_id", "conflicting_batch_id"}
    }

@router.get("/summary")
def summary(conn=Depends(get_db)):
    return _run(get_truewealth_summary, conn)

@router.post("/imports/preview")
def import_preview(request: TrueWealthImportPreviewRequest, conn=Depends(get_db)):
    return _safe_import_preview(_run(preview_truewealth_import, conn, request.model_dump()))

@router.post("/imports/confirm")
def import_confirm(request: TrueWealthImportConfirmRequest, conn=Depends(get_db)):
    return _run(confirm_truewealth_import, conn, request.model_dump())

@router.post("/manual-values/preview")
def manual_preview(request: TrueWealthManualPreviewRequest, conn=Depends(get_db)):
    return _run(preview_manual_truewealth_value, conn, request.model_dump())

@router.post("/manual-values/confirm")
def manual_confirm(request: TrueWealthManualConfirmRequest, conn=Depends(get_db)):
    return _run(confirm_manual_truewealth_value, conn, request.model_dump())

@router.post("/manual-values/{snapshot_id}/status")
def manual_status(
    snapshot_id: str,
    request: TrueWealthManualStatusRequest,
    conn=Depends(get_db),
):
    return _run(set_manual_truewealth_value_active, conn, snapshot_id, request.model_dump())
from __future__ import annotations

from pydantic import BaseModel, ConfigDict, Field

class TrueWealthImportPreviewRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")
    account_id: str | None = Field(default=None, min_length=1, max_length=120)
    file_name: str = Field(min_length=1, max_length=200)
    content_base64: str = Field(min_length=8, max_length=8_000_000)

class TrueWealthImportConfirmRequest(TrueWealthImportPreviewRequest):
    preview_id: str = Field(min_length=10, max_length=80)
    confirmation_id: str = Field(min_length=10, max_length=80)
    confirm: bool

class TrueWealthManualPreviewRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")
    total_value_chf: str = Field(min_length=1, max_length=40)
    valuation_at: str = Field(min_length=10, max_length=40)
    note: str = Field(default="", max_length=240)

class TrueWealthManualConfirmRequest(TrueWealthManualPreviewRequest):
    preview_id: str = Field(min_length=10, max_length=80)
    confirmation_id: str = Field(min_length=10, max_length=80)
    confirm: bool

class TrueWealthManualStatusRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")
    active: bool
    note: str = Field(default="", max_length=240)
    confirm: bool
import { apiGet, apiPost } from './client'

export type TrueWealthPosition = {
  snapshot_position_id: string
  instrument_name: string
  isin: string
  asset_type: string | null
  quantity: string
  price_currency: string
  source_price: string
  source_value_chf: string
}

export type TrueWealthCash = {
  snapshot_cash_id: string
  currency: string
  amount_original: string
  fx_rate_to_chf: string | null
  source_value_chf: string
}

export type TrueWealthHistory = {
  snapshot_id: string
  total_value_chf: string
  valuation_date: string
  valuation_at: string | null
  source_type: string
  quality_status: string
  notes: string | null
  is_active: number
  deactivated_at: string | null
  deactivation_reason: string | null
}

export type TrueWealthImportHistory = {
  batch_id: string
  snapshot_id: string
  snapshot_date: string
  parser_id: string
  parser_version: string
  confirmed_at: string
  status: string
  position_count: number
  cash_count: number
  source_total_chf: string
  securities_total_chf: string
  cash_total_chf: string
  components_total_chf: string
  reconciliation_difference_chf: string
  reconciliation_status: string
  completeness_status: string
}

export type TrueWealthSummary = {
  portfolio_label: string
  current_value_chf: string | null
  current_valuation_at: string | null
  current_source_type: string
  current_is_manual_provisional: boolean
  current_label: string
  positions_snapshot_date: string | null
  positions_label: string
  mixed_as_of: boolean
  allocation_performance_usable: boolean
  positions: TrueWealthPosition[]
  cash: TrueWealthCash[]
  history: TrueWealthHistory[]
  imports: TrueWealthImportHistory[]
  latest_official: Record<string, unknown> | null
}

export type TrueWealthImportPayload = {
  file_name: string
  content_base64: string
}

export type TrueWealthImportPreview = TrueWealthImportPayload & {
  preview_id: string
  confirmation_id: string
  portfolio_label: string
  snapshot_date: string
  period_from: string
  period_to: string
  page_count: number
  source_total_chf: string
  securities_total_chf: string
  cash_total_chf: string
  components_total_chf: string
  reconciliation_difference_chf: string
  reconciliation_status: string
  positions: Array<{ name: string; isin: string; quantity: string; currency: string; source_price: string; source_value_chf: string }>
  cash: Array<{ currency: string; amount_original: string; fx_rate_to_chf: string | null; source_value_chf: string }>
  duplicate: boolean
  conflict: boolean
  warnings: string[]
}

export type ManualValuePayload = { total_value_chf: string; valuation_at: string; note: string }
export type ManualValuePreview = ManualValuePayload & {
  preview_id: string
  confirmation_id: string
  valuation_date: string
  source_type: string
  status: string
  warnings: string[]
}

export const getTrueWealthSummary = (refresh = false) => apiGet<TrueWealthSummary>('/api/truewealth/summary', { refresh })
export const previewTrueWealthImport = (payload: TrueWealthImportPayload) => apiPost<TrueWealthImportPreview>('/api/truewealth/imports/preview', payload)
export const confirmTrueWealthImport = (payload: TrueWealthImportPayload & Pick<TrueWealthImportPreview, 'preview_id' | 'confirmation_id'> & { confirm: true }) => apiPost<{ status: string; batch_id: string; snapshot_id?: string; audit_id: string; idempotent: boolean }>('/api/truewealth/imports/confirm', payload)
export const previewManualTrueWealthValue = (payload: ManualValuePayload) => apiPost<ManualValuePreview>('/api/truewealth/manual-values/preview', payload)
export const confirmManualTrueWealthValue = (payload: ManualValuePayload & Pick<ManualValuePreview, 'preview_id' | 'confirmation_id'> & { confirm: true }) => apiPost<{ status: string; snapshot_id: string; audit_id: string }>('/api/truewealth/manual-values/confirm', payload)
export const setManualTrueWealthValueStatus = (snapshotId: string, active: boolean, note: string) => apiPost<{ status: string; snapshot_id: string; audit_id: string; idempotent: boolean }>(`/api/truewealth/manual-values/${encodeURIComponent(snapshotId)}/status`, { active, note, confirm: true })


sed -n '1,340p' frontend/src/pages/TrueWealthPage.vue


Output:
Chunk ID: 9b769b
Wall time: 0.0001 seconds
Process exited with code 0
Original token count: 4379
Output:
<template>

    <LoadActionBar
      testid="refresh-truewealth"
      title="TrueWealth · Freie Anlagen"
      description="Offizielle Portfolio-Snapshots und provisorische manuelle Gesamtwerte mit klar getrenntem Datenstand."
      :loading="loading"
      @load="loadData(true)"
    />
    <LocalDataStatus :backend-ok="backendOk" :runtime-ok="runtimeOk" :loading="loading" :error="error" :last-loaded-at="lastLoadedAt" />

Freie Anlagen

Aktueller TrueWealth-Gesamtwert CHF

{{ summary?.current_label ?? 'Noch kein Wert verfügbar' }}

        <MoneyValue :value="summary?.current_value_chf ?? '0.00'" />

        Manuell / provisorisch. {{ summary.positions_label }}. Die Differenz wird weder Positionen noch Cash zugerechnet.

        Gesamtwert und Positionsdetails haben unterschiedliche Stichtage. Allokation, Gewinn und Rendite sind für diesen Mischstand deaktiviert.

        <button data-testid="truewealth-update-button" type="button" class="rounded-lg border border-cyan-300 bg-cyan-50 px-3 py-2 font-semibold text-cyan-800" @click="showManualForm = !showManualForm">Gesamtwert manuell aktualisieren</button>
        <button data-testid="truewealth-import-button" type="button" class="rounded-lg border border-slate-300 bg-white px-3 py-2 font-semibold text-slate-800" @click="showImportForm = !showImportForm">Offiziellen Auszug importieren</button>
        Zur Portfolio-Übersicht

Gesamtwert manuell aktualisieren

Erstellt einen neuen historischen, provisorischen Datensatz. Offizielle Positionen bleiben unverändert.

        Gesamtwert CHF<input data-testid="truewealth-amount-input" v-model="manualForm.total_value_chf" class="mt-1 w-full rounded-lg border border-slate-300 bg-white p-2" inputmode="decimal" placeholder="Betrag in CHF" @input="manualPreview = null" />
        Bewertungszeitpunkt<input data-testid="truewealth-date-input" v-model="manualForm.valuation_at" class="mt-1 w-full rounded-lg border border-slate-300 bg-white p-2" type="datetime-local" @input="manualPreview = null" />
        Notiz optional<input data-testid="truewealth-note-input" v-model="manualForm.note" class="mt-1 w-full rounded-lg border border-slate-300 bg-white p-2" maxlength="240" placeholder="z. B. Stand im Kundenportal" @input="manualPreview = null" />

        <button data-testid="truewealth-preview-button" class="rounded-lg bg-slate-900 px-3 py-2 font-semibold text-white disabled:opacity-50" :disabled="actionLoading || !manualForm.total_value_chf || !manualForm.valuation_at" @click="doManualPreview">Preview</button>
        <button v-if="manualPreview" data-testid="truewealth-confirm-button" class="rounded-lg bg-emerald-600 px-3 py-2 font-semibold text-white disabled:opacity-50" :disabled="actionLoading" @click="doManualConfirm">Confirm</button>

Preview: CHF {{ money(manualPreview.total_value_chf) }} · {{ formatDateTime(manualPreview.valuation_at) }}

Manuell / provisorisch. Positionsdetails werden nicht verändert.

Offiziellen TrueWealth-Auszug importieren

Nur unveränderte TrueWealth-Steuerauszüge als PDF. Der Import wird zuerst vollständig geprüft und angezeigt.

      <input data-testid="truewealth-file-input" class="mt-3 block w-full rounded-lg border border-slate-300 bg-white p-2 text-sm" type="file" accept="application/pdf,.pdf" @change="onFileSelected" />

        <button data-testid="truewealth-import-preview-button" class="rounded-lg bg-slate-900 px-3 py-2 font-semibold text-white disabled:opacity-50" :disabled="actionLoading || !importPayload" @click="doImportPreview">Import-Preview</button>
        <button v-if="importPreview" data-testid="truewealth-import-confirm-button" class="rounded-lg bg-emerald-600 px-3 py-2 font-semibold text-white disabled:opacity-50" :disabled="actionLoading || importPreview.reconciliation_status === 'mismatch' || importPreview.conflict" @click="doImportConfirm">Import bestätigen</button>

Stichtag

{{ formatDate(importPreview.snapshot_date) }}

Gesamtwert

CHF {{ money(importPreview.source_total_chf) }}

Inhalt

{{ importPreview.positions.length }} Positionen · {{ importPreview.cash.length }} Cashkonten

Reconciliation

{{ importPreview.reconciliation_status }} · CHF {{ money(importPreview.reconciliation_difference_chf) }}

Identische Quelle bereits bestätigt. Ein erneuter Confirm ist idempotent.

Konflikt: Für diesen Stichtag existiert bereits eine andere offizielle Quelle. Confirm ist gesperrt.

{{ message }}

{{ actionError }}

Positionsdetails

{{ summary?.positions_label }}

{{ summary?.positions.length ?? 0 }} Positionen

{{ position.instrument_name }}

{{ position.isin }}

Menge
{{ position.quantity }}Quellwert
CHF {{ money(position.source_value_chf) }}

Noch keine offiziellen Positionen importiert.

{{ cash.currency }} {{ cash.amount_original }} · CHF {{ money(cash.source_value_chf) }}

Bewertungshistorie

Manuelle und offizielle Werte bleiben historisch und auditierbar erhalten.

CHF {{ money(item.total_value_chf) }}

{{ formatDateTime(item.valuation_at ?? item.valuation_date) }}

{{ sourceLabel(item.source_type) }}

{{ item.notes }}

Deaktiviert · historisch erhalten

          <button v-if="['truewealth_manual_provisional', 'manual_total_value'].includes(item.source_type)" class="mt-3 rounded-lg border border-slate-300 px-2 py-1 text-xs font-semibold" :data-testid="`truewealth-history-status-${index}`" @click="toggleManualStatus(item)">{{ item.is_active ? 'Provisorischen Wert deaktivieren' : 'Wert reaktivieren' }}</button>

Importhistorie

Offizieller Snapshot {{ formatDate(item.snapshot_date) }}

{{ item.position_count }} Positionen · {{ item.cash_count }} Cashkonten · CHF {{ money(item.source_total_chf) }}

Parser {{ item.parser_id }} v{{ item.parser_version }} · {{ item.reconciliation_status }}

Noch keine bestätigten offiziellen Importe.

</template>


PYTHONDONTWRITEBYTECODE=1 python - <<'PY'
import sys
sys.dont_write_bytecode=True
import jarvis_finance.services.truewealth_service as s
print('ok')
PY


Output:
Chunk ID: a7d68e
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1
Output:
ok


PYTHONDONTWRITEBYTECODE=1 pytest -q tests/test_truewealth_sprint12.py


Output:
Chunk ID: 46ef69
Wall time: 0.0358 seconds
Process exited with code 1
Original token count: 1654
Output:
Traceback (most recent call last):
  File "/home/agent/.hermes/worktrees/FinanceManager-sprint9-portfolio-basis-policy/.venv/bin/pytest", line 8, in <module>
    sys.exit(console_main())
             ^^^^^^^^^^^^^^
  File "/home/agent/.hermes/worktrees/FinanceManager-sprint9-portfolio-basis-policy/.venv/lib/python3.11/site-packages/_pytest/config/__init__.py", line 223, in console_main
    code = main()
           ^^^^^^
  File "/home/agent/.hermes/worktrees/FinanceManager-sprint9-portfolio-basis-policy/.venv/lib/python3.11/site-packages/_pytest/config/__init__.py", line 193, in main
    config = _prepareconfig(new_args, plugins)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/agent/.hermes/worktrees/FinanceManager-sprint9-portfolio-basis-policy/.venv/lib/python3.11/site-packages/_pytest/config/__init__.py", line 361, in _prepareconfig
    config: Config = pluginmanager.hook.pytest_cmdline_parse(
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/agent/.hermes/worktrees/FinanceManager-sprint9-portfolio-basis-policy/.venv/lib/python3.11/site-packages/pluggy/_hooks.py", line 512, in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/agent/.hermes/worktrees/FinanceManager-sprint9-portfolio-basis-policy/.venv/lib/python3.11/site-packages/pluggy/_manager.py", line 120, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/agent/.hermes/worktrees/FinanceManager-sprint9-portfolio-basis-policy/.venv/lib/python3.11/site-packages/pluggy/_callers.py", line 167, in _multicall
    raise exception
  File "/home/agent/.hermes/worktrees/FinanceManager-sprint9-portfolio-basis-policy/.venv/lib/python3.11/site-packages/pluggy/_callers.py", line 139, in _multicall
    teardown.throw(exception)
  File "/home/agent/.hermes/worktrees/FinanceManager-sprint9-portfolio-basis-policy/.venv/lib/python3.11/site-packages/_pytest/helpconfig.py", line 124, in pytest_cmdline_parse
    config = yield
             ^^^^^
  File "/home/agent/.hermes/worktrees/FinanceManager-sprint9-portfolio-basis-policy/.venv/lib/python3.11/site-packages/pluggy/_callers.py", line 121, in _multicall
    res = hook_impl.function(*args)
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/agent/.hermes/worktrees/FinanceManager-sprint9-portfolio-basis-policy/.venv/lib/python3.11/site-packages/_pytest/config/__init__.py", line 1186, in pytest_cmdline_parse
    self.parse(args)
  File "/home/agent/.hermes/worktrees/FinanceManager-sprint9-portfolio-basis-policy/.venv/lib/python3.11/site-packages/_pytest/config/__init__.py", line 1556, in parse
    self.hook.pytest_load_initial_conftests(
  File "/home/agent/.hermes/worktrees/FinanceManager-sprint9-portfolio-basis-policy/.venv/lib/python3.11/site-packages/pluggy/_hooks.py", line 512, in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/agent/.hermes/worktrees/FinanceManager-sprint9-portfolio-basis-policy/.venv/lib/python3.11/site-packages/pluggy/_manager.py", line 120, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/agent/.hermes/worktrees/FinanceManager-sprint9-portfolio-basis-policy/.venv/lib/python3.11/site-packages/pluggy/_callers.py", line 167, in _multicall
    raise exception
  File "/home/agent/.hermes/worktrees/FinanceManager-sprint9-portfolio-basis-policy/.venv/lib/python3.11/site-packages/pluggy/_callers.py", line 139, in _multicall
    teardown.throw(exception)
  File "/home/agent/.hermes/worktrees/FinanceManager-sprint9-portfolio-basis-policy/.venv/lib/python3.11/site-packages/_pytest/warnings.py", line 128, in pytest_load_initial_conftests
    return (yield)
            ^^^^^
  File "/home/agent/.hermes/worktrees/FinanceManager-sprint9-portfolio-basis-policy/.venv/lib/python3.11/site-packages/pluggy/_callers.py", line 116, in _multicall
    next(function_gen)  # first yield
    ^^^^^^^^^^^^^^^^^^
  File "/home/agent/.hermes/worktrees/FinanceManager-sprint9-portfolio-basis-policy/.venv/lib/python3.11/site-packages/_pytest/capture.py", line 170, in pytest_load_initial_conftests
    capman.start_global_capturing()
  File "/home/agent/.hermes/worktrees/FinanceManager-sprint9-portfolio-basis-policy/.venv/lib/python3.11/site-packages/_pytest/capture.py", line 773, in start_global_capturing
    self._global_capturing = _get_multicapture(self._method)
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/agent/.hermes/worktrees/FinanceManager-sprint9-portfolio-basis-policy/.venv/lib/python3.11/site-packages/_pytest/capture.py", line 714, in _get_multicapture
    return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2))
                                              ^^^^^^^^^^^^
  File "/home/agent/.hermes/worktrees/FinanceManager-sprint9-portfolio-basis-policy/.venv/lib/python3.11/site-packages/_pytest/capture.py", line 493, in __init__
    TemporaryFile(buffering=0),
    ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/agent/.local/share/uv/python/cpython-3.11.15-linux-x86_64-gnu/lib/python3.11/tempfile.py", line 628, in TemporaryFile
    prefix, suffix, dir, output_type = _sanitize_params(prefix, suffix, dir)
                                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/agent/.local/share/uv/python/cpython-3.11.15-linux-x86_64-gnu/lib/python3.11/tempfile.py", line 127, in _sanitize_params
    dir = gettempdir()
          ^^^^^^^^^^^^
  File "/home/agent/.local/share/uv/python/cpython-3.11.15-linux-x86_64-gnu/lib/python3.11/tempfile.py", line 316, in gettempdir
    return _os.fsdecode(_gettempdir())
                        ^^^^^^^^^^^^^
  File "/home/agent/.local/share/uv/python/cpython-3.11.15-linux-x86_64-gnu/lib/python3.11/tempfile.py", line 309, in _gettempdir
    tempdir = _get_default_tempdir()
              ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/agent/.local/share/uv/python/cpython-3.11.15-linux-x86_64-gnu/lib/python3.11/tempfile.py", line 224, in _get_default_tempdir
    raise FileNotFoundError(_errno.ENOENT,
FileNotFoundError: [Errno 2] No usable temporary directory found in ['/tmp', '/var/tmp', '/usr/tmp', '/home/agent/.hermes/worktrees/FinanceManager-sprint12-truewealth-verified-import']


sed -n '1,200p' src/jarvis_finance/api/dependencies.py && sed -n '1,120p' src/jarvis_finance/storage/database.py


Output:
Chunk ID: bee389
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 324
Output:
from __future__ import annotations

from collections.abc import Iterator
from sqlite3 import Connection

from fastapi import HTTPException

from jarvis_finance.config.settings import find_repo_root, load_settings
from jarvis_finance.storage.database import connect

def get_db() -> Iterator[Connection]:
    settings = load_settings(repo_root=find_repo_root())
    if not settings.db_path.exists():
        raise HTTPException(status_code=503, detail="Runtime database not available")
    conn = connect(settings.db_path)
    try:
        yield conn
    finally:
        conn.close()
from __future__ import annotations

import sqlite3
from pathlib import Path

def connect(db_path: str | Path) -> sqlite3.Connection:
    if str(db_path) == ":memory:":
        conn = sqlite3.connect(":memory:", check_same_thread=False)
    else:
        path = Path(db_path).expanduser()
        path.parent.mkdir(parents=True, exist_ok=True)
        conn = sqlite3.connect(path, check_same_thread=False)
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA foreign_keys = ON")
    return conn

def connect_memory() -> sqlite3.Connection:
    conn = sqlite3.connect(":memory:", check_same_thread=False)
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA foreign_keys = ON")
    return conn


sed -n '1,120p' src/jarvis_finance/api/main.py && sed -n '1,180p' src/jarvis_finance/api/security.py


Output:
Chunk ID: e75ac8
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 996
Output:
from __future__ import annotations

import os
from urllib.parse import urlparse

from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse

from jarvis_finance.api.routers import budget, cash, crypto, crypto_trader, equity, health, market, overview, positions, reports, system, truewealth
from jarvis_finance.api.security import WRITE_METHODS, is_write_request_allowed, resolve_write_mode

LOCAL_ORIGINS = [
    "http://localhost:5173",
    "http://127.0.0.1:5173",
    "http://100.85.29.67:5173",
    "http://agent.tailbd371b.ts.net:5173",
    "http://localhost:8503",
    "http://127.0.0.1:8503",
]
READ_ONLY_POST_PATHS = {
    "/api/portfolio/ingestion/preview",
    "/api/portfolio/policy/preview",
    "/api/truewealth/imports/preview",
    "/api/truewealth/manual-values/preview",
}

def _is_safe_local_origin(origin: str) -> bool:
    parsed = urlparse(origin)
    if parsed.scheme != "http" or not parsed.hostname or not parsed.port:
        return False
    hostname = parsed.hostname.lower()
    if hostname in {"localhost", "127.0.0.1"}:
        return True
    if hostname.startswith("100."):
        return True
    if hostname.endswith(".ts.net"):
        return True
    return False

def build_local_origins(environ: dict[str, str] | None = None) -> list[str]:
    env = environ or os.environ
    origins = list(LOCAL_ORIGINS)
    for raw_origin in env.get("JARVIS_FINANCE_CORS_ORIGINS", "").split(","):
        origin = raw_origin.strip().rstrip("/")
        if origin and _is_safe_local_origin(origin) and origin not in origins:
            origins.append(origin)
    return origins

def create_app(*, write_mode: str | None = None) -> FastAPI:
    active_write_mode = resolve_write_mode(write_mode)
    app = FastAPI(
        title="JARVIS Finance API",
        version="0.1.0",
        description="Read-only FastAPI v0 skeleton for the future Vue User Dashboard.",
    )
    app.add_middleware(
        CORSMiddleware,
        allow_origins=build_local_origins(),
        allow_credentials=False,
        allow_methods=["GET", "POST"] if active_write_mode == "disabled" else ["GET", "POST", "PUT", "PATCH", "DELETE"],
        allow_headers=["*"],
    )

    @app.middleware("http")
    async def block_untrusted_writes(request: Request, call_next):
        read_only_post = request.method == "POST" and request.url.path in READ_ONLY_POST_PATHS
        if request.method in WRITE_METHODS and not read_only_post and not is_write_request_allowed(request, active_write_mode):
            return JSONResponse(status_code=403, content={"detail": "write_operations_disabled"})
        return await call_next(request)

    app.state.write_mode = active_write_mode
    for router in (health.router, overview.router, crypto.router, crypto_trader.router, equity.router, cash.router, reports.router, positions.router, budget.router, system.router, market.router, truewealth.router):
        app.include_router(router, prefix="/api")
    return app

app = create_app()
from __future__ import annotations

import os
from ipaddress import ip_address
from collections.abc import Mapping

from fastapi import Request

WRITE_METHODS = {"POST", "PUT", "PATCH", "DELETE"}
WRITE_MODES = {"disabled", "local_only", "test"}

def resolve_write_mode(
    write_mode: str | None = None,
    environ: Mapping[str, str] | None = None,
) -> str:
    env = environ or os.environ
    candidate = (write_mode or env.get("JARVIS_FINANCE_WRITE_MODE", "disabled")).strip().lower()
    return candidate if candidate in WRITE_MODES else "disabled"

def is_write_request_allowed(request: Request, write_mode: str) -> bool:
    client_host = request.client.host if request.client else ""
    if write_mode == "test":
        return client_host == "testclient"
    if write_mode != "local_only":
        return False
    try:
        return ip_address(client_host).is_loopback
    except ValueError:
        return False


sed -n '1,160p' frontend/src/api/client.ts


Output:
Chunk ID: 9282d3
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 406
Output:
const DEFAULT_API_BASE_URL = ''

export const API_BASE_URL = (import.meta.env.VITE_API_BASE_URL as string | undefined) ?? DEFAULT_API_BASE_URL

const sessionCache = new Map<string, unknown>()

export class ApiError extends Error {
  constructor(message: string, public readonly status?: number) {
    super(message)
    this.name = 'ApiError'
  }
}

export async function apiGet<T>(path: string, options: { refresh?: boolean } = {}): Promise<T> {
  const url = `${API_BASE_URL}${path}`
  if (!options.refresh && sessionCache.has(url)) {
    return sessionCache.get(url) as T
  }
  let response: Response
  try {
    response = await fetch(url, { headers: { Accept: 'application/json' } })
  } catch {
    throw new ApiError('Backend nicht erreichbar')
  }
  if (!response.ok) {
    throw new ApiError(response.status === 404 ? 'Runtime-DB nicht verfügbar' : `Lokale API meldet Fehler ${response.status}`, response.status)
  }
  const data = (await response.json()) as T
  sessionCache.set(url, data)
  return data
}

export async function apiPost<T>(path: string, body: unknown): Promise<T> {
  const url = `${API_BASE_URL}${path}`
  let response: Response
  try {
    response = await fetch(url, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
  } catch {
    throw new ApiError('Backend nicht erreichbar')
  }
  if (!response.ok) {
    throw new ApiError(`Lokale API meldet Fehler ${response.status}`, response.status)
  }
  clearApiCache()
  return (await response.json()) as T
}

export function clearApiCache() {
  sessionCache.clear()
}


Output:
Chunk ID: afb6cc
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 0
Output:


PYTHONDONTWRITEBYTECODE=1 python - <<'PY'
from jarvis_finance.storage.database import connect_memory
from jarvis_finance.storage.migrations import apply_migrations
from jarvis_finance.services import truewealth_service as s
from decimal import Decimal
from hashlib import sha256
import base64, os
from jarvis_finance.imports.truewealth_tax_statement import TrueWealthStatement, TrueWealthPosition, TrueWealthCash
conn=connect_memory(); apply_migrations(conn)
print('schema ok')
conn.execute("INSERT INTO platforms(platform_id,name,platform_type,country,default_currency,is_active,created_at) VALUES('p','TrueWealth','robo','CH','CHF',1,'x')")
conn.execute("INSERT INTO accounts(account_id,platform_id,account_name,account_type,currency,performance_included,is_active,created_at,portfolio_bucket) VALUES('a','p','tw','managed_portfolio','CHF',1,1,'x','truewealth')")
conn.commit()
def st(raw):
    return TrueWealthStatement('2026-07-27','2026-01-01','2026-07-27',Decimal('12000'),(TrueWealthPosition('r1','ETF','CH0000000001',Decimal('1'),'CHF',Decimal('1'),Decimal('5000')),),(TrueWealthCash('c','CHF',Decimal('7000'),None,Decimal('7000')),),sha256(raw).hexdigest(),1)
s.parse_truewealth_tax_statement=st
raw=b'%PDF-x'
req={'account_id':'a','file_name':'x.pdf','content_base64':base64.b64encode(raw).decode()}
p=s.preview_truewealth_import(conn,req); print(p['preview_id'])
# can't archive default? runtime maybe read-only? set to /dev/shm? no? check writable none. skip confirm.
print(s.get_truewealth_summary(conn))
PY


Output:
Chunk ID: 5b4296
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 130
Output:
schema ok
twpreview_4b9bf4c22c9a1e9e8430b87e
{'portfolio_label': 'Freie Anlagen', 'current_value_chf': None, 'current_valuation_at': None, 'current_source_type': 'unavailable', 'current_is_manual_provisional': False, 'current_label': 'Noch keine Bewertung vorhanden', 'positions_snapshot_date': None, 'positions_label': 'Noch keine offiziellen Positionsdetails importiert', 'mixed_as_of': False, 'allocation_performance_usable': False, 'positions': [], 'cash': [], 'history': [], 'imports': [], 'latest_official': None}


grep -R "CREATE TABLE.*account_value_snapshots\|account_value_snapshots" -n src/jarvis_finance/storage/schema.py src/jarvis_finance/storage/migrations.py | head -40


Output:
Chunk ID: d84510
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 192
Output:
src/jarvis_finance/storage/migrations.py:1523:        CREATE TABLE IF NOT EXISTS account_value_snapshots (
src/jarvis_finance/storage/migrations.py:1535:        CREATE INDEX IF NOT EXISTS idx_account_value_snapshots_account_date ON account_value_snapshots(account_id, valuation_date, created_at);
src/jarvis_finance/storage/migrations.py:1931:        "account_value_snapshots",
src/jarvis_finance/storage/migrations.py:2038:            ON account_value_snapshots(source_reference)
src/jarvis_finance/storage/migrations.py:2042:        BEFORE DELETE ON account_value_snapshots
src/jarvis_finance/storage/migrations.py:2046:        BEFORE UPDATE ON account_value_snapshots
src/jarvis_finance/storage/migrations.py:2050:        BEFORE UPDATE ON account_value_snapshots


sed -n '1515,1545p' src/jarvis_finance/storage/migrations.py


Output:
Chunk ID: 26eefc
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 305
Output:
        """
    )
    _add_missing_columns(conn, "crypto_assets", {"binance_symbol": "TEXT", "binance_mapping_status": "TEXT NOT NULL DEFAULT 'missing'"})

def _create_account_value_snapshot_tables(conn: Connection) -> None:
    conn.executescript(
        """
        CREATE TABLE IF NOT EXISTS account_value_snapshots (
            snapshot_id TEXT PRIMARY KEY,
            account_id TEXT NOT NULL REFERENCES accounts(account_id),
            valuation_date TEXT NOT NULL,
            total_value_chf TEXT NOT NULL,
            currency TEXT NOT NULL DEFAULT 'CHF',
            source_type TEXT NOT NULL DEFAULT 'manual_total_value',
            quality_status TEXT NOT NULL DEFAULT 'ok',
            notes TEXT,
            created_at TEXT NOT NULL,
            updated_at TEXT
        );
        CREATE INDEX IF NOT EXISTS idx_account_value_snapshots_account_date ON account_value_snapshots(account_id, valuation_date, created_at);
        """
    )

def _create_cash_account_snapshot_tables(conn: Connection) -> None:
    _add_missing_columns(conn, "accounts", {
        "balance_mode": "TEXT NOT NULL DEFAULT 'manual'",
        "portfolio_bucket": "TEXT NOT NULL DEFAULT 'cash'",
    })
    conn.executescript(


nl -ba frontend/src/pages/TrueWealthPage.vue | sed -n '155,215p'


Output:
Chunk ID: 9a6b81
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 845
Output:
   155	  } finally { loading.value = false }
   156	}
   157
   158	async function runAction(action: () => Promise<void>) {
   159	  actionLoading.value = true; actionError.value = null; message.value = null
   160	  try { await action() } catch (err) { actionError.value = err instanceof Error ? err.message : 'Aktion fehlgeschlagen' } finally { actionLoading.value = false }
   161	}
   162
   163	async function doManualPreview() {
   164	  await runAction(async () => { manualPreview.value = await previewManualTrueWealthValue({ ...manualForm.value, valuation_at: new Date(manualForm.value.valuation_at).toISOString() }) })
   165	}
   166	async function doManualConfirm() {
   167	  if (!manualPreview.value) return
   168	  await runAction(async () => {
   169	    const payload = { ...manualForm.value, valuation_at: new Date(manualForm.value.valuation_at).toISOString() }
   170	    await confirmManualTrueWealthValue({ ...payload, preview_id: manualPreview.value!.preview_id, confirmation_id: manualPreview.value!.confirmation_id, confirm: true })
   171	    message.value = 'Manueller Gesamtwert bestätigt und im Audit protokolliert.'; manualPreview.value = null; await loadData(true)
   172	  })
   173	}
   174
   175	async function fileToBase64(file: File): Promise<string> {
   176	  const bytes = new Uint8Array(await file.arrayBuffer()); let binary = ''
   177	  for (let offset = 0; offset < bytes.length; offset += 0x8000) binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000))
   178	  return btoa(binary)
   179	}
   180	async function onFileSelected(event: Event) {
   181	  actionError.value = null; importPreview.value = null
   182	  const file = (event.target as HTMLInputElement).files?.[0]
   183	  if (!file) { importPayload.value = null; return }
   184	  if (file.type && file.type !== 'application/pdf') { actionError.value = 'Nur PDF-Dateien sind zulässig.'; importPayload.value = null; return }
   185	  importPayload.value = { file_name: file.name, content_base64: await fileToBase64(file) }
   186	}
   187	async function doImportPreview() {
   188	  if (!importPayload.value) return
   189	  await runAction(async () => { importPreview.value = await previewTrueWealthImport(importPayload.value!) })
   190	}
   191	async function doImportConfirm() {
   192	  if (!importPayload.value || !importPreview.value) return
   193	  await runAction(async () => {
   194	    const result = await confirmTrueWealthImport({ ...importPayload.value!, preview_id: importPreview.value!.preview_id, confirmation_id: importPreview.value!.confirmation_id, confirm: true })
   195	    message.value = result.idempotent ? 'Quelle war bereits bestätigt; Idempotenz nachgewiesen.' : 'Offizieller TrueWealth-Import bestätigt und auditierbar gespeichert.'
   196	    importPreview.value = null; await loadData(true)
   197	  })
   198	}
   199	async function toggleManualStatus(item: TrueWealthHistory) {
   200	  const active = !Boolean(item.is_active)
   201	  await runAction(async () => { await setManualTrueWealthValueStatus(item.snapshot_id, active, active ? 'Manuell reaktiviert' : 'Manuell deaktiviert'); message.value = active ? 'Provisorischer Wert reaktiviert.' : 'Provisorischer Wert deaktiviert; Historie bleibt erhalten.'; await loadData(true) })
   202	}
   203
   204	onMounted(() => { void loadData() })
   205	</script>


nl -ba frontend/src/pages/TrueWealthPage.vue | sed -n '135,155p'


Output:
Chunk ID: 5f4c78
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 467
Output:
   135	const showManualForm = ref(false)
   136	const showImportForm = ref(false)
   137	const manualPreview = ref<ManualValuePreview | null>(null)
   138	const importPreview = ref<TrueWealthImportPreview | null>(null)
   139	const importPayload = ref<TrueWealthImportPayload | null>(null)
   140	const message = ref<string | null>(null)
   141	const manualForm = ref({ total_value_chf: '', valuation_at: new Date().toISOString().slice(0, 16), note: '' })
   142
   143	const money = (value: string | number | null | undefined) => value == null ? '—' : new Intl.NumberFormat('de-CH', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(Number(value))
   144	const formatDate = (value: string | null | undefined) => value ? new Intl.DateTimeFormat('de-CH').format(new Date(`${value}T12:00:00`)) : '—'
   145	const formatDateTime = (value: string | null | undefined) => value ? new Intl.DateTimeFormat('de-CH', { dateStyle: 'medium', timeStyle: 'short' }).format(new Date(value)) : '—'
   146	function sourceLabel(type: string) { return ['truewealth_manual_provisional', 'manual_total_value'].includes(type) ? 'Manuell / provisorisch' : type === 'truewealth_official_import' ? 'Offizieller Import' : type }
   147
   148	async function loadData(refresh = false) {
   149	  loading.value = true; error.value = null
   150	  try {
   151	    const [health, runtime, data] = await Promise.all([getHealth(refresh), getRuntimeStatus(refresh), getTrueWealthSummary(refresh)])
   152	    backendOk.value = health.status === 'ok'; runtimeOk.value = runtime.db_available; summary.value = data; lastLoadedAt.value = new Date().toLocaleString('de-CH')
   153	  } catch (err) {
   154	    backendOk.value = false; runtimeOk.value = false; error.value = err instanceof Error ? err.message : 'Backend nicht erreichbar'
   155	  } finally { loading.value = false }


sed -n '200,420p' tests/test_truewealth_sprint12.py


Output:
Chunk ID: 19efe0
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 961
Output:
    assert service.get_truewealth_summary(conn)["current_source_type"] == "truewealth_official_import"

def test_preview_and_get_are_side_effect_free(monkeypatch):
    conn = make_conn()
    raw = b"%PDF-1.7 preview-only"
    monkeypatch.setattr(service, "parse_truewealth_tax_statement", lambda value: statement(value))
    before = conn.total_changes
    service.preview_truewealth_import(conn, import_request(raw))
    service.get_truewealth_summary(conn)
    assert conn.total_changes == before

def test_immutable_source_tables_reject_update_and_delete(monkeypatch, tmp_path):
    conn = make_conn()
    raw = b"%PDF-1.7 immutable"
    monkeypatch.setenv("JARVIS_FINANCE_RUNTIME_DIR", str(tmp_path))
    monkeypatch.setattr(service, "parse_truewealth_tax_statement", lambda value: statement(value))
    result = confirm_import(conn, raw)
    with pytest.raises(Exception, match="immutable"):
        conn.execute("UPDATE truewealth_snapshots SET source_total_chf='0' WHERE snapshot_id=?", (result["snapshot_id"],))
    with pytest.raises(Exception, match="cannot be deleted"):
        conn.execute("DELETE FROM truewealth_snapshots WHERE snapshot_id=?", (result["snapshot_id"],))

def test_confirm_rollback_removes_new_archive(monkeypatch, tmp_path):
    conn = make_conn()
    monkeypatch.setenv("JARVIS_FINANCE_RUNTIME_DIR", str(tmp_path))
    raw = b"%PDF-synthetic-rollback"
    monkeypatch.setattr(service, "parse_truewealth_tax_statement", lambda _raw: statement(raw))
    request = import_request(raw)
    preview = service.preview_truewealth_import(conn, request)
    conn.execute(
        "CREATE TRIGGER force_truewealth_failure BEFORE INSERT ON truewealth_import_batches "
        "BEGIN SELECT RAISE(ABORT, 'forced'); END"
    )
    with pytest.raises(ValueError, match="conflicted"):
        service.confirm_truewealth_import(
            conn,
            {**request, "preview_id": preview["preview_id"], "confirmation_id": preview["confirmation_id"], "confirm": True},
        )
    assert not list((tmp_path / "imports" / "truewealth" / "archive").glob("*.pdf"))
    assert conn.execute("SELECT COUNT(*) FROM truewealth_import_batches").fetchone()[0] == 0

def test_same_date_different_official_source_is_blocked(monkeypatch, tmp_path):
    conn = make_conn()
    monkeypatch.setenv("JARVIS_FINANCE_RUNTIME_DIR", str(tmp_path))
    monkeypatch.setattr(service, "parse_truewealth_tax_statement", lambda value: statement(value))
    confirm_import(conn, b"%PDF-1.7 source-one")
    second = import_request(b"%PDF-1.7 source-two")
    preview = service.preview_truewealth_import(conn, second)
    assert preview["conflict"] is True
    with pytest.raises(ValueError, match="different official TrueWealth source"):
        service.confirm_truewealth_import(
            conn,
            {**second, "preview_id": preview["preview_id"], "confirmation_id": preview["confirmation_id"], "confirm": True},
        )
    assert conn.execute("SELECT COUNT(*) FROM truewealth_import_batches").fetchone()[0] == 1

def test_manual_value_is_not_hard_deleted_or_overwritten():
    conn = make_conn()
    payload = {"total_value_chf": "12500", "valuation_at": "2026-07-28T08:00:00Z", "note": "immutable value"}
    preview = service.preview_manual_truewealth_value(conn, payload)
    result = service.confirm_manual_truewealth_value(
        conn,
        {**payload, "preview_id": preview["preview_id"], "confirmation_id": preview["confirmation_id"], "confirm": True},
    )
    with pytest.raises(Exception, match="fields are immutable"):
        conn.execute("UPDATE account_value_snapshots SET total_value_chf='1' WHERE snapshot_id=?", (result["snapshot_id"],))
    with pytest.raises(Exception, match="cannot be deleted"):
        conn.execute("DELETE FROM account_value_snapshots WHERE snapshot_id=?", (result["snapshot_id"],))


python - <<'PY'
from decimal import Decimal
try:
 print(Decimal('NaN') <= 0)
except Exception as e: print(type(e), e)
PY


Output:
Chunk ID: 5646c3
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 18
Output:
<class 'decimal.InvalidOperation'> [<class 'decimal.InvalidOperation'>]


nl -ba src/jarvis_finance/services/truewealth_service.py | sed -n '395,430p'


Output:
Chunk ID: be2efb
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 455
Output:
   395	                 snapshot_id,account_id,valuation_date,total_value_chf,currency,source_type,quality_status,notes,
   396	                 created_at,valuation_at,source_reference,is_active)
   397	               VALUES(?,?,?,?,'CHF','truewealth_official_import','ok','Official TrueWealth source total',?,?,?,1)""",
   398	            (
   399	                account_snapshot_id,
   400	                account_id,
   401	                statement.statement_date,
   402	                _decimal_text(statement.source_total_chf),
   403	                now,
   404	                None,
   405	                snapshot_id,
   406	            ),
   407	        )
   408	        conn.commit()
   409	    except IntegrityError as exc:
   410	        conn.rollback()
   411	        duplicate = conn.execute(
   412	            "SELECT batch_id,audit_id FROM truewealth_import_batches WHERE file_sha256=?",
   413	            (statement.file_sha256,),
   414	        ).fetchone()
   415	        if duplicate:
   416	            return {"status": "confirmed", "batch_id": duplicate["batch_id"], "audit_id": duplicate["audit_id"], "idempotent": True}
   417	        if archive_created:
   418	            _remove_new_archive(archive_reference)
   419	        raise ValueError("TrueWealth import conflicted with existing immutable data") from exc
   420	    except Exception:
   421	        conn.rollback()
   422	        if archive_created:
   423	            _remove_new_archive(archive_reference)
   424	        raise
   425	    return {"status": "confirmed", "batch_id": batch_id, "snapshot_id": snapshot_id, "audit_id": audit_id, "idempotent": False}
   426
   427
   428	def _parse_valuation_at(value: str) -> datetime:
   429	    try:
   430	        parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))


nl -ba src/jarvis_finance/services/truewealth_service.py | sed -n '438,462p'


Output:
Chunk ID: 22794a
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 324
Output:
   438	def _manual_payload(request: dict[str, Any]) -> dict[str, Any]:
   439	    try:
   440	        value = Decimal(str(request.get("total_value_chf") or ""))
   441	    except InvalidOperation as exc:
   442	        raise ValueError("Manual TrueWealth total is invalid") from exc
   443	    if value <= 0:
   444	        raise ValueError("Manual TrueWealth total must be positive")
   445	    valuation = _parse_valuation_at(str(request.get("valuation_at") or ""))
   446	    note = str(request.get("note") or "").strip()
   447	    if len(note) > 240:
   448	        raise ValueError("Manual TrueWealth note is too long")
   449	    return {
   450	        "total_value_chf": _decimal_text(value),
   451	        "valuation_at": valuation.isoformat().replace("+00:00", "Z"),
   452	        "valuation_date": valuation.date().isoformat(),
   453	        "note": note,
   454	        "source_type": "truewealth_manual_provisional",
   455	    }
   456
   457
   458	def preview_manual_truewealth_value(conn: Connection, request: dict[str, Any]) -> dict[str, Any]:
   459	    account_id = _validate_account(conn)
   460	    payload = _manual_payload(request)
   461	    preview_id, confirmation_id = _preview_identity("manual_value", payload, _revision(conn, account_id))
   462	    return {


[P2] Preserve local time when initializing datetime input

In non-UTC browsers such as Europe/Zurich, `toISOString().slice(0, 16)` puts a UTC clock value into a `datetime-local` field. If the user leaves the default timestamp unchanged, `new Date(manualForm.value.valuation_at).toISOString()` later interprets that displayed value as local time and shifts it by the timezone offset, so the confirmed manual valuation is stored one or two hours earlier than intended.

[P2] Reject non-finite manual totals explicitly

When `/manual-values/preview` or confirm receives a value like `NaN` or `Infinity`, `Decimal(...)` succeeds but the comparison/quantize path raises `decimal.InvalidOperation`, which `_run` does not translate to a 422. Since the schema accepts an arbitrary string here, malformed manual total inputs can surface as a 500 instead of a controlled validation error.

The patch introduces functional issues in the new TrueWealth manual valuation flow: default timestamps are shifted in non-UTC browsers, and malformed numeric input can escape validation as a server error. These should be fixed before considering the change correct.