feat: operational resource procurement facility
Implement RESOURCE-WP-0005: entity register, V0.1 terms parameters, entity association on inventory and planning records, transfer-price and credit-state arithmetic, monthly settlement, and entity views on the portfolio report. Live close emits nothing until delivered cost is known. Handoffs are FIN-WP-0006 and RAILIANCE-WP-0017.
This commit is contained in:
parent
325a505980
commit
f8d1c542d5
37 changed files with 1316 additions and 28 deletions
349
tools/settlement.py
Normal file
349
tools/settlement.py
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Delivered-cost transfer prices, credit state, and monthly settlement."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from calendar import monthrange
|
||||
from datetime import date, timedelta
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
from entities import RAILIANCE, association_ok, load_register, load_terms, require_entity
|
||||
from financial_exchange import money, money_text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def transfer_price(delivered_cost: Decimal | None, consuming_entity: str, terms: dict) -> Decimal | None:
|
||||
if delivered_cost is None:
|
||||
return None
|
||||
require_entity(consuming_entity)
|
||||
rate = terms["railiance_self_markup_rate"] if consuming_entity == RAILIANCE else terms["markup_rate"]
|
||||
return money(delivered_cost * (Decimal("1") + rate))
|
||||
|
||||
|
||||
def max_new_delivered_cost(allowance: Decimal | None, consuming_entity: str, terms: dict) -> Decimal | None:
|
||||
if allowance is None:
|
||||
return None
|
||||
if consuming_entity == RAILIANCE or allowance == Decimal("0.00"):
|
||||
return money(allowance) if consuming_entity == RAILIANCE else money(0)
|
||||
divisor = Decimal("1") + terms["markup_rate"]
|
||||
return money(allowance / divisor)
|
||||
|
||||
|
||||
def due_date(statement_date: date, terms: dict) -> date:
|
||||
return statement_date + timedelta(days=terms["payment_term_days"])
|
||||
|
||||
|
||||
def monthly_interest(overdue: Decimal, terms: dict) -> Decimal:
|
||||
if overdue <= 0:
|
||||
return money(0)
|
||||
return money(overdue * terms["interest_rate_per_year"] / Decimal(12))
|
||||
|
||||
|
||||
def apply_payment(interest_due: Decimal, principal: Decimal, payment: Decimal) -> dict:
|
||||
"""Interest first, then principal (Terms OQ-7)."""
|
||||
interest_due = money(interest_due)
|
||||
principal = money(principal)
|
||||
remaining = money(payment)
|
||||
interest_paid = money(min(interest_due, remaining))
|
||||
remaining = money(remaining - interest_paid)
|
||||
principal_paid = money(min(principal, remaining))
|
||||
remaining = money(remaining - principal_paid)
|
||||
return {
|
||||
"interest_paid": interest_paid,
|
||||
"principal_paid": principal_paid,
|
||||
"interest_remaining": money(interest_due - interest_paid),
|
||||
"principal_remaining": money(principal - principal_paid),
|
||||
"unapplied": remaining,
|
||||
}
|
||||
|
||||
|
||||
def credit_limit_for(entity_id: str, terms: dict, register: dict[str, dict]) -> Decimal | None:
|
||||
require_entity(entity_id, register)
|
||||
if entity_id == RAILIANCE:
|
||||
return None
|
||||
raw = register[entity_id].get("credit_limit_eur")
|
||||
if raw is None:
|
||||
return terms["default_credit_limit_eur"]
|
||||
return money(raw)
|
||||
|
||||
|
||||
def evaluate_credit(outstanding: Decimal, terms: dict, *, entity_id: str, register: dict[str, dict], overdue: bool) -> dict:
|
||||
outstanding = money(outstanding)
|
||||
limit = credit_limit_for(entity_id, terms, register)
|
||||
interest = monthly_interest(outstanding, terms) if overdue else money(0)
|
||||
if limit is None:
|
||||
return {
|
||||
"consumption_mode": "open",
|
||||
"credit_limit_eur": None,
|
||||
"credit_headroom_eur": None,
|
||||
"interest_this_month_eur": money_text(interest),
|
||||
"new_transfer_charges_allowed_eur": None,
|
||||
"max_new_delivered_cost_eur": None,
|
||||
}
|
||||
restricted = outstanding >= limit
|
||||
if restricted:
|
||||
allowance = money(max(Decimal("0.00"), terms["restricted_monthly_consumption_eur"] - interest))
|
||||
else:
|
||||
allowance = None
|
||||
headroom = money(max(Decimal("0.00"), limit - outstanding))
|
||||
return {
|
||||
"consumption_mode": "restricted" if restricted else "open",
|
||||
"credit_limit_eur": money_text(limit),
|
||||
"credit_headroom_eur": money_text(headroom),
|
||||
"interest_this_month_eur": money_text(interest),
|
||||
"new_transfer_charges_allowed_eur": None if allowance is None else money_text(allowance),
|
||||
"max_new_delivered_cost_eur": (
|
||||
None if allowance is None else money_text(max_new_delivered_cost(allowance, entity_id, terms))
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def statement_date_for(period: str) -> date:
|
||||
year, month = (int(part) for part in period.split("-"))
|
||||
if month == 12:
|
||||
return date(year + 1, 1, 1)
|
||||
return date(year, month + 1, 1)
|
||||
|
||||
|
||||
def _line_item(charge: dict, terms: dict) -> dict:
|
||||
entity_id = charge["financial_entity_id"]
|
||||
delivered = None if charge.get("delivered_cost") is None else money(charge["delivered_cost"])
|
||||
price = transfer_price(delivered, entity_id, terms)
|
||||
markup = None
|
||||
if delivered is not None and price is not None:
|
||||
markup = money(price - delivered)
|
||||
components = charge.get("components") or {}
|
||||
return {
|
||||
"resource_id": charge["resource_id"],
|
||||
"usage_summary": charge.get("usage_summary") or "",
|
||||
"delivered_cost_eur": None if delivered is None else money_text(delivered),
|
||||
"components": {
|
||||
"infrastructure": components.get("infrastructure"),
|
||||
"internal_labor": components.get("internal_labor"),
|
||||
"external_services": components.get("external_services"),
|
||||
"setup": components.get("setup"),
|
||||
"other": components.get("other"),
|
||||
},
|
||||
"markup_eur": None if markup is None else money_text(markup),
|
||||
"transfer_price_eur": None if price is None else money_text(price),
|
||||
"unknown_remainder": charge.get("unknown_remainder"),
|
||||
}
|
||||
|
||||
|
||||
def build_statement(
|
||||
*,
|
||||
entity_id: str,
|
||||
period: str,
|
||||
terms: dict,
|
||||
register: dict[str, dict],
|
||||
statement_date: date,
|
||||
lines: list[dict],
|
||||
prior_outstanding: Decimal,
|
||||
recognized_payments: list[dict],
|
||||
as_of: date | None = None,
|
||||
payment_status: str = "unknown",
|
||||
) -> dict | None:
|
||||
require_entity(entity_id, register)
|
||||
as_of = as_of or statement_date
|
||||
due = due_date(statement_date, terms)
|
||||
prior = money(prior_outstanding)
|
||||
paid = money(0)
|
||||
for payment in recognized_payments:
|
||||
if payment.get("financial_entity_id") == entity_id:
|
||||
paid = money(paid + money(payment["amount"]))
|
||||
payment_status = "recognized"
|
||||
known_cost = money(0)
|
||||
known_transfer = money(0)
|
||||
unknown = False
|
||||
for line in lines:
|
||||
if line["delivered_cost_eur"] is None or line["transfer_price_eur"] is None:
|
||||
unknown = True
|
||||
else:
|
||||
known_cost = money(known_cost + money(line["delivered_cost_eur"]))
|
||||
known_transfer = money(known_transfer + money(line["transfer_price_eur"]))
|
||||
prior_due = due_date(statement_date_for(_previous_period(period)), terms) if prior > 0 else due
|
||||
# Interest is quantified when prior outstanding is past its due date at as_of.
|
||||
overdue = prior > 0 and as_of > prior_due
|
||||
if prior > 0 and as_of == statement_date and statement_date <= due:
|
||||
# Brand-new statement of this period: prior from last period is overdue
|
||||
# only if its own due date has passed.
|
||||
overdue = as_of > prior_due
|
||||
interest = monthly_interest(prior, terms) if overdue else money(0)
|
||||
applied = apply_payment(interest, prior, paid)
|
||||
balance_before_interest = money(applied["principal_remaining"] + known_transfer)
|
||||
outstanding = money(balance_before_interest + applied["interest_remaining"])
|
||||
credit = evaluate_credit(
|
||||
balance_before_interest, terms, entity_id=entity_id, register=register, overdue=overdue,
|
||||
)
|
||||
has_activity = bool(lines) or prior > 0 or paid > 0 or interest > 0
|
||||
if not has_activity:
|
||||
return None
|
||||
entity = register[entity_id]
|
||||
provider = register[RAILIANCE]
|
||||
payment_instruction = None
|
||||
if entity_id != RAILIANCE and known_transfer > 0:
|
||||
payment_instruction = {
|
||||
"pay_from_account_ref": entity.get("account_ref"),
|
||||
"pay_to_account_ref": provider.get("account_ref"),
|
||||
}
|
||||
return {
|
||||
"schema_version": "0.1",
|
||||
"record_type": "settlement_statement",
|
||||
"terms_version": terms["terms_version"],
|
||||
"financial_entity_id": entity_id,
|
||||
"procuring_entity_id": RAILIANCE,
|
||||
"period": period,
|
||||
"statement_date": statement_date.isoformat(),
|
||||
"due_date": due.isoformat(),
|
||||
"currency": terms["currency"],
|
||||
"line_items": lines,
|
||||
"known_delivered_cost_eur": money_text(known_cost),
|
||||
"known_transfer_price_eur": money_text(known_transfer),
|
||||
"unknown_cost_remainder": unknown,
|
||||
"prior_outstanding_eur": money_text(prior),
|
||||
"recognized_payments_eur": None if payment_status == "unknown" and paid == 0 else money_text(paid),
|
||||
"payment_recognition": payment_status if paid > 0 else "unknown",
|
||||
"interest_eur": money_text(interest),
|
||||
"new_transfer_charges_eur": money_text(known_transfer),
|
||||
"outstanding_eur": money_text(outstanding),
|
||||
"credit_limit_eur": credit["credit_limit_eur"],
|
||||
"credit_headroom_eur": credit["credit_headroom_eur"],
|
||||
"consumption_mode": credit["consumption_mode"],
|
||||
"next_month_allowance_eur": credit["new_transfer_charges_allowed_eur"],
|
||||
"payment_instruction": payment_instruction,
|
||||
}
|
||||
|
||||
|
||||
def _previous_period(period: str) -> str:
|
||||
year, month = (int(part) for part in period.split("-"))
|
||||
if month == 1:
|
||||
return f"{year - 1}-12"
|
||||
return f"{year}-{month - 1:02d}"
|
||||
|
||||
|
||||
def close_fixture(payload: dict, terms: dict, register: dict[str, dict]) -> list[dict]:
|
||||
statement_date = date.fromisoformat(payload["statement_date"])
|
||||
as_of = date.fromisoformat(payload["as_of"]) if payload.get("as_of") else statement_date
|
||||
by_entity: dict[str, list[dict]] = {}
|
||||
for charge in payload.get("charges", []):
|
||||
association_ok({
|
||||
"financial_entity_id": charge["financial_entity_id"],
|
||||
"procuring_entity_id": charge.get("procuring_entity_id", RAILIANCE),
|
||||
"entity_gap": None,
|
||||
})
|
||||
by_entity.setdefault(charge["financial_entity_id"], []).append(_line_item(charge, terms))
|
||||
prior = {key: money(value) for key, value in (payload.get("prior_outstanding") or {}).items()}
|
||||
entities = sorted(set(by_entity) | set(prior) | {
|
||||
payment["financial_entity_id"] for payment in payload.get("recognized_payments") or []
|
||||
})
|
||||
statements = []
|
||||
for entity_id in entities:
|
||||
statement = build_statement(
|
||||
entity_id=entity_id,
|
||||
period=payload["period"],
|
||||
terms=terms,
|
||||
register=register,
|
||||
statement_date=statement_date,
|
||||
lines=by_entity.get(entity_id, []),
|
||||
prior_outstanding=prior.get(entity_id, money(0)),
|
||||
recognized_payments=payload.get("recognized_payments") or [],
|
||||
as_of=as_of,
|
||||
payment_status=payload.get("payment_recognition", "unknown"),
|
||||
)
|
||||
if statement:
|
||||
statements.append(statement)
|
||||
return statements
|
||||
|
||||
|
||||
def collect_live_charges(root: Path, period: str) -> list[dict]:
|
||||
"""Live close only emits a charge when delivered cost is known. Unknown is not zero."""
|
||||
charges = []
|
||||
year, month = (int(part) for part in period.split("-"))
|
||||
period_end = date(year, month, monthrange(year, month)[1])
|
||||
for path in sorted((root / "data" / "resources").glob("*.json")):
|
||||
resource = json.loads(path.read_text())
|
||||
if resource.get("status") in {"rejected", "retired"}:
|
||||
continue
|
||||
commissioned = resource.get("lifecycle", {}).get("commissioned_on")
|
||||
if commissioned and date.fromisoformat(commissioned) > period_end:
|
||||
continue
|
||||
proposed = resource.get("lifecycle", {}).get("proposed_on")
|
||||
if resource.get("status") == "proposed" and proposed and date.fromisoformat(proposed) > period_end:
|
||||
continue
|
||||
# No booked delivered cost lives on inventory records today.
|
||||
# A control-cycle actual with known infrastructure would be the source;
|
||||
# until then there are no live charges.
|
||||
del path
|
||||
control_dir = root / "data" / "control-cycle"
|
||||
if control_dir.exists():
|
||||
for path in sorted(control_dir.glob("*.json")):
|
||||
record = json.loads(path.read_text())
|
||||
if record.get("period") != period:
|
||||
continue
|
||||
costs = record.get("costs") or {}
|
||||
parts = [costs.get("infrastructure"), costs.get("internal_labor"), costs.get("external_labor")]
|
||||
if any(part is None for part in parts):
|
||||
continue
|
||||
entity_id = record.get("financial_entity_id")
|
||||
if not entity_id:
|
||||
continue
|
||||
charges.append({
|
||||
"financial_entity_id": entity_id,
|
||||
"procuring_entity_id": record.get("procuring_entity_id", RAILIANCE),
|
||||
"resource_id": record["resource_id"],
|
||||
"delivered_cost": money_text(sum(parts)),
|
||||
"usage_summary": f"control-cycle {record['record_type']} {record['record_id']}",
|
||||
"components": {
|
||||
"infrastructure": None if costs.get("infrastructure") is None else money_text(costs["infrastructure"]),
|
||||
"internal_labor": None if costs.get("internal_labor") is None else money_text(costs["internal_labor"]),
|
||||
"external_services": None if costs.get("external_labor") is None else money_text(costs["external_labor"]),
|
||||
"setup": None,
|
||||
"other": None,
|
||||
},
|
||||
})
|
||||
return charges
|
||||
|
||||
|
||||
def close_live(root: Path, period: str) -> list[dict]:
|
||||
terms = load_terms(root)
|
||||
_, register = load_register(root)
|
||||
payload = {
|
||||
"period": period,
|
||||
"statement_date": statement_date_for(period).isoformat(),
|
||||
"charges": collect_live_charges(root, period),
|
||||
"prior_outstanding": {},
|
||||
"recognized_payments": [],
|
||||
"payment_recognition": "unknown",
|
||||
}
|
||||
return close_fixture(payload, terms, register)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Monthly settlement close")
|
||||
parser.add_argument("command", choices=["close"])
|
||||
parser.add_argument("--period", help="YYYY-MM")
|
||||
parser.add_argument("--fixture", help="path to a settlement fixture")
|
||||
parser.add_argument("--root", default=str(ROOT))
|
||||
args = parser.parse_args()
|
||||
root = Path(args.root)
|
||||
terms = load_terms(root)
|
||||
_, register = load_register(root)
|
||||
if args.fixture:
|
||||
payload = json.loads(Path(args.fixture).read_text())
|
||||
statements = close_fixture(payload, terms, register)
|
||||
else:
|
||||
if not args.period:
|
||||
print("PERIOD=YYYY-MM is required without --fixture", file=sys.stderr)
|
||||
return 2
|
||||
statements = close_live(root, args.period)
|
||||
print(json.dumps(statements, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue