Integrate Distribution Lists with Campaign recipients

This commit is contained in:
2026-08-02 11:53:35 +02:00
parent 4eeba62bbc
commit b38597f2be
15 changed files with 1552 additions and 3 deletions
+119
View File
@@ -130,6 +130,125 @@ class CampaignPartialValidationTests(unittest.TestCase):
self.assertEqual(response.excluded[0].reason_code, "addresses.channel.opted_out")
self.assertTrue(response.provenance["governance_applied"])
def test_distribution_list_preview_preserves_provider_and_route_evidence(self) -> None:
class ExpansionCapability:
def expand(self, _session, _principal, *, request):
self.request = request
return _distribution_expansion_payload()
capability = ExpansionCapability()
payload = router.CampaignDistributionListExpansionRequest(
list_id="11111111-1111-1111-1111-111111111111",
revision=3,
requested_channels=["email", "postal"],
parameters={"district": "north"},
)
with patch.object(router, "_get_campaign_for_principal", return_value=None), patch.object(
router, "_registry_capability", return_value=capability
):
response = router.preview_campaign_recipient_distribution_list(
"campaign-1", payload, session=object(), principal=object()
)
self.assertTrue(capability.request.preview)
self.assertFalse(capability.request.freeze)
self.assertEqual(capability.request.parameters, {"district": "north"})
self.assertEqual(response.source.revision, 3)
self.assertEqual(response.recipients[0].source_entry_ids, ["entry-1"])
self.assertEqual(response.recipients[0].channels[0].decision_provenance["policy"], "allow")
self.assertEqual(response.provider_evidence[0].actual_revision, "provider-r7")
def test_distribution_list_snapshot_requires_and_forwards_idempotency(self) -> None:
class ExpansionCapability:
def expand(self, _session, _principal, *, request):
self.request = request
payload = _distribution_expansion_payload()
payload["snapshot_id"] = "snapshot-1"
return payload
capability = ExpansionCapability()
payload = router.CampaignDistributionListExpansionRequest(
list_id="11111111-1111-1111-1111-111111111111",
requested_channels=["email"],
idempotency_key="campaign:one:list:one",
)
with patch.object(router, "_get_campaign_for_principal", return_value=None), patch.object(
router, "_registry_capability", return_value=capability
), patch.object(router, "audit_from_principal") as audit:
response = router.snapshot_campaign_recipient_distribution_list(
"campaign-1", payload, session=object(), principal=object()
)
self.assertTrue(capability.request.freeze)
self.assertFalse(capability.request.preview)
self.assertEqual(capability.request.idempotency_key, "campaign:one:list:one")
self.assertEqual(response.snapshot_id, "snapshot-1")
self.assertTrue(audit.call_args.kwargs["commit"])
def _distribution_expansion_payload() -> dict[str, object]:
source = {
"id": "11111111-1111-1111-1111-111111111111",
"tenant_id": "tenant-1",
"name": "Residents",
"revision_id": "revision-id-3",
"revision": 3,
"definition_hash": "definition-hash",
"definition_kind": "parameterized",
"entry_count": 1,
"parameters": [],
"provenance": {},
"metadata": {},
}
reference = {
"provider": "addresses",
"resource_type": "address_list",
"resource_id": "list-1",
"revision": "provider-r7",
"metadata": {},
}
recipient = {
"recipient_key": "contact:1",
"display_name": "Ada Lovelace",
"status": "usable",
"channels": [
{
"channel": "email",
"target": "ada@example.local",
"target_key": "email:ada@example.local",
"status": "usable",
"preferred": True,
"source": reference,
"decision_provenance": {"policy": "allow"},
}
],
"contact_id": "contact-1",
"source_entry_ids": ["entry-1"],
"explanations": [],
"attributes": {"district": "north"},
"provenance": {"provider": "addresses"},
}
return {
"source": source,
"request": {"parameters": {"district": "north"}},
"recipients": [recipient],
"excluded": [],
"diagnostics": [],
"provider_evidence": [
{
"provider": "addresses",
"source": reference,
"actual_revision": "provider-r7",
"stale": False,
"details": {},
}
],
"expansion_hash": "expansion-hash",
"generated_at": "2026-08-02T10:00:00+00:00",
"stale": False,
"truncated": False,
}
class CampaignSemanticValidationTests(unittest.TestCase):
def test_send_mode_requires_campaign_owned_sender_for_each_inline_entry(self) -> None: