Allow users to select payment method and upload receipts before confirming. Add public API endpoints for payment updates and document management.
1514 lines
43 KiB
TypeScript
1514 lines
43 KiB
TypeScript
import { randomUUID } from 'node:crypto'
|
|
|
|
import type {
|
|
BookingModeOption,
|
|
BookingCapacitySettings,
|
|
BookingInventorySummary,
|
|
DinnerEvent,
|
|
BookingMode,
|
|
BookingTransactionDocument,
|
|
PaymentMethod,
|
|
BookingStatus,
|
|
PublicBookingConfig,
|
|
PublicBooking,
|
|
PublicBookingSeat,
|
|
ReceiptBooking,
|
|
TicketCatalogItem,
|
|
TicketType
|
|
} from '~~/shared/booking'
|
|
import type { AppLocale } from '~~/shared/i18n'
|
|
|
|
import { calculateBookingInventorySummary, getBookingStatusLabel, isBookingStatus, isPaymentMethod } from '~~/shared/booking'
|
|
import { resolveLocale } from '~~/shared/i18n'
|
|
|
|
import { randomToken, toIsoString } from './base64url'
|
|
import { ensureDatabaseReady } from './db-init'
|
|
import { getSqlClient } from './postgres'
|
|
|
|
type DbBookingRow = {
|
|
id: string
|
|
confirmation_token: string
|
|
receipt_token: string
|
|
event_id: string
|
|
event_title: string
|
|
event_date_label: string
|
|
event_time_label: string
|
|
event_venue: string
|
|
customer_name: string
|
|
customer_phone: string
|
|
locale: AppLocale | string | null
|
|
deleted_at: Date | string | null
|
|
booking_mode_id: string | null
|
|
booking_mode: string
|
|
booking_mode_label: string | null
|
|
booking_mode_seats_per_unit: number | string | null
|
|
quantity: number | string
|
|
seat_count: number | string
|
|
ticket_type_id: string | null
|
|
ticket_type: string
|
|
ticket_label: string | null
|
|
ticket_description: string | null
|
|
unit_price: number | string
|
|
total_price: number | string
|
|
person_in_charge_id: string
|
|
person_in_charge_name: string | null
|
|
person_in_charge_phone_number: string | null
|
|
payment_method: PaymentMethod | string
|
|
transaction_document_original_name: string | null
|
|
transaction_document_storage_name: string | null
|
|
transaction_document_mime_type: string | null
|
|
transaction_document_size: number | string | null
|
|
transaction_document_uploaded_at: Date | string | null
|
|
remark?: string | null
|
|
status: BookingStatus | string
|
|
status_label: string | null
|
|
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 & Omit<DbBookingRow, 'id' | 'created_at'> & {
|
|
booking_id: string
|
|
booking_created_at: Date | string
|
|
}
|
|
|
|
type DbBookingTransactionDocumentRow = {
|
|
payment_method: PaymentMethod | string
|
|
transaction_document_original_name: string | null
|
|
transaction_document_storage_name: string | null
|
|
transaction_document_mime_type: string | null
|
|
transaction_document_size: number | string | null
|
|
transaction_document_uploaded_at: Date | string | null
|
|
}
|
|
|
|
type BookingTransactionDocumentRecord = BookingTransactionDocument & {
|
|
storageName: string
|
|
}
|
|
|
|
type DbBookingSettingsRow = {
|
|
event_id: string
|
|
total_tables: number | string | null
|
|
total_seats: number | string | null
|
|
updated_at: Date | string
|
|
}
|
|
|
|
type DbDinnerEventRow = {
|
|
id: string
|
|
title: string
|
|
date_label: string
|
|
time_label: string
|
|
venue: string
|
|
}
|
|
|
|
type DbBookingModeOptionRow = {
|
|
id: string
|
|
event_id: string
|
|
code: string
|
|
label: string
|
|
quantity_label: string
|
|
seats_per_unit: number | string
|
|
sort_order: number | string
|
|
}
|
|
|
|
type DbTicketCatalogItemRow = {
|
|
id: string
|
|
event_id: string
|
|
code: string
|
|
label: string
|
|
description: string
|
|
price: number | string
|
|
sort_order: number | string
|
|
}
|
|
|
|
export interface BookingModeOptionRecord extends BookingModeOption {
|
|
eventId: string
|
|
}
|
|
|
|
export interface TicketCatalogItemRecord extends TicketCatalogItem {
|
|
eventId: string
|
|
}
|
|
|
|
function bookingSelectColumns(sql: any) {
|
|
return sql`
|
|
bookings.id,
|
|
bookings.confirmation_token,
|
|
bookings.receipt_token,
|
|
dinner_events.id as event_id,
|
|
dinner_events.title as event_title,
|
|
dinner_events.date_label as event_date_label,
|
|
dinner_events.time_label as event_time_label,
|
|
dinner_events.venue as event_venue,
|
|
bookings.customer_name,
|
|
bookings.customer_phone,
|
|
bookings.locale,
|
|
bookings.deleted_at,
|
|
bookings.booking_mode_id,
|
|
coalesce(booking_modes.code, bookings.booking_mode) as booking_mode,
|
|
booking_modes.label as booking_mode_label,
|
|
booking_modes.seats_per_unit as booking_mode_seats_per_unit,
|
|
bookings.quantity,
|
|
bookings.seat_count,
|
|
bookings.ticket_type_id,
|
|
coalesce(ticket_types.code, bookings.ticket_type) as ticket_type,
|
|
ticket_types.label as ticket_label,
|
|
ticket_types.description as ticket_description,
|
|
bookings.unit_price,
|
|
bookings.total_price,
|
|
bookings.person_in_charge_id,
|
|
coalesce(users.full_name, bookings.person_in_charge_name) as person_in_charge_name,
|
|
coalesce(users.phone_number, bookings.person_in_charge_phone_number) as person_in_charge_phone_number,
|
|
bookings.payment_method,
|
|
bookings.transaction_document_original_name,
|
|
bookings.transaction_document_storage_name,
|
|
bookings.transaction_document_mime_type,
|
|
bookings.transaction_document_size,
|
|
bookings.transaction_document_uploaded_at,
|
|
bookings.remark,
|
|
bookings.status,
|
|
booking_statuses.label as status_label,
|
|
bookings.created_at,
|
|
bookings.confirmed_at
|
|
`
|
|
}
|
|
|
|
function bookingJoins(sql: any) {
|
|
return sql`
|
|
inner join dinner_events on dinner_events.id = bookings.event_id
|
|
left join booking_modes on booking_modes.id = bookings.booking_mode_id
|
|
left join ticket_types on ticket_types.id = bookings.ticket_type_id
|
|
left join users on users.id = bookings.person_in_charge_id
|
|
left join booking_statuses on booking_statuses.code = bookings.status
|
|
`
|
|
}
|
|
|
|
function parseInteger(value: number | string) {
|
|
return typeof value === 'number' ? value : Number.parseInt(value, 10)
|
|
}
|
|
|
|
function mapDinnerEvent(row: DbDinnerEventRow): DinnerEvent {
|
|
return {
|
|
id: row.id,
|
|
title: row.title,
|
|
dateLabel: row.date_label,
|
|
timeLabel: row.time_label,
|
|
venue: row.venue
|
|
}
|
|
}
|
|
|
|
function mapDinnerEventFromBooking(row: DbBookingRow | DbBookingSeatWithBookingRow): DinnerEvent {
|
|
return {
|
|
id: row.event_id,
|
|
title: row.event_title,
|
|
dateLabel: row.event_date_label,
|
|
timeLabel: row.event_time_label,
|
|
venue: row.event_venue
|
|
}
|
|
}
|
|
|
|
function mapBookingModeOption(row: DbBookingModeOptionRow): BookingModeOptionRecord {
|
|
return {
|
|
id: row.id,
|
|
eventId: row.event_id,
|
|
value: row.code,
|
|
label: row.label,
|
|
quantityLabel: row.quantity_label,
|
|
seatsPerUnit: parseInteger(row.seats_per_unit),
|
|
sortOrder: parseInteger(row.sort_order)
|
|
}
|
|
}
|
|
|
|
function mapTicketCatalogItem(row: DbTicketCatalogItemRow): TicketCatalogItemRecord {
|
|
return {
|
|
id: row.id,
|
|
eventId: row.event_id,
|
|
value: row.code,
|
|
label: row.label,
|
|
description: row.description,
|
|
price: parseInteger(row.price),
|
|
sortOrder: parseInteger(row.sort_order)
|
|
}
|
|
}
|
|
|
|
function mapBookingTransactionDocument(row: DbBookingTransactionDocumentRow, url: string): BookingTransactionDocument | null {
|
|
if (
|
|
row.payment_method !== 'bank'
|
|
|| !row.transaction_document_original_name
|
|
|| !row.transaction_document_storage_name
|
|
|| !row.transaction_document_mime_type
|
|
|| row.transaction_document_size === null
|
|
|| !row.transaction_document_uploaded_at
|
|
) {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
originalName: row.transaction_document_original_name,
|
|
mimeType: row.transaction_document_mime_type,
|
|
size: parseInteger(row.transaction_document_size),
|
|
uploadedAt: toIsoString(row.transaction_document_uploaded_at) ?? new Date().toISOString(),
|
|
url
|
|
}
|
|
}
|
|
|
|
function mapBookingTransactionDocumentRecord(row: DbBookingTransactionDocumentRow, url: string): BookingTransactionDocumentRecord | null {
|
|
const document = mapBookingTransactionDocument(row, url)
|
|
|
|
if (!document || !row.transaction_document_storage_name) {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
...document,
|
|
storageName: row.transaction_document_storage_name
|
|
}
|
|
}
|
|
|
|
function mapBooking(row: DbBookingRow, options?: {
|
|
includeTransactionDocument?: boolean
|
|
transactionDocumentUrl?: string
|
|
}): PublicBooking {
|
|
const seatCount = parseInteger(row.seat_count)
|
|
const status = isBookingStatus(row.status) ? row.status : 'pending'
|
|
const ticketType = row.ticket_type
|
|
const bookingMode = row.booking_mode
|
|
const paymentMethod = isPaymentMethod(row.payment_method) ? row.payment_method : 'cash'
|
|
|
|
return {
|
|
id: row.id,
|
|
confirmationToken: row.confirmation_token,
|
|
receiptToken: row.receipt_token,
|
|
event: mapDinnerEventFromBooking(row),
|
|
customerName: row.customer_name,
|
|
customerPhone: row.customer_phone,
|
|
locale: resolveLocale(row.locale),
|
|
bookingModeId: row.booking_mode_id,
|
|
bookingMode,
|
|
bookingModeLabel: row.booking_mode_label || bookingMode,
|
|
quantity: parseInteger(row.quantity),
|
|
seatCount,
|
|
ticketTypeId: row.ticket_type_id,
|
|
ticketType,
|
|
ticketLabel: row.ticket_label || ticketType.toUpperCase(),
|
|
ticketDescription: row.ticket_description,
|
|
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 || '',
|
|
paymentMethod,
|
|
transactionDocument: options?.includeTransactionDocument
|
|
? mapBookingTransactionDocument(row, options.transactionDocumentUrl || `/api/bookings/${row.id}/transaction-document`)
|
|
: null,
|
|
remark: row.remark || null,
|
|
status,
|
|
statusLabel: row.status_label || getBookingStatusLabel(status),
|
|
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)
|
|
const status = isBookingStatus(row.status) ? row.status : 'pending'
|
|
const ticketType = row.ticket_type
|
|
const bookingMode = row.booking_mode
|
|
|
|
return {
|
|
id: row.id,
|
|
receiptToken: row.receipt_token,
|
|
event: mapDinnerEventFromBooking(row),
|
|
customerName: row.customer_name,
|
|
customerPhone: row.customer_phone,
|
|
locale: resolveLocale(row.locale),
|
|
bookingModeId: row.booking_mode_id,
|
|
bookingMode,
|
|
bookingModeLabel: row.booking_mode_label || bookingMode,
|
|
quantity: parseInteger(row.quantity),
|
|
seatCount,
|
|
ticketTypeId: row.ticket_type_id,
|
|
ticketType,
|
|
ticketLabel: row.ticket_label || ticketType.toUpperCase(),
|
|
ticketDescription: row.ticket_description,
|
|
unitPrice: parseInteger(row.unit_price),
|
|
totalPrice: parseInteger(row.total_price),
|
|
status,
|
|
statusLabel: row.status_label || getBookingStatusLabel(status),
|
|
createdAt: toIsoString('booking_created_at' in row ? row.booking_created_at : row.created_at) ?? new Date().toISOString(),
|
|
confirmedAt: toIsoString(row.confirmed_at)
|
|
}
|
|
}
|
|
|
|
function mapPublicBookingToReceiptBooking(booking: PublicBooking): ReceiptBooking {
|
|
return {
|
|
id: booking.id,
|
|
receiptToken: booking.receiptToken,
|
|
event: booking.event,
|
|
customerName: booking.customerName,
|
|
customerPhone: booking.customerPhone,
|
|
locale: booking.locale,
|
|
bookingModeId: booking.bookingModeId,
|
|
bookingMode: booking.bookingMode,
|
|
bookingModeLabel: booking.bookingModeLabel,
|
|
quantity: booking.quantity,
|
|
seatCount: booking.seatCount,
|
|
ticketTypeId: booking.ticketTypeId,
|
|
ticketType: booking.ticketType,
|
|
ticketLabel: booking.ticketLabel,
|
|
ticketDescription: booking.ticketDescription,
|
|
unitPrice: booking.unitPrice,
|
|
totalPrice: booking.totalPrice,
|
|
status: booking.status,
|
|
statusLabel: booking.statusLabel,
|
|
createdAt: booking.createdAt,
|
|
confirmedAt: booking.confirmedAt
|
|
}
|
|
}
|
|
|
|
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 ? null : parseInteger(row.total_seats)
|
|
|
|
return {
|
|
totalSeats,
|
|
updatedAt: toIsoString(row.updated_at)
|
|
}
|
|
}
|
|
|
|
export async function getPublicBookingConfig(): Promise<PublicBookingConfig> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [event] = await sql<DbDinnerEventRow[]>`
|
|
select
|
|
id,
|
|
title,
|
|
date_label,
|
|
time_label,
|
|
venue
|
|
from dinner_events
|
|
where is_active = true
|
|
order by sort_order asc, created_at asc
|
|
limit 1
|
|
`
|
|
|
|
if (!event) {
|
|
throw new Error('No active dinner event is configured.')
|
|
}
|
|
|
|
const [bookingModes, ticketCatalog] = await Promise.all([
|
|
sql<DbBookingModeOptionRow[]>`
|
|
select
|
|
id,
|
|
event_id,
|
|
code,
|
|
label,
|
|
quantity_label,
|
|
seats_per_unit,
|
|
sort_order
|
|
from booking_modes
|
|
where event_id = ${event.id}
|
|
and is_active = true
|
|
order by sort_order asc, label asc
|
|
`,
|
|
sql<DbTicketCatalogItemRow[]>`
|
|
select
|
|
id,
|
|
event_id,
|
|
code,
|
|
label,
|
|
description,
|
|
price,
|
|
sort_order
|
|
from ticket_types
|
|
where event_id = ${event.id}
|
|
and is_active = true
|
|
order by sort_order asc, label asc
|
|
`
|
|
])
|
|
|
|
return {
|
|
event: mapDinnerEvent(event),
|
|
bookingModes: bookingModes.map(mapBookingModeOption),
|
|
ticketCatalog: ticketCatalog.map(mapTicketCatalogItem)
|
|
}
|
|
}
|
|
|
|
export async function getActiveBookingModeOptionByCode(code: string): Promise<BookingModeOptionRecord | null> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [row] = await sql<DbBookingModeOptionRow[]>`
|
|
select
|
|
booking_modes.id,
|
|
booking_modes.event_id,
|
|
booking_modes.code,
|
|
booking_modes.label,
|
|
booking_modes.quantity_label,
|
|
booking_modes.seats_per_unit,
|
|
booking_modes.sort_order
|
|
from booking_modes
|
|
inner join dinner_events on dinner_events.id = booking_modes.event_id
|
|
where dinner_events.is_active = true
|
|
and booking_modes.is_active = true
|
|
and booking_modes.code = ${code}
|
|
order by booking_modes.sort_order asc
|
|
limit 1
|
|
`
|
|
|
|
return row ? mapBookingModeOption(row) : null
|
|
}
|
|
|
|
export async function getActiveTicketCatalogItemByCode(code: string): Promise<TicketCatalogItemRecord | null> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [row] = await sql<DbTicketCatalogItemRow[]>`
|
|
select
|
|
ticket_types.id,
|
|
ticket_types.event_id,
|
|
ticket_types.code,
|
|
ticket_types.label,
|
|
ticket_types.description,
|
|
ticket_types.price,
|
|
ticket_types.sort_order
|
|
from ticket_types
|
|
inner join dinner_events on dinner_events.id = ticket_types.event_id
|
|
where dinner_events.is_active = true
|
|
and ticket_types.is_active = true
|
|
and ticket_types.code = ${code}
|
|
order by ticket_types.sort_order asc
|
|
limit 1
|
|
`
|
|
|
|
return row ? mapTicketCatalogItem(row) : null
|
|
}
|
|
|
|
async function insertBookingSeats(
|
|
tx: ReturnType<typeof getSqlClient>,
|
|
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: {
|
|
eventId: string
|
|
customerName: string
|
|
customerPhone: string
|
|
locale: AppLocale
|
|
bookingModeId: string
|
|
bookingMode: BookingMode
|
|
quantity: number
|
|
seatCount: number
|
|
ticketTypeId: string
|
|
ticketType: TicketType
|
|
unitPrice: number
|
|
totalPrice: number
|
|
personInChargeId: 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<DbBookingRow[]>`
|
|
with inserted_booking as (
|
|
insert into bookings (
|
|
id,
|
|
confirmation_token,
|
|
receipt_token,
|
|
event_id,
|
|
customer_name,
|
|
customer_phone,
|
|
locale,
|
|
booking_mode_id,
|
|
booking_mode,
|
|
quantity,
|
|
seat_count,
|
|
ticket_type_id,
|
|
ticket_type,
|
|
unit_price,
|
|
total_price,
|
|
person_in_charge_id,
|
|
remark,
|
|
status
|
|
)
|
|
values (
|
|
${bookingId},
|
|
${confirmationToken},
|
|
${receiptToken},
|
|
${input.eventId},
|
|
${input.customerName},
|
|
${input.customerPhone},
|
|
${input.locale},
|
|
${input.bookingModeId},
|
|
${input.bookingMode},
|
|
${input.quantity},
|
|
${input.seatCount},
|
|
${input.ticketTypeId},
|
|
${input.ticketType},
|
|
${input.unitPrice},
|
|
${input.totalPrice},
|
|
${input.personInChargeId},
|
|
null,
|
|
'pending'
|
|
)
|
|
returning *
|
|
)
|
|
select ${bookingSelectColumns(tx)}
|
|
from inserted_booking as bookings
|
|
${bookingJoins(tx)}
|
|
`
|
|
|
|
await insertBookingSeats(tx, bookingId, input.seatCount)
|
|
|
|
return createdBooking
|
|
})
|
|
|
|
return {
|
|
booking: mapBooking(row),
|
|
confirmationToken,
|
|
receiptToken
|
|
}
|
|
}
|
|
|
|
export async function getBookingByConfirmationToken(confirmationToken: string, options?: {
|
|
includeTransactionDocument?: boolean
|
|
}): Promise<PublicBooking | null> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [row] = await sql<DbBookingRow[]>`
|
|
select ${bookingSelectColumns(sql)}
|
|
from bookings
|
|
${bookingJoins(sql)}
|
|
where bookings.confirmation_token = ${confirmationToken}
|
|
and bookings.deleted_at is null
|
|
limit 1
|
|
`
|
|
|
|
return row
|
|
? mapBooking(row, {
|
|
includeTransactionDocument: options?.includeTransactionDocument,
|
|
transactionDocumentUrl: `/api/public/bookings/${confirmationToken}/transaction-document`
|
|
})
|
|
: null
|
|
}
|
|
|
|
export async function getBookingByReceiptToken(receiptToken: string): Promise<PublicBooking | null> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [row] = await sql<DbBookingRow[]>`
|
|
select ${bookingSelectColumns(sql)}
|
|
from bookings
|
|
${bookingJoins(sql)}
|
|
where bookings.receipt_token = ${receiptToken}
|
|
and bookings.deleted_at is null
|
|
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 ${bookingSelectColumns(sql)}
|
|
from bookings
|
|
${bookingJoins(sql)}
|
|
where dinner_events.is_active = true
|
|
and bookings.deleted_at is null
|
|
and bookings.person_in_charge_id = ${options.personInChargeId}
|
|
order by bookings.created_at desc
|
|
`
|
|
: await sql<DbBookingRow[]>`
|
|
select ${bookingSelectColumns(sql)}
|
|
from bookings
|
|
${bookingJoins(sql)}
|
|
where dinner_events.is_active = true
|
|
and bookings.deleted_at is null
|
|
order by bookings.created_at desc
|
|
`
|
|
|
|
return rows.map((row) => mapBooking(row, { includeTransactionDocument: true }))
|
|
}
|
|
|
|
export async function updateBookingRemark(input: {
|
|
bookingId: string
|
|
personInChargeId?: string
|
|
remark: string | null
|
|
}): Promise<PublicBooking | null> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const rows = input.personInChargeId
|
|
? await sql<DbBookingRow[]>`
|
|
with updated_booking as (
|
|
update bookings
|
|
set
|
|
remark = ${input.remark},
|
|
updated_at = now()
|
|
where id = ${input.bookingId}
|
|
and deleted_at is null
|
|
and person_in_charge_id = ${input.personInChargeId}
|
|
returning *
|
|
)
|
|
select ${bookingSelectColumns(sql)}
|
|
from updated_booking as bookings
|
|
${bookingJoins(sql)}
|
|
where dinner_events.is_active = true
|
|
limit 1
|
|
`
|
|
: await sql<DbBookingRow[]>`
|
|
with updated_booking as (
|
|
update bookings
|
|
set
|
|
remark = ${input.remark},
|
|
updated_at = now()
|
|
where id = ${input.bookingId}
|
|
and deleted_at is null
|
|
returning *
|
|
)
|
|
select ${bookingSelectColumns(sql)}
|
|
from updated_booking as bookings
|
|
${bookingJoins(sql)}
|
|
where dinner_events.is_active = true
|
|
limit 1
|
|
`
|
|
|
|
return rows[0] ? mapBooking(rows[0], { includeTransactionDocument: true }) : null
|
|
}
|
|
|
|
export async function updateBookingPersonInCharge(input: {
|
|
bookingId: string
|
|
currentPersonInChargeId?: string
|
|
nextPersonInChargeId: string
|
|
}): Promise<PublicBooking | null> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const rows = input.currentPersonInChargeId
|
|
? await sql<DbBookingRow[]>`
|
|
with updated_booking as (
|
|
update bookings
|
|
set
|
|
person_in_charge_id = ${input.nextPersonInChargeId},
|
|
updated_at = now()
|
|
where id = ${input.bookingId}
|
|
and deleted_at is null
|
|
and person_in_charge_id = ${input.currentPersonInChargeId}
|
|
returning *
|
|
)
|
|
select ${bookingSelectColumns(sql)}
|
|
from updated_booking as bookings
|
|
${bookingJoins(sql)}
|
|
where dinner_events.is_active = true
|
|
limit 1
|
|
`
|
|
: await sql<DbBookingRow[]>`
|
|
with updated_booking as (
|
|
update bookings
|
|
set
|
|
person_in_charge_id = ${input.nextPersonInChargeId},
|
|
updated_at = now()
|
|
where id = ${input.bookingId}
|
|
and deleted_at is null
|
|
returning *
|
|
)
|
|
select ${bookingSelectColumns(sql)}
|
|
from updated_booking as bookings
|
|
${bookingJoins(sql)}
|
|
where dinner_events.is_active = true
|
|
limit 1
|
|
`
|
|
|
|
return rows[0] ? mapBooking(rows[0], { includeTransactionDocument: true }) : null
|
|
}
|
|
|
|
export async function getBookingById(bookingId: string, options?: {
|
|
personInChargeId?: string
|
|
}): Promise<PublicBooking | null> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const rows = options?.personInChargeId
|
|
? await sql<DbBookingRow[]>`
|
|
select ${bookingSelectColumns(sql)}
|
|
from bookings
|
|
${bookingJoins(sql)}
|
|
where bookings.id = ${bookingId}
|
|
and bookings.deleted_at is null
|
|
and bookings.person_in_charge_id = ${options.personInChargeId}
|
|
limit 1
|
|
`
|
|
: await sql<DbBookingRow[]>`
|
|
select ${bookingSelectColumns(sql)}
|
|
from bookings
|
|
${bookingJoins(sql)}
|
|
where bookings.id = ${bookingId}
|
|
and bookings.deleted_at is null
|
|
limit 1
|
|
`
|
|
|
|
return rows[0] ? mapBooking(rows[0], { includeTransactionDocument: true }) : null
|
|
}
|
|
|
|
async function syncBookingSeats(tx: ReturnType<typeof getSqlClient>, bookingId: string, currentSeatCount: number, nextSeatCount: number) {
|
|
if (nextSeatCount > currentSeatCount) {
|
|
for (let seatNumber = currentSeatCount + 1; seatNumber <= nextSeatCount; seatNumber += 1) {
|
|
await tx`
|
|
insert into booking_seats (
|
|
id,
|
|
booking_id,
|
|
seat_number,
|
|
seat_token
|
|
)
|
|
values (
|
|
${randomUUID()},
|
|
${bookingId},
|
|
${seatNumber},
|
|
${randomToken(24)}
|
|
)
|
|
`
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
if (nextSeatCount < currentSeatCount) {
|
|
await tx`
|
|
delete from booking_seats
|
|
where booking_id = ${bookingId}
|
|
and seat_number > ${nextSeatCount}
|
|
`
|
|
}
|
|
}
|
|
|
|
export async function updateBookingDetails(input: {
|
|
bookingId: string
|
|
customerName: string
|
|
customerPhone: string
|
|
bookingModeId: string
|
|
bookingMode: BookingMode
|
|
quantity: number
|
|
seatCount: number
|
|
ticketTypeId: string
|
|
ticketType: TicketType
|
|
unitPrice: number
|
|
totalPrice: number
|
|
paymentMethod: PaymentMethod
|
|
remark: string | null
|
|
personInChargeId?: string
|
|
}) {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
return await sql.begin(async (tx) => {
|
|
const [currentBooking] = await tx<{ seat_count: number | string }[]>`
|
|
select seat_count
|
|
from bookings
|
|
where id = ${input.bookingId}
|
|
and deleted_at is null
|
|
limit 1
|
|
`
|
|
|
|
if (!currentBooking) {
|
|
return null
|
|
}
|
|
|
|
const currentSeatCount = parseInteger(currentBooking.seat_count)
|
|
|
|
const [row] = await tx<DbBookingRow[]>`
|
|
with updated_booking as (
|
|
update bookings
|
|
set
|
|
customer_name = ${input.customerName},
|
|
customer_phone = ${input.customerPhone},
|
|
booking_mode_id = ${input.bookingModeId},
|
|
booking_mode = ${input.bookingMode},
|
|
quantity = ${input.quantity},
|
|
seat_count = ${input.seatCount},
|
|
ticket_type_id = ${input.ticketTypeId},
|
|
ticket_type = ${input.ticketType},
|
|
unit_price = ${input.unitPrice},
|
|
total_price = ${input.totalPrice},
|
|
payment_method = ${input.paymentMethod},
|
|
remark = ${input.remark},
|
|
updated_at = now()
|
|
where id = ${input.bookingId}
|
|
and deleted_at is null
|
|
${input.personInChargeId ? tx`and person_in_charge_id = ${input.personInChargeId}` : tx``}
|
|
returning *
|
|
)
|
|
select ${bookingSelectColumns(tx)}
|
|
from updated_booking as bookings
|
|
${bookingJoins(tx)}
|
|
limit 1
|
|
`
|
|
|
|
if (!row) {
|
|
return null
|
|
}
|
|
|
|
await syncBookingSeats(tx, input.bookingId, currentSeatCount, input.seatCount)
|
|
|
|
return mapBooking(row, { includeTransactionDocument: true })
|
|
})
|
|
}
|
|
|
|
export async function getBookingTransactionDocument(input: {
|
|
bookingId: string
|
|
personInChargeId?: string
|
|
}): Promise<BookingTransactionDocumentRecord | null> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [row] = input.personInChargeId
|
|
? await sql<(DbBookingTransactionDocumentRow & { id: string })[]>`
|
|
select
|
|
id,
|
|
payment_method,
|
|
transaction_document_original_name,
|
|
transaction_document_storage_name,
|
|
transaction_document_mime_type,
|
|
transaction_document_size,
|
|
transaction_document_uploaded_at
|
|
from bookings
|
|
where id = ${input.bookingId}
|
|
and deleted_at is null
|
|
and person_in_charge_id = ${input.personInChargeId}
|
|
limit 1
|
|
`
|
|
: await sql<(DbBookingTransactionDocumentRow & { id: string })[]>`
|
|
select
|
|
id,
|
|
payment_method,
|
|
transaction_document_original_name,
|
|
transaction_document_storage_name,
|
|
transaction_document_mime_type,
|
|
transaction_document_size,
|
|
transaction_document_uploaded_at
|
|
from bookings
|
|
where id = ${input.bookingId}
|
|
and deleted_at is null
|
|
limit 1
|
|
`
|
|
|
|
return row ? mapBookingTransactionDocumentRecord(row, `/api/bookings/${row.id}/transaction-document`) : null
|
|
}
|
|
|
|
export async function getBookingTransactionDocumentByConfirmationToken(confirmationToken: string): Promise<BookingTransactionDocumentRecord | null> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [row] = await sql<(DbBookingTransactionDocumentRow & { confirmation_token: string })[]>`
|
|
select
|
|
confirmation_token,
|
|
payment_method,
|
|
transaction_document_original_name,
|
|
transaction_document_storage_name,
|
|
transaction_document_mime_type,
|
|
transaction_document_size,
|
|
transaction_document_uploaded_at
|
|
from bookings
|
|
where confirmation_token = ${confirmationToken}
|
|
and deleted_at is null
|
|
limit 1
|
|
`
|
|
|
|
return row ? mapBookingTransactionDocumentRecord(row, `/api/public/bookings/${row.confirmation_token}/transaction-document`) : null
|
|
}
|
|
|
|
export async function updateBookingPaymentMethodByConfirmationToken(input: {
|
|
confirmationToken: string
|
|
paymentMethod: PaymentMethod
|
|
}): Promise<PublicBooking | null> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [row] = await sql<DbBookingRow[]>`
|
|
with updated_booking as (
|
|
update bookings
|
|
set
|
|
payment_method = ${input.paymentMethod},
|
|
updated_at = now()
|
|
where confirmation_token = ${input.confirmationToken}
|
|
and deleted_at is null
|
|
and status = 'pending'
|
|
returning *
|
|
)
|
|
select ${bookingSelectColumns(sql)}
|
|
from updated_booking as bookings
|
|
${bookingJoins(sql)}
|
|
limit 1
|
|
`
|
|
|
|
return row
|
|
? mapBooking(row, {
|
|
includeTransactionDocument: true,
|
|
transactionDocumentUrl: `/api/public/bookings/${input.confirmationToken}/transaction-document`
|
|
})
|
|
: null
|
|
}
|
|
|
|
export async function replaceBookingTransactionDocumentByConfirmationToken(input: {
|
|
confirmationToken: string
|
|
originalName: string
|
|
storageName: string
|
|
mimeType: string
|
|
size: number
|
|
}): Promise<{ booking: PublicBooking, previousStorageName: string | null } | null> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [row] = await sql<(DbBookingRow & { previous_storage_name: string | null })[]>`
|
|
with current_booking as (
|
|
select transaction_document_storage_name
|
|
from bookings
|
|
where confirmation_token = ${input.confirmationToken}
|
|
and deleted_at is null
|
|
and status = 'pending'
|
|
limit 1
|
|
),
|
|
updated_booking as (
|
|
update bookings
|
|
set
|
|
payment_method = 'bank',
|
|
transaction_document_original_name = ${input.originalName},
|
|
transaction_document_storage_name = ${input.storageName},
|
|
transaction_document_mime_type = ${input.mimeType},
|
|
transaction_document_size = ${input.size},
|
|
transaction_document_uploaded_at = now(),
|
|
updated_at = now()
|
|
where confirmation_token = ${input.confirmationToken}
|
|
and deleted_at is null
|
|
and status = 'pending'
|
|
returning *
|
|
)
|
|
select
|
|
${bookingSelectColumns(sql)},
|
|
(select transaction_document_storage_name from current_booking) as previous_storage_name
|
|
from updated_booking as bookings
|
|
${bookingJoins(sql)}
|
|
limit 1
|
|
`
|
|
|
|
if (!row) {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
booking: mapBooking(row, {
|
|
includeTransactionDocument: true,
|
|
transactionDocumentUrl: `/api/public/bookings/${input.confirmationToken}/transaction-document`
|
|
}),
|
|
previousStorageName: row.previous_storage_name
|
|
}
|
|
}
|
|
|
|
export async function clearBookingTransactionDocumentByConfirmationToken(confirmationToken: string): Promise<{ booking: PublicBooking, previousStorageName: string | null } | null> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [row] = await sql<(DbBookingRow & { previous_storage_name: string | null })[]>`
|
|
with current_booking as (
|
|
select transaction_document_storage_name
|
|
from bookings
|
|
where confirmation_token = ${confirmationToken}
|
|
and deleted_at is null
|
|
and status = 'pending'
|
|
limit 1
|
|
),
|
|
updated_booking as (
|
|
update bookings
|
|
set
|
|
transaction_document_original_name = null,
|
|
transaction_document_storage_name = null,
|
|
transaction_document_mime_type = null,
|
|
transaction_document_size = null,
|
|
transaction_document_uploaded_at = null,
|
|
updated_at = now()
|
|
where confirmation_token = ${confirmationToken}
|
|
and deleted_at is null
|
|
and status = 'pending'
|
|
returning *
|
|
)
|
|
select
|
|
${bookingSelectColumns(sql)},
|
|
(select transaction_document_storage_name from current_booking) as previous_storage_name
|
|
from updated_booking as bookings
|
|
${bookingJoins(sql)}
|
|
limit 1
|
|
`
|
|
|
|
if (!row) {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
booking: mapBooking(row, {
|
|
includeTransactionDocument: true,
|
|
transactionDocumentUrl: `/api/public/bookings/${confirmationToken}/transaction-document`
|
|
}),
|
|
previousStorageName: row.previous_storage_name
|
|
}
|
|
}
|
|
|
|
export async function replaceBookingTransactionDocument(input: {
|
|
bookingId: string
|
|
personInChargeId?: string
|
|
originalName: string
|
|
storageName: string
|
|
mimeType: string
|
|
size: number
|
|
}): Promise<{ booking: PublicBooking, previousStorageName: string | null } | null> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [row] = await sql<(DbBookingRow & { previous_storage_name: string | null })[]>`
|
|
with current_booking as (
|
|
select transaction_document_storage_name
|
|
from bookings
|
|
where id = ${input.bookingId}
|
|
and deleted_at is null
|
|
${input.personInChargeId ? sql`and person_in_charge_id = ${input.personInChargeId}` : sql``}
|
|
limit 1
|
|
),
|
|
updated_booking as (
|
|
update bookings
|
|
set
|
|
payment_method = 'bank',
|
|
transaction_document_original_name = ${input.originalName},
|
|
transaction_document_storage_name = ${input.storageName},
|
|
transaction_document_mime_type = ${input.mimeType},
|
|
transaction_document_size = ${input.size},
|
|
transaction_document_uploaded_at = now(),
|
|
updated_at = now()
|
|
where id = ${input.bookingId}
|
|
and deleted_at is null
|
|
${input.personInChargeId ? sql`and person_in_charge_id = ${input.personInChargeId}` : sql``}
|
|
returning *
|
|
)
|
|
select
|
|
${bookingSelectColumns(sql)},
|
|
(select transaction_document_storage_name from current_booking) as previous_storage_name
|
|
from updated_booking as bookings
|
|
${bookingJoins(sql)}
|
|
limit 1
|
|
`
|
|
|
|
if (!row) {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
booking: mapBooking(row, { includeTransactionDocument: true }),
|
|
previousStorageName: row.previous_storage_name
|
|
}
|
|
}
|
|
|
|
export async function clearBookingTransactionDocument(input: {
|
|
bookingId: string
|
|
personInChargeId?: string
|
|
}): Promise<{ booking: PublicBooking, previousStorageName: string | null } | null> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [row] = await sql<(DbBookingRow & { previous_storage_name: string | null })[]>`
|
|
with current_booking as (
|
|
select transaction_document_storage_name
|
|
from bookings
|
|
where id = ${input.bookingId}
|
|
and deleted_at is null
|
|
${input.personInChargeId ? sql`and person_in_charge_id = ${input.personInChargeId}` : sql``}
|
|
limit 1
|
|
),
|
|
updated_booking as (
|
|
update bookings
|
|
set
|
|
transaction_document_original_name = null,
|
|
transaction_document_storage_name = null,
|
|
transaction_document_mime_type = null,
|
|
transaction_document_size = null,
|
|
transaction_document_uploaded_at = null,
|
|
updated_at = now()
|
|
where id = ${input.bookingId}
|
|
and deleted_at is null
|
|
${input.personInChargeId ? sql`and person_in_charge_id = ${input.personInChargeId}` : sql``}
|
|
returning *
|
|
)
|
|
select
|
|
${bookingSelectColumns(sql)},
|
|
(select transaction_document_storage_name from current_booking) as previous_storage_name
|
|
from updated_booking as bookings
|
|
${bookingJoins(sql)}
|
|
limit 1
|
|
`
|
|
|
|
if (!row) {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
booking: mapBooking(row, { includeTransactionDocument: true }),
|
|
previousStorageName: row.previous_storage_name
|
|
}
|
|
}
|
|
|
|
export async function softDeleteBooking(input: {
|
|
bookingId: string
|
|
personInChargeId?: string
|
|
}) {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const rows = input.personInChargeId
|
|
? await sql<DbBookingRow[]>`
|
|
with updated_booking as (
|
|
update bookings
|
|
set
|
|
deleted_at = now(),
|
|
updated_at = now()
|
|
where id = ${input.bookingId}
|
|
and deleted_at is null
|
|
and person_in_charge_id = ${input.personInChargeId}
|
|
returning *
|
|
)
|
|
select ${bookingSelectColumns(sql)}
|
|
from updated_booking as bookings
|
|
${bookingJoins(sql)}
|
|
limit 1
|
|
`
|
|
: await sql<DbBookingRow[]>`
|
|
with updated_booking as (
|
|
update bookings
|
|
set
|
|
deleted_at = now(),
|
|
updated_at = now()
|
|
where id = ${input.bookingId}
|
|
and deleted_at is null
|
|
returning *
|
|
)
|
|
select ${bookingSelectColumns(sql)}
|
|
from updated_booking as bookings
|
|
${bookingJoins(sql)}
|
|
limit 1
|
|
`
|
|
|
|
return rows[0] ? mapBooking(rows[0], { includeTransactionDocument: true }) : null
|
|
}
|
|
|
|
export async function listBookingSeats(bookingId: string): Promise<PublicBookingSeat[]> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const rows = await sql<DbBookingSeatRow[]>`
|
|
select
|
|
id,
|
|
seat_number,
|
|
seat_token,
|
|
recipient_name,
|
|
recipient_phone,
|
|
shared_at,
|
|
created_at,
|
|
updated_at
|
|
from booking_seats
|
|
where booking_id = ${bookingId}
|
|
and exists (
|
|
select 1
|
|
from bookings
|
|
where bookings.id = booking_seats.booking_id
|
|
and bookings.deleted_at is null
|
|
)
|
|
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: mapPublicBookingToReceiptBooking(booking),
|
|
seats
|
|
}
|
|
}
|
|
|
|
export async function getSeatReceiptBySeatToken(seatToken: string): Promise<{
|
|
booking: ReceiptBooking
|
|
seat: PublicBookingSeat
|
|
} | null> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [row] = await sql<DbBookingSeatWithBookingRow[]>`
|
|
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,
|
|
dinner_events.id as event_id,
|
|
dinner_events.title as event_title,
|
|
dinner_events.date_label as event_date_label,
|
|
dinner_events.time_label as event_time_label,
|
|
dinner_events.venue as event_venue,
|
|
bookings.customer_name,
|
|
bookings.customer_phone,
|
|
bookings.locale,
|
|
bookings.deleted_at,
|
|
bookings.booking_mode_id,
|
|
coalesce(booking_modes.code, bookings.booking_mode) as booking_mode,
|
|
booking_modes.label as booking_mode_label,
|
|
booking_modes.seats_per_unit as booking_mode_seats_per_unit,
|
|
bookings.quantity,
|
|
bookings.seat_count,
|
|
bookings.ticket_type_id,
|
|
coalesce(ticket_types.code, bookings.ticket_type) as ticket_type,
|
|
ticket_types.label as ticket_label,
|
|
ticket_types.description as ticket_description,
|
|
bookings.unit_price,
|
|
bookings.total_price,
|
|
bookings.person_in_charge_id,
|
|
coalesce(users.full_name, bookings.person_in_charge_name) as person_in_charge_name,
|
|
coalesce(users.phone_number, bookings.person_in_charge_phone_number) as person_in_charge_phone_number,
|
|
bookings.status,
|
|
booking_statuses.label as status_label,
|
|
bookings.created_at as booking_created_at,
|
|
bookings.confirmed_at
|
|
from booking_seats
|
|
inner join bookings on bookings.id = booking_seats.booking_id
|
|
${bookingJoins(sql)}
|
|
where booking_seats.seat_token = ${seatToken}
|
|
and bookings.deleted_at is null
|
|
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<PublicBookingSeat | null> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
const nextSeatToken = input.shared ? null : randomToken(24)
|
|
|
|
const [row] = await sql<DbBookingSeatRow[]>`
|
|
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 bookings.deleted_at is null
|
|
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<BookingCapacitySettings> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [row] = await sql<DbBookingSettingsRow[]>`
|
|
select
|
|
booking_settings.event_id,
|
|
total_tables,
|
|
total_seats,
|
|
booking_settings.updated_at
|
|
from booking_settings
|
|
inner join dinner_events on dinner_events.id = booking_settings.event_id
|
|
where dinner_events.is_active = true
|
|
order by dinner_events.sort_order asc
|
|
limit 1
|
|
`
|
|
|
|
return mapBookingCapacitySettings(row)
|
|
}
|
|
|
|
export async function updateBookingCapacitySettings(input: {
|
|
totalSeats: number | null
|
|
}): Promise<BookingCapacitySettings> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [row] = await sql<DbBookingSettingsRow[]>`
|
|
update booking_settings
|
|
set
|
|
total_seats = ${input.totalSeats},
|
|
updated_at = now()
|
|
from dinner_events
|
|
where booking_settings.event_id = dinner_events.id
|
|
and dinner_events.is_active = true
|
|
returning
|
|
booking_settings.event_id,
|
|
total_tables,
|
|
total_seats,
|
|
booking_settings.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[]>`
|
|
with updated_booking as (
|
|
update bookings
|
|
set
|
|
status = 'confirmed',
|
|
confirmed_at = now(),
|
|
updated_at = now()
|
|
where confirmation_token = ${confirmationToken}
|
|
and deleted_at is null
|
|
and status = 'pending'
|
|
returning *
|
|
)
|
|
select ${bookingSelectColumns(sql)}
|
|
from updated_booking as bookings
|
|
${bookingJoins(sql)}
|
|
`
|
|
|
|
if (row) {
|
|
return mapBooking(row)
|
|
}
|
|
|
|
return await getBookingByConfirmationToken(confirmationToken)
|
|
}
|
|
|
|
export async function cancelBookingConfirmationByConfirmationToken(confirmationToken: string): Promise<PublicBooking | null> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [row] = await sql<DbBookingRow[]>`
|
|
with updated_booking as (
|
|
update bookings
|
|
set
|
|
status = 'pending',
|
|
confirmed_at = null,
|
|
updated_at = now()
|
|
where confirmation_token = ${confirmationToken}
|
|
and deleted_at is null
|
|
and status = 'confirmed'
|
|
returning *
|
|
)
|
|
select ${bookingSelectColumns(sql)}
|
|
from updated_booking as bookings
|
|
${bookingJoins(sql)}
|
|
`
|
|
|
|
if (row) {
|
|
return mapBooking(row)
|
|
}
|
|
|
|
return await getBookingByConfirmationToken(confirmationToken)
|
|
}
|