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>
184 lines
5.3 KiB
Go
184 lines
5.3 KiB
Go
package admin
|
|
|
|
import (
|
|
"crussell/db"
|
|
"crussell/internal/validators"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
// PatchTest represents a patch test definition
|
|
type PatchTest struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Description *string `json:"description,omitempty"`
|
|
NoticeDurationHours int `json:"notice_duration_hours"`
|
|
ExpiryMonths int `json:"expiry_months"`
|
|
ServiceIDs []string `json:"service_ids"`
|
|
}
|
|
|
|
// CreatePatchTestRequest represents the request payload
|
|
type CreatePatchTestRequest struct {
|
|
Name string `json:"name" validate:"required,min=1,max=200"`
|
|
Description *string `json:"description,omitempty" validate:"omitempty,max=1000"`
|
|
NoticeDurationHours int `json:"notice_duration_hours"`
|
|
ExpiryMonths int `json:"expiry_months"`
|
|
ServiceIDs []string `json:"service_ids"`
|
|
}
|
|
|
|
// UpdatePatchTestRequest represents the request payload
|
|
type UpdatePatchTestRequest struct {
|
|
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=200"`
|
|
Description *string `json:"description,omitempty" validate:"omitempty,max=1000"`
|
|
NoticeDurationHours *int `json:"notice_duration_hours,omitempty"`
|
|
ExpiryMonths *int `json:"expiry_months,omitempty"`
|
|
ServiceIDs []string `json:"service_ids,omitempty"`
|
|
}
|
|
|
|
// GetPatchTests handles GET /api/admin/patch-tests
|
|
func GetPatchTests(w http.ResponseWriter, r *http.Request) {
|
|
query := `
|
|
SELECT id, name, description, notice_duration_hours, expiry_months, service_ids
|
|
FROM patch_tests
|
|
ORDER BY name
|
|
`
|
|
|
|
rows, err := db.Conn.Query(r.Context(), query)
|
|
if err != nil {
|
|
http.Error(w, "Failed to fetch patch tests: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var patchTests []PatchTest
|
|
for rows.Next() {
|
|
var pt PatchTest
|
|
var desc sql.NullString
|
|
err := rows.Scan(&pt.ID, &pt.Name, &desc, &pt.NoticeDurationHours, &pt.ExpiryMonths, &pt.ServiceIDs)
|
|
if err != nil {
|
|
http.Error(w, "Failed to read patch test data: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if desc.Valid {
|
|
pt.Description = &desc.String
|
|
}
|
|
patchTests = append(patchTests, pt)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(patchTests)
|
|
}
|
|
|
|
// CreatePatchTest handles POST /api/admin/patch-tests
|
|
func CreatePatchTest(w http.ResponseWriter, r *http.Request) {
|
|
var req CreatePatchTestRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := validators.Validate.Struct(&req); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
query := `
|
|
INSERT INTO patch_tests (name, description, notice_duration_hours, expiry_months, service_ids)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
RETURNING id
|
|
`
|
|
|
|
var id string
|
|
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
|
|
}
|
|
|
|
w.WriteHeader(http.StatusCreated)
|
|
json.NewEncoder(w).Encode(map[string]string{"id": id})
|
|
}
|
|
|
|
// UpdatePatchTest handles PUT /api/admin/patch-tests/{id}
|
|
func UpdatePatchTest(w http.ResponseWriter, r *http.Request) {
|
|
id := chi.URLParam(r, "id")
|
|
if id == "" || !validators.IsValidID(id) {
|
|
http.Error(w, "Patch test not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
var req UpdatePatchTestRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := validators.Validate.Struct(&req); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
query := "UPDATE patch_tests SET "
|
|
args := []interface{}{}
|
|
i := 1
|
|
|
|
if req.Name != nil {
|
|
query += "name = $" + strconv.Itoa(i) + ", "
|
|
args = append(args, *req.Name)
|
|
i++
|
|
}
|
|
if req.Description != nil {
|
|
query += "description = $" + strconv.Itoa(i) + ", "
|
|
args = append(args, *req.Description)
|
|
i++
|
|
}
|
|
if req.NoticeDurationHours != nil {
|
|
query += "notice_duration_hours = $" + strconv.Itoa(i) + ", "
|
|
args = append(args, *req.NoticeDurationHours)
|
|
i++
|
|
}
|
|
if req.ExpiryMonths != nil {
|
|
query += "expiry_months = $" + strconv.Itoa(i) + ", "
|
|
args = append(args, *req.ExpiryMonths)
|
|
i++
|
|
}
|
|
if req.ServiceIDs != nil {
|
|
query += "service_ids = $" + strconv.Itoa(i) + ", "
|
|
args = append(args, req.ServiceIDs)
|
|
i++
|
|
}
|
|
|
|
query = query[:len(query)-2]
|
|
query += " WHERE id = $" + strconv.Itoa(i)
|
|
args = append(args, id)
|
|
|
|
_, err := db.Conn.Exec(r.Context(), query, args...)
|
|
if err != nil {
|
|
http.Error(w, "Failed to update patch test", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// DeletePatchTest handles DELETE /api/admin/patch-tests/{id}
|
|
func DeletePatchTest(w http.ResponseWriter, r *http.Request) {
|
|
id := chi.URLParam(r, "id")
|
|
if id == "" || !validators.IsValidID(id) {
|
|
http.Error(w, "Patch test not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
_, 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
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|