feat(bookings): implement ticket receipts and seat sharing system

Add receipt tokens and booking_seats table to track individual tickets
Create receipt and seat view pages with QR code generation
This commit is contained in:
2026-04-12 22:48:26 +08:00
parent 7f582b530c
commit 6194c96ead
15 changed files with 1663 additions and 61 deletions

View File

@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto'
import { DEFAULT_USER_PASSWORD } from '~~/shared/auth'
import { randomToken } from './base64url'
import { hashPassword } from './password'
import { getSqlClient } from './postgres'
@@ -65,6 +66,7 @@ async function initializeDatabase() {
create table if not exists bookings (
id text primary key,
confirmation_token text not null unique,
receipt_token text not null unique,
customer_name text not null,
customer_phone text not null,
booking_mode text not null check (booking_mode in ('table', 'pax')),
@@ -83,6 +85,36 @@ async function initializeDatabase() {
)
`
await sql`
alter table bookings
add column if not exists receipt_token text
`
await sql`
create unique index if not exists bookings_receipt_token_idx
on bookings (receipt_token)
`
await sql`
create table if not exists booking_seats (
id text primary key,
booking_id text not null references bookings(id) on delete cascade,
seat_number integer not null check (seat_number >= 1),
seat_token text not null unique,
recipient_name text,
recipient_phone text,
shared_at timestamptz,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (booking_id, seat_number)
)
`
await sql`
create index if not exists booking_seats_booking_id_idx
on booking_seats (booking_id)
`
await sql`
create table if not exists booking_settings (
id text primary key,
@@ -98,6 +130,74 @@ async function initializeDatabase() {
on conflict (id) do nothing
`
const bookingsMissingReceiptTokens = await sql<{ id: string }[]>`
select id
from bookings
where receipt_token is null or receipt_token = ''
`
for (const booking of bookingsMissingReceiptTokens) {
await sql`
update bookings
set
receipt_token = ${randomToken(24)},
updated_at = now()
where id = ${booking.id}
`
}
const existingBookings = await sql<{ id: string, seat_count: number | string }[]>`
select
id,
seat_count
from bookings
`
for (const booking of existingBookings) {
const seatCount = typeof booking.seat_count === 'number'
? booking.seat_count
: Number.parseInt(booking.seat_count, 10)
const existingSeatRows = await sql<{ seat_number: number | string }[]>`
select seat_number
from booking_seats
where booking_id = ${booking.id}
`
const existingSeatNumbers = new Set(
existingSeatRows.map((seat) => typeof seat.seat_number === 'number'
? seat.seat_number
: Number.parseInt(seat.seat_number, 10))
)
for (let seatNumber = 1; seatNumber <= seatCount; seatNumber += 1) {
if (existingSeatNumbers.has(seatNumber)) {
continue
}
await sql`
insert into booking_seats (
id,
booking_id,
seat_number,
seat_token
)
values (
${randomUUID()},
${booking.id},
${seatNumber},
${randomToken(24)}
)
on conflict (booking_id, seat_number) do nothing
`
}
}
await sql`
alter table bookings
alter column receipt_token set not null
`
const [existingSuperAdmin] = await sql<{ id: string }[]>`
select id
from users