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:
150
shared/booking.ts
Normal file
150
shared/booking.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
export type BookingMode = 'table' | 'pax'
|
||||
export type TicketType = 'vip' | 'supporter'
|
||||
export type BookingStatus = 'pending' | 'confirmed'
|
||||
|
||||
export const BOOKING_MODE_OPTIONS = [
|
||||
{
|
||||
value: 'table',
|
||||
label: 'Table (10 pax)'
|
||||
},
|
||||
{
|
||||
value: 'pax',
|
||||
label: 'Person'
|
||||
}
|
||||
] satisfies Array<{ value: BookingMode, label: string }>
|
||||
|
||||
export const BOOKING_TICKET_CATALOG = [
|
||||
{
|
||||
value: 'vip',
|
||||
label: 'VIP',
|
||||
description: 'RM150 / pax',
|
||||
price: 150
|
||||
},
|
||||
{
|
||||
value: 'supporter',
|
||||
label: 'Supporter',
|
||||
description: 'RM60 / pax',
|
||||
price: 60
|
||||
}
|
||||
] satisfies Array<{ value: TicketType, label: string, description: string, price: number }>
|
||||
|
||||
export interface PublicBooking {
|
||||
id: string
|
||||
confirmationToken: string
|
||||
customerName: string
|
||||
customerPhone: string
|
||||
bookingMode: BookingMode
|
||||
quantity: number
|
||||
seatCount: number
|
||||
ticketType: TicketType
|
||||
unitPrice: number
|
||||
totalPrice: number
|
||||
personInChargeId: string
|
||||
personInChargeName: string
|
||||
personInChargePhoneNumber: string
|
||||
status: BookingStatus
|
||||
createdAt: string
|
||||
confirmedAt: string | null
|
||||
}
|
||||
|
||||
export interface BookingCapacitySettings {
|
||||
totalTables: number | null
|
||||
updatedAt: string | null
|
||||
}
|
||||
|
||||
export interface BookingInventorySummary {
|
||||
totalTables: number | null
|
||||
totalCapacitySeats: number | null
|
||||
soldTables: number
|
||||
pendingTables: number
|
||||
soldSeats: number
|
||||
pendingSeats: number
|
||||
soldCapacitySeats: number
|
||||
pendingCapacitySeats: number
|
||||
leftTables: number | null
|
||||
leftSeats: number | null
|
||||
leftCapacitySeats: number | null
|
||||
}
|
||||
|
||||
export interface CreateBookingResponse {
|
||||
booking: PublicBooking
|
||||
confirmationUrl: string
|
||||
whatsappUrl: string
|
||||
}
|
||||
|
||||
export function isBookingMode(value: string | null | undefined): value is BookingMode {
|
||||
return value === 'table' || value === 'pax'
|
||||
}
|
||||
|
||||
export function isTicketType(value: string | null | undefined): value is TicketType {
|
||||
return value === 'vip' || value === 'supporter'
|
||||
}
|
||||
|
||||
export function isBookingStatus(value: string | null | undefined): value is BookingStatus {
|
||||
return value === 'pending' || value === 'confirmed'
|
||||
}
|
||||
|
||||
export function getBookingModeLabel(value: BookingMode) {
|
||||
return value === 'table' ? 'Table (10 pax each)' : 'Per person'
|
||||
}
|
||||
|
||||
export function getBookingStatusLabel(value: BookingStatus) {
|
||||
return value === 'confirmed' ? 'Confirmed' : 'Pending PIC confirmation'
|
||||
}
|
||||
|
||||
export function getSeatCount(bookingMode: BookingMode, quantity: number) {
|
||||
return bookingMode === 'table' ? quantity * 10 : quantity
|
||||
}
|
||||
|
||||
export function getTicketCatalogItem(ticketType: TicketType) {
|
||||
return BOOKING_TICKET_CATALOG.find((ticket) => ticket.value === ticketType) ?? null
|
||||
}
|
||||
|
||||
export function formatBookingCurrency(value: number) {
|
||||
return new Intl.NumberFormat('en-MY', {
|
||||
style: 'currency',
|
||||
currency: 'MYR',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0
|
||||
}).format(value)
|
||||
}
|
||||
|
||||
export function calculateBookingInventorySummary(
|
||||
bookings: Pick<PublicBooking, 'bookingMode' | 'quantity' | 'seatCount' | 'status'>[],
|
||||
settings: BookingCapacitySettings
|
||||
): BookingInventorySummary {
|
||||
const soldTables = bookings
|
||||
.filter((booking) => booking.status === 'confirmed' && booking.bookingMode === 'table')
|
||||
.reduce((total, booking) => total + booking.quantity, 0)
|
||||
|
||||
const pendingTables = bookings
|
||||
.filter((booking) => booking.status === 'pending' && booking.bookingMode === 'table')
|
||||
.reduce((total, booking) => total + booking.quantity, 0)
|
||||
|
||||
const soldSeats = bookings
|
||||
.filter((booking) => booking.status === 'confirmed' && booking.bookingMode === 'pax')
|
||||
.reduce((total, booking) => total + booking.seatCount, 0)
|
||||
|
||||
const pendingSeats = bookings
|
||||
.filter((booking) => booking.status === 'pending' && booking.bookingMode === 'pax')
|
||||
.reduce((total, booking) => total + booking.seatCount, 0)
|
||||
|
||||
const totalCapacitySeats = settings.totalTables === null ? null : settings.totalTables * 10
|
||||
const soldCapacitySeats = (soldTables * 10) + soldSeats
|
||||
const pendingCapacitySeats = (pendingTables * 10) + pendingSeats
|
||||
const leftCapacitySeats = totalCapacitySeats === null ? null : Math.max(totalCapacitySeats - soldCapacitySeats, 0)
|
||||
|
||||
return {
|
||||
totalTables: settings.totalTables,
|
||||
totalCapacitySeats,
|
||||
soldTables,
|
||||
pendingTables,
|
||||
soldSeats,
|
||||
pendingSeats,
|
||||
soldCapacitySeats,
|
||||
pendingCapacitySeats,
|
||||
leftTables: leftCapacitySeats === null ? null : Math.floor(leftCapacitySeats / 10),
|
||||
leftSeats: leftCapacitySeats === null ? null : leftCapacitySeats % 10,
|
||||
leftCapacitySeats
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user