From a2e0a8e05c24508756d9b24103cd489af1730f58 Mon Sep 17 00:00:00 2001 From: Stephen Adamson Date: Mon, 2 Mar 2026 11:03:59 +0000 Subject: [PATCH] update tests --- backend/handlers/bookings/bookings.go | 52 +++++++++++++++++++++++++++ backend/handlers/bookings/manage.go | 17 +++++++++ 2 files changed, 69 insertions(+) diff --git a/backend/handlers/bookings/bookings.go b/backend/handlers/bookings/bookings.go index 8f560aa..4ae9ff8 100644 --- a/backend/handlers/bookings/bookings.go +++ b/backend/handlers/bookings/bookings.go @@ -1240,8 +1240,35 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) { if req.StartTime.Before(time.Now()) { http.Error(w, "Start time cannot be in the past", http.StatusBadRequest) return + return } + // Validate booking fits within operating hours for regular users + var svcDuration int + if err := db.DB.QueryRow(r.Context(), ` + SELECT COALESCE(SUM(duration_minutes), 0) FROM services WHERE id = ANY($1) + `, req.ServiceIDs).Scan(&svcDuration); err != nil { + log.Printf("Failed to calc duration: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + weekday := int(req.StartTime.Weekday()) + var closeStr string + if err := db.DB.QueryRow(r.Context(), `SELECT end_time::text FROM working_hours WHERE weekday = $1`, weekday).Scan(&closeStr); err != nil { + log.Printf("Failed to get hours: %v", err) + http.Error(w, "Could not verify hours", http.StatusInternalServerError) + return + } + + endTime := req.StartTime.Add(time.Duration(svcDuration) * time.Minute) + closeTime, _ := time.Parse("15:04:05", closeStr) + if endTime.Hour() > closeTime.Hour() || (endTime.Hour() == closeTime.Hour() && endTime.Minute() > closeTime.Minute()) { + http.Error(w, "Cannot book this time - services would extend beyond closing hours", http.StatusBadRequest) + return + } + + // Get created by from context (if available) var createdBy *string if creatorID, ok := r.Context().Value(mw.UserIDKey).(string); ok { @@ -1698,6 +1725,31 @@ func ConfirmBookingHandler(w http.ResponseWriter, r *http.Request) { } } + // Check for overlapping confirmed/in_progress/completed bookings before confirming + var bkStart time.Time + if err := db.DB.QueryRow(r.Context(), "SELECT start_time FROM bookings WHERE id = $1", bookingID).Scan(&bkStart); err != nil { + log.Printf("Failed to get start time: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + var dur int + db.DB.QueryRow(r.Context(), ` + SELECT COALESCE(SUM(COALESCE(bs.override_duration_minutes, s.duration_minutes)), 60) + FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = $1 + `, bookingID).Scan(&dur) + newEnd := bkStart.Add(time.Duration(dur) * time.Minute) + + var cnt int + db.DB.QueryRow(r.Context(), ` + SELECT COUNT(*) FROM bookings WHERE id != $1 AND status IN ('confirmed','in_progress','completed') AND start_time < $3 AND start_time + (INTERVAL '1 minute' * (SELECT COALESCE(SUM(COALESCE(bs.override_duration_minutes,s.duration_minutes)),60) FROM booking_services bs JOIN services s ON bs.service_id=s.id WHERE bs.booking_id=bookings.id)) > $2 + `, bookingID, bkStart, newEnd).Scan(&cnt) + if cnt > 0 { + http.Error(w, "Cannot confirm - time slot overlaps with existing booking", http.StatusConflict) + return + } + + tx, err := db.DB.Begin(r.Context()) if err != nil { log.Printf("Failed to start transaction: %v", err) diff --git a/backend/handlers/bookings/manage.go b/backend/handlers/bookings/manage.go index 81ba8fc..481951a 100644 --- a/backend/handlers/bookings/manage.go +++ b/backend/handlers/bookings/manage.go @@ -537,8 +537,25 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) { if isClosed { http.Error(w, "Cannot book during holiday hours when the salon is closed", http.StatusConflict) return + return } + // Check for overlapping confirmed/in_progress/completed bookings + var dur int + db.DB.QueryRow(r.Context(), ` + SELECT COALESCE(SUM(duration_minutes), 0) FROM services WHERE id = ANY($1) + `, req.ServiceIDs).Scan(&dur) + newEnd := req.StartTime.Add(time.Duration(dur) * time.Minute) + var cnt int + db.DB.QueryRow(r.Context(), ` + SELECT COUNT(*) FROM bookings WHERE status IN ('confirmed','in_progress','completed') AND start_time < $2 AND start_time + (INTERVAL '1 minute' * (SELECT COALESCE(SUM(COALESCE(bs.override_duration_minutes,s.duration_minutes)),60) FROM booking_services bs JOIN services s ON bs.service_id=s.id WHERE bs.booking_id=bookings.id)) > $1 + `, req.StartTime, newEnd).Scan(&cnt) + if cnt > 0 { + http.Error(w, "Cannot create booking - time slot overlaps with existing booking", http.StatusConflict) + return + } + + tx, err := db.DB.Begin(r.Context()) if err != nil { log.Printf("Failed to start transaction: %v", err)