feat(notifications): renderNotificationEmail + previewEmailTemplate + logistics sample data

- Export `renderNotificationEmail` from email.ts (pure template resolver, no send)
- Refactor `sendStyledNotificationEmail` to delegate to `renderNotificationEmail`
- Hoist sampleData to module-level `NOTIFICATION_SAMPLE_DATA` in notification router
- Add 8 logistics sample entries (FINALIST_*/TRAVEL_CONFIRMED/VISA_STATUS_UPDATE)
- Add `notification.previewEmailTemplate` adminProcedure query (returns subject/html/hasStyledTemplate)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt
2026-06-04 16:39:29 +02:00
parent 27bdf8cdef
commit e5788b3e9d
3 changed files with 230 additions and 124 deletions

View File

@@ -0,0 +1,62 @@
/**
* Task 1: renderNotificationEmail + previewEmailTemplate
*
* Tests that:
* 1. previewEmailTemplate returns rendered HTML and a non-empty subject for a
* type that has a styled template (VISA_STATUS_UPDATE).
* 2. previewEmailTemplate returns non-empty HTML for a fallback type
* (FINALIST_EXPIRED — no registered template, uses generic fallback).
*/
import { afterAll, describe, expect, it } from 'vitest'
import { createCaller, prisma } from '../setup'
import { createTestUser, cleanupTestData, uid } from '../helpers'
import { notificationRouter } from '../../src/server/routers/notification'
describe('notification.previewEmailTemplate', () => {
let adminId: string
let adminEmail: string
afterAll(async () => {
if (adminId) {
await prisma.user.deleteMany({ where: { id: adminId } })
}
})
async function getAdminCaller() {
if (!adminId) {
const admin = await createTestUser('SUPER_ADMIN', {
name: 'Preview Admin',
email: `preview-admin-${uid()}@test.local`,
})
adminId = admin.id
adminEmail = admin.email
}
return createCaller(notificationRouter, {
id: adminId,
email: adminEmail,
name: 'Preview Admin',
role: 'SUPER_ADMIN',
})
}
it('returns HTML containing a visa-related string and a non-empty subject for VISA_STATUS_UPDATE', async () => {
const caller = await getAdminCaller()
const result = await caller.previewEmailTemplate({ notificationType: 'VISA_STATUS_UPDATE' })
expect(result.subject).toBeTruthy()
expect(result.html).toBeTruthy()
// The visa template includes 'visa' or 'Grand' in its content
const lower = result.html.toLowerCase()
expect(lower.includes('visa') || lower.includes('grand')).toBe(true)
expect(result.hasStyledTemplate).toBe(true)
})
it('returns non-empty HTML for FINALIST_EXPIRED (fallback — no styled template)', async () => {
const caller = await getAdminCaller()
const result = await caller.previewEmailTemplate({ notificationType: 'FINALIST_EXPIRED' })
expect(result.html).toBeTruthy()
expect(result.subject).toBeTruthy()
expect(result.hasStyledTemplate).toBe(false)
})
})