Integrate governed address contact resolution
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user