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:
@@ -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
|
SELECT id, start_time, duration_minutes, description, cron_expression, created_at, created_by
|
||||||
FROM time_blockers
|
FROM time_blockers
|
||||||
WHERE cron_expression IS NULL
|
WHERE cron_expression IS NULL
|
||||||
|
AND description NOT LIKE 'RESERVATION:%'
|
||||||
AND start_time >= $1 AND start_time <= $2
|
AND start_time >= $1 AND start_time <= $2
|
||||||
ORDER BY start_time
|
ORDER BY start_time
|
||||||
`, start, end)
|
`, start, end)
|
||||||
|
|||||||
+13
-15
@@ -137,15 +137,16 @@ func main() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Public booking endpoints (optional auth for slot reservation)
|
// Public booking endpoints (optional auth for slot reservation and guest bookings)
|
||||||
r.With(mw.RateLimit(30, time.Minute), mw.OptionalAuth).Post("/bookings/reserve", bookings.ReserveSlotHandler)
|
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)
|
// Guest user creation (public, no auth required)
|
||||||
r.With(mw.RateLimit(10, time.Minute)).Post("/users/guest", user.CreateGuestUserHandler)
|
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
|
// Authenticated users
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(mw.RequireAuth)
|
r.Use(mw.RequireAuth)
|
||||||
@@ -160,16 +161,13 @@ func main() {
|
|||||||
r.Delete("/user/account", user.DeleteAccountHandler)
|
r.Delete("/user/account", user.DeleteAccountHandler)
|
||||||
r.Get("/user/loyalty", user.GetLoyaltyHandler)
|
r.Get("/user/loyalty", user.GetLoyaltyHandler)
|
||||||
|
|
||||||
r.Route("/bookings", func(r chi.Router) {
|
r.Get("/bookings", bookings.GetAllUserBookingsHandler)
|
||||||
r.Get("/", bookings.GetAllUserBookingsHandler)
|
r.Get("/bookings/{id}", bookings.GetBookingHandler)
|
||||||
r.Get("/{id}", bookings.GetBookingHandler)
|
r.Get("/bookings/{id}/calendar", bookings.GetBookingCalendarHandler)
|
||||||
r.Get("/{id}/calendar", bookings.GetBookingCalendarHandler)
|
r.Put("/bookings/{id}", bookings.EditBookingHandler)
|
||||||
r.Put("/{id}", bookings.EditBookingHandler)
|
r.Delete("/bookings/{id}", bookings.DeleteBookingHandler)
|
||||||
r.Delete("/{id}", bookings.DeleteBookingHandler)
|
r.Post("/bookings/{id}/edit-request", bookings.RequestEditHandler)
|
||||||
// Edit request endpoints
|
r.Delete("/bookings/{id}/edit-request", bookings.DeleteEditRequestHandler)
|
||||||
r.Post("/{id}/edit-request", bookings.RequestEditHandler)
|
|
||||||
r.Delete("/{id}/edit-request", bookings.DeleteEditRequestHandler)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Admin-only (no rate limit - trusted users with authenticated sessions)
|
// Admin-only (no rate limit - trusted users with authenticated sessions)
|
||||||
|
|||||||
+47
-16
@@ -464,23 +464,21 @@ create_admin_booking() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Returns the nearest OPEN business day at or after the given date.
|
# 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,...,6=Sat (GNU date %w)
|
||||||
# dow: date's day-of-week as 0=Sun,1=Mon,...,6=Sat (GNU date %w)
|
|
||||||
open_day() {
|
open_day() {
|
||||||
local d="$1"
|
local d="$1"
|
||||||
local max=7
|
local max=7
|
||||||
for ((i=0; i<max; i++)); do
|
for ((i=0; i<max; i++)); do
|
||||||
local dow
|
local dow
|
||||||
dow=$(TZ=Europe/London date -d "$d" +%w) # 0=Sun, 1=Mon, 6=Sat
|
dow=$(TZ=Europe/London date -d "$d" +%w)
|
||||||
# Closed: Sunday (0) or Monday (1)
|
if [[ "$dow" == "0" || "$dow" == "6" ]]; then
|
||||||
if [[ "$dow" == "0" || "$dow" == "1" ]]; then
|
|
||||||
d=$(TZ=Europe/London date -d "$d +1 day" +%Y-%m-%d)
|
d=$(TZ=Europe/London date -d "$d +1 day" +%Y-%m-%d)
|
||||||
else
|
else
|
||||||
echo "$d"
|
echo "$d"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
echo "$d" # fallback
|
echo "$d"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Same but shifts BACKWARDS to find an open day (for past bookings)
|
# 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
|
for ((i=0; i<max; i++)); do
|
||||||
local dow
|
local dow
|
||||||
dow=$(TZ=Europe/London date -d "$d" +%w)
|
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)
|
d=$(TZ=Europe/London date -d "$d -1 day" +%Y-%m-%d)
|
||||||
else
|
else
|
||||||
echo "$d"
|
echo "$d"
|
||||||
@@ -728,35 +726,68 @@ count_guest=0
|
|||||||
|
|
||||||
if [[ -n "$GUEST1_ID" ]]; then
|
if [[ -n "$GUEST1_ID" ]]; then
|
||||||
echo "${C_GREEN}✅ Created guest: Nina ($GUEST1_ID)${C_RESET}"
|
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)
|
# Book 16 days out — beyond the upcoming loop range (day_offset 2-15)
|
||||||
D_G1=$(open_day "$(TZ=Europe/London date -d "$TODAY +3 days" +%Y-%m-%d)")
|
D_G1=$(open_day "$(TZ=Europe/London date -d "$TODAY +16 days" +%Y-%m-%d)")
|
||||||
GUEST1_TIME=$(format_london_time "$D_G1" "$SLOT_B")
|
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)\"]}"
|
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_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)
|
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
|
fi
|
||||||
|
|
||||||
if [[ -n "$GUEST2_ID" ]]; then
|
if [[ -n "$GUEST2_ID" ]]; then
|
||||||
echo "${C_GREEN}✅ Created guest: Bob ($GUEST2_ID)${C_RESET}"
|
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)
|
# 20 days out — beyond the upcoming loop range
|
||||||
D_G2=$(open_day "$(TZ=Europe/London date -d "$TODAY +5 days" +%Y-%m-%d)")
|
D_G2=$(open_day "$(TZ=Europe/London date -d "$TODAY +20 days" +%Y-%m-%d)")
|
||||||
GUEST2_TIME=$(format_london_time "$D_G2" "$SLOT_C")
|
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)\"]}"
|
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_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)
|
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
|
fi
|
||||||
|
|
||||||
if [[ -n "$GUEST3_ID" ]]; then
|
if [[ -n "$GUEST3_ID" ]]; then
|
||||||
echo "${C_GREEN}✅ Created guest: Carol ($GUEST3_ID)${C_RESET}"
|
echo "${C_GREEN}✅ Created guest: Carol ($GUEST3_ID)${C_RESET}"
|
||||||
# 3 days out, SLOT_D — free (different slot from Nina, same day)
|
# 22 days out — beyond the upcoming loop range
|
||||||
D_G3=$(open_day "$(TZ=Europe/London date -d "$TODAY +3 days" +%Y-%m-%d)")
|
D_G3=$(open_day "$(TZ=Europe/London date -d "$TODAY +22 days" +%Y-%m-%d)")
|
||||||
GUEST3_TIME=$(format_london_time "$D_G3" "$SLOT_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)\"]}"
|
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_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)
|
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
|
fi
|
||||||
|
|
||||||
if [[ -n "$GUEST4_ID" && "$GUEST4_ID" != "$GUEST1_ID" ]]; then
|
if [[ -n "$GUEST4_ID" && "$GUEST4_ID" != "$GUEST1_ID" ]]; then
|
||||||
|
|||||||
Reference in New Issue
Block a user