"""Private, deterministic aggregation for comprehensive settlement previews.""" from __future__ import annotations from datetime import date from .shared import CASHFLOW_KIND_SQL _AGGREGATION_VERSION = "comprehensive-v1" _SNAPSHOT_SCHEMA_VERSION = 1 _ALLOWED_KINDS = {"income", "expense", "excluded"} def _normalize_cashflow_policy(payload: dict) -> dict: if not isinstance(payload, dict) or set(payload) - {"savings_categories", "category_decisions"}: raise ValueError("cashflow policy must contain only supported keys") savings = payload.get("savings_categories", []) decisions = payload.get("category_decisions", []) if not isinstance(savings, list) or not isinstance(decisions, list): raise ValueError("policy categories and decisions must be lists") def pair(row, required): if not isinstance(row, dict) or set(row) != required: raise ValueError("invalid category policy entry") if any(not isinstance(row[k], str) for k in ("category", "subcategory")): raise ValueError("category fields must be strings") return row["category"], row["subcategory"] normalized_savings = [pair(x, {"category", "subcategory"}) for x in savings] if len(set(normalized_savings)) != len(normalized_savings): raise ValueError("duplicate savings category") normalized_decisions = [] for row in decisions: cat, sub = pair(row, {"category", "subcategory", "cashflow_kind", "reason"}) kind, reason = row["cashflow_kind"], row["reason"] if not isinstance(kind, str) or kind not in _ALLOWED_KINDS: raise ValueError("invalid cashflow decision kind") if not isinstance(reason, str) or not reason.strip(): raise ValueError("decision reason must be nonempty") normalized_decisions.append((cat, sub, kind, reason)) keys = [(x[0], x[1]) for x in normalized_decisions] if len(set(keys)) != len(keys): raise ValueError("duplicate category decision") return {"savings_categories": [{"category": c, "subcategory": s} for c, s in sorted(normalized_savings)], "category_decisions": [{"category": c, "subcategory": s, "cashflow_kind": k, "reason": r} for c, s, k, r in sorted(normalized_decisions)]} def _review_cashflow(conn, month: str, policy: dict) -> dict: policy = _normalize_cashflow_policy(policy) start = date.fromisoformat(month + "-01") end = date(start.year + (start.month == 12), 1 if start.month == 12 else start.month + 1, 1) rows = conn.execute(f"""SELECT t.id,t.transaction_date,t.type,t.amount,t.currency,t.category,t.subcategory, {CASHFLOW_KIND_SQL} AS originalcashflow_kind, lc.cashflow_kind AS category_cashflow_kind, lc.active AS category_active FROM transactions t LEFT JOIN ledger_categories lc ON lc.category=t.category AND lc.subcategory=t.subcategory WHERE t.status='active' AND t.transaction_date>=? AND t.transaction_date