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.
92 lines
3.6 KiB
Python
92 lines
3.6 KiB
Python
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)
|