import { randomUUID } from 'node:crypto' import type { BookingCapacitySettings, BookingInventorySummary, BookingMode, BookingStatus, PublicBooking, PublicBookingSeat, ReceiptBooking, TicketType } from '~~/shared/booking' import { calculateBookingInventorySummary, isBookingMode, 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 receipt_token: string customer_name: string customer_phone: string booking_mode: string 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 DbBookingSeatRow = { id: string seat_number: number | string seat_token: string recipient_name: string | null recipient_phone: string | null shared_at: Date | string | null created_at: Date | string updated_at: Date | string } type DbBookingSeatWithBookingRow = DbBookingSeatRow & { booking_id: string confirmation_token: string receipt_token: string customer_name: string customer_phone: string booking_mode: string quantity: number | string seat_count: number | string ticket_type: TicketType unit_price: number | string total_price: number | string status: BookingStatus | string booking_created_at: Date | string confirmed_at: Date | string | null } type DbBookingSettingsRow = { total_tables: number | string | null total_seats: number | string | null updated_at: Date | string } 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: normalizeBookingMode(row.booking_mode), quantity: parseInteger(row.quantity), seatCount, 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 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: normalizeBookingMode(row.booking_mode), quantity: parseInteger(row.quantity), seatCount, ticketType: row.ticket_type, unitPrice: parseInteger(row.unit_price), totalPrice: parseInteger(row.total_price), status: isBookingStatus(row.status) ? row.status : 'pending', createdAt: toIsoString('booking_created_at' in row ? row.booking_created_at : row.created_at) ?? new Date().toISOString(), confirmedAt: toIsoString(row.confirmed_at) } } function mapBookingSeat(row: DbBookingSeatRow): PublicBookingSeat { return { id: row.id, seatNumber: parseInteger(row.seat_number), seatToken: row.seat_token, recipientName: row.recipient_name, recipientPhone: row.recipient_phone, sharedAt: toIsoString(row.shared_at), createdAt: toIsoString(row.created_at) ?? new Date().toISOString(), updatedAt: toIsoString(row.updated_at) ?? new Date().toISOString() } } function mapBookingCapacitySettings(row: DbBookingSettingsRow | undefined): BookingCapacitySettings { if (!row) { return { 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 { totalSeats, updatedAt: toIsoString(row.updated_at) } } async function insertBookingSeats( tx: ReturnType, bookingId: string, seatCount: number ) { for (let seatNumber = 1; seatNumber <= seatCount; seatNumber += 1) { await tx` insert into booking_seats ( id, booking_id, seat_number, seat_token ) values ( ${randomUUID()}, ${bookingId}, ${seatNumber}, ${randomToken(24)} ) ` } } 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 bookingId = randomUUID() const confirmationToken = randomToken(24) const receiptToken = randomToken(24) const row = await sql.begin(async (tx) => { const [createdBooking] = await tx` insert into bookings ( id, confirmation_token, receipt_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 ( ${bookingId}, ${confirmationToken}, ${receiptToken}, ${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, receipt_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 ` await insertBookingSeats(tx, bookingId, input.seatCount) return createdBooking }) return { booking: mapBooking(row), confirmationToken, receiptToken } } export async function getBookingByConfirmationToken(confirmationToken: string): Promise { await ensureDatabaseReady() const sql = getSqlClient() const [row] = await sql` select id, confirmation_token, receipt_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 getBookingByReceiptToken(receiptToken: string): Promise { await ensureDatabaseReady() const sql = getSqlClient() const [row] = await sql` select id, confirmation_token, receipt_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 receipt_token = ${receiptToken} limit 1 ` return row ? mapBooking(row) : null } export async function listBookings(options?: { personInChargeId?: string }): Promise { await ensureDatabaseReady() const sql = getSqlClient() const rows = options?.personInChargeId ? await sql` select id, confirmation_token, receipt_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` select id, confirmation_token, receipt_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 listBookingSeats(bookingId: string): Promise { await ensureDatabaseReady() const sql = getSqlClient() const rows = await sql` select id, seat_number, seat_token, recipient_name, recipient_phone, shared_at, created_at, updated_at from booking_seats where booking_id = ${bookingId} order by seat_number asc ` return rows.map(mapBookingSeat) } export async function getBookingReceiptByReceiptToken(receiptToken: string): Promise<{ booking: ReceiptBooking seats: PublicBookingSeat[] } | null> { const booking = await getBookingByReceiptToken(receiptToken) if (!booking) { return null } const seats = await listBookingSeats(booking.id) return { booking: mapReceiptBooking({ id: booking.id, confirmation_token: booking.confirmationToken, receipt_token: booking.receiptToken, customer_name: booking.customerName, customer_phone: booking.customerPhone, booking_mode: booking.bookingMode, quantity: booking.quantity, seat_count: booking.seatCount, ticket_type: booking.ticketType, unit_price: booking.unitPrice, total_price: booking.totalPrice, person_in_charge_id: booking.personInChargeId, person_in_charge_name: booking.personInChargeName, person_in_charge_phone_number: booking.personInChargePhoneNumber, status: booking.status, created_at: booking.createdAt, confirmed_at: booking.confirmedAt }), seats } } export async function getSeatReceiptBySeatToken(seatToken: string): Promise<{ booking: ReceiptBooking seat: PublicBookingSeat } | null> { await ensureDatabaseReady() const sql = getSqlClient() const [row] = await sql` select booking_seats.id, booking_seats.seat_number, booking_seats.seat_token, booking_seats.recipient_name, booking_seats.recipient_phone, booking_seats.shared_at, booking_seats.created_at, booking_seats.updated_at, bookings.id as booking_id, bookings.confirmation_token, bookings.receipt_token, bookings.customer_name, bookings.customer_phone, bookings.booking_mode, bookings.quantity, bookings.seat_count, bookings.ticket_type, bookings.unit_price, bookings.total_price, bookings.status, bookings.created_at as booking_created_at, bookings.confirmed_at from booking_seats inner join bookings on bookings.id = booking_seats.booking_id where booking_seats.seat_token = ${seatToken} limit 1 ` if (!row) { return null } return { booking: mapReceiptBooking({ ...row, id: row.booking_id }), seat: mapBookingSeat(row) } } export async function updateBookingSeatShareByReceiptToken(input: { receiptToken: string seatId: string shared: boolean recipientName: string | null recipientPhone: string | null }): Promise { await ensureDatabaseReady() const sql = getSqlClient() const nextSeatToken = input.shared ? null : randomToken(24) const [row] = await sql` update booking_seats set recipient_name = ${input.shared ? input.recipientName : null}, recipient_phone = ${input.shared ? input.recipientPhone : null}, shared_at = ${input.shared ? new Date() : null}, seat_token = coalesce(${nextSeatToken}, seat_token), updated_at = now() from bookings where booking_seats.booking_id = bookings.id and bookings.receipt_token = ${input.receiptToken} and booking_seats.id = ${input.seatId} returning booking_seats.id, booking_seats.seat_number, booking_seats.seat_token, booking_seats.recipient_name, booking_seats.recipient_phone, booking_seats.shared_at, booking_seats.created_at, booking_seats.updated_at ` return row ? mapBookingSeat(row) : null } export async function getBookingCapacitySettings(): Promise { await ensureDatabaseReady() const sql = getSqlClient() const [row] = await sql` select total_tables, total_seats, updated_at from booking_settings where id = 'default' limit 1 ` return mapBookingCapacitySettings(row) } export async function updateBookingCapacitySettings(input: { totalSeats: number | null }): Promise { await ensureDatabaseReady() const sql = getSqlClient() const [row] = await sql` update booking_settings set total_seats = ${input.totalSeats}, updated_at = now() where id = 'default' returning total_tables, total_seats, updated_at ` return mapBookingCapacitySettings(row) } export async function getBookingInventorySummary(): Promise { const [bookings, settings] = await Promise.all([ listBookings(), getBookingCapacitySettings() ]) return calculateBookingInventorySummary(bookings, settings) } export async function confirmBookingByConfirmationToken(confirmationToken: string): Promise { await ensureDatabaseReady() const sql = getSqlClient() const [row] = await sql` update bookings set status = 'confirmed', confirmed_at = now(), updated_at = now() where confirmation_token = ${confirmationToken} and status = 'pending' returning id, confirmation_token, receipt_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) }