feat(bookings): implement booking system and confirmation flow

Add database tables and repository for managing bookings
Create API endpoints for booking submission and capacity management
Update landing page to persist bookings before WhatsApp redirection
This commit is contained in:
2026-04-12 21:43:30 +08:00
parent 07e5d42005
commit 8541c4a2d1
17 changed files with 1585 additions and 92 deletions

View File

@@ -0,0 +1,21 @@
import { requireAuth } from '../utils/auth'
import { getBookingCapacitySettings, getBookingInventorySummary, listBookings } from '../utils/booking-repository'
export default defineEventHandler(async (event) => {
const auth = await requireAuth(event)
const [bookings, settings, summary] = await Promise.all([
listBookings(
auth.user.role === 'super_admin'
? undefined
: { personInChargeId: auth.user.id }
),
getBookingCapacitySettings(),
getBookingInventorySummary()
])
return {
bookings,
settings,
summary
}
})

View File

@@ -0,0 +1,26 @@
import { requireRole } from '../../utils/auth'
import { parseBookingCapacityInput } from '../../utils/bookings'
import { getBookingInventorySummary, updateBookingCapacitySettings } from '../../utils/booking-repository'
import { assertBadRequest } from '../../utils/http'
export default defineEventHandler(async (event) => {
await requireRole(event, 'super_admin')
const body = await readBody<{
totalTables?: number | string | null
}>(event)
const input = parseBookingCapacityInput(body)
const summary = await getBookingInventorySummary()
assertBadRequest(
input.totalTables === null || (input.totalTables * 10) >= summary.soldCapacitySeats,
`Total tables cannot be lower than the currently sold capacity of ${summary.soldTables} tables and ${summary.soldSeats} seats`
)
const settings = await updateBookingCapacitySettings(input)
return {
settings
}
})

View File

@@ -0,0 +1,56 @@
import type { BookingMode, CreateBookingResponse, TicketType } from '~~/shared/booking'
import { getTicketCatalogItem, getSeatCount } from '~~/shared/booking'
import { buildAppUrl } from '../../utils/app-url'
import { createBooking } from '../../utils/booking-repository'
import { buildBookingMessage, parseCreateBookingInput } from '../../utils/bookings'
import { assertBadRequest } from '../../utils/http'
import { getPublicContactById } from '../../utils/user-repository'
export default defineEventHandler(async (event): Promise<CreateBookingResponse> => {
const body = await readBody<{
customerName?: string
customerPhone?: string
bookingMode?: BookingMode
quantity?: number
ticketType?: TicketType
personInChargeId?: string
}>(event)
const input = parseCreateBookingInput(body)
const personInCharge = await getPublicContactById(input.personInChargeId)
assertBadRequest(personInCharge, 'Selected person in charge is not available')
const ticket = getTicketCatalogItem(input.ticketType)
assertBadRequest(ticket, 'Ticket category is invalid')
const seatCount = getSeatCount(input.bookingMode, input.quantity)
const totalPrice = seatCount * ticket.price
const { booking, confirmationToken } = await createBooking({
customerName: input.customerName,
customerPhone: input.customerPhone,
bookingMode: input.bookingMode,
quantity: input.quantity,
seatCount,
ticketType: input.ticketType,
unitPrice: ticket.price,
totalPrice,
personInChargeId: personInCharge.id,
personInChargeName: personInCharge.fullName,
personInChargePhoneNumber: personInCharge.phoneNumber
})
const confirmationUrl = buildAppUrl(event, `/confirmation/${confirmationToken}`)
const whatsappMessage = buildBookingMessage(booking, confirmationUrl)
const whatsappUrl = `https://wa.me/${booking.personInChargePhoneNumber}?text=${encodeURIComponent(whatsappMessage)}`
return {
booking,
confirmationUrl,
whatsappUrl
}
})

View File

@@ -0,0 +1,15 @@
import { getRequiredRouteParam, httpError } from '../../../utils/http'
import { getBookingByConfirmationToken } from '../../../utils/booking-repository'
export default defineEventHandler(async (event) => {
const token = getRequiredRouteParam(event, 'token', 'Confirmation token')
const booking = await getBookingByConfirmationToken(token)
if (!booking) {
httpError(404, 'Booking not found')
}
return {
booking
}
})

View File

@@ -0,0 +1,35 @@
import { confirmBookingByConfirmationToken, getBookingByConfirmationToken, getBookingInventorySummary } from '../../../../utils/booking-repository'
import { getRequiredRouteParam, httpError } from '../../../../utils/http'
export default defineEventHandler(async (event) => {
const token = getRequiredRouteParam(event, 'token', 'Confirmation token')
const existingBooking = await getBookingByConfirmationToken(token)
if (!existingBooking) {
httpError(404, 'Booking not found')
}
if (existingBooking.status === 'confirmed') {
return {
booking: existingBooking,
alreadyConfirmed: true
}
}
const summary = await getBookingInventorySummary()
if (summary.leftCapacitySeats !== null && existingBooking.seatCount > summary.leftCapacitySeats) {
httpError(409, 'Not enough capacity left to confirm this booking')
}
const booking = await confirmBookingByConfirmationToken(token)
if (!booking) {
httpError(404, 'Booking not found')
}
return {
booking,
alreadyConfirmed: false
}
})