import { BadRequestException, Body, Controller, Headers, Post } from '@nestjs/common'; import { IiosTemplateChannel } from '@prisma/client'; import { SessionVerifier } from '../platform/session.verifier'; import { ActorResolver, type MessagePrincipal } from '../identity/actor.resolver'; import { TemplatedSender, type ExternalChannel } from './templated-sender'; import { TemplateService } from './template.service'; import { PreviewTemplateDto, SendTemplateDto } from './template.dto'; import type { TemplateSource } from './template.model'; @Controller('v1/templates') export class TemplateController { constructor( private readonly sender: TemplatedSender, private readonly templates: TemplateService, private readonly session: SessionVerifier, private readonly actors: ActorResolver, ) {} /** Render a stored/inline template and queue it for external delivery (EMAIL/SMS). */ @Post('send') async send(@Body() body: SendTemplateDto, @Headers('authorization') authorization?: string) { const principal = this.principal(authorization); const scope = await this.actors.resolveScope(principal); return this.sender.sendTemplated({ source: this.source(body), channel: body.channel as ExternalChannel, target: body.target, vars: body.vars ?? {}, scopeId: scope.id, ...(body.locale ? { locale: body.locale } : {}), idempotencyKey: body.idempotencyKey, ...(body.purpose ? { purpose: body.purpose } : {}), }); } /** Render only — no send. For marketing/QA to eyeball copy before it goes out. */ @Post('preview') async preview(@Body() body: PreviewTemplateDto, @Headers('authorization') authorization?: string) { const principal = this.principal(authorization); const scope = await this.actors.resolveScope(principal); const { content } = await this.templates.render(this.source(body), body.vars ?? {}, { channel: body.channel as IiosTemplateChannel, ...(body.locale ? { locale: body.locale } : {}), scopeId: scope.id, }); return content; } /** Map the DTO's key|inline into a TemplateSource (exactly one must be present). */ private source(body: { key?: string; version?: number; inline?: { subject?: string; html?: string; text?: string; variables?: string[] } }): TemplateSource { if (body.inline && body.key) throw new BadRequestException('provide either "key" or "inline", not both'); if (body.inline) return { inline: body.inline }; if (body.key) return body.version != null ? { key: body.key, version: body.version } : { key: body.key }; throw new BadRequestException('one of "key" or "inline" is required'); } private principal(authorization?: string): MessagePrincipal { const token = (authorization ?? '').replace(/^Bearer\s+/i, ''); if (!token) throw new BadRequestException('Authorization bearer token is required'); return this.session.verify(token); } }