Skip to main content
Glama
alcylu

Nightlife Search

by alcylu

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
NameRequiredDescriptionDefault
booking_request_idYes
customer_emailYes
customer_phoneYes
cancellation_reasonNo

Implementation Reference

  • 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,
      };
    }
  • 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,
          }),
        ),
      );
    }
  • 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(),
    };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/alcylu/nightlife-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server