feat: lunch.getEvent + lunch.updateEvent procedures
Lazy-creates LunchEvent on first read or update. Audit-logs every update with the patched fields. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,7 @@ import { resultLockRouter } from './resultLock'
|
||||
// Grand-finale logistics
|
||||
import { finalistRouter } from './finalist'
|
||||
import { logisticsRouter } from './logistics'
|
||||
import { lunchRouter } from './lunch'
|
||||
|
||||
/**
|
||||
* Root tRPC router that combines all domain routers
|
||||
@@ -110,6 +111,7 @@ export const appRouter = router({
|
||||
// Grand-finale logistics
|
||||
finalist: finalistRouter,
|
||||
logistics: logisticsRouter,
|
||||
lunch: lunchRouter,
|
||||
})
|
||||
|
||||
export type AppRouter = typeof appRouter
|
||||
|
||||
81
src/server/routers/lunch.ts
Normal file
81
src/server/routers/lunch.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { z } from 'zod'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { router, adminProcedure, protectedProcedure } from '../trpc'
|
||||
import { logAudit } from '../utils/audit'
|
||||
|
||||
// ─── Shared zod schemas ──────────────────────────────────────────────────────
|
||||
|
||||
const dietaryTags = z.array(
|
||||
z.enum(['VEGETARIAN', 'VEGAN', 'GLUTEN_FREE', 'PESCATARIAN']),
|
||||
)
|
||||
|
||||
const allergens = z.array(
|
||||
z.enum([
|
||||
'GLUTEN', 'CRUSTACEANS', 'EGGS', 'FISH', 'PEANUTS', 'SOYBEANS', 'MILK',
|
||||
'TREE_NUTS', 'CELERY', 'MUSTARD', 'SESAME', 'SULPHITES', 'LUPIN', 'MOLLUSCS',
|
||||
]),
|
||||
)
|
||||
|
||||
// ─── Router ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export const lunchRouter = router({
|
||||
/**
|
||||
* Get-or-create the LunchEvent for a program. Lazy creation mirrors
|
||||
* the hotel pattern: callers don't have to know whether the row
|
||||
* already exists.
|
||||
*/
|
||||
getEvent: adminProcedure
|
||||
.input(z.object({ programId: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const existing = await ctx.prisma.lunchEvent.findUnique({
|
||||
where: { programId: input.programId },
|
||||
})
|
||||
if (existing) return existing
|
||||
return ctx.prisma.lunchEvent.create({ data: { programId: input.programId } })
|
||||
}),
|
||||
|
||||
/** Patch any subset of LunchEvent config fields. Audit-logged. */
|
||||
updateEvent: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
programId: z.string(),
|
||||
enabled: z.boolean().optional(),
|
||||
eventAt: z.date().nullable().optional(),
|
||||
endAt: z.date().nullable().optional(),
|
||||
venue: z.string().nullable().optional(),
|
||||
notes: z.string().nullable().optional(),
|
||||
changeCutoffHours: z.number().int().min(0).max(720).optional(),
|
||||
reminderHoursBeforeDeadline: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(720)
|
||||
.nullable()
|
||||
.optional(),
|
||||
cronEnabled: z.boolean().optional(),
|
||||
extraRecipients: z.array(z.string().email()).optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { programId, ...patch } = input
|
||||
// Lazy-create before patching so updateEvent doubles as "create + update"
|
||||
await ctx.prisma.lunchEvent.upsert({
|
||||
where: { programId },
|
||||
create: { programId },
|
||||
update: {},
|
||||
})
|
||||
const updated = await ctx.prisma.lunchEvent.update({
|
||||
where: { programId },
|
||||
data: patch,
|
||||
})
|
||||
await logAudit({
|
||||
prisma: ctx.prisma,
|
||||
userId: ctx.user.id,
|
||||
action: 'LUNCH_EVENT_UPDATED',
|
||||
entityType: 'LunchEvent',
|
||||
entityId: updated.id,
|
||||
detailsJson: patch as Record<string, unknown>,
|
||||
})
|
||||
return updated
|
||||
}),
|
||||
})
|
||||
Reference in New Issue
Block a user