feat(bookings): add payment and document upload to confirmation page
Allow users to select payment method and upload receipts before confirming. Add public API endpoints for payment updates and document management.
This commit is contained in:
@@ -239,7 +239,7 @@ function mapTicketCatalogItem(row: DbTicketCatalogItemRow): TicketCatalogItemRec
|
||||
}
|
||||
}
|
||||
|
||||
function mapBookingTransactionDocument(row: DbBookingTransactionDocumentRow, bookingId: string): BookingTransactionDocumentRecord | null {
|
||||
function mapBookingTransactionDocument(row: DbBookingTransactionDocumentRow, url: string): BookingTransactionDocument | null {
|
||||
if (
|
||||
row.payment_method !== 'bank'
|
||||
|| !row.transaction_document_original_name
|
||||
@@ -253,16 +253,29 @@ function mapBookingTransactionDocument(row: DbBookingTransactionDocumentRow, boo
|
||||
|
||||
return {
|
||||
originalName: row.transaction_document_original_name,
|
||||
storageName: row.transaction_document_storage_name,
|
||||
mimeType: row.transaction_document_mime_type,
|
||||
size: parseInteger(row.transaction_document_size),
|
||||
uploadedAt: toIsoString(row.transaction_document_uploaded_at) ?? new Date().toISOString(),
|
||||
url: `/api/bookings/${bookingId}/transaction-document`
|
||||
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'
|
||||
@@ -293,7 +306,9 @@ function mapBooking(row: DbBookingRow, options?: {
|
||||
personInChargeName: row.person_in_charge_name || '',
|
||||
personInChargePhoneNumber: row.person_in_charge_phone_number || '',
|
||||
paymentMethod,
|
||||
transactionDocument: options?.includeTransactionDocument ? mapBookingTransactionDocument(row, row.id) : null,
|
||||
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),
|
||||
@@ -603,7 +618,9 @@ export async function createBooking(input: {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getBookingByConfirmationToken(confirmationToken: string): Promise<PublicBooking | null> {
|
||||
export async function getBookingByConfirmationToken(confirmationToken: string, options?: {
|
||||
includeTransactionDocument?: boolean
|
||||
}): Promise<PublicBooking | null> {
|
||||
await ensureDatabaseReady()
|
||||
const sql = getSqlClient()
|
||||
|
||||
@@ -616,7 +633,12 @@ export async function getBookingByConfirmationToken(confirmationToken: string):
|
||||
limit 1
|
||||
`
|
||||
|
||||
return row ? mapBooking(row) : null
|
||||
return row
|
||||
? mapBooking(row, {
|
||||
includeTransactionDocument: options?.includeTransactionDocument,
|
||||
transactionDocumentUrl: `/api/public/bookings/${confirmationToken}/transaction-document`
|
||||
})
|
||||
: null
|
||||
}
|
||||
|
||||
export async function getBookingByReceiptToken(receiptToken: string): Promise<PublicBooking | null> {
|
||||
@@ -924,7 +946,164 @@ export async function getBookingTransactionDocument(input: {
|
||||
limit 1
|
||||
`
|
||||
|
||||
return row ? mapBookingTransactionDocument(row, row.id) : null
|
||||
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: {
|
||||
|
||||
Reference in New Issue
Block a user