Files
dticket.tootaio.com/server/utils/booking-repository.ts
xiaomai 8541c4a2d1 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
2026-04-12 21:43:30 +08:00

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)
}