fix: resolve guest booking failures from router conflict, reservation self-block, and closed-day miscalculation

- backend/main.go: Flatten /bookings/* sub-Route to explicit paths to prevent
  RequireAuth middleware from bleeding into OptionalAuth POST /bookings
- backend/handlers/scheduling/time-blockers.go: Exclude RESERVATION:* entries
  from GetTimeBlockersInRange so overlap checks dont reject the users own
  reservation before CreateBookingHandler can delete it
- local-dev-2.sh: Fix open_day to skip Saturday (6) not Monday (1), matching
  working_hours schema; move guest booking dates to +16/+20/+22 days beyond
  the upcoming loop range; add reserve-then-book step mirroring frontend flow
This commit is contained in:
2026-04-30 16:06:51 +01:00
parent 819a7afb76
commit 7b396b7a9d
3 changed files with 70 additions and 40 deletions
@@ -196,6 +196,7 @@ func GetTimeBlockersInRange(ctx context.Context, start, end time.Time) ([]TimeBl
SELECT id, start_time, duration_minutes, description, cron_expression, created_at, created_by
FROM time_blockers
WHERE cron_expression IS NULL
AND description NOT LIKE 'RESERVATION:%'
AND start_time >= $1 AND start_time <= $2
ORDER BY start_time
`, start, end)
+13 -15
View File
@@ -137,15 +137,16 @@ func main() {
})
})
// Public booking endpoints (optional auth for slot reservation)
r.With(mw.RateLimit(30, time.Minute), mw.OptionalAuth).Post("/bookings/reserve", bookings.ReserveSlotHandler)
// Public booking endpoints (optional auth for slot reservation and guest bookings)
r.Group(func(r chi.Router) {
r.Use(mw.RateLimit(30, time.Minute), mw.OptionalAuth)
r.Post("/bookings/reserve", bookings.ReserveSlotHandler)
r.Post("/bookings", bookings.CreateBookingHandler)
})
// Guest user creation (public, no auth required)
r.With(mw.RateLimit(10, time.Minute)).Post("/users/guest", user.CreateGuestUserHandler)
// Booking creation (accepts both authenticated and guest users)
r.With(mw.RateLimit(30, time.Minute), mw.OptionalAuth).Post("/bookings", bookings.CreateBookingHandler)
// Authenticated users
r.Group(func(r chi.Router) {
r.Use(mw.RequireAuth)
@@ -160,16 +161,13 @@ func main() {
r.Delete("/user/account", user.DeleteAccountHandler)
r.Get("/user/loyalty", user.GetLoyaltyHandler)
r.Route("/bookings", func(r chi.Router) {
r.Get("/", bookings.GetAllUserBookingsHandler)
r.Get("/{id}", bookings.GetBookingHandler)
r.Get("/{id}/calendar", bookings.GetBookingCalendarHandler)
r.Put("/{id}", bookings.EditBookingHandler)
r.Delete("/{id}", bookings.DeleteBookingHandler)
// Edit request endpoints
r.Post("/{id}/edit-request", bookings.RequestEditHandler)
r.Delete("/{id}/edit-request", bookings.DeleteEditRequestHandler)
})
r.Get("/bookings", bookings.GetAllUserBookingsHandler)
r.Get("/bookings/{id}", bookings.GetBookingHandler)
r.Get("/bookings/{id}/calendar", bookings.GetBookingCalendarHandler)
r.Put("/bookings/{id}", bookings.EditBookingHandler)
r.Delete("/bookings/{id}", bookings.DeleteBookingHandler)
r.Post("/bookings/{id}/edit-request", bookings.RequestEditHandler)
r.Delete("/bookings/{id}/edit-request", bookings.DeleteEditRequestHandler)
})
// Admin-only (no rate limit - trusted users with authenticated sessions)
+47 -16
View File
@@ -464,23 +464,21 @@ create_admin_booking() {
}
# Returns the nearest OPEN business day at or after the given date.
# Schema: Monday=0 closed, Sunday=6 closed. Tue-Sat open.
# dow: date's day-of-week as 0=Sun,1=Mon,...,6=Sat (GNU date %w)
# dow: date's day-of-week as 0=Sun,...,6=Sat (GNU date %w)
open_day() {
local d="$1"
local max=7
for ((i=0; i<max; i++)); do
local dow
dow=$(TZ=Europe/London date -d "$d" +%w) # 0=Sun, 1=Mon, 6=Sat
# Closed: Sunday (0) or Monday (1)
if [[ "$dow" == "0" || "$dow" == "1" ]]; then
dow=$(TZ=Europe/London date -d "$d" +%w)
if [[ "$dow" == "0" || "$dow" == "6" ]]; then
d=$(TZ=Europe/London date -d "$d +1 day" +%Y-%m-%d)
else
echo "$d"
return
fi
done
echo "$d" # fallback
echo "$d"
}
# Same but shifts BACKWARDS to find an open day (for past bookings)
@@ -490,7 +488,7 @@ open_day_past() {
for ((i=0; i<max; i++)); do
local dow
dow=$(TZ=Europe/London date -d "$d" +%w)
if [[ "$dow" == "0" || "$dow" == "1" ]]; then
if [[ "$dow" == "0" || "$dow" == "6" ]]; then
d=$(TZ=Europe/London date -d "$d -1 day" +%Y-%m-%d)
else
echo "$d"
@@ -728,35 +726,68 @@ count_guest=0
if [[ -n "$GUEST1_ID" ]]; then
echo "${C_GREEN}✅ Created guest: Nina ($GUEST1_ID)${C_RESET}"
# Book 3 days out — SLOT_B is free that day (upcoming pattern puts Isla in SLOT_B on day 5)
D_G1=$(open_day "$(TZ=Europe/London date -d "$TODAY +3 days" +%Y-%m-%d)")
# Book 16 days out — beyond the upcoming loop range (day_offset 2-15)
D_G1=$(open_day "$(TZ=Europe/London date -d "$TODAY +16 days" +%Y-%m-%d)")
GUEST1_TIME=$(format_london_time "$D_G1" "$SLOT_B")
# Step 1: Reserve the slot (matches frontend flow)
GUEST1_RESERVE_JSON="{\"start_time\":\"$GUEST1_TIME\",\"service_ids\":[\"$(get_svc 0)\"]}"
G1_RES_RESP=$(curl -s -w "\n%{http_code}" -X POST -H 'Content-Type: application/json' -d "$GUEST1_RESERVE_JSON" "$BASE_URL/bookings/reserve")
G1_RES_CODE=$(echo "$G1_RES_RESP" | tail -n1)
if [[ ! "$G1_RES_CODE" =~ ^2 ]]; then
G1_RES_BODY=$(echo "$G1_RES_RESP" | sed '$d')
echo "${C_RED}❌ Guest reserve failed: Nina (HTTP $G1_RES_CODE) — $G1_RES_BODY${C_RESET}"
else
# Step 2: Create the booking
GUEST1_JSON="{\"user_id\":\"$GUEST1_ID\",\"start_time\":\"$GUEST1_TIME\",\"service_ids\":[\"$(get_svc 0)\"]}"
G1_RESP=$(curl -s -w "\n%{http_code}" -X POST -H 'Content-Type: application/json' -d "$GUEST1_JSON" "$BASE_URL/bookings")
G1_CODE=$(echo "$G1_RESP" | tail -n1)
if [[ "$G1_CODE" =~ ^2 ]]; then count_guest=$((count_guest+1)); echo "${C_GREEN}✅ Guest booking: Nina - Classic Manicure ($D_G1)${C_RESET}"; fi
if [[ "$G1_CODE" =~ ^2 ]]; then count_guest=$((count_guest+1)); echo "${C_GREEN}✅ Guest booking: Nina - Classic Manicure ($D_G1)${C_RESET}";
else echo "${C_RED}❌ Guest booking failed: Nina (HTTP $G1_CODE) — $(echo "$G1_RESP" | sed '$d')${C_RESET}"; fi
fi
fi
if [[ -n "$GUEST2_ID" ]]; then
echo "${C_GREEN}✅ Created guest: Bob ($GUEST2_ID)${C_RESET}"
# 5 days out — SLOT_C is free (upcoming pattern puts Ava in SLOT_A that day)
D_G2=$(open_day "$(TZ=Europe/London date -d "$TODAY +5 days" +%Y-%m-%d)")
# 20 days out — beyond the upcoming loop range
D_G2=$(open_day "$(TZ=Europe/London date -d "$TODAY +20 days" +%Y-%m-%d)")
GUEST2_TIME=$(format_london_time "$D_G2" "$SLOT_C")
# Step 1: Reserve the slot
GUEST2_RESERVE_JSON="{\"start_time\":\"$GUEST2_TIME\",\"service_ids\":[\"$(get_svc 3)\"]}"
G2_RES_RESP=$(curl -s -w "\n%{http_code}" -X POST -H 'Content-Type: application/json' -d "$GUEST2_RESERVE_JSON" "$BASE_URL/bookings/reserve")
G2_RES_CODE=$(echo "$G2_RES_RESP" | tail -n1)
if [[ ! "$G2_RES_CODE" =~ ^2 ]]; then
G2_RES_BODY=$(echo "$G2_RES_RESP" | sed '$d')
echo "${C_RED}❌ Guest reserve failed: Bob (HTTP $G2_RES_CODE) — $G2_RES_BODY${C_RESET}"
else
# Step 2: Create the booking
GUEST2_JSON="{\"user_id\":\"$GUEST2_ID\",\"start_time\":\"$GUEST2_TIME\",\"service_ids\":[\"$(get_svc 3)\"]}"
G2_RESP=$(curl -s -w "\n%{http_code}" -X POST -H 'Content-Type: application/json' -d "$GUEST2_JSON" "$BASE_URL/bookings")
G2_CODE=$(echo "$G2_RESP" | tail -n1)
if [[ "$G2_CODE" =~ ^2 ]]; then count_guest=$((count_guest+1)); echo "${C_GREEN}✅ Guest booking: Bob - Express Mani & Pedi ($D_G2)${C_RESET}"; fi
if [[ "$G2_CODE" =~ ^2 ]]; then count_guest=$((count_guest+1)); echo "${C_GREEN}✅ Guest booking: Bob - Express Mani & Pedi ($D_G2)${C_RESET}";
else echo "${C_RED}❌ Guest booking failed: Bob (HTTP $G2_CODE) — $(echo "$G2_RESP" | sed '$d')${C_RESET}"; fi
fi
fi
if [[ -n "$GUEST3_ID" ]]; then
echo "${C_GREEN}✅ Created guest: Carol ($GUEST3_ID)${C_RESET}"
# 3 days out, SLOT_D — free (different slot from Nina, same day)
D_G3=$(open_day "$(TZ=Europe/London date -d "$TODAY +3 days" +%Y-%m-%d)")
# 22 days out — beyond the upcoming loop range
D_G3=$(open_day "$(TZ=Europe/London date -d "$TODAY +22 days" +%Y-%m-%d)")
GUEST3_TIME=$(format_london_time "$D_G3" "$SLOT_D")
# Step 1: Reserve the slot
GUEST3_RESERVE_JSON="{\"start_time\":\"$GUEST3_TIME\",\"service_ids\":[\"$(get_svc 1)\"]}"
G3_RES_RESP=$(curl -s -w "\n%{http_code}" -X POST -H 'Content-Type: application/json' -d "$GUEST3_RESERVE_JSON" "$BASE_URL/bookings/reserve")
G3_RES_CODE=$(echo "$G3_RES_RESP" | tail -n1)
if [[ ! "$G3_RES_CODE" =~ ^2 ]]; then
G3_RES_BODY=$(echo "$G3_RES_RESP" | sed '$d')
echo "${C_RED}❌ Guest reserve failed: Carol (HTTP $G3_RES_CODE) — $G3_RES_BODY${C_RESET}"
else
# Step 2: Create the booking
GUEST3_JSON="{\"user_id\":\"$GUEST3_ID\",\"start_time\":\"$GUEST3_TIME\",\"service_ids\":[\"$(get_svc 1)\"]}"
G3_RESP=$(curl -s -w "\n%{http_code}" -X POST -H 'Content-Type: application/json' -d "$GUEST3_JSON" "$BASE_URL/bookings")
G3_CODE=$(echo "$G3_RESP" | tail -n1)
if [[ "$G3_CODE" =~ ^2 ]]; then count_guest=$((count_guest+1)); echo "${C_GREEN}✅ Guest booking: Carol - Gel Manicure ($D_G3)${C_RESET}"; fi
if [[ "$G3_CODE" =~ ^2 ]]; then count_guest=$((count_guest+1)); echo "${C_GREEN}✅ Guest booking: Carol - Gel Manicure ($D_G3)${C_RESET}";
else echo "${C_RED}❌ Guest booking failed: Carol (HTTP $G3_CODE) — $(echo "$G3_RESP" | sed '$d')${C_RESET}"; fi
fi
fi
if [[ -n "$GUEST4_ID" && "$GUEST4_ID" != "$GUEST1_ID" ]]; then