Files
Crussell/backend/handlers/admin/patch_tests.go
T
popertotsandSisyphus e4b9003439 refactor(handlers): migrate remaining backend handlers to clock.Now() and transaction patterns
Apply clock.Now() migration, transaction wrapping, and minor refactors across admin, scheduling, today, user, auth handler, notifications, webhooks, services, portfolio, ratelimit, testutils, and main.go.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-24 23:43:50 +01:00

223 lines
6.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)
}
if err := rows.Err(); err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
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
`
tx, err := db.Conn.Begin(r.Context())
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
var id string
err = tx.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
}
if err := tx.Commit(r.Context()); err != nil {
http.Error(w, "Internal server 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)
tx, err := db.Conn.Begin(r.Context())
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
_, err = tx.Exec(r.Context(), query, args...)
if err != nil {
http.Error(w, "Failed to update patch test", http.StatusInternalServerError)
return
}
if err := tx.Commit(r.Context()); err != nil {
http.Error(w, "Internal server error", 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
}
tx, err := db.Conn.Begin(r.Context())
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
_, err = tx.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
}
if err := tx.Commit(r.Context()); err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}