#!/usr/bin/env python3 """Produce planning evidence and consume fin-hub booked-cost projections.""" from __future__ import annotations import json import sys from calendar import monthrange from datetime import date from decimal import Decimal, ROUND_HALF_EVEN from pathlib import Path CENT = Decimal("0.01") def money(value: object) -> Decimal: return Decimal(str(value)).quantize(CENT, rounding=ROUND_HALF_EVEN) def money_text(value: object) -> str: return format(money(value), ".2f") def forecast_records(payload: dict, *, resource_id: str, source_ref: str) -> list[dict]: if payload.get("schema_version") != "0.1" or payload.get("record_type") != "forecast": raise ValueError("expected a resource-control forecast v0.1") if not resource_id.startswith("resource:"): raise ValueError("resource_id must use the resource: namespace") created_at = payload["created_at"] version = f"{payload['provider_id']}:{created_at}" records = [] for row in payload["rows"]: infrastructure = money(row["infrastructure_eur"]) labor = money(row["internal_labor_eur"]) if money(row["total_eur"]) != infrastructure + labor: raise ValueError("total_eur does not match infrastructure plus internal labor") year, month = (int(part) for part in row["period"].split("-")) period_start = date(year, month, 1) period_end = date(year, month, monthrange(year, month)[1]) records.append({ "schema_version": "0.1", "record_type": "forecast", "record_id": f"forecast:{payload['provider_id']}:{payload['cost_attribution_key']}:{row['period']}:{created_at}", "revision_of": payload.get("forecast_ref"), "resource_id": resource_id, "service_id": payload["provider_id"], "workload_id": payload["workload"], "tenant_id": None, "environment": "production", "cost_attribution_key": payload["cost_attribution_key"], "period_start": period_start.isoformat(), "period_end": period_end.isoformat(), "currency": "EUR", "source_evidence": [source_ref, *row.get("evidence", [])], "created_at": created_at, "scenario": payload.get("scenario") or "base", "forecast_version": version, "costs": { "infrastructure": money_text(infrastructure), "internal_labor": money_text(labor), "external_services": "0.00", "setup": "0.00", "other": "0.00" }, "uncertainty": None, "assumptions": [ f"database_gb={row['database_gb']}", f"stored_gb={row['stored_gb']}", f"wal_gb={row['wal_gb']}", f"restore_egress_gb={row['restore_egress_gb']}", f"write_requests={row['write_requests']}", f"read_requests={row['read_requests']}", f"internal_labor_hours={row['internal_labor_hours']}" ] }) return records def validate_booked_cost(record: dict) -> dict: required = { "schema_version", "record_type", "financial_fact_id", "adjustment_kind", "source_document_id", "source_line_id", "content_fingerprint", "provider", "accounting_period", "currency", "gross_amount", "adjustment_amount", "effective_amount", "source_evidence_ref", "recorded_at" } missing = required - record.keys() if missing: raise ValueError(f"booked-cost evidence missing {sorted(missing)}") if record["schema_version"] != "0.1" or record["record_type"] != "booked_cost": raise ValueError("unsupported booked-cost schema") if len(record["currency"]) != 3 or record["currency"] != record["currency"].upper(): raise ValueError("currency must be an uppercase three-letter code") if money(record["effective_amount"]) != money(record["gross_amount"]) + money(record["adjustment_amount"]): raise ValueError("effective_amount must equal gross plus adjustment") if record.get("tax_status") == "unknown" and record.get("tax_amount") is not None: raise ValueError("unknown tax must not have a tax amount") return record def reconcile(forecasts: list[dict], booked_costs: list[dict]) -> list[dict]: planned = { (record["period_start"][:7], record["currency"], record.get("cost_attribution_key")): money(record["costs"]["infrastructure"]) for record in forecasts if record["record_type"] == "forecast" } observed: dict[tuple[str, str, str | None], Decimal] = {} fact_ids: dict[tuple[str, str, str | None], list[str]] = {} for raw in booked_costs: record = validate_booked_cost(raw) key = (record["accounting_period"], record["currency"], record.get("cost_attribution_key")) observed[key] = observed.get(key, Decimal("0")) + money(record["effective_amount"]) fact_ids.setdefault(key, []).append(record["financial_fact_id"]) rows = [] for key in sorted(set(planned) | set(observed), key=lambda item: (item[0], item[1], item[2] or "")): forecast = planned.get(key) actual = observed.get(key) rows.append({ "period": key[0], "currency": key[1], "cost_attribution_key": key[2], "forecast_infrastructure": None if forecast is None else money_text(forecast), "booked_effective": None if actual is None else money_text(actual), "variance": None if forecast is None or actual is None else money_text(actual - forecast), "financial_fact_ids": fact_ids.get(key, []), "status": "reconciled" if forecast is not None and actual is not None else "missing-booked-cost" if forecast is not None else "missing-forecast" }) return rows def main() -> int: if len(sys.argv) < 3 or sys.argv[1] not in {"forecast", "reconcile"}: print(f"usage: {sys.argv[0]} forecast FORECAST.json [RESOURCE_ID] | reconcile FORECAST_EVIDENCE.json BOOKED_COST.json", file=sys.stderr) return 2 if sys.argv[1] == "forecast": payload = json.loads(Path(sys.argv[2]).read_text()) resource_id = sys.argv[3] if len(sys.argv) > 3 else "resource:platform_audit_storage" print(json.dumps(forecast_records(payload, resource_id=resource_id, source_ref=sys.argv[2]), indent=2)) return 0 forecasts = json.loads(Path(sys.argv[2]).read_text()) booked = json.loads(Path(sys.argv[3]).read_text()) print(json.dumps(reconcile(forecasts, booked), indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main())