feat(campaign): bound synchronous delivery

This commit is contained in:
2026-07-22 08:21:42 +02:00
parent a8c0750dd7
commit 7e1660344d
10 changed files with 689 additions and 45 deletions

View File

@@ -2,6 +2,11 @@ from __future__ import annotations
from govoplan_core.core.modules import DocumentationCondition, DocumentationContext, DocumentationLink, DocumentationTopic
from govoplan_campaign.backend.delivery_policy import (
CampaignDeliveryPolicyError,
effective_synchronous_send_policy,
)
_CAMPAIGN_USER_SCOPES = (
"campaigns:campaign:read",
@@ -278,21 +283,20 @@ CAMPAIGN_USER_DOCUMENTATION = (
),
steps=(
"Open Review and send and confirm the selected version, build, recipients, warnings, and review completion.",
"Use an authorized supporting client to run the queue dry run and resolve every blocker.",
"Invoke the Queue action through that client so background workers can process the build.",
"Open the campaign Report and monitor queue, SMTP, IMAP, failure, and uncertain-outcome counts.",
"Select Queue for workers and confirm the exact durable execution that will be committed.",
"Remain on Review and send or leave and return later; both views read the same durable job states.",
"Use the delivery controls to pause, resume, cancel unsent work, or retry eligible failures without requeueing accepted or uncertain outcomes.",
),
outcome="Eligible jobs queued with an auditable execution snapshot and protected delivery state.",
verification="The Report shows the selected version's jobs as queued or progressing, without changing any accepted job back to retryable.",
related_topic_ids=("campaigns.workflow.complete-review", "campaigns.workflow.retry-and-reconcile"),
related_modules=("mail", "notifications"),
limitations=("The current Campaign Web UI does not expose the Queue action; use an authorized supporting client or API.",),
),
_workflow_topic(
topic_id="campaigns.workflow.send-small-controlled-run",
title="Send a campaign immediately",
summary="Run the eligible jobs synchronously only after deliberately confirming that the reviewed campaign is small enough for an interactive request.",
body="The current synchronous action does not enforce a server-side job-count limit. It must not replace the worker queue for ordinary batches, but it does preserve the same protected SMTP, IMAP, retry, and uncertain-outcome rules.",
body="Send now is protected by an effective deployment/tenant recipient-job maximum. The server counts the exact persisted eligible build, rejects an oversized or empty run before SMTP, and preflights every message and the Mail profile revision before the first provider effect.",
order=36,
audience=("campaign_sender", "campaign_operator"),
required_modules=("campaigns", "mail"),
@@ -308,12 +312,12 @@ CAMPAIGN_USER_DOCUMENTATION = (
help_contexts=("campaign.review-send",),
prerequisites=(
"The selected version is validated, locked, built, and reviewed.",
"An operator has deliberately confirmed that this is a small controlled run; the server does not currently enforce that bound.",
"The exact eligible recipient-job count is within the effective limit shown on Review and send.",
"Its Mail profile remains available and authorized for the current campaign context.",
),
steps=(
"Open Review and send and inspect the exact build and delivery readiness checks.",
"Confirm that this is an intentionally small controlled run; use Queue for an ordinary batch.",
"Review the effective synchronous limit and exact eligible count; use Queue for workers when Send now is unavailable or for an ordinary batch.",
"Select Send now and confirm the protected delivery action.",
"Open Report and inspect each resulting delivery state before attempting any recovery action.",
),
@@ -321,7 +325,6 @@ CAMPAIGN_USER_DOCUMENTATION = (
verification="The Report distinguishes accepted, failed, unattempted, cancelled, and outcome-unknown jobs and retains their attempt history.",
related_topic_ids=("campaigns.workflow.queue-delivery", "campaigns.workflow.retry-and-reconcile"),
related_modules=("mail",),
limitations=("No server-side synchronous job-count limit is currently enforced; use Queue and workers for ordinary batches.",),
),
_workflow_topic(
topic_id="campaigns.workflow.view-delivery-report",
@@ -481,10 +484,13 @@ def documentation_topics(context: DocumentationContext) -> tuple[DocumentationTo
current_configuration = list(_actor_capabilities(principal, mail_available=mail_available))
integration_configuration, integration_limitations = _integration_summary(context.registry, principal)
current_configuration.extend(integration_configuration)
delivery_configuration, delivery_limitations = _delivery_policy_summary(context)
current_configuration.extend(delivery_configuration)
limitations = [
"Access to a particular campaign still depends on its owner or sharing rules and on the campaign's current state.",
"Profile, file, and address pickers re-evaluate the actual resources visible in the selected campaign; this overview does not claim that an eligible item currently exists.",
*integration_limitations,
*delivery_limitations,
]
return (
@@ -640,6 +646,24 @@ def _integration_summary(registry: object, principal: object) -> tuple[tuple[str
return tuple(configured), tuple(limitations)
def _delivery_policy_summary(context: DocumentationContext) -> tuple[tuple[str, ...], tuple[str, ...]]:
session = context.session
tenant_id = str(getattr(context.principal, "tenant_id", "") or "").strip()
if session is None or not tenant_id:
return (), ("The effective synchronous Campaign limit could not be resolved for this documentation request.",)
try:
policy = effective_synchronous_send_policy(session, tenant_id=tenant_id) # type: ignore[arg-type]
except CampaignDeliveryPolicyError as exc:
return (), (f"Synchronous Campaign delivery is disabled until its policy configuration is corrected: {exc}",)
return (
(
"Delivery policy: Send now is limited to "
f"{policy.max_recipient_jobs} eligible recipient job(s) for this tenant. "
"The exact built count and Queue for workers alternative are shown before confirmation."
),
), ()
def _append_if(
target: list[str],
principal: object,