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:
@@ -73,7 +73,7 @@ func GetCustomServices(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil || n <= 0 || n > 20 {
|
||||
n = 3
|
||||
}
|
||||
rows, err := db.DB.Query(r.Context(), `
|
||||
rows, err := db.Conn.Query(r.Context(), `
|
||||
SELECT id, name, price, duration_minutes, minimum_age_required, created_at, usage_count
|
||||
FROM custom_services
|
||||
WHERE usage_count > 0
|
||||
@@ -136,7 +136,7 @@ func GetCustomServices(w http.ResponseWriter, r *http.Request) {
|
||||
dataArgs = append(dataArgs, perPage+1)
|
||||
}
|
||||
|
||||
rows, err := db.DB.Query(r.Context(), dataQuery, dataArgs...)
|
||||
rows, err := db.Conn.Query(r.Context(), dataQuery, dataArgs...)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to fetch custom services: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -146,17 +146,6 @@ func GetCustomServices(w http.ResponseWriter, r *http.Request) {
|
||||
var services []CustomService
|
||||
var total int64
|
||||
|
||||
// Compute total from whichever count query ran above.
|
||||
if q != "" {
|
||||
var countTotal int64
|
||||
db.DB.QueryRow(r.Context(), "SELECT COUNT(*) FROM custom_services WHERE name ILIKE $1 OR description ILIKE $1", "%"+q+"%").Scan(&countTotal)
|
||||
total = countTotal
|
||||
} else {
|
||||
var countTotal int64
|
||||
db.DB.QueryRow(r.Context(), "SELECT COUNT(*) FROM custom_services").Scan(&countTotal)
|
||||
total = countTotal
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var cs CustomService
|
||||
if err := rows.Scan(&cs.ID, &cs.Name, &cs.Price, &cs.DurationMinutes, &cs.MinimumAgeRequired, &cs.CreatedAt, &cs.UsageCount); err != nil {
|
||||
@@ -165,6 +154,18 @@ func GetCustomServices(w http.ResponseWriter, r *http.Request) {
|
||||
services = append(services, cs)
|
||||
}
|
||||
|
||||
// Run count query ONLY after consuming the data query result set,
|
||||
// so pgx does not return "conn busy" on the same transaction.
|
||||
if q != "" {
|
||||
var countTotal int64
|
||||
db.Conn.QueryRow(r.Context(), "SELECT COUNT(*) FROM custom_services WHERE name ILIKE $1 OR description ILIKE $1", "%"+q+"%").Scan(&countTotal)
|
||||
total = countTotal
|
||||
} else {
|
||||
var countTotal int64
|
||||
db.Conn.QueryRow(r.Context(), "SELECT COUNT(*) FROM custom_services").Scan(&countTotal)
|
||||
total = countTotal
|
||||
}
|
||||
|
||||
// nextCursor is set only when we fetched perPage+1 items, proving a next page exists.
|
||||
var nextCursor *string
|
||||
if len(services) > perPage {
|
||||
@@ -229,7 +230,7 @@ func CreateCustomService(w http.ResponseWriter, r *http.Request) {
|
||||
var desc, notes, createdByDB sql.NullString
|
||||
var lastUsedAt sql.NullTime
|
||||
|
||||
err := db.DB.QueryRow(r.Context(), query, req.Name, req.Description, req.Price, req.DurationMinutes, req.MinimumAgeRequired, req.Notes, createdBy).Scan(
|
||||
err := db.Conn.QueryRow(r.Context(), query, req.Name, req.Description, req.Price, req.DurationMinutes, req.MinimumAgeRequired, req.Notes, createdBy).Scan(
|
||||
&cs.ID, &cs.Name, &desc, &cs.Price, &cs.DurationMinutes, &cs.MinimumAgeRequired, ¬es, &cs.CreatedAt, &createdByDB, &cs.UsageCount, &lastUsedAt,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -266,7 +267,7 @@ func GetCustomService(w http.ResponseWriter, r *http.Request) {
|
||||
var desc, notes, createdBy sql.NullString
|
||||
var lastUsedAt sql.NullTime
|
||||
|
||||
err := db.DB.QueryRow(r.Context(), `
|
||||
err := db.Conn.QueryRow(r.Context(), `
|
||||
SELECT id, name, description, price, duration_minutes, minimum_age_required, notes, created_at, created_by, usage_count, last_used_at
|
||||
FROM custom_services WHERE id = $1
|
||||
`, id).Scan(&cs.ID, &cs.Name, &desc, &cs.Price, &cs.DurationMinutes, &cs.MinimumAgeRequired, ¬es, &cs.CreatedAt, &createdBy, &cs.UsageCount, &lastUsedAt)
|
||||
@@ -350,7 +351,7 @@ func UpdateCustomService(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
query := "UPDATE custom_services SET " + joinStrings(setClauses, ", ") + " WHERE id = $" + strconv.Itoa(argIdx)
|
||||
|
||||
result, err := db.DB.Exec(r.Context(), query, args...)
|
||||
result, err := db.Conn.Exec(r.Context(), query, args...)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to update custom service: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -371,7 +372,7 @@ func PromoteCustomService(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
tx, err := db.DB.Begin(r.Context())
|
||||
tx, err := db.Conn.Begin(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to start transaction", http.StatusInternalServerError)
|
||||
return
|
||||
@@ -455,7 +456,7 @@ func DeleteCustomService(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
var usageCount int
|
||||
err := db.DB.QueryRow(r.Context(), `SELECT usage_count FROM custom_services WHERE id = $1`, id).Scan(&usageCount)
|
||||
err := db.Conn.QueryRow(r.Context(), `SELECT usage_count FROM custom_services WHERE id = $1`, id).Scan(&usageCount)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
http.Error(w, "Custom service not found", http.StatusNotFound)
|
||||
return
|
||||
@@ -469,7 +470,7 @@ func DeleteCustomService(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
result, err := db.DB.Exec(r.Context(), `DELETE FROM custom_services WHERE id = $1`, id)
|
||||
result, err := db.Conn.Exec(r.Context(), `DELETE FROM custom_services WHERE id = $1`, id)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to delete custom service: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
@@ -99,7 +99,7 @@ func GetDiscountCampaigns(w http.ResponseWriter, r *http.Request) {
|
||||
WHERE status = $1
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
rows, err = db.DB.Query(r.Context(), query, statusFilter)
|
||||
rows, err = db.Conn.Query(r.Context(), query, statusFilter)
|
||||
} else {
|
||||
query = `
|
||||
SELECT id, name, description, campaign_type, discount_percent, scope,
|
||||
@@ -108,7 +108,7 @@ func GetDiscountCampaigns(w http.ResponseWriter, r *http.Request) {
|
||||
FROM discount_campaigns
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
rows, err = db.DB.Query(r.Context(), query)
|
||||
rows, err = db.Conn.Query(r.Context(), query)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -295,7 +295,7 @@ func CreateDiscountCampaign(w http.ResponseWriter, r *http.Request) {
|
||||
var milestoneValue, maxRedemptions sql.NullInt32
|
||||
var createdByDB sql.NullString
|
||||
|
||||
err := db.DB.QueryRow(r.Context(),
|
||||
err := db.Conn.QueryRow(r.Context(),
|
||||
query,
|
||||
req.Name,
|
||||
req.Description,
|
||||
@@ -389,7 +389,7 @@ func UpdateDiscountCampaign(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Check if campaign exists
|
||||
var exists bool
|
||||
err := db.DB.QueryRow(r.Context(), "SELECT EXISTS(SELECT 1 FROM discount_campaigns WHERE id = $1)", campaignID).Scan(&exists)
|
||||
err := db.Conn.QueryRow(r.Context(), "SELECT EXISTS(SELECT 1 FROM discount_campaigns WHERE id = $1)", campaignID).Scan(&exists)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to check campaign: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -496,7 +496,7 @@ func UpdateDiscountCampaign(w http.ResponseWriter, r *http.Request) {
|
||||
query += " WHERE id = $" + strconv.Itoa(argNum)
|
||||
args = append(args, campaignID)
|
||||
|
||||
_, err = db.DB.Exec(r.Context(), query, args...)
|
||||
_, err = db.Conn.Exec(r.Context(), query, args...)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to update campaign: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -509,7 +509,7 @@ func UpdateDiscountCampaign(w http.ResponseWriter, r *http.Request) {
|
||||
var milestoneValue, maxRedemptions sql.NullInt32
|
||||
var createdBy sql.NullString
|
||||
|
||||
err = db.DB.QueryRow(r.Context(), `
|
||||
err = db.Conn.QueryRow(r.Context(), `
|
||||
SELECT id, name, description, campaign_type, discount_percent, scope,
|
||||
start_date, end_date, milestone_type, milestone_value, milestone_unit,
|
||||
status, max_redemptions, times_redeemed, created_at, updated_at, created_by
|
||||
@@ -595,7 +595,7 @@ func DeleteDiscountCampaign(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Check if campaign exists
|
||||
var exists bool
|
||||
err := db.DB.QueryRow(r.Context(), "SELECT EXISTS(SELECT 1 FROM discount_campaigns WHERE id = $1)", campaignID).Scan(&exists)
|
||||
err := db.Conn.QueryRow(r.Context(), "SELECT EXISTS(SELECT 1 FROM discount_campaigns WHERE id = $1)", campaignID).Scan(&exists)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to check campaign: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -607,7 +607,7 @@ func DeleteDiscountCampaign(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Soft delete - set status to cancelled
|
||||
query := "UPDATE discount_campaigns SET status = 'cancelled', updated_at = NOW() WHERE id = $1"
|
||||
result, err := db.DB.Exec(r.Context(), query, campaignID)
|
||||
result, err := db.Conn.Exec(r.Context(), query, campaignID)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to delete campaign: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -641,7 +641,7 @@ func GetCampaignStats(w http.ResponseWriter, r *http.Request) {
|
||||
var milestoneValue, maxRedemptions sql.NullInt32
|
||||
var createdBy sql.NullString
|
||||
|
||||
err := db.DB.QueryRow(r.Context(), `
|
||||
err := db.Conn.QueryRow(r.Context(), `
|
||||
SELECT id, name, description, campaign_type, discount_percent, scope,
|
||||
start_date, end_date, milestone_type, milestone_value, milestone_unit,
|
||||
status, max_redemptions, times_redeemed, created_at, updated_at, created_by
|
||||
@@ -717,7 +717,7 @@ func GetCampaignStats(w http.ResponseWriter, r *http.Request) {
|
||||
var totalDiscounts float64
|
||||
var bookingCount int
|
||||
|
||||
err = db.DB.QueryRow(r.Context(), `
|
||||
err = db.Conn.QueryRow(r.Context(), `
|
||||
SELECT COALESCE(SUM(discount_amount), 0), COUNT(*)
|
||||
FROM booking_discounts
|
||||
WHERE source_id = $1
|
||||
|
||||
@@ -47,7 +47,7 @@ func GetPatchTests(w http.ResponseWriter, r *http.Request) {
|
||||
ORDER BY name
|
||||
`
|
||||
|
||||
rows, err := db.DB.Query(r.Context(), query)
|
||||
rows, err := db.Conn.Query(r.Context(), query)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to fetch patch tests: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -93,7 +93,7 @@ func CreatePatchTest(w http.ResponseWriter, r *http.Request) {
|
||||
`
|
||||
|
||||
var id string
|
||||
err := db.DB.QueryRow(r.Context(), query, req.Name, req.Description, req.NoticeDurationHours, req.ExpiryMonths, req.ServiceIDs).Scan(&id)
|
||||
err := db.Conn.QueryRow(r.Context(), query, req.Name, req.Description, req.NoticeDurationHours, req.ExpiryMonths, req.ServiceIDs).Scan(&id)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to create patch test: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -156,7 +156,7 @@ func UpdatePatchTest(w http.ResponseWriter, r *http.Request) {
|
||||
query += " WHERE id = $" + strconv.Itoa(i)
|
||||
args = append(args, id)
|
||||
|
||||
_, err := db.DB.Exec(r.Context(), query, args...)
|
||||
_, err := db.Conn.Exec(r.Context(), query, args...)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to update patch test", http.StatusInternalServerError)
|
||||
return
|
||||
@@ -173,7 +173,7 @@ func DeletePatchTest(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
_, err := db.DB.Exec(r.Context(), "DELETE FROM patch_tests WHERE id = $1", id)
|
||||
_, err := db.Conn.Exec(r.Context(), "DELETE FROM patch_tests WHERE id = $1", id)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to delete patch test: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
@@ -24,7 +24,7 @@ type BusinessSettings struct {
|
||||
|
||||
func GetBusinessSettings(w http.ResponseWriter, r *http.Request) {
|
||||
var s BusinessSettings
|
||||
err := db.DB.QueryRow(r.Context(), `
|
||||
err := db.Conn.QueryRow(r.Context(), `
|
||||
SELECT business_name, business_address, business_phone, business_email,
|
||||
vat_registration_number, is_vat_registered, default_vat_rate,
|
||||
currency_code, website_url, gift_card_expiry_months, voucher_type
|
||||
@@ -148,7 +148,7 @@ func UpdateBusinessSettings(w http.ResponseWriter, r *http.Request) {
|
||||
query += clause
|
||||
}
|
||||
|
||||
_, err := db.DB.Exec(r.Context(), query, args...)
|
||||
_, err := db.Conn.Exec(r.Context(), query, args...)
|
||||
if err != nil {
|
||||
log.Printf("Failed to update business settings: %v", err)
|
||||
http.Error(w, "Failed to update settings", http.StatusInternalServerError)
|
||||
|
||||
@@ -18,19 +18,19 @@ import (
|
||||
|
||||
// makeAdminRequest creates a request with admin context
|
||||
// Note: Using 12-char IDs to match CHAR(12) columns in schema (e.g., created_by)
|
||||
func makeAdminRequest(handler http.Handler, method, path string, body interface{}) *httptest.ResponseRecorder {
|
||||
return makeRequestWithContext(handler, method, path, body, "admin001", "admin")
|
||||
func makeAdminRequest(handler http.Handler, method, path string, body interface{}, ctx context.Context) *httptest.ResponseRecorder {
|
||||
return makeRequestWithContext(handler, method, path, body, "admin001", "admin", ctx)
|
||||
}
|
||||
|
||||
// makeUserRequest creates a request with regular user context
|
||||
// Note: Using 12-char IDs to match CHAR(12) columns in schema (e.g., created_by)
|
||||
func makeUserRequest(handler http.Handler, method, path string, body interface{}) *httptest.ResponseRecorder {
|
||||
return makeRequestWithContext(handler, method, path, body, "user001", "verified_email")
|
||||
func makeUserRequest(handler http.Handler, method, path string, body interface{}, ctx context.Context) *httptest.ResponseRecorder {
|
||||
return makeRequestWithContext(handler, method, path, body, "user001", "verified_email", ctx)
|
||||
}
|
||||
|
||||
// makeRequestWithContext creates a request with specific user context
|
||||
|
||||
func makeRequestWithContext(handler http.Handler, method, path string, body interface{}, userID, role string) *httptest.ResponseRecorder {
|
||||
func makeRequestWithContext(handler http.Handler, method, path string, body interface{}, userID, role string, ctx context.Context) *httptest.ResponseRecorder {
|
||||
var req *http.Request
|
||||
if body != nil {
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
@@ -45,9 +45,13 @@ func makeRequestWithContext(handler http.Handler, method, path string, body inte
|
||||
if id, paramName := extractIDFromPath(path); id != "" {
|
||||
rctx.URLParams.Add(paramName, id)
|
||||
}
|
||||
// Extract request_id for: /api/admin/bookings/{id}/edit-requests/{request_id}/approve
|
||||
if parts := strings.Split(path, "/"); len(parts) >= 8 && parts[5] == "edit-requests" {
|
||||
rctx.URLParams.Add("request_id", parts[6])
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.WithValue(req.Context(), chi.RouteCtxKey, rctx)
|
||||
ctx = context.WithValue(ctx, chi.RouteCtxKey, rctx)
|
||||
ctx = context.WithValue(ctx, mw.UserIDKey, userID)
|
||||
ctx = context.WithValue(ctx, mw.UserRoleKey, role)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
Reference in New Issue
Block a user