diff --git a/app/pages/bookings/index.vue b/app/pages/bookings/index.vue
index 16096c1..3078ad6 100644
--- a/app/pages/bookings/index.vue
+++ b/app/pages/bookings/index.vue
@@ -302,6 +302,17 @@
icon="i-lucide-receipt"
size="sm"
/>
+
@@ -365,7 +376,7 @@
diff --git a/app/pages/confirmation/[token].vue b/app/pages/confirmation/[token].vue
index 46aa4a6..e326dd0 100644
--- a/app/pages/confirmation/[token].vue
+++ b/app/pages/confirmation/[token].vue
@@ -1,5 +1,5 @@
@@ -198,13 +240,24 @@ async function confirmBooking() {
/>
+
+
diff --git a/server/api/public/bookings/[token]/cancel.post.ts b/server/api/public/bookings/[token]/cancel.post.ts
new file mode 100644
index 0000000..e6e5117
--- /dev/null
+++ b/server/api/public/bookings/[token]/cancel.post.ts
@@ -0,0 +1,31 @@
+import type { CancelBookingConfirmationResponse } from '~~/shared/booking'
+
+import { cancelBookingConfirmationByConfirmationToken, getBookingByConfirmationToken } from '../../../../utils/booking-repository'
+import { getRequiredRouteParam, httpError } from '../../../../utils/http'
+
+export default defineEventHandler(async (event): Promise => {
+ const token = getRequiredRouteParam(event, 'token', 'Confirmation token')
+ const existingBooking = await getBookingByConfirmationToken(token)
+
+ if (!existingBooking) {
+ httpError(404, 'Booking not found')
+ }
+
+ if (existingBooking.status === 'pending') {
+ return {
+ booking: existingBooking,
+ alreadyPending: true
+ }
+ }
+
+ const booking = await cancelBookingConfirmationByConfirmationToken(token)
+
+ if (!booking) {
+ httpError(404, 'Booking not found')
+ }
+
+ return {
+ booking,
+ alreadyPending: false
+ }
+})
diff --git a/server/utils/booking-repository.ts b/server/utils/booking-repository.ts
index 099f5e2..3b10b15 100644
--- a/server/utils/booking-repository.ts
+++ b/server/utils/booking-repository.ts
@@ -859,3 +859,30 @@ export async function confirmBookingByConfirmationToken(confirmationToken: strin
return await getBookingByConfirmationToken(confirmationToken)
}
+
+export async function cancelBookingConfirmationByConfirmationToken(confirmationToken: string): Promise {
+ await ensureDatabaseReady()
+ const sql = getSqlClient()
+
+ const [row] = await sql`
+ with updated_booking as (
+ update bookings
+ set
+ status = 'pending',
+ confirmed_at = null,
+ updated_at = now()
+ where confirmation_token = ${confirmationToken}
+ and status = 'confirmed'
+ returning *
+ )
+ select ${bookingSelectColumns(sql)}
+ from updated_booking as bookings
+ ${bookingJoins(sql)}
+ `
+
+ if (row) {
+ return mapBooking(row)
+ }
+
+ return await getBookingByConfirmationToken(confirmationToken)
+}
diff --git a/shared/booking.ts b/shared/booking.ts
index 3c4e816..44b79a1 100644
--- a/shared/booking.ts
+++ b/shared/booking.ts
@@ -145,6 +145,11 @@ export interface ConfirmBookingResponse {
ticketReceiptWhatsApp: WhatsAppDeliveryResult
}
+export interface CancelBookingConfirmationResponse {
+ booking: PublicBooking
+ alreadyPending: boolean
+}
+
export function isBookingStatus(value: string | null | undefined): value is BookingStatus {
return value === 'pending' || value === 'confirmed'
}