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 <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-06-21 21:47:12 +01:00
co-authored by Sisyphus
parent 2e231eea4c
commit 7ea6fab3af
4 changed files with 41 additions and 262 deletions
+2 -10
View File
@@ -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. // DB uses 0=Monday..6=Sunday; Go uses 0=Sunday..6=Saturday. Convert.
weekday := int((localStart.Weekday() + 6) % 7) weekday := int((localStart.Weekday() + 6) % 7)
var closeStr string 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) { if errors.Is(err, pgx.ErrNoRows) {
http.Error(w, "Not open on this day", http.StatusBadRequest) http.Error(w, "Not open on this day", http.StatusBadRequest)
return return
@@ -132,15 +132,7 @@ func AdminReserveSlotHandler(w http.ResponseWriter, r *http.Request) {
if err := db.Conn.QueryRow(r.Context(), ` if err := db.Conn.QueryRow(r.Context(), `
SELECT COUNT(*) FROM bookings WHERE status IN ('pending','confirmed','in_progress','completed') SELECT COUNT(*) FROM bookings WHERE status IN ('pending','confirmed','in_progress','completed')
AND start_time < $2 AND start_time < $2
AND start_time + (INTERVAL '1 minute' * ( AND end_time > $1
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
`, req.StartTime, endTime).Scan(&cnt); err != nil { `, req.StartTime, endTime).Scan(&cnt); err != nil {
log.Printf("Failed to check overlap: %v", err) log.Printf("Failed to check overlap: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError) http.Error(w, "Internal server error", http.StatusInternalServerError)
+22 -187
View File
@@ -428,23 +428,11 @@ func GetAllUserBookingsHandler(w http.ResponseWriter, r *http.Request) {
dataQuery := ` dataQuery := `
SELECT SELECT
b.id, b.start_time, b.status, b.notes, b.created_at, b.updated_at, b.created_by, 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 total_amount FROM bookings WHERE id = b.id) AS total_amount,
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 COALESCE(SUM(amount), 0) (SELECT COALESCE(SUM(amount), 0)
FROM payments FROM payments
WHERE booking_id = b.id AND status = 'completed') AS amount_paid, WHERE booking_id = b.id AND status = 'completed') AS amount_paid,
(SELECT COALESCE(SUM(dur_val), 0) FROM ( (SELECT total_duration_minutes FROM bookings WHERE id = b.id) AS duration_minutes,
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,
b.deposit_required, b.deposit_required,
(SELECT COALESCE(SUM(amount), 0) (SELECT COALESCE(SUM(amount), 0)
FROM payments FROM payments
@@ -1440,15 +1428,7 @@ func UpdateBookingServicesHandler(w http.ResponseWriter, r *http.Request) {
WHERE id != $1 WHERE id != $1
AND status IN ('confirmed', 'pending', 'in_progress', 'completed') AND status IN ('confirmed', 'pending', 'in_progress', 'completed')
AND start_time < $3 AND start_time < $3
AND start_time + (INTERVAL '1 minute' * ( AND end_time > $2
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
`, bookingID, startTime, newEndTime).Scan(&overlapCount) `, bookingID, startTime, newEndTime).Scan(&overlapCount)
if err != nil { if err != nil {
log.Printf("Failed to check overlap: %v", err) 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. // DB uses 0=Monday..6=Sunday; Go uses 0=Sunday..6=Saturday. Convert.
weekday := int((localStart.Weekday() + 6) % 7) weekday := int((localStart.Weekday() + 6) % 7)
var closeStr string 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) log.Printf("Failed to get hours: %v", err)
http.Error(w, "Could not verify hours", http.StatusInternalServerError) http.Error(w, "Could not verify hours", http.StatusInternalServerError)
return return
@@ -2209,15 +2189,7 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) {
if err := tx.QueryRow(r.Context(), ` if err := tx.QueryRow(r.Context(), `
SELECT COUNT(*) FROM bookings WHERE status IN ('pending','confirmed','in_progress','completed') SELECT COUNT(*) FROM bookings WHERE status IN ('pending','confirmed','in_progress','completed')
AND start_time < $2 AND start_time < $2
AND start_time + (INTERVAL '1 minute' * ( AND end_time > $1
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
`, req.StartTime, endTime).Scan(&cnt); err != nil { `, req.StartTime, endTime).Scan(&cnt); err != nil {
log.Printf("Failed to check slot overlap: %v", err) log.Printf("Failed to check slot overlap: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError) http.Error(w, "Internal server error", http.StatusInternalServerError)
@@ -2406,17 +2378,7 @@ func EditBookingHandler(w http.ResponseWriter, r *http.Request) {
var durationMinutes int var durationMinutes int
if err := db.Conn.QueryRow(r.Context(), ` if err := db.Conn.QueryRow(r.Context(), `
SELECT COALESCE(SUM(dur), 60) FROM ( SELECT total_duration_minutes FROM bookings WHERE id = $1
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(&durationMinutes); err != nil { `, bookingID).Scan(&durationMinutes); err != nil {
log.Printf("Failed to get booking duration %s: %v", bookingID, err) log.Printf("Failed to get booking duration %s: %v", bookingID, err)
durationMinutes = 60 durationMinutes = 60
@@ -2445,15 +2407,7 @@ func EditBookingHandler(w http.ResponseWriter, r *http.Request) {
WHERE id != $1 WHERE id != $1
AND status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'deposit_lapsed') AND status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'deposit_lapsed')
AND start_time < $3 AND start_time < $3
AND start_time + (INTERVAL '1 minute' * ( AND end_time > $2
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
`, bookingID, req.StartTime, newEndTime).Scan(&overlapCount); err != nil { `, bookingID, req.StartTime, newEndTime).Scan(&overlapCount); err != nil {
log.Printf("Failed to check overlap %s: %v", bookingID, err) log.Printf("Failed to check overlap %s: %v", bookingID, err)
http.Error(w, "Internal server error", http.StatusInternalServerError) http.Error(w, "Internal server error", http.StatusInternalServerError)
@@ -2631,17 +2585,7 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
var bookingTotal float64 var bookingTotal float64
if err := tx.QueryRow(r.Context(), ` if err := tx.QueryRow(r.Context(), `
SELECT COALESCE(SUM(price_val), 0) FROM ( SELECT total_amount FROM bookings WHERE id = $1
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
`, bookingID).Scan(&bookingTotal); err != nil { `, bookingID).Scan(&bookingTotal); err != nil {
log.Printf("Failed to calculate booking total for %s: %v", bookingID, err) 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 return
} }
if err := db.Conn.QueryRow(r.Context(), ` if err := db.Conn.QueryRow(r.Context(), `
SELECT COALESCE(SUM(dur), 60) FROM ( SELECT total_duration_minutes FROM bookings WHERE id = $1
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(&dur); err != nil { `, bookingID).Scan(&dur); err != nil {
log.Printf("Failed to calculate duration on confirm: %v", err) log.Printf("Failed to calculate duration on confirm: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError) 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(), ` if err := tx.QueryRow(r.Context(), `
SELECT COUNT(*) FROM bookings WHERE id != $1 AND status IN ('pending','confirmed','in_progress','completed') SELECT COUNT(*) FROM bookings WHERE id != $1 AND status IN ('pending','confirmed','in_progress','completed')
AND start_time < $3 AND start_time < $3
AND start_time + (INTERVAL '1 minute' * ( AND end_time > $2
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
`, bookingID, bkStart, endTime).Scan(&cnt); err != nil { `, bookingID, bkStart, endTime).Scan(&cnt); err != nil {
log.Printf("Failed to check overlap on confirm: %v", err) log.Printf("Failed to check overlap on confirm: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError) http.Error(w, "Internal server error", http.StatusInternalServerError)
@@ -3100,17 +3030,7 @@ func ConfirmBookingHandler(w http.ResponseWriter, r *http.Request) {
if dav.Service != nil { if dav.Service != nil {
var durationMinutes int var durationMinutes int
db.Conn.QueryRow(r.Context(), ` db.Conn.QueryRow(r.Context(), `
SELECT COALESCE(SUM(dur), 60) FROM ( SELECT total_duration_minutes FROM bookings WHERE id = $1
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(&durationMinutes) `, bookingID).Scan(&durationMinutes)
if durationMinutes == 0 { if durationMinutes == 0 {
durationMinutes = 60 durationMinutes = 60
@@ -3547,15 +3467,7 @@ func GetBookingCalendarHandler(w http.ResponseWriter, r *http.Request) {
if err := db.Conn.QueryRow(r.Context(), ` if err := db.Conn.QueryRow(r.Context(), `
SELECT id, user_id, start_time, status, COALESCE(notes, ''), COALESCE(created_by, ''), created_at, updated_at, SELECT id, user_id, start_time, status, COALESCE(notes, ''), COALESCE(created_by, ''), created_at, updated_at,
COALESCE((SELECT COALESCE(SUM(dur), 60) FROM ( total_duration_minutes
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)
FROM bookings FROM bookings
WHERE id = $1 AND user_id = $2 WHERE id = $1 AND user_id = $2
`, bookingID, userID).Scan(&bookingIDDB, &userIDDB, &startTime, &status, &notes, &createdBy, &createdAt, &updatedAt, &durationMinutes); err != nil { `, bookingID, userID).Scan(&bookingIDDB, &userIDDB, &startTime, &status, &notes, &createdBy, &createdAt, &updatedAt, &durationMinutes); err != nil {
@@ -3697,13 +3609,7 @@ func GetOverlappingBookingsByTimeHandler(w http.ResponseWriter, r *http.Request)
b.start_time, b.start_time,
b.status, b.status,
b.created_at, b.created_at,
(SELECT COALESCE(SUM(dur_val), 60) FROM ( b.total_duration_minutes as duration,
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,
u.id as user_id, u.id as user_id,
u.fn, u.fn,
u.email, u.email,
@@ -3712,15 +3618,7 @@ func GetOverlappingBookingsByTimeHandler(w http.ResponseWriter, r *http.Request)
LEFT JOIN users u ON b.user_id = u.id LEFT JOIN users u ON b.user_id = u.id
WHERE b.status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'deposit_lapsed') WHERE b.status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'deposit_lapsed')
AND b.start_time < $2 AND b.start_time < $2
AND b.start_time + (INTERVAL '1 minute' * ( AND b.end_time > $1
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
GROUP BY b.id, b.start_time, b.status, b.created_at, u.id, u.fn, u.email, u.phone 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 ORDER BY b.start_time ASC
`, startTime, endTime) `, startTime, endTime)
@@ -3790,14 +3688,7 @@ func GetOverlappingBookingsHandler(w http.ResponseWriter, r *http.Request) {
var startTime time.Time var startTime time.Time
var durationMinutes int var durationMinutes int
if err := db.Conn.QueryRow(r.Context(), ` if err := db.Conn.QueryRow(r.Context(), `
SELECT b.start_time, SELECT b.start_time, b.total_duration_minutes
(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)
FROM bookings b FROM bookings b
WHERE b.id = $1 WHERE b.id = $1
`, bookingID).Scan(&startTime, &durationMinutes); err != nil { `, bookingID).Scan(&startTime, &durationMinutes); err != nil {
@@ -3819,13 +3710,7 @@ func GetOverlappingBookingsHandler(w http.ResponseWriter, r *http.Request) {
b.start_time, b.start_time,
b.status, b.status,
b.created_at, b.created_at,
(SELECT COALESCE(SUM(dur_val), 60) FROM ( b.total_duration_minutes as duration,
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,
u.fn, u.fn,
u.email u.email
FROM bookings b FROM bookings b
@@ -3833,15 +3718,7 @@ func GetOverlappingBookingsHandler(w http.ResponseWriter, r *http.Request) {
WHERE b.id != $1 WHERE b.id != $1
AND b.status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'deposit_lapsed') AND b.status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'deposit_lapsed')
AND b.start_time < $3 AND b.start_time < $3
AND b.start_time + (INTERVAL '1 minute' * ( AND b.end_time > $2
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
GROUP BY b.id, b.start_time, b.status, b.created_at, u.fn, u.email GROUP BY b.id, b.start_time, b.status, b.created_at, u.fn, u.email
ORDER BY b.created_at ASC ORDER BY b.created_at ASC
`, bookingID, startTime, endTime) `, bookingID, startTime, endTime)
@@ -3928,13 +3805,7 @@ func GetBookingsByDateRangeHandler(w http.ResponseWriter, r *http.Request) {
b.status, b.status,
b.notes, b.notes,
b.created_at, b.created_at,
(SELECT COALESCE(SUM(dur_val), 60) FROM ( b.total_duration_minutes as duration,
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,
u.id as user_id, u.id as user_id,
u.fn, u.fn,
u.email, u.email,
@@ -4035,13 +3906,7 @@ func GetBookingsByCreatedRangeHandler(w http.ResponseWriter, r *http.Request) {
b.status, b.status,
b.notes, b.notes,
b.created_at, b.created_at,
(SELECT COALESCE(SUM(dur_val), 60) FROM ( b.total_duration_minutes as duration,
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,
u.id as user_id, u.id as user_id,
u.fn, u.fn,
u.email, u.email,
@@ -4190,17 +4055,7 @@ func AdminRescheduleBookingHandler(w http.ResponseWriter, r *http.Request) {
var durationMinutes int var durationMinutes int
if err := db.Conn.QueryRow(r.Context(), ` if err := db.Conn.QueryRow(r.Context(), `
SELECT COALESCE(SUM(dur), 60) FROM ( SELECT total_duration_minutes FROM bookings WHERE id = $1
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(&durationMinutes); err != nil { `, bookingID).Scan(&durationMinutes); err != nil {
log.Printf("Failed to get booking duration %s: %v", bookingID, err) log.Printf("Failed to get booking duration %s: %v", bookingID, err)
durationMinutes = 60 durationMinutes = 60
@@ -4257,19 +4112,7 @@ func AdminRescheduleBookingHandler(w http.ResponseWriter, r *http.Request) {
WHERE id != $1 WHERE id != $1
AND status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'deposit_lapsed') AND status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'deposit_lapsed')
AND start_time < $3 AND start_time < $3
AND start_time + (INTERVAL '1 minute' * ( AND end_time > $2
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
`, bookingID, req.StartTime, newEndTime).Scan(&overlapCount); err != nil { `, bookingID, req.StartTime, newEndTime).Scan(&overlapCount); err != nil {
log.Printf("Failed to check overlap %s: %v", bookingID, err) log.Printf("Failed to check overlap %s: %v", bookingID, err)
http.Error(w, "Internal server error", http.StatusInternalServerError) 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() UPDATE bookings SET status = 'deposit_lapsed', updated_at = NOW()
WHERE status = 'pending_release' WHERE status = 'pending_release'
AND start_time < $2 AND start_time < $2
AND start_time + (INTERVAL '1 minute' * ( AND end_time > $1
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 NOT EXISTS ( AND NOT EXISTS (
SELECT 1 FROM time_blockers SELECT 1 FROM time_blockers
WHERE description = 'PAYMENT_IN_FLIGHT:' || bookings.id WHERE description = 'PAYMENT_IN_FLIGHT:' || bookings.id
+15 -55
View File
@@ -672,7 +672,7 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) {
// Check for overlapping confirmed/in_progress/completed bookings (inside transaction) // Check for overlapping confirmed/in_progress/completed bookings (inside transaction)
var cnt int var cnt int
err = tx.QueryRow(r.Context(), ` 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) `, req.StartTime, newEnd).Scan(&cnt)
if err != nil { if err != nil {
log.Printf("Failed to check overlap: %v", err) 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) http.Error(w, "Internal server error", http.StatusInternalServerError)
return return
} }
for _, csID := range req.CustomServiceIDs { if _, err := tx.Exec(r.Context(), `
if _, err := tx.Exec(r.Context(), ` UPDATE custom_services SET usage_count = usage_count + 1, last_used_at = NOW() WHERE id = ANY($1)
UPDATE custom_services SET usage_count = usage_count + 1, last_used_at = NOW() WHERE id = $1 `, req.CustomServiceIDs); err != nil {
`, csID); err != nil { log.Printf("ALERT: failed to update custom service usage: %v", err)
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 durMinutes = 60
} }
} else { } else {
if err := tx.QueryRow(r.Context(), ` if err := tx.QueryRow(r.Context(), `
SELECT COALESCE(SUM(dur), 60) FROM ( SELECT total_duration_minutes FROM bookings WHERE id = $1
SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur `, bookingID).Scan(&durMinutes); err != nil {
FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = $1 log.Printf("Failed to get duration for existing services: %v", err)
UNION ALL durMinutes = 60
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 durMinutes <= 0 { if durMinutes <= 0 {
durMinutes = 60 durMinutes = 60
@@ -1444,15 +1436,7 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
WHERE id != $1 WHERE id != $1
AND status NOT IN ('completed','client_cancelled','we_cancelled','no_show','deposit_lapsed') AND status NOT IN ('completed','client_cancelled','we_cancelled','no_show','deposit_lapsed')
AND start_time < $3 AND start_time < $3
AND start_time + (INTERVAL '1 minute' * ( AND end_time > $2
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
`, bookingID, *req.NewStartTime, newEnd).Scan(&overlapCount); err != nil { `, bookingID, *req.NewStartTime, newEnd).Scan(&overlapCount); err != nil {
log.Printf("Failed to check overlap: %v", err) log.Printf("Failed to check overlap: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError) http.Error(w, "Internal server error", http.StatusInternalServerError)
@@ -1545,17 +1529,7 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
`, req.NewServices).Scan(&durationMinutes) `, req.NewServices).Scan(&durationMinutes)
} else { } else {
_ = tx.QueryRow(r.Context(), ` _ = tx.QueryRow(r.Context(), `
SELECT COALESCE(SUM(dur), 60) FROM ( SELECT total_duration_minutes FROM bookings WHERE id = $1
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(&durationMinutes) `, bookingID).Scan(&durationMinutes)
} }
@@ -1761,13 +1735,7 @@ func AdminApproveEditRequestHandler(w http.ResponseWriter, r *http.Request) {
if hasOverrides { if hasOverrides {
// Use the existing booking_services with overrides // Use the existing booking_services with overrides
err = tx.QueryRow(r.Context(), ` err = tx.QueryRow(r.Context(), `
SELECT COALESCE(SUM(dur), 60) FROM ( SELECT total_duration_minutes FROM bookings WHERE id = $1
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(&durationMinutes) `, bookingID).Scan(&durationMinutes)
} else { } else {
// Use standard durations or new_services if provided // Use standard durations or new_services if provided
@@ -1805,15 +1773,7 @@ func AdminApproveEditRequestHandler(w http.ResponseWriter, r *http.Request) {
WHERE id != $1 WHERE id != $1
AND status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'deposit_lapsed') AND status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'deposit_lapsed')
AND start_time < $3 AND start_time < $3
AND start_time + (INTERVAL '1 minute' * ( AND end_time > $2
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
`, bookingID, *newStartTime, newEndTime).Scan(&overlapCount) `, bookingID, *newStartTime, newEndTime).Scan(&overlapCount)
if err != nil { if err != nil {
log.Printf("Failed to check overlap: %v", err) log.Printf("Failed to check overlap: %v", err)
+2 -10
View File
@@ -113,7 +113,7 @@ func ReserveSlotHandler(w http.ResponseWriter, r *http.Request) {
localStart := req.StartTime.In(londonLocation) localStart := req.StartTime.In(londonLocation)
weekday := int((localStart.Weekday() + 6) % 7) weekday := int((localStart.Weekday() + 6) % 7)
var closeStr string 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) log.Printf("Failed to get hours: %v", err)
http.Error(w, "Could not verify hours", http.StatusInternalServerError) http.Error(w, "Could not verify hours", http.StatusInternalServerError)
return return
@@ -132,15 +132,7 @@ func ReserveSlotHandler(w http.ResponseWriter, r *http.Request) {
if err := db.Conn.QueryRow(r.Context(), ` if err := db.Conn.QueryRow(r.Context(), `
SELECT COUNT(*) FROM bookings WHERE status IN ('pending','confirmed','in_progress','completed') SELECT COUNT(*) FROM bookings WHERE status IN ('pending','confirmed','in_progress','completed')
AND start_time < $2 AND start_time < $2
AND start_time + (INTERVAL '1 minute' * ( AND end_time > $1
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
`, req.StartTime, endTime).Scan(&cnt); err != nil { `, req.StartTime, endTime).Scan(&cnt); err != nil {
log.Printf("Failed to check overlap: %v", err) log.Printf("Failed to check overlap: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError) http.Error(w, "Internal server error", http.StatusInternalServerError)