Implement governed hybrid campaign delivery

This commit is contained in:
2026-08-02 13:58:37 +02:00
parent b38597f2be
commit 7733265cc8
40 changed files with 2531 additions and 122 deletions
@@ -37,10 +37,12 @@ from govoplan_campaign.backend.db.models import (
ImapAppendAttempt,
JobImapStatus,
JobPostboxStatus,
JobPrintStatus,
JobQueueStatus,
JobSendStatus,
JobValidationStatus,
PostboxDeliveryAttempt,
PrintOutputAttempt,
SendAttempt,
)
from govoplan_campaign.backend.response_security import (
@@ -84,11 +86,13 @@ def _job_summary_payload(
"send_status": job.send_status,
"delivery_channel_policy": getattr(job, "delivery_channel_policy", "mail"),
"postbox_status": getattr(job, "postbox_status", "not_requested"),
"print_status": getattr(job, "print_status", "not_requested"),
"imap_status": job.imap_status,
"eml_size_bytes": job.eml_size_bytes,
"eml_sha256": job.eml_sha256,
"attempt_count": job.attempt_count,
"postbox_attempt_count": getattr(job, "postbox_attempt_count", 0),
"print_attempt_count": getattr(job, "print_attempt_count", 0),
"postbox_target_count": len(
getattr(job, "resolved_postbox_targets", None) or []
),
@@ -122,8 +126,12 @@ def _job_detail_payload(job: CampaignJob) -> dict[str, object]:
"issues": job.issues_snapshot or [],
"attachments": public_campaign_payload(job.resolved_attachments or []),
"resolved_recipients": job.resolved_recipients or {},
"delivery_provenance": getattr(job, "delivery_provenance", None) or {},
"resolved_postbox_targets": getattr(job, "resolved_postbox_targets", None)
or [],
"resolved_print_output": public_campaign_payload(
getattr(job, "resolved_print_output", None)
),
}
@@ -131,6 +139,7 @@ def _job_attempts_payload(
send_attempts: list[SendAttempt],
imap_attempts: list[ImapAppendAttempt],
postbox_attempts: Sequence[PostboxDeliveryAttempt] = (),
print_attempts: Sequence[PrintOutputAttempt] = (),
message_actions: Sequence[CampaignMessageAction] = (),
message_action_attempts: Sequence[CampaignMessageActionAttempt] = (),
*,
@@ -212,6 +221,22 @@ def _job_attempts_payload(
receipt_summary
)
postbox_payloads.append(payload)
print_payloads: list[dict[str, object]] = []
for attempt in print_attempts:
payload = {
"id": attempt.id,
"attempt_number": attempt.attempt_number,
"status": attempt.status,
"render_id": attempt.render_id,
"artifact_sha256": attempt.artifact_sha256,
"started_at": attempt.started_at,
"finished_at": attempt.finished_at,
}
if include_diagnostics:
payload["idempotency_key"] = attempt.idempotency_key
payload["evidence"] = attempt.evidence or {}
payload["error_message"] = attempt.error_message
print_payloads.append(payload)
action_attempts_by_action = {
attempt.action_id: attempt
for attempt in message_action_attempts
@@ -270,6 +295,7 @@ def _job_attempts_payload(
"smtp": smtp_payloads,
"imap": imap_payloads,
"postbox": postbox_payloads,
"print": print_payloads,
"message_actions": message_action_payloads,
}
@@ -305,6 +331,7 @@ def _job_diagnostics_payload(
send_attempts: list[SendAttempt],
imap_attempts: list[ImapAppendAttempt],
postbox_attempts: Sequence[PostboxDeliveryAttempt] = (),
print_attempts: Sequence[PrintOutputAttempt] = (),
message_actions: Sequence[CampaignMessageAction] = (),
message_action_attempts: Sequence[CampaignMessageActionAttempt] = (),
*,
@@ -333,6 +360,7 @@ def _job_diagnostics_payload(
send_attempts,
imap_attempts,
postbox_attempts,
print_attempts,
message_actions,
message_action_attempts,
postbox_receipts=postbox_receipts,
@@ -452,6 +480,7 @@ def _status_counts(
"queue_status",
"send_status",
"postbox_status",
"print_status",
"imap_status",
):
column = getattr(CampaignJob, field_name)
@@ -475,6 +504,7 @@ CAMPAIGN_JOB_GRID_SORT_COLUMNS = {
"queue": CampaignJob.queue_status,
"send": CampaignJob.send_status,
"postbox": CampaignJob.postbox_status,
"print": CampaignJob.print_status,
"imap": CampaignJob.imap_status,
"attempts": CampaignJob.attempt_count,
"updated": CampaignJob.updated_at,
@@ -490,6 +520,10 @@ CAMPAIGN_JOB_GRID_LIST_FILTERS = {
CampaignJob.postbox_status,
{item.value for item in JobPostboxStatus},
),
"print": (
CampaignJob.print_status,
{item.value for item in JobPrintStatus},
),
"imap": (CampaignJob.imap_status, {item.value for item in JobImapStatus}),
}
@@ -957,6 +991,7 @@ class CampaignJobsQuery:
"queue",
"send",
"postbox",
"print",
"imap",
"attempts",
"updated",
@@ -968,6 +1003,7 @@ class CampaignJobsQuery:
filter_queue: str | None = Query(default=None, max_length=1000),
filter_send: str | None = Query(default=None, max_length=1000),
filter_postbox: str | None = Query(default=None, max_length=1000),
filter_print: str | None = Query(default=None, max_length=1000),
filter_imap: str | None = Query(default=None, max_length=1000),
filter_attempts: str | None = Query(default=None, max_length=100),
filter_evidence: str | None = Query(default=None, max_length=500),
@@ -991,6 +1027,7 @@ class CampaignJobsQuery:
"queue": filter_queue,
"send": filter_send,
"postbox": filter_postbox,
"print": filter_print,
"imap": filter_imap,
"attempts": filter_attempts,
"evidence": filter_evidence,