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
This commit is contained in:
26
server/api/bookings/[id]/transaction-document.delete.ts
Normal file
26
server/api/bookings/[id]/transaction-document.delete.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { UpdateBookingDetailsResponse } from '~~/shared/booking'
|
||||
|
||||
import { requireAuth } from '../../../utils/auth'
|
||||
import { clearBookingTransactionDocument } from '../../../utils/booking-repository'
|
||||
import { getRequiredRouteParam, httpError } from '../../../utils/http'
|
||||
import { deleteTransactionDocument } from '../../../utils/transaction-documents'
|
||||
|
||||
export default defineEventHandler(async (event): Promise<UpdateBookingDetailsResponse> => {
|
||||
const auth = await requireAuth(event)
|
||||
const bookingId = getRequiredRouteParam(event, 'id', 'Booking ID')
|
||||
|
||||
const result = await clearBookingTransactionDocument({
|
||||
bookingId,
|
||||
personInChargeId: auth.user.role === 'super_admin' ? undefined : auth.user.id
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
httpError(404, 'Booking not found')
|
||||
}
|
||||
|
||||
await deleteTransactionDocument(result.previousStorageName)
|
||||
|
||||
return {
|
||||
booking: result.booking
|
||||
}
|
||||
})
|
||||
35
server/api/bookings/[id]/transaction-document.get.ts
Normal file
35
server/api/bookings/[id]/transaction-document.get.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { sendStream, setHeader } from 'h3'
|
||||
|
||||
import { requireAuth } from '../../../utils/auth'
|
||||
import { getBookingTransactionDocument } from '../../../utils/booking-repository'
|
||||
import { getRequiredRouteParam, httpError } from '../../../utils/http'
|
||||
import {
|
||||
getSafeDownloadName,
|
||||
getTransactionDocumentFile
|
||||
} from '../../../utils/transaction-documents'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const auth = await requireAuth(event)
|
||||
const bookingId = getRequiredRouteParam(event, 'id', 'Booking ID')
|
||||
|
||||
const document = await getBookingTransactionDocument({
|
||||
bookingId,
|
||||
personInChargeId: auth.user.role === 'super_admin' ? undefined : auth.user.id
|
||||
})
|
||||
|
||||
if (!document) {
|
||||
httpError(404, 'Transaction document not found')
|
||||
}
|
||||
|
||||
const file = await getTransactionDocumentFile(document.storageName)
|
||||
const downloadName = getSafeDownloadName(document.originalName)
|
||||
|
||||
setHeader(event, 'content-type', document.mimeType)
|
||||
setHeader(event, 'content-length', String(file.size))
|
||||
setHeader(event, 'content-disposition', `attachment; filename="${downloadName}"`)
|
||||
setHeader(event, 'x-content-type-options', 'nosniff')
|
||||
setHeader(event, 'cache-control', 'private, no-store')
|
||||
setHeader(event, 'content-security-policy', 'sandbox')
|
||||
|
||||
return sendStream(event, file.stream)
|
||||
})
|
||||
86
server/api/bookings/[id]/transaction-document.post.ts
Normal file
86
server/api/bookings/[id]/transaction-document.post.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { UpdateBookingDetailsResponse } from '~~/shared/booking'
|
||||
|
||||
import { getHeader, readMultipartFormData } from 'h3'
|
||||
|
||||
import { requireAuth } from '../../../utils/auth'
|
||||
import {
|
||||
getBookingById,
|
||||
replaceBookingTransactionDocument
|
||||
} from '../../../utils/booking-repository'
|
||||
import { getRequiredRouteParam, httpError } from '../../../utils/http'
|
||||
import {
|
||||
deleteTransactionDocument,
|
||||
saveTransactionDocument,
|
||||
TRANSACTION_DOCUMENT_MAX_SIZE,
|
||||
validateTransactionDocumentUpload
|
||||
} from '../../../utils/transaction-documents'
|
||||
|
||||
export default defineEventHandler(async (event): Promise<UpdateBookingDetailsResponse> => {
|
||||
const auth = await requireAuth(event)
|
||||
const bookingId = getRequiredRouteParam(event, 'id', 'Booking ID')
|
||||
const accessScope = auth.user.role === 'super_admin'
|
||||
? undefined
|
||||
: { personInChargeId: auth.user.id }
|
||||
|
||||
const booking = await getBookingById(bookingId, accessScope)
|
||||
|
||||
if (!booking) {
|
||||
httpError(404, 'Booking not found')
|
||||
}
|
||||
|
||||
if (booking.paymentMethod !== 'bank') {
|
||||
httpError(400, 'Transaction document can only be uploaded for Bank payments')
|
||||
}
|
||||
|
||||
const contentType = String(getHeader(event, 'content-type') || '').toLowerCase()
|
||||
|
||||
if (!contentType.startsWith('multipart/form-data;')) {
|
||||
httpError(400, 'Transaction document upload must use multipart form data')
|
||||
}
|
||||
|
||||
const contentLength = Number(getHeader(event, 'content-length') || 0)
|
||||
|
||||
if (contentLength > TRANSACTION_DOCUMENT_MAX_SIZE + 1024 * 1024) {
|
||||
httpError(413, 'Transaction document must be 10MB or smaller')
|
||||
}
|
||||
|
||||
const formData = await readMultipartFormData(event)
|
||||
const filePart = formData?.find((part) => part.name === 'document' && part.filename)
|
||||
|
||||
if (!filePart) {
|
||||
httpError(400, 'Transaction document is required')
|
||||
}
|
||||
|
||||
const upload = validateTransactionDocumentUpload({
|
||||
data: filePart.data,
|
||||
filename: filePart.filename,
|
||||
contentType: filePart.type
|
||||
})
|
||||
|
||||
const storageName = await saveTransactionDocument(filePart.data, upload.fileType)
|
||||
|
||||
try {
|
||||
const result = await replaceBookingTransactionDocument({
|
||||
bookingId,
|
||||
personInChargeId: auth.user.role === 'super_admin' ? undefined : auth.user.id,
|
||||
originalName: upload.originalName,
|
||||
storageName,
|
||||
mimeType: upload.fileType.mimeType,
|
||||
size: filePart.data.length
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
await deleteTransactionDocument(storageName)
|
||||
httpError(404, 'Booking not found')
|
||||
}
|
||||
|
||||
await deleteTransactionDocument(result.previousStorageName)
|
||||
|
||||
return {
|
||||
booking: result.booking
|
||||
}
|
||||
} catch (error) {
|
||||
await deleteTransactionDocument(storageName)
|
||||
throw error
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user