#!/usr/bin/env python3 """Financial entity register and Terms V0.1 parameter loader.""" from __future__ import annotations import json import re import sys from decimal import Decimal from pathlib import Path ENTITY_ID = re.compile(r"^entity:[a-z0-9]+$") RAILIANCE = "entity:railiance" REQUIRED_ENTITY_IDS = ( "entity:binky", "entity:frontier", "entity:railiance", "entity:netkingdom", "entity:helixforge", "entity:coulomb", ) # Terms § 14 — tools must load these from data/terms, then assert this contract. V0_1_PARAMETERS = { "terms_version": "0.1", "currency": "EUR", "markup_rate": Decimal("0.20"), "railiance_self_markup_rate": Decimal("0.00"), "settlement_period": "calendar_month", "payment_term_days": 10, "interest_rate_per_year": Decimal("0.05"), "interest_convention": "simple_monthly", "default_credit_limit_eur": Decimal("1000.00"), "restricted_monthly_consumption_eur": Decimal("50.00"), "procuring_entity_id": RAILIANCE, "labor_rate_eur_per_hour": Decimal("60.00"), } def default_root() -> Path: return Path(__file__).resolve().parents[1] def load_json(path: Path) -> dict: return json.loads(path.read_text()) def validate_register(register: dict) -> dict[str, dict]: if register.get("schema_version") != "0.1": raise ValueError("entity register must use schema_version 0.1") entities = register.get("entities") or [] by_id: dict[str, dict] = {} for entity in entities: entity_id = entity.get("id", "") if not ENTITY_ID.fullmatch(entity_id): raise ValueError(f"invalid entity id: {entity_id}") if entity_id in by_id: raise ValueError(f"duplicate entity id: {entity_id}") by_id[entity_id] = entity missing = [entity_id for entity_id in REQUIRED_ENTITY_IDS if entity_id not in by_id] if missing: raise ValueError(f"entity register missing required ids: {missing}") extra = sorted(set(by_id) - set(REQUIRED_ENTITY_IDS)) if extra: raise ValueError(f"entity register has unknown ids: {extra}") if by_id[RAILIANCE]["role"] != "provider": raise ValueError("entity:railiance must have role provider") if by_id[RAILIANCE]["credit_limit_eur"] is not None: raise ValueError("entity:railiance has no credit limit under these terms") return by_id def require_entity(entity_id: str | None, entities: dict[str, dict] | None = None) -> str: if not entity_id or not ENTITY_ID.fullmatch(entity_id): raise ValueError(f"unknown entity id: {entity_id}") known = entities if entities is not None else {item: {} for item in REQUIRED_ENTITY_IDS} if entity_id not in known: raise ValueError(f"unknown entity id: {entity_id}") return entity_id def validate_terms(terms: dict) -> dict: if terms.get("schema_version") != "0.1": raise ValueError("procurement terms must use schema_version 0.1") if terms.get("terms_version") != V0_1_PARAMETERS["terms_version"]: raise ValueError("tools currently accept only terms_version 0.1") parsed = { "terms_version": terms["terms_version"], "currency": terms["currency"], "markup_rate": Decimal(terms["markup_rate"]), "railiance_self_markup_rate": Decimal(terms["railiance_self_markup_rate"]), "settlement_period": terms["settlement_period"], "payment_term_days": int(terms["payment_term_days"]), "interest_rate_per_year": Decimal(terms["interest_rate_per_year"]), "interest_convention": terms["interest_convention"], "default_credit_limit_eur": Decimal(terms["default_credit_limit_eur"]), "restricted_monthly_consumption_eur": Decimal(terms["restricted_monthly_consumption_eur"]), "procuring_entity_id": terms["procuring_entity_id"], "labor_rate_eur_per_hour": Decimal(terms["labor_rate_eur_per_hour"]), } for key, expected in V0_1_PARAMETERS.items(): if parsed[key] != expected: raise ValueError(f"terms parameter {key} is {parsed[key]!r}, expected {expected!r}") return parsed def load_register(root: Path | None = None) -> tuple[dict, dict[str, dict]]: root = root or default_root() payload = load_json(root / "data" / "entities" / "register.json") return payload, validate_register(payload) def load_terms(root: Path | None = None) -> dict: root = root or default_root() return validate_terms(load_json(root / "data" / "terms" / "procurement-v0.1.json")) def association_ok(record: dict) -> None: """A cost-bearing record needs a consuming entity or an explicit gap.""" entity_id = record.get("financial_entity_id") gap = record.get("entity_gap") procuring = record.get("procuring_entity_id") if not procuring: raise ValueError("cost-bearing records require procuring_entity_id") require_entity(procuring) if entity_id: require_entity(entity_id) if gap: raise ValueError("financial_entity_id and entity_gap cannot both be set") return if not gap: raise ValueError("untagged record needs financial_entity_id or an explicit entity_gap") def main() -> int: root = Path(sys.argv[1]) if len(sys.argv) > 1 else default_root() _, entities = load_register(root) terms = load_terms(root) print(f"entities: {len(entities)}") print(f"terms_version: {terms['terms_version']}") return 0 if __name__ == "__main__": raise SystemExit(main())