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:
@@ -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, ¤tStatus); err != nil {
|
||||
if err := db.Conn.QueryRow(r.Context(), "SELECT start_time, status FROM bookings WHERE id = $1", bookingID).Scan(&startTime, ¤tStatus); 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(¤tStatus); err != nil {
|
||||
if err := db.Conn.QueryRow(r.Context(), "SELECT status FROM bookings WHERE id = $1 AND user_id = $2", bookingID, userID).Scan(¤tStatus); 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(¤tStatus, &bookingUserID, &startTime); err != nil {
|
||||
if err := db.Conn.QueryRow(r.Context(), "SELECT status, user_id, start_time FROM bookings WHERE id = $1", bookingID).Scan(¤tStatus, &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
|
||||
|
||||
Reference in New Issue
Block a user