feat(templates): T4 sendTemplated() + outbound provenance

- IiosOutboundCommand gains templateKey/version/locale/renderedHash (migration).
- OutboundService.send accepts an optional opaque `provenance` and writes it into
  the command on BOTH the PENDING and RATE_LIMITED create paths — the only way to
  stamp provenance by construction, since send() creates the row itself (review
  finding). OutboundService still neither renders nor resolves templates.
- TemplatedSender.sendTemplated: render -> OutboundService.send -> provenance, one
  entry point. EMAIL payload = {subject,html,text}; SMS = {text}. Idempotent per key.

Tests: 4 sender + 16 adapters regression green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 12:28:28 +05:30
parent 8ce647552a
commit 10f3545a25
6 changed files with 162 additions and 3 deletions
@@ -0,0 +1,9 @@
-- Template provenance on the outbound command: which template (key/version/locale) produced this
-- send, plus a hash of the rendered content. The rendered content itself already lives in `payload`;
-- these columns answer "which template version produced this send?" for replay/audit. All nullable —
-- non-templated sends leave them null.
ALTER TABLE "IiosOutboundCommand"
ADD COLUMN "templateKey" TEXT,
ADD COLUMN "templateVersion" INTEGER,
ADD COLUMN "templateLocale" TEXT,
ADD COLUMN "renderedHash" TEXT;
@@ -868,6 +868,11 @@ model IiosOutboundCommand {
idempotencyKey String @unique
providerRef String?
consentReceiptRef String? // CMP consent receipt this send went out under (P9)
// Template provenance (which source produced this send) — null for non-templated sends.
templateKey String?
templateVersion Int?
templateLocale String?
renderedHash String? // sha256 of the rendered content, for replay/audit
createdAt DateTime @default(now())
attempts IiosDeliveryAttempt[]