import { Injectable, NotFoundException } from '@nestjs/common'; import { IiosMessageTemplate, IiosTemplateChannel } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; export class TemplateNotFoundError extends NotFoundException { constructor(key: string, channel: string, locale: string) { super(`no active template for key="${key}" channel=${channel} locale=${locale}`); this.name = 'TemplateNotFoundError'; } } @Injectable() export class TemplateRepository { constructor(private readonly prisma: PrismaService) {} /** * Resolve the template to use: the highest active version for (key, channel, locale), preferring * a row owned by `scopeId` (a tenant override) and falling back to the global default (scopeId * NULL). Throws if neither exists. */ async resolve( key: string, channel: IiosTemplateChannel, locale = 'en', scopeId?: string, version?: number, ): Promise { // Scoped override first, then the global default — for both the pinned and highest-active paths. if (scopeId) { const scoped = await this.pick({ key, channel, locale, scopeId }, version); if (scoped) return scoped; } const global = await this.pick({ key, channel, locale, scopeId: null }, version); if (global) return global; throw new TemplateNotFoundError(key, channel, locale); } /** A pinned `version` fetches that exact version (the caller was explicit); otherwise the highest * active version — a retracted (inactive) newest version falls back to the last good one. */ private pick( where: { key: string; channel: IiosTemplateChannel; locale: string; scopeId: string | null }, version?: number, ) { return this.prisma.iiosMessageTemplate.findFirst({ where: version != null ? { ...where, version } : { ...where, active: true }, orderBy: { version: 'desc' }, }); } }