Add payment method selection (Cash/Bank) to booking details Support uploading, downloading, and deleting transaction documents Update database schema and API endpoints to handle file storage
187 lines
6.6 KiB
TypeScript
187 lines
6.6 KiB
TypeScript
import type { BookingCapacitySettings, BookingMode, PaymentMethod, PublicBooking, TicketType } from '~~/shared/booking'
|
|
|
|
import {
|
|
formatBookingCurrency,
|
|
isPaymentMethod
|
|
} from '~~/shared/booking'
|
|
import { hasValidFullName, isValidPhoneNumber, normalizeFullName, normalizePhoneNumber } from '~~/shared/auth'
|
|
import { resolveLocale } from '~~/shared/i18n'
|
|
|
|
import { assertBadRequest } from './http'
|
|
|
|
export function parseCreateBookingInput(body: {
|
|
customerName?: string
|
|
customerPhone?: string
|
|
bookingMode?: BookingMode | string | null
|
|
quantity?: number
|
|
ticketType?: TicketType
|
|
personInChargeId?: string
|
|
locale?: string | null
|
|
}) {
|
|
const customerName = normalizeFullName(body.customerName || '')
|
|
const customerPhone = normalizePhoneNumber(body.customerPhone || '')
|
|
const bookingMode = typeof body.bookingMode === 'string' ? body.bookingMode.trim().toLowerCase() : body.bookingMode
|
|
const ticketType = typeof body.ticketType === 'string' ? body.ticketType.trim().toLowerCase() : body.ticketType
|
|
const quantity = Number(body.quantity)
|
|
const personInChargeId = (body.personInChargeId || '').trim()
|
|
const locale = resolveLocale(body.locale)
|
|
|
|
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(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(typeof ticketType === 'string' && ticketType.trim().length > 0, 'Ticket category is required')
|
|
assertBadRequest(personInChargeId, 'Person in charge is required')
|
|
|
|
return {
|
|
customerName,
|
|
customerPhone,
|
|
bookingMode,
|
|
quantity,
|
|
ticketType,
|
|
personInChargeId,
|
|
locale
|
|
}
|
|
}
|
|
|
|
export function parseUpdateBookingDetailsInput(body: {
|
|
customerName?: string
|
|
customerPhone?: string
|
|
bookingMode?: BookingMode | string | null
|
|
quantity?: number
|
|
ticketType?: TicketType
|
|
paymentMethod?: PaymentMethod | string | null
|
|
remark?: string | null
|
|
}) {
|
|
const customerName = normalizeFullName(body.customerName || '')
|
|
const customerPhone = normalizePhoneNumber(body.customerPhone || '')
|
|
const bookingMode = typeof body.bookingMode === 'string' ? body.bookingMode.trim().toLowerCase() : body.bookingMode
|
|
const ticketType = typeof body.ticketType === 'string' ? body.ticketType.trim().toLowerCase() : body.ticketType
|
|
const paymentMethod = typeof body.paymentMethod === 'string' ? body.paymentMethod.trim().toLowerCase() : body.paymentMethod
|
|
const quantity = Number(body.quantity)
|
|
const remark = typeof body.remark === 'string' ? body.remark.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(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(typeof ticketType === 'string' && ticketType.trim().length > 0, 'Ticket category is required')
|
|
assertBadRequest(isPaymentMethod(paymentMethod), 'Payment method must be Cash or Bank')
|
|
assertBadRequest(remark.length <= 1000, 'Remark must be 1,000 characters or fewer')
|
|
|
|
return {
|
|
customerName,
|
|
customerPhone,
|
|
bookingMode,
|
|
quantity,
|
|
ticketType,
|
|
paymentMethod,
|
|
remark: remark || null
|
|
}
|
|
}
|
|
|
|
export function parseBookingRemarkInput(body: {
|
|
remark?: string | null
|
|
}) {
|
|
const remark = typeof body.remark === 'string' ? body.remark.trim() : ''
|
|
|
|
assertBadRequest(remark.length <= 1000, 'Remark must be 1,000 characters or fewer')
|
|
|
|
return {
|
|
remark: remark || null
|
|
}
|
|
}
|
|
|
|
export function parseBookingPicTransferInput(body: {
|
|
personInChargeId?: string | null
|
|
}) {
|
|
const personInChargeId = typeof body.personInChargeId === 'string'
|
|
? body.personInChargeId.trim()
|
|
: ''
|
|
|
|
assertBadRequest(personInChargeId, 'Person in charge is required')
|
|
|
|
return {
|
|
personInChargeId
|
|
}
|
|
}
|
|
|
|
export function buildBookingMessage(booking: PublicBooking, confirmationUrl: string) {
|
|
if (booking.locale === 'zh') {
|
|
return [
|
|
`我想预订 ${booking.event.title} 的票券。`,
|
|
'',
|
|
`姓名:${booking.customerName}`,
|
|
`联络号码:${booking.customerPhone}`,
|
|
`座位:${booking.seatCount}`,
|
|
`票券类别:${booking.ticketLabel || booking.ticketType.toUpperCase()}`,
|
|
`总价:${formatBookingCurrency(booking.totalPrice, booking.locale)}`,
|
|
'',
|
|
'负责人确认链接:',
|
|
confirmationUrl
|
|
].join('\n')
|
|
}
|
|
|
|
return [
|
|
`I'd like to book tickets for the ${booking.event.title}.`,
|
|
'',
|
|
`Name: ${booking.customerName}`,
|
|
`Phone Number: ${booking.customerPhone}`,
|
|
`Seats: ${booking.seatCount}`,
|
|
`Ticket Category: ${booking.ticketLabel || booking.ticketType.toUpperCase()}`,
|
|
`Total Price: ${formatBookingCurrency(booking.totalPrice, booking.locale)}`,
|
|
'',
|
|
'PIC confirmation link:',
|
|
confirmationUrl
|
|
].join('\n')
|
|
}
|
|
|
|
export function parseSeatShareInput(body: {
|
|
shared?: boolean
|
|
recipientName?: string | null
|
|
recipientPhone?: string | null
|
|
}) {
|
|
const shared = body.shared
|
|
const recipientName = normalizeFullName(body.recipientName || '')
|
|
const recipientPhone = normalizePhoneNumber(body.recipientPhone || '')
|
|
|
|
assertBadRequest(typeof shared === 'boolean', 'Shared flag is required')
|
|
|
|
if (shared) {
|
|
assertBadRequest(!recipientName || hasValidFullName(recipientName), 'Recipient name must be at least 2 characters')
|
|
assertBadRequest(!recipientPhone || isValidPhoneNumber(recipientPhone), 'Recipient phone number must include a country code, e.g. +60123456789')
|
|
}
|
|
|
|
return {
|
|
shared,
|
|
recipientName: recipientName || null,
|
|
recipientPhone: recipientPhone || null
|
|
}
|
|
}
|
|
|
|
export function parseBookingCapacityInput(body: {
|
|
totalSeats?: number | string | null
|
|
}): Pick<BookingCapacitySettings, 'totalSeats'> {
|
|
const totalSeats = parseOptionalInteger(body.totalSeats)
|
|
|
|
assertBadRequest(totalSeats === null || totalSeats >= 0, 'Total seats must be 0 or greater')
|
|
|
|
return {
|
|
totalSeats
|
|
}
|
|
}
|
|
|
|
function parseOptionalInteger(value: number | string | null | undefined) {
|
|
if (value === null || value === undefined || value === '') {
|
|
return null
|
|
}
|
|
|
|
const parsed = typeof value === 'number'
|
|
? value
|
|
: Number.parseInt(String(value), 10)
|
|
|
|
assertBadRequest(Number.isInteger(parsed), 'Capacity values must be whole numbers')
|
|
|
|
return parsed
|
|
}
|