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:
90
server/utils/bookings.ts
Normal file
90
server/utils/bookings.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import type { BookingCapacitySettings, BookingMode, PublicBooking, TicketType } from '~~/shared/booking'
|
||||
|
||||
import {
|
||||
formatBookingCurrency,
|
||||
getBookingModeLabel,
|
||||
getTicketCatalogItem,
|
||||
isBookingMode,
|
||||
isTicketType
|
||||
} from '~~/shared/booking'
|
||||
import { hasValidFullName, isValidPhoneNumber, normalizeFullName, normalizePhoneNumber } from '~~/shared/auth'
|
||||
|
||||
import { assertBadRequest } from './http'
|
||||
|
||||
export function parseCreateBookingInput(body: {
|
||||
customerName?: string
|
||||
customerPhone?: string
|
||||
bookingMode?: BookingMode
|
||||
quantity?: number
|
||||
ticketType?: TicketType
|
||||
personInChargeId?: string
|
||||
}) {
|
||||
const customerName = normalizeFullName(body.customerName || '')
|
||||
const customerPhone = normalizePhoneNumber(body.customerPhone || '')
|
||||
const bookingMode = body.bookingMode
|
||||
const ticketType = body.ticketType
|
||||
const quantity = Number(body.quantity)
|
||||
const personInChargeId = (body.personInChargeId || '').trim()
|
||||
|
||||
assertBadRequest(hasValidFullName(customerName), 'Guest or organizer name must be at least 2 characters')
|
||||
assertBadRequest(isValidPhoneNumber(customerPhone), 'Phone number must contain 8 to 15 digits')
|
||||
assertBadRequest(isBookingMode(bookingMode), 'Booking mode is invalid')
|
||||
assertBadRequest(Number.isInteger(quantity) && quantity >= 1, 'Quantity must be a whole number of at least 1')
|
||||
assertBadRequest(isTicketType(ticketType), 'Ticket category is invalid')
|
||||
assertBadRequest(personInChargeId, 'Person in charge is required')
|
||||
|
||||
return {
|
||||
customerName,
|
||||
customerPhone,
|
||||
bookingMode,
|
||||
quantity,
|
||||
ticketType,
|
||||
personInChargeId
|
||||
}
|
||||
}
|
||||
|
||||
export function buildBookingMessage(booking: PublicBooking, confirmationUrl: string) {
|
||||
const ticket = getTicketCatalogItem(booking.ticketType)
|
||||
const ticketLabel = ticket?.label || booking.ticketType.toUpperCase()
|
||||
|
||||
return [
|
||||
"I'd like to book tickets for the DAP Johor 60th Anniversary Celebration.",
|
||||
'',
|
||||
`Name: ${booking.customerName}`,
|
||||
`Phone Number: ${booking.customerPhone}`,
|
||||
`Booking Mode: ${getBookingModeLabel(booking.bookingMode)}`,
|
||||
`Quantity: ${booking.quantity}`,
|
||||
`Ticket Category: ${ticketLabel}`,
|
||||
`Seats Covered: ${booking.seatCount}`,
|
||||
`Total Price: ${formatBookingCurrency(booking.totalPrice)}`,
|
||||
'',
|
||||
'PIC confirmation link:',
|
||||
confirmationUrl
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
export function parseBookingCapacityInput(body: {
|
||||
totalTables?: number | string | null
|
||||
}): Pick<BookingCapacitySettings, 'totalTables'> {
|
||||
const totalTables = parseOptionalInteger(body.totalTables)
|
||||
|
||||
assertBadRequest(totalTables === null || totalTables >= 0, 'Total tables must be 0 or greater')
|
||||
|
||||
return {
|
||||
totalTables
|
||||
}
|
||||
}
|
||||
|
||||
function parseOptionalInteger(value: number | string | null | undefined) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return null
|
||||
}
|
||||
|
||||
const parsed = typeof value === 'number'
|
||||
? value
|
||||
: Number.parseInt(String(value), 10)
|
||||
|
||||
assertBadRequest(Number.isInteger(parsed), 'Capacity values must be whole numbers')
|
||||
|
||||
return parsed
|
||||
}
|
||||
Reference in New Issue
Block a user