refactor(bookings): simplify capacity tracking to use seats
Replace 'pax' booking mode with 'seat' Consolidate inventory summary to track only seat counts Update database schema and UI for seat-centric capacity
This commit is contained in:
@@ -7,15 +7,15 @@ export default defineEventHandler(async (event) => {
|
||||
await requireRole(event, 'super_admin')
|
||||
|
||||
const body = await readBody<{
|
||||
totalTables?: number | string | null
|
||||
totalSeats?: 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`
|
||||
input.totalSeats === null || input.totalSeats >= summary.soldSeats,
|
||||
`Total seats cannot be lower than the currently sold count of ${summary.soldSeats} seats`
|
||||
)
|
||||
|
||||
const settings = await updateBookingCapacitySettings(input)
|
||||
|
||||
@@ -12,7 +12,7 @@ export default defineEventHandler(async (event): Promise<CreateBookingResponse>
|
||||
const body = await readBody<{
|
||||
customerName?: string
|
||||
customerPhone?: string
|
||||
bookingMode?: BookingMode
|
||||
bookingMode?: BookingMode | string | null
|
||||
quantity?: number
|
||||
ticketType?: TicketType
|
||||
personInChargeId?: string
|
||||
|
||||
@@ -18,7 +18,7 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
const summary = await getBookingInventorySummary()
|
||||
|
||||
if (summary.leftCapacitySeats !== null && existingBooking.seatCount > summary.leftCapacitySeats) {
|
||||
if (summary.leftSeats !== null && existingBooking.seatCount > summary.leftSeats) {
|
||||
httpError(409, 'Not enough capacity left to confirm this booking')
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
TicketType
|
||||
} from '~~/shared/booking'
|
||||
|
||||
import { calculateBookingInventorySummary, isBookingStatus } from '~~/shared/booking'
|
||||
import { calculateBookingInventorySummary, isBookingMode, isBookingStatus } from '~~/shared/booking'
|
||||
|
||||
import { randomToken, toIsoString } from './base64url'
|
||||
import { ensureDatabaseReady } from './db-init'
|
||||
@@ -23,7 +23,7 @@ type DbBookingRow = {
|
||||
receipt_token: string
|
||||
customer_name: string
|
||||
customer_phone: string
|
||||
booking_mode: BookingMode
|
||||
booking_mode: string
|
||||
quantity: number | string
|
||||
seat_count: number | string
|
||||
ticket_type: TicketType
|
||||
@@ -54,7 +54,7 @@ type DbBookingSeatWithBookingRow = DbBookingSeatRow & {
|
||||
receipt_token: string
|
||||
customer_name: string
|
||||
customer_phone: string
|
||||
booking_mode: BookingMode
|
||||
booking_mode: string
|
||||
quantity: number | string
|
||||
seat_count: number | string
|
||||
ticket_type: TicketType
|
||||
@@ -67,6 +67,7 @@ type DbBookingSeatWithBookingRow = DbBookingSeatRow & {
|
||||
|
||||
type DbBookingSettingsRow = {
|
||||
total_tables: number | string | null
|
||||
total_seats: number | string | null
|
||||
updated_at: Date | string
|
||||
}
|
||||
|
||||
@@ -74,16 +75,22 @@ function parseInteger(value: number | string) {
|
||||
return typeof value === 'number' ? value : Number.parseInt(value, 10)
|
||||
}
|
||||
|
||||
function normalizeBookingMode(value: string): BookingMode {
|
||||
return isBookingMode(value) ? value : 'seat'
|
||||
}
|
||||
|
||||
function mapBooking(row: DbBookingRow): PublicBooking {
|
||||
const seatCount = parseInteger(row.seat_count)
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
confirmationToken: row.confirmation_token,
|
||||
receiptToken: row.receipt_token,
|
||||
customerName: row.customer_name,
|
||||
customerPhone: row.customer_phone,
|
||||
bookingMode: row.booking_mode,
|
||||
bookingMode: normalizeBookingMode(row.booking_mode),
|
||||
quantity: parseInteger(row.quantity),
|
||||
seatCount: parseInteger(row.seat_count),
|
||||
seatCount,
|
||||
ticketType: row.ticket_type,
|
||||
unitPrice: parseInteger(row.unit_price),
|
||||
totalPrice: parseInteger(row.total_price),
|
||||
@@ -97,14 +104,16 @@ function mapBooking(row: DbBookingRow): PublicBooking {
|
||||
}
|
||||
|
||||
function mapReceiptBooking(row: DbBookingRow | DbBookingSeatWithBookingRow): ReceiptBooking {
|
||||
const seatCount = parseInteger(row.seat_count)
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
receiptToken: row.receipt_token,
|
||||
customerName: row.customer_name,
|
||||
customerPhone: row.customer_phone,
|
||||
bookingMode: row.booking_mode,
|
||||
bookingMode: normalizeBookingMode(row.booking_mode),
|
||||
quantity: parseInteger(row.quantity),
|
||||
seatCount: parseInteger(row.seat_count),
|
||||
seatCount,
|
||||
ticketType: row.ticket_type,
|
||||
unitPrice: parseInteger(row.unit_price),
|
||||
totalPrice: parseInteger(row.total_price),
|
||||
@@ -130,13 +139,17 @@ function mapBookingSeat(row: DbBookingSeatRow): PublicBookingSeat {
|
||||
function mapBookingCapacitySettings(row: DbBookingSettingsRow | undefined): BookingCapacitySettings {
|
||||
if (!row) {
|
||||
return {
|
||||
totalTables: null,
|
||||
totalSeats: null,
|
||||
updatedAt: null
|
||||
}
|
||||
}
|
||||
|
||||
const totalSeats = row.total_seats === null
|
||||
? (row.total_tables === null ? null : parseInteger(row.total_tables) * 10)
|
||||
: parseInteger(row.total_seats)
|
||||
|
||||
return {
|
||||
totalTables: row.total_tables === null ? null : parseInteger(row.total_tables),
|
||||
totalSeats,
|
||||
updatedAt: toIsoString(row.updated_at)
|
||||
}
|
||||
}
|
||||
@@ -521,6 +534,7 @@ export async function getBookingCapacitySettings(): Promise<BookingCapacitySetti
|
||||
const [row] = await sql<DbBookingSettingsRow[]>`
|
||||
select
|
||||
total_tables,
|
||||
total_seats,
|
||||
updated_at
|
||||
from booking_settings
|
||||
where id = 'default'
|
||||
@@ -531,7 +545,7 @@ export async function getBookingCapacitySettings(): Promise<BookingCapacitySetti
|
||||
}
|
||||
|
||||
export async function updateBookingCapacitySettings(input: {
|
||||
totalTables: number | null
|
||||
totalSeats: number | null
|
||||
}): Promise<BookingCapacitySettings> {
|
||||
await ensureDatabaseReady()
|
||||
const sql = getSqlClient()
|
||||
@@ -539,11 +553,12 @@ export async function updateBookingCapacitySettings(input: {
|
||||
const [row] = await sql<DbBookingSettingsRow[]>`
|
||||
update booking_settings
|
||||
set
|
||||
total_tables = ${input.totalTables},
|
||||
total_seats = ${input.totalSeats},
|
||||
updated_at = now()
|
||||
where id = 'default'
|
||||
returning
|
||||
total_tables,
|
||||
total_seats,
|
||||
updated_at
|
||||
`
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { BookingCapacitySettings, BookingMode, PublicBooking, TicketType }
|
||||
|
||||
import {
|
||||
formatBookingCurrency,
|
||||
getBookingModeLabel,
|
||||
getTicketCatalogItem,
|
||||
isBookingMode,
|
||||
isTicketType
|
||||
@@ -14,14 +13,14 @@ import { assertBadRequest } from './http'
|
||||
export function parseCreateBookingInput(body: {
|
||||
customerName?: string
|
||||
customerPhone?: string
|
||||
bookingMode?: BookingMode
|
||||
bookingMode?: BookingMode | string | null
|
||||
quantity?: number
|
||||
ticketType?: TicketType
|
||||
personInChargeId?: string
|
||||
}) {
|
||||
const customerName = normalizeFullName(body.customerName || '')
|
||||
const customerPhone = normalizePhoneNumber(body.customerPhone || '')
|
||||
const bookingMode = body.bookingMode
|
||||
const bookingMode = typeof body.bookingMode === 'string' ? body.bookingMode.trim().toLowerCase() : body.bookingMode
|
||||
const ticketType = body.ticketType
|
||||
const quantity = Number(body.quantity)
|
||||
const personInChargeId = (body.personInChargeId || '').trim()
|
||||
@@ -52,10 +51,8 @@ export function buildBookingMessage(booking: PublicBooking, confirmationUrl: str
|
||||
'',
|
||||
`Name: ${booking.customerName}`,
|
||||
`Phone Number: ${booking.customerPhone}`,
|
||||
`Booking Mode: ${getBookingModeLabel(booking.bookingMode)}`,
|
||||
`Quantity: ${booking.quantity}`,
|
||||
`Seats: ${booking.seatCount}`,
|
||||
`Ticket Category: ${ticketLabel}`,
|
||||
`Seats Covered: ${booking.seatCount}`,
|
||||
`Total Price: ${formatBookingCurrency(booking.totalPrice)}`,
|
||||
'',
|
||||
'PIC confirmation link:',
|
||||
@@ -87,14 +84,14 @@ export function parseSeatShareInput(body: {
|
||||
}
|
||||
|
||||
export function parseBookingCapacityInput(body: {
|
||||
totalTables?: number | string | null
|
||||
}): Pick<BookingCapacitySettings, 'totalTables'> {
|
||||
const totalTables = parseOptionalInteger(body.totalTables)
|
||||
totalSeats?: number | string | null
|
||||
}): Pick<BookingCapacitySettings, 'totalSeats'> {
|
||||
const totalSeats = parseOptionalInteger(body.totalSeats)
|
||||
|
||||
assertBadRequest(totalTables === null || totalTables >= 0, 'Total tables must be 0 or greater')
|
||||
assertBadRequest(totalSeats === null || totalSeats >= 0, 'Total seats must be 0 or greater')
|
||||
|
||||
return {
|
||||
totalTables
|
||||
totalSeats
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ async function initializeDatabase() {
|
||||
receipt_token text not null unique,
|
||||
customer_name text not null,
|
||||
customer_phone text not null,
|
||||
booking_mode text not null check (booking_mode in ('table', 'pax')),
|
||||
booking_mode text not null check (booking_mode in ('table', 'seat')),
|
||||
quantity integer not null check (quantity >= 1),
|
||||
seat_count integer not null check (seat_count >= 1),
|
||||
ticket_type text not null check (ticket_type in ('vip', 'supporter')),
|
||||
@@ -124,6 +124,11 @@ async function initializeDatabase() {
|
||||
)
|
||||
`
|
||||
|
||||
await sql`
|
||||
alter table booking_settings
|
||||
add column if not exists total_seats integer
|
||||
`
|
||||
|
||||
await sql`
|
||||
insert into booking_settings (id)
|
||||
values ('default')
|
||||
@@ -146,6 +151,45 @@ async function initializeDatabase() {
|
||||
`
|
||||
}
|
||||
|
||||
await sql`
|
||||
alter table bookings
|
||||
drop constraint if exists bookings_booking_mode_check
|
||||
`
|
||||
|
||||
await sql`
|
||||
alter table bookings
|
||||
add constraint bookings_booking_mode_check
|
||||
check (booking_mode in ('table', 'pax', 'seat'))
|
||||
`
|
||||
|
||||
await sql`
|
||||
update bookings
|
||||
set
|
||||
booking_mode = 'seat',
|
||||
updated_at = now()
|
||||
where booking_mode = 'pax'
|
||||
`
|
||||
|
||||
await sql`
|
||||
alter table bookings
|
||||
drop constraint if exists bookings_booking_mode_check
|
||||
`
|
||||
|
||||
await sql`
|
||||
alter table bookings
|
||||
add constraint bookings_booking_mode_check
|
||||
check (booking_mode in ('table', 'seat'))
|
||||
`
|
||||
|
||||
await sql`
|
||||
update booking_settings
|
||||
set
|
||||
total_seats = total_tables * 10,
|
||||
updated_at = now()
|
||||
where total_seats is null
|
||||
and total_tables is not null
|
||||
`
|
||||
|
||||
const existingBookings = await sql<{ id: string, seat_count: number | string }[]>`
|
||||
select
|
||||
id,
|
||||
|
||||
Reference in New Issue
Block a user