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
+46 -14
View File
@@ -30,12 +30,13 @@ The shorter task documents remain useful companions:
## What Campaign is for
Campaign turns governed source data into individually built messages and then
controls their review, delivery, and evidence. It is intentionally a
composition module: it demonstrates how one user journey can use optional Mail,
Files, Addresses, and Notifications capabilities alongside Core access/audit
infrastructure without copying ownership from those modules. Policy may consume
Campaign context through a narrow capability; Campaign does not import Policy.
Campaign turns governed source data into individually built messages or
printable output and then controls their review, delivery, and evidence. It is
intentionally a composition module: it demonstrates how one user journey can
use optional Mail, Files, Addresses, Distribution Lists, Templates, Postbox,
and Notifications capabilities alongside Core access/audit infrastructure
without copying ownership from those modules. Policy may consume Campaign
context through a narrow capability; Campaign does not import Policy.
Campaign owns:
@@ -66,9 +67,9 @@ The supported process is a controlled progression, not a single "send" call:
create/edit
-> validate and resolve policy/integrations
-> review warnings and blockers
-> build exact recipient messages
-> build exact recipient messages and/or printable artifacts
-> complete review and queue
-> SMTP attempt per job
-> selected Mail, Postbox, or print effect per job
-> optional IMAP append per accepted job
-> report, retry, reconcile, or correct
-> archive when no active/uncertain delivery remains
@@ -413,6 +414,7 @@ Current principal contracts include:
| `addresses.lookup` 0.1.x | Addresses -> Campaign | Optional address suggestions |
| `addresses.recipient_source` 0.1.x | Addresses -> Campaign | Optional versioned recipient-source snapshots |
| `dist_lists.source` / `dist_lists.expand` 0.1.x | Distribution Lists -> Campaign | Discover, preview, and freeze reusable audiences without importing module internals |
| `templates.catalog` / `templates.renderer` 0.1.x | Templates -> Campaign | Select compatible published printable templates and produce deterministic, evidence-bearing artifacts |
| `campaigns.access` 0.1.x | Campaign -> platform | Explain campaign access/existence without exporting ORM objects |
| `campaigns.mail_policy_context` 0.1.x | Campaign -> Mail | Resolve campaign tenant/owner context for Mail policy |
| `campaigns.delivery_tasks` 0.1.x | Campaign -> workers | Execute narrow queued send/append tasks |
@@ -433,16 +435,46 @@ the resulting rows into the editable Campaign version.
Each copied row retains the list and revision IDs, definition and expansion
hashes, snapshot ID, source entry IDs, provider references, channel candidates,
the one selected route where it is unambiguous, fallback candidates, and the
decision explanation. Campaign-only fields, attachment rules, review state,
and outcomes remain local to Campaign and never mutate the reusable list.
the explicitly selected primary route, optional fallback, and the decision
explanation. Campaign-only fields, attachment rules, review state, and outcomes
remain local to Campaign and never mutate the reusable list.
A later list revision only raises a drift warning. Refresh is deliberate and
uses append or replace; saving that changed Campaign version clears prior
validation, build, review, and execution state through the normal content
invalidation path. Postal-only or otherwise unsupported routes remain present
in the frozen evidence but inactive until a compatible Campaign output path is
configured.
invalidation path. Preferred or single usable candidates are preselected
visibly; ambiguous rows must be decided before freezing. Postal and
internal-mail routes remain active when a compatible published Templates output
is selected.
### Governed hybrid and printable delivery
Campaign supports Mail, Postbox, printable output, and bounded ordered
fallbacks without making any of those provider modules mandatory. Opt-in and
channel-preference data are inputs to the visible routing decision; they never
silently cause duplicate delivery.
For a printable route, select a published label, envelope, serial-letter,
form-letter, list-layout, or generic template on the Template page. Validation
checks the selected revision, output format, and required fields. Build sends
one deterministic item collection to `templates.renderer`, records template,
input, output, actor, route, and artifact hashes, and stores the resulting
artifact through Files when configured. The review stage exposes that exact
artifact and its hashes before execution.
Each recipient job records an idempotent print acceptance attempt for its item
in the frozen artifact. `mail_then_print` and `postbox_then_print` invoke print
only after a confirmed rejection before acceptance. An accepted or
outcome-unknown digital effect never falls through to print because that could
produce duplicate delivery. Reports and CSV exports include route provenance,
print state, attempts, artifact reference, and hashes.
Without Templates, Campaign still loads and Mail/Postbox authoring remains
available; validation explains why a configured print route cannot proceed.
Without Files, Templates may return a bounded artifact instead of a managed
file. Campaign copies that payload into shared object storage and exposes it
through the Campaign ACL plus `campaigns:recipient:read`; it never redistributes
the broader Templates URL. A print-only Campaign does not require Mail or Postbox.
### External API expectations