From 7ea6fab3affcaa93c739dde7a0f72e12fbb8963c Mon Sep 17 00:00:00 2001 From: Stephen Adamson Date: Sun, 21 Jun 2026 21:47:12 +0100 Subject: [PATCH] refactor(bookings): replace inline computed queries with booking fields Simplify bookings handlers by using bookings.total_duration_minutes, bookings.total_amount, and bookings.end_time computed columns instead of inline UNION sub-queries against booking_services/booking_custom_services. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- backend/handlers/bookings/admin_reserve.go | 12 +- backend/handlers/bookings/bookings.go | 209 +++------------------ backend/handlers/bookings/manage.go | 70 ++----- backend/handlers/bookings/reserve.go | 12 +- 4 files changed, 41 insertions(+), 262 deletions(-) diff --git a/backend/handlers/bookings/admin_reserve.go b/backend/handlers/bookings/admin_reserve.go index 85ea5d4..0af0392 100644 --- a/backend/handlers/bookings/admin_reserve.go +++ b/backend/handlers/bookings/admin_reserve.go @@ -110,7 +110,7 @@ func AdminReserveSlotHandler(w http.ResponseWriter, r *http.Request) { // DB uses 0=Monday..6=Sunday; Go uses 0=Sunday..6=Saturday. Convert. weekday := int((localStart.Weekday() + 6) % 7) var closeStr string - if err := db.Conn.QueryRow(r.Context(), `SELECT end_time::text FROM working_hours WHERE weekday = $1`, weekday).Scan(&closeStr); err != nil { + if err := db.Conn.QueryRow(r.Context(), `SELECT end_time FROM working_hours WHERE weekday = $1`, weekday).Scan(&closeStr); err != nil { if errors.Is(err, pgx.ErrNoRows) { http.Error(w, "Not open on this day", http.StatusBadRequest) return @@ -132,15 +132,7 @@ func AdminReserveSlotHandler(w http.ResponseWriter, r *http.Request) { if err := db.Conn.QueryRow(r.Context(), ` SELECT COUNT(*) FROM bookings WHERE status IN ('pending','confirmed','in_progress','completed') AND start_time < $2 - AND start_time + (INTERVAL '1 minute' * ( - SELECT COALESCE(SUM(dur),60) FROM ( - SELECT COALESCE(bs.override_duration_minutes,s.duration_minutes) AS dur - FROM booking_services bs JOIN services s ON bs.service_id=s.id WHERE bs.booking_id=bookings.id - UNION ALL - SELECT COALESCE(bcs.override_duration_minutes,cs.duration_minutes) - FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id=cs.id WHERE bcs.booking_id=bookings.id - ) sub - )) > $1 + AND end_time > $1 `, req.StartTime, endTime).Scan(&cnt); err != nil { log.Printf("Failed to check overlap: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) diff --git a/backend/handlers/bookings/bookings.go b/backend/handlers/bookings/bookings.go index a01f0ed..c1a8ebe 100644 --- a/backend/handlers/bookings/bookings.go +++ b/backend/handlers/bookings/bookings.go @@ -428,23 +428,11 @@ func GetAllUserBookingsHandler(w http.ResponseWriter, r *http.Request) { dataQuery := ` SELECT b.id, b.start_time, b.status, b.notes, b.created_at, b.updated_at, b.created_by, - (SELECT COALESCE(SUM(price_val), 0) FROM ( - SELECT COALESCE(bs.override_price, s.price) AS price_val - FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = b.id - UNION ALL - SELECT COALESCE(bcs.override_price, cs.price) - FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = b.id - ) sub) AS total_amount, + (SELECT total_amount FROM bookings WHERE id = b.id) AS total_amount, (SELECT COALESCE(SUM(amount), 0) FROM payments WHERE booking_id = b.id AND status = 'completed') AS amount_paid, - (SELECT COALESCE(SUM(dur_val), 0) FROM ( - SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur_val - FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = b.id - UNION ALL - SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes) - FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = b.id - ) sub) AS duration_minutes, + (SELECT total_duration_minutes FROM bookings WHERE id = b.id) AS duration_minutes, b.deposit_required, (SELECT COALESCE(SUM(amount), 0) FROM payments @@ -1440,15 +1428,7 @@ func UpdateBookingServicesHandler(w http.ResponseWriter, r *http.Request) { WHERE id != $1 AND status IN ('confirmed', 'pending', 'in_progress', 'completed') AND start_time < $3 - AND start_time + (INTERVAL '1 minute' * ( - SELECT COALESCE(SUM(dur), 60) FROM ( - SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur - FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = bookings.id - UNION ALL - SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes) - FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = bookings.id - ) sub - )) > $2 + AND end_time > $2 `, bookingID, startTime, newEndTime).Scan(&overlapCount) if err != nil { log.Printf("Failed to check overlap: %v", err) @@ -2176,7 +2156,7 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) { // DB uses 0=Monday..6=Sunday; Go uses 0=Sunday..6=Saturday. Convert. weekday := int((localStart.Weekday() + 6) % 7) var closeStr string - if err := db.Conn.QueryRow(r.Context(), `SELECT end_time::text FROM working_hours WHERE weekday = $1`, weekday).Scan(&closeStr); err != nil { + if err := db.Conn.QueryRow(r.Context(), `SELECT end_time 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 @@ -2209,15 +2189,7 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) { if err := tx.QueryRow(r.Context(), ` SELECT COUNT(*) FROM bookings WHERE status IN ('pending','confirmed','in_progress','completed') AND start_time < $2 - AND start_time + (INTERVAL '1 minute' * ( - SELECT COALESCE(SUM(dur), 60) FROM ( - SELECT COALESCE(bs.override_duration_minutes,s.duration_minutes) AS dur - FROM booking_services bs JOIN services s ON bs.service_id=s.id WHERE bs.booking_id=bookings.id - UNION ALL - SELECT COALESCE(bcs.override_duration_minutes,cs.duration_minutes) - FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id=cs.id WHERE bcs.booking_id=bookings.id - ) sub - )) > $1 + AND end_time > $1 `, req.StartTime, endTime).Scan(&cnt); err != nil { log.Printf("Failed to check slot overlap: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) @@ -2406,17 +2378,7 @@ func EditBookingHandler(w http.ResponseWriter, r *http.Request) { var durationMinutes int if err := db.Conn.QueryRow(r.Context(), ` - SELECT COALESCE(SUM(dur), 60) FROM ( - SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur - FROM booking_services bs - JOIN services s ON bs.service_id = s.id - WHERE bs.booking_id = $1 - UNION ALL - SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes) - FROM booking_custom_services bcs - JOIN custom_services cs ON bcs.custom_service_id = cs.id - WHERE bcs.booking_id = $1 - ) sub + SELECT total_duration_minutes FROM bookings WHERE id = $1 `, bookingID).Scan(&durationMinutes); err != nil { log.Printf("Failed to get booking duration %s: %v", bookingID, err) durationMinutes = 60 @@ -2445,15 +2407,7 @@ func EditBookingHandler(w http.ResponseWriter, r *http.Request) { WHERE id != $1 AND status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'deposit_lapsed') AND start_time < $3 - AND start_time + (INTERVAL '1 minute' * ( - SELECT COALESCE(SUM(dur), 60) FROM ( - SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur - FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = bookings.id - UNION ALL - SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes) - FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = bookings.id - ) sub - )) > $2 + AND end_time > $2 `, bookingID, req.StartTime, newEndTime).Scan(&overlapCount); err != nil { log.Printf("Failed to check overlap %s: %v", bookingID, err) http.Error(w, "Internal server error", http.StatusInternalServerError) @@ -2631,17 +2585,7 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) { var bookingTotal float64 if err := tx.QueryRow(r.Context(), ` - SELECT COALESCE(SUM(price_val), 0) FROM ( - SELECT COALESCE(bs.override_price, s.price) AS price_val - FROM booking_services bs - JOIN services s ON bs.service_id = s.id - WHERE bs.booking_id = $1 - UNION ALL - SELECT COALESCE(bcs.override_price, cs.price) - FROM booking_custom_services bcs - JOIN custom_services cs ON bcs.custom_service_id = cs.id - WHERE bcs.booking_id = $1 - ) sub + SELECT total_amount FROM bookings WHERE id = $1 `, bookingID).Scan(&bookingTotal); err != nil { log.Printf("Failed to calculate booking total for %s: %v", bookingID, err) } @@ -2955,13 +2899,7 @@ func ConfirmBookingHandler(w http.ResponseWriter, r *http.Request) { return } if err := db.Conn.QueryRow(r.Context(), ` - SELECT COALESCE(SUM(dur), 60) FROM ( - SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur - FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = $1 - UNION ALL - SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes) - FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = $1 - ) sub + SELECT total_duration_minutes FROM bookings WHERE id = $1 `, bookingID).Scan(&dur); err != nil { log.Printf("Failed to calculate duration on confirm: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) @@ -2987,15 +2925,7 @@ func ConfirmBookingHandler(w http.ResponseWriter, r *http.Request) { if err := tx.QueryRow(r.Context(), ` SELECT COUNT(*) FROM bookings WHERE id != $1 AND status IN ('pending','confirmed','in_progress','completed') AND start_time < $3 - AND start_time + (INTERVAL '1 minute' * ( - SELECT COALESCE(SUM(dur), 60) FROM ( - SELECT COALESCE(bs.override_duration_minutes,s.duration_minutes) AS dur - FROM booking_services bs JOIN services s ON bs.service_id=s.id WHERE bs.booking_id=bookings.id - UNION ALL - SELECT COALESCE(bcs.override_duration_minutes,cs.duration_minutes) - FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id=cs.id WHERE bcs.booking_id=bookings.id - ) sub - )) > $2 + AND end_time > $2 `, bookingID, bkStart, endTime).Scan(&cnt); err != nil { log.Printf("Failed to check overlap on confirm: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) @@ -3100,17 +3030,7 @@ func ConfirmBookingHandler(w http.ResponseWriter, r *http.Request) { if dav.Service != nil { var durationMinutes int db.Conn.QueryRow(r.Context(), ` - SELECT COALESCE(SUM(dur), 60) FROM ( - SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur - FROM booking_services bs - JOIN services s ON bs.service_id = s.id - WHERE bs.booking_id = $1 - UNION ALL - SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes) - FROM booking_custom_services bcs - JOIN custom_services cs ON bcs.custom_service_id = cs.id - WHERE bcs.booking_id = $1 - ) sub + SELECT total_duration_minutes FROM bookings WHERE id = $1 `, bookingID).Scan(&durationMinutes) if durationMinutes == 0 { durationMinutes = 60 @@ -3547,15 +3467,7 @@ func GetBookingCalendarHandler(w http.ResponseWriter, r *http.Request) { if err := db.Conn.QueryRow(r.Context(), ` SELECT id, user_id, start_time, status, COALESCE(notes, ''), COALESCE(created_by, ''), created_at, updated_at, - COALESCE((SELECT COALESCE(SUM(dur), 60) FROM ( - SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur - FROM booking_services bs JOIN services s ON bs.service_id = s.id - WHERE bs.booking_id = bookings.id - UNION ALL - SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes) - FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id - WHERE bcs.booking_id = bookings.id - ) sub), 60) + total_duration_minutes FROM bookings WHERE id = $1 AND user_id = $2 `, bookingID, userID).Scan(&bookingIDDB, &userIDDB, &startTime, &status, ¬es, &createdBy, &createdAt, &updatedAt, &durationMinutes); err != nil { @@ -3697,13 +3609,7 @@ func GetOverlappingBookingsByTimeHandler(w http.ResponseWriter, r *http.Request) b.start_time, b.status, b.created_at, - (SELECT COALESCE(SUM(dur_val), 60) FROM ( - SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur_val - FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = b.id - UNION ALL - SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes) - FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = b.id - ) sub) as duration, + b.total_duration_minutes as duration, u.id as user_id, u.fn, u.email, @@ -3712,15 +3618,7 @@ func GetOverlappingBookingsByTimeHandler(w http.ResponseWriter, r *http.Request) LEFT JOIN users u ON b.user_id = u.id WHERE b.status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'deposit_lapsed') AND b.start_time < $2 - AND b.start_time + (INTERVAL '1 minute' * ( - SELECT COALESCE(SUM(dur_val), 60) FROM ( - SELECT COALESCE(bs2.override_duration_minutes, s2.duration_minutes) AS dur_val - FROM booking_services bs2 JOIN services s2 ON bs2.service_id = s2.id WHERE bs2.booking_id = b.id - UNION ALL - SELECT COALESCE(bcs2.override_duration_minutes, cs2.duration_minutes) - FROM booking_custom_services bcs2 JOIN custom_services cs2 ON bcs2.custom_service_id = cs2.id WHERE bcs2.booking_id = b.id - ) sub - )) > $1 + AND b.end_time > $1 GROUP BY b.id, b.start_time, b.status, b.created_at, u.id, u.fn, u.email, u.phone ORDER BY b.start_time ASC `, startTime, endTime) @@ -3790,14 +3688,7 @@ func GetOverlappingBookingsHandler(w http.ResponseWriter, r *http.Request) { var startTime time.Time var durationMinutes int if err := db.Conn.QueryRow(r.Context(), ` - SELECT b.start_time, - (SELECT COALESCE(SUM(dur_val), 60) FROM ( - SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur_val - FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = b.id - UNION ALL - SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes) - FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = b.id - ) sub) + SELECT b.start_time, b.total_duration_minutes FROM bookings b WHERE b.id = $1 `, bookingID).Scan(&startTime, &durationMinutes); err != nil { @@ -3819,13 +3710,7 @@ func GetOverlappingBookingsHandler(w http.ResponseWriter, r *http.Request) { b.start_time, b.status, b.created_at, - (SELECT COALESCE(SUM(dur_val), 60) FROM ( - SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur_val - FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = b.id - UNION ALL - SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes) - FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = b.id - ) sub) as duration, + b.total_duration_minutes as duration, u.fn, u.email FROM bookings b @@ -3833,15 +3718,7 @@ func GetOverlappingBookingsHandler(w http.ResponseWriter, r *http.Request) { WHERE b.id != $1 AND b.status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'deposit_lapsed') AND b.start_time < $3 - AND b.start_time + (INTERVAL '1 minute' * ( - SELECT COALESCE(SUM(dur_val), 60) FROM ( - SELECT COALESCE(bs2.override_duration_minutes, s2.duration_minutes) AS dur_val - FROM booking_services bs2 JOIN services s2 ON bs2.service_id = s2.id WHERE bs2.booking_id = b.id - UNION ALL - SELECT COALESCE(bcs2.override_duration_minutes, cs2.duration_minutes) - FROM booking_custom_services bcs2 JOIN custom_services cs2 ON bcs2.custom_service_id = cs2.id WHERE bcs2.booking_id = b.id - ) sub - )) > $2 + AND b.end_time > $2 GROUP BY b.id, b.start_time, b.status, b.created_at, u.fn, u.email ORDER BY b.created_at ASC `, bookingID, startTime, endTime) @@ -3928,13 +3805,7 @@ func GetBookingsByDateRangeHandler(w http.ResponseWriter, r *http.Request) { b.status, b.notes, b.created_at, - (SELECT COALESCE(SUM(dur_val), 60) FROM ( - SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur_val - FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = b.id - UNION ALL - SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes) - FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = b.id - ) sub) as duration, + b.total_duration_minutes as duration, u.id as user_id, u.fn, u.email, @@ -4035,13 +3906,7 @@ func GetBookingsByCreatedRangeHandler(w http.ResponseWriter, r *http.Request) { b.status, b.notes, b.created_at, - (SELECT COALESCE(SUM(dur_val), 60) FROM ( - SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur_val - FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = b.id - UNION ALL - SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes) - FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = b.id - ) sub) as duration, + b.total_duration_minutes as duration, u.id as user_id, u.fn, u.email, @@ -4190,17 +4055,7 @@ func AdminRescheduleBookingHandler(w http.ResponseWriter, r *http.Request) { var durationMinutes int if err := db.Conn.QueryRow(r.Context(), ` - SELECT COALESCE(SUM(dur), 60) FROM ( - SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur - FROM booking_services bs - JOIN services s ON bs.service_id = s.id - WHERE bs.booking_id = $1 - UNION ALL - SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes) - FROM booking_custom_services bcs - JOIN custom_services cs ON bcs.custom_service_id = cs.id - WHERE bcs.booking_id = $1 - ) sub + SELECT total_duration_minutes FROM bookings WHERE id = $1 `, bookingID).Scan(&durationMinutes); err != nil { log.Printf("Failed to get booking duration %s: %v", bookingID, err) durationMinutes = 60 @@ -4257,19 +4112,7 @@ func AdminRescheduleBookingHandler(w http.ResponseWriter, r *http.Request) { WHERE id != $1 AND status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'deposit_lapsed') AND start_time < $3 - AND start_time + (INTERVAL '1 minute' * ( - SELECT COALESCE(SUM(dur), 60) FROM ( - SELECT COALESCE(bs2.override_duration_minutes, s2.duration_minutes) AS dur - FROM booking_services bs2 - JOIN services s2 ON bs2.service_id = s2.id - WHERE bs2.booking_id = bookings.id - UNION ALL - SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes) - FROM booking_custom_services bcs - JOIN custom_services cs ON bcs.custom_service_id = cs.id - WHERE bcs.booking_id = bookings.id - ) sub - )) > $2 + AND end_time > $2 `, bookingID, req.StartTime, newEndTime).Scan(&overlapCount); err != nil { log.Printf("Failed to check overlap %s: %v", bookingID, err) http.Error(w, "Internal server error", http.StatusInternalServerError) @@ -4330,15 +4173,7 @@ func EvictPendingReleaseOverlapping(ctx context.Context, tx pgx.Tx, startTime, e UPDATE bookings SET status = 'deposit_lapsed', updated_at = NOW() WHERE status = 'pending_release' AND start_time < $2 - AND start_time + (INTERVAL '1 minute' * ( - SELECT COALESCE(SUM(dur), 60) FROM ( - SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur - FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = bookings.id - UNION ALL - SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes) - FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = bookings.id - ) sub - )) > $1 + AND end_time > $1 AND NOT EXISTS ( SELECT 1 FROM time_blockers WHERE description = 'PAYMENT_IN_FLIGHT:' || bookings.id diff --git a/backend/handlers/bookings/manage.go b/backend/handlers/bookings/manage.go index 2d6b178..cfb7e21 100644 --- a/backend/handlers/bookings/manage.go +++ b/backend/handlers/bookings/manage.go @@ -672,7 +672,7 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) { // Check for overlapping confirmed/in_progress/completed bookings (inside transaction) var cnt int err = tx.QueryRow(r.Context(), ` - SELECT COUNT(*) FROM bookings WHERE status IN ('pending','confirmed','in_progress','completed') AND start_time < $2 AND start_time + (INTERVAL '1 minute' * (SELECT COALESCE(SUM(dur),60) FROM (SELECT COALESCE(bs.override_duration_minutes,s.duration_minutes) AS dur FROM booking_services bs JOIN services s ON bs.service_id=s.id WHERE bs.booking_id=bookings.id UNION ALL SELECT COALESCE(bcs.override_duration_minutes,cs.duration_minutes) FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id=cs.id WHERE bcs.booking_id=bookings.id) sub)) > $1 + SELECT COUNT(*) FROM bookings WHERE status IN ('pending','confirmed','in_progress','completed') AND start_time < $2 AND end_time > $1 `, req.StartTime, newEnd).Scan(&cnt) if err != nil { log.Printf("Failed to check overlap: %v", err) @@ -755,12 +755,10 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) { http.Error(w, "Internal server error", http.StatusInternalServerError) return } - for _, csID := range req.CustomServiceIDs { - if _, err := tx.Exec(r.Context(), ` - UPDATE custom_services SET usage_count = usage_count + 1, last_used_at = NOW() WHERE id = $1 - `, csID); err != nil { - log.Printf("ALERT: failed to update custom service usage: %v", err) - } + if _, err := tx.Exec(r.Context(), ` + UPDATE custom_services SET usage_count = usage_count + 1, last_used_at = NOW() WHERE id = ANY($1) + `, req.CustomServiceIDs); err != nil { + log.Printf("ALERT: failed to update custom service usage: %v", err) } } @@ -1419,18 +1417,12 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) { durMinutes = 60 } } else { - if err := tx.QueryRow(r.Context(), ` - SELECT COALESCE(SUM(dur), 60) FROM ( - SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur - FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = $1 - UNION ALL - SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes) - FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = $1 - ) sub - `, bookingID).Scan(&durMinutes); err != nil { - log.Printf("Failed to get duration for existing services: %v", err) - durMinutes = 60 - } + if err := tx.QueryRow(r.Context(), ` + SELECT total_duration_minutes FROM bookings WHERE id = $1 + `, bookingID).Scan(&durMinutes); err != nil { + log.Printf("Failed to get duration for existing services: %v", err) + durMinutes = 60 + } } if durMinutes <= 0 { durMinutes = 60 @@ -1444,15 +1436,7 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) { WHERE id != $1 AND status NOT IN ('completed','client_cancelled','we_cancelled','no_show','deposit_lapsed') AND start_time < $3 - AND start_time + (INTERVAL '1 minute' * ( - SELECT COALESCE(SUM(dur), 60) FROM ( - SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur - FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = bookings.id - UNION ALL - SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes) - FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = bookings.id - ) sub - )) > $2 + AND end_time > $2 `, bookingID, *req.NewStartTime, newEnd).Scan(&overlapCount); err != nil { log.Printf("Failed to check overlap: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) @@ -1545,17 +1529,7 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) { `, req.NewServices).Scan(&durationMinutes) } else { _ = tx.QueryRow(r.Context(), ` - SELECT COALESCE(SUM(dur), 60) FROM ( - SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur - FROM booking_services bs - JOIN services s ON bs.service_id = s.id - WHERE bs.booking_id = $1 - UNION ALL - SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes) - FROM booking_custom_services bcs - JOIN custom_services cs ON bcs.custom_service_id = cs.id - WHERE bcs.booking_id = $1 - ) sub + SELECT total_duration_minutes FROM bookings WHERE id = $1 `, bookingID).Scan(&durationMinutes) } @@ -1761,13 +1735,7 @@ func AdminApproveEditRequestHandler(w http.ResponseWriter, r *http.Request) { if hasOverrides { // Use the existing booking_services with overrides err = tx.QueryRow(r.Context(), ` - SELECT COALESCE(SUM(dur), 60) FROM ( - SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur - FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = $1 - UNION ALL - SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes) - FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = $1 - ) sub + SELECT total_duration_minutes FROM bookings WHERE id = $1 `, bookingID).Scan(&durationMinutes) } else { // Use standard durations or new_services if provided @@ -1805,15 +1773,7 @@ func AdminApproveEditRequestHandler(w http.ResponseWriter, r *http.Request) { WHERE id != $1 AND status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'deposit_lapsed') AND start_time < $3 - AND start_time + (INTERVAL '1 minute' * ( - SELECT COALESCE(SUM(dur), 60) FROM ( - SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur - FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = bookings.id - UNION ALL - SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes) - FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = bookings.id - ) sub - )) > $2 + AND end_time > $2 `, bookingID, *newStartTime, newEndTime).Scan(&overlapCount) if err != nil { log.Printf("Failed to check overlap: %v", err) diff --git a/backend/handlers/bookings/reserve.go b/backend/handlers/bookings/reserve.go index f35dde6..6b891ba 100644 --- a/backend/handlers/bookings/reserve.go +++ b/backend/handlers/bookings/reserve.go @@ -113,7 +113,7 @@ func ReserveSlotHandler(w http.ResponseWriter, r *http.Request) { localStart := req.StartTime.In(londonLocation) weekday := int((localStart.Weekday() + 6) % 7) var closeStr string - if err := db.Conn.QueryRow(r.Context(), `SELECT end_time::text FROM working_hours WHERE weekday = $1`, weekday).Scan(&closeStr); err != nil { + if err := db.Conn.QueryRow(r.Context(), `SELECT end_time 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 @@ -132,15 +132,7 @@ func ReserveSlotHandler(w http.ResponseWriter, r *http.Request) { if err := db.Conn.QueryRow(r.Context(), ` SELECT COUNT(*) FROM bookings WHERE status IN ('pending','confirmed','in_progress','completed') AND start_time < $2 - AND start_time + (INTERVAL '1 minute' * ( - SELECT COALESCE(SUM(dur),60) FROM ( - SELECT COALESCE(bs.override_duration_minutes,s.duration_minutes) AS dur - FROM booking_services bs JOIN services s ON bs.service_id=s.id WHERE bs.booking_id=bookings.id - UNION ALL - SELECT COALESCE(bcs.override_duration_minutes,cs.duration_minutes) - FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id=cs.id WHERE bcs.booking_id=bookings.id - ) sub - )) > $1 + AND end_time > $1 `, req.StartTime, endTime).Scan(&cnt); err != nil { log.Printf("Failed to check overlap: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError)