feat: hotel CRUD on logistics router
This commit is contained in:
@@ -51,6 +51,7 @@ import { deliberationRouter } from './deliberation'
|
||||
import { resultLockRouter } from './resultLock'
|
||||
// Grand-finale logistics
|
||||
import { finalistRouter } from './finalist'
|
||||
import { logisticsRouter } from './logistics'
|
||||
|
||||
/**
|
||||
* Root tRPC router that combines all domain routers
|
||||
@@ -108,6 +109,7 @@ export const appRouter = router({
|
||||
resultLock: resultLockRouter,
|
||||
// Grand-finale logistics
|
||||
finalist: finalistRouter,
|
||||
logistics: logisticsRouter,
|
||||
})
|
||||
|
||||
export type AppRouter = typeof appRouter
|
||||
|
||||
56
src/server/routers/logistics.ts
Normal file
56
src/server/routers/logistics.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { z } from 'zod'
|
||||
import { router, adminProcedure } from '../trpc'
|
||||
import { logAudit } from '../utils/audit'
|
||||
|
||||
export const logisticsRouter = router({
|
||||
/** Read the hotel for a program (1:1). Null if not yet set. */
|
||||
getHotel: adminProcedure
|
||||
.input(z.object({ programId: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
return ctx.prisma.hotel.findUnique({ where: { programId: input.programId } })
|
||||
}),
|
||||
|
||||
/** Create or update the program's hotel. Empty link strings are stored as null. */
|
||||
upsertHotel: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
programId: z.string(),
|
||||
name: z.string().min(1).max(200),
|
||||
address: z.string().max(500).optional(),
|
||||
link: z
|
||||
.string()
|
||||
.url()
|
||||
.or(z.literal(''))
|
||||
.optional(),
|
||||
notes: z.string().max(2000).optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const link = input.link && input.link.trim().length > 0 ? input.link : null
|
||||
const hotel = await ctx.prisma.hotel.upsert({
|
||||
where: { programId: input.programId },
|
||||
create: {
|
||||
programId: input.programId,
|
||||
name: input.name,
|
||||
address: input.address ?? null,
|
||||
link,
|
||||
notes: input.notes ?? null,
|
||||
},
|
||||
update: {
|
||||
name: input.name,
|
||||
address: input.address ?? null,
|
||||
link,
|
||||
notes: input.notes ?? null,
|
||||
},
|
||||
})
|
||||
await logAudit({
|
||||
prisma: ctx.prisma,
|
||||
userId: ctx.user.id,
|
||||
action: 'HOTEL_UPSERT',
|
||||
entityType: 'Hotel',
|
||||
entityId: hotel.id,
|
||||
detailsJson: { programId: input.programId, name: input.name },
|
||||
})
|
||||
return hotel
|
||||
}),
|
||||
})
|
||||
99
tests/unit/logistics-hotel.test.ts
Normal file
99
tests/unit/logistics-hotel.test.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { afterAll, describe, expect, it } from 'vitest'
|
||||
import { prisma, createCaller } from '../setup'
|
||||
import { createTestUser, createTestProgram, cleanupTestData, uid } from '../helpers'
|
||||
import { logisticsRouter } from '../../src/server/routers/logistics'
|
||||
|
||||
describe('logistics.getHotel + upsertHotel', () => {
|
||||
const programIds: string[] = []
|
||||
const userIds: string[] = []
|
||||
|
||||
afterAll(async () => {
|
||||
for (const programId of programIds) {
|
||||
await prisma.hotel.deleteMany({ where: { programId } })
|
||||
await cleanupTestData(programId, [])
|
||||
}
|
||||
if (userIds.length > 0) {
|
||||
await prisma.user.deleteMany({ where: { id: { in: userIds } } })
|
||||
}
|
||||
})
|
||||
|
||||
it('getHotel returns null when no hotel set', async () => {
|
||||
const admin = await createTestUser('SUPER_ADMIN')
|
||||
userIds.push(admin.id)
|
||||
const program = await createTestProgram({ name: `hotel-empty-${uid()}` })
|
||||
programIds.push(program.id)
|
||||
const caller = createCaller(logisticsRouter, {
|
||||
id: admin.id,
|
||||
email: admin.email,
|
||||
role: 'SUPER_ADMIN',
|
||||
})
|
||||
const hotel = await caller.getHotel({ programId: program.id })
|
||||
expect(hotel).toBeNull()
|
||||
})
|
||||
|
||||
it('upsertHotel creates a hotel on first call', async () => {
|
||||
const admin = await createTestUser('SUPER_ADMIN')
|
||||
userIds.push(admin.id)
|
||||
const program = await createTestProgram({ name: `hotel-create-${uid()}` })
|
||||
programIds.push(program.id)
|
||||
const caller = createCaller(logisticsRouter, {
|
||||
id: admin.id,
|
||||
email: admin.email,
|
||||
role: 'SUPER_ADMIN',
|
||||
})
|
||||
const hotel = await caller.upsertHotel({
|
||||
programId: program.id,
|
||||
name: 'Hotel Hermitage',
|
||||
address: 'Square Beaumarchais, 98000 Monaco',
|
||||
link: 'https://hotelhermitagemontecarlo.com',
|
||||
notes: 'Adjacent to the venue',
|
||||
})
|
||||
expect(hotel.name).toBe('Hotel Hermitage')
|
||||
expect(hotel.programId).toBe(program.id)
|
||||
expect(hotel.link).toContain('hermitage')
|
||||
})
|
||||
|
||||
it('upsertHotel updates the existing hotel on second call', async () => {
|
||||
const admin = await createTestUser('SUPER_ADMIN')
|
||||
userIds.push(admin.id)
|
||||
const program = await createTestProgram({ name: `hotel-update-${uid()}` })
|
||||
programIds.push(program.id)
|
||||
const caller = createCaller(logisticsRouter, {
|
||||
id: admin.id,
|
||||
email: admin.email,
|
||||
role: 'SUPER_ADMIN',
|
||||
})
|
||||
await caller.upsertHotel({
|
||||
programId: program.id,
|
||||
name: 'Hotel A',
|
||||
})
|
||||
const updated = await caller.upsertHotel({
|
||||
programId: program.id,
|
||||
name: 'Hotel B',
|
||||
notes: 'Changed our mind',
|
||||
})
|
||||
expect(updated.name).toBe('Hotel B')
|
||||
expect(updated.notes).toBe('Changed our mind')
|
||||
// Still 1:1 — no second hotel row
|
||||
const count = await prisma.hotel.count({ where: { programId: program.id } })
|
||||
expect(count).toBe(1)
|
||||
})
|
||||
|
||||
it('upsertHotel normalizes empty link string to null', async () => {
|
||||
const admin = await createTestUser('SUPER_ADMIN')
|
||||
userIds.push(admin.id)
|
||||
const program = await createTestProgram({ name: `hotel-empty-link-${uid()}` })
|
||||
programIds.push(program.id)
|
||||
const caller = createCaller(logisticsRouter, {
|
||||
id: admin.id,
|
||||
email: admin.email,
|
||||
role: 'SUPER_ADMIN',
|
||||
})
|
||||
const hotel = await caller.upsertHotel({
|
||||
programId: program.id,
|
||||
name: 'No-link Hotel',
|
||||
link: '',
|
||||
})
|
||||
expect(hotel.link).toBeNull()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user