Files
dticket.tootaio.com/server/api/bookings/[id].patch.ts
xiaomai b64a2b4c1c feat(bookings): add transaction document uploads for bank payments
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
2026-05-09 12:56:32 +08:00

108 lines
3.2 KiB
TypeScript

import type { UpdateBookingDetailsResponse } from '~~/shared/booking'
import { getSeatCount } from '~~/shared/booking'
import { requireAuth } from '../../utils/auth'
import {
getBookingById,
getBookingInventorySummary,
getActiveBookingModeOptionByCode,
getActiveTicketCatalogItemByCode,
clearBookingTransactionDocument,
updateBookingDetails
} from '../../utils/booking-repository'
import { parseUpdateBookingDetailsInput } from '../../utils/bookings'
import { deleteTransactionDocument } from '../../utils/transaction-documents'
import { getRequiredRouteParam, httpError } from '../../utils/http'
export default defineEventHandler(async (event): Promise<UpdateBookingDetailsResponse> => {
const auth = await requireAuth(event)
const bookingId = getRequiredRouteParam(event, 'id', 'Booking ID')
const body = await readBody<{
customerName?: string
customerPhone?: string
bookingMode?: string | null
quantity?: number
ticketType?: string
paymentMethod?: string | null
remark?: string | null
}>(event)
const existingBooking = await getBookingById(bookingId, auth.user.role === 'super_admin'
? undefined
: { personInChargeId: auth.user.id })
if (!existingBooking) {
httpError(404, 'Booking not found')
}
const input = parseUpdateBookingDetailsInput(body)
const [bookingMode, ticket] = await Promise.all([
getActiveBookingModeOptionByCode(input.bookingMode),
getActiveTicketCatalogItemByCode(input.ticketType)
])
if (!bookingMode) {
httpError(400, 'Booking mode is invalid')
}
if (!ticket) {
httpError(400, 'Ticket category is invalid')
}
if (bookingMode.eventId !== ticket.eventId || bookingMode.eventId !== existingBooking.event.id) {
httpError(400, 'Booking mode and ticket category must belong to the same event')
}
const seatCount = getSeatCount(bookingMode, input.quantity)
const totalPrice = seatCount * ticket.price
const seatIncrease = Math.max(seatCount - existingBooking.seatCount, 0)
if (existingBooking.status === 'confirmed' && seatIncrease > 0) {
const summary = await getBookingInventorySummary()
if (summary.leftSeats !== null && seatIncrease > summary.leftSeats) {
httpError(409, `Total seats cannot exceed the remaining capacity by ${seatIncrease - summary.leftSeats} seats`)
}
}
const booking = await updateBookingDetails({
bookingId,
customerName: input.customerName,
customerPhone: input.customerPhone,
bookingModeId: bookingMode.id,
bookingMode: bookingMode.value,
quantity: input.quantity,
seatCount,
ticketTypeId: ticket.id,
ticketType: ticket.value,
unitPrice: ticket.price,
totalPrice,
paymentMethod: input.paymentMethod,
remark: input.remark,
personInChargeId: auth.user.role === 'super_admin' ? undefined : auth.user.id
})
if (!booking) {
httpError(404, 'Booking not found')
}
if (input.paymentMethod === 'cash') {
const cleared = await clearBookingTransactionDocument({
bookingId,
personInChargeId: auth.user.role === 'super_admin' ? undefined : auth.user.id
})
if (cleared) {
await deleteTransactionDocument(cleared.previousStorageName)
return {
booking: cleared.booking
}
}
}
return {
booking
}
})