Integrate governed address contact resolution

This commit is contained in:
2026-08-02 07:24:29 +02:00
parent 7b0c9cfdde
commit 8099e34c6c
9 changed files with 774 additions and 12 deletions
+11
View File
@@ -15,6 +15,17 @@ class DistributionListsManifestTests(unittest.TestCase):
self.assertFalse(manifest.dependencies)
self.assertIn("addresses", manifest.optional_dependencies)
self.assertIn("auth.principalResolver", manifest.optional_capabilities)
self.assertIn(
"addresses.contact_point_resolution",
manifest.optional_capabilities,
)
requirements = {item.name: item for item in manifest.requires_interfaces}
self.assertTrue(requirements["addresses.contact_point_resolution"].optional)
self.assertEqual(
"1.0.0",
requirements["addresses.contact_point_resolution"].version_min,
)
self.assertTrue(requirements["addresses.channel_facts"].optional)
self.assertIn("dist_lists.expand", {interface.name for interface in manifest.provides_interfaces})
self.assertIn("dist_lists:list:read", {permission.scope for permission in manifest.permissions})
self.assertIsNotNone(manifest.route_factory)
+250
View File
@@ -6,6 +6,12 @@ import unittest
from govoplan_core.auth import ApiPrincipal
from govoplan_core.core.access import PrincipalRef
from govoplan_core.core.concurrency import RevisionConflictError
from govoplan_core.core.contact_points import (
CONTACT_POINT_CONTRACT_VERSION,
ContactPointCandidate,
ContactPointResolution,
ContactPointSourcePreview,
)
from govoplan_core.core.dataflows import (
DataflowDatasetDescriptor,
DataflowDatasetResult,
@@ -14,7 +20,9 @@ from govoplan_core.core.distribution_lists import (
DistributionChannelPolicyDecision,
DistributionExpansionLimits,
DistributionExpansionRequest,
DistributionExplanation,
DistributionListConflictError,
DistributionSourceReference,
)
from govoplan_core.db.base import Base
from govoplan_core.db.session import configure_database, reset_database
@@ -151,6 +159,120 @@ class _Policy:
)
class _ContactPoints:
def __init__(self) -> None:
self.resolve_request = None
self.source_requests = []
source = DistributionSourceReference(
provider="addresses",
resource_type="contact",
resource_id="contact-1",
revision="contact-revision-1",
fingerprint="contact-fingerprint-1",
label="Ada Example",
)
self.resolution = ContactPointResolution(
contract_version=CONTACT_POINT_CONTRACT_VERSION,
subject=source,
status="usable",
contact_id="contact-1",
display_name="Ada Example",
candidates=(
ContactPointCandidate(
channel="postal",
target="Ada Example\nMain Street 1\n10115 Berlin\nGermany",
target_key="postal:main street 1|10115|berlin||germany",
status="usable",
contact_point_id="postal-1",
address_purpose="official",
locale="de-DE",
preferred=True,
preference_rank=1,
source=source,
source_revision="facts-revision-1",
preference_revision="preference-revision-1",
consent_revision="consent-revision-1",
value={
"street": "Main Street 1",
"postal_code": "10115",
"locality": "Berlin",
"country": "Germany",
},
provenance={
"rule_ids": ["rule-1"],
"source_revision": "contact-revision-1",
},
),
),
excluded=(
ContactPointCandidate(
channel="email",
target="ada@example.test",
target_key="email:ada@example.test",
status="suppressed",
contact_point_id="email-1",
reason_code="addresses.channel.opted_out",
explanation="The contact opted out of email delivery.",
source=source,
source_revision="facts-revision-1",
provenance={
"rule_ids": ["rule-2"],
"source_revision": "contact-revision-1",
},
),
),
explanations=(
DistributionExplanation(
code="addresses.channel_fact.expired",
message="An older preference expired.",
severity="info",
provider="addresses",
source=source,
),
),
source_revision="facts-revision-1",
source_fingerprint="facts-fingerprint-1",
provenance={"active_rule_ids": ["rule-1", "rule-2"]},
)
def resolve_contact_points(self, session, principal, *, request):
del session, principal
self.resolve_request = request
return self.resolution
def preview_source(self, session, principal, *, request, offset=0, limit=100):
del session, principal
self.source_requests.append(request)
resolutions = (self.resolution, self.resolution)[offset : offset + limit]
return ContactPointSourcePreview(
contract_version=CONTACT_POINT_CONTRACT_VERSION,
source=DistributionSourceReference(
provider="addresses",
resource_type="address_list",
resource_id="list-1",
),
request=request,
resolutions=resolutions,
total_count=2,
usable_count=len(resolutions),
excluded_count=len(resolutions),
offset=offset,
limit=limit,
has_more=offset + len(resolutions) < 2,
source_revision="list-revision-1",
source_fingerprint="list-fingerprint-1",
generated_at=datetime(2026, 1, 1, tzinfo=UTC),
provenance={"bounded": True},
)
def freeze_source(self, session, principal, *, request):
raise AssertionError("Distribution Lists freezes its complete expansion snapshot.")
def get_snapshot(self, session, principal, *, snapshot_id):
del session, principal, snapshot_id
return None
class _Registry:
def __init__(self, capabilities=None) -> None:
self.capabilities = dict(capabilities or {})
@@ -494,6 +616,134 @@ class DistributionListServiceTests(unittest.TestCase):
self.assertEqual("channel.not_requested", by_channel["email"].reason_code)
self.assertEqual("usable", by_channel["postal"].status)
def test_address_contact_resolution_is_purpose_aware_and_frozen(self) -> None:
contact_points = _ContactPoints()
registry = _Registry(
{"addresses.contact_point_resolution": contact_points}
)
with self.database.session() as session:
item, _ = create_distribution_list(
session,
principal(),
DistributionListCreateRequest.model_validate(
{
"name": "Official postal recipients",
"entries": [
{
"entry_key": "ada",
"kind": "address_contact",
"purpose": "official_notice",
"requested_channels": ["postal"],
"source": {
"provider": "addresses",
"resource_type": "contact",
"resource_id": "contact-1",
"revision": "contact-revision-1",
},
"configuration": {
"address_purpose": "official",
"fallback_rule": "none",
"locale": "de-DE",
"postal_format": "international",
},
}
],
}
),
)
result = expand_distribution_list(
session,
principal(),
registry=registry,
request=DistributionExpansionRequest(
list_id=item.id,
effective_at=datetime(2026, 1, 1, tzinfo=UTC),
requested_channels=("postal",),
freeze=True,
idempotency_key="official-postal-1",
),
)
session.commit()
self.assertEqual(1, len(result.recipients))
recipient = result.recipients[0]
self.assertEqual("contact-1", recipient.contact_id)
self.assertEqual("postal-1", recipient.channels[0].contact_point_id)
self.assertIn("Main Street 1", recipient.channels[0].target)
self.assertEqual("suppressed", recipient.channels[1].status)
self.assertEqual(
"addresses.channel.opted_out",
recipient.channels[1].reason_code,
)
self.assertEqual("official_notice", contact_points.resolve_request.purpose)
self.assertEqual("official", contact_points.resolve_request.address_purpose)
self.assertEqual("international", contact_points.resolve_request.postal_format)
self.assertEqual(
"1.0",
recipient.provenance["contact_point_resolution"]["contract_version"],
)
snapshot = session.get(DistributionListSnapshot, result.snapshot_id)
self.assertIn("Main Street 1", snapshot.recipients[0]["channels"][0]["target"])
self.assertEqual(
"postal-1",
snapshot.recipients[0]["channels"][0]["contact_point_id"],
)
self.assertEqual(
"facts-fingerprint-1",
snapshot.provider_evidence[0]["actual_fingerprint"],
)
def test_address_list_resolution_is_bounded_and_channel_neutral(self) -> None:
contact_points = _ContactPoints()
registry = _Registry(
{"addresses.contact_point_resolution": contact_points}
)
with self.database.session() as session:
item, _ = create_distribution_list(
session,
principal(),
DistributionListCreateRequest.model_validate(
{
"name": "Address source",
"entries": [
{
"entry_key": "address-list",
"kind": "address_list",
"source": {
"provider": "addresses",
"resource_type": "address_list",
"resource_id": "addresses:address_list:list-1",
},
}
],
}
),
)
result = expand_distribution_list(
session,
principal(),
registry=registry,
request=DistributionExpansionRequest(
list_id=item.id,
effective_at=datetime(2026, 1, 1, tzinfo=UTC),
requested_channels=("postal",),
limits=DistributionExpansionLimits(max_provider_results=1),
),
)
self.assertEqual(1, len(result.recipients))
self.assertTrue(result.truncated)
self.assertEqual(
"addresses:address_list:list-1",
contact_points.source_requests[0].source_id,
)
self.assertEqual(("postal",), contact_points.source_requests[0].requested_channels)
self.assertEqual("list-revision-1", result.provider_evidence[0].actual_revision)
self.assertIn(
"provider.result_limit",
{item.code for item in result.diagnostics},
)
def _nested_entry(list_id: str, key: str) -> dict[str, object]:
return {