Reservation Management — House of Legends

Documented: 2026-05-11 Doc Status: Excellent | ✓ All 6 checks passed

Overview

Staff manage reservations via the admin dashboard — view list, filter, and take actions (cancel, refund, check-in).

Reservations Table

reservations: defineTable({
  eventId: v.id("experienceEvents"),
  customerFirstName: v.string(),
  customerLastName: v.string(),
  customerEmail: v.string(),
  customerPhone: v.string(),
  ticketType: v.union(
    v.literal("DINNER_THEATRE"),
    v.literal("EXPERIENCE_ONLY"),
  ),
  quantity: v.number(),
  addOns: v.record(v.string(), v.number()),
  subtotal: v.number(),
  totalAmount: v.number(),
  dayOfWeekSurcharge: v.number(),
  smallPartySurcharge: v.number(),
  paymentStatus: v.union(
    v.literal("PENDING"),
    v.literal("PAID"),
    v.literal("REFUNDED"),
    v.literal("FAILED"),
    v.literal("CANCELLED"),
    v.literal("REFUND_PENDING"),
  ),
  paymentGateway: v.optional(v.string()),
  onePayOrderId: v.optional(v.string()),
  onePayTransactionId: v.optional(v.string()),
  vaNumber: v.optional(v.string()),
  qrCode: v.optional(v.string()),
  qrCodeUrl: v.optional(v.string()),
  paymentExpiresAt: v.optional(v.number()),
  bookingExpiresAt: v.optional(v.number()),
  checkedInAt: v.optional(v.number()),
  createdAt: v.number(),
})
  .index("by_event", ["eventId"])
  .index("by_status", ["paymentStatus"])
  .index("by_email", ["customerEmail"]);

Reservation List (/dashboard/reservations)

Table Columns

ColumnSource
IDreservation._id (short)
Guest${firstName} ${lastName}
EmailcustomerEmail
PhonecustomerPhone
EventexperienceEvents lookup
TypeticketType
Qtyquantity
TotaltotalAmount formatted
StatuspaymentStatus badge
DatecreatedAt formatted

Filters

  • Status: PENDING, PAID, CANCELLED, REFUNDED
  • Date range: Created between dates
  • Event: Specific experience or all
  • Search: Name, email, phone

Status Badges

StatusBadge ColorMeaning
PENDINGYellowAwaiting payment
PAIDGreenPayment confirmed
REFUND_PENDINGOrangeRefund requested
REFUNDEDGrayRefund completed
CANCELLEDRedCancelled
FAILEDRedPayment failed

Reservation Detail (Drawer)

Opens on row click:

Guest Info

  • Full name, email, phone
  • Booking created timestamp

Event Info

  • Experience name
  • Date + time
  • Venue

Ticket Info

  • Ticket type (Dinner Theatre / Show Only)
  • Quantity
  • Unit price

Add-ons

  • List of add-ons with quantities

Pricing Summary

  • Subtotal
  • Day-of-week surcharge
  • Small party surcharge
  • Total

Payment Info

  • Gateway (OnePay)
  • Order ID
  • Transaction ID
  • VA Number (if bank transfer)
  • QR code preview

Status History

  • Created
  • Payment confirmed (if PAID)
  • Checked in (if applicable)

Actions

ActionMutationCondition
Cancelreservations.cancelReservationPENDING or PAID
Check-incheckIns.createPAID not yet checked in
Resend confirmationnotifications.sendConfirmationAny status

Cancel Flow (with optional refund)

The cancelReservation mutation handles both cancellation and refund in a single flow:
  1. Staff clicks “Cancel”
  2. Confirmation dialog
  3. reservations.cancelReservation called
  4. If PENDING: Status — CANCELLED, seats released
  5. If PAID with OnePay order: Status — REFUND_PENDING, OnePay refund API called
  6. Refund confirmation comes via webhook → Status — REFUNDED
  7. Notification sent to guest
// packages/backend/convex/domains/reservations.ts
export const cancelReservation = staffMutation({
  args: CancelReservationArgsSchema,
  handler: async (ctx, { reservationId, reason }) => {
    const reservation = await ctx.db.get(reservationId);
    // If not paid, just release seats and cancel
    if (reservation.paymentStatus !== "PAID") {
      await ctx.db.patch(reservationId, { paymentStatus: "CANCELLED" });
      return { refunded: false };
    }
    // For paid reservations, trigger refund
    if (reservation.paymentStatus === "PAID" && reservation.onePayOrderId) {
      await ctx.db.patch(reservationId, { paymentStatus: "REFUND_PENDING" });
      await triggerSepayRefund(ctx, reservation);
      return { refunded: true };
    }
  },
});

Check-in

Located at /dashboard/checkin:
  • QR scanner (camera)
  • Manual lookup by reservation ID
On scan/lookup:
  • Shows guest details + party size
  • “Check In” button
  • Creates checkIns record
  • Updates reservation.checkedInAt

Backend Functions

FunctionPurpose
reservations.listAll reservations with filters
reservations.getByIdSingle reservation detail
reservations.cancelReservationCancel reservation + trigger refund if paid
checkIns.createRecord check-in
notifications.sendConfirmationResend confirmation email/WhatsApp