[Feature] Audit-proof single-recipient campaign send #69

Open
opened 2026-07-14 20:08:41 +02:00 by zemion · 4 comments
Owner

Problem / User Need

Operators need to transport exactly one immutable built campaign message without accidentally changing unrelated recipients. Test, official one-off send, and resend have distinct state and audit consequences.

Accepted Semantics

  • Test: transport the selected rendered message once and log the attempt/result, but leave canonical job, recipient, campaign completion, and official-attempt state unsent/unchanged.
  • Single-send: make one initial official delivery attempt for a selected unsent message, log it, and mark only that message done after successful SMTP acceptance. Failure is logged and leaves it not done.
  • Single-resend: explicitly transport one selected message again whether it previously succeeded, failed, was cancelled, or was never attempted. Log a new action/attempt linked to the earlier history. A failed resend never erases an earlier successful disposition.

These operations never enqueue or alter other campaign messages.

Acceptance Criteria

  • API requires kind: test | single_send | single_resend; omission never defaults to an official action.
  • Every invocation targets exactly one immutable built job and performs at most one SMTP attempt; replay of the same command is idempotent and does not become an implicit retry.
  • Test leaves official state and completion aggregation unchanged.
  • Single-send marks only the selected message done after successful transport.
  • Single-resend is allowed regardless of prior final outcome. Only active or uncertain concurrency states such as claimed, sending, or outcome-unknown must be resolved first.
  • Successful resend sets done if not already done; failed resend preserves any earlier successful disposition.
  • Resend requires a reason. Every mode records kind, actor, timestamps, campaign/version/job/message identities and hashes, recipient evidence, reason/context, idempotency key, SMTP outcome, and linked attempt.
  • The action record exists before delivery and is finalized afterwards, so initiation failures are auditable. Test/resend history remains separate from canonical state.
  • Protocol/report output distinguishes all three action kinds and outcomes.
  • UI names each consequence, shows recipient/version/message identity, requires the resend reason, and confirms the selected mode.
  • Tests cover accepted, temporary, permanent, and unknown outcomes; the prior-state matrix; duplicate/concurrent calls; authorization; audit/report output; and completion aggregation.

Audited Local State (2026-07-20)

Current WIP implements only a generic initial official send. It has no kind/reason, cannot test-send with transport while preserving unsent state, rejects resends after any prior attempt, and lacks the required action/audit fields and focused UI tests. Implement this contract as an isolated slice rather than treating the WIP as complete.

## Problem / User Need Operators need to transport exactly one immutable built campaign message without accidentally changing unrelated recipients. Test, official one-off send, and resend have distinct state and audit consequences. ## Accepted Semantics - **Test**: transport the selected rendered message once and log the attempt/result, but leave canonical job, recipient, campaign completion, and official-attempt state unsent/unchanged. - **Single-send**: make one initial official delivery attempt for a selected unsent message, log it, and mark only that message done after successful SMTP acceptance. Failure is logged and leaves it not done. - **Single-resend**: explicitly transport one selected message again whether it previously succeeded, failed, was cancelled, or was never attempted. Log a new action/attempt linked to the earlier history. A failed resend never erases an earlier successful disposition. These operations never enqueue or alter other campaign messages. ## Acceptance Criteria - [ ] API requires `kind: test | single_send | single_resend`; omission never defaults to an official action. - [ ] Every invocation targets exactly one immutable built job and performs at most one SMTP attempt; replay of the same command is idempotent and does not become an implicit retry. - [ ] Test leaves official state and completion aggregation unchanged. - [ ] Single-send marks only the selected message done after successful transport. - [ ] Single-resend is allowed regardless of prior final outcome. Only active or uncertain concurrency states such as claimed, sending, or outcome-unknown must be resolved first. - [ ] Successful resend sets done if not already done; failed resend preserves any earlier successful disposition. - [ ] Resend requires a reason. Every mode records kind, actor, timestamps, campaign/version/job/message identities and hashes, recipient evidence, reason/context, idempotency key, SMTP outcome, and linked attempt. - [ ] The action record exists before delivery and is finalized afterwards, so initiation failures are auditable. Test/resend history remains separate from canonical state. - [ ] Protocol/report output distinguishes all three action kinds and outcomes. - [ ] UI names each consequence, shows recipient/version/message identity, requires the resend reason, and confirms the selected mode. - [ ] Tests cover accepted, temporary, permanent, and unknown outcomes; the prior-state matrix; duplicate/concurrent calls; authorization; audit/report output; and completion aggregation. ## Audited Local State (2026-07-20) Current WIP implements only a generic initial official send. It has no kind/reason, cannot test-send with transport while preserving unsent state, rejects resends after any prior attempt, and lacks the required action/audit fields and focused UI tests. Implement this contract as an isolated slice rather than treating the WIP as complete.
Author
Owner

Codex State: started

Summary

  • Starting implementation: support audit-proof single-message send from preview/review for one recipient entry.

Suggested status label: status/in-progress

## Codex State: started ### Summary - Starting implementation: support audit-proof single-message send from preview/review for one recipient entry. Suggested status label: `status/in-progress`
Author
Owner

Codex State: done

Summary

  • Implemented audit-path single message send from built-message preview with confirmation.
  • Added backend endpoint /campaigns/{campaign_id}/jobs/{job_id}/send using the existing CampaignJob/SendAttempt protocol and EML evidence verification.
  • Added preview footer action, guarded disabled states, and UI status refresh after the one-message send.

Changed Files

  • src/govoplan_campaign/backend/sending/jobs.py
  • src/govoplan_campaign/backend/router.py
  • src/govoplan_campaign/backend/schemas.py
  • webui/src/api/campaigns.ts
  • webui/src/features/campaigns/ReviewSendPage.tsx
  • webui/src/features/campaigns/components/MessagePreviewOverlay.tsx

Verification

  • /mnt/DATA/git/govoplan/.venv/bin/python -m unittest tests.test_zip_service tests.test_attachment_building
  • GOVOPLAN_WEBUI_MODULE_PACKAGES=@govoplan/campaign-webui npm run build (from govoplan-core/webui)
## Codex State: done ### Summary - Implemented audit-path single message send from built-message preview with confirmation. - Added backend endpoint /campaigns/{campaign_id}/jobs/{job_id}/send using the existing CampaignJob/SendAttempt protocol and EML evidence verification. - Added preview footer action, guarded disabled states, and UI status refresh after the one-message send. ### Changed Files - `src/govoplan_campaign/backend/sending/jobs.py` - `src/govoplan_campaign/backend/router.py` - `src/govoplan_campaign/backend/schemas.py` - `webui/src/api/campaigns.ts` - `webui/src/features/campaigns/ReviewSendPage.tsx` - `webui/src/features/campaigns/components/MessagePreviewOverlay.tsx` ### Verification - `/mnt/DATA/git/govoplan/.venv/bin/python -m unittest tests.test_zip_service tests.test_attachment_building` - `GOVOPLAN_WEBUI_MODULE_PACKAGES=@govoplan/campaign-webui npm run build (from govoplan-core/webui)`
Author
Owner

No remote commit, PR, or branch contains this work; the endpoint and UI are only uncommitted WIP. The WIP uses the existing claim path, but it has no send-kind or reason/context, treats every action as a normal SMTP attempt, does not fully audit failed initiation, and has no focused tests. Reopening until terminology and guardrails for test, partial/official, and resend behavior are decided and the isolated implementation is integrated.

No remote commit, PR, or branch contains this work; the endpoint and UI are only uncommitted WIP. The WIP uses the existing claim path, but it has no send-kind or reason/context, treats every action as a normal SMTP attempt, does not fully audit failed initiation, and has no focused tests. Reopening until terminology and guardrails for test, partial/official, and resend behavior are decided and the isolated implementation is integrated.
zemion reopened this issue 2026-07-20 14:35:21 +02:00
Author
Owner

Product terminology and state semantics were accepted on 2026-07-20. The issue is now ready for an isolated implementation slice; #63 vocabulary remains a separate review decision.

Product terminology and state semantics were accepted on 2026-07-20. The issue is now ready for an isolated implementation slice; #63 vocabulary remains a separate review decision.
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: GovOPlaN/govoplan-campaign#69
No description provided.