feat: implement institutional service catalogue

This commit is contained in:
2026-08-01 17:48:39 +02:00
commit c6c23aab8c
24 changed files with 1445 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
import unittest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from govoplan_core.core.institutional import (
InstitutionalReference,
ServiceAvailabilityRequirement,
ServiceDefinition,
TemporalRevision,
)
from govoplan_services.backend.db.models import ServiceDefinitionRevision
from govoplan_services.backend.service import (
RegistryServiceAvailabilityEvaluator,
ServiceStoreError,
SqlServiceDefinitionProvider,
record_service_definition,
)
NOW = datetime(2026, 8, 1, 10, 0, tzinfo=UTC)
@dataclass
class Principal:
tenant_id: str = "tenant-1"
account_id: str = "account-1"
class Registry:
def has(self, module_id: str) -> bool:
return module_id == "cases"
def has_capability(self, capability: str) -> bool:
return capability == "workflow.runtime"
def service(*, revision: str = "1", state: str = "published") -> ServiceDefinition:
recorded_at = NOW + timedelta(minutes=int(revision) - 1)
return ServiceDefinition(
reference=InstitutionalReference(kind="service", owner_module="services", object_id="permit", tenant_id="tenant-1", version=revision, valid_at=NOW),
key="permit",
temporal=TemporalRevision(revision=revision, valid_from=NOW, recorded_at=recorded_at, change_reason="Initial service." if revision == "1" else "Publication changed."),
title="Apply for a permit",
audience=("resident",),
channels=("portal",),
availability_requirements=(
ServiceAvailabilityRequirement(kind="module", reference="cases"),
ServiceAvailabilityRequirement(kind="capability", reference="workflow.runtime"),
),
publication_state=state, # type: ignore[arg-type]
)
class ServiceTests(unittest.TestCase):
def setUp(self) -> None:
self.engine = create_engine("sqlite+pysqlite:///:memory:")
ServiceDefinitionRevision.__table__.create(self.engine)
self.session = Session(self.engine)
self.principal = Principal()
def tearDown(self) -> None:
self.session.close()
self.engine.dispose()
def test_provider_returns_exact_published_revision(self) -> None:
first = record_service_definition(self.session, self.principal, definition=service())
provider = SqlServiceDefinitionProvider()
listed = provider.list_service_definitions(self.session, self.principal, tenant_id="tenant-1")
self.assertEqual((first,), tuple(listed))
exact = provider.get_service_definition(self.session, self.principal, reference=first.reference, effective_at=NOW)
self.assertEqual("1", exact.temporal.revision if exact else None)
suspended = service(revision="2", state="suspended")
record_service_definition(self.session, self.principal, definition=suspended, expected_revision="1")
historical = provider.get_service_definition(self.session, self.principal, reference=first.reference)
self.assertEqual("1", historical.temporal.revision if historical else None)
def test_occ_availability_and_tenant_boundary(self) -> None:
record_service_definition(self.session, self.principal, definition=service())
with self.assertRaisesRegex(ServiceStoreError, "stale"):
record_service_definition(self.session, self.principal, definition=service(revision="2"), expected_revision="0")
assessment = RegistryServiceAvailabilityEvaluator(Registry()).evaluate_service_availability(
self.session,
self.principal,
definition=service(),
effective_at=NOW,
)
self.assertEqual(
{"module:cases": True, "capability:workflow.runtime": True},
assessment.requirement_states,
)
with self.assertRaisesRegex(Exception, "cross tenants"):
SqlServiceDefinitionProvider().list_service_definitions(self.session, Principal("tenant-2"), tenant_id="tenant-1")
if __name__ == "__main__":
unittest.main()