import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; import { PrismaClient } from '@prisma/client'; import { makeFakePorts } from '@insignia/iios-testkit'; import { resetDb } from '../test-utils/reset-db'; import { OutboundService } from '../adapters/outbound.service'; import { CapabilityBroker } from '../capability/capability.broker'; import { CapabilityProviderRegistry } from '../capability/capability.registry'; import { IdempotencyService } from '../idempotency/idempotency.service'; import { TemplateRepository } from './template.repository'; import { TemplateService } from './template.service'; import { TemplatedSender } from './templated-sender'; import type { PrismaService } from '../prisma/prisma.service'; const url = process.env.DATABASE_URL ?? 'postgresql://iios:iios@localhost:5434/iios?schema=public'; const prisma = new PrismaClient({ datasources: { db: { url } } }); const asService = prisma as unknown as PrismaService; function sender(): TemplatedSender { const outbound = new OutboundService(asService, new CapabilityBroker(makeFakePorts(), new CapabilityProviderRegistry()), new IdempotencyService(asService)); const templates = new TemplateService(new TemplateRepository(asService)); return new TemplatedSender(templates, outbound, makeFakePorts()); } async function seedReceipt() { await prisma.iiosMessageTemplate.create({ data: { key: 'payment.receipt', channel: 'EMAIL', locale: 'en', version: 1, subject: 'Thanks {{firstName}}', bodyHtml: '

{{amount}}

', bodyText: '{{amount}}', variables: ['firstName', 'amount'] }, }); } beforeAll(async () => { await prisma.$connect(); }); afterAll(async () => { await prisma.$disconnect(); }); beforeEach(async () => { await resetDb(prisma); }); describe('TemplatedSender.sendTemplated', () => { it('renders a stored template, queues an outbound command, and stamps provenance', async () => { await seedReceipt(); const cmd = await sender().sendTemplated({ source: { key: 'payment.receipt' }, channel: 'EMAIL', target: 'dana@acme.com', vars: { firstName: 'Dana', amount: '$2,000' }, idempotencyKey: 'receipt:cs_1', }); expect(cmd.channelType).toBe('EMAIL'); expect(cmd.target).toBe('dana@acme.com'); expect((cmd.payload as { subject: string }).subject).toBe('Thanks Dana'); expect(cmd.templateKey).toBe('payment.receipt'); expect(cmd.templateVersion).toBe(1); expect(cmd.templateLocale).toBe('en'); expect(cmd.renderedHash).toMatch(/^[a-f0-9]{64}$/); }); it('is idempotent per key: a replay yields ONE command', async () => { await seedReceipt(); const s = sender(); const input = { source: { key: 'payment.receipt' }, channel: 'EMAIL' as const, target: 'dana@acme.com', vars: { firstName: 'Dana', amount: '$1' }, idempotencyKey: 'receipt:cs_dup' }; const a = await s.sendTemplated(input); const b = await s.sendTemplated(input); expect(b.id).toBe(a.id); expect(await prisma.iiosOutboundCommand.count({ where: { idempotencyKey: 'receipt:cs_dup' } })).toBe(1); }); it('sends inline content with null template key but a hash', async () => { const cmd = await sender().sendTemplated({ source: { inline: { subject: 'Hi {{n}}', html: '{{n}}', variables: ['n'] } }, channel: 'EMAIL', target: 'x@y.com', vars: { n: 'Z' }, idempotencyKey: 'inline:1', }); expect((cmd.payload as { subject: string }).subject).toBe('Hi Z'); expect(cmd.templateKey).toBeNull(); expect(cmd.renderedHash).toMatch(/^[a-f0-9]{64}$/); }); it('carries attachment refs into the EMAIL command payload', async () => { await seedReceipt(); const cmd = await sender().sendTemplated({ source: { key: 'payment.receipt' }, channel: 'EMAIL', target: 'dana@acme.com', vars: { firstName: 'Dana', amount: '$1' }, idempotencyKey: 'att:1', attachments: [{ filename: 'invoice.pdf', contentRef: 'obj/1', mimeType: 'application/pdf' }], }); expect((cmd.payload as { attachments: unknown[] }).attachments).toEqual([{ filename: 'invoice.pdf', contentRef: 'obj/1', mimeType: 'application/pdf' }]); }); it('shapes an SMS send as text only', async () => { await prisma.iiosMessageTemplate.create({ data: { key: 'payment.receipt', channel: 'SMS', locale: 'en', version: 1, bodyText: 'Paid {{amount}}', variables: ['amount'] }, }); const cmd = await sender().sendTemplated({ source: { key: 'payment.receipt' }, channel: 'SMS', target: '+1415', vars: { amount: '$2,000' }, idempotencyKey: 'sms:1', }); expect(cmd.channelType).toBe('SMS'); expect(cmd.payload).toEqual({ text: 'Paid $2,000' }); }); });