Output:
Chunk ID: 9955e9
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2033
Output:
"""Deterministic, in-memory PostFinance ZIP/PDF parser for verified customer exports.

Raw source bytes and filenames are intentionally never logged. The parser keeps source
identity hashes and semantic references so callers can provide Preview -> Confirm -> Audit
without trusting extracted temporary files.
"""

from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal, InvalidOperation
from hashlib import sha256
import io
from pathlib import PurePosixPath
import re
import stat
from zipfile import BadZipFile, ZipFile

from pypdf import PdfReader

PARSER_ID = "postfinance_verified_documents"
PARSER_VERSION = "1.0.0"
MAX_ZIP_BYTES = 50_000_000
MAX_FILES = 100
MAX_UNCOMPRESSED_BYTES = 100_000_000
MAX_PDF_BYTES = 12_000_000
CURRENCIES = {"CHF", "EUR", "USD", "GBP", "JPY", "CAD", "AUD", "NOK", "SEK", "DKK"}

@dataclass(frozen=True)
class PFDocument:
    document_hash: str
    filename_hash: str
    document_type: str
    semantic_reference: str
    semantic_identity: str
    document_date: str | None
    account_reference_hash: str | None
    account_role: str
    page_count: int
    raw: bytes
    text: str

@dataclass(frozen=True)
class PFEvent:
    event_fingerprint: str
    document_hash: str
    semantic_reference: str
    event_type: str
    account_role: str
    occurred_on: str
    settlement_on: str | None
    direction: str | None
    instrument_name: str | None
    isin: str | None
    quantity: Decimal | None
    price_original: Decimal | None
    gross_original: Decimal | None
    fee_original: Decimal
    tax_original: Decimal
    net_original: Decimal | None
    currency: str
    fx_rate_to_chf: Decimal | None
    internal_transfer_group: str | None
    quality_status: str
    reason_codes: tuple[str, ...]

@dataclass(frozen=True)
class PFSnapshotPosition:
    row_reference: str
    source_label: str
    normalized_label: str
    asset_class: str
    quantity: Decimal
    provider_average_cost_original: Decimal | None
    provider_cost_total_original: Decimal | None
    market_price_original: Decimal
    price_currency: str
    market_value_chf: Decimal
    weight_pct: Decimal | None

@dataclass(frozen=True)
class PFSnapshotCash:
    currency: str
    amount_original: Decimal
    fx_rate_to_chf: Decimal
    source_value_chf: Decimal

@dataclass(frozen=True)
class PFSnapshot:
    source_hash: str
    page_count: int
    valuation_at: str
    positions: tuple[PFSnapshotPosition, ...]
    cash: tuple[PFSnapshotCash, ...]
    securities_total_chf: Decimal
    cash_total_chf: Decimal
    total_value_chf: Decimal
    stock_total_chf: Decimal
    etf_total_chf: Decimal
    open_orders: int

@dataclass(frozen=True)
class PFBundle:
    zip_hash: str
    overview_hash: str
    bundle_hash: str
    documents: tuple[PFDocument, ...]
    events: tuple[PFEvent, ...]
    snapshot: PFSnapshot
    transfer_groups: tuple[str, ...]
    reason_codes: tuple[str, ...]

def _hash(*parts: object) -> str:
    return sha256("|".join(str(p) for p in parts).encode()).hexdigest()

def normalize_label(value: str) -> str:
    import unicodedata

    ascii_value = unicodedata.normalize("NFKD", value).encode("ascii", "ignore").decode()
    return re.sub(r"[^a-z0-9]+", " ", ascii_value.lower()).strip()

def _number(value: str | None, *, required: bool = False) -> Decimal | None:
    text = (
        (value or "")
        .strip()
        .replace("\u00a0", "")
        .replace("\u202f", "")
        .replace("’", "'")
        .replace("−", "-")
    )
    text = re.sub(r"(?<=\d)[ '\u2009](?=\d)", "", text)
    if "," in text and "." in text:
        text = (
            text.replace(".", "").replace(",", ".")
            if text.rfind(",") > text.rfind(".")
            else text.replace(",", "")
        )
    elif "," in text:
        text = text.replace(",", ".")
    text = re.sub(r"[^0-9+\-.]", "", text)
    if not text:
        if required:
            raise ValueError("PostFinance source has a missing numeric value")
        return None
    try:
        result = Decimal(text)
    except InvalidOperation as exc:
        raise ValueError("PostFinance source has an invalid numeric value") from exc
    if not result.is_finite():
        raise ValueError("PostFinance source has a non-finite numeric value")
    return result

def _date(value: str | None) -> str | None:
    if not value:
        return None
    text = value.strip()
    for fmt in ("%d.%m.%Y", "%d/%m/%Y", "%Y%m%d", "%Y-%m-%d"):
        try:
            return datetime.strptime(text, fmt).date().isoformat()
        except ValueError:
            continue
    return None

def _pdf_text(raw: bytes) -> tuple[str, int]:
    if not raw or len(raw) > MAX_PDF_BYTES or not raw.startswith(b"%PDF"):
        raise ValueError("PostFinance PDF is empty, too large, or invalid")
    try:
        # PostFinance-generated PDFs contain harmless duplicate dictionary entries;
        # strict=False accepts those bytes while all container/size/encryption gates remain enforced.
        reader = PdfReader(io.BytesIO(raw), strict=False)
        if reader.is_encrypted:
            raise ValueError("Encrypted PostFinance 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("PostFinance PDF cannot be parsed") from exc
    if not pages or any(not page.strip() for page in pages):
        raise ValueError("PostFinance PDF has a page without selectable text")
    return "\n".join(pages), len(pages)

def _classify(name: str, text: str) -> str:
    key = normalize_label(PurePosixPath(name).name)
    tests = (
        ("account_statement", "kontoauszug"),
        ("trade_confirmation", "borsenabrechnung"),
        ("corporate_action", "corporate action abrechnung"),
        ("transfer_confirmation", "transferabrechnung"),
        ("custody_fee", "depotgebuhr"),
        ("interest_statement", "zinsabrechnung"),
        ("portfolio_performance", "portfolio wertentwicklung"),
    )
    for kind, marker in tests:
        if marker in key:
            return kind
    low = normalize_label(text[:2500])
    if "transaktionsbeleg" in low and "borsentransaktion" in low:
        return "trade_confirmation"
    if "transaktionsbeleg" in low and "dividende" in low:
        return "corporate_action"
    raise ValueError("Unsupported document in PostFinance bundle")

def _field(text: str, label: str) -> str | None:
    match = re.search(label + r"\s*:?\s*([^\n]+)", text, flags=re.I)
    return match.group(1).strip() if match else None

def _account_reference(text: str) -> str | None:
    match = re.search(
        r"(?:Kontonummer|Konto-Nr\.|Konto)\s*:?[ ]*([0-9][0-9 .-]{4,})", text, flags=re.I
    )
    return re.sub(r"\D", "", match.group(1)) if match else None

def _reference(name: str, text: str) -> str:
    match = re.search(r"Unsere Referenz\s*:\s*([0-9]+)", text, flags=re.I)
    if not match:
        match = re.search(r"_(\d{8,})_\d{8}(?:\(\d+\))?\.pdf$", name, flags=re.I)
    if match:
        return match.group(1)
    period = re.search(
        r"(?:vom|von)\s+(\d{1,2}[./]\d{1,2}[./]\d{4}).{0,40}(?:bis|per)\s+(\d{1,2}[./]\d{1,2}[./]\d{4})",
        text,
        flags=re.I | re.S,
    )
    return _hash("period", *(period.groups() if period else (sha256(text.encode()).hexdigest(),)))[
        :24
    ]

def _document_date(name: str, text: str) -> str | None:
    match = re.search(r"_(20\d{6})(?:\(\d+\))?\.pdf$", name, flags=re.I)
    if match:
        return _date(match.group(1))
    match = re.search(
        r"(?:Ausführungsdatum|Valutadatum|Stand)\s*:?\s*(\d{1,2}[./]\d{1,2}[./]\d{4})",
        text,
        flags=re.I,
    )
    return _date(match.group(1)) if match else None

def _currency_amount(text: str, label: str) -> tuple[str, Decimal] | None:
    match = re.search(label + r"[^\n]*?\b([A-Z]{3})\s+([+\-−]?[0-9][0-9'’., ]*)", text, flags=re.I)
    if not match or match.group(1).upper() not in CURRENCIES:
        return None


The service now performs preview duplicate checks against immutable batch identity, semantic document identity, and same-day canonical projections. I’m checking for any gap where canonical rows are inserted without the protective marker or where FIFO/account-role behavior diverges from the tests.


sed -n '620,980p' src/jarvis_finance/services/postfinance_service.py


rg -n "CREATE TABLE IF NOT EXISTS (transactions|cash_balances|positions_snapshot|account_value_snapshots)|source_type|source_reference" src/jarvis_finance/storage/migrations.py src/jarvis_finance/storage/*.py


sed -n '1,220p' src/jarvis_finance/storage/migrations.py


Output:
Chunk ID: 099605
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 3850
Output:
            instrument_id = event_instruments[event.event_fingerprint]
            event_id = f"pfevent_{event.event_fingerprint[:24]}"
            account_id = accounts[event.account_role]
            conn.execute(
                """INSERT INTO postfinance_ledger_events(event_id,event_fingerprint,document_hash,semantic_reference_hash,event_type,account_role,account_id,occurred_on,settlement_on,direction,instrument_id,quantity,price_original,gross_original,fee_original,tax_original,net_original,currency,fx_rate_to_chf,internal_transfer_group,quality_status,reason_codes_json,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
                (
                    event_id,
                    event.event_fingerprint,
                    event.document_hash,
                    _hash(event.semantic_reference),
                    event.event_type,
                    event.account_role,
                    account_id,
                    event.occurred_on,
                    event.settlement_on,
                    event.direction or "neutral",
                    instrument_id,
                    _text(event.quantity),
                    _text(event.price_original),
                    _text(event.gross_original),
                    _text(event.fee_original) or "0",
                    _text(event.tax_original) or "0",
                    _text(event.net_original),
                    event.currency,
                    _text(event.fx_rate_to_chf),
                    event.internal_transfer_group,
                    event.quality_status,
                    json.dumps(event.reason_codes),
                    now,
                ),
            )
            for kind, amount in (
                ("gross", event.gross_original),
                ("fee", event.fee_original),
                ("tax", event.tax_original),
                ("net", event.net_original),
            ):
                if amount is not None and event.currency:
                    conn.execute(
                        "INSERT INTO postfinance_event_components(component_id,event_id,component_type,amount_original,currency,provenance_document_hash,created_at) VALUES(?,?,?,?,?,?,?)",
                        (
                            _id("pfcomponent"),
                            event_id,
                            kind,
                            _text(amount),
                            event.currency,
                            event.document_hash,
                            now,
                        ),
                    )
            _canonical_transaction(conn, event_id, event, account_id, instrument_id, now)
        lots: dict[str, list[dict[str, Any]]] = defaultdict(list)
        for event in sorted(
            bundle.events,
            key=lambda item: (
                item.occurred_on,
                0 if item.event_type == "buy" else 1,
                item.event_fingerprint,
            ),
        ):
            instrument_id = event_instruments[event.event_fingerprint]
            if (
                event.event_type == "buy"
                and instrument_id
                and event.quantity
                and event.price_original
                and event.currency
            ):
                lots[instrument_id].append({"event": event, "remaining": event.quantity})
            elif event.event_type == "sell" and instrument_id and event.quantity:
                to_consume = event.quantity
                for lot in lots[instrument_id]:
                    if to_consume <= 0:
                        break
                    consumed = min(lot["remaining"], to_consume)
                    lot["remaining"] -= consumed
                    to_consume -= consumed
        for instrument_id, instrument_lots in lots.items():
            for lot in instrument_lots:
                event = lot["event"]
                conn.execute(
                    """INSERT INTO postfinance_cost_basis_lots(lot_id,event_id,instrument_id,acquired_on,quantity_acquired,quantity_remaining,unit_cost_original,fees_original,currency,method,provenance_document_hash,created_at) VALUES(?,?,?,?,?,?,?,?,?,'documented_trade_fifo',?,?)""",
                    (
                        _id("pflot"),
                        f"pfevent_{event.event_fingerprint[:24]}",
                        instrument_id,
                        event.occurred_on,
                        _text(event.quantity),
                        _text(lot["remaining"]),
                        _text(event.price_original),
                        _text(event.fee_original) or "0",
                        event.currency,
                        event.document_hash,
                        now,
                    ),
                )
        for position in snapshot.positions:
            instrument_id = position_map[position.row_reference]
            matched_lots = lots.get(instrument_id, [])
            quantity = sum((lot["remaining"] for lot in matched_lots), Decimal("0"))
            complete = (
                bool(matched_lots)
                and quantity == position.quantity
                and len({lot["event"].currency for lot in matched_lots}) == 1
            )
            calculated = (
                sum(
                    (
                        lot["remaining"] * (lot["event"].price_original or 0)
                        + (lot["event"].fee_original + lot["event"].tax_original)
                        * (lot["remaining"] / lot["event"].quantity)
                        for lot in matched_lots
                        if lot["event"].quantity
                    ),
                    Decimal("0"),
                )
                if complete
                else None
            )
            status = "complete" if complete else "partial" if matched_lots else "unavailable"
            conn.execute(
                """INSERT INTO postfinance_snapshot_positions(snapshot_position_id,snapshot_id,source_row_reference,instrument_id,asset_class,quantity,provider_average_cost_original,provider_cost_total_original,price_original,price_currency,value_chf,weight_pct,computed_cost_basis_original,computed_cost_basis_status,provenance_json,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
                (
                    _id("pfposition"),
                    snapshot_id,
                    position.row_reference,
                    instrument_id,
                    position.asset_class,
                    _text(position.quantity),
                    _text(position.provider_average_cost_original),
                    _text(position.provider_cost_total_original),
                    _text(position.market_price_original),
                    position.price_currency,
                    _money(position.market_value_chf),
                    _text(position.weight_pct),
                    _text(calculated),
                    status,
                    json.dumps(
                        {
                            "provider": "official_account_overview",
                            "calculated": "documented_trade_fifo",
                            "estimated": False,
                        },
                        sort_keys=True,
                    ),
                    now,
                ),
            )
            conn.execute(
                """INSERT INTO positions_snapshot(position_snapshot_id,snapshot_date,account_id,platform_id,instrument_id,quantity,average_cost_original,cost_basis_original,market_price_original,market_value_original,market_fx_rate_to_chf,market_value_chf,portfolio_weight_pct,category,data_quality_status,source_type,source_reference,created_at) SELECT ?,?,?,a.platform_id,?,?,?,?,?,?,?,?,?,?,?,?,?,? FROM accounts a WHERE a.account_id=?""",
                (
                    _id("position"),
                    snapshot.valuation_at[:10],
                    accounts["etrading_depot"],
                    instrument_id,
                    _text(position.quantity),
                    _text(position.provider_average_cost_original),
                    _text(position.provider_cost_total_original),
                    _text(position.market_price_original),
                    _text(position.quantity * position.market_price_original),
                    None,
                    _money(position.market_value_chf),
                    _text(position.weight_pct),
                    position.asset_class,
                    "verified",
                    "postfinance_official_import",
                    snapshot_id,
                    now,
                    accounts["etrading_depot"],
                ),
            )
        for cash in snapshot.cash:
            conn.execute(
                "INSERT INTO postfinance_snapshot_cash(snapshot_cash_id,snapshot_id,currency,amount_original,fx_rate_to_chf,value_chf,created_at) VALUES(?,?,?,?,?,?,?)",
                (
                    _id("pfcash"),
                    snapshot_id,
                    cash.currency,
                    _text(cash.amount_original),
                    _text(cash.fx_rate_to_chf),
                    _money(cash.source_value_chf),
                    now,
                ),
            )
            conn.execute(
                "INSERT INTO cash_balances(cash_balance_id,account_id,balance_date,currency,amount_original,fx_rate_to_chf,amount_chf,source_type,quality_status,notes,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)",
                (
                    _id("cash"),
                    accounts["etrading_cash"],
                    snapshot.valuation_at[:10],
                    cash.currency,
                    _text(cash.amount_original),
                    _text(cash.fx_rate_to_chf),
                    _money(cash.source_value_chf),
                    "postfinance_official_import",
                    "verified",
                    "Official PostFinance account overview",
                    now,
                ),
            )
        conn.execute(
            "INSERT INTO account_value_snapshots(snapshot_id,account_id,valuation_date,total_value_chf,currency,source_type,quality_status,notes,created_at,valuation_at,source_reference,is_active) VALUES(?,?,?,?,'CHF','postfinance_official_import','ok','Official PostFinance account overview',?,?,?,1)",
            (
                _id("acctval"),
                accounts["etrading_depot"],
                snapshot.valuation_at[:10],
                _money(snapshot.total_value_chf),
                now,
                snapshot.valuation_at,
                snapshot_id,
            ),
        )
        conn.commit()
    except IntegrityError as exc:
        conn.rollback()
        duplicate = conn.execute(
            "SELECT batch_id,audit_id FROM postfinance_import_batches WHERE bundle_sha256=?",
            (bundle.bundle_hash,),
        ).fetchone()
        if duplicate:
            return {
                "status": "confirmed",
                "batch_id": duplicate["batch_id"],
                "audit_id": duplicate["audit_id"],
                "idempotent": True,
            }
        for ref, created in archive_refs:
            if created:
                _remove_archive(ref)
        raise ValueError("PostFinance import conflicted with immutable data") from exc
    except Exception:
        conn.rollback()
        for ref, created in archive_refs:
            if created:
                _remove_archive(ref)
        raise
    return {
        "status": "confirmed",
        "batch_id": batch_id,
        "snapshot_id": snapshot_id,
        "audit_id": audit_id,
        "idempotent": False,
    }

def get_postfinance_summary(conn: Connection) -> dict[str, Any]:
    latest = conn.execute(
        """SELECT snapshot_id,snapshot_at,total_chf,securities_chf,cash_chf,stocks_chf,etfs_chf,
                  position_count,cash_count,open_orders,difference_chf,reconciliation_status,created_at
           FROM postfinance_snapshots ORDER BY snapshot_at DESC,created_at DESC LIMIT 1"""
    ).fetchone()
    if not latest:
        return {
            "latest_snapshot": None,
            "positions": [],
            "cash": [],
            "event_counts": {},
            "imports": [],
        }
    positions = [
        {
            "row_key": _hash("position", row["instrument_name"], row["quantity"])[:16],
            "instrument_name": row["instrument_name"],
            "ticker": row["ticker"],
            "asset_class": row["asset_class"],
            "quantity": row["quantity"],
            "provider_average_cost_original": row["provider_average_cost_original"],
            "provider_cost_total_original": row["provider_cost_total_original"],
            "price_original": row["price_original"],
            "price_currency": row["price_currency"],
            "value_chf": row["value_chf"],
            "weight_pct": row["weight_pct"],
            "computed_cost_basis_original": row["computed_cost_basis_original"],
            "computed_cost_basis_status": row["computed_cost_basis_status"],
        }
        for row in conn.execute(
            """SELECT p.asset_class,p.quantity,p.provider_average_cost_original,
                      p.provider_cost_total_original,p.price_original,p.price_currency,p.value_chf,
                      p.weight_pct,p.computed_cost_basis_original,p.computed_cost_basis_status,
                      i.name AS instrument_name,i.ticker
               FROM postfinance_snapshot_positions p
               JOIN instruments i ON i.instrument_id=p.instrument_id
               WHERE p.snapshot_id=? ORDER BY CAST(p.value_chf AS REAL) DESC""",
            (latest["snapshot_id"],),
        ).fetchall()
    ]
    cash = [
        {
            "row_key": _hash("cash", row["currency"])[:16],
            "currency": row["currency"],
            "amount_original": row["amount_original"],
            "fx_rate_to_chf": row["fx_rate_to_chf"],
            "value_chf": row["value_chf"],
        }
        for row in conn.execute(
            """SELECT currency,amount_original,fx_rate_to_chf,value_chf
               FROM postfinance_snapshot_cash WHERE snapshot_id=? ORDER BY currency""",
            (latest["snapshot_id"],),
        ).fetchall()
    ]
    events = {
        str(row["event_type"]): int(row["c"])
        for row in conn.execute(
            "SELECT event_type,COUNT(*) c FROM postfinance_ledger_events GROUP BY event_type ORDER BY event_type"
        ).fetchall()
    }
    imports = [
        dict(row)
        for row in conn.execute(
            """SELECT b.snapshot_at,b.parser_id,b.parser_version,b.confirmed_at,b.status,
                      s.total_chf,s.securities_chf,s.cash_chf,s.position_count,s.cash_count,
                      s.difference_chf,s.reconciliation_status,
                      (SELECT COUNT(*) FROM postfinance_batch_documents d WHERE d.batch_id=b.batch_id) AS document_count
               FROM postfinance_import_batches b JOIN postfinance_snapshots s ON s.batch_id=b.batch_id
               ORDER BY b.snapshot_at DESC,b.confirmed_at DESC"""
        ).fetchall()
    ]
    safe_latest = {key: latest[key] for key in latest.keys() if key != "snapshot_id"}
    return {
        "latest_snapshot": safe_latest,
        "positions": positions,
        "cash": cash,
        "event_counts": events,
        "imports": imports,
    }


Output:
Chunk ID: 6521a2
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2233
Output:
tests/test_postfinance_sprint13.py:56:            "INSERT INTO positions_snapshot(position_snapshot_id,snapshot_date,account_id,platform_id,instrument_id,quantity,average_cost_original,cost_basis_original,market_price_original,market_value_original,market_fx_rate_to_chf,market_value_chf,category,data_quality_status,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
tests/test_postfinance_sprint13.py:259:    safe_summary = service.get_postfinance_summary(conn)
tests/test_postfinance_sprint13.py:260:    serialized = str(safe_summary)
tests/test_postfinance_sprint13.py:262:    assert "snapshot_id" not in safe_summary["latest_snapshot"]
tests/test_postfinance_sprint13.py:263:    assert "instrument_id" not in safe_summary["positions"][0]
tests/test_postfinance_sprint13.py:264:    assert "batch_id" not in safe_summary["imports"][0]
tests/test_postfinance_sprint13.py:287:            "SELECT activity_kind FROM transactions WHERE source_type='postfinance_official_import'"
tests/test_postfinance_sprint13.py:317:    assert conn.execute("SELECT COUNT(*) FROM postfinance_cost_basis_lots").fetchone()[0] == 1
tests/test_postfinance_sprint13.py:320:def test_sell_consumes_documented_fifo_lot_before_cost_basis_persistence(monkeypatch, tmp_path):
tests/test_postfinance_sprint13.py:330:    sell = replace(
tests/test_postfinance_sprint13.py:332:        event_fingerprint=sha256(b"sell-event").hexdigest(),
tests/test_postfinance_sprint13.py:333:        event_type="sell",
tests/test_postfinance_sprint13.py:342:    fifo_bundle = replace(original, events=(buy, sell, *original.events[1:]))
tests/test_postfinance_sprint13.py:356:        "SELECT quantity_acquired,quantity_remaining FROM postfinance_cost_basis_lots"
tests/test_postfinance_sprint13.py:403:        """INSERT INTO positions_snapshot(position_snapshot_id,snapshot_date,account_id,platform_id,
tests/test_postfinance_sprint13.py:455:        "positions_snapshot": "source_type='postfinance_official_import'",
tests/test_postfinance_sprint13.py:456:        "cash_balances": "source_type='postfinance_official_import'",
tests/test_postfinance_sprint13.py:457:        "transactions": "source_type='postfinance_official_import'",
src/jarvis_finance/storage/postfinance_schema.py:8:    position_columns = {row[1] for row in conn.execute("PRAGMA table_info(positions_snapshot)")}
src/jarvis_finance/storage/postfinance_schema.py:9:    if "source_type" not in position_columns:
src/jarvis_finance/storage/postfinance_schema.py:10:        conn.execute("ALTER TABLE positions_snapshot ADD COLUMN source_type TEXT")
src/jarvis_finance/storage/postfinance_schema.py:11:    if "source_reference" not in position_columns:
src/jarvis_finance/storage/postfinance_schema.py:12:        conn.execute("ALTER TABLE positions_snapshot ADD COLUMN source_reference TEXT")
src/jarvis_finance/storage/postfinance_schema.py:18:            source_reference_hash TEXT,
src/jarvis_finance/storage/postfinance_schema.py:110:            event_type TEXT NOT NULL CHECK(event_type IN ('buy','sell','dividend','interest','fee','internal_transfer','fx','split','corporate_action')),
src/jarvis_finance/storage/postfinance_schema.py:142:        CREATE TABLE IF NOT EXISTS postfinance_cost_basis_lots (
src/jarvis_finance/storage/postfinance_schema.py:148:            quantity_remaining TEXT NOT NULL,
src/jarvis_finance/storage/postfinance_schema.py:157:            ON account_value_snapshots(source_reference)
src/jarvis_finance/storage/postfinance_schema.py:158:            WHERE source_type='postfinance_official_import' AND source_reference IS NOT NULL;
src/jarvis_finance/storage/postfinance_schema.py:192:        CREATE TRIGGER IF NOT EXISTS postfinance_cost_basis_immutable BEFORE UPDATE ON postfinance_cost_basis_lots
src/jarvis_finance/storage/postfinance_schema.py:194:        CREATE TRIGGER IF NOT EXISTS postfinance_cost_basis_no_delete BEFORE DELETE ON postfinance_cost_basis_lots
src/jarvis_finance/storage/postfinance_schema.py:196:        CREATE TRIGGER IF NOT EXISTS postfinance_official_account_values_immutable BEFORE UPDATE ON account_value_snapshots
src/jarvis_finance/storage/postfinance_schema.py:197:        WHEN OLD.source_type='postfinance_official_import'
src/jarvis_finance/storage/postfinance_schema.py:199:        CREATE TRIGGER IF NOT EXISTS postfinance_official_account_values_no_delete BEFORE DELETE ON account_value_snapshots
src/jarvis_finance/storage/postfinance_schema.py:200:        WHEN OLD.source_type='postfinance_official_import'
src/jarvis_finance/storage/postfinance_schema.py:202:        CREATE TRIGGER IF NOT EXISTS postfinance_official_positions_immutable BEFORE UPDATE ON positions_snapshot
src/jarvis_finance/storage/postfinance_schema.py:203:        WHEN OLD.source_type='postfinance_official_import'
src/jarvis_finance/storage/postfinance_schema.py:205:        CREATE TRIGGER IF NOT EXISTS postfinance_official_positions_no_delete BEFORE DELETE ON positions_snapshot
src/jarvis_finance/storage/postfinance_schema.py:206:        WHEN OLD.source_type='postfinance_official_import'
src/jarvis_finance/storage/postfinance_schema.py:208:        CREATE TRIGGER IF NOT EXISTS postfinance_official_cash_immutable BEFORE UPDATE ON cash_balances
src/jarvis_finance/storage/postfinance_schema.py:209:        WHEN OLD.source_type='postfinance_official_import'
src/jarvis_finance/storage/postfinance_schema.py:211:        CREATE TRIGGER IF NOT EXISTS postfinance_official_cash_no_delete BEFORE DELETE ON cash_balances
src/jarvis_finance/storage/postfinance_schema.py:212:        WHEN OLD.source_type='postfinance_official_import'
src/jarvis_finance/storage/postfinance_schema.py:215:        WHEN OLD.source_type='postfinance_official_import'
src/jarvis_finance/storage/postfinance_schema.py:218:        WHEN OLD.source_type='postfinance_official_import'
src/jarvis_finance/services/postfinance_service.py:159:        "account_value_snapshots",
src/jarvis_finance/services/postfinance_service.py:160:        "positions_snapshot",
src/jarvis_finance/services/postfinance_service.py:161:        "cash_balances",
src/jarvis_finance/services/postfinance_service.py:183:           FROM positions_snapshot p JOIN instruments i ON i.instrument_id=p.instrument_id
src/jarvis_finance/services/postfinance_service.py:323:        "SELECT COUNT(*) FROM positions_snapshot WHERE account_id=? AND snapshot_date=?",
src/jarvis_finance/services/postfinance_service.py:327:        """SELECT COUNT(*) FROM cash_balances
src/jarvis_finance/services/postfinance_service.py:328:           WHERE account_id=? AND balance_date=? AND source_type='postfinance_official_import'""",
src/jarvis_finance/services/postfinance_service.py:421:        "sell": "sell",
src/jarvis_finance/services/postfinance_service.py:438:        "fx_rate_to_chf,fx_source,fx_status,gross_amount_chf,fee_chf,tax_chf,net_amount_chf,source_type,source_id,"
src/jarvis_finance/services/postfinance_service.py:440:        "activity_kind,booking_date,base_currency,internal_transfer_group_id,source_reference"
src/jarvis_finance/services/postfinance_service.py:537:                "INSERT OR IGNORE INTO postfinance_account_roles(role,account_id,source_reference_hash,created_at) VALUES(?,?,NULL,?)",
src/jarvis_finance/services/postfinance_service.py:689:            elif event.event_type == "sell" and instrument_id and event.quantity:
src/jarvis_finance/services/postfinance_service.py:701:                    """INSERT INTO postfinance_cost_basis_lots(lot_id,event_id,instrument_id,acquired_on,quantity_acquired,quantity_remaining,unit_cost_original,fees_original,currency,method,provenance_document_hash,created_at) VALUES(?,?,?,?,?,?,?,?,?,'documented_trade_fifo',?,?)""",
src/jarvis_finance/services/postfinance_service.py:769:                """INSERT INTO positions_snapshot(position_snapshot_id,snapshot_date,account_id,platform_id,instrument_id,quantity,average_cost_original,cost_basis_original,market_price_original,market_value_original,market_fx_rate_to_chf,market_value_chf,portfolio_weight_pct,category,data_quality_status,source_type,source_reference,created_at) SELECT ?,?,?,a.platform_id,?,?,?,?,?,?,?,?,?,?,?,?,?,? FROM accounts a WHERE a.account_id=?""",
src/jarvis_finance/services/postfinance_service.py:805:                "INSERT INTO cash_balances(cash_balance_id,account_id,balance_date,currency,amount_original,fx_rate_to_chf,amount_chf,source_type,quality_status,notes,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)",
src/jarvis_finance/services/postfinance_service.py:821:            "INSERT INTO account_value_snapshots(snapshot_id,account_id,valuation_date,total_value_chf,currency,source_type,quality_status,notes,created_at,valuation_at,source_reference,is_active) VALUES(?,?,?,?,'CHF','postfinance_official_import','ok','Official PostFinance account overview',?,?,?,1)",
src/jarvis_finance/services/postfinance_service.py:865:def get_postfinance_summary(conn: Connection) -> dict[str, Any]:


Output:
Chunk ID: c416aa
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 5421
Output:
src/jarvis_finance/storage/schema.py:1:INITIAL_SCHEMA_SQL = "\nCREATE TABLE IF NOT EXISTS schema_migrations (\n    version INTEGER PRIMARY KEY,\n    name TEXT NOT NULL,\n    applied_at TEXT NOT NULL,\n    checksum TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS platforms (\n    platform_id TEXT PRIMARY KEY,\n    name TEXT NOT NULL UNIQUE,\n    platform_type TEXT NOT NULL,\n    country TEXT,\n    default_currency TEXT NOT NULL DEFAULT 'CHF',\n    is_active INTEGER NOT NULL DEFAULT 1,\n    notes TEXT,\n    created_at TEXT NOT NULL,\n    updated_at TEXT\n);\n\nCREATE TABLE IF NOT EXISTS accounts (\n    account_id TEXT PRIMARY KEY,\n    platform_id TEXT NOT NULL REFERENCES platforms(platform_id),\n    account_name TEXT NOT NULL,\n    account_type TEXT NOT NULL,\n    currency TEXT NOT NULL DEFAULT 'CHF',\n    performance_included INTEGER NOT NULL DEFAULT 1,\n    is_health_reserve INTEGER NOT NULL DEFAULT 0,\n    target_cash_min_chf NUMERIC,\n    target_cash_max_chf NUMERIC,\n    is_active INTEGER NOT NULL DEFAULT 1,\n    notes TEXT,\n    created_at TEXT NOT NULL,\n    updated_at TEXT,\n    UNIQUE(platform_id, account_name)\n);\n\nCREATE TABLE IF NOT EXISTS instruments (\n    instrument_id TEXT PRIMARY KEY,\n    asset_class TEXT NOT NULL,\n    name TEXT NOT NULL,\n    ticker TEXT,\n    isin TEXT,\n    exchange TEXT,\n    currency TEXT NOT NULL,\n    country TEXT,\n    sector TEXT,\n    industry TEXT,\n    provider_symbol TEXT,\n    data_provider_primary TEXT,\n    position_category TEXT,\n    ter TEXT,\n    distribution_policy TEXT,\n    index_name TEXT,\n    fund_domicile TEXT,\n    benchmark TEXT,\n    data_provider_fallback TEXT,\n    is_active INTEGER NOT NULL DEFAULT 1,\n    notes TEXT,\n    created_at TEXT NOT NULL,\n    updated_at TEXT\n);\n\nCREATE TABLE IF NOT EXISTS transactions (\n    transaction_id TEXT PRIMARY KEY,\n    transaction_type TEXT NOT NULL,\n    account_id TEXT NOT NULL REFERENCES accounts(account_id),\n    instrument_id TEXT REFERENCES instruments(instrument_id),\n    trade_date TEXT NOT NULL,\n    settlement_date TEXT,\n    quantity TEXT,\n    price_original TEXT,\n    gross_amount_original TEXT,\n    fee_original TEXT DEFAULT '0',\n    tax_original TEXT DEFAULT '0',\n    net_amount_original TEXT,\n    currency_original TEXT NOT NULL,\n    fx_rate_to_chf TEXT,\n    fx_source TEXT,\n    fx_status TEXT NOT NULL DEFAULT 'ok',\n    gross_amount_chf TEXT,\n    fee_chf TEXT,\n    tax_chf TEXT,\n    net_amount_chf TEXT,\n    source_type TEXT NOT NULL,\n    source_id TEXT,\n    external_transaction_id TEXT,\n    row_hash TEXT,\n    is_confirmed INTEGER NOT NULL DEFAULT 1,\n    quality_status TEXT NOT NULL DEFAULT 'ok',\n    notes TEXT,\n    created_at TEXT NOT NULL,\n    updated_at TEXT\n);\n\nCREATE TABLE IF NOT EXISTS positions_snapshot (\n    position_snapshot_id TEXT PRIMARY KEY,\n    snapshot_date TEXT NOT NULL,\n    account_id TEXT NOT NULL REFERENCES accounts(account_id),\n    platform_id TEXT NOT NULL REFERENCES platforms(platform_id),\n    instrument_id TEXT NOT NULL REFERENCES instruments(instrument_id),\n    quantity NUMERIC NOT NULL,\n    average_cost_original NUMERIC,\n    cost_basis_original NUMERIC,\n    cost_basis_chf NUMERIC,\n    market_price_original NUMERIC,\n    market_value_original NUMERIC,\n    market_fx_rate_to_chf NUMERIC,\n    market_value_chf NUMERIC,\n    unrealized_price_pnl_chf NUMERIC,\n    unrealized_fx_pnl_chf NUMERIC,\n    realized_pnl_chf NUMERIC,\n    income_chf NUMERIC,\n    fees_chf NUMERIC,\n    taxes_chf NUMERIC,\n    total_return_chf NUMERIC,\n    portfolio_weight_pct NUMERIC,\n    category TEXT,\n    data_quality_status TEXT NOT NULL DEFAULT 'ok',\n    created_at TEXT NOT NULL,\n    UNIQUE(snapshot_date, account_id, instrument_id)\n);\n\nCREATE TABLE IF NOT EXISTS cash_balances (\n    cash_balance_id TEXT PRIMARY KEY,\n    account_id TEXT NOT NULL REFERENCES accounts(account_id),\n    balance_date TEXT NOT NULL,\n    currency TEXT NOT NULL,\n    amount_original NUMERIC NOT NULL,\n    fx_rate_to_chf NUMERIC,\n    amount_chf NUMERIC,\n    source_type TEXT NOT NULL,\n    quality_status TEXT NOT NULL DEFAULT 'ok',\n    notes TEXT,\n    created_at TEXT NOT NULL,\n    UNIQUE(account_id, balance_date, currency, source_type)\n);\n\nCREATE TABLE IF NOT EXISTS fx_rates (\n    fx_rate_id TEXT PRIMARY KEY,\n    base_currency TEXT NOT NULL,\n    quote_currency TEXT NOT NULL DEFAULT 'CHF',\n    rate_date TEXT NOT NULL,\n    rate_timestamp TEXT,\n    rate NUMERIC NOT NULL,\n    provider TEXT NOT NULL,\n    rate_type TEXT NOT NULL,\n    quality_status TEXT NOT NULL DEFAULT 'ok',\n    created_at TEXT NOT NULL,\n    UNIQUE(base_currency, quote_currency, rate_date, provider, rate_type)\n);\n\nCREATE TABLE IF NOT EXISTS market_prices (\n    market_price_id TEXT PRIMARY KEY,\n    instrument_id TEXT NOT NULL REFERENCES instruments(instrument_id),\n    price_date TEXT NOT NULL,\n    price_timestamp TEXT,\n    open NUMERIC,\n    high NUMERIC,\n    low NUMERIC,\n    close NUMERIC NOT NULL,\n    adjusted_close NUMERIC,\n    currency TEXT NOT NULL,\n    provider TEXT NOT NULL,\n    provider_symbol TEXT,\n    quality_status TEXT NOT NULL DEFAULT 'ok',\n    created_at TEXT NOT NULL,\n    UNIQUE(instrument_id, price_date, provider)\n);\n\nCREATE TABLE IF NOT EXISTS crypto_wallets (\n    wallet_id TEXT PRIMARY KEY,\n    wallet_name TEXT NOT NULL UNIQUE,\n    wallet_type TEXT NOT NULL,\n    platform_provider TEXT,\n    network_chain TEXT,\n    wallet_address TEXT,\n    owner TEXT,\n    is_active INTEGER NOT NULL DEFAULT 1,\n    last_verified_at TEXT,\n    notes TEXT,\n    created_at TEXT NOT NULL,\n    updated_at TEXT\n);\n\nCREATE TABLE IF NOT EXISTS crypto_assets (\n    asset_id TEXT PRIMARY KEY,\n    coin_name TEXT NOT NULL,\n    symbol TEXT NOT NULL,\n    coingecko_id TEXT UNIQUE,\n    network_chain_default TEXT,\n    is_stablecoin INTEGER NOT NULL DEFAULT 0,\n    price_provider_primary TEXT NOT NULL DEFAULT 'CoinGecko',\n    price_provider_fallback TEXT,\n    is_active INTEGER NOT NULL DEFAULT 1,\n    notes TEXT,\n    created_at TEXT NOT NULL,\n    updated_at TEXT\n);\n\nCREATE TABLE IF NOT EXISTS crypto_holdings (\n    crypto_holding_id TEXT PRIMARY KEY,\n    asset_id TEXT NOT NULL REFERENCES crypto_assets(asset_id),\n    wallet_id TEXT NOT NULL REFERENCES crypto_wallets(wallet_id),\n    quantity TEXT NOT NULL,\n    acquisition_source TEXT,\n    last_verified_at TEXT,\n    verification_status TEXT NOT NULL DEFAULT 'unverified',\n    legacy_snapshot_value_original TEXT,\n    legacy_snapshot_value_chf TEXT,\n    legacy_snapshot_currency TEXT,\n    legacy_snapshot_date TEXT,\n    notes TEXT,\n    created_at TEXT NOT NULL,\n    updated_at TEXT,\n    UNIQUE(asset_id, wallet_id)\n);\n\nCREATE TABLE IF NOT EXISTS crypto_transactions (\n    crypto_transaction_id TEXT PRIMARY KEY,\n    transaction_id TEXT REFERENCES transactions(transaction_id),\n    transaction_type TEXT NOT NULL,\n    asset_id TEXT NOT NULL REFERENCES crypto_assets(asset_id),\n    quantity TEXT NOT NULL,\n    price_original TEXT,\n    currency_original TEXT,\n    gross_amount_original TEXT,\n    fee_quantity TEXT,\n    fee_original TEXT,\n    fee_currency TEXT,\n    fx_rate_to_chf TEXT,\n    fx_source TEXT,\n    amount_chf TEXT,\n    from_wallet_id TEXT REFERENCES crypto_wallets(wallet_id),\n    to_wallet_id TEXT REFERENCES crypto_wallets(wallet_id),\n    transaction_datetime TEXT NOT NULL,\n    tx_hash TEXT,\n    source TEXT NOT NULL,\n    confirmation_status TEXT NOT NULL DEFAULT 'confirmed',\n    parse_confidence NUMERIC,\n    original_input_text TEXT,\n    notes TEXT,\n    created_at TEXT NOT NULL,\n    updated_at TEXT\n);\n\nCREATE TABLE IF NOT EXISTS crypto_prices (\n    crypto_price_id TEXT PRIMARY KEY,\n    asset_id TEXT NOT NULL REFERENCES crypto_assets(asset_id),\n    coingecko_id TEXT,\n    price_currency TEXT NOT NULL,\n    price TEXT NOT NULL,\n    provider TEXT NOT NULL DEFAULT 'CoinGecko',\n    provider_timestamp TEXT,\n    fetched_at TEXT NOT NULL,\n    quality_status TEXT NOT NULL DEFAULT 'fresh',\n    error_message TEXT\n);\n\nCREATE TABLE IF NOT EXISTS watchlist (\n    watchlist_id TEXT PRIMARY KEY,\n    instrument_id TEXT REFERENCES instruments(instrument_id),\n    crypto_asset_id TEXT REFERENCES crypto_assets(asset_id),\n    name TEXT NOT NULL,\n    asset_class TEXT NOT NULL,\n    reason TEXT NOT NULL,\n    target_entry_price NUMERIC,\n    target_entry_currency TEXT,\n    desired_position_size_chf NUMERIC,\n    desired_weight_pct NUMERIC,\n    trigger_rules TEXT,\n    risk_notes TEXT,\n    investment_case TEXT,\n    bear_case TEXT,\n    sources TEXT,\n    status TEXT NOT NULL DEFAULT 'active',\n    next_review_date TEXT,\n    created_at TEXT NOT NULL,\n    updated_at TEXT\n);\n\nCREATE TABLE IF NOT EXISTS reports (\n    report_id TEXT PRIMARY KEY,\n    report_type TEXT NOT NULL,\n    title TEXT NOT NULL,\n    period_start TEXT,\n    period_end TEXT,\n    generated_at TEXT NOT NULL,\n    file_path TEXT,\n    format TEXT NOT NULL,\n    data_quality_status TEXT NOT NULL DEFAULT 'ok',\n    summary_json TEXT,\n    created_at TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS alerts (\n    alert_id TEXT PRIMARY KEY,\n    priority TEXT NOT NULL,\n    category TEXT NOT NULL,\n    entity_type TEXT,\n    entity_id TEXT,\n    rule_id TEXT,\n    message TEXT NOT NULL,\n    evidence_json TEXT,\n    status TEXT NOT NULL DEFAULT 'active',\n    created_at TEXT NOT NULL,\n    resolved_at TEXT,\n    muted_until TEXT,\n    last_seen_at TEXT,\n    occurrence_count INTEGER NOT NULL DEFAULT 1,\n    fingerprint TEXT,\n    dedup_key TEXT\n);\n\nCREATE INDEX IF NOT EXISTS idx_alerts_dedup ON alerts(dedup_key, status);\n\nCREATE TABLE IF NOT EXISTS audit_log (\n    audit_id TEXT PRIMARY KEY,\n    timestamp TEXT NOT NULL,\n    source TEXT NOT NULL,\n    action TEXT NOT NULL,\n    entity_type TEXT NOT NULL,\n    entity_id TEXT NOT NULL,\n    old_values_json TEXT,\n    new_values_json TEXT,\n    user_text_note TEXT,\n    original_input_text TEXT,\n    confirmed INTEGER NOT NULL DEFAULT 1,\n    confirmation_timestamp TEXT,\n    auto_parsed INTEGER NOT NULL DEFAULT 0,\n    parse_confidence NUMERIC,\n    created_by TEXT NOT NULL DEFAULT 'system',\n    quality_status TEXT NOT NULL DEFAULT 'ok',\n    created_at TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS import_sessions (\n    import_session_id TEXT PRIMARY KEY,\n    import_type TEXT NOT NULL,\n    source_filename TEXT NOT NULL,\n    source_hash TEXT,\n    started_at TEXT NOT NULL,\n    finished_at TEXT,\n    status TEXT NOT NULL,\n    rows_total INTEGER DEFAULT 0,\n    rows_imported INTEGER DEFAULT 0,\n    rows_failed INTEGER DEFAULT 0,\n    errors_json TEXT,\n    notes TEXT\n);\n\nCREATE TABLE IF NOT EXISTS decision_journal (\n    decision_id TEXT PRIMARY KEY,\n    decision_date TEXT NOT NULL,\n    decision_type TEXT NOT NULL,\n    entity_type TEXT,\n    entity_id TEXT,\n    system_recommendation TEXT,\n    human_decision TEXT NOT NULL,\n    rationale TEXT NOT NULL,\n    investment_case TEXT,\n    risks TEXT,\n    exit_rule TEXT,\n    alternatives_considered TEXT,\n    sources TEXT,\n    review_date TEXT,\n    outcome_status TEXT,\n    outcome_return_chf NUMERIC,\n    created_at TEXT NOT NULL,\n    updated_at TEXT\n);\n\nCREATE INDEX IF NOT EXISTS idx_accounts_platform_id ON accounts(platform_id);\nCREATE INDEX IF NOT EXISTS idx_transactions_account_date ON transactions(account_id, trade_date);\nCREATE INDEX IF NOT EXISTS idx_transactions_instrument_date ON transactions(instrument_id, trade_date);\nCREATE INDEX IF NOT EXISTS idx_transactions_type ON transactions(transaction_type);\nCREATE UNIQUE INDEX IF NOT EXISTS idx_transactions_external_id ON transactions(external_transaction_id) WHERE external_transaction_id IS NOT NULL AND external_transaction_id != '';\nCREATE UNIQUE INDEX IF NOT EXISTS idx_transactions_row_hash ON transactions(row_hash) WHERE row_hash IS NOT NULL AND row_hash != '';\nCREATE INDEX IF NOT EXISTS idx_crypto_transactions_asset_datetime ON crypto_transactions(asset_id, transaction_datetime);\nCREATE INDEX IF NOT EXISTS idx_alerts_priority_status ON alerts(priority, status);\nCREATE INDEX IF NOT EXISTS idx_audit_entity ON audit_log(entity_type, entity_id);\n"
src/jarvis_finance/storage/postfinance_schema.py:9:    if "source_type" not in position_columns:
src/jarvis_finance/storage/postfinance_schema.py:10:        conn.execute("ALTER TABLE positions_snapshot ADD COLUMN source_type TEXT")
src/jarvis_finance/storage/postfinance_schema.py:11:    if "source_reference" not in position_columns:
src/jarvis_finance/storage/postfinance_schema.py:12:        conn.execute("ALTER TABLE positions_snapshot ADD COLUMN source_reference TEXT")
src/jarvis_finance/storage/postfinance_schema.py:18:            source_reference_hash TEXT,
src/jarvis_finance/storage/postfinance_schema.py:157:            ON account_value_snapshots(source_reference)
src/jarvis_finance/storage/postfinance_schema.py:158:            WHERE source_type='postfinance_official_import' AND source_reference IS NOT NULL;
src/jarvis_finance/storage/postfinance_schema.py:197:        WHEN OLD.source_type='postfinance_official_import'
src/jarvis_finance/storage/postfinance_schema.py:200:        WHEN OLD.source_type='postfinance_official_import'
src/jarvis_finance/storage/postfinance_schema.py:203:        WHEN OLD.source_type='postfinance_official_import'
src/jarvis_finance/storage/postfinance_schema.py:206:        WHEN OLD.source_type='postfinance_official_import'
src/jarvis_finance/storage/postfinance_schema.py:209:        WHEN OLD.source_type='postfinance_official_import'
src/jarvis_finance/storage/postfinance_schema.py:212:        WHEN OLD.source_type='postfinance_official_import'
src/jarvis_finance/storage/postfinance_schema.py:215:        WHEN OLD.source_type='postfinance_official_import'
src/jarvis_finance/storage/postfinance_schema.py:218:        WHEN OLD.source_type='postfinance_official_import'
src/jarvis_finance/storage/migrations.py:583:            source_type TEXT NOT NULL CHECK(source_type IN ('manual','import_candidate_later','system')),
src/jarvis_finance/storage/migrations.py:652:            source_type TEXT NOT NULL DEFAULT 'manual' CHECK(source_type IN ('manual','excel_seed_dry_run','system')),
src/jarvis_finance/storage/migrations.py:746:            source_type TEXT NOT NULL DEFAULT 'csv_seed',
src/jarvis_finance/storage/migrations.py:782:                source_type TEXT NOT NULL DEFAULT 'csv_seed',
src/jarvis_finance/storage/migrations.py:808:                transaction_candidate_id, source_file_label, source_row_or_range, source_type,
src/jarvis_finance/storage/migrations.py:813:            SELECT transaction_candidate_id, source_file_label, source_row_or_range, source_type,
src/jarvis_finance/storage/migrations.py:873:            source_type TEXT,
src/jarvis_finance/storage/migrations.py:883:        CREATE INDEX IF NOT EXISTS idx_budget_import_rules_active ON budget_import_rules(is_active, source_type);
src/jarvis_finance/storage/migrations.py:899:    if "source_type IN ('manual','import_candidate_later','system')" in sql:
src/jarvis_finance/storage/migrations.py:919:                source_type TEXT NOT NULL CHECK(source_type IN ('manual','import_candidate','import_candidate_later','system')),
src/jarvis_finance/storage/migrations.py:929:                amount_chf, fx_status, category_id, status, source_type, notes, created_at, updated_at,
src/jarvis_finance/storage/migrations.py:934:                amount_chf, fx_status, category_id, status, source_type, notes, created_at, updated_at,
src/jarvis_finance/storage/migrations.py:950:                source_type TEXT NOT NULL DEFAULT 'csv_seed',
src/jarvis_finance/storage/migrations.py:980:                transaction_candidate_id, source_file_label, source_row_or_range, source_type,
src/jarvis_finance/storage/migrations.py:988:            SELECT transaction_candidate_id, source_file_label, source_row_or_range, source_type,
src/jarvis_finance/storage/migrations.py:1087:            source_type TEXT,
src/jarvis_finance/storage/migrations.py:1094:        CREATE INDEX IF NOT EXISTS idx_budget_review_rules_active ON budget_review_rules(is_active, source_type);
src/jarvis_finance/storage/migrations.py:1133:            source_type TEXT,
src/jarvis_finance/storage/migrations.py:1315:            source_type TEXT NOT NULL DEFAULT 'migros_receipt',
src/jarvis_finance/storage/migrations.py:1524:        CREATE TABLE IF NOT EXISTS account_value_snapshots (
src/jarvis_finance/storage/migrations.py:1530:            source_type TEXT NOT NULL DEFAULT 'manual_total_value',
src/jarvis_finance/storage/migrations.py:1699:            "source_reference": "TEXT",
src/jarvis_finance/storage/migrations.py:1730:            source_reference TEXT,
src/jarvis_finance/storage/migrations.py:1935:            "source_reference": "TEXT",
src/jarvis_finance/storage/migrations.py:1946:            source_reference_hash TEXT NOT NULL,
src/jarvis_finance/storage/migrations.py:2039:            ON account_value_snapshots(source_reference)
src/jarvis_finance/storage/migrations.py:2040:            WHERE source_type='truewealth_official_import' AND source_reference IS NOT NULL;
src/jarvis_finance/storage/migrations.py:2044:        WHEN OLD.source_type IN ('truewealth_official_import','truewealth_manual_provisional')
src/jarvis_finance/storage/migrations.py:2048:        WHEN OLD.source_type='truewealth_official_import'
src/jarvis_finance/storage/migrations.py:2052:        WHEN OLD.source_type='truewealth_manual_provisional' AND (
src/jarvis_finance/storage/migrations.py:2055:            NEW.currency IS NOT OLD.currency OR NEW.source_type IS NOT OLD.source_type OR
src/jarvis_finance/storage/migrations.py:2058:            NEW.source_reference IS NOT OLD.source_reference
src/jarvis_finance/storage/migrations.py:583:            source_type TEXT NOT NULL CHECK(source_type IN ('manual','import_candidate_later','system')),
src/jarvis_finance/storage/migrations.py:652:            source_type TEXT NOT NULL DEFAULT 'manual' CHECK(source_type IN ('manual','excel_seed_dry_run','system')),
src/jarvis_finance/storage/migrations.py:746:            source_type TEXT NOT NULL DEFAULT 'csv_seed',
src/jarvis_finance/storage/migrations.py:782:                source_type TEXT NOT NULL DEFAULT 'csv_seed',
src/jarvis_finance/storage/migrations.py:808:                transaction_candidate_id, source_file_label, source_row_or_range, source_type,
src/jarvis_finance/storage/migrations.py:813:            SELECT transaction_candidate_id, source_file_label, source_row_or_range, source_type,
src/jarvis_finance/storage/migrations.py:873:            source_type TEXT,
src/jarvis_finance/storage/migrations.py:883:        CREATE INDEX IF NOT EXISTS idx_budget_import_rules_active ON budget_import_rules(is_active, source_type);
src/jarvis_finance/storage/migrations.py:899:    if "source_type IN ('manual','import_candidate_later','system')" in sql:
src/jarvis_finance/storage/migrations.py:919:                source_type TEXT NOT NULL CHECK(source_type IN ('manual','import_candidate','import_candidate_later','system')),
src/jarvis_finance/storage/migrations.py:929:                amount_chf, fx_status, category_id, status, source_type, notes, created_at, updated_at,
src/jarvis_finance/storage/migrations.py:934:                amount_chf, fx_status, category_id, status, source_type, notes, created_at, updated_at,
src/jarvis_finance/storage/migrations.py:950:                source_type TEXT NOT NULL DEFAULT 'csv_seed',
src/jarvis_finance/storage/migrations.py:980:                transaction_candidate_id, source_file_label, source_row_or_range, source_type,
src/jarvis_finance/storage/migrations.py:988:            SELECT transaction_candidate_id, source_file_label, source_row_or_range, source_type,
src/jarvis_finance/storage/migrations.py:1087:            source_type TEXT,
src/jarvis_finance/storage/migrations.py:1094:        CREATE INDEX IF NOT EXISTS idx_budget_review_rules_active ON budget_review_rules(is_active, source_type);
src/jarvis_finance/storage/migrations.py:1133:            source_type TEXT,
src/jarvis_finance/storage/migrations.py:1315:            source_type TEXT NOT NULL DEFAULT 'migros_receipt',
src/jarvis_finance/storage/migrations.py:1524:        CREATE TABLE IF NOT EXISTS account_value_snapshots (
src/jarvis_finance/storage/migrations.py:1530:            source_type TEXT NOT NULL DEFAULT 'manual_total_value',
src/jarvis_finance/storage/migrations.py:1699:            "source_reference": "TEXT",
src/jarvis_finance/storage/migrations.py:1730:            source_reference TEXT,
src/jarvis_finance/storage/migrations.py:1935:            "source_reference": "TEXT",
src/jarvis_finance/storage/migrations.py:1946:            source_reference_hash TEXT NOT NULL,
src/jarvis_finance/storage/migrations.py:2039:            ON account_value_snapshots(source_reference)
src/jarvis_finance/storage/migrations.py:2040:            WHERE source_type='truewealth_official_import' AND source_reference IS NOT NULL;
src/jarvis_finance/storage/migrations.py:2044:        WHEN OLD.source_type IN ('truewealth_official_import','truewealth_manual_provisional')
src/jarvis_finance/storage/migrations.py:2048:        WHEN OLD.source_type='truewealth_official_import'
src/jarvis_finance/storage/migrations.py:2052:        WHEN OLD.source_type='truewealth_manual_provisional' AND (
src/jarvis_finance/storage/migrations.py:2055:            NEW.currency IS NOT OLD.currency OR NEW.source_type IS NOT OLD.source_type OR
src/jarvis_finance/storage/migrations.py:2058:            NEW.source_reference IS NOT OLD.source_reference


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

import hashlib
from datetime import datetime, timezone
from sqlite3 import Connection

from .schema import INITIAL_SCHEMA_SQL
from .postfinance_schema import create_postfinance_ledger_import_v1

MIGRATION_VERSION = 45
MIGRATION_NAME = "045_postfinance_ledger_import_v1"

INSTRUMENT_OPTIONAL_COLUMNS = {
    "position_category": "TEXT",
    "ter": "TEXT",
    "distribution_policy": "TEXT",
    "index_name": "TEXT",
    "fund_domicile": "TEXT",
    "benchmark": "TEXT",
    "is_currency_hedged": "INTEGER NOT NULL DEFAULT 0",
    "hedged_to_currency": "TEXT",
    "hedge_status": "TEXT NOT NULL DEFAULT 'unknown'",
    "base_exposure_currency": "TEXT",
    "trading_currency": "TEXT",
    "instrument_status": "TEXT NOT NULL DEFAULT 'unknown'",
    "valuation_policy": "TEXT NOT NULL DEFAULT 'live_price'",
    "corporate_action_status": "TEXT NOT NULL DEFAULT 'not_checked'",
    "split_or_corporate_action_review_required": "INTEGER NOT NULL DEFAULT 0",
}

CATALOG_OPTIONAL_COLUMNS = {
    "last_price": "TEXT",
    "price_currency": "TEXT",
    "price_date": "TEXT",
    "price_source": "TEXT",
    "exchange_name": "TEXT",
    "mic": "TEXT",
    "security_type": "TEXT",
}

TEXT_AFFINITY_COLUMNS = {
    "transactions": {"quantity", "price_original", "gross_amount_original", "fee_original", "tax_original", "net_amount_original", "fx_rate_to_chf", "gross_amount_chf", "fee_chf", "tax_chf", "net_amount_chf"},
    "crypto_holdings": {"quantity", "legacy_snapshot_value_original", "legacy_snapshot_value_chf"},
    "crypto_transactions": {"quantity", "price_original", "gross_amount_original", "fee_quantity", "fee_original", "fx_rate_to_chf", "amount_chf"},
    "crypto_prices": {"price", "market_cap", "volume_24h", "change_24h_pct"},
    "fx_rates": {"rate"},
    "market_prices": {"open", "high", "low", "close", "adjusted_close"},
    "equity_price_points": {"price"},
    "equity_intraday_candles": {"open", "close", "low", "high", "volume"},
    "crypto_price_points": {"price"},
}

def utc_now() -> str:
    return datetime.now(timezone.utc).isoformat()

def checksum_sql(sql: str) -> str:
    return hashlib.sha256(sql.encode("utf-8")).hexdigest()

def get_schema_version(conn: Connection) -> int:
    row = conn.execute("SELECT MAX(version) AS version FROM schema_migrations").fetchone()
    return int(row["version"] or 0) if row else 0

def _table_columns(conn: Connection, table: str) -> dict[str, str]:
    return {row["name"]: (row["type"] or "") for row in conn.execute(f"PRAGMA table_info({table})").fetchall()}

def _add_missing_instrument_columns(conn: Connection) -> None:
    existing = _table_columns(conn, "instruments")
    for name, col_type in INSTRUMENT_OPTIONAL_COLUMNS.items():
        if name not in existing:
            conn.execute(f"ALTER TABLE instruments ADD COLUMN {name} {col_type}")

def _rebuild_table_with_text_columns(conn: Connection, table: str, text_columns: set[str]) -> None:
    cols = conn.execute(f"PRAGMA table_info({table})").fetchall()
    if not cols:
        return
    needs_rebuild = any(row["name"] in text_columns and (row["type"] or "").upper() != "TEXT" for row in cols)
    if not needs_rebuild:
        return
    tmp = f"{table}__text_migration"
    col_defs: list[str] = []
    pk_cols = [row["name"] for row in cols if row["pk"]]
    for row in cols:
        name = row["name"]
        col_type = "TEXT" if name in text_columns else (row["type"] or "TEXT")
        parts = [name, col_type]
        if row["pk"] and len(pk_cols) == 1:
            parts.append("PRIMARY KEY")
        if row["notnull"]:
            parts.append("NOT NULL")
        if row["dflt_value"] is not None:
            parts.append(f"DEFAULT {row['dflt_value']}")
        col_defs.append(" ".join(parts))
    if len(pk_cols) > 1:
        col_defs.append("PRIMARY KEY (" + ", ".join(pk_cols) + ")")
    conn.execute(f"DROP TABLE IF EXISTS {tmp}")
    conn.execute(f"CREATE TABLE {tmp} (" + ", ".join(col_defs) + ")")
    names = [row["name"] for row in cols]
    select_exprs = [f"CAST({name} AS TEXT)" if name in text_columns else name for name in names]
    conn.execute(f"INSERT INTO {tmp} ({', '.join(names)}) SELECT {', '.join(select_exprs)} FROM {table}")
    conn.execute(f"DROP TABLE {table}")
    conn.execute(f"ALTER TABLE {tmp} RENAME TO {table}")
    if table == "crypto_holdings":
        conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_crypto_holdings_asset_wallet ON crypto_holdings(asset_id, wallet_id)")

def _add_missing_columns(conn: Connection, table: str, columns: dict[str, str]) -> None:
    existing = _table_columns(conn, table)
    for name, col_type in columns.items():
        if name not in existing:
            conn.execute(f"ALTER TABLE {table} ADD COLUMN {name} {col_type}")

def _create_broker_bank_mapping_tables(conn: Connection) -> None:
    conn.executescript(
        """
        CREATE TABLE IF NOT EXISTS instrument_mappings (
            mapping_id TEXT PRIMARY KEY,
            source_name TEXT,
            source_platform TEXT NOT NULL,
            source_account TEXT,
            source_label TEXT NOT NULL,
            normalized_name TEXT NOT NULL,
            isin TEXT,
            ticker TEXT,
            exchange TEXT,
            currency TEXT,
            asset_class TEXT NOT NULL DEFAULT 'other',
            source_instrument_name TEXT,
            source_isin TEXT,
            source_ticker TEXT,
            source_exchange TEXT,
            source_currency TEXT,
            instrument_id TEXT REFERENCES instruments(instrument_id),
            mapping_status TEXT NOT NULL DEFAULT 'needs_manual_review',
            confidence TEXT NOT NULL DEFAULT '0',
            quality_flags_json TEXT,
            notes TEXT,
            created_at TEXT NOT NULL,
            updated_at TEXT
        );

        CREATE TABLE IF NOT EXISTS platform_account_mappings (
            mapping_id TEXT PRIMARY KEY,
            source_name TEXT,
            source_platform TEXT NOT NULL,
            source_platform_label TEXT,
            source_account_label TEXT NOT NULL,
            normalized_platform TEXT NOT NULL,
            normalized_account_name TEXT NOT NULL,
            source_account_type TEXT,
            account_type TEXT NOT NULL DEFAULT 'other',
            source_currency TEXT,
            currency TEXT,
            platform_id TEXT REFERENCES platforms(platform_id),
            account_id TEXT REFERENCES accounts(account_id),
            internal_platform_id TEXT REFERENCES platforms(platform_id),
            internal_account_id TEXT REFERENCES accounts(account_id),
            mapping_status TEXT NOT NULL DEFAULT 'needs_manual_review',
            quality_flags_json TEXT,
            notes TEXT,
            created_at TEXT NOT NULL,
            updated_at TEXT
        );

        CREATE TABLE IF NOT EXISTS broker_import_dry_runs (
            dry_run_id TEXT PRIMARY KEY,
            source_name TEXT,
            source_platform TEXT NOT NULL,
            source_file_label TEXT,
            source_file_type TEXT NOT NULL,
            source_hash TEXT,
            source_filename_hash TEXT,
            detected_snapshot_date TEXT,
            snapshot_date_status TEXT NOT NULL DEFAULT 'missing',
            detected_sections_json TEXT NOT NULL,
            field_coverage_json TEXT,
            rows_total INTEGER NOT NULL DEFAULT 0,
            rows_position_candidates INTEGER NOT NULL DEFAULT 0,
            rows_cash_candidates INTEGER NOT NULL DEFAULT 0,
            candidate_positions INTEGER NOT NULL DEFAULT 0,
            candidate_cash_rows INTEGER NOT NULL DEFAULT 0,
            mapped_positions INTEGER NOT NULL DEFAULT 0,
            blocked_positions INTEGER NOT NULL DEFAULT 0,
            warnings_count INTEGER NOT NULL DEFAULT 0,
            errors_count INTEGER NOT NULL DEFAULT 0,
            quality_flags_json TEXT NOT NULL,
            summary_json TEXT NOT NULL DEFAULT '{}',
            created_at TEXT NOT NULL,
            notes TEXT
        );
        """
    )
    _add_missing_columns(conn, "instrument_mappings", {
        "source_name": "TEXT",
        "source_platform": "TEXT",
        "source_account": "TEXT",
        "source_label": "TEXT",
        "normalized_name": "TEXT",
        "isin": "TEXT",
        "ticker": "TEXT",
        "exchange": "TEXT",
        "currency": "TEXT",
        "asset_class": "TEXT DEFAULT 'other'",
        "confidence": "TEXT DEFAULT '0'",
    })
    _add_missing_columns(conn, "platform_account_mappings", {
        "source_platform": "TEXT",
        "normalized_platform": "TEXT",
        "normalized_account_name": "TEXT",
        "account_type": "TEXT DEFAULT 'other'",
        "currency": "TEXT",
        "internal_platform_id": "TEXT",
        "internal_account_id": "TEXT",
    })


So far I don’t see a concrete blocker in the reviewed paths: canonical inserts use `postfinance_official_import`, triggers protect those rows, account-role resolution rejects duplicate account IDs, and summary omits internal IDs/hashes. I’m doing one pass over migration registration and frontend/API exposure before closing.


rg -n "create_postfinance_ledger_import_v1|045_postfinance|postfinance" src/jarvis_finance/storage/migrations.py src/jarvis_finance/api/main.py frontend/src/api/postfinance.ts frontend/src/pages/PostFinancePage.vue frontend/src/pages/PostFinancePage.test.ts frontend/src/router/index.ts


sed -n '1480,1570p' src/jarvis_finance/storage/migrations.py


sed -n '1900,2085p' src/jarvis_finance/storage/migrations.py


sed -n '2085,2165p' src/jarvis_finance/storage/migrations.py


Output:
Chunk ID: 3bff32
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1623
Output:
frontend/src/router/index.ts:43:    { path: '/portfolio/postfinance', name: 'postfinance', component: PostFinancePage, meta: { title: 'PostFinance Depot & Ledger' } },
frontend/src/pages/PostFinancePage.vue:2:

frontend/src/pages/PostFinancePage.vue:3:    <LoadActionBar testid="refresh-postfinance" title="PostFinance · Depot & Ledger" description="Offizielle Depot-, Cash- und Ereignisdaten mit unveränderter Provenienz." :loading="loading" @load="loadData(true)" />
frontend/src/pages/PostFinancePage.vue:17:
<button data-testid="postfinance-import-button" class="rounded-lg bg-slate-900 px-3 py-2 font-semibold text-white" @click="showImport = !showImport">Offizielle Quellen importieren</button>Zur Portfolio-Übersicht

frontend/src/pages/PostFinancePage.vue:20:

frontend/src/pages/PostFinancePage.vue:23:        Dokumenten-ZIP<input data-testid="postfinance-zip-input" type="file" accept="application/zip,.zip" class="mt-1 block w-full rounded-lg border bg-white p-2" @change="selectFile($event, 'zip')" />
frontend/src/pages/PostFinancePage.vue:24:        Konto-/Portfolioübersicht PDF<input data-testid="postfinance-overview-input" type="file" accept="application/pdf,.pdf" class="mt-1 block w-full rounded-lg border bg-white p-2" @change="selectFile($event, 'overview')" />
frontend/src/pages/PostFinancePage.vue:26:
<button data-testid="postfinance-preview-button" class="rounded-lg bg-slate-900 px-3 py-2 font-semibold text-white disabled:opacity-50" :disabled="actionLoading || !payloadReady" @click="doPreview">Import-Preview</button><button v-if="preview" data-testid="postfinance-confirm-button" class="rounded-lg bg-emerald-600 px-3 py-2 font-semibold text-white disabled:opacity-50" :disabled="actionLoading || preview.conflict || preview.reconciliation_status === 'mismatch'" @click="doConfirm">Import bestätigen</button>

frontend/src/pages/PostFinancePage.vue:27:

frontend/src/pages/PostFinancePage.vue:60:import { confirmPostFinanceImport, getPostFinanceSummary, previewPostFinanceImport, type PostFinanceImportPayload, type PostFinancePreview, type PostFinanceSummary } from '@/api/postfinance'
src/jarvis_finance/api/main.py:10:from jarvis_finance.api.routers import budget, cash, crypto, crypto_trader, equity, health, market, overview, positions, postfinance, reports, system, truewealth
src/jarvis_finance/api/main.py:24:    "/api/postfinance/imports/preview",
src/jarvis_finance/api/main.py:77:    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, postfinance.router):
frontend/src/pages/PostFinancePage.test.ts:4:import * as postfinance from '@/api/postfinance'
frontend/src/pages/PostFinancePage.test.ts:7:vi.mock('@/api/postfinance')
frontend/src/pages/PostFinancePage.test.ts:22:  vi.mocked(postfinance.getPostFinanceSummary).mockResolvedValue(summary)
frontend/src/pages/PostFinancePage.test.ts:29:    expect(wrapper.get('[data-testid="postfinance-page"]').text()).toContain('E-Finance, Depot und Währungscash')
frontend/src/pages/PostFinancePage.test.ts:37:    vi.mocked(postfinance.previewPostFinanceImport).mockResolvedValue({ preview_id: 'preview', confirmation_id: 'confirm', parser_id: 'postfinance_documents', parser_version: '1', snapshot_at: '2026-07-27', document_count: 49, zip_document_count: 48, page_count: 80, position_count: 22, cash_count: 2, event_count: 41, event_counts: {}, quality_counts: {}, transfer_group_count: 3, total_chf: '321149.23', securities_chf: '293465.16', cash_chf: '27684.07', stocks_chf: '255791.97', etfs_chf: '37673.19', component_total_chf: '321149.23', reconciliation_difference_chf: '0.00', reconciliation_status: 'matched', open_orders: 0, mapped_position_count: 22, duplicate: false, conflict: false, warnings: [] })
frontend/src/pages/PostFinancePage.test.ts:38:    vi.mocked(postfinance.confirmPostFinanceImport).mockResolvedValue({ status: 'confirmed', batch_id: 'batch', audit_id: 'audit', idempotent: false })
frontend/src/pages/PostFinancePage.test.ts:41:    await wrapper.get('[data-testid="postfinance-import-button"]').trigger('click')
frontend/src/pages/PostFinancePage.test.ts:45:    expect(postfinance.previewPostFinanceImport).toHaveBeenCalledOnce()
frontend/src/pages/PostFinancePage.test.ts:46:    expect(wrapper.get('[data-testid="postfinance-confirm-button"]').attributes('disabled')).toBeUndefined()
frontend/src/pages/PostFinancePage.test.ts:48:    expect(postfinance.confirmPostFinanceImport).toHaveBeenCalledWith(expect.objectContaining({ preview_id: 'preview', confirmation_id: 'confirm', confirm: true }))
frontend/src/api/postfinance.ts:48:export const getPostFinanceSummary = (refresh = false) => apiGet<PostFinanceSummary>('/api/postfinance/summary', { refresh })
frontend/src/api/postfinance.ts:49:export const previewPostFinanceImport = (payload: PostFinanceImportPayload) => apiPost<PostFinancePreview>('/api/postfinance/imports/preview', payload)
frontend/src/api/postfinance.ts:50:export const confirmPostFinanceImport = (payload: PostFinanceImportPayload & Pick<PostFinancePreview, 'preview_id' | 'confirmation_id'> & { confirm: true }) => apiPost<{ status: string; batch_id: string; snapshot_id?: string; audit_id: string; idempotent: boolean }>('/api/postfinance/imports/confirm', payload)
src/jarvis_finance/storage/migrations.py:8:from .postfinance_schema import create_postfinance_ledger_import_v1
src/jarvis_finance/storage/migrations.py:11:MIGRATION_NAME = "045_postfinance_ledger_import_v1"
src/jarvis_finance/storage/migrations.py:459:def _create_postfinance_baseline_mapping_audit_v1(conn: Connection) -> None:
src/jarvis_finance/storage/migrations.py:2125:    _create_postfinance_baseline_mapping_audit_v1(conn)
src/jarvis_finance/storage/migrations.py:2127:    create_postfinance_ledger_import_v1(conn)


Output:
Chunk ID: 967696
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 987
Output:
        );
        CREATE INDEX IF NOT EXISTS idx_equity_price_points_instrument_time ON equity_price_points(instrument_id, timestamp);
        CREATE TABLE IF NOT EXISTS equity_intraday_candles (
            candle_id TEXT PRIMARY KEY,
            instrument_id TEXT NOT NULL REFERENCES instruments(instrument_id),
            provider TEXT NOT NULL,
            provider_symbol TEXT NOT NULL,
            range_key TEXT NOT NULL DEFAULT '1d',
            interval_key TEXT NOT NULL DEFAULT '5m',
            timestamp TEXT NOT NULL,
            open TEXT NOT NULL,
            close TEXT NOT NULL,
            low TEXT NOT NULL,
            high TEXT NOT NULL,
            volume TEXT,
            currency TEXT,
            exchange_timezone TEXT,
            quality_status TEXT NOT NULL DEFAULT 'fresh',
            fetched_at TEXT NOT NULL,
            UNIQUE(instrument_id, provider, provider_symbol, range_key, interval_key, timestamp)
        );
        CREATE INDEX IF NOT EXISTS idx_equity_intraday_candles_lookup ON equity_intraday_candles(instrument_id, range_key, interval_key, fetched_at);
        CREATE TABLE IF NOT EXISTS crypto_price_points (
            point_id TEXT PRIMARY KEY,
            asset_id TEXT NOT NULL REFERENCES crypto_assets(asset_id),
            provider TEXT NOT NULL,
            provider_symbol TEXT,
            timestamp TEXT NOT NULL,
            price TEXT NOT NULL,
            currency TEXT NOT NULL,
            interval TEXT NOT NULL DEFAULT 'quote',
            source_quality TEXT NOT NULL DEFAULT 'fresh',
            fetched_at TEXT NOT NULL,
            UNIQUE(asset_id, provider, provider_symbol, timestamp, interval, currency)
        );
        CREATE INDEX IF NOT EXISTS idx_crypto_price_points_asset_time ON crypto_price_points(asset_id, currency, timestamp);
        """
    )
    _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(
        """
        CREATE TABLE IF NOT EXISTS cash_account_snapshots (
            snapshot_id TEXT PRIMARY KEY,
            account_id TEXT NOT NULL REFERENCES accounts(account_id),
            snapshot_type TEXT NOT NULL,
            balance_date TEXT NOT NULL,
            amount_original TEXT NOT NULL,
            currency TEXT NOT NULL DEFAULT 'CHF',
            amount_chf TEXT NOT NULL,
            source TEXT NOT NULL,
            note TEXT,
            created_at TEXT NOT NULL,
            created_by TEXT NOT NULL DEFAULT 'user',
            audit_id TEXT
        );
        CREATE INDEX IF NOT EXISTS idx_cash_account_snapshots_account_type_date ON cash_account_snapshots(account_id, snapshot_type, balance_date, created_at);
        """
    )

def _create_transfer_pairing_v2_tables(conn: Connection) -> None:
    _add_missing_columns(
        conn,
        "budget_transaction_candidates",


Output:
Chunk ID: 371730
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2600
Output:
            quality_status TEXT NOT NULL CHECK(quality_status IN ('complete','partial','unavailable')),
            reason_codes_json TEXT NOT NULL DEFAULT '[]'
                CHECK(json_valid(reason_codes_json) AND json_type(reason_codes_json)='array'),
            summary_json TEXT NOT NULL DEFAULT '{}'
                CHECK(json_valid(summary_json) AND json_type(summary_json)='object'),
            created_at TEXT NOT NULL
        );
        CREATE INDEX IF NOT EXISTS idx_portfolio_analysis_snapshots_time
            ON portfolio_analysis_snapshots(as_of DESC, created_at DESC);

        CREATE TRIGGER IF NOT EXISTS market_data_runs_no_delete
        BEFORE DELETE ON market_data_runs
        BEGIN SELECT RAISE(ABORT, 'market data runs cannot be deleted'); END;
        CREATE TRIGGER IF NOT EXISTS benchmark_snapshots_immutable
        BEFORE UPDATE ON benchmark_snapshots
        BEGIN SELECT RAISE(ABORT, 'benchmark snapshots are immutable'); END;
        CREATE TRIGGER IF NOT EXISTS benchmark_snapshots_no_delete
        BEFORE DELETE ON benchmark_snapshots
        BEGIN SELECT RAISE(ABORT, 'benchmark snapshots cannot be deleted'); END;
        CREATE TRIGGER IF NOT EXISTS portfolio_analysis_snapshots_immutable
        BEFORE UPDATE ON portfolio_analysis_snapshots
        BEGIN SELECT RAISE(ABORT, 'portfolio analysis snapshots are immutable'); END;
        CREATE TRIGGER IF NOT EXISTS portfolio_analysis_snapshots_no_delete
        BEFORE DELETE ON portfolio_analysis_snapshots
        BEGIN SELECT RAISE(ABORT, 'portfolio analysis snapshots cannot be deleted'); END;
        """
    )

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;


Output:
Chunk ID: 2d3d56
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 777
Output:
        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():
        _rebuild_table_with_text_columns(conn, table, text_columns)
    _create_broker_bank_mapping_tables(conn)
    _create_broker_import_review_items(conn)
    _create_broker_import_execution_plans(conn)
    _add_transaction_void_columns(conn)
    _create_fx_market_data_tables(conn)
    _create_market_quote_chart_tables(conn)
    _create_account_value_snapshot_tables(conn)
    _create_cash_account_snapshot_tables(conn)
    _create_instrument_import_candidates(conn)
    _create_budget_phase1_tables(conn)
    _create_budget_phase11_tables(conn)
    _create_budget_phase14_tables(conn)
    _create_budget_phase15_tables(conn)
    _create_budget_phase18_tables(conn)
    _create_budget_phase19_tables(conn)
    _create_budget_import_production_v1_tables(conn)
    _create_budget_categories_ux_fix_tables(conn)
    _create_budget_planning_forecast_v1_tables(conn)
    _create_budget_fixed_costs_subscriptions_v1_tables(conn)
    _create_budget_monthly_import_rule_learning_v1_tables(conn)
    _create_transfer_pairing_v2_tables(conn)
    _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_postfinance_ledger_import_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()


Migration registration includes the new PostFinance schema on the compatibility path, so the triggers are applied with normal migrations. I’m checking line-level details once more for hidden identifiers in responses and then I’ll give the review result.


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


nl -ba src/jarvis_finance/services/postfinance_service.py | sed -n '300,920p'


nl -ba src/jarvis_finance/storage/postfinance_schema.py | sed -n '1,240p'


nl -ba src/jarvis_finance/api/routers/postfinance.py | sed -n '1,120p'


Output:
Chunk ID: 303882
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 455
Output:
import { apiGet, apiPost } from './client'

export type PostFinanceImportPayload = {
  zip_file_name: string
  overview_file_name: string
  zip_content_base64: string
  overview_content_base64: string
}

export type PostFinancePreview = {
  preview_id: string
  confirmation_id: string
  parser_id: string
  parser_version: string
  snapshot_at: string
  document_count: number
  zip_document_count: number
  page_count: number
  position_count: number
  cash_count: number
  event_count: number
  event_counts: Record<string, number>
  quality_counts: Record<string, number>
  transfer_group_count: number
  total_chf: string
  securities_chf: string
  cash_chf: string
  stocks_chf: string
  etfs_chf: string
  component_total_chf: string
  reconciliation_difference_chf: string
  reconciliation_status: string
  open_orders: number
  mapped_position_count: number
  duplicate: boolean
  conflict: boolean
  warnings: string[]
}

export type PostFinanceSummary = {
  latest_snapshot: Record<string, string | number> | null
  positions: Array<Record<string, string | number | null>>
  cash: Array<Record<string, string | number | null>>
  event_counts: Record<string, number>
  imports: Array<Record<string, string | number>>
}

export const getPostFinanceSummary = (refresh = false) => apiGet<PostFinanceSummary>('/api/postfinance/summary', { refresh })
export const previewPostFinanceImport = (payload: PostFinanceImportPayload) => apiPost<PostFinancePreview>('/api/postfinance/imports/preview', payload)
export const confirmPostFinanceImport = (payload: PostFinanceImportPayload & Pick<PostFinancePreview, 'preview_id' | 'confirmation_id'> & { confirm: true }) => apiPost<{ status: string; batch_id: string; snapshot_id?: string; audit_id: string; idempotent: boolean }>('/api/postfinance/imports/confirm', payload)