feat(booking): move event and ticket configuration to database
Replace hardcoded event details and ticket types with dynamic DB records Add booking-config API endpoint to serve active event settings
This commit is contained in:
@@ -1,17 +1,21 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import type {
|
||||
BookingModeOption,
|
||||
BookingCapacitySettings,
|
||||
BookingInventorySummary,
|
||||
DinnerEvent,
|
||||
BookingMode,
|
||||
BookingStatus,
|
||||
PublicBookingConfig,
|
||||
PublicBooking,
|
||||
PublicBookingSeat,
|
||||
ReceiptBooking,
|
||||
TicketCatalogItem,
|
||||
TicketType
|
||||
} from '~~/shared/booking'
|
||||
|
||||
import { calculateBookingInventorySummary, isBookingMode, isBookingStatus } from '~~/shared/booking'
|
||||
import { calculateBookingInventorySummary, getBookingStatusLabel, isBookingStatus } from '~~/shared/booking'
|
||||
|
||||
import { randomToken, toIsoString } from './base64url'
|
||||
import { ensureDatabaseReady } from './db-init'
|
||||
@@ -21,18 +25,30 @@ 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
|
||||
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: TicketType
|
||||
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
|
||||
person_in_charge_phone_number: string
|
||||
person_in_charge_name: string | null
|
||||
person_in_charge_phone_number: string | null
|
||||
status: BookingStatus | string
|
||||
status_label: string | null
|
||||
created_at: Date | string
|
||||
confirmed_at: Date | string | null
|
||||
}
|
||||
@@ -48,56 +64,175 @@ type DbBookingSeatRow = {
|
||||
updated_at: Date | string
|
||||
}
|
||||
|
||||
type DbBookingSeatWithBookingRow = DbBookingSeatRow & {
|
||||
type DbBookingSeatWithBookingRow = DbBookingSeatRow & Omit<DbBookingRow, 'id' | 'created_at'> & {
|
||||
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 = {
|
||||
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.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,
|
||||
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 normalizeBookingMode(value: string): BookingMode {
|
||||
return isBookingMode(value) ? value : 'seat'
|
||||
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 mapBooking(row: DbBookingRow): 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
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
confirmationToken: row.confirmation_token,
|
||||
receiptToken: row.receipt_token,
|
||||
event: mapDinnerEventFromBooking(row),
|
||||
customerName: row.customer_name,
|
||||
customerPhone: row.customer_phone,
|
||||
bookingMode: normalizeBookingMode(row.booking_mode),
|
||||
bookingModeId: row.booking_mode_id,
|
||||
bookingMode,
|
||||
bookingModeLabel: row.booking_mode_label || bookingMode,
|
||||
quantity: parseInteger(row.quantity),
|
||||
seatCount,
|
||||
ticketType: row.ticket_type,
|
||||
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,
|
||||
status: isBookingStatus(row.status) ? row.status : 'pending',
|
||||
personInChargeName: row.person_in_charge_name || '',
|
||||
personInChargePhoneNumber: row.person_in_charge_phone_number || '',
|
||||
status,
|
||||
statusLabel: row.status_label || getBookingStatusLabel(status),
|
||||
createdAt: toIsoString(row.created_at) ?? new Date().toISOString(),
|
||||
confirmedAt: toIsoString(row.confirmed_at)
|
||||
}
|
||||
@@ -105,24 +240,59 @@ function mapBooking(row: DbBookingRow): PublicBooking {
|
||||
|
||||
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,
|
||||
bookingMode: normalizeBookingMode(row.booking_mode),
|
||||
bookingModeId: row.booking_mode_id,
|
||||
bookingMode,
|
||||
bookingModeLabel: row.booking_mode_label || bookingMode,
|
||||
quantity: parseInteger(row.quantity),
|
||||
seatCount,
|
||||
ticketType: row.ticket_type,
|
||||
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: isBookingStatus(row.status) ? row.status : 'pending',
|
||||
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,
|
||||
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,
|
||||
@@ -144,9 +314,7 @@ function mapBookingCapacitySettings(row: DbBookingSettingsRow | undefined): Book
|
||||
}
|
||||
}
|
||||
|
||||
const totalSeats = row.total_seats === null
|
||||
? (row.total_tables === null ? null : parseInteger(row.total_tables) * 10)
|
||||
: parseInteger(row.total_seats)
|
||||
const totalSeats = row.total_seats === null ? null : parseInteger(row.total_seats)
|
||||
|
||||
return {
|
||||
totalSeats,
|
||||
@@ -154,6 +322,115 @@ function mapBookingCapacitySettings(row: DbBookingSettingsRow | undefined): Book
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -178,17 +455,18 @@ async function insertBookingSeats(
|
||||
}
|
||||
|
||||
export async function createBooking(input: {
|
||||
eventId: string
|
||||
customerName: string
|
||||
customerPhone: string
|
||||
bookingModeId: string
|
||||
bookingMode: BookingMode
|
||||
quantity: number
|
||||
seatCount: number
|
||||
ticketTypeId: string
|
||||
ticketType: TicketType
|
||||
unitPrice: number
|
||||
totalPrice: number
|
||||
personInChargeId: string
|
||||
personInChargeName: string
|
||||
personInChargePhoneNumber: string
|
||||
}) {
|
||||
await ensureDatabaseReady()
|
||||
const sql = getSqlClient()
|
||||
@@ -198,58 +476,48 @@ export async function createBooking(input: {
|
||||
|
||||
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,
|
||||
booking_mode_id,
|
||||
booking_mode,
|
||||
quantity,
|
||||
seat_count,
|
||||
ticket_type_id,
|
||||
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.eventId},
|
||||
${input.customerName},
|
||||
${input.customerPhone},
|
||||
${input.bookingModeId},
|
||||
${input.bookingMode},
|
||||
${input.quantity},
|
||||
${input.seatCount},
|
||||
${input.ticketTypeId},
|
||||
${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
|
||||
returning *
|
||||
)
|
||||
select ${bookingSelectColumns(tx)}
|
||||
from inserted_booking as bookings
|
||||
${bookingJoins(tx)}
|
||||
`
|
||||
|
||||
await insertBookingSeats(tx, bookingId, input.seatCount)
|
||||
@@ -269,26 +537,10 @@ export async function getBookingByConfirmationToken(confirmationToken: string):
|
||||
const sql = getSqlClient()
|
||||
|
||||
const [row] = await sql<DbBookingRow[]>`
|
||||
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
|
||||
select ${bookingSelectColumns(sql)}
|
||||
from bookings
|
||||
where confirmation_token = ${confirmationToken}
|
||||
${bookingJoins(sql)}
|
||||
where bookings.confirmation_token = ${confirmationToken}
|
||||
limit 1
|
||||
`
|
||||
|
||||
@@ -300,26 +552,10 @@ export async function getBookingByReceiptToken(receiptToken: string): Promise<Pu
|
||||
const sql = getSqlClient()
|
||||
|
||||
const [row] = await sql<DbBookingRow[]>`
|
||||
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
|
||||
select ${bookingSelectColumns(sql)}
|
||||
from bookings
|
||||
where receipt_token = ${receiptToken}
|
||||
${bookingJoins(sql)}
|
||||
where bookings.receipt_token = ${receiptToken}
|
||||
limit 1
|
||||
`
|
||||
|
||||
@@ -334,49 +570,19 @@ export async function listBookings(options?: {
|
||||
|
||||
const rows = options?.personInChargeId
|
||||
? await sql<DbBookingRow[]>`
|
||||
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
|
||||
select ${bookingSelectColumns(sql)}
|
||||
from bookings
|
||||
where person_in_charge_id = ${options.personInChargeId}
|
||||
order by created_at desc
|
||||
${bookingJoins(sql)}
|
||||
where dinner_events.is_active = true
|
||||
and bookings.person_in_charge_id = ${options.personInChargeId}
|
||||
order by bookings.created_at desc
|
||||
`
|
||||
: await sql<DbBookingRow[]>`
|
||||
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
|
||||
select ${bookingSelectColumns(sql)}
|
||||
from bookings
|
||||
order by created_at desc
|
||||
${bookingJoins(sql)}
|
||||
where dinner_events.is_active = true
|
||||
order by bookings.created_at desc
|
||||
`
|
||||
|
||||
return rows.map(mapBooking)
|
||||
@@ -417,25 +623,7 @@ export async function getBookingReceiptByReceiptToken(receiptToken: string): Pro
|
||||
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
|
||||
}),
|
||||
booking: mapPublicBookingToReceiptBooking(booking),
|
||||
seats
|
||||
}
|
||||
}
|
||||
@@ -460,19 +648,35 @@ export async function getSeatReceiptBySeatToken(seatToken: string): Promise<{
|
||||
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.booking_mode,
|
||||
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,
|
||||
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}
|
||||
limit 1
|
||||
`
|
||||
@@ -533,11 +737,14 @@ export async function getBookingCapacitySettings(): Promise<BookingCapacitySetti
|
||||
|
||||
const [row] = await sql<DbBookingSettingsRow[]>`
|
||||
select
|
||||
booking_settings.event_id,
|
||||
total_tables,
|
||||
total_seats,
|
||||
updated_at
|
||||
booking_settings.updated_at
|
||||
from booking_settings
|
||||
where id = 'default'
|
||||
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
|
||||
`
|
||||
|
||||
@@ -555,11 +762,14 @@ export async function updateBookingCapacitySettings(input: {
|
||||
set
|
||||
total_seats = ${input.totalSeats},
|
||||
updated_at = now()
|
||||
where id = 'default'
|
||||
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,
|
||||
updated_at
|
||||
booking_settings.updated_at
|
||||
`
|
||||
|
||||
return mapBookingCapacitySettings(row)
|
||||
@@ -579,31 +789,19 @@ export async function confirmBookingByConfirmationToken(confirmationToken: strin
|
||||
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,
|
||||
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
|
||||
with updated_booking as (
|
||||
update bookings
|
||||
set
|
||||
status = 'confirmed',
|
||||
confirmed_at = now(),
|
||||
updated_at = now()
|
||||
where confirmation_token = ${confirmationToken}
|
||||
and status = 'pending'
|
||||
returning *
|
||||
)
|
||||
select ${bookingSelectColumns(sql)}
|
||||
from updated_booking as bookings
|
||||
${bookingJoins(sql)}
|
||||
`
|
||||
|
||||
if (row) {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import type { BookingCapacitySettings, BookingMode, PublicBooking, TicketType } from '~~/shared/booking'
|
||||
|
||||
import {
|
||||
formatBookingCurrency,
|
||||
getTicketCatalogItem,
|
||||
isBookingMode,
|
||||
isTicketType
|
||||
formatBookingCurrency
|
||||
} from '~~/shared/booking'
|
||||
import { hasValidFullName, isValidPhoneNumber, normalizeFullName, normalizePhoneNumber } from '~~/shared/auth'
|
||||
|
||||
@@ -21,15 +18,15 @@ export function parseCreateBookingInput(body: {
|
||||
const customerName = normalizeFullName(body.customerName || '')
|
||||
const customerPhone = normalizePhoneNumber(body.customerPhone || '')
|
||||
const bookingMode = typeof body.bookingMode === 'string' ? body.bookingMode.trim().toLowerCase() : body.bookingMode
|
||||
const ticketType = body.ticketType
|
||||
const ticketType = typeof body.ticketType === 'string' ? body.ticketType.trim().toLowerCase() : body.ticketType
|
||||
const quantity = Number(body.quantity)
|
||||
const personInChargeId = (body.personInChargeId || '').trim()
|
||||
|
||||
assertBadRequest(hasValidFullName(customerName), 'Guest or organizer name must be at least 2 characters')
|
||||
assertBadRequest(isValidPhoneNumber(customerPhone), 'Phone number must include a country code, e.g. +60123456789')
|
||||
assertBadRequest(isBookingMode(bookingMode), 'Booking mode is invalid')
|
||||
assertBadRequest(typeof bookingMode === 'string' && bookingMode.length > 0, 'Booking mode is required')
|
||||
assertBadRequest(Number.isInteger(quantity) && quantity >= 1, 'Quantity must be a whole number of at least 1')
|
||||
assertBadRequest(isTicketType(ticketType), 'Ticket category is invalid')
|
||||
assertBadRequest(typeof ticketType === 'string' && ticketType.trim().length > 0, 'Ticket category is required')
|
||||
assertBadRequest(personInChargeId, 'Person in charge is required')
|
||||
|
||||
return {
|
||||
@@ -43,16 +40,13 @@ export function parseCreateBookingInput(body: {
|
||||
}
|
||||
|
||||
export function buildBookingMessage(booking: PublicBooking, confirmationUrl: string) {
|
||||
const ticket = getTicketCatalogItem(booking.ticketType)
|
||||
const ticketLabel = ticket?.label || booking.ticketType.toUpperCase()
|
||||
|
||||
return [
|
||||
"I'd like to book tickets for the DAP Johor 60th Anniversary Celebration.",
|
||||
`I'd like to book tickets for the ${booking.event.title}.`,
|
||||
'',
|
||||
`Name: ${booking.customerName}`,
|
||||
`Phone Number: ${booking.customerPhone}`,
|
||||
`Seats: ${booking.seatCount}`,
|
||||
`Ticket Category: ${ticketLabel}`,
|
||||
`Ticket Category: ${booking.ticketLabel || booking.ticketType.toUpperCase()}`,
|
||||
`Total Price: ${formatBookingCurrency(booking.totalPrice)}`,
|
||||
'',
|
||||
'PIC confirmation link:',
|
||||
|
||||
@@ -62,23 +62,205 @@ async function initializeDatabase() {
|
||||
on user_passkeys (user_id)
|
||||
`
|
||||
|
||||
await sql`
|
||||
create table if not exists dinner_events (
|
||||
id text primary key,
|
||||
title text not null,
|
||||
date_label text not null,
|
||||
time_label text not null,
|
||||
venue text not null,
|
||||
is_active boolean not null default false,
|
||||
sort_order integer not null default 0,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
)
|
||||
`
|
||||
|
||||
await sql`
|
||||
create unique index if not exists dinner_events_single_active_idx
|
||||
on dinner_events (is_active)
|
||||
where is_active = true
|
||||
`
|
||||
|
||||
await sql`
|
||||
insert into dinner_events (
|
||||
id,
|
||||
title,
|
||||
date_label,
|
||||
time_label,
|
||||
venue,
|
||||
is_active,
|
||||
sort_order
|
||||
)
|
||||
values (
|
||||
'dap-johor-60',
|
||||
'DAP JOHOR 60th Anniversary Celebration',
|
||||
'Saturday, 30 May 2026',
|
||||
'6:30 PM',
|
||||
'Yong Peng''s Chee Ann Kor',
|
||||
true,
|
||||
1
|
||||
)
|
||||
on conflict (id) do nothing
|
||||
`
|
||||
|
||||
await sql`
|
||||
update dinner_events
|
||||
set
|
||||
is_active = true,
|
||||
updated_at = now()
|
||||
where id = 'dap-johor-60'
|
||||
and not exists (
|
||||
select 1
|
||||
from dinner_events
|
||||
where is_active = true
|
||||
)
|
||||
`
|
||||
|
||||
await sql`
|
||||
create table if not exists booking_modes (
|
||||
id text primary key,
|
||||
event_id text not null references dinner_events(id) on delete cascade,
|
||||
code text not null,
|
||||
label text not null,
|
||||
quantity_label text not null,
|
||||
seats_per_unit integer not null check (seats_per_unit >= 1),
|
||||
is_active boolean not null default true,
|
||||
sort_order integer not null default 0,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
unique (event_id, code)
|
||||
)
|
||||
`
|
||||
|
||||
await sql`
|
||||
insert into booking_modes (
|
||||
id,
|
||||
event_id,
|
||||
code,
|
||||
label,
|
||||
quantity_label,
|
||||
seats_per_unit,
|
||||
is_active,
|
||||
sort_order
|
||||
)
|
||||
values
|
||||
(
|
||||
'dap-johor-60-table',
|
||||
'dap-johor-60',
|
||||
'table',
|
||||
'Table (10 seats)',
|
||||
'Number of Tables',
|
||||
10,
|
||||
true,
|
||||
1
|
||||
),
|
||||
(
|
||||
'dap-johor-60-seat',
|
||||
'dap-johor-60',
|
||||
'seat',
|
||||
'Seat',
|
||||
'Number of Seats',
|
||||
1,
|
||||
true,
|
||||
2
|
||||
)
|
||||
on conflict (event_id, code) do nothing
|
||||
`
|
||||
|
||||
await sql`
|
||||
create table if not exists ticket_types (
|
||||
id text primary key,
|
||||
event_id text not null references dinner_events(id) on delete cascade,
|
||||
code text not null,
|
||||
label text not null,
|
||||
description text not null,
|
||||
price integer not null check (price >= 0),
|
||||
is_active boolean not null default true,
|
||||
sort_order integer not null default 0,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
unique (event_id, code)
|
||||
)
|
||||
`
|
||||
|
||||
await sql`
|
||||
insert into ticket_types (
|
||||
id,
|
||||
event_id,
|
||||
code,
|
||||
label,
|
||||
description,
|
||||
price,
|
||||
is_active,
|
||||
sort_order
|
||||
)
|
||||
values
|
||||
(
|
||||
'dap-johor-60-vip',
|
||||
'dap-johor-60',
|
||||
'vip',
|
||||
'VIP',
|
||||
'RM150 / seat',
|
||||
150,
|
||||
true,
|
||||
1
|
||||
),
|
||||
(
|
||||
'dap-johor-60-supporter',
|
||||
'dap-johor-60',
|
||||
'supporter',
|
||||
'Supporter',
|
||||
'RM60 / seat',
|
||||
60,
|
||||
true,
|
||||
2
|
||||
)
|
||||
on conflict (event_id, code) do nothing
|
||||
`
|
||||
|
||||
await sql`
|
||||
create table if not exists booking_statuses (
|
||||
code text primary key,
|
||||
label text not null,
|
||||
sort_order integer not null default 0,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
)
|
||||
`
|
||||
|
||||
await sql`
|
||||
insert into booking_statuses (
|
||||
code,
|
||||
label,
|
||||
sort_order
|
||||
)
|
||||
values
|
||||
('pending', 'Pending PIC confirmation', 1),
|
||||
('confirmed', 'Confirmed', 2)
|
||||
on conflict (code) do nothing
|
||||
`
|
||||
|
||||
await sql`
|
||||
create table if not exists bookings (
|
||||
id text primary key,
|
||||
confirmation_token text not null unique,
|
||||
receipt_token text not null unique,
|
||||
event_id text references dinner_events(id) on delete restrict,
|
||||
customer_name text not null,
|
||||
customer_phone text not null,
|
||||
booking_mode text not null check (booking_mode in ('table', 'seat')),
|
||||
booking_mode_id text references booking_modes(id) on delete restrict,
|
||||
booking_mode text not null,
|
||||
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')),
|
||||
ticket_type_id text references ticket_types(id) on delete restrict,
|
||||
ticket_type text not null,
|
||||
unit_price integer not null check (unit_price >= 0),
|
||||
total_price integer not null check (total_price >= 0),
|
||||
person_in_charge_id text not null references users(id) on delete restrict,
|
||||
person_in_charge_name text not null,
|
||||
person_in_charge_phone_number text not null,
|
||||
status text not null check (status in ('pending', 'confirmed')) default 'pending',
|
||||
status text not null default 'pending',
|
||||
confirmed_at timestamptz,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
@@ -90,11 +272,41 @@ async function initializeDatabase() {
|
||||
add column if not exists receipt_token text
|
||||
`
|
||||
|
||||
await sql`
|
||||
alter table bookings
|
||||
add column if not exists event_id text
|
||||
`
|
||||
|
||||
await sql`
|
||||
alter table bookings
|
||||
add column if not exists booking_mode_id text
|
||||
`
|
||||
|
||||
await sql`
|
||||
alter table bookings
|
||||
add column if not exists ticket_type_id text
|
||||
`
|
||||
|
||||
await sql`
|
||||
create unique index if not exists bookings_receipt_token_idx
|
||||
on bookings (receipt_token)
|
||||
`
|
||||
|
||||
await sql`
|
||||
create index if not exists bookings_event_id_idx
|
||||
on bookings (event_id)
|
||||
`
|
||||
|
||||
await sql`
|
||||
create index if not exists bookings_booking_mode_id_idx
|
||||
on bookings (booking_mode_id)
|
||||
`
|
||||
|
||||
await sql`
|
||||
create index if not exists bookings_ticket_type_id_idx
|
||||
on bookings (ticket_type_id)
|
||||
`
|
||||
|
||||
await sql`
|
||||
create table if not exists booking_seats (
|
||||
id text primary key,
|
||||
@@ -118,21 +330,28 @@ async function initializeDatabase() {
|
||||
await sql`
|
||||
create table if not exists booking_settings (
|
||||
id text primary key,
|
||||
event_id text references dinner_events(id) on delete cascade,
|
||||
total_tables integer,
|
||||
total_seats integer,
|
||||
updated_at timestamptz not null default now()
|
||||
)
|
||||
`
|
||||
|
||||
await sql`
|
||||
alter table booking_settings
|
||||
add column if not exists event_id text
|
||||
`
|
||||
|
||||
await sql`
|
||||
alter table booking_settings
|
||||
add column if not exists total_seats integer
|
||||
`
|
||||
|
||||
await sql`
|
||||
insert into booking_settings (id)
|
||||
values ('default')
|
||||
on conflict (id) do nothing
|
||||
insert into booking_settings (id, event_id)
|
||||
values ('default', 'dap-johor-60')
|
||||
on conflict (id) do update
|
||||
set event_id = coalesce(booking_settings.event_id, excluded.event_id)
|
||||
`
|
||||
|
||||
const bookingsMissingReceiptTokens = await sql<{ id: string }[]>`
|
||||
@@ -156,12 +375,6 @@ async function initializeDatabase() {
|
||||
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
|
||||
@@ -177,14 +390,203 @@ async function initializeDatabase() {
|
||||
|
||||
await sql`
|
||||
alter table bookings
|
||||
add constraint bookings_booking_mode_check
|
||||
check (booking_mode in ('table', 'seat'))
|
||||
drop constraint if exists bookings_ticket_type_check
|
||||
`
|
||||
|
||||
await sql`
|
||||
alter table bookings
|
||||
drop constraint if exists bookings_status_check
|
||||
`
|
||||
|
||||
const [activeEvent] = await sql<{ id: string }[]>`
|
||||
select id
|
||||
from dinner_events
|
||||
where is_active = true
|
||||
order by sort_order asc, created_at asc
|
||||
limit 1
|
||||
`
|
||||
|
||||
if (activeEvent) {
|
||||
await sql`
|
||||
update booking_settings
|
||||
set
|
||||
event_id = ${activeEvent.id},
|
||||
updated_at = now()
|
||||
where event_id is null
|
||||
`
|
||||
|
||||
await sql`
|
||||
update bookings
|
||||
set
|
||||
event_id = ${activeEvent.id},
|
||||
updated_at = now()
|
||||
where event_id is null
|
||||
`
|
||||
|
||||
await sql`
|
||||
update bookings
|
||||
set
|
||||
booking_mode_id = booking_modes.id,
|
||||
updated_at = now()
|
||||
from booking_modes
|
||||
where bookings.booking_mode_id is null
|
||||
and booking_modes.event_id = bookings.event_id
|
||||
and booking_modes.code = bookings.booking_mode
|
||||
`
|
||||
|
||||
await sql`
|
||||
update bookings
|
||||
set
|
||||
ticket_type_id = ticket_types.id,
|
||||
updated_at = now()
|
||||
from ticket_types
|
||||
where bookings.ticket_type_id is null
|
||||
and ticket_types.event_id = bookings.event_id
|
||||
and ticket_types.code = bookings.ticket_type
|
||||
`
|
||||
|
||||
const [fallbackBookingMode] = await sql<{ id: string, code: string }[]>`
|
||||
select id, code
|
||||
from booking_modes
|
||||
where event_id = ${activeEvent.id}
|
||||
and is_active = true
|
||||
order by sort_order asc, created_at asc
|
||||
limit 1
|
||||
`
|
||||
|
||||
if (fallbackBookingMode) {
|
||||
await sql`
|
||||
update bookings
|
||||
set
|
||||
booking_mode_id = ${fallbackBookingMode.id},
|
||||
booking_mode = ${fallbackBookingMode.code},
|
||||
updated_at = now()
|
||||
where booking_mode_id is null
|
||||
`
|
||||
}
|
||||
|
||||
const [fallbackTicketType] = await sql<{ id: string, code: string }[]>`
|
||||
select id, code
|
||||
from ticket_types
|
||||
where event_id = ${activeEvent.id}
|
||||
and is_active = true
|
||||
order by sort_order asc, created_at asc
|
||||
limit 1
|
||||
`
|
||||
|
||||
if (fallbackTicketType) {
|
||||
await sql`
|
||||
update bookings
|
||||
set
|
||||
ticket_type_id = ${fallbackTicketType.id},
|
||||
ticket_type = ${fallbackTicketType.code},
|
||||
updated_at = now()
|
||||
where ticket_type_id is null
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
await sql`
|
||||
create unique index if not exists booking_settings_event_id_idx
|
||||
on booking_settings (event_id)
|
||||
`
|
||||
|
||||
await sql`
|
||||
alter table bookings
|
||||
alter column person_in_charge_name drop not null
|
||||
`
|
||||
|
||||
await sql`
|
||||
alter table bookings
|
||||
alter column person_in_charge_phone_number drop not null
|
||||
`
|
||||
|
||||
await sql`
|
||||
alter table bookings
|
||||
alter column event_id set not null
|
||||
`
|
||||
|
||||
await sql`
|
||||
alter table bookings
|
||||
alter column booking_mode_id set not null
|
||||
`
|
||||
|
||||
await sql`
|
||||
alter table bookings
|
||||
alter column ticket_type_id set not null
|
||||
`
|
||||
|
||||
await sql`
|
||||
alter table booking_settings
|
||||
alter column event_id set not null
|
||||
`
|
||||
|
||||
await sql`
|
||||
alter table bookings
|
||||
drop constraint if exists bookings_event_id_fkey
|
||||
`
|
||||
|
||||
await sql`
|
||||
alter table bookings
|
||||
add constraint bookings_event_id_fkey
|
||||
foreign key (event_id) references dinner_events(id) on delete restrict
|
||||
`
|
||||
|
||||
await sql`
|
||||
alter table bookings
|
||||
drop constraint if exists bookings_booking_mode_id_fkey
|
||||
`
|
||||
|
||||
await sql`
|
||||
alter table bookings
|
||||
add constraint bookings_booking_mode_id_fkey
|
||||
foreign key (booking_mode_id) references booking_modes(id) on delete restrict
|
||||
`
|
||||
|
||||
await sql`
|
||||
alter table bookings
|
||||
drop constraint if exists bookings_ticket_type_id_fkey
|
||||
`
|
||||
|
||||
await sql`
|
||||
alter table bookings
|
||||
add constraint bookings_ticket_type_id_fkey
|
||||
foreign key (ticket_type_id) references ticket_types(id) on delete restrict
|
||||
`
|
||||
|
||||
await sql`
|
||||
alter table bookings
|
||||
drop constraint if exists bookings_status_fkey
|
||||
`
|
||||
|
||||
await sql`
|
||||
alter table bookings
|
||||
add constraint bookings_status_fkey
|
||||
foreign key (status) references booking_statuses(code) on delete restrict
|
||||
`
|
||||
|
||||
await sql`
|
||||
alter table booking_settings
|
||||
drop constraint if exists booking_settings_event_id_fkey
|
||||
`
|
||||
|
||||
await sql`
|
||||
alter table booking_settings
|
||||
add constraint booking_settings_event_id_fkey
|
||||
foreign key (event_id) references dinner_events(id) on delete cascade
|
||||
`
|
||||
|
||||
await sql`
|
||||
update booking_settings
|
||||
set
|
||||
total_seats = total_tables * 10,
|
||||
total_seats = total_tables * coalesce((
|
||||
select booking_modes.seats_per_unit
|
||||
from booking_modes
|
||||
where booking_modes.event_id = booking_settings.event_id
|
||||
and booking_modes.code = 'table'
|
||||
order by booking_modes.sort_order asc
|
||||
limit 1
|
||||
), 1),
|
||||
updated_at = now()
|
||||
where total_seats is null
|
||||
and total_tables is not null
|
||||
|
||||
@@ -9,7 +9,8 @@ export function getSqlClient() {
|
||||
sqlClient = postgres(
|
||||
config.databaseUrl || 'postgresql://postgres:postgres@127.0.0.1:5432/dinner_ticket_system',
|
||||
{
|
||||
max: 10
|
||||
max: 10,
|
||||
onnotice: false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,12 +3,7 @@ import type { H3Event } from 'h3'
|
||||
import type { PublicBooking, WhatsAppDeliveryResult } from '~~/shared/booking'
|
||||
|
||||
import {
|
||||
DINNER_EVENT_DATE_LABEL,
|
||||
DINNER_EVENT_TIME_LABEL,
|
||||
DINNER_EVENT_TITLE,
|
||||
DINNER_EVENT_VENUE,
|
||||
formatBookingCurrency,
|
||||
getTicketCatalogItem
|
||||
formatBookingCurrency
|
||||
} from '~~/shared/booking'
|
||||
import { normalizePhoneNumber } from '~~/shared/auth'
|
||||
|
||||
@@ -29,22 +24,20 @@ export function buildWhatsAppDeepLink(phoneNumber: string, message: string) {
|
||||
}
|
||||
|
||||
export function buildBookingTicketReceiptMessage(event: H3Event, booking: PublicBooking) {
|
||||
const ticket = getTicketCatalogItem(booking.ticketType)
|
||||
const ticketLabel = ticket?.label || booking.ticketType.toUpperCase()
|
||||
const receiptUrl = buildAppUrl(event, `/receipt/${booking.receiptToken}`)
|
||||
|
||||
return [
|
||||
DINNER_EVENT_TITLE,
|
||||
booking.event.title,
|
||||
'',
|
||||
`Hi ${booking.customerName}, your ticket receipt has been confirmed.`,
|
||||
'',
|
||||
`Receipt: ${receiptUrl}`,
|
||||
`Seats: ${booking.seatCount}`,
|
||||
`Ticket Category: ${ticketLabel}`,
|
||||
`Ticket Category: ${booking.ticketLabel || booking.ticketType.toUpperCase()}`,
|
||||
`Total Price: ${formatBookingCurrency(booking.totalPrice)}`,
|
||||
`Date: ${DINNER_EVENT_DATE_LABEL}`,
|
||||
`Time: ${DINNER_EVENT_TIME_LABEL}`,
|
||||
`Venue: ${DINNER_EVENT_VENUE}`,
|
||||
`Date: ${booking.event.dateLabel}`,
|
||||
`Time: ${booking.event.timeLabel}`,
|
||||
`Venue: ${booking.event.venue}`,
|
||||
'',
|
||||
'Please present the QR code from the receipt at the event.'
|
||||
].join('\n')
|
||||
|
||||
Reference in New Issue
Block a user