feat: extend comprehensive monthly settlement snapshots (#18)
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from .shared import read_db, DatabaseNotInitializedError
|
||||
|
||||
from .assets_service import get_asset_snapshot_report
|
||||
from .imports_service import list_ledger_import_batches
|
||||
from .reconciliation_service import get_balance_reconciliation_report
|
||||
@@ -130,3 +134,122 @@ def get_monthly_settlement_summary(month: str = "", db_path: str | None = None)
|
||||
},
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
|
||||
def get_monthly_settlement_snapshot(month: str = "", run_id: str = "", db_path: str | None = None) -> dict[str, Any]:
|
||||
"""Read a persisted monthly settlement close without consulting live ledger state."""
|
||||
if not isinstance(month, str) or not isinstance(run_id, str):
|
||||
return fail("validation_error", "month and run_id must be strings")
|
||||
try:
|
||||
selected_month = normalize_month(month) if month else ""
|
||||
if selected_month and (len(selected_month) != 7 or selected_month[4] != "-"):
|
||||
raise ValueError("invalid month")
|
||||
except (TypeError, ValueError):
|
||||
return fail("validation_error", "invalid settlement month")
|
||||
|
||||
def parsed(value: Any, default: Any):
|
||||
if value in (None, ""):
|
||||
return default
|
||||
return json.loads(value)
|
||||
|
||||
try:
|
||||
with read_db(db_path) as conn:
|
||||
tables = {r["name"] for r in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")}
|
||||
if "asset_snapshot_runs" not in tables or "asset_snapshots" not in tables:
|
||||
return fail("not_found", "settlement snapshot not found")
|
||||
run = None
|
||||
rows = []
|
||||
legacy = False
|
||||
metadata: dict[str, Any] = {}
|
||||
if run_id:
|
||||
run = conn.execute("SELECT rowid AS _run_rowid,* FROM asset_snapshot_runs WHERE id=?", (run_id,)).fetchone()
|
||||
if run:
|
||||
from .assets_service import _snapshot_rows_for_run
|
||||
rows, metadata = _snapshot_rows_for_run(conn, run_id)
|
||||
else:
|
||||
cols = {r["name"] for r in conn.execute("PRAGMA table_info(asset_snapshots)")}
|
||||
if {"snapshot_run_id", "closed_at"} <= cols:
|
||||
rows = conn.execute("SELECT rowid AS _rowid,* FROM asset_snapshots WHERE snapshot_run_id=? AND closed_at<>'' ORDER BY rowid", (run_id,)).fetchall()
|
||||
if rows:
|
||||
legacy = True
|
||||
metadata = {"snapshot_run_id": run_id, "legacy_closed": True, "close_status": "legacy_read_only"}
|
||||
else:
|
||||
if selected_month:
|
||||
run = conn.execute("SELECT rowid AS _run_rowid,* FROM asset_snapshot_runs WHERE basis_date=? ORDER BY rowid DESC LIMIT 1", (_month_end_date(selected_month),)).fetchone()
|
||||
if run is None:
|
||||
basis = _month_end_date(selected_month) if selected_month else ""
|
||||
if basis:
|
||||
from .assets_service import _snapshot_rows_for_latest_run
|
||||
rows, metadata = _snapshot_rows_for_latest_run(conn, basis)
|
||||
legacy = bool(metadata.get("legacy_closed"))
|
||||
if legacy:
|
||||
rows = [r for r in rows if r["snapshot_run_id"] == metadata.get("snapshot_run_id") and r["closed_at"]]
|
||||
elif not metadata.get("snapshot_run_id"):
|
||||
rows = []
|
||||
if metadata.get("snapshot_run_id"):
|
||||
run = conn.execute("SELECT rowid AS _run_rowid,* FROM asset_snapshot_runs WHERE id=?", (metadata["snapshot_run_id"],)).fetchone()
|
||||
if not run and not rows:
|
||||
return fail("not_found", "settlement snapshot not found")
|
||||
if run:
|
||||
rid = run["id"]
|
||||
basis_date = run["basis_date"]
|
||||
run_month = run["settlement_month"] if "settlement_month" in run.keys() else None
|
||||
if selected_month and ((run_month and run_month != selected_month) or (not run_month and basis_date[:7] != selected_month)):
|
||||
return fail("validation_error", "run does not match settlement month")
|
||||
if not metadata:
|
||||
from .assets_service import _snapshot_rows_for_run
|
||||
rows, metadata = _snapshot_rows_for_run(conn, rid)
|
||||
else:
|
||||
rid = metadata.get("snapshot_run_id", run_id)
|
||||
basis_date = rows[0]["basis_date"]
|
||||
if selected_month and basis_date[:7] != selected_month:
|
||||
return fail("validation_error", "run does not match settlement month")
|
||||
legacy = True
|
||||
keys = set(run.keys()) if run else set()
|
||||
def field(name): return run[name] if run and name in keys else None
|
||||
from .settlement_cashflow_service import _AGGREGATION_VERSION
|
||||
if (field("snapshot_schema_version") is not None or field("aggregation_version") is not None) and (
|
||||
field("snapshot_schema_version") != 1 or field("aggregation_version") != _AGGREGATION_VERSION
|
||||
):
|
||||
raise ValueError("unsupported comprehensive snapshot version")
|
||||
comprehensive = bool(field("snapshot_schema_version") == 1 and not legacy)
|
||||
legacy = not comprehensive
|
||||
cashflows = []
|
||||
if comprehensive and "settlement_cashflow_snapshots" in tables:
|
||||
cashflows = [dict(r) for r in conn.execute("SELECT cashflow_kind,category,subcategory,amount,transaction_count,currency FROM settlement_cashflow_snapshots WHERE settlement_run_id=? ORDER BY cashflow_kind,category,subcategory,currency", (rid,))]
|
||||
totals = {"income_total": field("income_total") if comprehensive else None, "expense_total": field("expense_total") if comprehensive else None, "net_cashflow": field("net_cashflow") if comprehensive else None, "total_assets": field("total_assets") if comprehensive else None, "total_liabilities": field("total_liabilities") if comprehensive else None, "net_worth": field("net_worth") if comprehensive else None}
|
||||
safe_items = []
|
||||
for row in rows:
|
||||
d = dict(row)
|
||||
safe_items.append({k: d.get(k) for k in ("id","source_type","source_id","name","amount","currency","liquidity_bucket","valuation_source","snapshot_run_id","correction_of_run_id","correction_reason","is_correction","closed_at","valuation_id","selection_policy","verification_status","evidence_kind","exclusion_reason","created_at") if k in d})
|
||||
source = parsed(field("source_snapshot_json"), {}) if comprehensive else {}
|
||||
if not isinstance(source, dict): raise ValueError()
|
||||
if comprehensive:
|
||||
tx_fields = ("id", "transaction_date", "type", "amount", "currency", "category", "subcategory", "original_cashflow_kind", "originalcashflow_kind", "cashflow_kind", "applied_amount", "sourceprovenance", "category_cashflow_kind", "category_active")
|
||||
asset_fields = ("id", "source_id", "source_type", "valuation_id", "valuation_date", "basis_date", "amount", "currency", "liquidity_bucket", "selection_mode", "applied_amount", "authority_source", "is_liability", "status", "verified", "verification_status", "evidence_kind", "exclusion_reason", "state_hash")
|
||||
def project(records, fields):
|
||||
if not isinstance(records, list) or any(not isinstance(x, dict) for x in records):
|
||||
raise ValueError()
|
||||
result = []
|
||||
for record in records:
|
||||
item = {k: record[k] for k in fields if k in record}
|
||||
if any(value is not None and not isinstance(value, (str, int, bool)) for value in item.values()):
|
||||
raise ValueError()
|
||||
for key, allowed in (("decision", ("category", "subcategory", "cashflow_kind", "reason")), ("source_classification", ("id", "category", "subcategory", "cashflow_kind", "active"))):
|
||||
value = record.get(key)
|
||||
if isinstance(value, dict):
|
||||
item[key] = {k: value[k] for k in allowed if k in value and (value[k] is None or isinstance(value[k], (str, int, bool)))}
|
||||
result.append(item)
|
||||
return result
|
||||
source = {"transactions": project(source.get("transactions", []), tx_fields), "assets": project(source.get("assets", []), asset_fields)}
|
||||
policy = parsed(field("cashflow_policy_json"), {}) if comprehensive else None
|
||||
if comprehensive:
|
||||
from .settlement_cashflow_service import _normalize_cashflow_policy
|
||||
if not isinstance(policy, dict):
|
||||
raise ValueError()
|
||||
policy = _normalize_cashflow_policy(policy)
|
||||
return ok(source="settlement_snapshot", snapshot_run_id=rid, run_id=rid, settlement_month=field("settlement_month") or basis_date[:7], basis_date=basis_date, currency=field("currency"), is_closed_snapshot=True, is_comprehensive=comprehensive, legacy=legacy, aggregation_version=field("aggregation_version") if comprehensive else None, snapshot_schema_version=field("snapshot_schema_version") if comprehensive else None, cashflow_policy=policy, cashflow_items=cashflows, items=safe_items, source_snapshot=source, totals=totals, **totals, close_status=(field("close_status") or metadata.get("close_status", "legacy_read_only")), correction={"is_correction": bool(field("correction_of_run_id") or metadata.get("is_correction")), "correction_of_run_id": field("correction_of_run_id") or metadata.get("correction_of_run_id", ""), "correction_reason": field("correction_reason") or metadata.get("correction_reason", "")})
|
||||
except DatabaseNotInitializedError:
|
||||
return fail("db_not_initialized", "ledger database is not initialized")
|
||||
except Exception:
|
||||
return fail("db_error", "settlement snapshot read failed")
|
||||
|
||||
Reference in New Issue
Block a user