railiance-platform/scripts/consumption_mode.py
codex 34a3123799
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Finish RAILIANCE-WP-0017 consumption-mode enforcement
Read the resource-control open/restricted signal and refuse new orders
that would exceed a restricted entity's published allowance. Open and
missing signals stay unchanged. Safety paths admit with an exception.
2026-08-15 14:56:02 +02:00

199 lines
6.9 KiB
Python
Executable file

#!/usr/bin/env python3
"""Admit or refuse a platform order from the resource-control consumption-mode signal.
RAILIANCE-WP-0017-T01. resource-control publishes the signal; this repo
enforces it. Missing or unknown mode is not restricted (facility runbook).
Only restricted + a new-order/elastic charge that exceeds the published
allowance is refused. Safety paths are admitted and recorded as exceptions.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from decimal import Decimal, InvalidOperation
from pathlib import Path
from typing import Any
REPO_DIR = Path(__file__).resolve().parents[1]
DEFAULT_SIGNAL = REPO_DIR / "data" / "consumption-mode" / "current.json"
ENTITY_RE_PREFIX = "entity:"
MODES = {"open", "restricted"}
CLASSES = {"new-order", "elastic", "safety", "committed"}
RAILIANCE = "entity:railiance"
def money(value: Any) -> Decimal | None:
if value is None or value == "":
return None
try:
amount = Decimal(str(value))
except (InvalidOperation, ValueError):
return None
if amount < 0:
return None
return amount.quantize(Decimal("0.01"))
def load_payload(path: Path) -> list[dict[str, Any]]:
if not path.is_file():
return []
raw = json.loads(path.read_text(encoding="utf-8"))
if isinstance(raw, list):
out: list[dict[str, Any]] = []
for item in raw:
if not isinstance(item, dict):
continue
if item.get("record_type") == "settlement_statement":
out.append(signal_from_statement(item))
else:
out.append(item)
return out
if isinstance(raw, dict) and raw.get("record_type") == "consumption_mode":
return [raw]
if isinstance(raw, dict) and isinstance(raw.get("signals"), list):
return [item for item in raw["signals"] if isinstance(item, dict)]
if isinstance(raw, dict) and raw.get("record_type") == "settlement_statement":
return [signal_from_statement(raw)]
return []
def signal_from_statement(statement: dict[str, Any]) -> dict[str, Any]:
return {
"schema_version": "0.1",
"record_type": "consumption_mode",
"financial_entity_id": statement.get("financial_entity_id"),
"period": statement.get("period"),
"consumption_mode": statement.get("consumption_mode"),
"new_transfer_charges_allowed_eur": statement.get("next_month_allowance_eur"),
"terms_version": statement.get("terms_version"),
}
def signal_for(signals: list[dict[str, Any]], entity_id: str) -> dict[str, Any] | None:
matches = [
item
for item in signals
if item.get("financial_entity_id") == entity_id
and item.get("record_type", "consumption_mode") == "consumption_mode"
]
return matches[-1] if matches else None
def decide(
*,
entity_id: str,
order_class: str,
estimate_eur: Decimal | None,
signal: dict[str, Any] | None,
) -> dict[str, Any]:
if entity_id == RAILIANCE:
return _result("admit", "railiance-self-use", "open", None, order_class)
if signal is None:
return _result("admit", "no-signal-not-restricted", None, None, order_class)
mode = signal.get("consumption_mode")
if mode not in MODES:
return _result("admit", "unknown-mode-not-restricted", None, None, order_class)
allowance = money(signal.get("new_transfer_charges_allowed_eur"))
if mode == "open":
return _result("admit", "open", mode, allowance, order_class)
if order_class == "safety":
return _result(
"admit",
"safety-exception",
mode,
allowance,
order_class,
exception=True,
)
if order_class == "committed":
return _result(
"admit",
"committed-flagged",
mode,
allowance,
order_class,
exception=True,
)
if allowance is None:
return _result("refuse", "restricted-without-allowance", mode, allowance, order_class)
if estimate_eur is None:
return _result("refuse", "restricted-estimate-required", mode, allowance, order_class)
if estimate_eur > allowance:
return _result("refuse", "exceeds-allowance", mode, allowance, order_class)
return _result("admit", "within-allowance", mode, allowance, order_class)
def _result(
decision: str,
reason: str,
mode: str | None,
allowance: Decimal | None,
order_class: str,
exception: bool = False,
) -> dict[str, Any]:
return {
"decision": decision,
"reason": reason,
"consumption_mode": mode,
"allowance_eur": None if allowance is None else f"{allowance:.2f}",
"order_class": order_class,
"exception": exception,
}
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__.split("\n\n", 1)[0])
parser.add_argument("command", choices=["check"], help="check an order against the signal")
parser.add_argument("--entity", required=True, help="financial_entity_id, e.g. entity:coulomb")
parser.add_argument(
"--class",
dest="order_class",
choices=sorted(CLASSES),
default="new-order",
)
parser.add_argument("--estimate-eur", help="transfer-price estimate for the new order")
parser.add_argument(
"--signal",
type=Path,
default=Path(os.environ.get("CONSUMPTION_MODE_FILE", DEFAULT_SIGNAL)),
help="JSON signal file (list, one record, or a settlement statement)",
)
return parser.parse_args(argv)
def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)
if not args.entity.startswith(ENTITY_RE_PREFIX) or len(args.entity) < 8:
print(f"[FAIL] --entity must be entity:<slug>: {args.entity}", file=sys.stderr)
return 2
estimate = money(args.estimate_eur)
if args.estimate_eur and estimate is None:
print(f"[FAIL] --estimate-eur is not a non-negative EUR amount", file=sys.stderr)
return 2
try:
signals = load_payload(args.signal)
except json.JSONDecodeError as exc:
print(f"[FAIL] signal is not JSON: {exc}", file=sys.stderr)
return 2
signal = signal_for(signals, args.entity)
result = decide(
entity_id=args.entity,
order_class=args.order_class,
estimate_eur=estimate,
signal=signal,
)
tag = "OK" if result["decision"] == "admit" else "REFUSE"
print(
f"[{tag}] {args.entity} {result['order_class']} "
f"mode={result['consumption_mode'] or 'unknown'} "
f"allowance={result['allowance_eur']} reason={result['reason']}"
)
if result["exception"]:
print("[EXCEPTION] restricted safety/committed path; record overage on the next statement")
return 0 if result["decision"] == "admit" else 2
if __name__ == "__main__":
sys.exit(main())