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:
@@ -138,7 +138,7 @@ func calculateDiscountPreview(ctx context.Context, bookingID string, userID stri
|
||||
}
|
||||
|
||||
var bookingTotal float64
|
||||
db.DB.QueryRow(ctx, `
|
||||
db.Conn.QueryRow(ctx, `
|
||||
SELECT COALESCE(SUM(price_val), 0) FROM (
|
||||
SELECT COALESCE(bs.override_price, s.price) AS price_val
|
||||
FROM booking_services bs
|
||||
@@ -162,7 +162,7 @@ func calculateDiscountPreview(ctx context.Context, bookingID string, userID stri
|
||||
var campaignID string
|
||||
var campaignPercent float64
|
||||
var campaignName string
|
||||
if err := db.DB.QueryRow(ctx, `
|
||||
if err := db.Conn.QueryRow(ctx, `
|
||||
SELECT id, discount_percent, name FROM discount_campaigns
|
||||
WHERE status = 'active' AND campaign_type = 'time_based'
|
||||
AND start_date <= NOW() AND end_date >= NOW()
|
||||
@@ -170,7 +170,7 @@ func calculateDiscountPreview(ctx context.Context, bookingID string, userID stri
|
||||
ORDER BY discount_percent DESC LIMIT 1
|
||||
`).Scan(&campaignID, &campaignPercent, &campaignName); err == nil && campaignID != "" {
|
||||
var exists int
|
||||
db.DB.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, campaignID).Scan(&exists)
|
||||
db.Conn.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, campaignID).Scan(&exists)
|
||||
if exists == 0 {
|
||||
amount := roundTo2(bookingTotal * campaignPercent / 100)
|
||||
resp.Discounts = append(resp.Discounts, DiscountPreview{
|
||||
@@ -184,12 +184,12 @@ func calculateDiscountPreview(ctx context.Context, bookingID string, userID stri
|
||||
}
|
||||
|
||||
var userBookingCount int
|
||||
db.DB.QueryRow(ctx, `SELECT COUNT(*) FROM bookings WHERE user_id = $1 AND status = 'completed'`, userID).Scan(&userBookingCount)
|
||||
db.Conn.QueryRow(ctx, `SELECT COUNT(*) FROM bookings WHERE user_id = $1 AND status = 'completed'`, userID).Scan(&userBookingCount)
|
||||
|
||||
var milestoneCampaignID string
|
||||
var milestonePercent float64
|
||||
var milestoneName string
|
||||
db.DB.QueryRow(ctx, `
|
||||
db.Conn.QueryRow(ctx, `
|
||||
SELECT id, discount_percent, name FROM discount_campaigns
|
||||
WHERE status = 'active' AND campaign_type = 'milestone' AND milestone_type = 'per_user_booking_count'
|
||||
AND milestone_value = $1
|
||||
@@ -198,7 +198,7 @@ func calculateDiscountPreview(ctx context.Context, bookingID string, userID stri
|
||||
|
||||
if milestoneCampaignID != "" {
|
||||
var exists int
|
||||
db.DB.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, milestoneCampaignID).Scan(&exists)
|
||||
db.Conn.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, milestoneCampaignID).Scan(&exists)
|
||||
if exists == 0 {
|
||||
amount := roundTo2(bookingTotal * milestonePercent / 100)
|
||||
resp.Discounts = append(resp.Discounts, DiscountPreview{
|
||||
@@ -212,7 +212,7 @@ func calculateDiscountPreview(ctx context.Context, bookingID string, userID stri
|
||||
}
|
||||
|
||||
var firstVisitDate time.Time
|
||||
db.DB.QueryRow(ctx, `SELECT MIN(start_time) FROM bookings WHERE user_id = $1 AND status = 'completed'`, userID).Scan(&firstVisitDate)
|
||||
db.Conn.QueryRow(ctx, `SELECT MIN(start_time) FROM bookings WHERE user_id = $1 AND status = 'completed'`, userID).Scan(&firstVisitDate)
|
||||
if !firstVisitDate.IsZero() {
|
||||
type annCamp struct {
|
||||
id string
|
||||
@@ -221,7 +221,7 @@ func calculateDiscountPreview(ctx context.Context, bookingID string, userID stri
|
||||
unit string
|
||||
name string
|
||||
}
|
||||
annRows, err := db.DB.Query(ctx, `
|
||||
annRows, err := db.Conn.Query(ctx, `
|
||||
SELECT id, discount_percent, milestone_value, milestone_unit, name FROM discount_campaigns
|
||||
WHERE status = 'active' AND campaign_type = 'milestone' AND milestone_type = 'anniversary'
|
||||
AND NOT EXISTS (SELECT 1 FROM booking_discounts WHERE user_id = $1 AND source_id = discount_campaigns.id AND milestone_type = 'anniversary')
|
||||
@@ -238,7 +238,7 @@ func calculateDiscountPreview(ctx context.Context, bookingID string, userID stri
|
||||
|
||||
for _, c := range campaigns {
|
||||
var exists int
|
||||
db.DB.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, c.id).Scan(&exists)
|
||||
db.Conn.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, c.id).Scan(&exists)
|
||||
if exists > 0 {
|
||||
continue
|
||||
}
|
||||
@@ -268,13 +268,13 @@ func calculateDiscountPreview(ctx context.Context, bookingID string, userID stri
|
||||
// Check for referrer's unused referral discount
|
||||
var rdID string
|
||||
var rdPercent float64
|
||||
if err := db.DB.QueryRow(ctx, `
|
||||
if err := db.Conn.QueryRow(ctx, `
|
||||
SELECT id, discount_percent FROM referral_discounts
|
||||
WHERE user_id = $1 AND used = FALSE
|
||||
LIMIT 1
|
||||
`, userID).Scan(&rdID, &rdPercent); err == nil && rdID != "" {
|
||||
exists := 0
|
||||
db.DB.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND discount_source = 'referral' AND source_id = $2`, bookingID, rdID).Scan(&exists)
|
||||
db.Conn.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND discount_source = 'referral' AND source_id = $2`, bookingID, rdID).Scan(&exists)
|
||||
if exists == 0 {
|
||||
amount := roundTo2(bookingTotal * rdPercent / 100)
|
||||
resp.Discounts = append(resp.Discounts, DiscountPreview{
|
||||
@@ -370,7 +370,7 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Route based on payment method
|
||||
if req.PaymentMethod != nil && (*req.PaymentMethod == "cash" || *req.PaymentMethod == "giftcard") {
|
||||
tx, err := db.DB.Begin(r.Context())
|
||||
tx, err := db.Conn.Begin(r.Context())
|
||||
if err != nil {
|
||||
log.Printf("Failed to begin transaction: %v", err)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
@@ -700,10 +700,10 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) {
|
||||
//
|
||||
// We acquire a dedicated connection from the pool and hold it for the
|
||||
// duration of the handler so that lock and unlock use the same connection.
|
||||
// Using db.DB.Exec() for both would be unsafe — each call may get a
|
||||
// Using db.Conn.Exec() for both would be unsafe — each call may get a
|
||||
// different pool connection, and pg_advisory_unlock on a different session
|
||||
// is a silent no-op, leaking the lock.
|
||||
pinConn, err := db.DB.Acquire(r.Context())
|
||||
pinConn, err := db.Conn.Acquire(r.Context())
|
||||
if err != nil {
|
||||
log.Printf("Failed to acquire connection for payment lock: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
@@ -772,7 +772,7 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) {
|
||||
// concurrent payments of the same type.
|
||||
if req.PaymentType != "partial" {
|
||||
var existingCount int
|
||||
if err := db.DB.QueryRow(r.Context(), `
|
||||
if err := db.Conn.QueryRow(r.Context(), `
|
||||
SELECT COUNT(*) FROM payments
|
||||
WHERE booking_id = $1
|
||||
AND status = 'completed'
|
||||
@@ -887,7 +887,7 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) {
|
||||
// so that if any insert fails the entire group rolls back. This prevents
|
||||
// a data inconsistency where Square charged the customer but only part of
|
||||
// the split is reflected in the DB.
|
||||
tx, txErr := db.DB.Begin(r.Context())
|
||||
tx, txErr := db.Conn.Begin(r.Context())
|
||||
if txErr != nil {
|
||||
log.Printf("Failed to begin transaction for payment records: %v", txErr)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
@@ -972,7 +972,7 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) {
|
||||
// would create a credit balance or require a refund.
|
||||
func applyEligibleCampaignsAtPayment(ctx context.Context, bookingID string, userID string) {
|
||||
var existingPayment int
|
||||
db.DB.QueryRow(ctx, `
|
||||
db.Conn.QueryRow(ctx, `
|
||||
SELECT COUNT(*) FROM payments
|
||||
WHERE booking_id = $1 AND status = 'completed' AND payment_method NOT IN ('discount', 'on_the_house')
|
||||
`, bookingID).Scan(&existingPayment)
|
||||
@@ -987,7 +987,7 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, bookingID string, user
|
||||
}
|
||||
|
||||
var bookingTotal float64
|
||||
if err := db.DB.QueryRow(ctx, `
|
||||
if err := db.Conn.QueryRow(ctx, `
|
||||
SELECT COALESCE(SUM(price_val), 0) FROM (
|
||||
SELECT COALESCE(bs.override_price, s.price) AS price_val
|
||||
FROM booking_services bs
|
||||
@@ -1007,7 +1007,7 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, bookingID string, user
|
||||
return
|
||||
}
|
||||
|
||||
tx, err := db.DB.Begin(ctx)
|
||||
tx, err := db.Conn.Begin(ctx)
|
||||
if err != nil {
|
||||
log.Printf("Failed to begin discount application transaction: %v", err)
|
||||
return
|
||||
@@ -1768,7 +1768,7 @@ func AcquirePaymentLock(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Verify the user owns this booking.
|
||||
var bookingUserID string
|
||||
if err := db.DB.QueryRow(r.Context(),
|
||||
if err := db.Conn.QueryRow(r.Context(),
|
||||
"SELECT user_id FROM bookings WHERE id = $1", bookingID,
|
||||
).Scan(&bookingUserID); err != nil {
|
||||
http.Error(w, "Booking not found", http.StatusNotFound)
|
||||
@@ -1786,7 +1786,7 @@ func AcquirePaymentLock(w http.ResponseWriter, r *http.Request) {
|
||||
// handles the sub-5-minute race, this catches the >5-minute gap.
|
||||
var currentStatus string
|
||||
var startTime time.Time
|
||||
if err := db.DB.QueryRow(r.Context(),
|
||||
if err := db.Conn.QueryRow(r.Context(),
|
||||
"SELECT status, start_time FROM bookings WHERE id = $1", bookingID,
|
||||
).Scan(¤tStatus, &startTime); err != nil {
|
||||
http.Error(w, "Booking not found", http.StatusNotFound)
|
||||
@@ -1803,14 +1803,14 @@ func AcquirePaymentLock(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Upsert the time_blocker: delete any existing PAYMENT_IN_FLIGHT for this
|
||||
// booking, then insert a fresh one. This effectively extends the lock.
|
||||
if _, err := db.DB.Exec(r.Context(), `
|
||||
if _, err := db.Conn.Exec(r.Context(), `
|
||||
DELETE FROM time_blockers
|
||||
WHERE description = 'PAYMENT_IN_FLIGHT:' || $1
|
||||
`, bookingID); err != nil {
|
||||
log.Printf("Failed to clear previous payment lock for booking %s: %v", bookingID, err)
|
||||
}
|
||||
|
||||
if _, err := db.DB.Exec(r.Context(), `
|
||||
if _, err := db.Conn.Exec(r.Context(), `
|
||||
INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
|
||||
VALUES (NOW(), $1, $2, $3)
|
||||
`, PaymentLockDuration, "PAYMENT_IN_FLIGHT:"+bookingID, userID); err != nil {
|
||||
@@ -1835,7 +1835,7 @@ func ReleasePaymentLock(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := db.DB.Exec(r.Context(), `
|
||||
if _, err := db.Conn.Exec(r.Context(), `
|
||||
DELETE FROM time_blockers
|
||||
WHERE description = 'PAYMENT_IN_FLIGHT:' || $1
|
||||
`, bookingID); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user