Output:
Chunk ID: 4eda61
Wall time: 0.0001 seconds
Process exited with code 0
Original token count: 3156
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)
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 })


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


Output:
Chunk ID: de615e
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1553
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
         """,


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


Output:
Chunk ID: 21bf5d
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 4418
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>


grep -R "def apply_migrations" -n src/jarvis_finance/storage/migrations.py && sed -n '2120,2205p' src/jarvis_finance/storage/migrations.py && sed -n '1,140p' src/jarvis_finance/api/dependencies.py


Output:
Chunk ID: 049bd8
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 486
Output:
2131:def apply_migrations(conn: Connection) -> None:
    _create_portfolio_policy_tables(conn)
    _create_portfolio_performance_tables(conn)
    _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)

def apply_migrations(conn: Connection) -> None:
    conn.executescript(INITIAL_SCHEMA_SQL)
    existing_initial = conn.execute("SELECT 1 FROM schema_migrations WHERE version = 1").fetchone()
    if not existing_initial:
        conn.execute(
            "INSERT INTO schema_migrations(version, name, applied_at, checksum) VALUES (?, ?, ?, ?)",
            (1, "001_initial_schema", utc_now(), checksum_sql(INITIAL_SCHEMA_SQL)),
        )
    _apply_compat_migrations(conn)
    existing = conn.execute("SELECT 1 FROM schema_migrations WHERE version = ?", (MIGRATION_VERSION,)).fetchone()
    if not existing:
        conn.execute(
            "INSERT INTO schema_migrations(version, name, applied_at, checksum) VALUES (?, ?, ?, ?)",
            (MIGRATION_VERSION, MIGRATION_NAME, utc_now(), checksum_sql(MIGRATION_NAME)),
        )
    conn.commit()
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()


grep -R "account_value_snapshots" -n src/jarvis_finance/storage/schema.py | head -20 && sed -n '1,220p' tests/test_truewealth_sprint12.py


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

import base64
from decimal import Decimal
from hashlib import sha256

import pytest

from jarvis_finance.imports.truewealth_tax_statement import (
    TrueWealthCash,
    TrueWealthPosition,
    TrueWealthStatement,
    parse_truewealth_text,
)
from jarvis_finance.services import truewealth_service as service
from jarvis_finance.storage.database import connect_memory
from jarvis_finance.storage.migrations import apply_migrations

TEST_ACCOUNT_ID = "account_truewealth_test"

def make_conn():
    conn = connect_memory()
    apply_migrations(conn)
    conn.execute(
        "INSERT INTO platforms(platform_id,name,platform_type,country,default_currency,is_active,created_at) VALUES('p','True Wealth','robo','CH','CHF',1,'2026-01-01')"
    )
    conn.execute(
        """INSERT INTO accounts(account_id,platform_id,account_name,account_type,currency,performance_included,is_active,created_at,portfolio_bucket)
           VALUES(?,'p','Manual Portfolio','managed_portfolio','CHF',1,1,'2026-01-01','truewealth')""",
        (TEST_ACCOUNT_ID,),
    )
    conn.execute(
        """INSERT INTO account_value_snapshots(snapshot_id,account_id,valuation_date,total_value_chf,currency,source_type,quality_status,notes,created_at)
           VALUES('legacy_manual',?,'2026-06-30','10000','CHF','manual_total_value','ok','historical','2026-06-30T12:00:00Z')""",
        (TEST_ACCOUNT_ID,),
    )
    conn.commit()
    return conn

def statement(raw: bytes, date: str = "2026-07-27", total: str = "12000") -> TrueWealthStatement:
    return TrueWealthStatement(
        statement_date=date,
        period_from="2026-01-01",
        period_to=date,
        source_total_chf=Decimal(total),
        positions=(
            TrueWealthPosition("position:CH0000000001", "Synthetic ETF A", "CH0000000001", Decimal("100"), "CHF", Decimal("50"), Decimal("5000")),
            TrueWealthPosition("position:US0000000002", "Synthetic ETF B", "US0000000002", Decimal("20"), "CHF", Decimal("200"), Decimal("4000")),
        ),
        cash=(TrueWealthCash("cash:CHF", "CHF", Decimal("3000"), None, Decimal("3000")),),
        file_sha256=sha256(raw).hexdigest(),
        page_count=7,
    )

def import_request(raw: bytes) -> dict[str, str]:
    return {
        "account_id": TEST_ACCOUNT_ID,
        "file_name": "official.pdf",
        "content_base64": base64.b64encode(raw).decode(),
    }

def confirm_import(conn, raw: bytes):
    request = import_request(raw)
    preview = service.preview_truewealth_import(conn, request)
    return service.confirm_truewealth_import(
        conn,
        {**request, "preview_id": preview["preview_id"], "confirmation_id": preview["confirmation_id"], "confirm": True},
    )

def test_text_parser_extracts_official_rows_without_inventing_values():
    text = """
True Wealth AG
Periode 01.01.2026 - 27.07.2026
Steuerauszug in CHF 27.07.2026
Total Steuerwert der A, B, DA-1 und USA-Werte am 27.07.2026
12’000 0 0 0
CHF (Synthetic Custodian AG)
27.07.2026 Steuerwert / Ertrag CHF 7’000.00 7’000.00 0.00
0000000 CH0000000001 - Synthetic ETF A, CH
27.07.2026 Bestand / Steuerwert / Ertrag 100.00 CHF 50.00 5’000.00 0.00
True Wealth AG
"""
    parsed = parse_truewealth_text(text, file_sha256="synthetic", page_count=1)
    assert parsed.statement_date == "2026-07-27"
    assert parsed.source_total_chf == Decimal("12000")
    assert parsed.positions[0].isin == "CH0000000001"
    assert parsed.cash[0].amount_original == Decimal("7000.00")
    assert parsed.components_total_chf == Decimal("12000.00")

def test_legacy_manual_value_is_never_labelled_as_official():
    conn = make_conn()
    summary = service.get_truewealth_summary(conn)
    assert summary["current_source_type"] == "manual_total_value"
    assert summary["current_is_manual_provisional"] is True
    assert "manuell" in summary["current_label"].lower()
    assert summary["allocation_performance_usable"] is False
    service.set_manual_truewealth_value_active(conn, "legacy_manual", {"active": False, "note": "reversible", "confirm": True})
    disabled = service.get_truewealth_summary(conn)
    assert disabled["current_value_chf"] is None
    assert any(row["snapshot_id"] == "legacy_manual" and row["is_active"] == 0 for row in disabled["history"])

def test_platform_spelling_without_space_and_empty_account_bootstrap(monkeypatch):
    conn = make_conn()
    conn.execute("UPDATE platforms SET name='TrueWealth' WHERE platform_id='p'")
    conn.execute("DELETE FROM account_value_snapshots")
    conn.commit()
    raw = b"%PDF-synthetic-bootstrap"
    monkeypatch.setattr(service, "parse_truewealth_tax_statement", lambda value: statement(value))
    preview = service.preview_truewealth_import(conn, import_request(raw))
    assert preview["reconciliation_status"] == "matched"
    assert service.get_truewealth_summary(conn)["current_value_chf"] is None

def test_official_confirm_is_idempotent_preserves_legacy_and_archives(monkeypatch, tmp_path):
    conn = make_conn()
    raw = b"%PDF-1.7 official-a"
    monkeypatch.setenv("JARVIS_FINANCE_RUNTIME_DIR", str(tmp_path))
    monkeypatch.setattr(service, "parse_truewealth_tax_statement", lambda value: statement(value))

    first = confirm_import(conn, raw)
    second = confirm_import(conn, raw)
    summary = service.get_truewealth_summary(conn)

    assert first["idempotent"] is False
    assert second["idempotent"] is True
    assert second["batch_id"] == first["batch_id"]
    assert summary["current_value_chf"] == "12000.00"
    assert summary["current_source_type"] == "truewealth_official_import"
    assert summary["allocation_performance_usable"] is True
    assert len(summary["positions"]) == 2
    assert conn.execute("SELECT COUNT(*) FROM account_value_snapshots WHERE snapshot_id='legacy_manual'").fetchone()[0] == 1
    assert conn.execute("SELECT COUNT(*) FROM truewealth_import_batches").fetchone()[0] == 1
    archived = tmp_path / "imports" / "truewealth" / "archive" / f"{sha256(raw).hexdigest()}.pdf"
    assert archived.read_bytes() == raw
    assert archived.stat().st_mode & 0o777 == 0o600

def test_newer_manual_value_wins_without_changing_positions_and_can_be_reversed(monkeypatch, tmp_path):
    conn = make_conn()
    raw = b"%PDF-1.7 official-b"
    monkeypatch.setenv("JARVIS_FINANCE_RUNTIME_DIR", str(tmp_path))
    monkeypatch.setattr(service, "parse_truewealth_tax_statement", lambda value: statement(value))
    confirm_import(conn, raw)

    payload = {"total_value_chf": "12500", "valuation_at": "2026-07-28T08:00:00Z", "note": "portal view"}
    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},
    )
    summary = service.get_truewealth_summary(conn)
    assert summary["current_value_chf"] == "12500.00"
    assert summary["current_is_manual_provisional"] is True
    assert summary["positions_snapshot_date"] == "2026-07-27"
    assert summary["mixed_as_of"] is True
    assert summary["allocation_performance_usable"] is False
    assert len(summary["positions"]) == 2

    service.set_manual_truewealth_value_active(conn, result["snapshot_id"], {"active": False, "note": "revert", "confirm": True})
    restored = service.get_truewealth_summary(conn)
    assert restored["current_value_chf"] == "12000.00"
    assert restored["current_source_type"] == "truewealth_official_import"
    assert any(row["snapshot_id"] == result["snapshot_id"] and row["is_active"] == 0 for row in restored["history"])
    service.set_manual_truewealth_value_active(conn, result["snapshot_id"], {"active": True, "note": "reactivate", "confirm": True})
    assert service.get_truewealth_summary(conn)["current_value_chf"] == "12500.00"

def test_older_official_import_cannot_displace_newer_manual(monkeypatch, tmp_path):
    conn = make_conn()
    monkeypatch.setenv("JARVIS_FINANCE_RUNTIME_DIR", str(tmp_path))
    newest_raw = b"%PDF-1.7 official-new"
    old_raw = b"%PDF-1.7 official-old"
    monkeypatch.setattr(service, "parse_truewealth_tax_statement", lambda value: statement(value, "2026-07-27" if value == newest_raw else "2026-07-20"))
    confirm_import(conn, newest_raw)
    payload = {"total_value_chf": "12500", "valuation_at": "2026-07-28T08:00:00Z", "note": "newer manual"}
    preview = service.preview_manual_truewealth_value(conn, payload)
    service.confirm_manual_truewealth_value(conn, {**payload, "preview_id": preview["preview_id"], "confirmation_id": preview["confirmation_id"], "confirm": True})
    confirm_import(conn, old_raw)
    summary = service.get_truewealth_summary(conn)
    assert summary["current_value_chf"] == "12500.00"
    assert summary["positions_snapshot_date"] == "2026-07-27"

def test_same_day_official_has_priority_over_manual(monkeypatch, tmp_path):
    conn = make_conn()
    monkeypatch.setenv("JARVIS_FINANCE_RUNTIME_DIR", str(tmp_path))
    payload = {"total_value_chf": "11500", "valuation_at": "2026-07-27T22:00:00Z", "note": "same day"}
    preview = service.preview_manual_truewealth_value(conn, payload)
    service.confirm_manual_truewealth_value(conn, {**payload, "preview_id": preview["preview_id"], "confirmation_id": preview["confirmation_id"], "confirm": True})
    raw = b"%PDF-1.7 same-day"
    monkeypatch.setattr(service, "parse_truewealth_tax_statement", lambda value: statement(value))
    confirm_import(conn, raw)
    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"],))


sed -n '220,520p' tests/test_truewealth_sprint12.py && grep -n "account_value_snapshots" -A25 -B5 src/jarvis_finance/storage/schema.py


Output:
Chunk ID: 64ffa3
Wall time: 0.0000 seconds
Process exited with code 1
Original token count: 830
Output:
        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_non_finite_values_are_rejected():
    conn = make_conn()
    for value in ("NaN", "Infinity", "-Infinity"):
        with pytest.raises(ValueError, match="positive finite"):
            service.preview_manual_truewealth_value(
                conn,
                {"total_value_chf": value, "valuation_at": "2026-07-28T08:00:00Z", "note": ""},
            )

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"],))


grep -n "CREATE TABLE account_value_snapshots" -A35 src/jarvis_finance/storage/schema.py || grep -n "account_value_snapshots" -A40 src/jarvis_finance/storage/schema.py | head -60


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


grep -R "account_value_snapshots" -n src/jarvis_finance/storage | head -20


Output:
Chunk ID: 597d57
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 238
Output:
grep: src/jarvis_finance/storage/__pycache__/migrations.cpython-312.pyc: binary file matches
grep: src/jarvis_finance/storage/__pycache__/migrations.cpython-311.pyc: binary file matches
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 '1518,1542p' src/jarvis_finance/storage/migrations.py


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

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'",


python - <<'PY'
try:
 import pypdf; print(pypdf.__version__)
except Exception as e: print('ERR', e)
PY


Output:
Chunk ID: e25377
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 7
Output:
ERR No module named 'pypdf'


grep -R "UPDATE account_value_snapshots\|updated_at=.*account_value_snapshots\|updated_at" -n src/jarvis_finance tests | grep account_value -C2 | head -80


Output:
Chunk ID: e7dc22
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2852
Output:
grep: src/jarvis_finance/crypto/__pycache__/wallets.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/crypto/__pycache__/holdings.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/ledger/__pycache__/corrections.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/market/__pycache__/providers.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/imports/__pycache__/execution.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/imports/__pycache__/broker_mapping.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/imports/__pycache__/instrument_candidates.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/storage/__pycache__/migrations.cpython-312.pyc: binary file matches
grep: src/jarvis_finance/storage/__pycache__/schema.cpython-312.pyc: binary file matches
grep: src/jarvis_finance/storage/__pycache__/migrations.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/storage/__pycache__/schema.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/dashboard/__pycache__/data.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/market_data/__pycache__/catalog.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/market_data/__pycache__/instruments.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/market_data/__pycache__/mappings.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/market_data/__pycache__/candidates.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/equity/__pycache__/manage.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/equity/__pycache__/cleanup.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/budget_transactions.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/grocery_price_providers.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/portfolio_data.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/budget_accounts.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/market_service.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/cash_service.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/budget_recurring.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/budget_cleanup.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/equity_service.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/manual_entry_service.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/budget_plans.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/budget_planning.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/truewealth_service.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/budget_imports.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/budget_seed_review.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/budget_monthly_import.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/budget_import_upload.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/truewealth_service.cpython-312.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/grocery_optimizer.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/portfolio_analytics.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/budget_import_status.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/budget_categories.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/transfer_pairing.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/fx/__pycache__/rates.cpython-311.pyc: binary file matches
grep: tests/__pycache__/test_truewealth_sprint12.cpython-311.pyc: binary file matches
grep: tests/__pycache__/test_truewealth_sprint12.cpython-311-pytest-9.0.2.pyc: binary file matches
grep: tests/unit/__pycache__/test_budget_phase18_core_flow.cpython-311-pytest-9.0.2.pyc: binary file matches
grep: tests/unit/__pycache__/test_budget_ux_v2.cpython-311-pytest-9.0.2.pyc: binary file matches
grep: tests/unit/__pycache__/test_portfolio_market_analytics_v1.cpython-311.pyc: binary file matches
grep: tests/unit/__pycache__/test_budget_import_upload_ux_v1.cpython-311-pytest-9.0.2.pyc: binary file matches
grep: tests/unit/__pycache__/test_finance_command_center_monthly_close_v1.cpython-311.pyc: binary file matches
grep: tests/unit/__pycache__/test_portfolio_market_analytics_v1.cpython-311-pytest-9.0.2.pyc: binary file matches
grep: tests/unit/__pycache__/test_postfinance_portfolio_source.cpython-311.pyc: binary file matches
grep: tests/unit/__pycache__/test_budget_import_status_audit_v1.cpython-311-pytest-9.0.2.pyc: binary file matches
grep: tests/unit/__pycache__/test_vue_dashboard_v12_acceptance.cpython-311-pytest-9.0.2.pyc: binary file matches
grep: tests/unit/__pycache__/test_crypto_price_update.cpython-311-pytest-9.0.2.pyc: binary file matches
grep: tests/unit/__pycache__/test_transfer_pairing_v2.cpython-311.pyc: binary file matches
grep: tests/unit/__pycache__/test_budget_import_status_audit_v1.cpython-311.pyc: binary file matches
grep: tests/unit/__pycache__/test_budget_analytics_dashboard_v2.cpython-311.pyc: binary file matches
grep: tests/unit/__pycache__/test_portfolio_data_ingestion_reconciliation.cpython-311.pyc: binary file matches
grep: tests/unit/__pycache__/test_budget_analytics_dashboard_v2.cpython-311-pytest-9.0.2.pyc: binary file matches
grep: tests/unit/__pycache__/test_budget_categories_cleanup_duplicates.cpython-311-pytest-9.0.2.pyc: binary file matches
grep: tests/unit/__pycache__/test_budget_phase110_2026_core.cpython-311-pytest-9.0.2.pyc: binary file matches
grep: tests/unit/__pycache__/test_budget_import_upload_ux_v1.cpython-311.pyc: binary file matches
grep: tests/unit/__pycache__/test_vue_dashboard_v12_acceptance.cpython-311.pyc: binary file matches
grep: tests/unit/__pycache__/test_budget_phase18_core_flow.cpython-311.pyc: binary file matches
grep: tests/unit/__pycache__/test_review_backlog_cleanup_v1.cpython-311.pyc: binary file matches
grep: tests/unit/__pycache__/test_budget_duplicate_handling_hotfix.cpython-311-pytest-9.0.2.pyc: binary file matches
grep: tests/unit/__pycache__/test_coingecko_provider.cpython-311-pytest-9.0.2.pyc: binary file matches
grep: tests/unit/__pycache__/test_crypto_price_update.cpython-311.pyc: binary file matches
grep: tests/unit/__pycache__/test_budget_categories_cleanup_duplicates.cpython-311.pyc: binary file matches
grep: tests/unit/__pycache__/test_portfolio_data_ingestion_reconciliation.cpython-311-pytest-9.0.2.pyc: binary file matches
grep: tests/unit/__pycache__/test_finance_command_center_monthly_close_v1.cpython-311-pytest-9.0.2.pyc: binary file matches
grep: tests/unit/__pycache__/test_budget_phase110_2026_core.cpython-311.pyc: binary file matches
grep: tests/unit/__pycache__/test_transfer_pairing_v2.cpython-311-pytest-9.0.2.pyc: binary file matches
grep: tests/unit/__pycache__/test_budget_ux_v2.cpython-311.pyc: binary file matches
grep: tests/unit/__pycache__/test_reports_v1.cpython-311-pytest-9.0.2.pyc: binary file matches
grep: tests/unit/__pycache__/test_budget_phase12_seed_review.cpython-311-pytest-9.0.2.pyc: binary file matches
grep: tests/unit/__pycache__/test_review_backlog_cleanup_v1.cpython-311-pytest-9.0.2.pyc: binary file matches
grep: tests/unit/__pycache__/test_budget_duplicate_handling_hotfix.cpython-311.pyc: binary file matches
grep: tests/unit/__pycache__/test_reports_v1.cpython-311.pyc: binary file matches
grep: tests/unit/__pycache__/test_portfolio_performance_foundation.cpython-311-pytest-9.0.2.pyc: binary file matches
grep: tests/unit/__pycache__/test_portfolio_performance_foundation.cpython-311.pyc: binary file matches
grep: tests/unit/__pycache__/test_postfinance_portfolio_source.cpython-311-pytest-9.0.2.pyc: binary file matches
grep: tests/unit/__pycache__/test_budget_phase12_seed_review.cpython-311.pyc: binary file matches
grep: tests/unit/__pycache__/test_coingecko_provider.cpython-311.pyc: binary file matches
src/jarvis_finance/services/budget_seed_review.py:429:        conn.execute("UPDATE budget_seed_candidates SET status='confirmed', proposed_category_id=?, updated_at=? WHERE seed_candidate_id=?", (result["entity_id"], ts, item["seed_candidate_id"]))
src/jarvis_finance/services/grocery_price_providers.py:355:                matches.append({"mapping_id": m["mapping_id"], "product_item_id": item["product_item_id"], "retailer": m["target_retailer"], "product_name": m["target_product_name"], "product_url": m["target_product_url"], "price_text": m.get("target_price_text"), "unit_price_text": m.get("target_unit_price_text"), "source_fetched_at": m.get("last_price_checked_at"), "fetched_at": m.get("updated_at"), "quality_flags": json.loads(m.get("quality_flags_json") or "[]"), "confidence": m.get("confidence"), "status": "suggested" if has_sourced_price else "needs_review", "source": "known_mapping", "cache_status": m.get("cache_status")})
src/jarvis_finance/services/truewealth_service.py:540:            """UPDATE account_value_snapshots SET is_active=?,deactivated_at=?,deactivation_reason=?,updated_at=?
src/jarvis_finance/services/cash_service.py:100:                "UPDATE accounts SET platform_id=?, currency='CHF', balance_mode=?, portfolio_bucket='cash', updated_at=? WHERE account_id=?",
src/jarvis_finance/services/transfer_pairing.py:244:            updated_at
--
src/jarvis_finance/fx/rates.py:93:            "UPDATE transactions SET fx_rate_to_chf='1', fx_source='not_needed', fx_status='not_needed', updated_at=? WHERE transaction_id=?",
src/jarvis_finance/fx/rates.py:145:                        "UPDATE transactions SET fx_rate_to_chf='1', fx_source='not_needed', fx_status='not_needed', updated_at=? WHERE transaction_id=?",
tests/test_truewealth_sprint12.py:280:        conn.execute("UPDATE account_value_snapshots SET total_value_chf='1' WHERE snapshot_id=?", (result["snapshot_id"],))
tests/unit/test_postfinance_portfolio_source.py:256:def test_portfolio_read_does_not_refresh_accounts_updated_at() -> None:
tests/unit/test_postfinance_portfolio_source.py:259:    conn.execute("UPDATE accounts SET updated_at='2020-01-01T00:00:00Z'")
--
tests/unit/test_reports_v1.py:20:    conn.execute("""INSERT INTO budget_transactions (budget_transaction_id, account_id, category_id, transaction_date, transaction_type, description, amount_original, currency_original, amount_chf, fx_status, status, source_type, created_at, updated_at)
tests/unit/test_reports_v1.py:27:    conn.execute("""INSERT INTO budget_transaction_candidates (transaction_candidate_id, source_type, source_file_label, transaction_date, description, amount_original, currency_original, status, classification, requires_review, created_at, updated_at)
tests/unit/test_portfolio_data_ingestion_reconciliation.py:63:        """INSERT INTO account_value_snapshots(snapshot_id,account_id,valuation_date,total_value_chf,currency,source_type,quality_status,created_at,updated_at)


sed -n '50,85p' tests/unit/test_portfolio_data_ingestion_reconciliation.py && grep -R "updated_at.*account_value_snapshots" -n tests src/jarvis_finance | grep -v pyc


Output:
Chunk ID: 4ad1c9
Wall time: 0.0000 seconds
Process exited with code 1
Original token count: 518
Output:
    conn.execute(
        """INSERT INTO instruments(instrument_id,asset_class,name,ticker,isin,exchange,currency,is_active,created_at)
           VALUES('i','equity','Synthetic Asset','SYN','CH0000000001','SIX','CHF',1,?)""", (NOW,)
    )
    conn.execute(
        """INSERT INTO instruments(instrument_id,asset_class,name,ticker,isin,exchange,currency,is_active,created_at)
           VALUES('eur','equity','Synthetic EUR Asset','EURX','CH0000000002','SIX','EUR',1,?)""", (NOW,)
    )
    conn.commit()

def add_legacy(conn: sqlite3.Connection, snapshot_id: str, account: str, at: str, value: str, *, created: str = NOW, updated: str | None = None) -> None:
    conn.execute(
        """INSERT INTO account_value_snapshots(snapshot_id,account_id,valuation_date,total_value_chf,currency,source_type,quality_status,created_at,updated_at)
           VALUES(?,?,?,?,'CHF','manual_total_value','ok',?,?)""", (snapshot_id, account, at, value, created, updated)
    )
    conn.commit()

def add_cash(conn: sqlite3.Connection, snapshot_id: str, account: str, at: str, value: str) -> None:
    conn.execute(
        """INSERT INTO cash_account_snapshots(snapshot_id,account_id,snapshot_type,balance_date,amount_original,currency,amount_chf,source,created_at)
           VALUES(?,?, 'reconciliation', ?,?,'CHF',?,'manual',?)""", (snapshot_id, account, at, value, value, NOW)
    )
    conn.commit()

def add_transaction(
    conn: sqlite3.Connection, transaction_id: str, kind: str, account: str, at: str,
    *, instrument: str | None = None, quantity: str | None = None, gross: str | None = None,
    net: str | None = None, row_hash: str | None = None, transfer_group: str | None = None,
) -> None:
    conn.execute(
        """INSERT INTO transactions(transaction_id,transaction_type,activity_kind,account_id,instrument_id,trade_date,
             event_timestamp,quantity,gross_amount_original,net_amount_original,currency_original,fx_rate_to_chf,
             fx_status,source_type,source_id,row_hash,is_confirmed,quality_status,internal_transfer_group_id,created_at)


grep -n "updated=" -n tests/unit/test_portfolio_data_ingestion_reconciliation.py


Output:
Chunk ID: ca07c0
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 19
Output:
142:    add_legacy(conn, "blocked", "b", "2025-12-31", "900", updated=NOW)


sed -n '130,180p' tests/unit/test_portfolio_data_ingestion_reconciliation.py


Output:
Chunk ID: ded2c9
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 735
Output:
    with pytest.raises(sqlite3.DatabaseError, match="audit is immutable"):
        conn.execute("UPDATE audit_log SET action='changed' WHERE entity_id=?", (result["batch_id"],))
    with pytest.raises(sqlite3.DatabaseError, match="audit cannot be deleted"):
        conn.execute("DELETE FROM audit_log WHERE entity_id=?", (result["batch_id"],))
    run_portfolio_market_analytics_v1_scenarios(tmp_path / "market-analytics")
    run_sprint9_fx_fmp_history_contract(tmp_path / "sprint9-history", monkeypatch)

def test_preview_is_storage_free_and_classifies_new_duplicate_blocked_and_unchanged() -> None:
    conn = database()
    add_legacy(conn, "one", "a", "2025-12-31", "1000")
    add_legacy(conn, "duplicate", "a", "2025-12-31", "1000")
    add_legacy(conn, "blocked", "b", "2025-12-31", "900", updated=NOW)
    before = conn.total_changes
    preview = preview_ingestion(conn, request())
    assert conn.total_changes == before
    assert preview["counts"] == {"new": 1, "unchanged": 0, "duplicate": 1, "ambiguous": 0, "blocked": 1, "versioned": 0, "discovered": 3}
    assert preview["quality_impact"]["status"] == "partial"
    assert len(preview["payload_hash"]) == 64
    assert all("/" not in item["source_record_ref"] for item in preview["items"])
    confirmed = confirm_ingestion(conn, confirm_payload(preview))
    assert confirmed["written_records"] == 1
    assert conn.execute("SELECT COUNT(*) FROM portfolio_ingestion_items WHERE batch_id=?", (confirmed["batch_id"],)).fetchone()[0] == 3

def test_confirm_is_atomic_audited_idempotent_and_second_fresh_preview_is_unchanged() -> None:
    conn = database()
    add_legacy(conn, "legacy", "a", "2025-12-31", "1000")
    preview = preview_ingestion(conn, request())
    payload = confirm_payload(preview)
    first = confirm_ingestion(conn, payload)
    second = confirm_ingestion(conn, payload)
    assert first["idempotent"] is False and first["written_records"] == 1
    assert second == {"batch_id": first["batch_id"], "audit_id": first["audit_id"], "idempotent": True}
    assert conn.execute("SELECT COUNT(*) FROM portfolio_valuation_snapshots WHERE source='ingestion:legacy_account_values'").fetchone()[0] == 1
    assert conn.execute("SELECT COUNT(*) FROM audit_log WHERE entity_id=?", (first["batch_id"],)).fetchone()[0] == 1
    fresh = preview_ingestion(conn, request())
    assert fresh["counts"]["unchanged"] == 1 and fresh["counts"]["new"] == 0
    run_postfinance_source_contract()

def test_confirmation_identity_rejects_changed_payload_and_stale_source_revision() -> None:
    conn = database()
    add_legacy(conn, "legacy", "a", "2025-12-31", "1000")
    preview = preview_ingestion(conn, request())
    payload = confirm_payload(preview)
    confirm_ingestion(conn, payload)
    changed = dict(payload)
    changed["period_from"] = "2025-02-01"
    with pytest.raises(ValueError, match="verändertem Inhalt"):
        confirm_ingestion(conn, changed)


nl -ba src/jarvis_finance/services/portfolio_performance.py | sed -n '220,242p'; nl -ba src/jarvis_finance/services/portfolio_data.py | sed -n '1590,1606p'; nl -ba src/jarvis_finance/services/reconciliation_snapshot.py | sed -n '196,208p'


Output:
Chunk ID: a98cb0
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 852
Output:
   220	    canonical_account_days = {
   221	        (item.account_id or item.scope_id, item.valuation_at[:10])
   222	        for item in values
   223	        if item.scope_kind == "account"
   224	    }
   225	    legacy_rows = conn.execute(
   226	        f"""
   227	        WITH ranked AS (
   228	          SELECT s.*,
   229	                 ROW_NUMBER() OVER (
   230	                   PARTITION BY account_id, valuation_date
   231	                   ORDER BY CASE source_type WHEN 'truewealth_official_import' THEN 3 WHEN 'truewealth_manual_provisional' THEN 2 ELSE 1 END DESC,
   232	                            COALESCE(valuation_at,created_at) DESC, created_at DESC, snapshot_id DESC
   233	                 ) AS rn
   234	          FROM account_value_snapshots s
   235	          WHERE account_id IN ({placeholders})
   236	            AND valuation_date BETWEEN ? AND ?
   237	            AND created_at <= ?
   238	            AND COALESCE(is_active,1)=1
   239	            AND source_type <> 'truewealth_manual_provisional'
   240	        )
   241	        SELECT * FROM ranked WHERE rn=1 ORDER BY valuation_date, account_id
   242	        """,
  1590	    account_covered = 0
  1591	    for account, meta in account_map.items():
  1592	        cash = conn.execute(
  1593	            """SELECT amount_chf,balance_date,source FROM cash_account_snapshots WHERE account_id=? AND balance_date<=? AND created_at<=?
  1594	               ORDER BY balance_date DESC,created_at DESC,snapshot_id DESC LIMIT 1""", (account, as_of, cutoff)
  1595	        ).fetchone()
  1596	        cash_value = _stored_decimal(cash["amount_chf"], "Cashbestand") if cash else None
  1597	        cash_date = str(cash["balance_date"]) if cash else None
  1598	        legacy = conn.execute(
  1599	            """SELECT total_value_chf,valuation_date,source_type FROM account_value_snapshots
  1600	               WHERE account_id=? AND valuation_date<=? AND created_at<=? AND COALESCE(is_active,1)=1
  1601	               ORDER BY valuation_date DESC,
  1602	                        CASE source_type WHEN 'truewealth_official_import' THEN 3 WHEN 'truewealth_manual_provisional' THEN 2 ELSE 1 END DESC,
  1603	                        COALESCE(valuation_at,created_at) DESC,created_at DESC,snapshot_id DESC LIMIT 1""", (account, as_of, cutoff)
  1604	        ).fetchone()
  1605	        reported_total = _stored_decimal(legacy["total_value_chf"], "Kontototal") if legacy else None
  1606	        total_date = str(legacy["valuation_date"]) if legacy else None
   196
   197	def list_snapshot_metadata(conn: Connection, *, now: datetime | None = None) -> list[SnapshotMetadata]:
   198	    """Return only each logical series' newest stored metadata, in stable bounded order."""
   199	    snapshots: list[SnapshotMetadata] = []
   200	    valuations = _rows(
   201	        conn,
   202	        """SELECT account_id, valuation_date, created_at, source_type, currency, total_value_chf, quality_status
   203	           FROM account_value_snapshots
   204	           WHERE COALESCE(is_active,1)=1
   205	           ORDER BY account_id, valuation_date DESC,
   206	                    CASE source_type WHEN 'truewealth_official_import' THEN 3 WHEN 'truewealth_manual_provisional' THEN 2 ELSE 1 END DESC,
   207	                    COALESCE(valuation_at,created_at) DESC, created_at DESC, snapshot_id DESC""",
   208	    )


[P2] Preserve tombstone filtering for legacy valuations

When an older `account_value_snapshots` row has been superseded, the existing tombstone marker is `updated_at IS NOT NULL` rather than `is_active = 0`; those rows will have `is_active` NULL after this migration, so `COALESCE(is_active,1)=1` now resurrects blocked/superseded legacy valuations in performance calculations. Please keep the previous `updated_at IS NULL` filter for non-TrueWealth/manual legacy rows while adding the new `is_active` handling.

The change can include previously superseded legacy account valuation snapshots in portfolio performance, changing results for accounts with corrected/import-blocked rows. This is a functional regression even though the new TrueWealth-specific paths are otherwise structured correctly.