Files
seminarhof-e2e-mqock9p6/packages/functions/src/functions/get-booking-detail.function.ts
2026-06-22 00:17:20 +02:00

217 lines
6.1 KiB
TypeScript

import { z } from 'zod'
import { ForbiddenError } from '@pikku/core/errors'
import { pikkuFunc } from '#pikku'
import {
BookingZ,
ClientZ,
ParticipantZ,
ParticipantAllergyZ,
InvoiceZ,
} from '#pikku/db/zod.gen.js'
export const GetBookingDetailInput = z.object({
bookingId: z.string(),
})
const ParticipantRow = ParticipantZ
.pick({
participantId: true,
name: true,
email: true,
age: true,
kind: true,
dietaryTag: true,
notes: true,
})
.extend({
roomId: z.string().nullable(),
roomNumber: z.number().nullable(),
allergies: z.array(ParticipantAllergyZ.pick({ allergen: true, severity: true, note: true })),
})
const InvoiceRow = InvoiceZ.pick({
invoiceId: true,
invoiceNumber: true,
kind: true,
amountCents: true,
status: true,
pdfUrl: true,
}).extend({
issuedOn: z.date(),
dueOn: z.date().nullable(),
paidOn: z.date().nullable(),
})
export const GetBookingDetailOutput = z.object({
booking: BookingZ.pick({
bookingId: true,
eventName: true,
status: true,
halfHouse: true,
expectedPersons: true,
eventOutline: true,
description: true,
mealTimeBreakfast: true,
mealTimeLunch: true,
mealTimeDinner: true,
arrivalTime: true,
departureTime: true,
dailyPlan: true,
notes: true,
}).extend({
startDate: z.date().nullable(),
endDate: z.date().nullable(),
}),
client: ClientZ.pick({ clientId: true, name: true }),
participants: z.array(ParticipantRow),
invoices: z.array(InvoiceRow),
dietaryBreakdown: z.array(z.object({
tag: z.string(),
count: z.number(),
})),
})
export const getBookingDetail = pikkuFunc({
expose: true,
description: 'Booking detail — booking + org + participants + invoices + dietary breakdown.',
input: GetBookingDetailInput,
output: GetBookingDetailOutput,
func: async ({ kysely, content }, { bookingId }, { session }) => {
const booking = await kysely
.selectFrom('booking')
.where('bookingId', '=', bookingId)
.selectAll()
.executeTakeFirstOrThrow()
if (session.role !== 'admin' && session.role !== 'owner') {
const membership = await kysely
.selectFrom('clientMember')
.where('userId', '=', session.userId)
.where('clientId', '=', booking.clientId)
.select('userId')
.executeTakeFirst()
if (!membership) {
throw new ForbiddenError()
}
}
const client = await kysely
.selectFrom('client')
.where('clientId', '=', booking.clientId)
.select(['clientId', 'name'])
.executeTakeFirstOrThrow()
const participants = await kysely
.selectFrom('participant')
.leftJoin('roomAssignment', 'roomAssignment.participantId', 'participant.participantId')
.leftJoin('room', 'room.roomId', 'roomAssignment.roomId')
.where('participant.bookingId', '=', bookingId)
.select([
'participant.participantId',
'participant.name',
'participant.email',
'participant.age',
'participant.kind',
'participant.dietaryTag',
'participant.notes',
'roomAssignment.roomId as roomId',
'room.number as roomNumber',
])
.orderBy('participant.name', 'asc')
.execute()
const participantIds = participants.map((p) => p.participantId)
const allergies = participantIds.length
? await kysely
.selectFrom('participantAllergy')
.where('participantId', 'in', participantIds)
.selectAll()
.execute()
: []
const allergiesByParticipant = new Map<string, typeof allergies>()
for (const a of allergies) {
const list = allergiesByParticipant.get(a.participantId) ?? []
list.push(a)
allergiesByParticipant.set(a.participantId, list)
}
const dietaryRows = await kysely
.selectFrom('participant')
.where('bookingId', '=', bookingId)
.select(({ fn }) => ['dietaryTag', fn.countAll<number>().as('count')])
.groupBy('dietaryTag')
.execute()
const dietaryBreakdown = dietaryRows.map((r) => ({
tag: r.dietaryTag ?? 'unknown',
count: Number(r.count),
}))
const invoices = await kysely
.selectFrom('invoice')
.where('bookingId', '=', bookingId)
.orderBy('issuedOn', 'desc')
.selectAll()
.execute()
const signedInvoices = await Promise.all(
invoices.map(async (i) => ({
invoiceId: i.invoiceId,
invoiceNumber: i.invoiceNumber,
kind: i.kind,
amountCents: i.amountCents,
status: i.status,
issuedOn: i.issuedOn,
dueOn: i.dueOn,
paidOn: i.paidOn,
pdfUrl: i.pdfUrl && content
? await content.signContentKey({
bucket: 'invoices',
contentKey: i.pdfUrl,
dateLessThan: new Date(Date.now() + 60 * 60 * 1000),
})
: null,
})),
)
return {
booking: {
bookingId: booking.bookingId,
eventName: booking.eventName,
startDate: booking.startDate ? new Date(booking.startDate) : null,
endDate: booking.endDate ? new Date(booking.endDate) : null,
status: booking.status,
halfHouse: booking.halfHouse,
expectedPersons: booking.expectedPersons,
eventOutline: booking.eventOutline,
description: booking.description,
mealTimeBreakfast: booking.mealTimeBreakfast,
mealTimeLunch: booking.mealTimeLunch,
mealTimeDinner: booking.mealTimeDinner,
arrivalTime: booking.arrivalTime,
departureTime: booking.departureTime,
dailyPlan: booking.dailyPlan,
notes: booking.notes,
},
client,
participants: participants.map((p) => ({
participantId: p.participantId,
name: p.name,
email: p.email,
age: p.age,
kind: p.kind,
dietaryTag: p.dietaryTag,
notes: p.notes,
roomId: p.roomId,
roomNumber: p.roomNumber,
allergies: (allergiesByParticipant.get(p.participantId) ?? []).map((a) => ({
allergen: a.allergen,
severity: a.severity,
note: a.note,
})),
})),
invoices: signedInvoices,
dietaryBreakdown,
}
},
})