Files
Crussell/backend/handlers/bookings/admin_cancel_reservation.go
T
popertots c2aa0b93cb feat(api): add admin reservation cancel endpoint
Add DELETE /api/admin/bookings/reserve to release admin walk-in/call-in reservations.

New handler AdminCancelReservationHandler targets only RESERVATION:admin:%
entries (partitioned from user RESERVATION:user:% by WHERE clause). Includes
12 tests covering walkin + callin success, isolation, no-op, unauth, empty ctx,
walkin+callin coexistence, anon untouched, response format parity, overlapping
reservations deleted, user reservations untouched, and idempotent double-cancel.

Inverse-isolation tests in cancel_reservation_test.go prove the user-side
DELETE /api/bookings/reserve does not touch admin or anon reservations.
2026-07-06 17:58:03 +01:00

42 lines
1.4 KiB
Go

package bookings
import (
"log"
"net/http"
"crussell/db"
"crussell/mw"
)
// AdminCancelReservationHandler releases the authenticated admin's active
// reservation (walk-in or call-in) by deleting their RESERVATION:admin entries
// from time_blockers. It does NOT create a booking — it only releases the
// temporary slot hold.
//
// Mirrors CancelReservationHandler but targets RESERVATION:admin:% (created by
// AdminReserveSlotHandler). The user-facing DELETE /api/bookings/reserve only
// matches RESERVATION:user:% — it cannot release admin reservations, which
// would otherwise leak the slot for the full 15-min TTL.
//
// DELETE /api/admin/bookings/reserve
func AdminCancelReservationHandler(w http.ResponseWriter, r *http.Request) {
adminID, ok := r.Context().Value(mw.UserIDKey).(string)
if !ok || adminID == "" {
log.Printf("AdminCancelReservationHandler: admin ID not found in context")
http.Error(w, "Authentication required", http.StatusUnauthorized)
return
}
_, err := db.Conn.Exec(r.Context(), `
DELETE FROM time_blockers
WHERE created_by = $1 AND description LIKE 'RESERVATION:admin:%'
`, adminID)
if err != nil {
log.Printf("AdminCancelReservationHandler: failed to delete reservation for admin %s: %v", adminID, err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
mw.RespondJSON(w, http.StatusOK, map[string]string{"status": "reservation cancelled"})
}