cancel_vip_booking_request
Cancel VIP nightlife bookings by providing booking ID, email, and phone for verification. Works for submitted, in-review, or confirmed bookings.
Instructions
Cancel a VIP booking request. Requires booking ID plus customer email and phone to verify ownership. Works for bookings in submitted, in_review, or confirmed status.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| booking_request_id | Yes | ||
| customer_email | Yes | ||
| customer_phone | Yes | ||
| cancellation_reason | No |
Implementation Reference
- src/services/vipBookings.ts:1291-1449 (handler)The 'cancelVipBookingRequest' function handles the business logic for cancelling a VIP booking, including database updates, logging, and triggering optional side effects like refunds and cancellation emails.
export async function cancelVipBookingRequest( supabase: SupabaseClient, input: CancelVipBookingRequestInput, options?: { stripeSecretKey?: string; resendApiKey?: string }, ): Promise<VipBookingStatusResult> { const bookingRequestId = ensureUuid(input.booking_request_id, "booking_request_id"); const customerEmail = normalizeCustomerEmail(input.customer_email); const customerPhone = normalizeCustomerPhone(input.customer_phone); const cancellationReason = normalizeOptionalText(input.cancellation_reason, 500); const { data: booking, error: bookingError } = await supabase .from("vip_booking_requests") .select("id,status,updated_at,status_message,customer_email,customer_phone") .eq("id", bookingRequestId) .maybeSingle<VipBookingLookupRow>(); if (bookingError) { throw new NightlifeError("DB_QUERY_FAILED", "Failed to fetch VIP booking request.", { cause: bookingError.message, }); } if (!booking || booking.customer_email !== customerEmail || booking.customer_phone !== customerPhone) { throw new NightlifeError("BOOKING_REQUEST_NOT_FOUND", "VIP booking request not found."); } if (!isAllowedVipStatusTransition(booking.status, "cancelled")) { throw new NightlifeError( "INVALID_BOOKING_REQUEST", `Cannot cancel a booking that is already ${booking.status}.`, ); } const statusMessage = cancellationReason || "Cancelled by customer."; const { data: updated, error: updateError } = await supabase .from("vip_booking_requests") .update({ status: "cancelled", status_message: statusMessage }) .eq("id", bookingRequestId) .select("id,status,updated_at,status_message") .single<{ id: string; status: VipBookingStatus; updated_at: string; status_message: string }>(); if (updateError || !updated) { throw new NightlifeError( "BOOKING_STATUS_UPDATE_FAILED", "Failed to cancel VIP booking request.", { cause: updateError?.message || "Unknown update error" }, ); } const { error: eventError } = await supabase .from("vip_booking_status_events") .insert({ booking_request_id: bookingRequestId, from_status: booking.status, to_status: "cancelled", actor_type: "customer", note: cancellationReason, }); if (eventError) { throw new NightlifeError( "BOOKING_STATUS_UPDATE_FAILED", "Booking cancelled but event logging failed.", { cause: eventError.message }, ); } const { error: settleError } = await supabase .from("vip_agent_tasks") .update({ status: "done", last_error: null }) .eq("booking_request_id", bookingRequestId) .in("status", ["pending", "claimed"]); if (settleError) { throw new NightlifeError( "BOOKING_STATUS_UPDATE_FAILED", "Booking cancelled but queue settlement failed.", { cause: settleError.message }, ); } // Process deposit refund if applicable (non-blocking) if (options?.stripeSecretKey) { try { const { processRefundOnCancellation } = await import("./deposits.js"); await processRefundOnCancellation(supabase, options.stripeSecretKey, bookingRequestId, { resendApiKey: options?.resendApiKey }); } catch (refundError) { logEvent("deposit.refund_error", { booking_request_id: bookingRequestId, error: refundError instanceof Error ? refundError.message : "Unknown error", }); } } // Send cancellation email (fire-and-forget) if (options?.resendApiKey) { try { const { getDepositForBooking } = await import("./deposits.js"); const cancelDeposit = await getDepositForBooking(supabase, bookingRequestId); const depositOutcome = cancelDeposit ? cancelDeposit.status : undefined; const { sendBookingCancelledEmail } = await import("./email.js"); await sendBookingCancelledEmail(supabase, options.resendApiKey, bookingRequestId, depositOutcome); } catch (emailError) { logEvent("email.send_error", { booking_request_id: bookingRequestId, error: emailError instanceof Error ? emailError.message : "Unknown error", }); } } const { data: events, error: eventsError } = await supabase .from("vip_booking_status_events") .select("to_status,note,created_at") .eq("booking_request_id", bookingRequestId) .order("created_at", { ascending: true }) .limit(20); if (eventsError) { throw new NightlifeError("DB_QUERY_FAILED", "Failed to fetch VIP booking history.", { cause: eventsError.message, }); } const history = ((events || []) as VipStatusEventRow[]) .filter((row) => isVipStatus(row.to_status)) .map((row) => ({ status: row.to_status, at: row.created_at, note: row.note, })); const latestHistory = history[history.length - 1]; // Load deposit info for response let depositStatus: string | null = null; let depositAmountJpy: number | null = null; const { data: depositRow } = await supabase .from("vip_booking_deposits") .select("status,amount_jpy") .eq("booking_request_id", bookingRequestId) .maybeSingle(); if (depositRow) { depositStatus = depositRow.status as string; depositAmountJpy = depositRow.amount_jpy as number; } return { booking_request_id: updated.id, status: updated.status, last_updated_at: updated.updated_at, status_message: updated.status_message, latest_note: latestHistory?.note || null, history, deposit_status: depositStatus, deposit_amount_jpy: depositAmountJpy, deposit_payment_url: null, }; } - src/tools/vipBookings.ts:173-190 (registration)Registration of the 'cancel_vip_booking_request' MCP tool, which maps the input schema, tool description, and handler function to the server.
server.registerTool( "cancel_vip_booking_request", { description: "Cancel a VIP booking request. Requires booking ID plus customer email and phone to verify ownership. Works for bookings in submitted, in_review, or confirmed status.", inputSchema: cancelVipBookingInputSchema, outputSchema: vipBookingStatusOutputSchema, }, async (args) => runTool( "cancel_vip_booking_request", vipBookingStatusOutputSchema, async () => cancelVipBookingRequest(deps.supabase, args, { stripeSecretKey: deps.config.stripeSecretKey ?? undefined, resendApiKey: deps.config.resendApiKey ?? undefined, }), ), ); } - src/tools/vipBookings.ts:65-70 (schema)Input validation schema for 'cancel_vip_booking_request', defining the required parameters (booking_request_id, customer_email, customer_phone) and optional fields.
export const cancelVipBookingInputSchema = { booking_request_id: z.string().min(1), customer_email: z.string().min(1), customer_phone: z.string().min(1), cancellation_reason: z.string().max(500).optional(), };