/** * Task 2: Multi-hotel + rooming assignment procedures * Tests: listHotels, createHotel, updateHotel, deleteHotel, listRooming, * assignStay, assignTeamToHotel, unassignStay */ import { afterAll, describe, expect, it } from 'vitest' import { prisma, createCaller } from '../setup' import { createTestUser, createTestProgram, createTestProject, cleanupTestData, uid, } from '../helpers' import { logisticsRouter } from '../../src/server/routers/logistics' // --------------------------------------------------------------------------- // Helper: build a CONFIRMED FinalistConfirmation with N attendees // --------------------------------------------------------------------------- async function setupConfirmedTeam( programId: string, projectTitle: string, memberCount: number, ) { const project = await createTestProject(programId, { title: projectTitle, competitionCategory: 'STARTUP', }) const users = await Promise.all( Array.from({ length: memberCount }, (_, i) => prisma.user.create({ data: { id: uid('user'), email: `member${i}_${uid()}@test.local`, name: `Member ${i} of ${projectTitle}`, role: 'APPLICANT', roles: ['APPLICANT'], status: 'ACTIVE', }, }), ), ) const confirmation = await prisma.finalistConfirmation.create({ data: { projectId: project.id, category: 'STARTUP', status: 'CONFIRMED', deadline: new Date(Date.now() + 86400000), token: `tok_${uid()}`, confirmedAt: new Date(), }, }) const attendees = await Promise.all( users.map((u) => prisma.attendingMember.create({ data: { confirmationId: confirmation.id, userId: u.id, needsVisa: false }, }), ), ) return { project, users, confirmation, attendees } } // --------------------------------------------------------------------------- // Suite // --------------------------------------------------------------------------- describe('logistics hotel CRUD + rooming assignment', () => { const programIds: string[] = [] const userIds: string[] = [] afterAll(async () => { for (const programId of programIds) { // Clean in dependency order await prisma.hotelStay.deleteMany({ where: { attendingMember: { confirmation: { project: { programId } } } }, }) await prisma.attendingMember.deleteMany({ where: { confirmation: { project: { programId } } }, }) await prisma.finalistConfirmation.deleteMany({ where: { project: { programId } } }) await prisma.hotel.deleteMany({ where: { programId } }) await cleanupTestData(programId, []) } if (userIds.length > 0) { await prisma.user.deleteMany({ where: { id: { in: userIds } } }) } }) // ── listHotels ──────────────────────────────────────────────────────────── it('listHotels returns all hotels for a program with stay counts', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const program = await createTestProgram({ name: `hotels-list-${uid()}` }) programIds.push(program.id) // Create 2 hotels directly await prisma.hotel.createMany({ data: [ { programId: program.id, name: 'Hotel Alpha', address: '1 Alpha St' }, { programId: program.id, name: 'Hotel Beta' }, ], }) const caller = createCaller(logisticsRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) const hotels = await caller.listHotels({ programId: program.id }) expect(hotels).toHaveLength(2) // Sorted by name ascending expect(hotels[0].name).toBe('Hotel Alpha') expect(hotels[1].name).toBe('Hotel Beta') // _count.stays present expect(hotels[0]._count.stays).toBe(0) }) // ── createHotel ─────────────────────────────────────────────────────────── it('createHotel creates a hotel and normalizes empty link to null', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const program = await createTestProgram({ name: `hotels-create-${uid()}` }) programIds.push(program.id) const caller = createCaller(logisticsRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) const hotel = await caller.createHotel({ programId: program.id, name: 'Hotel Hermitage', address: 'Square Beaumarchais, Monaco', link: '', notes: 'Near the venue', }) expect(hotel.name).toBe('Hotel Hermitage') expect(hotel.programId).toBe(program.id) expect(hotel.link).toBeNull() expect(hotel.notes).toBe('Near the venue') // A second hotel can be created for the same program const hotel2 = await caller.createHotel({ programId: program.id, name: 'Hotel Fairmont', link: 'https://fairmont.com', }) expect(hotel2.name).toBe('Hotel Fairmont') expect(hotel2.link).toBe('https://fairmont.com') const count = await prisma.hotel.count({ where: { programId: program.id } }) expect(count).toBe(2) }) // ── updateHotel ─────────────────────────────────────────────────────────── it('updateHotel changes the hotel fields', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const program = await createTestProgram({ name: `hotels-update-${uid()}` }) programIds.push(program.id) const created = await prisma.hotel.create({ data: { programId: program.id, name: 'Old Name', address: 'Old Address' }, }) const caller = createCaller(logisticsRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) const updated = await caller.updateHotel({ id: created.id, name: 'New Name', address: 'New Address', notes: 'Updated notes', }) expect(updated.id).toBe(created.id) expect(updated.name).toBe('New Name') expect(updated.notes).toBe('Updated notes') }) // ── deleteHotel ─────────────────────────────────────────────────────────── it('deleteHotel rejects when hotel has occupants', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const program = await createTestProgram({ name: `hotels-del-occupied-${uid()}` }) programIds.push(program.id) const hotel = await prisma.hotel.create({ data: { programId: program.id, name: 'Occupied Hotel' }, }) const { attendees } = await setupConfirmedTeam(program.id, `OccupiedTeam-${uid()}`, 1) // Assign the stay await prisma.hotelStay.create({ data: { attendingMemberId: attendees[0].id, hotelId: hotel.id }, }) const caller = createCaller(logisticsRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) await expect(caller.deleteHotel({ id: hotel.id })).rejects.toMatchObject({ code: 'BAD_REQUEST', message: expect.stringContaining('Reassign'), }) }) it('deleteHotel succeeds when hotel has no occupants', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const program = await createTestProgram({ name: `hotels-del-empty-${uid()}` }) programIds.push(program.id) const hotel = await prisma.hotel.create({ data: { programId: program.id, name: 'Empty Hotel' }, }) const caller = createCaller(logisticsRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) const result = await caller.deleteHotel({ id: hotel.id }) expect(result.success).toBe(true) const found = await prisma.hotel.findUnique({ where: { id: hotel.id } }) expect(found).toBeNull() }) // ── assignStay (upsert: create → update) ───────────────────────────────── it('assignStay upserts: creates then updates room number', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const program = await createTestProgram({ name: `hotels-assign-${uid()}` }) programIds.push(program.id) const hotel = await prisma.hotel.create({ data: { programId: program.id, name: 'Stay Hotel' }, }) const { attendees } = await setupConfirmedTeam(program.id, `StayTeam-${uid()}`, 1) const caller = createCaller(logisticsRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) // Create const stay1 = await caller.assignStay({ attendingMemberId: attendees[0].id, hotelId: hotel.id, roomNumber: '101', }) expect(stay1.hotelId).toBe(hotel.id) expect(stay1.roomNumber).toBe('101') // Update (upsert same attendee → same row) const stay2 = await caller.assignStay({ attendingMemberId: attendees[0].id, hotelId: hotel.id, roomNumber: '202', }) expect(stay2.id).toBe(stay1.id) expect(stay2.roomNumber).toBe('202') // Exactly 1 row const count = await prisma.hotelStay.count({ where: { attendingMemberId: attendees[0].id }, }) expect(count).toBe(1) }) it('assignStay rejects when hotel program does not match attendee program', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const program1 = await createTestProgram({ name: `hotels-mismatch-p1-${uid()}` }) const program2 = await createTestProgram({ name: `hotels-mismatch-p2-${uid()}` }) programIds.push(program1.id, program2.id) const hotelInP2 = await prisma.hotel.create({ data: { programId: program2.id, name: 'Wrong Hotel' }, }) const { attendees } = await setupConfirmedTeam(program1.id, `MismatchTeam-${uid()}`, 1) const caller = createCaller(logisticsRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) await expect( caller.assignStay({ attendingMemberId: attendees[0].id, hotelId: hotelInP2.id }), ).rejects.toMatchObject({ code: 'BAD_REQUEST' }) }) // ── assignTeamToHotel ───────────────────────────────────────────────────── it('assignTeamToHotel assigns all attendees of a 2-person team', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const program = await createTestProgram({ name: `hotels-team-assign-${uid()}` }) programIds.push(program.id) const hotel = await prisma.hotel.create({ data: { programId: program.id, name: 'Team Hotel' }, }) const { confirmation, attendees } = await setupConfirmedTeam( program.id, `TeamAssign-${uid()}`, 2, ) const caller = createCaller(logisticsRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) await caller.assignTeamToHotel({ confirmationId: confirmation.id, hotelId: hotel.id, checkInAt: new Date('2026-06-28T14:00:00Z'), checkOutAt: new Date('2026-07-01T12:00:00Z'), }) const stays = await prisma.hotelStay.findMany({ where: { attendingMember: { confirmationId: confirmation.id } }, }) expect(stays).toHaveLength(2) for (const s of stays) { expect(s.hotelId).toBe(hotel.id) expect(s.checkInAt?.toISOString()).toBe('2026-06-28T14:00:00.000Z') } // roomNumber not set (null by default on create) expect(stays[0].roomNumber).toBeNull() // Call again (update) — preserve existing roomNumber (here null → still null) await caller.assignTeamToHotel({ confirmationId: confirmation.id, hotelId: hotel.id, }) const stays2 = await prisma.hotelStay.findMany({ where: { attendingMember: { confirmationId: confirmation.id } }, }) expect(stays2).toHaveLength(2) // Set individual room, then re-run assignTeamToHotel → roomNumber preserved await caller.assignStay({ attendingMemberId: attendees[0].id, hotelId: hotel.id, roomNumber: '301', }) await caller.assignTeamToHotel({ confirmationId: confirmation.id, hotelId: hotel.id }) const preserved = await prisma.hotelStay.findUnique({ where: { attendingMemberId: attendees[0].id }, }) expect(preserved?.roomNumber).toBe('301') }) // ── unassignStay ────────────────────────────────────────────────────────── it('unassignStay removes the stay (and is a no-op when none exists)', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const program = await createTestProgram({ name: `hotels-unassign-${uid()}` }) programIds.push(program.id) const hotel = await prisma.hotel.create({ data: { programId: program.id, name: 'Unassign Hotel' }, }) const { attendees } = await setupConfirmedTeam(program.id, `UnassignTeam-${uid()}`, 1) const caller = createCaller(logisticsRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) await caller.assignStay({ attendingMemberId: attendees[0].id, hotelId: hotel.id }) await caller.unassignStay({ attendingMemberId: attendees[0].id }) const count = await prisma.hotelStay.count({ where: { attendingMemberId: attendees[0].id }, }) expect(count).toBe(0) // No-op: calling again doesn't throw await expect( caller.unassignStay({ attendingMemberId: attendees[0].id }), ).resolves.not.toThrow() }) // ── listRooming ─────────────────────────────────────────────────────────── it('listRooming returns CONFIRMED attendees with and without stays', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const program = await createTestProgram({ name: `hotels-rooming-${uid()}` }) programIds.push(program.id) const hotel = await prisma.hotel.create({ data: { programId: program.id, name: 'Rooming Hotel' }, }) // One 2-person confirmed team const { attendees, project } = await setupConfirmedTeam( program.id, `RoomingTeam-${uid()}`, 2, ) // Assign only attendee[0] to a hotel await prisma.hotelStay.create({ data: { attendingMemberId: attendees[0].id, hotelId: hotel.id, roomNumber: '42' }, }) // Also create a PENDING confirmation (should be excluded) const pendingProject = await createTestProject(program.id, { title: `Pending-${uid()}`, competitionCategory: 'STARTUP', }) const pendingUser = await prisma.user.create({ data: { id: uid('user'), email: `pending_${uid()}@test.local`, name: 'Pending User', role: 'APPLICANT', roles: ['APPLICANT'], status: 'ACTIVE', }, }) userIds.push(pendingUser.id) const pendingConf = await prisma.finalistConfirmation.create({ data: { projectId: pendingProject.id, category: 'STARTUP', status: 'PENDING', deadline: new Date(Date.now() + 86400000), token: `tok_${uid()}`, }, }) await prisma.attendingMember.create({ data: { confirmationId: pendingConf.id, userId: pendingUser.id }, }) const caller = createCaller(logisticsRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) const rows = await caller.listRooming({ programId: program.id }) type RoomingRow = (typeof rows)[number] // Only CONFIRMED (2 rows from the one confirmed team) expect(rows).toHaveLength(2) // All rows belong to the confirmed team's project expect(rows.every((r: RoomingRow) => r.projectId === project.id)).toBe(true) // Attendee with stay has non-null stay const withStay = rows.find((r: RoomingRow) => r.attendingMemberId === attendees[0].id) expect(withStay?.stay).not.toBeNull() expect(withStay?.stay?.roomNumber).toBe('42') // Attendee without stay has null stay const withoutStay = rows.find((r: RoomingRow) => r.attendingMemberId === attendees[1].id) expect(withoutStay?.stay).toBeNull() }) })