generated from coulomb/repo-seed
Implement financial Fabric vNext read model
This commit is contained in:
@@ -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"] == {}
|
||||
|
||||
Reference in New Issue
Block a user