import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' import { prisma, createCaller } from '../setup' import { createTestUser, createTestProgram, cleanupTestData, uid, } from '../helpers' import { lunchRouter } from '@/server/routers/lunch' import { buildRecapPayload } from '@/server/services/lunch-recap' vi.mock('@/lib/email', async () => { const actual = await vi.importActual('@/lib/email') return { ...actual, sendLunchRecapEmail: vi.fn(async () => undefined) } }) const programIds: string[] = [] const userIds: string[] = [] afterAll(async () => { for (const programId of programIds) { await prisma.externalAttendee.deleteMany({ where: { lunchEvent: { programId } } }) await prisma.dish.deleteMany({ where: { lunchEvent: { programId } } }) await prisma.lunchEvent.deleteMany({ where: { programId } }) await cleanupTestData(programId, []) } if (userIds.length > 0) { await prisma.auditLog.deleteMany({ where: { userId: { in: userIds } } }) await prisma.user.deleteMany({ where: { id: { in: userIds } } }) } }) describe('buildRecapPayload', () => { it('aggregates dish + dietary + allergen counts', async () => { const program = await createTestProgram({ name: `recap-${uid()}` }) programIds.push(program.id) const event = await prisma.lunchEvent.create({ data: { programId: program.id, enabled: true }, }) const veg = await prisma.dish.create({ data: { lunchEventId: event.id, name: 'Risotto', dietaryTags: ['VEGETARIAN'] }, }) const fish = await prisma.dish.create({ data: { lunchEventId: event.id, name: 'Sea bass', dietaryTags: ['PESCATARIAN'] }, }) await prisma.externalAttendee.create({ data: { lunchEventId: event.id, name: 'A', dishId: veg.id, allergens: ['GLUTEN'] }, }) await prisma.externalAttendee.create({ data: { lunchEventId: event.id, name: 'B', dishId: fish.id, allergens: ['GLUTEN', 'FISH'] }, }) const payload = await buildRecapPayload(prisma, program.id) expect(payload.dishCounts['Risotto']).toBe(1) expect(payload.dishCounts['Sea bass']).toBe(1) expect(payload.dietaryCounts['VEGETARIAN']).toBe(1) expect(payload.dietaryCounts['PESCATARIAN']).toBe(1) expect(payload.allergenCounts['GLUTEN']).toBe(2) expect(payload.allergenCounts['FISH']).toBe(1) }) }) describe('lunch.sendRecap', () => { beforeEach(() => { vi.clearAllMocks() }) async function newAdminCaller() { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const caller = createCaller(lunchRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) return { admin, caller } } it('sends and stamps recapSentAt on first call', async () => { const program = await createTestProgram({ name: `recap-send-${uid()}` }) programIds.push(program.id) await prisma.lunchEvent.create({ data: { programId: program.id, enabled: true }, }) const { caller } = await newAdminCaller() await caller.sendRecap({ programId: program.id }) const row = await prisma.lunchEvent.findUnique({ where: { programId: program.id } }) expect(row?.recapSentAt).not.toBeNull() }) it('throws PRECONDITION_FAILED on second send unless forceUpdate', async () => { const program = await createTestProgram({ name: `recap-pre-${uid()}` }) programIds.push(program.id) await prisma.lunchEvent.create({ data: { programId: program.id, enabled: true, recapSentAt: new Date() }, }) const { caller } = await newAdminCaller() await expect( caller.sendRecap({ programId: program.id }), ).rejects.toThrow(/already sent/i) await expect( caller.sendRecap({ programId: program.id, forceUpdate: true }), ).resolves.toBeTruthy() }) it('writes a LUNCH_RECAP_SENT audit row', async () => { const program = await createTestProgram({ name: `recap-audit-${uid()}` }) programIds.push(program.id) await prisma.lunchEvent.create({ data: { programId: program.id, enabled: true }, }) const { admin, caller } = await newAdminCaller() await caller.sendRecap({ programId: program.id }) const audit = await prisma.auditLog.findFirst({ where: { action: 'LUNCH_RECAP_SENT', userId: admin.id }, orderBy: { timestamp: 'desc' }, }) expect(audit).not.toBeNull() expect((audit?.detailsJson as Record | null)?.source).toBe('manual') }) })