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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user