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:
@@ -96,6 +96,12 @@ enum IiosInboxState {
|
||||
STALE
|
||||
}
|
||||
|
||||
enum IiosTemplateChannel {
|
||||
EMAIL
|
||||
SMS
|
||||
INTERNAL
|
||||
}
|
||||
|
||||
enum IiosTicketState {
|
||||
NEW
|
||||
OPEN
|
||||
@@ -310,6 +316,7 @@ model IiosScope {
|
||||
tickets IiosTicket[]
|
||||
callbacks IiosCallbackRequest[]
|
||||
notificationSubscriptions IiosNotificationSubscription[]
|
||||
messageTemplates IiosMessageTemplate[]
|
||||
|
||||
@@index([orgId, appId, tenantId])
|
||||
}
|
||||
@@ -677,6 +684,33 @@ model IiosInboxItem {
|
||||
@@index([ownerActorId, state, priority])
|
||||
}
|
||||
|
||||
/// A reusable message template (email/SMS/in-app). DB is runtime truth; platform defaults are
|
||||
/// seeded from repo files on boot. Versions are immutable — a change writes a new (higher) version,
|
||||
/// never edits in place, so a rendered send can always be traced to the exact source it used.
|
||||
/// scopeId NULL = a platform default; set = a tenant scope's override of the same key. Resolution
|
||||
/// prefers the scoped row, else the global default. (The global-uniqueness of NULL-scope rows is
|
||||
/// enforced by a partial unique index added in the migration — Postgres treats NULL as distinct.)
|
||||
model IiosMessageTemplate {
|
||||
id String @id @default(cuid())
|
||||
scopeId String?
|
||||
scope IiosScope? @relation(fields: [scopeId], references: [id], onDelete: Cascade)
|
||||
key String
|
||||
channel IiosTemplateChannel
|
||||
locale String @default("en")
|
||||
version Int @default(1)
|
||||
subject String?
|
||||
bodyHtml String?
|
||||
bodyText String?
|
||||
/// Declared variable names — render throws if a declared var is missing (fail loud).
|
||||
variables Json?
|
||||
active Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([scopeId, key, channel, locale, version])
|
||||
@@index([key, channel, locale, active])
|
||||
}
|
||||
|
||||
/// Audit trail of inbox-item state transitions.
|
||||
model IiosInboxItemStateHistory {
|
||||
id String @id @default(cuid())
|
||||
|
||||
Reference in New Issue
Block a user