feat: deliver schema 15 monthly settlement lifecycle

This commit is contained in:
2026-10-17 15:19:46 +09:00
parent dda76b3bbd
commit 38a854d525
79 changed files with 4003 additions and 1831 deletions
@@ -2,6 +2,9 @@
from __future__ import annotations
from datetime import date
import hashlib
import json
from decimal import Decimal
from .shared import CASHFLOW_KIND_SQL
@@ -10,6 +13,33 @@ _SNAPSHOT_SCHEMA_VERSION = 1
_ALLOWED_KINDS = {"income", "expense", "excluded"}
def _canonical_value(value):
"""Convert review inputs to a stable, JSON-safe representation."""
if isinstance(value, Decimal):
return format(value, "f")
if isinstance(value, dict):
return {str(k): _canonical_value(value[k]) for k in sorted(value)}
if isinstance(value, (list, tuple)):
return [_canonical_value(x) for x in value]
return value
def cashflow_fingerprint(review: dict, month: str) -> str:
"""Fingerprint policy, aggregation, and every included source row."""
payload = {
"month": month,
"aggregation_version": review.get("aggregation_version"),
"snapshot_schema_version": review.get("snapshot_schema_version"),
"cashflow_policy": review.get("cashflow_policy", {}),
"cashflow_items": review.get("cashflow_items", []),
"source_snapshot": review.get("source_snapshot", {}),
"blockers": review.get("cashflow_blockers", []),
}
encoded = json.dumps(_canonical_value(payload), ensure_ascii=False,
sort_keys=True, separators=(",", ":"), allow_nan=False)
return hashlib.sha256(encoded.encode("utf-8")).hexdigest()
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")
@@ -120,7 +150,9 @@ def _review_cashflow(conn, month: str, policy: dict) -> dict:
else: expense += applied
items = [{"cashflow_kind": k, "category": c, "subcategory": s, "amount": v[0], "transaction_count": v[1], "currency": cur}
for (k, c, s, cur), v in sorted(grouped.items())]
return {"cashflow_policy": policy, "aggregation_version": _AGGREGATION_VERSION,
result = {"cashflow_policy": policy, "aggregation_version": _AGGREGATION_VERSION,
"snapshot_schema_version": _SNAPSHOT_SCHEMA_VERSION, "income_total": income,
"expense_total": expense, "net_cashflow": income-expense, "cashflow_items": items,
"cashflow_blockers": blockers, "cashflow_warnings": [], "source_snapshot": {"transactions": source}}
result["cashflow_fingerprint"] = cashflow_fingerprint(result, month)
return result