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>
This commit is contained in:
@@ -3,6 +3,7 @@ package services
|
||||
import (
|
||||
"context"
|
||||
"crussell/auth"
|
||||
"crussell/clock"
|
||||
"crussell/db"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"crussell/internal/validators"
|
||||
@@ -60,8 +61,15 @@ func ToggleService(w http.ResponseWriter, r *http.Request) {
|
||||
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())
|
||||
|
||||
query := "UPDATE services SET is_active = NOT is_active WHERE id = $1"
|
||||
result, err := db.Conn.Exec(r.Context(), query, serviceID)
|
||||
result, err := tx.Exec(r.Context(), query, serviceID)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to toggle service: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -72,7 +80,11 @@ func ToggleService(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := tx.Commit(r.Context()); err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"message": "Service toggled successfully",
|
||||
@@ -122,6 +134,13 @@ func CreateServiceHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Insert new service
|
||||
tx, err := db.Conn.Begin(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer tx.Rollback(r.Context())
|
||||
|
||||
query := `
|
||||
INSERT INTO services (
|
||||
name, description, price, duration_minutes,
|
||||
@@ -136,7 +155,7 @@ func CreateServiceHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var service Service
|
||||
var createdByDB sql.NullString
|
||||
|
||||
err := db.Conn.QueryRow(r.Context(),
|
||||
err = tx.QueryRow(r.Context(),
|
||||
query,
|
||||
req.Name,
|
||||
req.Description,
|
||||
@@ -166,6 +185,11 @@ func CreateServiceHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := tx.Commit(r.Context()); err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// Check for duplicate name or other constraints
|
||||
if err.Error() == "pq: duplicate key value violates unique constraint" {
|
||||
@@ -182,7 +206,7 @@ func CreateServiceHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Return created service
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
if err := json.NewEncoder(w).Encode(service); err != nil {
|
||||
http.Error(w, "Failed to encode response: "+err.Error(), http.StatusInternalServerError)
|
||||
@@ -200,8 +224,15 @@ 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
|
||||
tx, err := db.Conn.Begin(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer tx.Rollback(r.Context())
|
||||
|
||||
query := "UPDATE services SET is_active = FALSE WHERE id = $1"
|
||||
result, err := db.Conn.Exec(r.Context(), query, serviceID)
|
||||
result, err := tx.Exec(r.Context(), query, serviceID)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to delete service: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -212,7 +243,11 @@ func DeleteServiceHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := tx.Commit(r.Context()); err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"message": "Service deleted successfully",
|
||||
@@ -288,7 +323,7 @@ func ServicesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
if services == nil {
|
||||
@@ -311,7 +346,7 @@ func ServicesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Calculate age
|
||||
now := time.Now()
|
||||
now := clock.Now()
|
||||
age := now.Year() - dob.Year()
|
||||
if now.YearDay() < dob.YearDay() {
|
||||
age--
|
||||
@@ -385,7 +420,7 @@ func ServicesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Combine: eligible + ineligible
|
||||
services = append(services, ineligibleServices...)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
if services == nil {
|
||||
@@ -419,7 +454,7 @@ func ServicesEligibleForUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Calculate age
|
||||
now := time.Now()
|
||||
now := clock.Now()
|
||||
age := now.Year() - dob.Year()
|
||||
if now.YearDay() < dob.YearDay() {
|
||||
age--
|
||||
@@ -492,7 +527,7 @@ func ServicesEligibleForUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Sort and combine: valid first, then grayed out
|
||||
services = append(services, grayedOutServices...)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
if services == nil {
|
||||
@@ -603,7 +638,7 @@ func checkPatchTestStatus(ctx context.Context, userID, serviceID string, patchTe
|
||||
|
||||
// Check if notice period has passed (can only book after this time)
|
||||
eligibleFrom := info.testedAt.Add(time.Duration(info.noticeDurationHours) * time.Hour)
|
||||
if time.Now().Before(eligibleFrom) {
|
||||
if clock.Now().Before(eligibleFrom) {
|
||||
// Not yet eligible (within notice period)
|
||||
status := "required"
|
||||
return &status
|
||||
@@ -611,7 +646,7 @@ func checkPatchTestStatus(ctx context.Context, userID, serviceID string, patchTe
|
||||
|
||||
// Check if patch test has expired
|
||||
expiresAt := info.testedAt.AddDate(0, info.expiryMonths, 0)
|
||||
if time.Now().After(expiresAt) {
|
||||
if clock.Now().After(expiresAt) {
|
||||
// Patch test expired
|
||||
status := "expired"
|
||||
return &status
|
||||
@@ -678,7 +713,7 @@ func AllServicesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Set response headers
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
// Return empty array instead of null if no services found
|
||||
|
||||
Reference in New Issue
Block a user