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.
42 lines
1.4 KiB
Go
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"})
|
|
}
|