Implement financial Fabric vNext read model

This commit is contained in:
2026-05-24 02:52:59 +02:00
parent 52d43304ba
commit f25569d9d4
8 changed files with 1248 additions and 24 deletions

View File

@@ -1058,6 +1058,193 @@ def _fabric_graph_export(generated_at="2026-05-23T12:00:00Z", extra_node=False):
}
def _financial_fabric_graph_export(generated_at="2026-05-24T00:00:00Z"):
return {
"apiVersion": "railiance.fabric/v1alpha2",
"kind": "FabricGraphExport",
"schema_version": "financial-fabric-v1",
"generated_at": generated_at,
"source": {
"producer": "railiance-fabric",
"registry": "registry",
"commit": "financial-example",
"generation_reason": "operator_refresh",
},
"compatibility": {
"legacy_v1alpha1_supported": True,
"breaking_reset": False,
},
"netkingdom": {
"id": "railiance.netkingdom",
"name": "Railiance Netkingdom",
"king_actor_id": "actor.railiance.king",
},
"actors": [
{
"id": "actor.railiance.king",
"kind": "FabricActor",
"role": "king",
"name": "Railiance King",
},
{
"id": "actor.railiance.primary-lord",
"kind": "FabricActor",
"role": "lord",
"name": "Railiance Primary Lord",
},
{
"id": "actor.coulomb.tenant",
"kind": "FabricActor",
"role": "tenant",
"name": "Coulomb Tenant",
},
],
"fabrics": [
{
"id": "fabric.railiance.primary",
"kind": "Fabric",
"name": "Railiance Primary Fabric",
"netkingdom_id": "railiance.netkingdom",
"lord_actor_id": "actor.railiance.primary-lord",
"parent_fabric_id": None,
"status": "active",
"boundary": {"boundary_type": "fabric"},
"evidence_refs": [],
},
{
"id": "subfabric.railiance.tenant.coulomb",
"kind": "Subfabric",
"name": "Coulomb Tenant Subfabric",
"netkingdom_id": "railiance.netkingdom",
"parent_fabric_id": "fabric.railiance.primary",
"tenant_actor_id": "actor.coulomb.tenant",
"status": "planned",
"boundary": {"boundary_type": "subfabric"},
"evidence_refs": [],
},
],
"nodes": [
{
"id": "state-hub.http",
"kind": "UtilityInterface",
"name": "State Hub HTTP API",
"repo": "state-hub",
"domain": "custodian",
"lifecycle": "active",
"containment": {
"netkingdom_id": "railiance.netkingdom",
"fabric_id": "fabric.railiance.primary",
"subfabric_id": None,
"environment": "local",
"deployment_scenario_id": None,
},
"ownership": {
"owner_actor_id": "actor.railiance.primary-lord",
"owner_role": "lord",
"resolution": "inherited",
"inherited_from": "fabric.railiance.primary",
"supporting_actor_ids": [],
},
"accounting": {
"cost_center_id": "cc.platform.shared",
"allocation_model": "direct",
},
"evidence": {
"state": "declared",
"review_state": "accepted",
"confidence": 0.9,
"refs": [],
},
"canon_category": "endpoint",
"canon_anchor": "model/network",
"mapping_fit": "partial",
"evidence_state": "declared",
"attributes": {},
},
{
"id": "coulomb.automation-client",
"kind": "Service",
"name": "Coulomb Automation Client",
"repo": "coulomb-automation",
"domain": "railiance",
"lifecycle": "planned",
"containment": {
"netkingdom_id": "railiance.netkingdom",
"fabric_id": "fabric.railiance.primary",
"subfabric_id": "subfabric.railiance.tenant.coulomb",
"environment": "local",
"deployment_scenario_id": None,
},
"ownership": {
"owner_actor_id": "actor.coulomb.tenant",
"owner_role": "tenant",
"resolution": "explicit",
"supporting_actor_ids": [],
},
"accounting": {
"cost_center_id": "cc.coulomb.automation",
"allocation_model": "direct",
},
"evidence": {
"state": "declared",
"review_state": "accepted",
"confidence": 0.8,
"refs": [],
},
"attributes": {},
},
],
"edges": [
{
"id": "utility:state-hub-http:coulomb-client",
"from": "state-hub.http",
"to": "coulomb.automation-client",
"type": "provides_utility_to",
"relationship_category": "utility",
"canonical_type": "depends_on",
"canon_anchor": "model/landscape",
"mapping_fit": "partial",
"display_only": False,
"evidence_state": "declared",
"provider": {
"owner_actor_id": "actor.railiance.primary-lord",
"fabric_id": "fabric.railiance.primary",
"subfabric_id": None,
},
"consumer": {
"owner_actor_id": "actor.coulomb.tenant",
"fabric_id": "fabric.railiance.primary",
"subfabric_id": "subfabric.railiance.tenant.coulomb",
},
"boundary": {
"crosses_fabric_boundary": False,
"crosses_subfabric_boundary": True,
},
"utility": {
"utility_type": "coordination_api",
"contract_id": "state-hub.http",
"payment_schema_id": "payment.internal-tenant-access",
"metering_basis": "unknown",
"business_model": "tenant_utility",
},
"accounting": {
"provider_profit_center_id": "pc.tenant-utilities",
"consumer_cost_center_id": "cc.coulomb.automation",
"allocation_model": "usage_weighted",
},
"evidence": {
"state": "declared",
"review_state": "accepted",
"confidence": 0.8,
"refs": [],
},
"attributes": {},
}
],
"unresolved": [],
}
class TestFabricGraphReadModel:
async def test_validation_failure_records_failed_import_without_read_model_rows(self, client):
payload = _fabric_graph_export()
@@ -1163,3 +1350,92 @@ class TestFabricGraphReadModel:
assert r.json()["status"] == "ready"
r = await client.get(f"/tasks/{task['id']}")
assert r.json()["status"] == "todo"
async def test_financial_vnext_ingest_materializes_ownership_utility_and_summary(self, client):
r = await client.post("/fabric/graph-exports", json=_financial_fabric_graph_export())
assert r.status_code == 200, r.text
body = r.json()
assert body["import_run"]["api_version"] == "railiance.fabric/v1alpha2"
assert body["import_run"]["schema_version"] == "financial-fabric-v1"
assert body["import_run"]["netkingdom_id"] == "railiance.netkingdom"
assert body["import_run"]["actor_count"] == 3
assert body["import_run"]["fabric_count"] == 2
r = await client.post(
"/fabric/graph-exports",
json=_financial_fabric_graph_export(generated_at="2026-05-24T00:05:00Z"),
)
assert r.status_code == 200, r.text
second_body = r.json()
assert second_body["created"] is False
assert second_body["idempotent"] is True
assert second_body["import_run"]["id"] == body["import_run"]["id"]
r = await client.get("/fabric/graph/nodes?owner_role=tenant")
assert r.status_code == 200
tenant_nodes = r.json()
assert [node["graph_id"] for node in tenant_nodes] == ["coulomb.automation-client"]
assert tenant_nodes[0]["subfabric_id"] == "subfabric.railiance.tenant.coulomb"
assert tenant_nodes[0]["owner_actor_id"] == "actor.coulomb.tenant"
assert tenant_nodes[0]["ownership_resolution"] == "explicit"
assert tenant_nodes[0]["cost_center_id"] == "cc.coulomb.automation"
r = await client.get(
"/fabric/graph/edges"
"?relationship_category=utility"
"&consumer_owner_actor_id=actor.coulomb.tenant"
"&crosses_subfabric_boundary=true"
)
assert r.status_code == 200
utility_edges = r.json()
assert len(utility_edges) == 1
assert utility_edges[0]["utility_type"] == "coordination_api"
assert utility_edges[0]["utility_payment_schema_id"] == "payment.internal-tenant-access"
assert utility_edges[0]["provider_profit_center_id"] == "pc.tenant-utilities"
r = await client.get("/fabric/graph/summary")
assert r.status_code == 200
summary = r.json()
assert summary["schema_version"] == "financial-fabric-v1"
assert summary["nodes_by_fabric"]["fabric.railiance.primary"] == 2
assert summary["nodes_by_subfabric"]["subfabric.railiance.tenant.coulomb"] == 1
assert summary["nodes_by_owner_role"]["lord"] == 1
assert summary["nodes_by_owner_role"]["tenant"] == 1
assert summary["edges_by_relationship_category"]["utility"] == 1
assert summary["utility_edges_by_provider_owner"]["actor.railiance.primary-lord"] == 1
assert summary["utility_edges_by_consumer_owner"]["actor.coulomb.tenant"] == 1
assert summary["utility_edges_by_business_model"]["tenant_utility"] == 1
assert summary["tenant_utilities_without_payment_schema"] == 0
assert summary["unresolved_ownership_count"] == 0
async def test_financial_vnext_validation_rejects_unresolved_accepted_ownership(self, client):
payload = _financial_fabric_graph_export()
payload["nodes"][0]["ownership"]["resolution"] = "unresolved"
r = await client.post("/fabric/graph-exports", json=payload)
assert r.status_code == 422
body = r.json()["detail"]
assert body["validation_status"] == "invalid"
assert "accepted nodes" in body["message"]
r = await client.get("/fabric/graph/nodes")
assert r.status_code == 404
async def test_legacy_fabric_exports_remain_compatible_with_null_financial_fields(self, client):
r = await client.post("/fabric/graph-exports", json=_fabric_graph_export())
assert r.status_code == 200, r.text
assert r.json()["import_run"]["schema_version"] is None
r = await client.get("/fabric/graph/nodes?repo=state-hub&canonical_category=service")
assert r.status_code == 200
node = r.json()[0]
assert node["graph_id"] == "the-custodian.state-hub"
assert node["fabric_id"] is None
assert node["owner_actor_id"] is None
r = await client.get("/fabric/graph/summary")
assert r.status_code == 200
summary = r.json()
assert summary["schema_version"] is None
assert summary["nodes_by_fabric"] == {}