feat: add governed postbox delivery and report hardening

This commit is contained in:
2026-07-29 14:16:28 +02:00
parent f11c56e890
commit 5240749ae1
47 changed files with 5538 additions and 288 deletions

View File

@@ -0,0 +1,208 @@
from __future__ import annotations
from unittest.mock import patch
from govoplan_core.core.postbox import (
PostboxDeliveryCatalogRef,
PostboxDeliveryTemplateRef,
PostboxDirectoryEntryRef,
PostboxOrganizationFunctionTargetRef,
PostboxOrganizationUnitTargetRef,
)
from govoplan_campaign.backend.campaign.models import CampaignConfig
from govoplan_campaign.backend.campaign.postbox_targets import (
resolve_entry_postbox_targets,
)
from govoplan_campaign.backend.campaign.validation import (
validate_campaign_config,
)
from govoplan_campaign.backend.messages.models import MessageValidationStatus
def _config() -> CampaignConfig:
return CampaignConfig.model_validate(
{
"version": "1.0",
"campaign": {
"id": "campaign-1",
"name": "Postbox campaign",
"mode": "send",
},
"fields": [
{"name": "org_key", "type": "organization_unit"},
{"name": "function_key", "type": "organization_function"},
{"name": "case_key", "type": "string"},
],
"template": {
"subject": "Decision",
"text": "A decision is available.",
"body_mode": "text",
},
"entries": {
"inline": [
{
"id": "recipient-1",
"fields": {
"org_key": "finance",
"function_key": "caseworker",
"case_key": "case-42",
},
}
]
},
"delivery": {
"channel_policy": "postbox",
"postbox": {
"targets": [
{
"id": "direct",
"mode": "direct",
"address_key": "central-intake",
},
{
"id": "derived",
"mode": "derived",
"template_id": "template-1",
"organization_unit_field": "org_key",
"organization_unit_match": "slug",
"function_field": "function_key",
"function_match": "slug",
"context_field": "case_key",
},
]
},
},
}
)
def _catalog() -> PostboxDeliveryCatalogRef:
return PostboxDeliveryCatalogRef(
templates=(
PostboxDeliveryTemplateRef(
id="template-1",
slug="case-inbox",
name="Case inbox",
description=None,
published_revision_id="revision-1",
function_type_id=None,
scope_kind="tenant",
scope_id=None,
classification="internal",
allow_vacant_delivery=True,
),
),
organization_units=(
PostboxOrganizationUnitTargetRef(
id="unit-1",
slug="finance",
name="Finance",
functions=(
PostboxOrganizationFunctionTargetRef(
id="function-1",
slug="caseworker",
name="Caseworker",
),
),
),
),
)
class _PostboxIntegration:
def __init__(self) -> None:
self.targets: list[tuple[object, bool]] = []
def delivery_catalog(self, _session, *, tenant_id: str):
assert tenant_id == "tenant-1"
return _catalog()
def resolve_postbox(
self,
_session,
*,
tenant_id: str,
target,
materialize: bool,
):
self.targets.append((target, materialize))
if target.address_key == "central-intake":
return PostboxDirectoryEntryRef(
id="postbox-direct",
tenant_id=tenant_id,
address="central-intake@postbox",
address_key="central-intake",
name="Central intake",
status="active",
classification="internal",
holder_count=2,
vacant=False,
)
assert target.template_id == "template-1"
assert target.organization_unit_id == "unit-1"
assert target.function_id == "function-1"
assert target.context_key == "case-42"
return PostboxDirectoryEntryRef(
id="postbox-derived",
tenant_id=tenant_id,
address="finance.caseworker.case-42@postbox",
address_key="finance.caseworker.case-42",
name="Finance caseworker",
status="active",
classification="internal",
organization_unit_id="unit-1",
function_id="function-1",
context_key="case-42",
holder_count=1,
vacant=False,
)
def test_postbox_only_campaign_does_not_require_mail() -> None:
config = _config()
available = validate_campaign_config(config, postbox_available=True)
unavailable = validate_campaign_config(config, postbox_available=False)
assert {
issue.code
for issue in available.issues
if issue.severity.value == "error"
} == set()
assert "missing_mail_profile" not in {
issue.code for issue in unavailable.issues
}
assert "missing_sender" not in {
issue.code for issue in unavailable.issues
}
assert "postbox_unavailable" in {
issue.code for issue in unavailable.issues
}
def test_row_resolves_multiple_direct_and_field_derived_postboxes() -> None:
config = _config()
integration = _PostboxIntegration()
with patch(
"govoplan_campaign.backend.campaign.postbox_targets.postbox_integration",
return_value=integration,
):
targets, issues, status = resolve_entry_postbox_targets(
object(), # type: ignore[arg-type]
tenant_id="tenant-1",
config=config,
entry=config.entries.inline[0], # type: ignore[index]
validation_status=MessageValidationStatus.READY,
materialize=True,
)
assert issues == []
assert status == MessageValidationStatus.READY
assert [target["postbox_id"] for target in targets] == [
"postbox-direct",
"postbox-derived",
]
assert targets[1]["context_key"] == "case-42"
assert len(integration.targets) == 2
assert all(materialize for _target, materialize in integration.targets)