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
+19 -17
View File
@@ -61,7 +61,7 @@ func ToggleService(w http.ResponseWriter, r *http.Request) {
}
query := "UPDATE services SET is_active = NOT is_active WHERE id = $1"
result, err := db.DB.Exec(r.Context(), query, serviceID)
result, err := db.Conn.Exec(r.Context(), query, serviceID)
if err != nil {
http.Error(w, "Failed to toggle service: "+err.Error(), http.StatusInternalServerError)
return
@@ -136,7 +136,7 @@ func CreateServiceHandler(w http.ResponseWriter, r *http.Request) {
var service Service
var createdByDB sql.NullString
err := db.DB.QueryRow(r.Context(),
err := db.Conn.QueryRow(r.Context(),
query,
req.Name,
req.Description,
@@ -201,7 +201,7 @@ func DeleteServiceHandler(w http.ResponseWriter, r *http.Request) {
// Use soft delete - set is_active to FALSE instead of hard delete
// This preserves referential integrity with booking_services
query := "UPDATE services SET is_active = FALSE WHERE id = $1"
result, err := db.DB.Exec(r.Context(), query, serviceID)
result, err := db.Conn.Exec(r.Context(), query, serviceID)
if err != nil {
http.Error(w, "Failed to delete service: "+err.Error(), http.StatusInternalServerError)
return
@@ -254,7 +254,7 @@ func ServicesHandler(w http.ResponseWriter, r *http.Request) {
ORDER BY s.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 services: "+err.Error(), http.StatusInternalServerError)
return
@@ -304,7 +304,7 @@ func ServicesHandler(w http.ResponseWriter, r *http.Request) {
// User is logged in and not admin - check eligibility
// Get user's date of birth
var dob time.Time
err := db.DB.QueryRow(r.Context(), `SELECT date_of_birth FROM users WHERE id = $1`, userID).Scan(&dob)
err := db.Conn.QueryRow(r.Context(), `SELECT date_of_birth FROM users WHERE id = $1`, userID).Scan(&dob)
if err != nil {
http.Error(w, "Failed to get user data: "+err.Error(), http.StatusInternalServerError)
return
@@ -317,6 +317,10 @@ func ServicesHandler(w http.ResponseWriter, r *http.Request) {
age--
}
// Preload patch test data once to avoid N+1 queries.
// Must be done BEFORE querying services so the tx connection isn't busy.
patchTests := loadPatchTests(r.Context(), userID)
// Get all active services
query := `
SELECT s.id, s.name, s.description, s.price, s.duration_minutes,
@@ -327,7 +331,7 @@ func ServicesHandler(w http.ResponseWriter, r *http.Request) {
ORDER BY s.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 services: "+err.Error(), http.StatusInternalServerError)
return
@@ -337,9 +341,6 @@ func ServicesHandler(w http.ResponseWriter, r *http.Request) {
var services []ServiceResponse
var ineligibleServices []ServiceResponse
// Preload patch test data once to avoid N+1 queries
patchTests := loadPatchTests(r.Context(), userID)
for rows.Next() {
var service ServiceResponse
@@ -407,7 +408,7 @@ func ServicesEligibleForUserHandler(w http.ResponseWriter, r *http.Request) {
// Get user's date of birth
var dob time.Time
err := db.DB.QueryRow(r.Context(), `SELECT date_of_birth FROM users WHERE id = $1`, userID).Scan(&dob)
err := db.Conn.QueryRow(r.Context(), `SELECT date_of_birth FROM users WHERE id = $1`, userID).Scan(&dob)
if errors.Is(err, pgx.ErrNoRows) {
http.Error(w, "user not found", http.StatusNotFound)
return
@@ -424,6 +425,10 @@ func ServicesEligibleForUserHandler(w http.ResponseWriter, r *http.Request) {
age--
}
// Preload patch test data once to avoid N+1 queries.
// Must be done BEFORE querying services so the tx connection isn't busy.
patchTests := loadPatchTests(r.Context(), userID)
// Get all active services
query := `
SELECT s.id, s.name, s.description, s.price, s.duration_minutes,
@@ -434,7 +439,7 @@ func ServicesEligibleForUserHandler(w http.ResponseWriter, r *http.Request) {
ORDER BY s.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 services: "+err.Error(), http.StatusInternalServerError)
return
@@ -444,9 +449,6 @@ func ServicesEligibleForUserHandler(w http.ResponseWriter, r *http.Request) {
var services []ServiceResponse
var grayedOutServices []ServiceResponse
// Preload patch test data once to avoid N+1 queries
patchTests := loadPatchTests(r.Context(), userID)
for rows.Next() {
var service ServiceResponse
@@ -516,7 +518,7 @@ func loadPatchTests(ctx context.Context, userID string) map[string]*patchTestInf
result := make(map[string]*patchTestInfo)
// Query 1: load all patch_test records
rows, err := db.DB.Query(ctx, `
rows, err := db.Conn.Query(ctx, `
SELECT id, notice_duration_hours, expiry_months, service_ids
FROM patch_tests
`)
@@ -547,7 +549,7 @@ func loadPatchTests(ctx context.Context, userID string) map[string]*patchTestInf
// Query 2: load user_patch_test records for this user
testedAtMap := make(map[string]time.Time)
if len(patchTests) > 0 {
uRows, err := db.DB.Query(ctx, `
uRows, err := db.Conn.Query(ctx, `
SELECT patch_test_id, tested_at
FROM user_patch_tests
WHERE user_id = $1
@@ -632,7 +634,7 @@ func AllServicesHandler(w http.ResponseWriter, r *http.Request) {
ORDER BY s.is_active DESC, s.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 services: "+err.Error(), http.StatusInternalServerError)
return