R1.4 Client callbacks for delivery and engagement events - #20
Conversation
Adds the plan R1.4 client callback channel alongside the existing CMP callback system, which stays because CMP is a production consumer: per-client CallbackEndpoint (URL, dedicated HMAC signing secret with rotation overlap, contract version, enable/disable), a durable ClientCallback outbox deduplicated on (client, event_id) with a stable uuid5 event id per transition, canonical JSON payloads that carry identifiers, timestamp, sequence, bounce_type and safe reason codes only, the same <timestamp>.<body> HMAC-SHA256 signature scheme as webhook tasks, bounded exponential backoff with terminal 4xx and redirects, per-message sequence counters, and the process_client_callbacks dispatcher wired into operator surfaces, docs and the sandbox deploy script. Callback emission covers transactional-message transitions and client-level subscription changes; campaign-recipient transitions and terminal send failures stay on the CMP channel (failed sends reach plan clients through reconciliation instead). Adds GET /api/transactional/messages?since= for reconciliation: Bearer-authenticated, per-client, oldest first, capped at 1000 rows, with the package reconciliation vocabulary (queued, retrying, sent, delivered, suppressed, failed, bounced, complained) and pattern-safe reason codes. Closes #17
1 similar comment
|
Pre-merge verification from a read-only pass at head 5d7bfbb: Issue coverage (R1.4 card) is complete at this head:
Before merging: GitHub reports this branch CONFLICTING with current main after the R1.5 merge. Overlapping files: docs/api.md, mailing/models.py, mailing/services/api.py, mailing/services/transactional.py, mailing/tests/test_client_api.py, mailing/tests/test_transactional_api.py, mailing/urls.py, mailing/views.py, relay/settings.py. One trap while resolving: both sides add a migration numbered 0026 (this branch: The local full pytest battery was interrupted mid-run (verification worktree torn down before completion), so CI plus the targeted checks above are the evidence recorded here. |
# Conflicts: # mailing/services/api.py # mailing/services/transactional.py # mailing/tests/test_transactional_api.py
|
Merged origin/main (R1.1, R1.3, R1.5) into the branch and re-verified. Post-merge adaptations:
Re-verification on the merged tree:
|
1 similar comment
|
Merged origin/main (R1.1, R1.3, R1.5) into the branch and re-verified. Post-merge adaptations:
Re-verification on the merged tree:
|
Plan issue: R1.4 (DataTalksClub/community-base docs/plan/phase-1.md). Mirrored as #17. Depends on R1.2 (merged in #9).
What this does
CallbackEndpoint: URL, dedicated HMAC signing secret with rotation overlap, contract version, enable/disable) and a durableClientCallbackoutbox deduplicated on(client, event_id), with stableuuid5event ids per transition.delivery.accepted,delivery.delivered,delivery.bounced(withbounce_typehard/soft),delivery.complained,delivery.suppressed,engagement.opened,engagement.clicked,subscription.changed— matching the contract pinned by the community-base package client (callback_ingress.py:timestampfield,bounce_typeselector,event_id/message_id/client_reference/reason_codeshapes).sha256=HMAC-SHA256 over<timestamp>.<raw body>, headersX-Relay-Timestamp/X-Relay-Signature(+ event id, type, contract version, attempt). No Bearer credential.sequencecounters for best-effort ordering; duplicateevent_idemission is deduplicated (unique constraint + test).GET /api/transactional/messages?since=reconciliation: Bearer-authenticated, scoped to the authenticated client,updated_at >= since, oldest first, capped at 1000 rows, package reconciliation vocabulary (queued,retrying,sent,delivered,suppressed,failed,bounced,complained) with pattern-safe reason codes.template_versionreports 1 until R1.3 introduces versions.docs/api.mddocuments callback payloads, signature headers, reference receiver, retry semantics, and the reconciliation endpoint.tests/fixtures/client_callback_contract_v1.jsongives consumers a deterministic verification vector.Scope decision: CMP callbacks stay
The first WIP commit on this branch deleted the CMP callback system. It is restored here unchanged: CMP (
DataTalksClub/course-management-platform) is a production consumer of those callbacks (docs/context.md: "CMP production depends on it. Breaking it stops course email"), and R1.4 does not scope its removal. The two channels now run side by side:client_reference).Ambiguity reading (per the plan): emission scope follows the receiver contract in community-base, which has no handling for campaign-scoped or unknown (
delivery.failed) event types; emitting them would poison the outbox with terminal 4xx failures.Verification
uv run --frozen pytest-> 608 passed (full suite; includes duplicateevent_iddedup, signature/replay-window receiver tests, retry classification, rotation overlap, reconciliation status mapping, auth and validation errors).uv run --frozen ruff check .-> all checks passed.uv run --frozen python scripts/validate_infra.py-> validated.uv run --frozen python manage.py makemigrations --check --dry-run-> no changes detected.Not run here, needs: the sandbox verification (send to the SES simulator bounce address and watch a reference receiver log
delivery.bouncedwith the sentclient_reference) after this is deployed to the sandbox environment.