Output:
Chunk ID: 5250ff
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1094
Output:
    35
    36
    37	def _hash(*values: object) -> str:
    38	    return sha256(
    39	        json.dumps(values, sort_keys=True, separators=(",", ":"), default=str).encode()
    40	    ).hexdigest()
    41
    42
    43	def _text(value: Decimal | None) -> str | None:
    44	    return format(value, "f") if value is not None else None
    45
    46
    47	def _money(value: Decimal) -> str:
    48	    return format(value.quantize(Decimal("0.01")), "f")
    49
    50
    51	def _decode(value: str, label: str) -> bytes:
    52	    try:
    53	        return base64.b64decode(value, validate=True)
    54	    except (binascii.Error, ValueError) as exc:
    55	        raise ValueError(f"PostFinance {label} is not valid base64") from exc
    56
    57
    58	def _source_bytes(request: dict[str, Any]) -> tuple[bytes, bytes]:
    59	    return (
    60	        _decode(str(request.get("zip_content_base64") or ""), "ZIP"),
    61	        _decode(str(request.get("overview_content_base64") or ""), "overview PDF"),
    62	    )
    63
    64
    65	def _resolve_accounts(conn: Connection, request: dict[str, Any] | None = None) -> dict[str, str]:
    66	    request = request or {}
    67	    explicit = request.get("account_roles") or {}
    68	    prospective_efinance = False
    69	    if explicit:
    70	        if set(explicit) != {"efinance", "etrading_depot", "etrading_cash"}:
    71	            raise ValueError("All three PostFinance account roles are required")
    72	        result = {key: str(value) for key, value in explicit.items()}
    73	    else:
    74	        rows = conn.execute(
    75	            """SELECT a.account_id,a.account_name,a.account_type,a.platform_id
    76	               FROM accounts a JOIN platforms p ON p.platform_id=a.platform_id
    77	               WHERE a.is_active=1 AND replace(replace(lower(p.name),' ',''),'-','') LIKE '%postfinance%'
    78	               ORDER BY a.account_id"""
    79	        ).fetchall()
    80	        depot = [
    81	            row
    82	            for row in rows
    83	            if "trading" in str(row["account_name"]).lower()
    84	            and str(row["account_type"]).lower() in {"brokerage", "depot"}
    85	            or str(row["account_type"]).lower() in {"brokerage", "depot"}
    86	        ]
    87	        cash = [
    88	            row
    89	            for row in rows
    90	            if row not in depot
    91	            and (
    92	                "cash" in str(row["account_name"]).lower()
    93	                or "finance" in str(row["account_name"]).lower()
    94	                or str(row["account_type"]).lower() in {"cash", "bank", "checking"}
    95	            )
    96	        ]
    97	        efinance = [
    98	            row
    99	            for row in cash
   100	            if "efinance"
   101	            in str(row["account_name"])
   102	            .lower()
   103	            .replace("postfinance", "")
   104	            .replace("-", "")
   105	            .replace(" ", "")
   106	        ]
   107	        trading_cash = [row for row in cash if row not in efinance]
   108	        if len(depot) != 1 or len(trading_cash) != 1 or len(efinance) > 1:
   109	            raise ValueError(
   110	                "PostFinance E-Finance, E-Trading depot and E-Trading cash cannot be resolved unambiguously"
   111	            )
   112	        if efinance:
   113	            efinance_id = str(efinance[0]["account_id"])
   114	        else:
   115	            efinance_id = f"account_pf_efinance_{_hash(depot[0]['platform_id'])[:16]}"
   116	            prospective_efinance = True
   117	        result = {
   118	            "efinance": efinance_id,
   119	            "etrading_depot": str(depot[0]["account_id"]),
   120	            "etrading_cash": str(trading_cash[0]["account_id"]),
   121	        }
   122	    if len(set(result.values())) != 3:
   123	        raise ValueError("PostFinance account roles must map to three distinct canonical accounts")
   124	    for role, account_id in result.items():
   125	        row = conn.execute(
   126	            "SELECT is_active FROM accounts WHERE account_id=?", (account_id,)
   127	        ).fetchone()
   128	        if not row and role == "efinance" and prospective_efinance:
   129	            continue
   130	        if not row or not row["is_active"]:


Output:
Chunk ID: 2c61a0
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 448
Output:
     1	from __future__ import annotations
     2
     3	from fastapi import APIRouter, Depends, HTTPException
     4
     5	from jarvis_finance.api.dependencies import get_db
     6	from jarvis_finance.api.schemas.postfinance import (
     7	    PostFinanceConfirmRequest,
     8	    PostFinanceImportRequest,
     9	)
    10	from jarvis_finance.services.postfinance_service import (
    11	    confirm_postfinance_import,
    12	    get_postfinance_summary,
    13	    preview_postfinance_import,
    14	)
    15
    16	router = APIRouter(prefix="/postfinance", tags=["postfinance"])
    17
    18
    19	def _run(callable_, *args):
    20	    try:
    21	        return callable_(*args)
    22	    except ValueError as exc:
    23	        raise HTTPException(status_code=422, detail=str(exc)) from exc
    24
    25
    26	def _safe_preview(payload: dict) -> dict:
    27	    hidden = {
    28	        "bundle_sha256",
    29	        "zip_sha256",
    30	        "overview_sha256",
    31	        "db_revision",
    32	        "existing_batch_id",
    33	        "conflicting_batch_id",
    34	    }
    35	    return {key: value for key, value in payload.items() if key not in hidden}
    36
    37
    38	@router.get("/summary")
    39	def summary(conn=Depends(get_db)):
    40	    return _run(get_postfinance_summary, conn)
    41
    42
    43	@router.post("/imports/preview")
    44	def import_preview(request: PostFinanceImportRequest, conn=Depends(get_db)):
    45	    return _safe_preview(_run(preview_postfinance_import, conn, request.model_dump()))
    46
    47
    48	@router.post("/imports/confirm")
    49	def import_confirm(request: PostFinanceConfirmRequest, conn=Depends(get_db)):
    50	    return _run(confirm_postfinance_import, conn, request.model_dump())


Output:
Chunk ID: 762dab
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1068
Output:
   852	def get_postfinance_summary(conn: Connection) -> dict[str, Any]:
   853	    latest = conn.execute(
   854	        """SELECT snapshot_id,snapshot_at,total_chf,securities_chf,cash_chf,stocks_chf,etfs_chf,
   855	                  position_count,cash_count,open_orders,difference_chf,reconciliation_status,created_at
   856	           FROM postfinance_snapshots ORDER BY snapshot_at DESC,created_at DESC LIMIT 1"""
   857	    ).fetchone()
   858	    if not latest:
   859	        return {
   860	            "latest_snapshot": None,
   861	            "positions": [],
   862	            "cash": [],
   863	            "event_counts": {},
   864	            "imports": [],
   865	        }
   866	    positions = [
   867	        {
   868	            "row_key": _hash("position", row["instrument_name"], row["quantity"])[:16],
   869	            "instrument_name": row["instrument_name"],
   870	            "ticker": row["ticker"],
   871	            "asset_class": row["asset_class"],
   872	            "quantity": row["quantity"],
   873	            "provider_average_cost_original": row["provider_average_cost_original"],
   874	            "provider_cost_total_original": row["provider_cost_total_original"],
   875	            "price_original": row["price_original"],
   876	            "price_currency": row["price_currency"],
   877	            "value_chf": row["value_chf"],
   878	            "weight_pct": row["weight_pct"],
   879	            "computed_cost_basis_original": row["computed_cost_basis_original"],
   880	            "computed_cost_basis_status": row["computed_cost_basis_status"],
   881	        }
   882	        for row in conn.execute(
   883	            """SELECT p.asset_class,p.quantity,p.provider_average_cost_original,
   884	                      p.provider_cost_total_original,p.price_original,p.price_currency,p.value_chf,
   885	                      p.weight_pct,p.computed_cost_basis_original,p.computed_cost_basis_status,
   886	                      i.name AS instrument_name,i.ticker
   887	               FROM postfinance_snapshot_positions p
   888	               JOIN instruments i ON i.instrument_id=p.instrument_id
   889	               WHERE p.snapshot_id=? ORDER BY CAST(p.value_chf AS REAL) DESC""",
   890	            (latest["snapshot_id"],),
   891	        ).fetchall()
   892	    ]
   893	    cash = [
   894	        {
   895	            "row_key": _hash("cash", row["currency"])[:16],
   896	            "currency": row["currency"],
   897	            "amount_original": row["amount_original"],
   898	            "fx_rate_to_chf": row["fx_rate_to_chf"],
   899	            "value_chf": row["value_chf"],
   900	        }
   901	        for row in conn.execute(
   902	            """SELECT currency,amount_original,fx_rate_to_chf,value_chf
   903	               FROM postfinance_snapshot_cash WHERE snapshot_id=? ORDER BY currency""",
   904	            (latest["snapshot_id"],),
   905	        ).fetchall()
   906	    ]
   907	    events = {
   908	        str(row["event_type"]): int(row["c"])
   909	        for row in conn.execute(
   910	            "SELECT event_type,COUNT(*) c FROM postfinance_ledger_events GROUP BY event_type ORDER BY event_type"
   911	        ).fetchall()
   912	    }
   913	    imports = [
   914	        dict(row)
   915	        for row in conn.execute(
   916	            """SELECT b.snapshot_at,b.parser_id,b.parser_version,b.confirmed_at,b.status,
   917	                      s.total_chf,s.securities_chf,s.cash_chf,s.position_count,s.cash_count,
   918	                      s.difference_chf,s.reconciliation_status,
   919	                      (SELECT COUNT(*) FROM postfinance_batch_documents d WHERE d.batch_id=b.batch_id) AS document_count
   920	               FROM postfinance_import_batches b JOIN postfinance_snapshots s ON s.batch_id=b.batch_id
   921	               ORDER BY b.snapshot_at DESC,b.confirmed_at DESC"""
   922	        ).fetchall()
   923	    ]
   924	    safe_latest = {key: latest[key] for key in latest.keys() if key != "snapshot_id"}
   925	    return {
   926	        "latest_snapshot": safe_latest,
   927	        "positions": positions,
   928	        "cash": cash,
   929	        "event_counts": events,
   930	        "imports": imports,


**Findings**

1. `src/jarvis_finance/services/postfinance_service.py:306` does not block same-day canonical projection collisions at preview. The preview conflict check only looks at `postfinance_import_batches` for the same snapshot date (`lines 314-321`), but confirm later inserts into canonical `positions_snapshot` and `cash_balances` (`lines 757-806`). Existing rows with the same date/account/instrument or date/account/currency/source will still pass preview and fail only during confirm.

2. Official PostFinance canonical projections remain mutable. Confirm writes source-derived rows into `transactions`, `positions_snapshot`, and `cash_balances` (`src/jarvis_finance/services/postfinance_service.py:659`, `:757`, `:791`), but the migration only adds immutability triggers for PostFinance-specific tables, audit, and `account_value_snapshots` (`src/jarvis_finance/storage/postfinance_schema.py:155-202`). Those canonical rows can still be updated or deleted after import, which breaks the immutable official-source contract.

Verification: `git diff --check` passed. `make PYTHON=.venv/bin/python verify` could not run because `.venv/bin/python` is missing.