import { afterAll, describe, expect, it } from 'vitest' import { prisma, createCaller } from '../setup' import { createTestUser, createTestProgram, createTestProject, cleanupTestData, uid, } from '../helpers' import { lunchRouter } from '@/server/routers/lunch' const programIds: string[] = [] const userIds: string[] = [] afterAll(async () => { for (const programId of programIds) { await prisma.memberLunchPick.deleteMany({ where: { attendingMember: { confirmation: { project: { programId } } } }, }) await prisma.attendingMember.deleteMany({ where: { confirmation: { project: { programId } } }, }) await prisma.finalistConfirmation.deleteMany({ where: { project: { programId } } }) 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 } } }) } }) 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 } } describe('lunch.getEvent', () => { it('lazily creates a LunchEvent on first call', async () => { const program = await createTestProgram({ name: `lunch-get-${uid()}` }) programIds.push(program.id) const { caller } = await newAdminCaller() const result = await caller.getEvent({ programId: program.id }) expect(result.programId).toBe(program.id) expect(result.enabled).toBe(false) expect(result.changeCutoffHours).toBe(48) const row = await prisma.lunchEvent.findUnique({ where: { programId: program.id } }) expect(row).not.toBeNull() }) it('returns the same row on subsequent calls', async () => { const program = await createTestProgram({ name: `lunch-get-${uid()}` }) programIds.push(program.id) const { caller } = await newAdminCaller() const a = await caller.getEvent({ programId: program.id }) const b = await caller.getEvent({ programId: program.id }) expect(a.id).toBe(b.id) }) }) describe('dish CRUD', () => { it('createDish + listDishes returns dishes ordered by sortOrder', async () => { const program = await createTestProgram({ name: `dish-list-${uid()}` }) programIds.push(program.id) const { caller } = await newAdminCaller() const event = await caller.getEvent({ programId: program.id }) await caller.createDish({ lunchEventId: event.id, name: 'Sea bass', dietaryTags: ['PESCATARIAN'], sortOrder: 1, }) await caller.createDish({ lunchEventId: event.id, name: 'Risotto', dietaryTags: ['VEGETARIAN'], sortOrder: 0, }) const dishes = await caller.listDishes({ lunchEventId: event.id }) expect(dishes.map((d: { name: string }) => d.name)).toEqual(['Risotto', 'Sea bass']) }) it('updateDish patches name + tags', async () => { const program = await createTestProgram({ name: `dish-upd-${uid()}` }) programIds.push(program.id) const { caller } = await newAdminCaller() const event = await caller.getEvent({ programId: program.id }) const dish = await caller.createDish({ lunchEventId: event.id, name: 'A', dietaryTags: [] }) const updated = await caller.updateDish({ dishId: dish.id, name: 'B', dietaryTags: ['VEGAN'], }) expect(updated.name).toBe('B') expect(updated.dietaryTags).toEqual(['VEGAN']) }) it('deleteDish sets dishId=null on existing picks', async () => { const program = await createTestProgram({ name: `dish-del-${uid()}` }) programIds.push(program.id) const { caller } = await newAdminCaller() const event = await caller.getEvent({ programId: program.id }) const dish = await caller.createDish({ lunchEventId: event.id, name: 'X', dietaryTags: [] }) const user = await createTestUser('APPLICANT') userIds.push(user.id) const project = await createTestProject(program.id, { title: `dish-del-proj-${uid()}`, competitionCategory: 'STARTUP', }) const conf = await prisma.finalistConfirmation.create({ data: { projectId: project.id, category: 'STARTUP', status: 'CONFIRMED', deadline: new Date(Date.now() + 86_400_000), token: `tok-${uid()}`, }, }) const member = await prisma.attendingMember.create({ data: { confirmationId: conf.id, userId: user.id }, }) await prisma.memberLunchPick.create({ data: { attendingMemberId: member.id, dishId: dish.id, pickedAt: new Date() }, }) await caller.deleteDish({ dishId: dish.id }) const pick = await prisma.memberLunchPick.findUnique({ where: { attendingMemberId: member.id }, }) expect(pick?.dishId).toBeNull() }) it('reorderDishes commits new sortOrder values', async () => { const program = await createTestProgram({ name: `dish-reorder-${uid()}` }) programIds.push(program.id) const { caller } = await newAdminCaller() const event = await caller.getEvent({ programId: program.id }) const a = await caller.createDish({ lunchEventId: event.id, name: 'a', dietaryTags: [], sortOrder: 0, }) const b = await caller.createDish({ lunchEventId: event.id, name: 'b', dietaryTags: [], sortOrder: 1, }) await caller.reorderDishes({ ordered: [ { dishId: b.id, sortOrder: 0 }, { dishId: a.id, sortOrder: 1 }, ], }) const dishes = await caller.listDishes({ lunchEventId: event.id }) expect(dishes.map((d: { name: string }) => d.name)).toEqual(['b', 'a']) }) }) describe('external attendees CRUD', () => { it('listExternals returns standalone + project-attached entries', async () => { const program = await createTestProgram({ name: `ext-list-${uid()}` }) programIds.push(program.id) const { caller } = await newAdminCaller() const event = await caller.getEvent({ programId: program.id }) const project = await createTestProject(program.id, { title: `ext-${uid()}`, competitionCategory: 'STARTUP', }) await caller.createExternal({ lunchEventId: event.id, name: 'Princess Albert', roleNote: 'Foundation rep', }) await caller.createExternal({ lunchEventId: event.id, name: 'Speaker Smith', projectId: project.id, email: 's@example.com', }) const list = await caller.listExternals({ lunchEventId: event.id }) expect(list).toHaveLength(2) expect(list.find((e: { name: string }) => e.name === 'Princess Albert')?.projectId).toBeNull() expect(list.find((e: { name: string }) => e.name === 'Speaker Smith')?.projectId).toBe(project.id) }) it('updateExternal patches fields including dishId + allergens', async () => { const program = await createTestProgram({ name: `ext-upd-${uid()}` }) programIds.push(program.id) const { caller } = await newAdminCaller() const event = await caller.getEvent({ programId: program.id }) const dish = await caller.createDish({ lunchEventId: event.id, name: 'Steak', dietaryTags: [], }) const ext = await caller.createExternal({ lunchEventId: event.id, name: 'X' }) const updated = await caller.updateExternal({ externalId: ext.id, dishId: dish.id, allergens: ['GLUTEN', 'TREE_NUTS'], allergenOther: 'sulphites in red wine', }) expect(updated.dishId).toBe(dish.id) expect(updated.allergens).toEqual(['GLUTEN', 'TREE_NUTS']) }) it('deleteExternal removes the row', async () => { const program = await createTestProgram({ name: `ext-del-${uid()}` }) programIds.push(program.id) const { caller } = await newAdminCaller() const event = await caller.getEvent({ programId: program.id }) const ext = await caller.createExternal({ lunchEventId: event.id, name: 'tmp' }) await caller.deleteExternal({ externalId: ext.id }) const list = await caller.listExternals({ lunchEventId: event.id }) expect(list.find((e: { id: string }) => e.id === ext.id)).toBeUndefined() }) it('rejects non-admin createExternal', async () => { const program = await createTestProgram({ name: `ext-rej-${uid()}` }) programIds.push(program.id) const { caller } = await newAdminCaller() const event = await caller.getEvent({ programId: program.id }) const member = await createTestUser('APPLICANT') userIds.push(member.id) const memberCaller = createCaller(lunchRouter, { id: member.id, email: member.email, role: 'APPLICANT', }) await expect( memberCaller.createExternal({ lunchEventId: event.id, name: 'nope' }), ).rejects.toThrow() }) }) describe('lunch.getEventForMember', () => { it('returns event details when enabled', async () => { const program = await createTestProgram({ name: `efm-en-${uid()}` }) programIds.push(program.id) const member = await createTestUser('APPLICANT') userIds.push(member.id) await prisma.lunchEvent.create({ data: { programId: program.id, enabled: true, eventAt: new Date('2026-06-28T12:30:00Z'), venue: 'Hôtel', changeCutoffHours: 48, }, }) const caller = createCaller(lunchRouter, { id: member.id, email: member.email, role: 'APPLICANT', }) const result = await caller.getEventForMember({ programId: program.id }) expect(result?.venue).toBe('Hôtel') expect(result?.changeDeadline).toBeInstanceOf(Date) }) it('returns null when event is disabled', async () => { const program = await createTestProgram({ name: `efm-dis-${uid()}` }) programIds.push(program.id) const member = await createTestUser('APPLICANT') userIds.push(member.id) await prisma.lunchEvent.create({ data: { programId: program.id, enabled: false }, }) const caller = createCaller(lunchRouter, { id: member.id, email: member.email, role: 'APPLICANT', }) const result = await caller.getEventForMember({ programId: program.id }) expect(result).toBeNull() }) }) describe('lunch.getTeamPicks', () => { it('returns picks for all attending members of the caller team', async () => { const program = await createTestProgram({ name: `tp-ok-${uid()}` }) programIds.push(program.id) const lead = await createTestUser('APPLICANT') const m1 = await createTestUser('APPLICANT') const m2 = await createTestUser('APPLICANT') userIds.push(lead.id, m1.id, m2.id) const project = await createTestProject(program.id, { title: `tp-${uid()}`, competitionCategory: 'STARTUP', }) await prisma.teamMember.createMany({ data: [ { projectId: project.id, userId: lead.id, role: 'LEAD' }, { projectId: project.id, userId: m1.id, role: 'MEMBER' }, { projectId: project.id, userId: m2.id, role: 'MEMBER' }, ], }) const conf = await prisma.finalistConfirmation.create({ data: { projectId: project.id, category: 'STARTUP', status: 'CONFIRMED', deadline: new Date(Date.now() + 86_400_000), token: `tok-${uid()}`, }, }) const am1 = await prisma.attendingMember.create({ data: { confirmationId: conf.id, userId: m1.id }, }) const am2 = await prisma.attendingMember.create({ data: { confirmationId: conf.id, userId: m2.id }, }) const event = await prisma.lunchEvent.create({ data: { programId: program.id, enabled: true }, }) const dish = await prisma.dish.create({ data: { lunchEventId: event.id, name: 'X', dietaryTags: [] }, }) await prisma.memberLunchPick.create({ data: { attendingMemberId: am1.id, dishId: dish.id, pickedAt: new Date() }, }) await prisma.memberLunchPick.create({ data: { attendingMemberId: am2.id } }) const caller = createCaller(lunchRouter, { id: lead.id, email: lead.email, role: 'APPLICANT', }) const picks = await caller.getTeamPicks({ projectId: project.id }) expect(picks).toHaveLength(2) expect(picks.find((p: { userId: string }) => p.userId === m1.id)?.hasPicked).toBe(true) expect(picks.find((p: { userId: string }) => p.userId === m2.id)?.hasPicked).toBe(false) }) it('rejects non-team-member callers', async () => { const program = await createTestProgram({ name: `tp-rej-${uid()}` }) programIds.push(program.id) const stranger = await createTestUser('APPLICANT') userIds.push(stranger.id) const project = await createTestProject(program.id, { title: `tp-rej-${uid()}`, competitionCategory: 'STARTUP', }) const caller = createCaller(lunchRouter, { id: stranger.id, email: stranger.email, role: 'APPLICANT', }) await expect( caller.getTeamPicks({ projectId: project.id }), ).rejects.toThrow() }) }) describe('lunch.getManifest', () => { it('returns confirmed attending members + externals with merged shape', async () => { const program = await createTestProgram({ name: `mfst-${uid()}` }) programIds.push(program.id) const { caller } = await newAdminCaller() const m = await createTestUser('APPLICANT') userIds.push(m.id) const project = await createTestProject(program.id, { title: `mfst-${uid()}`, competitionCategory: 'STARTUP', }) const conf = await prisma.finalistConfirmation.create({ data: { projectId: project.id, category: 'STARTUP', status: 'CONFIRMED', deadline: new Date(Date.now() + 86_400_000), token: `tok-${uid()}`, }, }) const am = await prisma.attendingMember.create({ data: { confirmationId: conf.id, userId: m.id }, }) const event = await prisma.lunchEvent.create({ data: { programId: program.id, enabled: true }, }) const dish = await prisma.dish.create({ data: { lunchEventId: event.id, name: 'Risotto', dietaryTags: ['VEGETARIAN'] }, }) await prisma.memberLunchPick.create({ data: { attendingMemberId: am.id, dishId: dish.id, pickedAt: new Date() }, }) await prisma.externalAttendee.create({ data: { lunchEventId: event.id, name: 'External Bob', dishId: dish.id }, }) const manifest = await caller.getManifest({ programId: program.id }) expect(manifest.members).toHaveLength(1) expect(manifest.externals).toHaveLength(1) expect(manifest.summary.picked).toBe(2) expect(manifest.summary.missing).toBe(0) }) it('excludes non-CONFIRMED confirmations', async () => { const program = await createTestProgram({ name: `mfst-x-${uid()}` }) programIds.push(program.id) const { caller } = await newAdminCaller() const u = await createTestUser('APPLICANT') userIds.push(u.id) const project = await createTestProject(program.id, { title: `mfst-x-${uid()}`, competitionCategory: 'STARTUP', }) const conf = await prisma.finalistConfirmation.create({ data: { projectId: project.id, category: 'STARTUP', status: 'PENDING', deadline: new Date(Date.now() + 86_400_000), token: `tok-${uid()}`, }, }) await prisma.attendingMember.create({ data: { confirmationId: conf.id, userId: u.id }, }) await prisma.lunchEvent.create({ data: { programId: program.id, enabled: true } }) const manifest = await caller.getManifest({ programId: program.id }) expect(manifest.members).toHaveLength(0) }) }) describe('lunch.exportManifestCsv', () => { it('returns a CSV string with header + one row per attendee', async () => { const program = await createTestProgram({ name: `csv-${uid()}` }) programIds.push(program.id) const { caller } = await newAdminCaller() const event = await prisma.lunchEvent.create({ data: { programId: program.id, enabled: true }, }) const dish = await prisma.dish.create({ data: { lunchEventId: event.id, name: 'Risotto', dietaryTags: [] }, }) await prisma.externalAttendee.create({ data: { lunchEventId: event.id, name: 'X Y', dishId: dish.id, allergens: ['GLUTEN'], }, }) const csv = await caller.exportManifestCsv({ programId: program.id }) expect(csv.split('\n')[0]).toBe('Type,Team,Name,Email,Dish,Allergens,Allergen notes') expect(csv).toContain('External,,X Y,,Risotto,GLUTEN,') }) }) describe('lunch.updateEvent', () => { it('patches an arbitrary subset of fields', async () => { const program = await createTestProgram({ name: `lunch-upd-${uid()}` }) programIds.push(program.id) const { caller } = await newAdminCaller() await caller.getEvent({ programId: program.id }) const updated = await caller.updateEvent({ programId: program.id, enabled: true, eventAt: new Date('2026-06-28T12:30:00Z'), venue: 'Hôtel Hermitage', changeCutoffHours: 24, extraRecipients: ['caterer@example.com'], }) expect(updated.enabled).toBe(true) expect(updated.venue).toBe('Hôtel Hermitage') expect(updated.changeCutoffHours).toBe(24) expect(updated.extraRecipients).toEqual(['caterer@example.com']) }) it('lazy-creates the event on first updateEvent', async () => { const program = await createTestProgram({ name: `lunch-lazy-${uid()}` }) programIds.push(program.id) const { caller } = await newAdminCaller() const updated = await caller.updateEvent({ programId: program.id, enabled: true, }) expect(updated.enabled).toBe(true) expect(updated.programId).toBe(program.id) }) it('rejects non-admin callers', async () => { const program = await createTestProgram({ name: `lunch-rej-${uid()}` }) programIds.push(program.id) const member = await createTestUser('APPLICANT') userIds.push(member.id) const memberCaller = createCaller(lunchRouter, { id: member.id, email: member.email, role: 'APPLICANT', }) await expect( memberCaller.updateEvent({ programId: program.id, enabled: true }), ).rejects.toThrow() }) })