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:
21
server/api/bookings.get.ts
Normal file
21
server/api/bookings.get.ts
Normal 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
|
||||
}
|
||||
})
|
||||
26
server/api/bookings/capacity.patch.ts
Normal file
26
server/api/bookings/capacity.patch.ts
Normal 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
|
||||
}
|
||||
})
|
||||
56
server/api/public/bookings.post.ts
Normal file
56
server/api/public/bookings.post.ts
Normal 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
|
||||
}
|
||||
})
|
||||
15
server/api/public/bookings/[token].get.ts
Normal file
15
server/api/public/bookings/[token].get.ts
Normal 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
|
||||
}
|
||||
})
|
||||
35
server/api/public/bookings/[token]/confirm.post.ts
Normal file
35
server/api/public/bookings/[token]/confirm.post.ts
Normal 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
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user