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
323 lines
7.6 KiB
TypeScript
323 lines
7.6 KiB
TypeScript
import { randomUUID } from 'node:crypto'
|
|
|
|
import type {
|
|
BookingCapacitySettings,
|
|
BookingInventorySummary,
|
|
BookingMode,
|
|
BookingStatus,
|
|
PublicBooking,
|
|
TicketType
|
|
} from '~~/shared/booking'
|
|
|
|
import { calculateBookingInventorySummary, isBookingStatus } from '~~/shared/booking'
|
|
|
|
import { randomToken, toIsoString } from './base64url'
|
|
import { ensureDatabaseReady } from './db-init'
|
|
import { getSqlClient } from './postgres'
|
|
|
|
type DbBookingRow = {
|
|
id: string
|
|
confirmation_token: string
|
|
customer_name: string
|
|
customer_phone: string
|
|
booking_mode: BookingMode
|
|
quantity: number | string
|
|
seat_count: number | string
|
|
ticket_type: TicketType
|
|
unit_price: number | string
|
|
total_price: number | string
|
|
person_in_charge_id: string
|
|
person_in_charge_name: string
|
|
person_in_charge_phone_number: string
|
|
status: BookingStatus | string
|
|
created_at: Date | string
|
|
confirmed_at: Date | string | null
|
|
}
|
|
|
|
type DbBookingSettingsRow = {
|
|
total_tables: number | string | null
|
|
updated_at: Date | string
|
|
}
|
|
|
|
function parseInteger(value: number | string) {
|
|
return typeof value === 'number' ? value : Number.parseInt(value, 10)
|
|
}
|
|
|
|
function mapBooking(row: DbBookingRow): PublicBooking {
|
|
return {
|
|
id: row.id,
|
|
confirmationToken: row.confirmation_token,
|
|
customerName: row.customer_name,
|
|
customerPhone: row.customer_phone,
|
|
bookingMode: row.booking_mode,
|
|
quantity: parseInteger(row.quantity),
|
|
seatCount: parseInteger(row.seat_count),
|
|
ticketType: row.ticket_type,
|
|
unitPrice: parseInteger(row.unit_price),
|
|
totalPrice: parseInteger(row.total_price),
|
|
personInChargeId: row.person_in_charge_id,
|
|
personInChargeName: row.person_in_charge_name,
|
|
personInChargePhoneNumber: row.person_in_charge_phone_number,
|
|
status: isBookingStatus(row.status) ? row.status : 'pending',
|
|
createdAt: toIsoString(row.created_at) ?? new Date().toISOString(),
|
|
confirmedAt: toIsoString(row.confirmed_at)
|
|
}
|
|
}
|
|
|
|
function mapBookingCapacitySettings(row: DbBookingSettingsRow | undefined): BookingCapacitySettings {
|
|
if (!row) {
|
|
return {
|
|
totalTables: null,
|
|
totalSeats: null,
|
|
updatedAt: null
|
|
}
|
|
}
|
|
|
|
return {
|
|
totalTables: row.total_tables === null ? null : parseInteger(row.total_tables),
|
|
updatedAt: toIsoString(row.updated_at)
|
|
}
|
|
}
|
|
|
|
export async function createBooking(input: {
|
|
customerName: string
|
|
customerPhone: string
|
|
bookingMode: BookingMode
|
|
quantity: number
|
|
seatCount: number
|
|
ticketType: TicketType
|
|
unitPrice: number
|
|
totalPrice: number
|
|
personInChargeId: string
|
|
personInChargeName: string
|
|
personInChargePhoneNumber: string
|
|
}) {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
const confirmationToken = randomToken(24)
|
|
|
|
const [row] = await sql<DbBookingRow[]>`
|
|
insert into bookings (
|
|
id,
|
|
confirmation_token,
|
|
customer_name,
|
|
customer_phone,
|
|
booking_mode,
|
|
quantity,
|
|
seat_count,
|
|
ticket_type,
|
|
unit_price,
|
|
total_price,
|
|
person_in_charge_id,
|
|
person_in_charge_name,
|
|
person_in_charge_phone_number,
|
|
status
|
|
)
|
|
values (
|
|
${randomUUID()},
|
|
${confirmationToken},
|
|
${input.customerName},
|
|
${input.customerPhone},
|
|
${input.bookingMode},
|
|
${input.quantity},
|
|
${input.seatCount},
|
|
${input.ticketType},
|
|
${input.unitPrice},
|
|
${input.totalPrice},
|
|
${input.personInChargeId},
|
|
${input.personInChargeName},
|
|
${input.personInChargePhoneNumber},
|
|
'pending'
|
|
)
|
|
returning
|
|
id,
|
|
confirmation_token,
|
|
customer_name,
|
|
customer_phone,
|
|
booking_mode,
|
|
quantity,
|
|
seat_count,
|
|
ticket_type,
|
|
unit_price,
|
|
total_price,
|
|
person_in_charge_id,
|
|
person_in_charge_name,
|
|
person_in_charge_phone_number,
|
|
status,
|
|
created_at,
|
|
confirmed_at
|
|
`
|
|
|
|
return {
|
|
booking: mapBooking(row),
|
|
confirmationToken
|
|
}
|
|
}
|
|
|
|
export async function getBookingByConfirmationToken(confirmationToken: string): Promise<PublicBooking | null> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [row] = await sql<DbBookingRow[]>`
|
|
select
|
|
id,
|
|
confirmation_token,
|
|
customer_name,
|
|
customer_phone,
|
|
booking_mode,
|
|
quantity,
|
|
seat_count,
|
|
ticket_type,
|
|
unit_price,
|
|
total_price,
|
|
person_in_charge_id,
|
|
person_in_charge_name,
|
|
person_in_charge_phone_number,
|
|
status,
|
|
created_at,
|
|
confirmed_at
|
|
from bookings
|
|
where confirmation_token = ${confirmationToken}
|
|
limit 1
|
|
`
|
|
|
|
return row ? mapBooking(row) : null
|
|
}
|
|
|
|
export async function listBookings(options?: {
|
|
personInChargeId?: string
|
|
}): Promise<PublicBooking[]> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const rows = options?.personInChargeId
|
|
? await sql<DbBookingRow[]>`
|
|
select
|
|
id,
|
|
confirmation_token,
|
|
customer_name,
|
|
customer_phone,
|
|
booking_mode,
|
|
quantity,
|
|
seat_count,
|
|
ticket_type,
|
|
unit_price,
|
|
total_price,
|
|
person_in_charge_id,
|
|
person_in_charge_name,
|
|
person_in_charge_phone_number,
|
|
status,
|
|
created_at,
|
|
confirmed_at
|
|
from bookings
|
|
where person_in_charge_id = ${options.personInChargeId}
|
|
order by created_at desc
|
|
`
|
|
: await sql<DbBookingRow[]>`
|
|
select
|
|
id,
|
|
confirmation_token,
|
|
customer_name,
|
|
customer_phone,
|
|
booking_mode,
|
|
quantity,
|
|
seat_count,
|
|
ticket_type,
|
|
unit_price,
|
|
total_price,
|
|
person_in_charge_id,
|
|
person_in_charge_name,
|
|
person_in_charge_phone_number,
|
|
status,
|
|
created_at,
|
|
confirmed_at
|
|
from bookings
|
|
order by created_at desc
|
|
`
|
|
|
|
return rows.map(mapBooking)
|
|
}
|
|
|
|
export async function getBookingCapacitySettings(): Promise<BookingCapacitySettings> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [row] = await sql<DbBookingSettingsRow[]>`
|
|
select
|
|
total_tables,
|
|
updated_at
|
|
from booking_settings
|
|
where id = 'default'
|
|
limit 1
|
|
`
|
|
|
|
return mapBookingCapacitySettings(row)
|
|
}
|
|
|
|
export async function updateBookingCapacitySettings(input: {
|
|
totalTables: number | null
|
|
}): Promise<BookingCapacitySettings> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [row] = await sql<DbBookingSettingsRow[]>`
|
|
update booking_settings
|
|
set
|
|
total_tables = ${input.totalTables},
|
|
updated_at = now()
|
|
where id = 'default'
|
|
returning
|
|
total_tables,
|
|
updated_at
|
|
`
|
|
|
|
return mapBookingCapacitySettings(row)
|
|
}
|
|
|
|
export async function getBookingInventorySummary(): Promise<BookingInventorySummary> {
|
|
const [bookings, settings] = await Promise.all([
|
|
listBookings(),
|
|
getBookingCapacitySettings()
|
|
])
|
|
|
|
return calculateBookingInventorySummary(bookings, settings)
|
|
}
|
|
|
|
export async function confirmBookingByConfirmationToken(confirmationToken: string): Promise<PublicBooking | null> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [row] = await sql<DbBookingRow[]>`
|
|
update bookings
|
|
set
|
|
status = 'confirmed',
|
|
confirmed_at = now(),
|
|
updated_at = now()
|
|
where confirmation_token = ${confirmationToken}
|
|
and status = 'pending'
|
|
returning
|
|
id,
|
|
confirmation_token,
|
|
customer_name,
|
|
customer_phone,
|
|
booking_mode,
|
|
quantity,
|
|
seat_count,
|
|
ticket_type,
|
|
unit_price,
|
|
total_price,
|
|
person_in_charge_id,
|
|
person_in_charge_name,
|
|
person_in_charge_phone_number,
|
|
status,
|
|
created_at,
|
|
confirmed_at
|
|
`
|
|
|
|
if (row) {
|
|
return mapBooking(row)
|
|
}
|
|
|
|
return await getBookingByConfirmationToken(confirmationToken)
|
|
}
|