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
92
tests/test_entities.py
Normal file
92
tests/test_entities.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import json
|
||||
import sys
|
||||
import unittest
|
||||
from copy import deepcopy
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).parents[1]
|
||||
sys.path.insert(0, str(ROOT / "tools"))
|
||||
from entities import (
|
||||
REQUIRED_ENTITY_IDS,
|
||||
V0_1_PARAMETERS,
|
||||
association_ok,
|
||||
load_register,
|
||||
load_terms,
|
||||
require_entity,
|
||||
validate_register,
|
||||
validate_terms,
|
||||
)
|
||||
from portfolio import validate_record
|
||||
|
||||
|
||||
class EntityRegisterTest(unittest.TestCase):
|
||||
def test_register_contains_exactly_the_six_entities(self):
|
||||
_, entities = load_register(ROOT)
|
||||
self.assertEqual(set(REQUIRED_ENTITY_IDS), set(entities))
|
||||
self.assertEqual("provider", entities["entity:railiance"]["role"])
|
||||
self.assertIsNone(entities["entity:railiance"]["credit_limit_eur"])
|
||||
|
||||
def test_unknown_entity_id_is_rejected(self):
|
||||
with self.assertRaisesRegex(ValueError, "unknown entity id"):
|
||||
require_entity("entity:unknown")
|
||||
|
||||
def test_terms_v0_1_match_published_parameters(self):
|
||||
terms = load_terms(ROOT)
|
||||
self.assertEqual(Decimal("0.20"), terms["markup_rate"])
|
||||
self.assertEqual(Decimal("0.00"), terms["railiance_self_markup_rate"])
|
||||
self.assertEqual(10, terms["payment_term_days"])
|
||||
self.assertEqual(Decimal("0.05"), terms["interest_rate_per_year"])
|
||||
self.assertEqual(Decimal("1000.00"), terms["default_credit_limit_eur"])
|
||||
self.assertEqual(Decimal("50.00"), terms["restricted_monthly_consumption_eur"])
|
||||
self.assertEqual("entity:railiance", terms["procuring_entity_id"])
|
||||
self.assertEqual(V0_1_PARAMETERS, {key: terms[key] for key in V0_1_PARAMETERS})
|
||||
|
||||
def test_wrong_markup_fails_closed(self):
|
||||
payload = json.loads((ROOT / "data/terms/procurement-v0.1.json").read_text())
|
||||
payload["markup_rate"] = "0.25"
|
||||
with self.assertRaisesRegex(ValueError, "markup_rate"):
|
||||
validate_terms(payload)
|
||||
|
||||
def test_extra_entity_fails_closed(self):
|
||||
payload = json.loads((ROOT / "data/entities/register.json").read_text())
|
||||
payload["entities"].append({
|
||||
"id": "entity:other", "display_name": "Other", "role": "consumer",
|
||||
"credit_limit_eur": "1000.00", "account_ref": None, "status": "active",
|
||||
})
|
||||
with self.assertRaisesRegex(ValueError, "unknown ids"):
|
||||
validate_register(payload)
|
||||
|
||||
|
||||
class AssociationTest(unittest.TestCase):
|
||||
def test_untagged_record_is_rejected(self):
|
||||
with self.assertRaisesRegex(ValueError, "untagged record"):
|
||||
association_ok({"procuring_entity_id": "entity:railiance"})
|
||||
|
||||
def test_explicit_gap_is_accepted(self):
|
||||
association_ok({
|
||||
"financial_entity_id": None,
|
||||
"procuring_entity_id": "entity:railiance",
|
||||
"entity_gap": "no allocation driver",
|
||||
})
|
||||
|
||||
def test_each_registered_entity_is_accepted(self):
|
||||
for entity_id in REQUIRED_ENTITY_IDS:
|
||||
association_ok({
|
||||
"financial_entity_id": entity_id,
|
||||
"procuring_entity_id": "entity:railiance",
|
||||
"entity_gap": None,
|
||||
})
|
||||
|
||||
def test_inventory_records_are_associated(self):
|
||||
for path in (ROOT / "data/resources").glob("*.json"):
|
||||
record = json.loads(path.read_text())
|
||||
with self.subTest(path=path):
|
||||
validate_record(record)
|
||||
|
||||
def test_missing_entity_on_a_copy_is_rejected(self):
|
||||
record = deepcopy(json.loads(next((ROOT / "data/resources").glob("*.json")).read_text()))
|
||||
record["financial_entity_id"] = None
|
||||
record["entity_gap"] = None
|
||||
with self.assertRaisesRegex(ValueError, "untagged record"):
|
||||
validate_record(record)
|
||||
|
|
@ -4,6 +4,7 @@ from datetime import date
|
|||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parents[1] / "tools"))
|
||||
from entities import REQUIRED_ENTITY_IDS
|
||||
from portfolio_report import (
|
||||
build,
|
||||
cost_section,
|
||||
|
|
@ -158,6 +159,17 @@ class LivePortfolioTest(unittest.TestCase):
|
|||
def test_report_is_deterministic_for_a_fixed_date(self):
|
||||
self.assertEqual(self.report, build(ROOT, TODAY))
|
||||
|
||||
def test_every_financial_entity_is_listed_and_unattributed_cost_is_named(self):
|
||||
ids = [row["financial_entity_id"] for row in self.report["entities"]["entities"]]
|
||||
self.assertEqual(list(REQUIRED_ENTITY_IDS), ids)
|
||||
self.assertIsNone(self.report["entities"]["known_monthly_spend_eur"])
|
||||
unattributed = {row["resource_id"] for row in self.report["entities"]["unattributed_resources"]}
|
||||
self.assertIn("resource:hosteurope:railiance01", unattributed)
|
||||
coulomb = next(row for row in self.report["entities"]["entities"] if row["financial_entity_id"] == "entity:coulomb")
|
||||
self.assertIn("resource:tenant:coulomb:coulomb-social", coulomb["dedicated_resources"])
|
||||
self.assertTrue(coulomb["shared_shares"])
|
||||
self.assertIsNone(coulomb["consumption_mode"])
|
||||
|
||||
|
||||
class DeliveredEvidenceTest(unittest.TestCase):
|
||||
"""RAILIANCE-WP-0016 delivered apps-pg evidence; partial delivery must not
|
||||
|
|
|
|||
116
tests/test_settlement.py
Normal file
116
tests/test_settlement.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import json
|
||||
import sys
|
||||
import unittest
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).parents[1]
|
||||
sys.path.insert(0, str(ROOT / "tools"))
|
||||
from entities import load_register, load_terms
|
||||
from financial_exchange import money
|
||||
from settlement import (
|
||||
apply_payment,
|
||||
close_fixture,
|
||||
close_live,
|
||||
due_date,
|
||||
evaluate_credit,
|
||||
monthly_interest,
|
||||
transfer_price,
|
||||
)
|
||||
|
||||
|
||||
class ArithmeticTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.terms = load_terms(ROOT)
|
||||
_, self.register = load_register(ROOT)
|
||||
|
||||
def test_coulomb_ordinary_month_section_15_1(self):
|
||||
price = transfer_price(money("200.00"), "entity:coulomb", self.terms)
|
||||
self.assertEqual(money("240.00"), price)
|
||||
self.assertEqual(date(2026, 9, 11), due_date(date(2026, 9, 1), self.terms))
|
||||
credit = evaluate_credit(
|
||||
money("240.00"), self.terms, entity_id="entity:coulomb",
|
||||
register=self.register, overdue=False,
|
||||
)
|
||||
self.assertEqual("open", credit["consumption_mode"])
|
||||
self.assertEqual("760.00", credit["credit_headroom_eur"])
|
||||
self.assertIsNone(credit["new_transfer_charges_allowed_eur"])
|
||||
|
||||
def test_unpaid_statement_interest_section_15_2(self):
|
||||
self.assertEqual(money("1.00"), monthly_interest(money("240.00"), self.terms))
|
||||
|
||||
def test_helixforge_at_default_limit_section_15_3(self):
|
||||
credit = evaluate_credit(
|
||||
money("1000.00"), self.terms, entity_id="entity:helixforge",
|
||||
register=self.register, overdue=True,
|
||||
)
|
||||
self.assertEqual("restricted", credit["consumption_mode"])
|
||||
self.assertEqual("4.17", credit["interest_this_month_eur"])
|
||||
self.assertEqual("45.83", credit["new_transfer_charges_allowed_eur"])
|
||||
self.assertEqual("38.19", credit["max_new_delivered_cost_eur"])
|
||||
|
||||
def test_railiance_self_use_has_no_markup(self):
|
||||
self.assertEqual(
|
||||
money("200.00"),
|
||||
transfer_price(money("200.00"), "entity:railiance", self.terms),
|
||||
)
|
||||
|
||||
def test_unknown_delivered_cost_stays_null(self):
|
||||
self.assertIsNone(transfer_price(None, "entity:coulomb", self.terms))
|
||||
|
||||
def test_payment_clears_interest_before_principal(self):
|
||||
applied = apply_payment(money("1.00"), money("240.00"), money("200.00"))
|
||||
self.assertEqual(money("1.00"), applied["interest_paid"])
|
||||
self.assertEqual(money("199.00"), applied["principal_paid"])
|
||||
self.assertEqual(money("41.00"), applied["principal_remaining"])
|
||||
self.assertEqual(money("0.00"), applied["interest_remaining"])
|
||||
|
||||
|
||||
class StatementFixtureTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.terms = load_terms(ROOT)
|
||||
_, self.register = load_register(ROOT)
|
||||
|
||||
def _close(self, name):
|
||||
payload = json.loads((ROOT / "examples/settlement" / name).read_text())
|
||||
return close_fixture(payload, self.terms, self.register)
|
||||
|
||||
def test_ordinary_statement(self):
|
||||
statements = self._close("15.1-ordinary.json")
|
||||
self.assertEqual(1, len(statements))
|
||||
statement = statements[0]
|
||||
self.assertEqual("entity:coulomb", statement["financial_entity_id"])
|
||||
self.assertEqual("240.00", statement["known_transfer_price_eur"])
|
||||
self.assertEqual("2026-09-11", statement["due_date"])
|
||||
self.assertEqual("760.00", statement["credit_headroom_eur"])
|
||||
self.assertEqual("open", statement["consumption_mode"])
|
||||
self.assertEqual("unknown", statement["payment_recognition"])
|
||||
|
||||
def test_extended_payment_statement(self):
|
||||
statements = self._close("15.2-extended.json")
|
||||
statement = statements[0]
|
||||
self.assertEqual("1.00", statement["interest_eur"])
|
||||
self.assertEqual("120.00", statement["new_transfer_charges_eur"])
|
||||
self.assertEqual("361.00", statement["outstanding_eur"])
|
||||
self.assertEqual("open", statement["consumption_mode"])
|
||||
|
||||
def test_restricted_statement(self):
|
||||
statements = self._close("15.3-restricted.json")
|
||||
statement = statements[0]
|
||||
self.assertEqual("entity:helixforge", statement["financial_entity_id"])
|
||||
self.assertEqual("4.17", statement["interest_eur"])
|
||||
self.assertEqual("restricted", statement["consumption_mode"])
|
||||
self.assertEqual("45.83", statement["next_month_allowance_eur"])
|
||||
|
||||
def test_empty_period_emits_nothing(self):
|
||||
self.assertEqual([], close_live(ROOT, "2026-08"))
|
||||
|
||||
|
||||
class PlanningForecastTest(unittest.TestCase):
|
||||
def test_forecast_fixture_carries_transfer_price_and_headroom(self):
|
||||
record = json.loads((ROOT / "examples/planning/entity-forecast-transfer.json").read_text())
|
||||
self.assertEqual("entity:coulomb", record["financial_entity_id"])
|
||||
self.assertEqual("240.00", record["transfer_price"])
|
||||
self.assertEqual("760.00", record["credit_headroom"])
|
||||
self.assertIn("not booked spend", record["uncertainty"])
|
||||
Loading…
Add table
Add a link
Reference in a new issue