refactor(backend): migrate db.DB to db.Conn PoolProxy across all handlers

Replace direct *pgxpool.Pool usage with PoolProxy wrapper across the entire backend:

- db.DB renamed to db.Conn (*pgxpool.Pool -> *PoolProxy)
- JWT functions now accept context.Context instead of using context.Background()
- Handler DB calls route through PoolProxy for per-test transaction support
- Fixture/helper/testdb functions accept Querier interface for decoupling
- Query ordering fixed in bookings handlers: COUNT after data query to avoid pgx conn busy
- Time truncation fixed: time.Date instead of Truncate(24*time.Hour) for week start calc
- testmain_test.go files updated with SeedBaseline and NewPoolProxy

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 19:28:54 +01:00
co-authored by Sisyphus
parent c69a243f75
commit 3d0e2afc4c
39 changed files with 751 additions and 638 deletions
+104 -100
View File
@@ -81,7 +81,7 @@ type BookingDiscount struct {
}
func fetchBookingDiscounts(ctx context.Context, bookingID string) ([]BookingDiscount, error) {
discountRows, err := db.DB.Query(ctx, `
discountRows, err := db.Conn.Query(ctx, `
SELECT
bd.id, bd.booking_id, bd.user_id, bd.discount_source, bd.source_id,
bd.campaign_type, bd.milestone_type, bd.discount_percent, bd.original_total, bd.discount_amount, bd.applied_at,
@@ -418,7 +418,7 @@ func GetAllUserBookingsHandler(w http.ResponseWriter, r *http.Request) {
// Count query uses the same WHERE but without complex SELECT subqueries,
// cursor, ORDER BY, or LIMIT — just a fast index scan on bookings.
var total int
if err := db.DB.QueryRow(r.Context(), "SELECT COUNT(*) FROM bookings b"+whereClause, whereArgs...).Scan(&total); err != nil {
if err := db.Conn.QueryRow(r.Context(), "SELECT COUNT(*) FROM bookings b"+whereClause, whereArgs...).Scan(&total); err != nil {
log.Printf("Failed to count bookings for user %s: %v", userID, err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
@@ -473,7 +473,7 @@ func GetAllUserBookingsHandler(w http.ResponseWriter, r *http.Request) {
dataArgs = append(dataArgs, req.PerPage+1)
}
rows, err := db.DB.Query(r.Context(), dataQuery, dataArgs...)
rows, err := db.Conn.Query(r.Context(), dataQuery, dataArgs...)
if err != nil {
log.Printf("Failed to fetch bookings for user %s: %v", userID, err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
@@ -515,7 +515,7 @@ func GetAllUserBookingsHandler(w http.ResponseWriter, r *http.Request) {
}
if len(bookingIDs) > 0 {
serviceRows, err := db.DB.Query(r.Context(), `
serviceRows, err := db.Conn.Query(r.Context(), `
SELECT
bs.service_id, bs.override_price, bs.override_duration_minutes,
s.name, s.description, s.price, s.duration_minutes,
@@ -742,15 +742,9 @@ func GetAllAdminBookingsHandler(w http.ResponseWriter, r *http.Request) {
args = append(args, req.PerPage+1)
}
rows, err := db.DB.Query(r.Context(), baseQuery, args...)
if err != nil {
log.Printf("Failed to fetch all bookings: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer rows.Close()
// Count query: simple SELECT COUNT(*) with same WHERE (no CTEs, joins, or subqueries).
// Run BEFORE the data query to avoid "conn busy" errors when routing
// through a per-test transaction (pgx.Tx does not support concurrent queries).
var total int
{
countWhere := ""
@@ -778,13 +772,21 @@ func GetAllAdminBookingsHandler(w http.ResponseWriter, r *http.Request) {
et, _ := time.Parse("2006-01-02", *req.EndDate)
countArgs = append(countArgs, et.Add(23*time.Hour+59*time.Minute+59*time.Second))
}
if err := db.DB.QueryRow(r.Context(), "SELECT COUNT(*) FROM bookings b"+countWhere, countArgs...).Scan(&total); err != nil {
if err := db.Conn.QueryRow(r.Context(), "SELECT COUNT(*) FROM bookings b"+countWhere, countArgs...).Scan(&total); err != nil {
log.Printf("Failed to count admin bookings: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
}
rows, err := db.Conn.Query(r.Context(), baseQuery, args...)
if err != nil {
log.Printf("Failed to fetch all bookings: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer rows.Close()
var bookings []Booking
var bookingIDs []string
@@ -816,7 +818,7 @@ func GetAllAdminBookingsHandler(w http.ResponseWriter, r *http.Request) {
}
if len(bookingIDs) > 0 {
serviceRows, err := db.DB.Query(r.Context(), `
serviceRows, err := db.Conn.Query(r.Context(), `
SELECT bs.booking_id, s.name
FROM booking_services bs
JOIN services s ON bs.service_id = s.id
@@ -868,7 +870,7 @@ func GetAllAdminBookingsHandler(w http.ResponseWriter, r *http.Request) {
}
}
if len(userIDs) > 0 {
nhRows, err := db.DB.Query(r.Context(), `
nhRows, err := db.Conn.Query(r.Context(), `
SELECT DISTINCT ON (user_id) user_id, previous_first_name, previous_last_name
FROM name_history
WHERE user_id = ANY($1) AND booking_id IS NULL
@@ -968,13 +970,13 @@ func GetAllBookingsByUserHandler(w http.ResponseWriter, r *http.Request) {
// Count query: simple SELECT COUNT(*) (no cursor, since that filters rows).
var total int
if err := db.DB.QueryRow(r.Context(), "SELECT COUNT(*) FROM bookings b WHERE b.user_id = $1", userID).Scan(&total); err != nil {
if err := db.Conn.QueryRow(r.Context(), "SELECT COUNT(*) FROM bookings b WHERE b.user_id = $1", userID).Scan(&total); err != nil {
log.Printf("Failed to count bookings for user %s: %v", userID, err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
rows, err := db.DB.Query(r.Context(), baseQuery, args...)
rows, err := db.Conn.Query(r.Context(), baseQuery, args...)
if err != nil {
log.Printf("Failed to fetch bookings for user %s: %v", userID, err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
@@ -1003,7 +1005,7 @@ func GetAllBookingsByUserHandler(w http.ResponseWriter, r *http.Request) {
}
if len(bookingIDs) > 0 {
serviceRows, err := db.DB.Query(r.Context(), `
serviceRows, err := db.Conn.Query(r.Context(), `
SELECT
bs.booking_id,
s.name,
@@ -1046,7 +1048,7 @@ func GetAllBookingsByUserHandler(w http.ResponseWriter, r *http.Request) {
}
serviceRows.Close()
paymentRows, err := db.DB.Query(r.Context(), `
paymentRows, err := db.Conn.Query(r.Context(), `
SELECT p.booking_id,
COALESCE(SUM(p.amount) FILTER (WHERE p.status = 'completed'), 0) AS amount_paid,
COALESCE(SUM(p.amount) FILTER (WHERE p.status = 'completed' AND p.created_at < b.start_time), 0) AS pre_start_paid
@@ -1138,7 +1140,7 @@ func GetAdminBookingHandler(w http.ResponseWriter, r *http.Request) {
var depositRequired bool
var dateOfBirth sql.NullTime
err := db.DB.QueryRow(r.Context(), `
err := db.Conn.QueryRow(r.Context(), `
SELECT
b.id, b.user_id, b.start_time, b.status, b.notes,
b.created_at, b.updated_at, b.created_by,
@@ -1174,7 +1176,7 @@ func GetAdminBookingHandler(w http.ResponseWriter, r *http.Request) {
}
var referralCodeUses int
if err := db.DB.QueryRow(r.Context(), `
if err := db.Conn.QueryRow(r.Context(), `
SELECT COUNT(*) FROM user_referrals WHERE referrer_id = $1
`, booking.User.ID).Scan(&referralCodeUses); err != nil {
log.Printf("Failed to fetch referral code uses for user %s: %v", booking.User.ID, err)
@@ -1182,7 +1184,7 @@ func GetAdminBookingHandler(w http.ResponseWriter, r *http.Request) {
booking.User.ReferralCodeUses = &referralCodeUses
var prevFirstName, prevLastName sql.NullString
err = db.DB.QueryRow(r.Context(), `
err = db.Conn.QueryRow(r.Context(), `
SELECT nh.previous_first_name, nh.previous_last_name
FROM name_history nh
WHERE nh.user_id = $1 AND nh.booking_id IS NULL
@@ -1194,7 +1196,7 @@ func GetAdminBookingHandler(w http.ResponseWriter, r *http.Request) {
booking.User.PreviousLastName = &prevLastName.String
}
serviceRows, err := db.DB.Query(r.Context(), `
serviceRows, err := db.Conn.Query(r.Context(), `
SELECT service_id, name, price, duration_minutes FROM (
SELECT
bs.service_id, s.name,
@@ -1242,7 +1244,7 @@ func GetAdminBookingHandler(w http.ResponseWriter, r *http.Request) {
}
booking.TotalAmount = totalAmount
paymentRows, err := db.DB.Query(r.Context(), `
paymentRows, err := db.Conn.Query(r.Context(), `
SELECT payment_type, payment_method, vendor_code, invoice_number,
status, amount, created_at
FROM payments
@@ -1355,7 +1357,7 @@ func UpdateBookingServicesHandler(w http.ResponseWriter, r *http.Request) {
var startTime time.Time
var currentStatus string
if err := db.DB.QueryRow(r.Context(), "SELECT start_time, status FROM bookings WHERE id = $1", bookingID).Scan(&startTime, &currentStatus); err != nil {
if err := db.Conn.QueryRow(r.Context(), "SELECT start_time, status FROM bookings WHERE id = $1", bookingID).Scan(&startTime, &currentStatus); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
http.Error(w, "Booking not found", http.StatusNotFound)
return
@@ -1390,7 +1392,7 @@ func UpdateBookingServicesHandler(w http.ResponseWriter, r *http.Request) {
}
}
if len(serviceIDsNeedingLookup) > 0 {
rows, err := db.DB.Query(r.Context(), `SELECT id, duration_minutes FROM services WHERE id = ANY($1)`, serviceIDsNeedingLookup)
rows, err := db.Conn.Query(r.Context(), `SELECT id, duration_minutes FROM services WHERE id = ANY($1)`, serviceIDsNeedingLookup)
if err != nil {
log.Printf("Failed to batch fetch service durations: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
@@ -1433,7 +1435,7 @@ func UpdateBookingServicesHandler(w http.ResponseWriter, r *http.Request) {
newEndTime := startTime.Add(time.Duration(newTotalDuration) * time.Minute)
var nextBookingStart *time.Time
err := db.DB.QueryRow(r.Context(), `
err := db.Conn.QueryRow(r.Context(), `
SELECT start_time FROM bookings
WHERE start_time > $1
AND status IN ('confirmed', 'pending', 'in_progress')
@@ -1451,7 +1453,7 @@ func UpdateBookingServicesHandler(w http.ResponseWriter, r *http.Request) {
return
}
tx, err := db.DB.Begin(r.Context())
tx, err := db.Conn.Begin(r.Context())
if err != nil {
log.Printf("Failed to start transaction: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
@@ -1517,7 +1519,7 @@ func UpdateBookingServicesHandler(w http.ResponseWriter, r *http.Request) {
booking.User = &UserSummary{}
var depositRequired bool
err = db.DB.QueryRow(r.Context(), `
err = db.Conn.QueryRow(r.Context(), `
SELECT
b.id, b.user_id, b.start_time, b.status, b.notes,
b.created_at, b.updated_at, b.created_by,
@@ -1542,14 +1544,14 @@ func UpdateBookingServicesHandler(w http.ResponseWriter, r *http.Request) {
}
var referralCodeUses int
if err := db.DB.QueryRow(r.Context(), `
if err := db.Conn.QueryRow(r.Context(), `
SELECT COUNT(*) FROM user_referrals WHERE referrer_id = $1
`, booking.User.ID).Scan(&referralCodeUses); err != nil {
log.Printf("Failed to fetch referral code uses for user %s: %v", booking.User.ID, err)
}
booking.User.ReferralCodeUses = &referralCodeUses
serviceRows, err := db.DB.Query(r.Context(), `
serviceRows, err := db.Conn.Query(r.Context(), `
SELECT
bs.service_id, bs.override_price, bs.override_duration_minutes,
s.name, s.description, s.price, s.duration_minutes
@@ -1624,7 +1626,7 @@ func UpdateBookingServicesHandler(w http.ResponseWriter, r *http.Request) {
}
booking.TotalAmount = totalAmount
paymentRows, err := db.DB.Query(r.Context(), `
paymentRows, err := db.Conn.Query(r.Context(), `
SELECT payment_type, payment_method, vendor_code, invoice_number,
status, amount, created_at
FROM payments
@@ -1795,30 +1797,12 @@ func SearchAdminBookingsHandler(w http.ResponseWriter, r *http.Request) {
searchQuery += fmt.Sprintf(" LIMIT $%d", paramCount)
args = append(args, perPage+1)
rows, err := db.DB.Query(r.Context(), searchQuery, args...)
rows, err := db.Conn.Query(r.Context(), searchQuery, args...)
if err != nil {
log.Printf("Failed to search bookings: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer rows.Close()
// Count query: SELECT COUNT(*) with the same search WHERE (no CTEs/joins/scoring).
var total int
{
countSQL := `SELECT COUNT(*) FROM bookings b LEFT JOIN users u ON b.user_id = u.id WHERE
b.id ILIKE $1 ESCAPE '\' OR b.notes ILIKE $1 ESCAPE '\' OR
b.status::text ILIKE $1 ESCAPE '\' OR u.n_first_name ILIKE $1 ESCAPE '\' OR
u.n_last_name ILIKE $1 ESCAPE '\' OR u.fn ILIKE $1 ESCAPE '\' OR
u.email ILIKE $1 ESCAPE '\' OR u.phone ILIKE $1 ESCAPE '\' OR
EXISTS (SELECT 1 FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = b.id AND s.name ILIKE $1 ESCAPE '\') OR
EXISTS (SELECT 1 FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = b.id AND cs.name ILIKE $1 ESCAPE '\')`
if err := db.DB.QueryRow(r.Context(), countSQL, searchPattern).Scan(&total); err != nil {
log.Printf("Failed to count search bookings: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
}
var bookings []Booking
var bookingIDs []string
@@ -1846,9 +1830,27 @@ func SearchAdminBookingsHandler(w http.ResponseWriter, r *http.Request) {
bookings = append(bookings, b)
bookingIDs = append(bookingIDs, b.ID)
}
rows.Close()
// Count query runs AFTER the data query result set is consumed to avoid pgx "conn busy".
var total int
{
countSQL := `SELECT COUNT(*) FROM bookings b LEFT JOIN users u ON b.user_id = u.id WHERE
b.id ILIKE $1 ESCAPE '\' OR b.notes ILIKE $1 ESCAPE '\' OR
b.status::text ILIKE $1 ESCAPE '\' OR u.n_first_name ILIKE $1 ESCAPE '\' OR
u.n_last_name ILIKE $1 ESCAPE '\' OR u.fn ILIKE $1 ESCAPE '\' OR
u.email ILIKE $1 ESCAPE '\' OR u.phone ILIKE $1 ESCAPE '\' OR
EXISTS (SELECT 1 FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = b.id AND s.name ILIKE $1 ESCAPE '\') OR
EXISTS (SELECT 1 FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = b.id AND cs.name ILIKE $1 ESCAPE '\')`
if err := db.Conn.QueryRow(r.Context(), countSQL, searchPattern).Scan(&total); err != nil {
log.Printf("Failed to count search bookings: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
}
if len(bookingIDs) > 0 {
serviceRows, err := db.DB.Query(r.Context(), `
serviceRows, err := db.Conn.Query(r.Context(), `
SELECT booking_id, name FROM (
SELECT bs.booking_id, s.name
FROM booking_services bs
@@ -1935,7 +1937,7 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) {
if idempotencyKey != "" {
var existingBooking Booking
existingBooking.User = &UserSummary{}
err := db.DB.QueryRow(r.Context(), `
err := db.Conn.QueryRow(r.Context(), `
SELECT b.id, b.user_id, b.start_time, b.status, b.notes, b.created_at, b.updated_at, b.created_by, b.deposit_required
FROM bookings b WHERE b.idempotency_key = $1
`, idempotencyKey).Scan(
@@ -1945,7 +1947,7 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) {
)
if err == nil {
// Booking already exists with this key — fetch services and return it
rows, err := db.DB.Query(r.Context(), `
rows, err := db.Conn.Query(r.Context(), `
SELECT bs.booking_id, bs.service_id, bs.override_price, bs.override_duration_minutes,
s.name, s.description, s.price, s.duration_minutes
FROM booking_services bs
@@ -1959,7 +1961,6 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) {
WHERE bcs.booking_id = $1
`, existingBooking.ID)
if err == nil {
defer rows.Close()
for rows.Next() {
var bs BookingService
if err := rows.Scan(
@@ -1970,11 +1971,12 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) {
}
existingBooking.Services = append(existingBooking.Services, bs)
}
rows.Close()
}
// Get payment info
var preStartPaid float64
db.DB.QueryRow(r.Context(), `SELECT COALESCE(SUM(amount), 0) FROM payments WHERE booking_id = $1 AND payment_type IN ('deposit', 'full') AND status = 'completed'`, existingBooking.ID).Scan(&preStartPaid)
db.Conn.QueryRow(r.Context(), `SELECT COALESCE(SUM(amount), 0) FROM payments WHERE booking_id = $1 AND payment_type IN ('deposit', 'full') AND status = 'completed'`, existingBooking.ID).Scan(&preStartPaid)
populateDepositFields(&existingBooking, existingBooking.DepositRequired, preStartPaid)
w.Header().Set("Content-Type", "application/json")
@@ -1995,7 +1997,7 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) {
}
var accountRole string
if err := db.DB.QueryRow(r.Context(), `SELECT account_role FROM users WHERE id = $1`, *req.UserID).Scan(&accountRole); err != nil {
if err := db.Conn.QueryRow(r.Context(), `SELECT account_role FROM users WHERE id = $1`, *req.UserID).Scan(&accountRole); err != nil {
http.Error(w, "User not found", http.StatusBadRequest)
return
}
@@ -2023,7 +2025,7 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) {
var depositsRequired int
if !isGuest {
if err := db.DB.QueryRow(r.Context(), `SELECT deposits_required FROM users WHERE id = $1`, userID).Scan(&depositsRequired); err != nil {
if err := db.Conn.QueryRow(r.Context(), `SELECT deposits_required FROM users WHERE id = $1`, userID).Scan(&depositsRequired); err != nil {
log.Printf("Failed to fetch deposits_required for user %s: %v", userID, err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
@@ -2032,7 +2034,7 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) {
if !isGuest && depositsRequired > 0 {
var activeCount int
if err := db.DB.QueryRow(r.Context(), `
if err := db.Conn.QueryRow(r.Context(), `
SELECT COUNT(*) FROM bookings
WHERE user_id = $1 AND status IN ('pending', 'confirmed')
`, userID).Scan(&activeCount); err != nil {
@@ -2063,7 +2065,7 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) {
depositRequiredSnapshot := depositsRequired > 0
if !isGuest && len(req.ServiceIDs) > 0 {
patchTestRows, err := db.DB.Query(r.Context(), `
patchTestRows, err := db.Conn.Query(r.Context(), `
SELECT id, service_ids, notice_duration_hours, expiry_months
FROM patch_tests
WHERE service_ids::text[] && $1::text[]
@@ -2098,7 +2100,7 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) {
userPatchTests := make(map[string]time.Time)
if len(allPtIDs) > 0 {
uptRows, err := db.DB.Query(r.Context(), `
uptRows, err := db.Conn.Query(r.Context(), `
SELECT patch_test_id, tested_at
FROM user_patch_tests
WHERE user_id = $1 AND patch_test_id = ANY($2)
@@ -2155,7 +2157,7 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) {
}
var svcDuration int
if err := db.DB.QueryRow(r.Context(), `
if err := db.Conn.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)
@@ -2167,7 +2169,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.DB.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::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
@@ -2181,7 +2183,7 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) {
return
}
tx, err := db.DB.Begin(r.Context())
tx, err := db.Conn.Begin(r.Context())
if err != nil {
log.Printf("Failed to start transaction: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
@@ -2312,7 +2314,7 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) {
// Fetch services for the response
booking.Services = []BookingService{}
rows, err := db.DB.Query(r.Context(), `
rows, err := db.Conn.Query(r.Context(), `
SELECT bs.booking_id, bs.service_id, bs.override_price, bs.override_duration_minutes,
s.name, s.description, s.price, s.duration_minutes
FROM booking_services bs
@@ -2380,7 +2382,7 @@ func EditBookingHandler(w http.ResponseWriter, r *http.Request) {
}
var currentStatus string
if err := db.DB.QueryRow(r.Context(), "SELECT status FROM bookings WHERE id = $1 AND user_id = $2", bookingID, userID).Scan(&currentStatus); err != nil {
if err := db.Conn.QueryRow(r.Context(), "SELECT status FROM bookings WHERE id = $1 AND user_id = $2", bookingID, userID).Scan(&currentStatus); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
http.Error(w, "Booking not found or access denied", http.StatusNotFound)
return
@@ -2396,7 +2398,7 @@ func EditBookingHandler(w http.ResponseWriter, r *http.Request) {
}
var durationMinutes int
if err := db.DB.QueryRow(r.Context(), `
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
@@ -2415,7 +2417,7 @@ func EditBookingHandler(w http.ResponseWriter, r *http.Request) {
newEndTime := req.StartTime.Add(time.Duration(durationMinutes) * time.Minute)
var overlapCount int
if err := db.DB.QueryRow(r.Context(), `
if err := db.Conn.QueryRow(r.Context(), `
SELECT COUNT(*) FROM bookings
WHERE id != $1
AND status NOT IN ('completed', 'client_cancelled', 'we_cancelled', 'no_show', 'deposit_lapsed')
@@ -2452,10 +2454,11 @@ func EditBookingHandler(w http.ResponseWriter, r *http.Request) {
if daysToMonday == 0 {
daysToMonday = 7
}
weekStart := req.StartTime.AddDate(0, 0, -daysToMonday+1).Truncate(24 * time.Hour)
tm := req.StartTime.AddDate(0, 0, -daysToMonday+1)
weekStart := time.Date(tm.Year(), tm.Month(), tm.Day(), 0, 0, 0, 0, tm.Location())
var isClosed bool
if err := db.DB.QueryRow(r.Context(), `
if err := db.Conn.QueryRow(r.Context(), `
SELECT EXISTS (
SELECT 1 FROM exceptional_working_hours ewh
JOIN exceptional_group_applications ega ON ewh.group_id = ega.group_id
@@ -2475,7 +2478,7 @@ func EditBookingHandler(w http.ResponseWriter, r *http.Request) {
var booking Booking
booking.User = &UserSummary{}
if err := db.DB.QueryRow(r.Context(), `
if err := db.Conn.QueryRow(r.Context(), `
UPDATE bookings
SET start_time = $1, updated_at = NOW()
WHERE id = $2 AND user_id = $3
@@ -2533,7 +2536,7 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
return
}
tx, txErr := db.DB.Begin(r.Context())
tx, txErr := db.Conn.Begin(r.Context())
if txErr != nil {
log.Printf("Failed to begin transaction for booking progress: %v", txErr)
http.Error(w, "Internal server error", http.StatusInternalServerError)
@@ -2725,7 +2728,7 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
_ = tx.QueryRow(r.Context(), `SELECT COUNT(*) FROM bookings WHERE status = 'completed'`).Scan(&globalCount)
var hasInPersonPayment bool
db.DB.QueryRow(r.Context(), `
db.Conn.QueryRow(r.Context(), `
SELECT EXISTS(SELECT 1 FROM payments WHERE booking_id = $1 AND payment_method = 'in_person_card')`, bookingID).Scan(&hasInPersonPayment)
if hasInPersonPayment {
@@ -2913,12 +2916,12 @@ func ConfirmBookingHandler(w http.ResponseWriter, r *http.Request) {
var bkStart time.Time
var dur int
if err := db.DB.QueryRow(r.Context(), "SELECT start_time FROM bookings WHERE id = $1", bookingID).Scan(&bkStart); err != nil {
if err := db.Conn.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
}
db.DB.QueryRow(r.Context(), `
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
@@ -2929,7 +2932,7 @@ func ConfirmBookingHandler(w http.ResponseWriter, r *http.Request) {
`, bookingID).Scan(&dur)
endTime := bkStart.Add(time.Duration(dur) * time.Minute)
tx, err := db.DB.Begin(r.Context())
tx, err := db.Conn.Begin(r.Context())
if err != nil {
log.Printf("Failed to start transaction: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
@@ -3057,7 +3060,7 @@ func ConfirmBookingHandler(w http.ResponseWriter, r *http.Request) {
if dav.Service != nil {
var durationMinutes int
db.DB.QueryRow(r.Context(), `
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
@@ -3103,7 +3106,7 @@ func DeleteBookingHandler(w http.ResponseWriter, r *http.Request) {
}
var paymentExists bool
if err := db.DB.QueryRow(r.Context(), "SELECT EXISTS(SELECT 1 FROM payments WHERE booking_id = $1)", bookingID).Scan(&paymentExists); err != nil {
if err := db.Conn.QueryRow(r.Context(), "SELECT EXISTS(SELECT 1 FROM payments WHERE booking_id = $1)", bookingID).Scan(&paymentExists); err != nil {
log.Printf("Failed to check booking %s for user %s: %v", bookingID, userID, err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
@@ -3131,7 +3134,7 @@ func DeleteBookingHandler(w http.ResponseWriter, r *http.Request) {
// Get booking info needed for refund (before any transaction)
var originalStatus string
var startTime time.Time
if err := db.DB.QueryRow(r.Context(), "SELECT status, start_time FROM bookings WHERE id = $1 AND user_id = $2", bookingID, userID).Scan(&originalStatus, &startTime); err != nil {
if err := db.Conn.QueryRow(r.Context(), "SELECT status, start_time FROM bookings WHERE id = $1 AND user_id = $2", bookingID, userID).Scan(&originalStatus, &startTime); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
http.Error(w, "Booking not found or access denied", http.StatusNotFound)
return
@@ -3163,7 +3166,7 @@ func DeleteBookingHandler(w http.ResponseWriter, r *http.Request) {
}
// Refund succeeded (or no refund needed) — now cancel the booking
tx, err := db.DB.Begin(r.Context())
tx, err := db.Conn.Begin(r.Context())
if err != nil {
log.Printf("Failed to start transaction: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
@@ -3241,7 +3244,7 @@ func DeleteBookingHandler(w http.ResponseWriter, r *http.Request) {
}
// Hard delete — no payments exist
tx, err := db.DB.Begin(r.Context())
tx, err := db.Conn.Begin(r.Context())
if err != nil {
log.Printf("Failed to start transaction: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
@@ -3301,7 +3304,7 @@ func GetBookingHandler(w http.ResponseWriter, r *http.Request) {
var createdBy sql.NullString
var depositRequired bool
if err := db.DB.QueryRow(r.Context(), `
if err := db.Conn.QueryRow(r.Context(), `
SELECT b.id, b.user_id, b.start_time, b.status, b.notes, b.created_at, b.updated_at, b.created_by,
b.deposit_required
FROM bookings b
@@ -3323,7 +3326,7 @@ func GetBookingHandler(w http.ResponseWriter, r *http.Request) {
booking.CreatedBy = &createdBy.String
}
serviceRows, err := db.DB.Query(r.Context(), `
serviceRows, err := db.Conn.Query(r.Context(), `
SELECT
bs.service_id, bs.override_price, bs.override_duration_minutes,
s.name, s.description, s.price, s.duration_minutes
@@ -3401,7 +3404,7 @@ func GetBookingHandler(w http.ResponseWriter, r *http.Request) {
booking.Services = append(booking.Services, s)
}
paymentRows, err := db.DB.Query(r.Context(), `
paymentRows, err := db.Conn.Query(r.Context(), `
SELECT
id, payment_type, payment_method, vendor_code, invoice_number,
status, amount, is_vat_applicable, vat_rate, vat_amount, net_amount,
@@ -3503,7 +3506,7 @@ func GetBookingCalendarHandler(w http.ResponseWriter, r *http.Request) {
var startTime, createdAt, updatedAt time.Time
var durationMinutes int
if err := db.DB.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,
COALESCE((SELECT COALESCE(SUM(dur), 60) FROM (
SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur
@@ -3526,7 +3529,7 @@ func GetBookingCalendarHandler(w http.ResponseWriter, r *http.Request) {
return
}
rows, err := db.DB.Query(r.Context(), `
rows, err := db.Conn.Query(r.Context(), `
SELECT s.name, COALESCE(bs.override_price, s.price)
FROM booking_services bs
JOIN services s ON bs.service_id = s.id
@@ -3649,7 +3652,7 @@ func GetOverlappingBookingsByTimeHandler(w http.ResponseWriter, r *http.Request)
return
}
rows, err := db.DB.Query(r.Context(), `
rows, err := db.Conn.Query(r.Context(), `
SELECT
b.id,
b.start_time,
@@ -3698,7 +3701,7 @@ func GetOverlappingBookingsByTimeHandler(w http.ResponseWriter, r *http.Request)
continue
}
serviceRows, err := db.DB.Query(r.Context(), `
serviceRows, err := db.Conn.Query(r.Context(), `
SELECT name FROM (
SELECT s.name
FROM booking_services bs
@@ -3747,7 +3750,7 @@ func GetOverlappingBookingsHandler(w http.ResponseWriter, r *http.Request) {
// Get the booking's start time and duration
var startTime time.Time
var durationMinutes int
if err := db.DB.QueryRow(r.Context(), `
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
@@ -3771,7 +3774,7 @@ func GetOverlappingBookingsHandler(w http.ResponseWriter, r *http.Request) {
endTime := startTime.Add(time.Duration(durationMinutes) * time.Minute)
// Find overlapping bookings (excluding the current booking and cancelled/completed ones)
rows, err := db.DB.Query(r.Context(), `
rows, err := db.Conn.Query(r.Context(), `
SELECT
b.id,
b.start_time,
@@ -3820,7 +3823,7 @@ func GetOverlappingBookingsHandler(w http.ResponseWriter, r *http.Request) {
}
// Get services for this booking
serviceRows, err := db.DB.Query(r.Context(), `
serviceRows, err := db.Conn.Query(r.Context(), `
SELECT name FROM (
SELECT s.name
FROM booking_services bs
@@ -3879,7 +3882,7 @@ func GetBookingsByDateRangeHandler(w http.ResponseWriter, r *http.Request) {
endOfDay := endTime.Add(24 * time.Hour)
rows, err := db.DB.Query(r.Context(), `
rows, err := db.Conn.Query(r.Context(), `
SELECT
b.id,
b.start_time,
@@ -3922,7 +3925,7 @@ func GetBookingsByDateRangeHandler(w http.ResponseWriter, r *http.Request) {
continue
}
serviceRows, err := db.DB.Query(r.Context(), `
serviceRows, err := db.Conn.Query(r.Context(), `
SELECT name FROM (
SELECT s.name
FROM booking_services bs
@@ -3986,7 +3989,7 @@ func GetBookingsByCreatedRangeHandler(w http.ResponseWriter, r *http.Request) {
}
}
rows, err := db.DB.Query(r.Context(), `
rows, err := db.Conn.Query(r.Context(), `
SELECT
b.id,
b.start_time,
@@ -4028,7 +4031,7 @@ func GetBookingsByCreatedRangeHandler(w http.ResponseWriter, r *http.Request) {
continue
}
serviceRows, err := db.DB.Query(r.Context(), `
serviceRows, err := db.Conn.Query(r.Context(), `
SELECT name FROM (
SELECT s.name
FROM booking_services bs
@@ -4106,7 +4109,7 @@ func AdminRescheduleBookingHandler(w http.ResponseWriter, r *http.Request) {
var currentStatus string
var bookingUserID string
var startTime time.Time
if err := db.DB.QueryRow(r.Context(), "SELECT status, user_id, start_time FROM bookings WHERE id = $1", bookingID).Scan(&currentStatus, &bookingUserID, &startTime); err != nil {
if err := db.Conn.QueryRow(r.Context(), "SELECT status, user_id, start_time FROM bookings WHERE id = $1", bookingID).Scan(&currentStatus, &bookingUserID, &startTime); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
http.Error(w, "Booking not found", http.StatusNotFound)
return
@@ -4122,7 +4125,7 @@ func AdminRescheduleBookingHandler(w http.ResponseWriter, r *http.Request) {
}
// All write operations wrapped in a transaction for atomicity.
tx, txErr := db.DB.Begin(r.Context())
tx, txErr := db.Conn.Begin(r.Context())
if txErr != nil {
log.Printf("Failed to begin transaction for reschedule: %v", txErr)
http.Error(w, "Internal server error", http.StatusInternalServerError)
@@ -4147,7 +4150,7 @@ func AdminRescheduleBookingHandler(w http.ResponseWriter, r *http.Request) {
}
var durationMinutes int
if err := db.DB.QueryRow(r.Context(), `
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
@@ -4180,10 +4183,11 @@ func AdminRescheduleBookingHandler(w http.ResponseWriter, r *http.Request) {
if daysToMonday == 0 {
daysToMonday = 7
}
weekStart := req.StartTime.AddDate(0, 0, -daysToMonday+1).Truncate(24 * time.Hour)
tm := req.StartTime.AddDate(0, 0, -daysToMonday+1)
weekStart := time.Date(tm.Year(), tm.Month(), tm.Day(), 0, 0, 0, 0, tm.Location())
var isClosed bool
if err := db.DB.QueryRow(r.Context(), `
if err := db.Conn.QueryRow(r.Context(), `
SELECT EXISTS (
SELECT 1 FROM exceptional_working_hours ewh
JOIN exceptional_group_applications ega ON ewh.group_id = ega.group_id