feat(templates): T2 IiosMessageTemplate table + resolve()

Schema + migration for the template source table, and TemplateRepository.resolve:
highest active version for (key,channel,locale), scoped override preferred over
the global (scopeId NULL) default.

Migration adds a PARTIAL unique index WHERE scopeId IS NULL so two platform
defaults for the same key can't coexist (Postgres treats NULL as distinct under
a plain UNIQUE — the scoped constraint alone wouldn't catch it). 6 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 12:23:44 +05:30
parent 8768af96c1
commit b3afd44d00
4 changed files with 180 additions and 0 deletions
@@ -0,0 +1,39 @@
-- Reusable message templates (email/SMS/in-app). DB is runtime truth; platform defaults seeded
-- from repo files on boot. Versions are immutable (a change = a new higher version).
CREATE TYPE "IiosTemplateChannel" AS ENUM ('EMAIL', 'SMS', 'INTERNAL');
CREATE TABLE "IiosMessageTemplate" (
"id" TEXT NOT NULL,
"scopeId" TEXT,
"key" TEXT NOT NULL,
"channel" "IiosTemplateChannel" NOT NULL,
"locale" TEXT NOT NULL DEFAULT 'en',
"version" INTEGER NOT NULL DEFAULT 1,
"subject" TEXT,
"bodyHtml" TEXT,
"bodyText" TEXT,
"variables" JSONB,
"active" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "IiosMessageTemplate_pkey" PRIMARY KEY ("id")
);
ALTER TABLE "IiosMessageTemplate"
ADD CONSTRAINT "IiosMessageTemplate_scopeId_fkey"
FOREIGN KEY ("scopeId") REFERENCES "IiosScope"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- Uniqueness for SCOPED rows (scopeId NOT NULL).
CREATE UNIQUE INDEX "IiosMessageTemplate_scopeId_key_channel_locale_version_key"
ON "IiosMessageTemplate" ("scopeId", "key", "channel", "locale", "version");
-- Uniqueness for GLOBAL rows (scopeId IS NULL). Postgres treats NULL as distinct under a normal
-- UNIQUE, so the constraint above does NOT stop two platform defaults for the same key — this
-- partial index does. (Same footgun handled in the inbox idempotency migration.)
CREATE UNIQUE INDEX "IiosMessageTemplate_global_key"
ON "IiosMessageTemplate" ("key", "channel", "locale", "version")
WHERE "scopeId" IS NULL;
-- Resolution lookup: by key/channel/locale among active rows.
CREATE INDEX "IiosMessageTemplate_key_channel_locale_active_idx"
ON "IiosMessageTemplate" ("key", "channel", "locale", "active");