Output:
Chunk ID: 952928
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 5042
Output:
    ).fetchone()
    semantic_conflicts = conn.execute(
        """SELECT COUNT(*) FROM postfinance_documents d
           WHERE EXISTS (SELECT 1 FROM json_each(?) j WHERE j.value=d.semantic_identity)
             AND d.document_hash NOT IN (SELECT value FROM json_each(?))""",
        (
            json.dumps([d.semantic_identity for d in bundle.documents]),
            json.dumps([d.document_hash for d in bundle.documents]),
        ),
    ).fetchone()[0]
    if semantic_conflicts:
        conflict = conflict or {"batch_id": "semantic_document_conflict"}
    if conflict:
        payload["warnings"].append(
            "A different immutable PostFinance source has the same business identity"
        )
    payload.update(
        {
            "preview_id": f"pfpreview_{fingerprint[:24]}",
            "confirmation_id": f"pfconfirm_{fingerprint[24:48]}",
            "db_revision": revision,
            "duplicate": bool(duplicate),
            "existing_batch_id": str(duplicate["batch_id"]) if duplicate else None,
            "conflict": bool(conflict),
            "conflicting_batch_id": str(conflict["batch_id"]) if conflict else None,
        }
    )
    return payload

def _archive(raw: bytes, digest: str, suffix: str) -> tuple[str, bool]:
    root = Path(os.environ.get("JARVIS_FINANCE_RUNTIME_DIR", str(DEFAULT_RUNTIME_DIR))).expanduser()
    folder = root / "imports" / "postfinance" / "archive"
    folder.mkdir(mode=0o700, parents=True, exist_ok=True)
    folder.chmod(0o700)
    target = folder / f"{digest}.{suffix}"
    if target.exists():
        if sha256(target.read_bytes()).hexdigest() != digest:
            raise ValueError("Archived PostFinance source hash mismatch")
        return f"postfinance/archive/{target.name}", False
    tmp = folder / f".{digest}.{uuid4().hex}.tmp"
    fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
    try:
        with os.fdopen(fd, "wb") as handle:
            handle.write(raw)
            handle.flush()
            os.fsync(handle.fileno())
        os.replace(tmp, target)
        target.chmod(0o600)
    finally:
        tmp.unlink(missing_ok=True)
    return f"postfinance/archive/{target.name}", True

def _remove_archive(reference: str) -> None:
    root = Path(os.environ.get("JARVIS_FINANCE_RUNTIME_DIR", str(DEFAULT_RUNTIME_DIR))).expanduser()
    (root / "imports" / reference).unlink(missing_ok=True)

def _audit(conn: Connection, batch_id: str, bundle: PFBundle, now: str) -> str:
    audit_id = _id("audit")
    details = {
        "bundle_sha256": bundle.bundle_hash,
        "parser_id": PARSER_ID,
        "parser_version": PARSER_VERSION,
        "document_count": len(bundle.documents) + 1,
        "event_count": len(bundle.events),
        "snapshot_at": bundle.snapshot.valuation_at,
    }
    conn.execute(
        """INSERT INTO audit_log(audit_id,timestamp,source,action,entity_type,entity_id,old_values_json,new_values_json,user_text_note,confirmed,confirmation_timestamp,auto_parsed,parse_confidence,created_by,quality_status,created_at)
           VALUES(?,?,'postfinance_sprint13','postfinance_import_confirm','postfinance_import_batch',?,NULL,?,'Verified official source bundle',1,?,0,NULL,'user','ok',?)""",
        (audit_id, now, batch_id, json.dumps(details, sort_keys=True), now, now),
    )
    return audit_id

def _canonical_transaction(
    conn: Connection,
    event_id: str,
    event: PFEvent,
    account_id: str,
    instrument_id: str | None,
    now: str,
) -> None:
    if event.event_type in {"split", "corporate_action"}:
        return
    tx_type = {
        "buy": "buy",
        "sell": "sell",
        "dividend": "dividend",
        "interest": "interest",
        "fee": "fee",
        "internal_transfer": "transfer",
        "fx": "fx",
    }[event.event_type]
    activity = (
        "internal_transfer"
        if event.event_type == "internal_transfer"
        else "external_cashflow"
        if event.event_type in {"dividend", "interest", "fee"}
        else "trade"
    )
    columns = (
        "transaction_id,transaction_type,account_id,instrument_id,trade_date,settlement_date,quantity,"
        "price_original,gross_amount_original,fee_original,tax_original,net_amount_original,currency_original,"
        "fx_rate_to_chf,fx_source,fx_status,gross_amount_chf,fee_chf,tax_chf,net_amount_chf,source_type,source_id,"
        "external_transaction_id,row_hash,is_confirmed,quality_status,notes,created_at,updated_at,is_voided,"
        "activity_kind,booking_date,base_currency,internal_transfer_group_id,source_reference"
    )
    values = (
        event_id,
        tx_type,
        account_id,
        instrument_id,
        event.occurred_on,
        event.settlement_on,
        _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),
        None,
        "unavailable",
        None,
        None,
        None,
        None,
        "postfinance_official_import",
        event_id,
        event.event_fingerprint,
        event.event_fingerprint,
        1,
        event.quality_status,
        "Verified PostFinance document event",
        now,
        None,
        0,
        activity,
        event.settlement_on or event.occurred_on,
        "CHF",
        event.internal_transfer_group,
        _hash(event.semantic_reference),
    )
    conn.execute(
        f"INSERT INTO transactions({columns}) VALUES({','.join('?' for _ in values)})", values
    )

def confirm_postfinance_import(conn: Connection, request: dict[str, Any]) -> dict[str, Any]:
    if request.get("confirm") is not True:
        raise ValueError("Explicit PostFinance import confirmation is required")
    preview = preview_postfinance_import(conn, request)
    if (
        request.get("preview_id") != preview["preview_id"]
        or request.get("confirmation_id") != preview["confirmation_id"]
    ):
        raise ValueError("PostFinance import preview is stale or changed")
    if preview["conflict"]:
        raise ValueError("PostFinance source conflicts with existing immutable business identity")
    if preview["duplicate"]:
        row = conn.execute(
            "SELECT batch_id,audit_id FROM postfinance_import_batches WHERE bundle_sha256=?",
            (preview["bundle_sha256"],),
        ).fetchone()
        return {
            "status": "confirmed",
            "batch_id": row["batch_id"],
            "audit_id": row["audit_id"],
            "idempotent": True,
        }
    accounts = _resolve_accounts(conn, request)
    zip_raw, overview_raw = _source_bytes(request)
    bundle = parse_postfinance_bundle(zip_raw, overview_raw)  # mandatory byte reparse on confirm
    if (
        bundle.bundle_hash != preview["bundle_sha256"]
        or _revision(conn, accounts) != preview["db_revision"]
    ):
        raise ValueError("PostFinance source bytes or database revision changed after preview")
    position_map = _match_instruments(conn, bundle, accounts["etrading_depot"])
    event_instruments = {
        event.event_fingerprint: _event_instrument(event, bundle, position_map, conn)
        for event in bundle.events
    }
    archive_refs: list[tuple[str, bool]] = []
    zip_ref, created = _archive(zip_raw, bundle.zip_hash, "zip")
    archive_refs.append((zip_ref, created))
    overview_ref, created = _archive(overview_raw, bundle.overview_hash, "pdf")
    archive_refs.append((overview_ref, created))
    for document in bundle.documents:
        reference, created = _archive(document.raw, document.document_hash, "pdf")
        archive_refs.append((reference, created))
    now = _now()
    batch_id = _id("pfbatch")
    snapshot_id = _id("pfsnapshot")
    try:
        conn.execute("BEGIN IMMEDIATE")
        if _revision(conn, accounts) != preview["db_revision"]:
            raise ValueError("PostFinance database revision changed before persistence")
        _ensure_efinance_account(conn, accounts, now)
        for role, account_id in accounts.items():
            conn.execute(
                "INSERT OR IGNORE INTO postfinance_account_roles(role,account_id,source_reference_hash,created_at) VALUES(?,?,NULL,?)",
                (role, account_id, now),
            )
            mapped = conn.execute(
                "SELECT account_id FROM postfinance_account_roles WHERE role=?", (role,)
            ).fetchone()
            if not mapped or mapped["account_id"] != account_id:
                raise ValueError(
                    "PostFinance account-role mapping conflicts with confirmed mapping"
                )
        audit_id = _audit(conn, batch_id, bundle, now)
        conn.execute(
            """INSERT INTO postfinance_import_batches(batch_id,bundle_sha256,zip_sha256,overview_sha256,parser_id,parser_version,db_revision,archive_reference,snapshot_at,status,audit_id,confirmed_at,confirmed_by) VALUES(?,?,?,?,?,?,?,?,?,'confirmed',?,?,'user')""",
            (
                batch_id,
                bundle.bundle_hash,
                bundle.zip_hash,
                bundle.overview_hash,
                PARSER_ID,
                PARSER_VERSION,
                preview["db_revision"],
                zip_ref,
                bundle.snapshot.valuation_at,
                audit_id,
                now,
            ),
        )
        for document in bundle.documents:
            reference = f"postfinance/archive/{document.document_hash}.pdf"
            ref_hash = _hash(document.semantic_reference)
            existing = conn.execute(
                "SELECT document_hash FROM postfinance_documents WHERE semantic_identity=?",
                (document.semantic_identity,),
            ).fetchone()
            if existing and existing["document_hash"] != document.document_hash:
                raise ValueError("PostFinance semantic document conflict")
            conn.execute(
                """INSERT OR IGNORE INTO postfinance_documents(document_hash,filename_hash,semantic_identity,semantic_reference_hash,document_type,document_date,account_reference_hash,account_role,page_count,archive_reference,parser_version,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)""",
                (
                    document.document_hash,
                    document.filename_hash,
                    document.semantic_identity,
                    ref_hash,
                    document.document_type,
                    document.document_date,
                    document.account_reference_hash,
                    document.account_role,
                    document.page_count,
                    reference,
                    PARSER_VERSION,
                    now,
                ),
            )
            conn.execute(
                "INSERT INTO postfinance_batch_documents(batch_id,document_hash) VALUES(?,?)",
                (batch_id, document.document_hash),
            )
        snapshot = bundle.snapshot
        component_total = snapshot.securities_total_chf + snapshot.cash_total_chf
        difference = snapshot.total_value_chf - component_total
        conn.execute(
            """INSERT INTO postfinance_snapshots(snapshot_id,batch_id,account_id,snapshot_at,total_chf,securities_chf,cash_chf,stocks_chf,etfs_chf,component_total_chf,difference_chf,tolerance_chf,position_count,cash_count,open_orders,reconciliation_status,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,? ,?,?,?,?,?)""",
            (
                snapshot_id,
                batch_id,
                accounts["etrading_depot"],
                snapshot.valuation_at,
                _money(snapshot.total_value_chf),
                _money(snapshot.securities_total_chf),
                _money(snapshot.cash_total_chf),
                _money(snapshot.stock_total_chf),
                _money(snapshot.etf_total_chf),
                _money(component_total),
                _money(difference),
                _money(TOLERANCE),
                len(snapshot.positions),
                len(snapshot.cash),
                snapshot.open_orders,
                "matched" if difference == 0 else "within_tolerance",
                now,
            ),
        )
        for event in bundle.events:
            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,created_at) SELECT ?,?,?,a.platform_id,?,?,?,?,?,?,?,?,?,?,?,? FROM accounts a WHERE a.account_id=?""",
                (
                    _id("position"),


Output:
Chunk ID: 0c81f9
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 3517
Output:
    amount = _number(match.group(2), required=True)
    assert amount is not None
    return match.group(1).upper(), amount

def _event_from_document(doc: PFDocument) -> PFEvent | None:
    text = doc.text
    reference = doc.semantic_reference
    occurred = doc.document_date or ""
    settlement = None
    match = re.search(r"Ausführungsdatum\s*:?\s*(\d{1,2}[./]\d{1,2}[./]\d{4})", text, flags=re.I)
    if match:
        occurred = _date(match.group(1)) or occurred
    match = re.search(r"Valutadatum\s*:?\s*(\d{1,2}[./]\d{1,2}[./]\d{4})", text, flags=re.I)
    if match:
        settlement = _date(match.group(1))
    if not occurred:
        raise ValueError("PostFinance economic document lacks an event date")
    fee = Decimal("0")
    tax = Decimal("0")
    fx = None
    instrument = None
    isin = None
    quantity = price = gross = net = None
    direction = None
    transfer_group = None
    reasons: list[str] = []
    currency = "CHF"
    event_type = ""

    if doc.document_type == "trade_confirmation":
        event_type = (
            "buy"
            if re.search(r"Börsentransaktion\s*:\s*Kauf", text, flags=re.I)
            else "sell"
            if re.search(r"Börsentransaktion\s*:\s*Verkauf", text, flags=re.I)
            else ""
        )
        if not event_type:
            raise ValueError("Unsupported PostFinance trade direction")
        im = re.search(r"([^\n]+?)\s+ISIN\s*:\s*([A-Z]{2}[A-Z0-9]{9}[0-9])", text)
        if not im:
            raise ValueError("PostFinance trade lacks instrument identity")
        instrument, isin = im.group(1).strip(), im.group(2).upper()
        lines = text.splitlines()
        header = next(
            (
                i
                for i, line in enumerate(lines)
                if "Anzahl" in line and "Preis" in line and "Betrag" in line
            ),
            None,
        )
        values: list[str] = []
        if header is not None:
            for line in lines[header + 1 : header + 6]:
                cols = [item.strip() for item in re.split(r"\s{2,}", line.strip()) if item.strip()]
                if len(cols) >= 4 and sum(bool(re.search(r"\d", item)) for item in cols) >= 3:
                    values = cols
                    break
        if not values:
            raise ValueError("PostFinance trade amount row cannot be parsed")
        quantity = _number(values[0], required=True)
        price = _number(values[1], required=True)
        currency = next((item for item in values if item in CURRENCIES), "")
        gross = _number(values[-1], required=True)
        if not currency:
            raise ValueError("PostFinance trade currency is missing")
        fee_item = _currency_amount(text, r"Kommission")
        stamp_item = _currency_amount(text, r"Abgabe\s*\([^\n]*Stempelsteuer[^\n]*\)")
        fee = fee_item[1] if fee_item else Decimal("0")
        tax = stamp_item[1] if stamp_item else Decimal("0")
        net_item = _currency_amount(text, r"Betrag\s+belastet[^\n]*")
        assert gross is not None
        net = (
            net_item[1]
            if net_item
            else (gross + fee + tax if event_type == "buy" else gross - fee - tax)
        )
        direction = "out" if event_type == "buy" else "in"
    elif doc.document_type == "corporate_action":
        if re.search(r"\bDividende\b", text, flags=re.I):
            event_type = "dividend"
            im = re.search(r"([^\n]+?)\s+ISIN\s*:\s*([A-Z]{2}[A-Z0-9]{9}[0-9])", text)
            if not im:
                im = re.search(r"ISIN\s*:\s*([A-Z]{2}[A-Z0-9]{9}[0-9])", text)
                isin = im.group(1).upper() if im else None
            else:
                instrument, isin = im.group(1).strip(), im.group(2).upper()
            gross_item = _currency_amount(text, r"(?:Dividende|Betrag)")
            total_item = _currency_amount(text, r"Total")
            if not gross_item or not total_item:
                raise ValueError("PostFinance dividend totals cannot be parsed")
            currency, gross = gross_item
            net = total_item[1]
            taxes = re.findall(
                r"(?:Quellensteuer|Steuerrückbehalt)[^\n]*?\b"
                + currency
                + r"\s+([+\-−]?[0-9][0-9'’., ]*)",
                text,
                flags=re.I,
            )
            tax = sum(
                (_number(item, required=True) or Decimal("0") for item in taxes), Decimal("0")
            )
            direction = "in"
        elif re.search(r"\bSplit\b", text, flags=re.I):
            event_type = "split"
            im = re.search(r"ISIN\s*:\s*([A-Z]{2}[A-Z0-9]{9}[0-9])", text)
            isin = im.group(1).upper() if im else None
            currency = "CHF"
            reasons.append("non_cash_corporate_action")
        else:
            raise ValueError("Unsupported PostFinance corporate action")
    elif doc.document_type == "custody_fee":
        event_type = "fee"
        item = _currency_amount(text, r"Betrag\s+belastet") or _currency_amount(
            text, r"Bruttobetrag"
        )
        if not item:
            raise ValueError("PostFinance custody fee amount cannot be parsed")
        currency, fee = item
        gross = fee
        net = fee
        direction = "out"
    elif doc.document_type == "transfer_confirmation":
        credit = bool(re.search(r"Zahlungsverkehr\s*-\s*Gutschrift", text, flags=re.I))
        debit = bool(re.search(r"Zahlungsverkehr\s*-\s*Belastung", text, flags=re.I))
        if credit == debit:
            raise ValueError("PostFinance transfer direction is ambiguous")
        event_type = "internal_transfer"
        direction = "in" if credit else "out"
        item = _currency_amount(
            text, r"(?:Gutgeschriebener|Belasteter)\s+Betrag"
        ) or _currency_amount(text, r"Total")
        if not item:
            raise ValueError("PostFinance transfer amount cannot be parsed")
        currency, gross = item
        net = gross
        fx_match = re.search(r"Wechselkurs\s+([0-9][0-9'’., ]*)", text, flags=re.I)
        fx = _number(fx_match.group(1)) if fx_match else None
        transfer_group = _hash("transfer", reference)[:32]
        reasons.append("internal_transfer_not_income_or_expense")
    elif doc.document_type == "interest_statement":
        event_type = "interest"
        reasons.append("multi_currency_interest_statement")
        # Detailed currency components remain in the document evidence; one aggregate event
        # is intentionally not invented when the source has several independent currencies.
        currency = "CHF"
        direction = "in"
        reasons.append("component_events_required")
        return None
    else:
        return None
    fingerprint = _hash(
        reference,
        doc.semantic_identity,
        event_type,
        direction,
        isin or "",
        occurred,
        currency,
        quantity or "",
        gross or "",
        net or "",
    )
    return PFEvent(
        fingerprint,
        doc.document_hash,
        reference,
        event_type,
        doc.account_role,
        occurred,
        settlement,
        direction,
        instrument,
        isin,
        quantity,
        price,
        gross,
        fee,
        tax,
        net,
        currency,
        fx,
        transfer_group,
        "partial" if reasons else "complete",
        tuple(sorted(set(reasons))),
    )

def _interest_events(doc: PFDocument) -> tuple[PFEvent, ...]:
    if doc.document_type != "interest_statement":
        return ()
    events: list[PFEvent] = []
    for block in re.split(r"ABSCHLUSSBUCHUNGEN", doc.text, flags=re.I)[1:]:
        cm = re.search(r"Währung\s*:\s*([A-Z]{3})", block, flags=re.I)
        if not cm or cm.group(1).upper() not in CURRENCIES:
            continue
        currency = cm.group(1).upper()
        period_dates = re.findall(r"\d{1,2}[./]\d{1,2}[./]\d{4}", block)
        total_line = next(
            (
                line
                for line in block.splitlines()
                if line.strip().startswith("Total") and re.search(r"\d", line)
            ),
            "",
        )
        values = [
            _number(value, required=True)
            for value in re.findall(r"[+\-−]?[0-9][0-9'’.,]*", total_line)
        ]
        if len(period_dates) < 2 or not values:
            raise ValueError("PostFinance interest component cannot be parsed")
        occurred = _date(period_dates[1])
        assert occurred is not None
        if currency == "CHF":
            net = values[-1]
            fx = Decimal("1")
        elif len(values) >= 3:
            net = values[-3]
            fx = values[-2]
        else:
            raise ValueError("PostFinance foreign-currency interest component is incomplete")
        assert net is not None
        direction = "in" if net >= 0 else "out"
        fingerprint = _hash(
            doc.semantic_reference, doc.semantic_identity, "interest", currency, occurred, net
        )
        events.append(
            PFEvent(
                fingerprint,
                doc.document_hash,
                doc.semantic_reference,
                "interest",
                "etrading_cash",
                occurred,
                occurred,
                direction,
                None,
                None,
                None,
                None,
                abs(net),
                Decimal("0"),
                Decimal("0"),
                abs(net),
                currency,
                fx,
                None,
                "partial",
                ("interest_breakdown_source_preserved",),
            )
        )
    if len(events) != 3:
        raise ValueError("PostFinance interest statement must contain three currency components")
    return tuple(events)

def _parse_snapshot(raw: bytes) -> PFSnapshot:
    text, page_count = _pdf_text(raw)
    timestamp = re.search(r"(\d{1,2}/\d{1,2}/\d{4})\s+(\d{1,2}:\d{2}:\d{2})", text)
    if not timestamp:
        raise ValueError("PostFinance account overview lacks an exact valuation timestamp")
    date = _date(timestamp.group(1))
    assert date is not None
    valuation_at = f"{date}T{timestamp.group(2)}"
    positions: list[PFSnapshotPosition] = []
    cash: list[PFSnapshotCash] = []
    section: str | None = None
    in_positions = False
    cash_total = securities_total = total = stock_total = etf_total = None
    open_orders = None
    for line in text.splitlines():
        stripped = line.strip()
        if stripped == "Aktien":
            section = "stock"
            continue
        if stripped == "ETFs":
            section = "etf"
            continue
        if "Produkt" in stripped and "Anzahl" in stripped and "Einstandskurs" in stripped:
            in_positions = True
            continue
        cols = [item.strip() for item in re.split(r"\s{2,}", stripped) if item.strip()]
        if len(cols) == 7 and cols[0] in CURRENCIES and re.search(r"\d", cols[1]):
            fx = _number(cols[1], required=True)
            amount = _number(cols[2], required=True)
            value = _number(cols[5], required=True)
            assert fx is not None and amount is not None and value is not None
            cash.append(PFSnapshotCash(cols[0], amount, fx, value))
            continue
        if (
            in_positions
            and section
            and len(cols) == 9
            and re.search(r"\d", cols[1])
            and re.search(r"[A-Z]{3}", cols[5])
        ):
            quantity = _number(cols[1], required=True)
            average = _number(cols[2])
            provider_total = _number(cols[3])
            pm = re.search(r"([+\-−]?[0-9][0-9'’., ]*)\s*([A-Z]{3})", cols[5])
            if not pm:
                raise ValueError("PostFinance overview position lacks price/currency")
            price = _number(pm.group(1), required=True)
            currency = pm.group(2)
            value = _number(cols[7], required=True)
            weight = _number(cols[8])
            assert quantity is not None and price is not None and value is not None
            label = cols[0]
            positions.append(
                PFSnapshotPosition(
                    _hash("snapshot-row", len(positions) + 1, normalize_label(label))[:32],
                    label,
                    normalize_label(label),
                    section,
                    quantity,
                    average,
                    provider_total,
                    price,
                    currency,
                    value,
                    weight,
                )
            )
            continue
        if stripped.startswith("Gesamt CHF") and not in_positions:
            values = [
                _number(value, required=True)
                for value in re.findall(r"[+\-−]?[0-9][0-9'’.,]*", stripped)
            ]
            if len(values) >= 3:
                cash_total, securities_total, total = values[0], values[1], values[2]
            continue
        if stripped.startswith("Zwischensumme Aktien in CHF"):
            values = [
                _number(value, required=True)
                for value in re.findall(r"[+\-−]?[0-9][0-9'’.,]*", stripped)
            ]
            if len(values) >= 3:
                stock_total = values[2]
            continue
        if stripped.startswith("Zwischensumme ETFs in CHF"):
            values = [
                _number(value, required=True)
                for value in re.findall(r"[+\-−]?[0-9][0-9'’.,]*", stripped)
            ]
            if len(values) >= 3:
                etf_total = values[2]
            continue
        m = re.search(r"Offene Aufträge\s+([0-9]+)", stripped)
        if m:
            open_orders = int(m.group(1))
        m = re.search(r"Wert der Wertschriften\s+([0-9][0-9'’.,]*(?: [0-9]{3})*)", stripped)
        if m:
            securities_total = _number(m.group(1), required=True)
        m = re.search(r"Barguthaben\s+([0-9][0-9'’.,]*(?: [0-9]{3})*)", stripped)
        if m:


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

from fastapi import APIRouter, Depends, HTTPException

from jarvis_finance.api.dependencies import get_db
from jarvis_finance.api.schemas.postfinance import (
    PostFinanceConfirmRequest,
    PostFinanceImportRequest,
)
from jarvis_finance.services.postfinance_service import (
    confirm_postfinance_import,
    get_postfinance_summary,
    preview_postfinance_import,
)

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

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

def _safe_preview(payload: dict) -> dict:
    hidden = {
        "bundle_sha256",
        "zip_sha256",
        "overview_sha256",
        "db_revision",
        "existing_batch_id",
        "conflicting_batch_id",
    }
    return {key: value for key, value in payload.items() if key not in hidden}

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

@router.post("/imports/preview")
def import_preview(request: PostFinanceImportRequest, conn=Depends(get_db)):
    return _safe_preview(_run(preview_postfinance_import, conn, request.model_dump()))

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


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

from typing import Any

from pydantic import BaseModel, Field

class PostFinanceImportRequest(BaseModel):
    zip_content_base64: str = Field(min_length=4)
    overview_content_base64: str = Field(min_length=4)
    zip_file_name: str = "postfinance-documents.zip"
    overview_file_name: str = "postfinance-account-overview.pdf"
    account_roles: dict[str, str] | None = None

class PostFinanceConfirmRequest(PostFinanceImportRequest):
    preview_id: str
    confirmation_id: str
    confirm: bool = False

class PostFinancePreviewResponse(BaseModel):
    preview_id: str
    confirmation_id: str
    db_revision: str
    bundle_sha256: str
    zip_sha256: str
    overview_sha256: str
    parser_id: str
    parser_version: str
    snapshot_at: str
    document_count: int
    zip_document_count: int
    page_count: int
    position_count: int
    cash_count: int
    event_count: int
    event_counts: dict[str, int]
    quality_counts: dict[str, int]
    transfer_group_count: int
    total_chf: str
    securities_chf: str
    cash_chf: str
    stocks_chf: str
    etfs_chf: str
    component_total_chf: str
    reconciliation_difference_chf: str
    reconciliation_status: str
    open_orders: int
    account_roles_resolved: list[str]
    mapped_position_count: int
    duplicate: bool
    existing_batch_id: str | None = None
    conflict: bool
    conflicting_batch_id: str | None = None
    warnings: list[str]

class PostFinanceConfirmResponse(BaseModel):
    status: str
    batch_id: str
    snapshot_id: str | None = None
    audit_id: str
    idempotent: bool

class PostFinanceSummaryResponse(BaseModel):
    latest_snapshot: dict[str, Any] | None
    positions: list[dict[str, Any]]
    cash: list[dict[str, Any]]
    event_counts: dict[str, int]
    imports: list[dict[str, Any]]


Preview hides the raw hashes and internal batch IDs at the HTTP boundary, which addresses part of the privacy finding. I’m now checking confirm persistence and the frontend summary to see whether IDs or instrument identifiers leak through another response.


sed -n '760,1160p' src/jarvis_finance/services/postfinance_service.py


sed -n '620,980p' src/jarvis_finance/imports/postfinance_documents.py


sed -n '1,320p' tests/test_postfinance_sprint13.py


sed -n '1,320p' tests/unit/test_postfinance_documents.py


Output:
Chunk ID: 770cdc
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1874
Output:
                    _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",
                    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: faccb6
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1968
Output:
        if m:
            cash_total = _number(m.group(1), required=True)
        m = re.search(r"Totalwert\s+([0-9][0-9'’.,]*(?: [0-9]{3})*)", stripped)
        if m and total is None:
            total = _number(m.group(1), required=True)
        m = re.search(r"Zwischensumme Aktien in CHF\s+([0-9][0-9'’.,]*(?: [0-9]{3})*)", stripped)
        if m:
            stock_total = _number(m.group(1), required=True)
        m = re.search(r"Zwischensumme ETFs in CHF\s+([0-9][0-9'’.,]*(?: [0-9]{3})*)", stripped)
        if m:
            etf_total = _number(m.group(1), required=True)
    if len(positions) != 22 or len(cash) != 2:
        raise ValueError(
            "PostFinance account overview does not contain the expected complete snapshot"
        )
    if None in (cash_total, securities_total, total, stock_total, etf_total, open_orders):
        raise ValueError("PostFinance account overview lacks required totals")
    assert (
        cash_total is not None
        and securities_total is not None
        and total is not None
        and stock_total is not None
        and etf_total is not None
        and open_orders is not None
    )
    if abs(sum((p.market_value_chf for p in positions), Decimal("0")) - securities_total) > Decimal(
        "0.05"
    ):
        raise ValueError("PostFinance position sum does not reconcile")
    if abs(securities_total + cash_total - total) > Decimal("0.05"):
        raise ValueError("PostFinance account total does not reconcile")
    if abs(stock_total + etf_total - securities_total) > Decimal("0.05"):
        raise ValueError("PostFinance asset-class totals do not reconcile")
    return PFSnapshot(
        sha256(raw).hexdigest(),
        page_count,
        valuation_at,
        tuple(positions),
        tuple(cash),
        securities_total,
        cash_total,
        total,
        stock_total,
        etf_total,
        open_orders,
    )

def parse_postfinance_bundle(zip_raw: bytes, overview_raw: bytes) -> PFBundle:
    if not zip_raw or len(zip_raw) > MAX_ZIP_BYTES:
        raise ValueError("PostFinance ZIP is empty or too large")
    documents: list[PFDocument] = []
    try:
        with ZipFile(io.BytesIO(zip_raw)) as archive:
            infos = archive.infolist()
            if not infos or len(infos) > MAX_FILES:
                raise ValueError("PostFinance ZIP has no documents or exceeds the safe limit")
            if sum(item.file_size for item in infos) > MAX_UNCOMPRESSED_BYTES:
                raise ValueError("PostFinance ZIP is too large after extraction")
            for info in infos:
                path = PurePosixPath(info.filename.replace("\\", "/"))
                mode = (info.external_attr >> 16) & 0xFFFF
                if (
                    info.flag_bits & 0x1
                    or path.is_absolute()
                    or ".." in path.parts
                    or stat.S_ISLNK(mode)
                ):
                    raise ValueError("PostFinance ZIP contains an unsafe entry")
                if info.is_dir() or path.suffix.lower() != ".pdf" or info.file_size > MAX_PDF_BYTES:
                    raise ValueError("PostFinance ZIP contains an unsupported entry")
                raw = archive.read(info)
                text, page_count = _pdf_text(raw)
                kind = _classify(info.filename, text)
                reference = _reference(info.filename, text)
                account = _account_reference(text)
                account_hash = _hash("account", account) if account else None
                role = (
                    "etrading_cash"
                    if kind
                    in {
                        "trade_confirmation",
                        "corporate_action",
                        "custody_fee",
                        "account_statement",
                        "interest_statement",
                    }
                    else "unresolved_transfer"
                    if kind == "transfer_confirmation"
                    else "etrading_depot"
                )
                direction = (
                    "credit"
                    if "Zahlungsverkehr - Gutschrift" in text
                    else "debit"
                    if "Zahlungsverkehr - Belastung" in text
                    else "none"
                )
                identity = _hash(kind, reference, account_hash or "", direction)
                documents.append(
                    PFDocument(
                        sha256(raw).hexdigest(),
                        sha256(path.name.encode()).hexdigest(),
                        kind,
                        reference,
                        identity,
                        _document_date(info.filename, text),
                        account_hash,
                        role,
                        page_count,
                        raw,
                        text,
                    )
                )
    except ValueError:
        raise
    except BadZipFile as exc:
        raise ValueError("PostFinance source is not a valid ZIP") from exc
    # Every paired transfer contains one debit and one credit. In this verified export the
    # E-Trading statements independently evidence the credited leg; the debit leg is
    # E-Finance. This semantic rule is validated again for every pair below.
    resolved_docs: list[PFDocument] = []
    for d in documents:
        if d.account_role != "unresolved_transfer":
            resolved_docs.append(d)
            continue
        if "Zahlungsverkehr - Gutschrift" in d.text:
            role = "etrading_cash"
        elif "Zahlungsverkehr - Belastung" in d.text:
            role = "efinance"
        else:
            raise ValueError("PostFinance transfer role is ambiguous")
        resolved_docs.append(
            PFDocument(
                d.document_hash,
                d.filename_hash,
                d.document_type,
                d.semantic_reference,
                d.semantic_identity,
                d.document_date,
                d.account_reference_hash,
                role,
                d.page_count,
                d.raw,
                d.text,
            )
        )
    documents = resolved_docs
    groups: dict[str, list[PFDocument]] = {}
    for doc in documents:
        if doc.document_type == "transfer_confirmation":
            groups.setdefault(doc.semantic_reference, []).append(doc)
    if any(
        len(items) != 2 or {d.account_role for d in items} != {"efinance", "etrading_cash"}
        for items in groups.values()
    ):
        raise ValueError("PostFinance internal transfer pairing is incomplete or ambiguous")
    identities: dict[str, str] = {}
    for doc in documents:
        prior = identities.setdefault(doc.semantic_identity, doc.document_hash)
        if prior != doc.document_hash:
            raise ValueError(
                "PostFinance semantic document identity has conflicting original bytes"
            )
    event_list: list[PFEvent] = []
    for doc in documents:
        event_list.extend(_interest_events(doc))
        event = _event_from_document(doc)
        if event is not None:
            event_list.append(event)
    events = tuple(event_list)
    if len({event.event_fingerprint for event in events}) != len(events):
        raise ValueError("PostFinance bundle contains duplicate economic events")
    snapshot = _parse_snapshot(overview_raw)
    zip_hash = sha256(zip_raw).hexdigest()
    overview_hash = sha256(overview_raw).hexdigest()
    return PFBundle(
        zip_hash,
        overview_hash,
        _hash(zip_hash, overview_hash, PARSER_ID, PARSER_VERSION),
        tuple(documents),
        events,
        snapshot,
        tuple(sorted(_hash("transfer", ref)[:32] for ref in groups)),
        ("interest_components_document_only", "statements_reconciliation_only"),
    )


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

import base64
from dataclasses import replace
from decimal import Decimal
from hashlib import sha256

import pytest

from jarvis_finance.imports.postfinance_documents import (
    PFBundle,
    PFDocument,
    PFEvent,
    PFSnapshot,
    PFSnapshotCash,
    PFSnapshotPosition,
)
from jarvis_finance.services import postfinance_service as service
from jarvis_finance.storage.database import connect_memory
from jarvis_finance.storage.migrations import apply_migrations

DEPOT = "account_pf_depot_test"
CASH = "account_pf_trading_cash_test"

def make_conn():
    conn = connect_memory()
    apply_migrations(conn)
    conn.execute(
        "INSERT INTO platforms(platform_id,name,platform_type,country,default_currency,is_active,created_at) VALUES('pf','PostFinance','bank','CH','CHF',1,'2026-01-01')"
    )
    conn.execute(
        "INSERT INTO accounts(account_id,platform_id,account_name,account_type,currency,performance_included,is_active,created_at,portfolio_bucket) VALUES(?,'pf','PostFinance E-Trading','brokerage','CHF',1,1,'2026-01-01','postfinance')",
        (DEPOT,),
    )
    conn.execute(
        "INSERT INTO accounts(account_id,platform_id,account_name,account_type,currency,performance_included,is_active,created_at,portfolio_bucket) VALUES(?,'pf','PostFinance Cash-Konto','cash','CHF',1,1,'2026-01-01','cash')",
        (CASH,),
    )
    for index in range(22):
        instrument = f"instrument_pf_{index:02d}"
        label = f"Synthetic Asset {index + 1:02d}"
        conn.execute(
            "INSERT INTO instruments(instrument_id,name,ticker,isin,asset_class,currency,is_active,created_at) VALUES(?,?,?,?,?,?,1,?)",
            (
                instrument,
                label,
                f"S{index:02d}",
                f"CH{index:010d}",
                "equity" if index < 11 else "etf",
                "CHF",
                "2026-01-01",
            ),
        )
        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,category,data_quality_status,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
            (
                f"baseline_{index}",
                "2026-06-30",
                DEPOT,
                "pf",
                instrument,
                "1",
                "50",
                "50",
                "90",
                "90",
                "1",
                "90",
                "equity" if index < 11 else "etf",
                "complete",
                "2026-06-30T00:00:00Z",
            ),
        )
    conn.commit()
    return conn

def bundle(
    zip_raw: bytes = b"synthetic-zip", overview_raw: bytes = b"%PDF-synthetic-overview"
) -> PFBundle:
    zip_hash = sha256(zip_raw).hexdigest()
    overview_hash = sha256(overview_raw).hexdigest()
    doc_raw = b"%PDF-synthetic-trade"
    document = PFDocument(
        sha256(doc_raw).hexdigest(),
        sha256(b"trade.pdf").hexdigest(),
        "trade_confirmation",
        "synthetic-reference",
        sha256(b"semantic").hexdigest(),
        "2026-07-01",
        sha256(b"account").hexdigest(),
        "etrading_cash",
        1,
        doc_raw,
        "synthetic",
    )
    event = PFEvent(
        sha256(b"event").hexdigest(),
        document.document_hash,
        document.semantic_reference,
        "buy",
        "etrading_cash",
        "2026-07-01",
        "2026-07-03",
        "out",
        "Synthetic Asset 01",
        "CH0000000000",
        Decimal("1"),
        Decimal("50"),
        Decimal("50"),
        Decimal("1"),
        Decimal("0"),
        Decimal("51"),
        "CHF",
        Decimal("1"),
        None,
        "complete",
        (),
    )
    transfer_out = PFEvent(
        sha256(b"transfer-out").hexdigest(),
        document.document_hash,
        "transfer-reference",
        "internal_transfer",
        "efinance",
        "2026-07-02",
        "2026-07-02",
        "out",
        None,
        None,
        None,
        None,
        Decimal("10"),
        Decimal("0"),
        Decimal("0"),
        Decimal("10"),
        "CHF",
        Decimal("1"),
        "synthetic-transfer-group",
        "partial",
        ("internal_transfer_not_income_or_expense",),
    )
    transfer_in = PFEvent(
        sha256(b"transfer-in").hexdigest(),
        document.document_hash,
        "transfer-reference",
        "internal_transfer",
        "etrading_cash",
        "2026-07-02",
        "2026-07-02",
        "in",
        None,
        None,
        None,
        None,
        Decimal("10"),
        Decimal("0"),
        Decimal("0"),
        Decimal("10"),
        "CHF",
        Decimal("1"),
        "synthetic-transfer-group",
        "partial",
        ("internal_transfer_not_income_or_expense",),
    )
    positions = tuple(
        PFSnapshotPosition(
            sha256(f"row-{index}".encode()).hexdigest()[:32],
            f"Synthetic Asset {index + 1:02d}",
            f"synthetic asset {index + 1:02d}",
            "stock" if index < 11 else "etf",
            Decimal("1"),
            Decimal("50"),
            Decimal("50"),
            Decimal("100"),
            "CHF",
            Decimal("100"),
            Decimal("4.17"),
        )
        for index in range(22)
    )
    snapshot = PFSnapshot(
        overview_hash,
        5,
        "2026-07-27T09:14:30+02:00",
        positions,
        (
            PFSnapshotCash("EUR", Decimal("100"), Decimal("1"), Decimal("100")),
            PFSnapshotCash("USD", Decimal("100"), Decimal("1"), Decimal("100")),
        ),
        Decimal("2200"),
        Decimal("200"),
        Decimal("2400"),
        Decimal("1100"),
        Decimal("1100"),
        0,
    )
    return PFBundle(
        zip_hash,
        overview_hash,
        sha256((zip_hash + overview_hash).encode()).hexdigest(),
        (document,),
        (event, transfer_out, transfer_in),
        snapshot,
        ("synthetic-transfer-group",),
        (),
    )

def request(zip_raw: bytes = b"synthetic-zip", overview_raw: bytes = b"%PDF-synthetic-overview"):
    return {
        "zip_file_name": "documents.zip",
        "overview_file_name": "overview.pdf",
        "zip_content_base64": base64.b64encode(zip_raw).decode(),
        "overview_content_base64": base64.b64encode(overview_raw).decode(),
    }

def test_preview_confirm_and_reparse_are_atomic_idempotent_and_keep_roles_separate(
    monkeypatch, tmp_path
):
    conn = make_conn()
    calls = []
    monkeypatch.setenv("JARVIS_FINANCE_RUNTIME_DIR", str(tmp_path))
    monkeypatch.setattr(
        service,
        "parse_postfinance_bundle",
        lambda zip_raw, overview_raw: (
            calls.append((zip_raw, overview_raw)) or bundle(zip_raw, overview_raw)
        ),
    )
    payload = request()
    before = conn.total_changes
    preview = service.preview_postfinance_import(conn, payload)
    assert conn.total_changes == before
    assert preview["mapped_position_count"] == 22
    first = service.confirm_postfinance_import(
        conn,
        {
            **payload,
            "preview_id": preview["preview_id"],
            "confirmation_id": preview["confirmation_id"],
            "confirm": True,
        },
    )
    again_preview = service.preview_postfinance_import(conn, payload)
    second = service.confirm_postfinance_import(
        conn,
        {
            **payload,
            "preview_id": again_preview["preview_id"],
            "confirmation_id": again_preview["confirmation_id"],
            "confirm": True,
        },
    )
    assert len(calls) >= 4
    assert first["idempotent"] is False and second["idempotent"] is True
    safe_summary = service.get_postfinance_summary(conn)
    serialized = str(safe_summary)
    assert "account_pf_" not in serialized and "CH0000000000" not in serialized
    assert "snapshot_id" not in safe_summary["latest_snapshot"]
    assert "instrument_id" not in safe_summary["positions"][0]
    assert "batch_id" not in safe_summary["imports"][0]
    assert conn.execute("SELECT COUNT(*) FROM postfinance_import_batches").fetchone()[0] == 1
    assert conn.execute("SELECT COUNT(*) FROM postfinance_snapshots").fetchone()[0] == 1
    assert conn.execute("SELECT COUNT(*) FROM postfinance_snapshot_positions").fetchone()[0] == 22
    assert conn.execute("SELECT COUNT(*) FROM postfinance_account_roles").fetchone()[0] == 3
    roles = dict(conn.execute("SELECT role,account_id FROM postfinance_account_roles"))
    assert roles["etrading_depot"] == DEPOT
    assert roles["etrading_cash"] == CASH
    assert roles["efinance"] not in {DEPOT, CASH}
    assert len(set(roles.values())) == 3
    assert (
        conn.execute(
            "SELECT account_name FROM accounts WHERE account_id=?", (roles["efinance"],)
        ).fetchone()[0]
        == "PostFinance E-Finance"
    )
    transfer_rows = conn.execute(
        "SELECT account_id,activity_kind FROM transactions WHERE internal_transfer_group_id='synthetic-transfer-group' ORDER BY account_id"
    ).fetchall()
    assert {row["account_id"] for row in transfer_rows} == {roles["efinance"], CASH}
    assert {row["activity_kind"] for row in transfer_rows} == {"internal_transfer"}
    assert (
        conn.execute(
            "SELECT activity_kind FROM transactions WHERE source_type='postfinance_official_import'"
        ).fetchone()[0]
        == "trade"
    )
    assert len(list((tmp_path / "imports" / "postfinance" / "archive").glob("*"))) == 3

def test_provider_and_computed_cost_basis_are_separate(monkeypatch, tmp_path):
    conn = make_conn()
    monkeypatch.setenv("JARVIS_FINANCE_RUNTIME_DIR", str(tmp_path))
    monkeypatch.setattr(
        service,
        "parse_postfinance_bundle",
        lambda zip_raw, overview_raw: bundle(zip_raw, overview_raw),
    )
    payload = request()
    preview = service.preview_postfinance_import(conn, payload)
    service.confirm_postfinance_import(
        conn,
        {
            **payload,
            "preview_id": preview["preview_id"],
            "confirmation_id": preview["confirmation_id"],
            "confirm": True,
        },
    )
    first = conn.execute(
        "SELECT provider_cost_total_original,computed_cost_basis_original,computed_cost_basis_status FROM postfinance_snapshot_positions WHERE computed_cost_basis_original IS NOT NULL"
    ).fetchone()
    assert first[0] == "50" and first[1] == "51" and first[2] == "complete"
    assert conn.execute("SELECT COUNT(*) FROM postfinance_cost_basis_lots").fetchone()[0] == 1

def test_sell_consumes_documented_fifo_lot_before_cost_basis_persistence(monkeypatch, tmp_path):


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

from io import BytesIO
from zipfile import ZipFile, ZipInfo

import pytest

from jarvis_finance.imports import postfinance_documents as parser

def overview_text() -> str:
    lines = [
        "Portfolio  Stand 27/07/2026 09:14:30",
        "Offene Aufträge 0",
        "EUR  1.0000  100.00  x  x  100.00  x",
        "USD  1.0000  100.00  x  x  100.00  x",
        "Gesamt CHF  200.00  2'200.00  2'400.00",
        "Produkt  Anzahl  Einstandskurs  Einstandswert  Differenz  Kurs  x  Wert CHF  Anteil %",
        "Aktien",
    ]
    for index in range(11):
        lines.append(f"Synthetic Asset {index + 1:02d}  1  50  50  -  100 CHF  x  100  4.17")
    lines.extend(["Zwischensumme Aktien in CHF  0  0  1'100.00", "ETFs"])
    for index in range(11, 22):
        lines.append(f"Synthetic Asset {index + 1:02d}  1  50  50  -  100 CHF  x  100  4.17")
    lines.append("Zwischensumme ETFs in CHF  0  0  1'100.00")
    return "\n".join(lines)

def trade_text() -> str:
    return """Transaktionsbeleg
Börsentransaktion: Kauf
Unsere Referenz: 100
Kontonummer: 123456
Ausführungsdatum: 01.07.2026
Valutadatum: 03.07.2026
Synthetic Asset 01 ISIN: CH0000000000
Anzahl  Preis  Betrag
1  50  CHF  50
Total CHF 50
"""

def zip_bytes(name: str = "20260701_Borsenabrechnung_100.pdf", raw: bytes = b"%PDF-trade") -> bytes:
    output = BytesIO()
    with ZipFile(output, "w") as archive:
        archive.writestr(name, raw)
    return output.getvalue()

def test_synthetic_zip_pdf_bundle_is_parsed_and_reconciled(monkeypatch):
    monkeypatch.setattr(parser, "_pdf_text", lambda raw: (trade_text(), 1) if raw == b"%PDF-trade" else (overview_text(), 5))
    result = parser.parse_postfinance_bundle(zip_bytes(), b"%PDF-overview")
    assert len(result.documents) == 1
    assert len(result.events) == 1 and result.events[0].event_type == "buy"
    assert len(result.snapshot.positions) == 22 and len(result.snapshot.cash) == 2
    assert result.snapshot.total_value_chf == result.snapshot.securities_total_chf + result.snapshot.cash_total_chf
    assert result.snapshot.open_orders == 0

def test_zip_path_traversal_and_symlink_are_rejected(monkeypatch):
    monkeypatch.setattr(parser, "_pdf_text", lambda _raw: (overview_text(), 1))
    with pytest.raises(ValueError, match="unsafe"):
        parser.parse_postfinance_bundle(zip_bytes("../escape.pdf"), b"%PDF-overview")
    output = BytesIO()
    with ZipFile(output, "w") as archive:
        info = ZipInfo("link.pdf")
        info.external_attr = (0o120777 << 16)
        archive.writestr(info, b"%PDF-link")
    with pytest.raises(ValueError, match="unsafe"):
        parser.parse_postfinance_bundle(output.getvalue(), b"%PDF-overview")

def test_preview_parser_rejects_incomplete_or_non_pdf_sources(monkeypatch):
    monkeypatch.setattr(parser, "_pdf_text", lambda _raw: (overview_text(), 5))
    with pytest.raises(ValueError, match="unsupported entry"):
        parser.parse_postfinance_bundle(zip_bytes("note.txt"), b"%PDF-overview")
    monkeypatch.setattr(parser, "_pdf_text", lambda raw: (trade_text(), 1) if raw == b"%PDF-trade" else ("27/07/2026 09:14:30\nno positions", 1))
    with pytest.raises(ValueError, match="complete snapshot"):
        parser.parse_postfinance_bundle(zip_bytes(), b"%PDF-overview")


The FIFO path has an explicit pre-persistence sell pass over documented buy lots, so I’m checking the surrounding tests and constraints for edge cases that would still let a bad projection through. After that I’ll run the required verification commands for a review-level sanity check without changing anything.


sed -n '320,760p' tests/test_postfinance_sprint13.py


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


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


sed -n '1,320p' frontend/src/pages/PostFinancePage.test.ts


Output:
Chunk ID: faa365
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1148
Output:
def test_sell_consumes_documented_fifo_lot_before_cost_basis_persistence(monkeypatch, tmp_path):
    conn = make_conn()
    monkeypatch.setenv("JARVIS_FINANCE_RUNTIME_DIR", str(tmp_path))
    original = bundle()
    buy = replace(
        original.events[0],
        quantity=Decimal("2"),
        gross_original=Decimal("100"),
        net_original=Decimal("101"),
    )
    sell = replace(
        buy,
        event_fingerprint=sha256(b"sell-event").hexdigest(),
        event_type="sell",
        occurred_on="2026-07-10",
        direction="in",
        quantity=Decimal("1"),
        price_original=Decimal("60"),
        gross_original=Decimal("60"),
        fee_original=Decimal("1"),
        net_original=Decimal("59"),
    )
    fifo_bundle = replace(original, events=(buy, sell, *original.events[1:]))
    monkeypatch.setattr(service, "parse_postfinance_bundle", lambda _zip, _overview: fifo_bundle)
    payload = request()
    preview = service.preview_postfinance_import(conn, payload)
    service.confirm_postfinance_import(
        conn,
        {
            **payload,
            "preview_id": preview["preview_id"],
            "confirmation_id": preview["confirmation_id"],
            "confirm": True,
        },
    )
    lot = conn.execute(
        "SELECT quantity_acquired,quantity_remaining FROM postfinance_cost_basis_lots"
    ).fetchone()
    assert tuple(lot) == ("2", "1")
    computed = conn.execute(
        "SELECT computed_cost_basis_original,computed_cost_basis_status FROM postfinance_snapshot_positions WHERE computed_cost_basis_original IS NOT NULL"
    ).fetchone()
    assert tuple(computed) == ("50.5", "complete")

def test_same_snapshot_with_different_original_source_is_blocked(monkeypatch, tmp_path):
    conn = make_conn()
    monkeypatch.setenv("JARVIS_FINANCE_RUNTIME_DIR", str(tmp_path))
    monkeypatch.setattr(
        service,
        "parse_postfinance_bundle",
        lambda zip_raw, overview_raw: bundle(zip_raw, overview_raw),
    )
    payload = request()
    preview = service.preview_postfinance_import(conn, payload)
    service.confirm_postfinance_import(
        conn,
        {
            **payload,
            "preview_id": preview["preview_id"],
            "confirmation_id": preview["confirmation_id"],
            "confirm": True,
        },
    )
    alternate = request(b"synthetic-zip-revision", b"%PDF-synthetic-overview-revision")
    conflict = service.preview_postfinance_import(conn, alternate)
    assert conflict["conflict"] is True
    with pytest.raises(ValueError, match="conflict"):
        service.confirm_postfinance_import(
            conn,
            {
                **alternate,
                "preview_id": conflict["preview_id"],
                "confirmation_id": conflict["confirmation_id"],
                "confirm": True,
            },
        )
    assert conn.execute("SELECT COUNT(*) FROM postfinance_import_batches").fetchone()[0] == 1

def test_immutable_postfinance_source_and_snapshot_rows(monkeypatch, tmp_path):
    conn = make_conn()
    monkeypatch.setenv("JARVIS_FINANCE_RUNTIME_DIR", str(tmp_path))
    monkeypatch.setattr(
        service,
        "parse_postfinance_bundle",
        lambda zip_raw, overview_raw: bundle(zip_raw, overview_raw),
    )
    payload = request()
    preview = service.preview_postfinance_import(conn, payload)
    result = service.confirm_postfinance_import(
        conn,
        {
            **payload,
            "preview_id": preview["preview_id"],
            "confirmation_id": preview["confirmation_id"],
            "confirm": True,
        },
    )
    with pytest.raises(Exception, match="immutable"):
        conn.execute(
            "UPDATE postfinance_snapshots SET total_chf='0' WHERE snapshot_id=?",
            (result["snapshot_id"],),
        )
    with pytest.raises(Exception, match="cannot be deleted"):
        conn.execute(
            "DELETE FROM postfinance_snapshots WHERE snapshot_id=?", (result["snapshot_id"],)
        )
    immutable_columns = {
        "postfinance_batch_documents": "document_hash=document_hash",
        "postfinance_snapshot_positions": "created_at=created_at",
        "postfinance_snapshot_cash": "created_at=created_at",
        "postfinance_event_components": "created_at=created_at",
    }
    for table, assignment in immutable_columns.items():
        with pytest.raises(Exception, match="immutable"):
            conn.execute(f"UPDATE {table} SET {assignment}")
        with pytest.raises(Exception, match="cannot be deleted"):
            conn.execute(f"DELETE FROM {table}")


Output:
Chunk ID: 0845d6
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)


Output:
Chunk ID: 41a944
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 3121
Output:
<template>

    <LoadActionBar testid="refresh-postfinance" title="PostFinance · Depot & Ledger" description="Offizielle Depot-, Cash- und Ereignisdaten mit unveränderter Provenienz." :loading="loading" @load="loadData(true)" />
    <LocalDataStatus :backend-ok="backendOk" :runtime-ok="runtimeOk" :loading="loading" :error="error" :last-loaded-at="lastLoadedAt" />

E-Trading Depot

Offizieller Portfoliostand

E-Finance, Depot und Währungscash bleiben fachlich getrennt.

        <MoneyValue :value="String(summary?.latest_snapshot?.total_chf ?? '0.00')" />

Stichtag

{{ formatDateTime(String(summary.latest_snapshot.snapshot_at)) }}

Wertschriften

CHF {{ money(summary.latest_snapshot.securities_chf) }}

Cash

CHF {{ money(summary.latest_snapshot.cash_chf) }}

Reconciliation

{{ summary.latest_snapshot.reconciliation_status }} · CHF {{ money(summary.latest_snapshot.difference_chf) }}

<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

ZIP- und Kontoübersicht prüfen

Preview ist nebenwirkungsfrei. Confirm liest, hasht und parst beide Originalquellen erneut.

        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')" />
        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')" />

<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>

Quellen

{{ preview.document_count }} PDFs · {{ preview.page_count }} Seiten

Snapshot

{{ preview.position_count }} Positionen · {{ preview.cash_count }} Cashkonten

Ledger

{{ preview.event_count }} Ereignisse · {{ preview.transfer_group_count }} Transfergruppen

Reconciliation

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

Identische Quellen bereits bestätigt; erneuter Confirm ist idempotent.

Konflikt: dieselbe fachliche Identität besitzt andere Originalbytes. Confirm ist gesperrt.

{{ message }}

{{ actionError }}

Positionen & Cost Basis

Provider-Einstandswerte und dokumentenbasierte Berechnung bleiben getrennt.

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

{{ position.instrument_name }}

Menge
{{ position.quantity }}Marktwert
CHF {{ money(position.value_chf) }}Provider-Einstand
{{ money(position.provider_cost_total_original) }}Berechnet
{{ position.computed_cost_basis_original ? money(position.computed_cost_basis_original) : 'teilweise / nicht verfügbar' }}

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

Ledger-Ereignisse

{{ eventLabel(kind) }} {{ count }}

Importhistorie

<div v-for="item in summary?.imports ?? []" :key="`${item.snapshot_at}-${item.confirmed_at}`" class="rounded-xl border p-3 text-sm">
{{ formatDateTime(String(item.snapshot_at)) }} · CHF {{ money(item.total_chf) }}

{{ item.position_count }} Positionen · {{ item.document_count }} Dokumente · {{ item.reconciliation_status }}

Noch kein bestätigter Import.

</template>


Output:
Chunk ID: 9885a0
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 908
Output:
import { flushPromises, mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import PostFinancePage from './PostFinancePage.vue'
import * as postfinance from '@/api/postfinance'
import * as runtime from '@/api/runtime'

vi.mock('@/api/postfinance')
vi.mock('@/api/runtime')

const summary = {
  latest_snapshot: { snapshot_at: '2026-07-27T09:14:30+02:00', total_chf: '321149.23', securities_chf: '293465.16', cash_chf: '27684.07', reconciliation_status: 'matched', difference_chf: '0.00' },
  positions: [{ row_key: 'safe-row', instrument_name: 'Synthetic Asset', quantity: '1', value_chf: '100.00', provider_cost_total_original: '90.00', computed_cost_basis_original: null }],
  cash: [{ row_key: 'cash-eur', currency: 'EUR', amount_original: '10', value_chf: '9.50' }],
  event_counts: { buy: 1, internal_transfer: 2 },
  imports: [],
}

beforeEach(() => {
  vi.resetAllMocks()
  vi.mocked(runtime.getHealth).mockResolvedValue({ status: 'ok' } as never)
  vi.mocked(runtime.getRuntimeStatus).mockResolvedValue({ db_available: true } as never)
  vi.mocked(postfinance.getPostFinanceSummary).mockResolvedValue(summary)
})

describe('PostFinance Sprint 13 page', () => {
  it('renders separated snapshot, cost basis, cash, ledger and history without technical account identifiers', async () => {
    const wrapper = mount(PostFinancePage)
    await flushPromises()
    expect(wrapper.get('[data-testid="postfinance-page"]').text()).toContain('E-Finance, Depot und Währungscash')
    expect(wrapper.text()).toContain('Provider-Einstand')
    expect(wrapper.text()).toContain('Interne Transfers')
    expect(wrapper.text()).not.toContain('account_pf')
    expect(wrapper.find('.grid.gap-3.md\\:grid-cols-2.xl\\:grid-cols-3').exists()).toBe(true)
  })

  it('keeps Preview before Confirm and forwards preview identities unchanged', async () => {
    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: [] })
    vi.mocked(postfinance.confirmPostFinanceImport).mockResolvedValue({ status: 'confirmed', batch_id: 'batch', audit_id: 'audit', idempotent: false })
    const wrapper = mount(PostFinancePage)
    await flushPromises()
    await wrapper.get('[data-testid="postfinance-import-button"]').trigger('click')
    const vm = wrapper.vm as unknown as { payload: Record<string, string>; doPreview: () => Promise<void>; doConfirm: () => Promise<void> }
    vm.payload = { zip_file_name: 'documents.zip', overview_file_name: 'overview.pdf', zip_content_base64: 'eg==', overview_content_base64: 'cA==' }
    await vm.doPreview(); await flushPromises()
    expect(postfinance.previewPostFinanceImport).toHaveBeenCalledOnce()
    expect(wrapper.get('[data-testid="postfinance-confirm-button"]').attributes('disabled')).toBeUndefined()
    await vm.doConfirm(); await flushPromises()
    expect(postfinance.confirmPostFinanceImport).toHaveBeenCalledWith(expect.objectContaining({ preview_id: 'preview', confirmation_id: 'confirm', confirm: true }))
  })
})


rg -n "MIGRATION_VERSION|schema_version|_apply_compat_migrations|PRAGMA user_version|045|postfinance" src/jarvis_finance/storage/migrations.py src/jarvis_finance/storage/schema.py tests/unit/test_schema.py docs/sprint13-postfinance-ledger-import.md


rg -n "CREATE TABLE IF NOT EXISTS (transactions|positions_snapshot|cash_balances|account_value_snapshots|audit_log)|source_type|immutable|no_delete" src/jarvis_finance/storage/schema.py src/jarvis_finance/storage/migrations.py src/jarvis_finance/storage/postfinance_schema.py


sed -n '1,240p' docs/sprint13-postfinance-ledger-import.md


git diff -- tests/unit/test_schema.py tests/unit/test_portfolio_data_ingestion_reconciliation.py


Output:
Chunk ID: 66a397
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 443
Output:
docs/sprint13-postfinance-ledger-import.md:33:- `POST /api/v1/postfinance/imports/preview`
docs/sprint13-postfinance-ledger-import.md:34:- `POST /api/v1/postfinance/imports/confirm`
docs/sprint13-postfinance-ledger-import.md:35:- `GET /api/v1/postfinance/summary`
docs/sprint13-postfinance-ledger-import.md:36:- responsive page: `/portfolio/postfinance`
tests/unit/test_schema.py:4:from jarvis_finance.storage.migrations import apply_migrations, get_schema_version
tests/unit/test_schema.py:15:def test_schema_version_recorded() -> None:
tests/unit/test_schema.py:18:    assert get_schema_version(conn) == 45
src/jarvis_finance/storage/migrations.py:8:from .postfinance_schema import create_postfinance_ledger_import_v1
src/jarvis_finance/storage/migrations.py:10:MIGRATION_VERSION = 45
src/jarvis_finance/storage/migrations.py:11:MIGRATION_NAME = "045_postfinance_ledger_import_v1"
src/jarvis_finance/storage/migrations.py:62:def get_schema_version(conn: Connection) -> int:
src/jarvis_finance/storage/migrations.py:459:def _create_postfinance_baseline_mapping_audit_v1(conn: Connection) -> None:
src/jarvis_finance/storage/migrations.py:2096:def _apply_compat_migrations(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)
src/jarvis_finance/storage/migrations.py:2141:    _apply_compat_migrations(conn)
src/jarvis_finance/storage/migrations.py:2142:    existing = conn.execute("SELECT 1 FROM schema_migrations WHERE version = ?", (MIGRATION_VERSION,)).fetchone()
src/jarvis_finance/storage/migrations.py:2146:            (MIGRATION_VERSION, MIGRATION_NAME, utc_now(), checksum_sql(MIGRATION_NAME)),