fix: replace silent json.Encode with error-logging pattern across all handlers

This commit is contained in:
2026-07-11 17:50:07 +01:00
parent 0bef0f7973
commit 5d9fa1178b
24 changed files with 629 additions and 433 deletions
+35 -21
View File
@@ -105,7 +105,9 @@ func GetCustomServices(w http.ResponseWriter, r *http.Request) {
if services == nil { if services == nil {
services = []CustomService{} services = []CustomService{}
} }
_ = json.NewEncoder(w).Encode(services) if err := json.NewEncoder(w).Encode(services); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
return return
} }
@@ -192,12 +194,14 @@ func GetCustomServices(w http.ResponseWriter, r *http.Request) {
if services == nil { if services == nil {
services = []CustomService{} services = []CustomService{}
} }
_ = json.NewEncoder(w).Encode(CustomServiceListResponse{ if err := json.NewEncoder(w).Encode(CustomServiceListResponse{
Services: services, Services: services,
Total: total, Total: total,
PerPage: perPage, PerPage: perPage,
NextCursor: nextCursor, NextCursor: nextCursor,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
func CreateCustomService(w http.ResponseWriter, r *http.Request) { func CreateCustomService(w http.ResponseWriter, r *http.Request) {
@@ -266,7 +270,9 @@ func CreateCustomService(w http.ResponseWriter, r *http.Request) {
} }
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(cs) if err := json.NewEncoder(w).Encode(cs); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
func GetCustomService(w http.ResponseWriter, r *http.Request) { func GetCustomService(w http.ResponseWriter, r *http.Request) {
@@ -306,7 +312,9 @@ func GetCustomService(w http.ResponseWriter, r *http.Request) {
cs.LastUsedAt = &lastUsedAt.Time cs.LastUsedAt = &lastUsedAt.Time
} }
_ = json.NewEncoder(w).Encode(cs) if err := json.NewEncoder(w).Encode(cs); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
func UpdateCustomService(w http.ResponseWriter, r *http.Request) { func UpdateCustomService(w http.ResponseWriter, r *http.Request) {
@@ -387,10 +395,10 @@ func UpdateCustomService(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
result, err := tx.Exec(r.Context(), query, args...) result, err := tx.Exec(r.Context(), query, args...)
if err != nil { if err != nil {
@@ -407,7 +415,9 @@ func UpdateCustomService(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(map[string]string{"message": "Custom service updated"}) if err := json.NewEncoder(w).Encode(map[string]string{"message": "Custom service updated"}); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
func PromoteCustomService(w http.ResponseWriter, r *http.Request) { func PromoteCustomService(w http.ResponseWriter, r *http.Request) {
@@ -423,10 +433,10 @@ func PromoteCustomService(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
var name, desc, notes sql.NullString var name, desc, notes sql.NullString
var price float64 var price float64
@@ -496,11 +506,13 @@ func PromoteCustomService(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(map[string]string{ if err := json.NewEncoder(w).Encode(map[string]string{
"message": "Custom service promoted to regular service", "message": "Custom service promoted to regular service",
"new_service_id": newServiceID, "new_service_id": newServiceID,
"custom_service_id": id, "custom_service_id": id,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
func DeleteCustomService(w http.ResponseWriter, r *http.Request) { func DeleteCustomService(w http.ResponseWriter, r *http.Request) {
@@ -531,10 +543,10 @@ func DeleteCustomService(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
result, err := tx.Exec(r.Context(), `DELETE FROM custom_services WHERE id = $1`, id) result, err := tx.Exec(r.Context(), `DELETE FROM custom_services WHERE id = $1`, id)
if err != nil { if err != nil {
@@ -551,7 +563,9 @@ func DeleteCustomService(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(map[string]string{"message": "Custom service deleted"}) if err := json.NewEncoder(w).Encode(map[string]string{"message": "Custom service deleted"}); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
func joinStrings(strs []string, sep string) string { func joinStrings(strs []string, sep string) string {
+16 -14
View File
@@ -282,10 +282,10 @@ func CreateDiscountCampaign(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
// Insert new campaign // Insert new campaign
query := ` query := `
@@ -520,10 +520,10 @@ func UpdateDiscountCampaign(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
_, err = tx.Exec(r.Context(), query, args...) _, err = tx.Exec(r.Context(), query, args...)
if err != nil { if err != nil {
@@ -648,10 +648,10 @@ func DeleteDiscountCampaign(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
result, err := tx.Exec(r.Context(), query, campaignID) result, err := tx.Exec(r.Context(), query, campaignID)
if err != nil { if err != nil {
@@ -671,10 +671,12 @@ func DeleteDiscountCampaign(w http.ResponseWriter, r *http.Request) {
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]any{ if err := json.NewEncoder(w).Encode(map[string]any{
"message": "Campaign deleted successfully", "message": "Campaign deleted successfully",
"id": campaignID, "id": campaignID,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// GetCampaignStats handles GET /api/admin/discount-campaigns/{id}/stats // GetCampaignStats handles GET /api/admin/discount-campaigns/{id}/stats
+18 -14
View File
@@ -75,7 +75,9 @@ func GetPatchTests(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(patchTests) if err := json.NewEncoder(w).Encode(patchTests); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// CreatePatchTest handles POST /api/admin/patch-tests // CreatePatchTest handles POST /api/admin/patch-tests
@@ -104,10 +106,10 @@ func CreatePatchTest(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
var id string var id string
err = tx.QueryRow(r.Context(), query, req.Name, req.Description, req.NoticeDurationHours, req.ExpiryMonths, req.ServiceIDs).Scan(&id) err = tx.QueryRow(r.Context(), query, req.Name, req.Description, req.NoticeDurationHours, req.ExpiryMonths, req.ServiceIDs).Scan(&id)
@@ -122,7 +124,9 @@ func CreatePatchTest(w http.ResponseWriter, r *http.Request) {
} }
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(map[string]string{"id": id}) if err := json.NewEncoder(w).Encode(map[string]string{"id": id}); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// UpdatePatchTest handles PUT /api/admin/patch-tests/{id} // UpdatePatchTest handles PUT /api/admin/patch-tests/{id}
@@ -185,10 +189,10 @@ func UpdatePatchTest(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
_, err = tx.Exec(r.Context(), query, args...) _, err = tx.Exec(r.Context(), query, args...)
if err != nil { if err != nil {
@@ -218,10 +222,10 @@ func DeletePatchTest(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
_, err = tx.Exec(r.Context(), "DELETE FROM patch_tests WHERE id = $1", id) _, err = tx.Exec(r.Context(), "DELETE FROM patch_tests WHERE id = $1", id)
if err != nil { if err != nil {
+10 -6
View File
@@ -69,7 +69,9 @@ func GetPublicBusinessInfo(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(info) if err := json.NewEncoder(w).Encode(info); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
func GetBusinessSettings(w http.ResponseWriter, r *http.Request) { func GetBusinessSettings(w http.ResponseWriter, r *http.Request) {
@@ -91,7 +93,9 @@ func GetBusinessSettings(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(s) if err := json.NewEncoder(w).Encode(s); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
type UpdateBusinessSettingsRequest struct { type UpdateBusinessSettingsRequest struct {
@@ -246,10 +250,10 @@ func UpdateBusinessSettings(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
_, err = tx.Exec(r.Context(), query.String(), args...) _, err = tx.Exec(r.Context(), query.String(), args...)
if err != nil { if err != nil {
+36 -26
View File
@@ -222,10 +222,10 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
now := clock.Now() now := clock.Now()
@@ -384,10 +384,10 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
err = tx.QueryRow(r.Context(), ` err = tx.QueryRow(r.Context(), `
UPDATE users UPDATE users
@@ -433,10 +433,10 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
_, err = tx.Exec(r.Context(), `UPDATE users SET failed_attempts = 0, locked_until = NULL, last_login_at = NOW() WHERE id = $1`, userID) _, err = tx.Exec(r.Context(), `UPDATE users SET failed_attempts = 0, locked_until = NULL, last_login_at = NOW() WHERE id = $1`, userID)
if err != nil { if err != nil {
@@ -458,10 +458,12 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(auth.AuthResponse{ if err := json.NewEncoder(w).Encode(auth.AuthResponse{
Token: tokenString, Token: tokenString,
JTI: jti, JTI: jti,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// POST /api/refresh-token (requires auth middleware) // POST /api/refresh-token (requires auth middleware)
@@ -501,10 +503,12 @@ func RefreshTokenHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(auth.AuthResponse{ if err := json.NewEncoder(w).Encode(auth.AuthResponse{
Token: newToken, Token: newToken,
JTI: jti, JTI: jti,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// POST /api/logout (requires auth middleware) // POST /api/logout (requires auth middleware)
@@ -522,7 +526,9 @@ func LogoutHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(map[string]bool{"success": true}) if err := json.NewEncoder(w).Encode(map[string]bool{"success": true}); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
type VerificationCodeRequest struct { type VerificationCodeRequest struct {
@@ -562,7 +568,9 @@ func GenerateVerificationCodeHandler(w http.ResponseWriter, r *http.Request) {
).Scan(&userID) ).Scan(&userID)
if err != nil { if err != nil {
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
_ = json.NewEncoder(w).Encode(VerificationResponse{Success: true, Message: "If the email exists, a verification code will be sent"}) if err := json.NewEncoder(w).Encode(VerificationResponse{Success: true, Message: "If the email exists, a verification code will be sent"}); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
return return
} }
log.Printf("Failed to look up user: %v", err) log.Printf("Failed to look up user: %v", err)
@@ -583,7 +591,9 @@ func GenerateVerificationCodeHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(VerificationResponse{Success: true, Message: "Verification code generated"}) if err := json.NewEncoder(w).Encode(VerificationResponse{Success: true, Message: "Verification code generated"}); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
func VerifyCodeHandler(w http.ResponseWriter, r *http.Request) { func VerifyCodeHandler(w http.ResponseWriter, r *http.Request) {
@@ -646,10 +656,10 @@ func VerifyCodeHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
_, err = tx.Exec(r.Context(), _, err = tx.Exec(r.Context(),
`UPDATE verification_codes SET used_at = NOW() WHERE code = $1`, `UPDATE verification_codes SET used_at = NOW() WHERE code = $1`,
@@ -679,7 +689,7 @@ func VerifyCodeHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(VerificationResponse{Success: true, Message: "Email verified successfully"}) if err := json.NewEncoder(w).Encode(VerificationResponse{Success: true, Message: "Email verified successfully"}); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
+14 -6
View File
@@ -2006,7 +2006,9 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(existingBooking) if err := json.NewEncoder(w).Encode(existingBooking); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
return return
} }
// If err is sql.ErrNoRows, proceed with creation // If err is sql.ErrNoRows, proceed with creation
@@ -3258,9 +3260,11 @@ func DeleteBookingHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("Refund processing failed for booking %s — cancellation aborted: %v", bookingID, calcErr) log.Printf("Refund processing failed for booking %s — cancellation aborted: %v", bookingID, calcErr)
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
_ = json.NewEncoder(w).Encode(map[string]string{ if err := json.NewEncoder(w).Encode(map[string]string{
"error": "Refund processing failed — cancellation aborted. Please try again or contact support.", "error": "Refund processing failed — cancellation aborted. Please try again or contact support.",
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
return return
} }
} }
@@ -3352,7 +3356,9 @@ func DeleteBookingHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(resp) if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
return return
} }
@@ -3393,10 +3399,12 @@ func DeleteBookingHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]any{ if err := json.NewEncoder(w).Encode(map[string]any{
"message": "Booking deleted successfully", "message": "Booking deleted successfully",
"id": bookingID, "id": bookingID,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// GET /api/bookings/{id} // GET /api/bookings/{id}
+70 -50
View File
@@ -160,10 +160,10 @@ func AdminCancelBookingHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
// Get current status and user ID — use FOR UPDATE to lock the row so // Get current status and user ID — use FOR UPDATE to lock the row so
// the refund and status change are atomic. // the refund and status change are atomic.
@@ -289,7 +289,9 @@ func AdminCancelBookingHandler(w http.ResponseWriter, r *http.Request) {
resp["refund_failed"] = true resp["refund_failed"] = true
resp["warning"] = "Booking was cancelled but refund processing failed — please process refund manually or retry" resp["warning"] = "Booking was cancelled but refund processing failed — please process refund manually or retry"
} }
_ = json.NewEncoder(w).Encode(resp) if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
return return
} }
@@ -417,14 +419,16 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) {
// Get deposit info — deposit_required already fetched above in the main booking query. // Get deposit info — deposit_required already fetched above in the main booking query.
var preStartPaid float64 var preStartPaid float64
if err := db.Conn.QueryRow(r.Context(), `SELECT COALESCE(SUM(amount), 0) FROM payments WHERE booking_id = $1 AND payment_type IN ('deposit', 'full') AND status = 'completed'`, existingID).Scan(&preStartPaid); err != nil { if err := db.Conn.QueryRow(r.Context(), `SELECT COALESCE(SUM(amount), 0) FROM payments WHERE booking_id = $1 AND payment_type IN ('deposit', 'full') AND status = 'completed'`, existingID).Scan(&preStartPaid); err != nil {
log.Printf("Failed to scan preStartPaid for booking %s: %v", existingID, err) log.Printf("Failed to scan preStartPaid for booking %s: %v", existingID, err)
} }
populateDepositFields(&existingBooking, existingBooking.DepositRequired, preStartPaid) populateDepositFields(&existingBooking, existingBooking.DepositRequired, preStartPaid)
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(existingBooking) if err := json.NewEncoder(w).Encode(existingBooking); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
return return
} }
} }
@@ -671,10 +675,10 @@ if err := db.Conn.QueryRow(r.Context(), `SELECT COALESCE(SUM(amount), 0) FROM pa
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
// Evict any pending_release bookings that overlap this slot. // Evict any pending_release bookings that overlap this slot.
if _, evictErr := EvictPendingReleaseOverlapping(r.Context(), tx, req.StartTime, newEnd); evictErr != nil { if _, evictErr := EvictPendingReleaseOverlapping(r.Context(), tx, req.StartTime, newEnd); evictErr != nil {
@@ -1217,10 +1221,10 @@ func DeleteEditRequestHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
// Delete the edit request for this booking // Delete the edit request for this booking
res, err := tx.Exec(r.Context(), ` res, err := tx.Exec(r.Context(), `
@@ -1402,10 +1406,10 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
// Delete any existing edit request for this booking (upsert behavior) // Delete any existing edit request for this booking (upsert behavior)
_, err = tx.Exec(r.Context(), ` _, err = tx.Exec(r.Context(), `
@@ -1567,10 +1571,12 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]any{ if err := json.NewEncoder(w).Encode(map[string]any{
"auto_approved": true, "auto_approved": true,
"edit_request": editReq, "edit_request": editReq,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
return return
} }
@@ -1667,7 +1673,9 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(editReq) if err := json.NewEncoder(w).Encode(editReq); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// AdminListEditRequestsHandler returns all edit requests // AdminListEditRequestsHandler returns all edit requests
@@ -1748,10 +1756,12 @@ func AdminListEditRequestsHandler(w http.ResponseWriter, r *http.Request) {
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{ if err := json.NewEncoder(w).Encode(map[string]any{
"requests": requests, "requests": requests,
"total": total, "total": total,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// AdminApproveEditRequestHandler approves an edit request and updates the booking // AdminApproveEditRequestHandler approves an edit request and updates the booking
@@ -1775,10 +1785,10 @@ func AdminApproveEditRequestHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
// Get the edit request // Get the edit request
var bookingID string var bookingID string
@@ -2064,10 +2074,10 @@ func AdminRejectEditRequestHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
// Delete the edit request // Delete the edit request
_, err = tx.Exec(r.Context(), ` _, err = tx.Exec(r.Context(), `
@@ -2162,9 +2172,11 @@ func GetMyEditRequestHandler(w http.ResponseWriter, r *http.Request) {
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]any{ if err := json.NewEncoder(w).Encode(map[string]any{
"edit_request": nil, "edit_request": nil,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
return return
} }
log.Printf("Failed to get edit request for booking %s: %v", bookingID, err) log.Printf("Failed to get edit request for booking %s: %v", bookingID, err)
@@ -2181,9 +2193,11 @@ func GetMyEditRequestHandler(w http.ResponseWriter, r *http.Request) {
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{ if err := json.NewEncoder(w).Encode(map[string]any{
"edit_request": enriched, "edit_request": enriched,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// GetMyEditRequestsHandler returns all pending edit requests for the current user across all bookings. // GetMyEditRequestsHandler returns all pending edit requests for the current user across all bookings.
@@ -2245,9 +2259,11 @@ func GetMyEditRequestsHandler(w http.ResponseWriter, r *http.Request) {
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{ if err := json.NewEncoder(w).Encode(map[string]any{
"edit_requests": enrichedRequests, "edit_requests": enrichedRequests,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// AdminListAllEditRequestsHandler returns ALL pending edit requests across all bookings. // AdminListAllEditRequestsHandler returns ALL pending edit requests across all bookings.
@@ -2302,9 +2318,11 @@ func AdminListAllEditRequestsHandler(w http.ResponseWriter, r *http.Request) {
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{ if err := json.NewEncoder(w).Encode(map[string]any{
"edit_requests": enrichedRequests, "edit_requests": enrichedRequests,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// AdminGetBookingEditRequestHandler returns the pending edit request for a specific booking. // AdminGetBookingEditRequestHandler returns the pending edit request for a specific booking.
@@ -2344,17 +2362,19 @@ func AdminGetBookingEditRequestHandler(w http.ResponseWriter, r *http.Request) {
editReq.NewServices = newServices editReq.NewServices = newServices
enriched, err := buildEnrichedEditRequest(r.Context(), &editReq) enriched, err := buildEnrichedEditRequest(r.Context(), &editReq)
if err != nil { if err != nil {
log.Printf("Failed to build enriched edit request: %v", err) log.Printf("Failed to build enriched edit request: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError) http.Error(w, "Internal server error", http.StatusInternalServerError)
return return
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]any{ if err := json.NewEncoder(w).Encode(map[string]any{
"edit_request": enriched, "edit_request": enriched,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// ======================================== // ========================================
+11 -9
View File
@@ -182,10 +182,10 @@ func ReserveSlotHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
// Check booking overlap inside transaction (TOCTOU fix) // Check booking overlap inside transaction (TOCTOU fix)
// pending_release is excluded — those bookings are evicted at creation time. // pending_release is excluded — those bookings are evicted at creation time.
@@ -256,10 +256,10 @@ func ReserveSlotHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
// Check anon rate cap inside transaction // Check anon rate cap inside transaction
tenMinutesAgo := clock.Now().Add(-10 * time.Minute) tenMinutesAgo := clock.Now().Add(-10 * time.Minute)
@@ -343,5 +343,7 @@ func ReserveSlotHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(response) if err := json.NewEncoder(w).Encode(response); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
@@ -245,10 +245,10 @@ func AcknowledgeNotification(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
query := ` query := `
UPDATE admin_notifications UPDATE admin_notifications
@@ -274,9 +274,11 @@ func AcknowledgeNotification(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(map[string]string{ if err := json.NewEncoder(w).Encode(map[string]string{
"status": "ok", "status": "ok",
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
func AcknowledgePendingBookingNotification(tx any, ctx context.Context, bookingID string) error { func AcknowledgePendingBookingNotification(tx any, ctx context.Context, bookingID string) error {
+70 -44
View File
@@ -332,7 +332,9 @@ func GetGiftCards(w http.ResponseWriter, r *http.Request) {
} }
resp.TotalPages = totalPages resp.TotalPages = totalPages
_ = json.NewEncoder(w).Encode(resp) if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
func CreateGiftCard(w http.ResponseWriter, r *http.Request) { func CreateGiftCard(w http.ResponseWriter, r *http.Request) {
@@ -361,10 +363,10 @@ func CreateGiftCard(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
var gc GiftCard var gc GiftCard
var lastUsedAt sql.NullTime var lastUsedAt sql.NullTime
@@ -426,7 +428,9 @@ func CreateGiftCard(w http.ResponseWriter, r *http.Request) {
} }
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(gc) if err := json.NewEncoder(w).Encode(gc); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
func TopUpGiftCard(w http.ResponseWriter, r *http.Request) { func TopUpGiftCard(w http.ResponseWriter, r *http.Request) {
@@ -468,10 +472,10 @@ func TopUpGiftCard(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
var redeemedBy sql.NullString var redeemedBy sql.NullString
var isInventory bool var isInventory bool
@@ -542,7 +546,9 @@ func TopUpGiftCard(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(gc) if err := json.NewEncoder(w).Encode(gc); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
func TransferGiftCard(w http.ResponseWriter, r *http.Request) { func TransferGiftCard(w http.ResponseWriter, r *http.Request) {
@@ -582,10 +588,10 @@ func TransferGiftCard(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
var fromRedeemedBy, toRedeemedBy sql.NullString var fromRedeemedBy, toRedeemedBy sql.NullString
var fromRemaining, toRemaining float64 var fromRemaining, toRemaining float64
@@ -654,7 +660,9 @@ func TransferGiftCard(w http.ResponseWriter, r *http.Request) {
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]string{"status": "success"}) if err := json.NewEncoder(w).Encode(map[string]string{"status": "success"}); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// --- User Handlers --- // --- User Handlers ---
@@ -686,10 +694,10 @@ func RedeemGiftCard(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
var amountRemaining float64 var amountRemaining float64
var redeemedBy sql.NullString var redeemedBy sql.NullString
@@ -762,10 +770,12 @@ func RedeemGiftCard(w http.ResponseWriter, r *http.Request) {
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]any{ if err := json.NewEncoder(w).Encode(map[string]any{
"status": "success", "status": "success",
"amount_redeemed": amountRemaining, "amount_redeemed": amountRemaining,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
func GetGiftCardBalance(w http.ResponseWriter, r *http.Request) { func GetGiftCardBalance(w http.ResponseWriter, r *http.Request) {
@@ -780,7 +790,9 @@ func GetGiftCardBalance(w http.ResponseWriter, r *http.Request) {
err := db.Conn.QueryRow(ctx, "SELECT balance FROM user_giftcard_balances WHERE user_id = $1", userID).Scan(&balance) err := db.Conn.QueryRow(ctx, "SELECT balance FROM user_giftcard_balances WHERE user_id = $1", userID).Scan(&balance)
if err != nil { if err != nil {
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
_ = json.NewEncoder(w).Encode(map[string]float64{"balance": 0.00}) if err := json.NewEncoder(w).Encode(map[string]float64{"balance": 0.00}); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
return return
} }
log.Printf("Failed to query user balance: %v", err) log.Printf("Failed to query user balance: %v", err)
@@ -788,7 +800,9 @@ func GetGiftCardBalance(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(map[string]float64{"balance": balance}) if err := json.NewEncoder(w).Encode(map[string]float64{"balance": balance}); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// GetUserGiftCardBalanceAdmin Handler returns any user's balance for the admin. // GetUserGiftCardBalanceAdmin Handler returns any user's balance for the admin.
@@ -806,7 +820,9 @@ func GetUserGiftCardBalanceAdmin(w http.ResponseWriter, r *http.Request) {
err := db.Conn.QueryRow(ctx, "SELECT balance FROM user_giftcard_balances WHERE user_id = $1", userID).Scan(&balance) err := db.Conn.QueryRow(ctx, "SELECT balance FROM user_giftcard_balances WHERE user_id = $1", userID).Scan(&balance)
if err != nil { if err != nil {
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
_ = json.NewEncoder(w).Encode(map[string]float64{"balance": 0.00}) if err := json.NewEncoder(w).Encode(map[string]float64{"balance": 0.00}); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
return return
} }
log.Printf("Failed to query user balance: %v", err) log.Printf("Failed to query user balance: %v", err)
@@ -821,10 +837,10 @@ func GetUserGiftCardBalanceAdmin(w http.ResponseWriter, r *http.Request) {
log.Printf("Failed to begin transaction: %v", err) log.Printf("Failed to begin transaction: %v", err)
} else { } else {
defer func() { defer func() {
if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
if _, err := tx.Exec(ctx, ` if _, err := tx.Exec(ctx, `
INSERT INTO admin_audit_log (admin_id, action_type, target_user_id, details) INSERT INTO admin_audit_log (admin_id, action_type, target_user_id, details)
@@ -838,7 +854,9 @@ func GetUserGiftCardBalanceAdmin(w http.ResponseWriter, r *http.Request) {
} }
} }
_ = json.NewEncoder(w).Encode(map[string]float64{"balance": balance}) if err := json.NewEncoder(w).Encode(map[string]float64{"balance": balance}); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
func BuyGiftCard(w http.ResponseWriter, r *http.Request) { func BuyGiftCard(w http.ResponseWriter, r *http.Request) {
@@ -880,7 +898,9 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) {
log.Printf("Failed to check idempotency: %v", err) log.Printf("Failed to check idempotency: %v", err)
} }
if existing != nil { if existing != nil {
_ = json.NewEncoder(w).Encode(existing) if err := json.NewEncoder(w).Encode(existing); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
return return
} }
} }
@@ -933,10 +953,10 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
var buyPaymentID string var buyPaymentID string
fees := paymentService.CalculateFees(req.Amount, "online") fees := paymentService.CalculateFees(req.Amount, "online")
@@ -1107,11 +1127,13 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) {
} }
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(map[string]any{ if err := json.NewEncoder(w).Encode(map[string]any{
"status": "success", "status": "success",
"code": cardID, "code": cardID,
"amount": amountPounds, "amount": amountPounds,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// --- Helpers --- // --- Helpers ---
@@ -1179,10 +1201,12 @@ func GetExpiredBalances(w http.ResponseWriter, r *http.Request) {
balances = []ExpiredBalance{} balances = []ExpiredBalance{}
} }
_ = json.NewEncoder(w).Encode(map[string]any{ if err := json.NewEncoder(w).Encode(map[string]any{
"expired_balances": balances, "expired_balances": balances,
"total": len(balances), "total": len(balances),
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
type ClaimExpiredBalanceRequest struct { type ClaimExpiredBalanceRequest struct {
@@ -1216,10 +1240,10 @@ func ClaimExpiredBalance(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
var existingClaimedAt sql.NullTime var existingClaimedAt sql.NullTime
err = tx.QueryRow(ctx, ` err = tx.QueryRow(ctx, `
@@ -1258,5 +1282,7 @@ func ClaimExpiredBalance(w http.ResponseWriter, r *http.Request) {
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]string{"status": "claimed"}) if err := json.NewEncoder(w).Encode(map[string]string{"status": "claimed"}); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
+80 -44
View File
@@ -130,7 +130,9 @@ func GetDiscountPreviewHandler(w http.ResponseWriter, r *http.Request) {
preview := calculateDiscountPreview(r.Context(), bookingID, userID) preview := calculateDiscountPreview(r.Context(), bookingID, userID)
_ = json.NewEncoder(w).Encode(preview) if err := json.NewEncoder(w).Encode(preview); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// calculateDiscountPreview runs the same queries as applyEligibleCampaignsAtPayment // calculateDiscountPreview runs the same queries as applyEligibleCampaignsAtPayment
@@ -242,10 +244,10 @@ func calculateDiscountPreview(ctx context.Context, bookingID string, userID stri
annRows.Close() annRows.Close()
for _, c := range campaigns { for _, c := range campaigns {
var exists int var exists int
if err := db.Conn.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, c.id).Scan(&exists); err != nil { if err := db.Conn.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, c.id).Scan(&exists); err != nil {
log.Printf("Failed to scan anniversary discount existence: %v", err) log.Printf("Failed to scan anniversary discount existence: %v", err)
} }
if exists > 0 { if exists > 0 {
continue continue
} }
@@ -389,10 +391,12 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) {
if err := tx.QueryRow(r.Context(), ` if err := tx.QueryRow(r.Context(), `
SELECT id, status FROM payments WHERE booking_id = $1 AND idempotency_key = $2 SELECT id, status FROM payments WHERE booking_id = $1 AND idempotency_key = $2
`, bookingID, idempotencyKey).Scan(&existingID, &existingStatus); err == nil { `, bookingID, idempotencyKey).Scan(&existingID, &existingStatus); err == nil {
_ = json.NewEncoder(w).Encode(CheckoutResponse{ if err := json.NewEncoder(w).Encode(CheckoutResponse{
CheckoutID: existingID, CheckoutID: existingID,
Status: existingStatus, Status: existingStatus,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
return return
} else if !errors.Is(err, pgx.ErrNoRows) { } else if !errors.Is(err, pgx.ErrNoRows) {
log.Printf("Failed to check idempotency: %v", err) log.Printf("Failed to check idempotency: %v", err)
@@ -531,10 +535,12 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(CheckoutResponse{ if err := json.NewEncoder(w).Encode(CheckoutResponse{
CheckoutID: paymentID, CheckoutID: paymentID,
Status: "COMPLETED", Status: "COMPLETED",
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
return return
} }
@@ -561,10 +567,12 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) {
log.Printf("Failed to check idempotency: %v", err) log.Printf("Failed to check idempotency: %v", err)
} }
if existingPayment != nil { if existingPayment != nil {
_ = json.NewEncoder(w).Encode(CheckoutResponse{ if err := json.NewEncoder(w).Encode(CheckoutResponse{
CheckoutID: existingPayment.ID, CheckoutID: existingPayment.ID,
Status: existingPayment.Status, Status: existingPayment.Status,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
return return
} }
@@ -583,10 +591,12 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(CheckoutResponse{ if err := json.NewEncoder(w).Encode(CheckoutResponse{
CheckoutID: checkout.ID, CheckoutID: checkout.ID,
Status: checkout.Status, Status: checkout.Status,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
func GetCheckoutStatus(w http.ResponseWriter, r *http.Request) { func GetCheckoutStatus(w http.ResponseWriter, r *http.Request) {
@@ -613,7 +623,9 @@ func GetCheckoutStatus(w http.ResponseWriter, r *http.Request) {
paymentResult, err := SquareClient.GetCheckout(r.Context(), checkoutID) paymentResult, err := SquareClient.GetCheckout(r.Context(), checkoutID)
if err != nil { if err != nil {
if err.Error() == "checkout pending" { if err.Error() == "checkout pending" {
_ = json.NewEncoder(w).Encode(PaymentStatusResponse{Status: "PENDING"}) if err := json.NewEncoder(w).Encode(PaymentStatusResponse{Status: "PENDING"}); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
return return
} }
log.Printf("Failed to get checkout status: %v", err) log.Printf("Failed to get checkout status: %v", err)
@@ -647,14 +659,16 @@ func GetCheckoutStatus(w http.ResponseWriter, r *http.Request) {
WHERE booking_id = $1 AND idempotency_key = $2 WHERE booking_id = $1 AND idempotency_key = $2
`, bookingID, idempotencyKey).Scan(&existingID, &existingSquarePayID); err == nil { `, bookingID, idempotencyKey).Scan(&existingID, &existingSquarePayID); err == nil {
if existingSquarePayID.Valid && existingSquarePayID.String == paymentResult.SquarePayID { if existingSquarePayID.Valid && existingSquarePayID.String == paymentResult.SquarePayID {
_ = json.NewEncoder(w).Encode(PaymentStatusResponse{ if err := json.NewEncoder(w).Encode(PaymentStatusResponse{
Status: "COMPLETED", Status: "COMPLETED",
PaymentID: existingID, PaymentID: existingID,
Amount: paymentResult.Amount, Amount: paymentResult.Amount,
CardBrand: paymentResult.CardBrand, CardBrand: paymentResult.CardBrand,
CardLast4: paymentResult.CardLast4, CardLast4: paymentResult.CardLast4,
ReceiptURL: paymentResult.ReceiptURL, ReceiptURL: paymentResult.ReceiptURL,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
return return
} }
} else if !errors.Is(err, pgx.ErrNoRows) { } else if !errors.Is(err, pgx.ErrNoRows) {
@@ -688,14 +702,16 @@ func GetCheckoutStatus(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(PaymentStatusResponse{ if err := json.NewEncoder(w).Encode(PaymentStatusResponse{
Status: "COMPLETED", Status: "COMPLETED",
PaymentID: paymentID, PaymentID: paymentID,
Amount: paymentResult.Amount, Amount: paymentResult.Amount,
CardBrand: paymentResult.CardBrand, CardBrand: paymentResult.CardBrand,
CardLast4: paymentResult.CardLast4, CardLast4: paymentResult.CardLast4,
ReceiptURL: paymentResult.ReceiptURL, ReceiptURL: paymentResult.ReceiptURL,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
return return
} }
@@ -773,7 +789,7 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) {
} }
if err := ValidatePartialAmount(req.Amount, remainingCents); err != nil { if err := ValidatePartialAmount(req.Amount, remainingCents); err != nil {
log.Printf("Failed to process request: %v", err) log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest) http.Error(w, "Invalid request", http.StatusBadRequest)
return return
} }
} }
@@ -876,14 +892,16 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) {
FROM payments FROM payments
WHERE booking_id = $1 AND idempotency_key = $2 WHERE booking_id = $1 AND idempotency_key = $2
`, bookingID, req.IdempotencyKey).Scan(&existingID, &existingBookingID, &existingPaymentType, &existingStatus, &existingAmount, &existingCreatedAt); err == nil { `, bookingID, req.IdempotencyKey).Scan(&existingID, &existingBookingID, &existingPaymentType, &existingStatus, &existingAmount, &existingCreatedAt); err == nil {
_ = json.NewEncoder(w).Encode(PaymentResponse{ if err := json.NewEncoder(w).Encode(PaymentResponse{
ID: existingID.String, ID: existingID.String,
BookingID: existingBookingID.String, BookingID: existingBookingID.String,
PaymentType: existingPaymentType.String, PaymentType: existingPaymentType.String,
Status: existingStatus.String, Status: existingStatus.String,
Amount: int64(existingAmount.Float64 * 100), Amount: int64(existingAmount.Float64 * 100),
CreatedAt: existingCreatedAt.Time.Format(time.RFC3339), CreatedAt: existingCreatedAt.Time.Format(time.RFC3339),
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
return return
} else if !errors.Is(err, pgx.ErrNoRows) { } else if !errors.Is(err, pgx.ErrNoRows) {
log.Printf("Failed to check idempotency: %v", err) log.Printf("Failed to check idempotency: %v", err)
@@ -1073,7 +1091,7 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(PaymentResponse{ if err := json.NewEncoder(w).Encode(PaymentResponse{
ID: primaryPaymentID, ID: primaryPaymentID,
BookingID: bookingID, BookingID: bookingID,
PaymentType: req.PaymentType, PaymentType: req.PaymentType,
@@ -1083,7 +1101,9 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) {
CardLast4: paymentResult.CardLast4, CardLast4: paymentResult.CardLast4,
ReceiptURL: paymentResult.ReceiptURL, ReceiptURL: paymentResult.ReceiptURL,
CreatedAt: clock.Now().Format(time.RFC3339), CreatedAt: clock.Now().Format(time.RFC3339),
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// applyEligibleCampaignsAtPayment checks and applies any eligible discount // applyEligibleCampaignsAtPayment checks and applies any eligible discount
@@ -1226,10 +1246,10 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingI
annRows.Close() annRows.Close()
for _, c := range campaigns { for _, c := range campaigns {
var exists int var exists int
if err := q.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, c.id).Scan(&exists); err != nil { if err := q.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, c.id).Scan(&exists); err != nil {
log.Printf("Failed to scan anniversary discount existence: %v", err) log.Printf("Failed to scan anniversary discount existence: %v", err)
} }
if exists > 0 { if exists > 0 {
continue continue
} }
@@ -1311,13 +1331,13 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingI
INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_by) INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_by)
VALUES ($1, 'partial', 'discount', $2, 'completed', $3) VALUES ($1, 'partial', 'discount', $2, 'completed', $3)
`, bookingID, discountAmount, userID); err != nil { `, bookingID, discountAmount, userID); err != nil {
log.Printf("ALERT: failed to insert discount payment record for campaign %s, booking %s: %v", globalCampaignID, bookingID, err) log.Printf("ALERT: failed to insert discount payment record for campaign %s, booking %s: %v", globalCampaignID, bookingID, err)
} }
if _, err := q.Exec(ctx, ` if _, err := q.Exec(ctx, `
UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = $1 UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = $1
`, globalCampaignID); err != nil { `, globalCampaignID); err != nil {
log.Printf("ALERT: failed to increment times_redeemed for campaign %s, booking %s: %v", globalCampaignID, bookingID, err) log.Printf("ALERT: failed to increment times_redeemed for campaign %s, booking %s: %v", globalCampaignID, bookingID, err)
} }
} }
} }
} }
@@ -1472,7 +1492,9 @@ func GetUserPaymentMethods(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(cards) if err := json.NewEncoder(w).Encode(cards); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
func AdminGetUserPaymentMethods(w http.ResponseWriter, r *http.Request) { func AdminGetUserPaymentMethods(w http.ResponseWriter, r *http.Request) {
@@ -1490,7 +1512,9 @@ func AdminGetUserPaymentMethods(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(cards) if err := json.NewEncoder(w).Encode(cards); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
func DeletePaymentMethod(w http.ResponseWriter, r *http.Request) { func DeletePaymentMethod(w http.ResponseWriter, r *http.Request) {
@@ -1514,7 +1538,9 @@ func DeletePaymentMethod(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(map[string]string{"status": "deleted"}) if err := json.NewEncoder(w).Encode(map[string]string{"status": "deleted"}); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
type CreatePaymentMethodRequest struct { type CreatePaymentMethodRequest struct {
@@ -1555,7 +1581,7 @@ func CreatePaymentMethod(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
if strings.Contains(err.Error(), "invalid") || strings.Contains(err.Error(), "expired") { if strings.Contains(err.Error(), "invalid") || strings.Contains(err.Error(), "expired") {
log.Printf("Failed to process request: %v", err) log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest) http.Error(w, "Invalid request", http.StatusBadRequest)
return return
} }
log.Printf("Failed to create payment method: %v", err) log.Printf("Failed to create payment method: %v", err)
@@ -1563,7 +1589,9 @@ func CreatePaymentMethod(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(card) if err := json.NewEncoder(w).Encode(card); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
func RefundPayment(w http.ResponseWriter, r *http.Request) { func RefundPayment(w http.ResponseWriter, r *http.Request) {
@@ -1691,14 +1719,16 @@ func RefundPayment(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(RefundResponse{ if err := json.NewEncoder(w).Encode(RefundResponse{
ID: refundID, ID: refundID,
PaymentID: paymentID, PaymentID: paymentID,
Amount: req.Amount, Amount: req.Amount,
Status: "completed", Status: "completed",
Reason: req.Reason, Reason: req.Reason,
CreatedAt: clock.Now().Format(time.RFC3339), CreatedAt: clock.Now().Format(time.RFC3339),
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
func CreateTipPayment(w http.ResponseWriter, r *http.Request) { func CreateTipPayment(w http.ResponseWriter, r *http.Request) {
@@ -1846,7 +1876,7 @@ func CreateTipPayment(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(PaymentResponse{ if err := json.NewEncoder(w).Encode(PaymentResponse{
ID: paymentID, ID: paymentID,
BookingID: bookingID, BookingID: bookingID,
PaymentType: "tip", PaymentType: "tip",
@@ -1856,7 +1886,9 @@ func CreateTipPayment(w http.ResponseWriter, r *http.Request) {
CardLast4: paymentResult.CardLast4, CardLast4: paymentResult.CardLast4,
ReceiptURL: paymentResult.ReceiptURL, ReceiptURL: paymentResult.ReceiptURL,
CreatedAt: clock.Now().Format(time.RFC3339), CreatedAt: clock.Now().Format(time.RFC3339),
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
func GetBookingPaymentSummary(w http.ResponseWriter, r *http.Request) { func GetBookingPaymentSummary(w http.ResponseWriter, r *http.Request) {
@@ -1921,7 +1953,7 @@ func GetBookingPaymentSummary(w http.ResponseWriter, r *http.Request) {
} }
} }
_ = json.NewEncoder(w).Encode(PaymentSummaryResponse{ if err := json.NewEncoder(w).Encode(PaymentSummaryResponse{
TotalAmount: int64(summary.TotalAmount * 100), TotalAmount: int64(summary.TotalAmount * 100),
PaidAmount: int64(summary.PaidAmount * 100), PaidAmount: int64(summary.PaidAmount * 100),
RefundedAmount: int64(summary.RefundedAmount * 100), RefundedAmount: int64(summary.RefundedAmount * 100),
@@ -1930,7 +1962,9 @@ func GetBookingPaymentSummary(w http.ResponseWriter, r *http.Request) {
TotalNetAmount: int64(summary.TotalNetAmount * 100), TotalNetAmount: int64(summary.TotalNetAmount * 100),
Payments: payments, Payments: payments,
Refunds: refunds, Refunds: refunds,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// PaymentLockDuration is the TTL for a payment-in-flight lock in minutes. // PaymentLockDuration is the TTL for a payment-in-flight lock in minutes.
@@ -2025,11 +2059,13 @@ func AcquirePaymentLock(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(map[string]any{ if err := json.NewEncoder(w).Encode(map[string]any{
"status": "locked", "status": "locked",
"ttl_min": PaymentLockDuration, "ttl_min": PaymentLockDuration,
"bookingID": bookingID, "bookingID": bookingID,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// ReleasePaymentLock removes the PAYMENT_IN_FLIGHT time_blocker for a booking. // ReleasePaymentLock removes the PAYMENT_IN_FLIGHT time_blocker for a booking.
+8 -6
View File
@@ -98,10 +98,10 @@ func ApplyLoyaltyRedemption(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
var bookingTotal float64 var bookingTotal float64
if err := tx.QueryRow(r.Context(), ` if err := tx.QueryRow(r.Context(), `
@@ -155,8 +155,10 @@ func ApplyLoyaltyRedemption(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(map[string]any{ if err := json.NewEncoder(w).Encode(map[string]any{
"success": true, "success": true,
"discount_amount": discountAmount, "discount_amount": discountAmount,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
+35 -23
View File
@@ -98,13 +98,15 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) {
err := db.Conn.QueryRow(ctx, `SELECT id FROM till_sales WHERE idempotency_key = $1`, req.IdempotencyKey).Scan(&existingID) err := db.Conn.QueryRow(ctx, `SELECT id FROM till_sales WHERE idempotency_key = $1`, req.IdempotencyKey).Scan(&existingID)
if err == nil { if err == nil {
// Existing sale found — return it (idempotent) // Existing sale found — return it (idempotent)
_ = json.NewEncoder(w).Encode(TillSaleResponse{ if err := json.NewEncoder(w).Encode(TillSaleResponse{
ID: existingID, ID: existingID,
ItemType: req.ItemType, ItemType: req.ItemType,
TotalAmount: req.Amount, TotalAmount: req.Amount,
PaymentMethod: req.PaymentMethod, PaymentMethod: req.PaymentMethod,
Status: "completed", Status: "completed",
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
return return
} }
} }
@@ -118,10 +120,10 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
var giftCardID string var giftCardID string
if req.Action == "create" { if req.Action == "create" {
@@ -447,7 +449,7 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) {
} }
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(TillSaleResponse{ if err := json.NewEncoder(w).Encode(TillSaleResponse{
ID: tillSaleID, ID: tillSaleID,
ItemType: req.ItemType, ItemType: req.ItemType,
ItemID: &giftCardID, ItemID: &giftCardID,
@@ -455,7 +457,9 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) {
PaymentMethod: req.PaymentMethod, PaymentMethod: req.PaymentMethod,
Status: saleStatus, Status: saleStatus,
CheckoutID: squareCheckoutID, CheckoutID: squareCheckoutID,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
func GetTillCheckoutStatus(w http.ResponseWriter, r *http.Request) { func GetTillCheckoutStatus(w http.ResponseWriter, r *http.Request) {
@@ -482,16 +486,20 @@ func GetTillCheckoutStatus(w http.ResponseWriter, r *http.Request) {
} }
if currentStatus == "completed" { if currentStatus == "completed" {
_ = json.NewEncoder(w).Encode(PaymentStatusResponse{ if err := json.NewEncoder(w).Encode(PaymentStatusResponse{
Status: "COMPLETED", Status: "COMPLETED",
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
return return
} }
paymentResult, err := SquareClient.GetCheckout(r.Context(), checkoutID) paymentResult, err := SquareClient.GetCheckout(r.Context(), checkoutID)
if err != nil { if err != nil {
if err.Error() == "checkout pending" { if err.Error() == "checkout pending" {
_ = json.NewEncoder(w).Encode(PaymentStatusResponse{Status: "PENDING"}) if err := json.NewEncoder(w).Encode(PaymentStatusResponse{Status: "PENDING"}); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
return return
} }
log.Printf("Failed to get checkout status: %v", err) log.Printf("Failed to get checkout status: %v", err)
@@ -507,10 +515,10 @@ func GetTillCheckoutStatus(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
_, err = tx.Exec(r.Context(), ` _, err = tx.Exec(r.Context(), `
UPDATE till_sales UPDATE till_sales
@@ -532,16 +540,20 @@ func GetTillCheckoutStatus(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(PaymentStatusResponse{ if err := json.NewEncoder(w).Encode(PaymentStatusResponse{
Status: "COMPLETED", Status: "COMPLETED",
PaymentID: tillSaleID, PaymentID: tillSaleID,
Amount: paymentResult.Amount, Amount: paymentResult.Amount,
CardBrand: paymentResult.CardBrand, CardBrand: paymentResult.CardBrand,
CardLast4: paymentResult.CardLast4, CardLast4: paymentResult.CardLast4,
ReceiptURL: paymentResult.ReceiptURL, ReceiptURL: paymentResult.ReceiptURL,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
return return
} }
_ = json.NewEncoder(w).Encode(PaymentStatusResponse{Status: "PENDING"}) if err := json.NewEncoder(w).Encode(PaymentStatusResponse{Status: "PENDING"}); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
+36 -24
View File
@@ -23,8 +23,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/kovidgoyal/imaging"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/kovidgoyal/imaging"
) )
const MaxInputLength = 256 const MaxInputLength = 256
@@ -136,14 +136,14 @@ func ListImages(w http.ResponseWriter, r *http.Request) {
if tagFilter != "" { if tagFilter != "" {
if err := validateInputLength(tagFilter); err != nil { if err := validateInputLength(tagFilter); err != nil {
log.Printf("Failed to process request: %v", err) log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest) http.Error(w, "Invalid request", http.StatusBadRequest)
return return
} }
} }
if tagsFilter != "" { if tagsFilter != "" {
if err := validateInputLength(tagsFilter); err != nil { if err := validateInputLength(tagsFilter); err != nil {
log.Printf("Failed to process request: %v", err) log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest) http.Error(w, "Invalid request", http.StatusBadRequest)
return return
} }
} }
@@ -187,7 +187,7 @@ func ListImages(w http.ResponseWriter, r *http.Request) {
// Validate input length // Validate input length
if err := validateInputLength(category + ":" + value); err != nil { if err := validateInputLength(category + ":" + value); err != nil {
log.Printf("Failed to process request: %v", err) log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest) http.Error(w, "Invalid request", http.StatusBadRequest)
return return
} }
@@ -381,10 +381,12 @@ func ListImages(w http.ResponseWriter, r *http.Request) {
nextCursor = &cursor nextCursor = &cursor
} }
_ = json.NewEncoder(w).Encode(ImageListResponse{ if err := json.NewEncoder(w).Encode(ImageListResponse{
Images: images, Images: images,
NextCursor: nextCursor, NextCursor: nextCursor,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
func ListTags(w http.ResponseWriter, r *http.Request) { func ListTags(w http.ResponseWriter, r *http.Request) {
@@ -450,7 +452,9 @@ func ListTags(w http.ResponseWriter, r *http.Request) {
tags = []Tag{} tags = []Tag{}
} }
_ = json.NewEncoder(w).Encode(tags) if err := json.NewEncoder(w).Encode(tags); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
type FilterCategory struct { type FilterCategory struct {
@@ -622,7 +626,9 @@ func ListFilters(w http.ResponseWriter, r *http.Request) {
return sumI > sumJ return sumI > sumJ
}) })
_ = json.NewEncoder(w).Encode(filters) if err := json.NewEncoder(w).Encode(filters); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
return return
} }
@@ -676,7 +682,9 @@ func ListFilters(w http.ResponseWriter, r *http.Request) {
} }
filters = uniqueFilters filters = uniqueFilters
_ = json.NewEncoder(w).Encode(filters) if err := json.NewEncoder(w).Encode(filters); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
func UploadImage(w http.ResponseWriter, r *http.Request) { func UploadImage(w http.ResponseWriter, r *http.Request) {
@@ -895,10 +903,10 @@ func UploadImage(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
var imgID string var imgID string
err = tx.QueryRow(r.Context(), ` err = tx.QueryRow(r.Context(), `
@@ -932,7 +940,7 @@ func UploadImage(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(Image{ if err := json.NewEncoder(w).Encode(Image{
ID: imgID, ID: imgID,
URL: fullURLs.Avif, URL: fullURLs.Avif,
ThumbnailURL: thumbURLs.Webp, ThumbnailURL: thumbURLs.Webp,
@@ -940,7 +948,9 @@ func UploadImage(w http.ResponseWriter, r *http.Request) {
Thumb: thumbURLs, Thumb: thumbURLs,
TagNames: tags, TagNames: tags,
CreatedAt: clock.Now(), CreatedAt: clock.Now(),
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
func DeleteImage(w http.ResponseWriter, r *http.Request) { func DeleteImage(w http.ResponseWriter, r *http.Request) {
@@ -1016,8 +1026,8 @@ func DeleteImage(w http.ResponseWriter, r *http.Request) {
} }
key := extractKey(u) key := extractKey(u)
if err := s3.Client.Delete(context.Background(), "crussell", key); err != nil { if err := s3.Client.Delete(context.Background(), "crussell", key); err != nil {
slog.Warn("failed to delete S3 object", "err", err) slog.Warn("failed to delete S3 object", "err", err)
} }
} }
} }
@@ -1028,10 +1038,10 @@ func DeleteImage(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
_, err = tx.Exec(r.Context(), `DELETE FROM images WHERE id = $1`, imageID) _, err = tx.Exec(r.Context(), `DELETE FROM images WHERE id = $1`, imageID)
if err != nil { if err != nil {
@@ -1139,5 +1149,7 @@ func GetImage(w http.ResponseWriter, r *http.Request) {
img.Thumb.Jpg = thumbJpg.String img.Thumb.Jpg = thumbJpg.String
} }
_ = json.NewEncoder(w).Encode(img) if err := json.NewEncoder(w).Encode(img); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
+14 -8
View File
@@ -58,7 +58,9 @@ func GetDefaultHours(w http.ResponseWriter, r *http.Request) {
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(hours) if err := json.NewEncoder(w).Encode(hours); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
func UpdateDefaultHours(w http.ResponseWriter, r *http.Request) { func UpdateDefaultHours(w http.ResponseWriter, r *http.Request) {
@@ -71,7 +73,7 @@ func UpdateDefaultHours(w http.ResponseWriter, r *http.Request) {
for _, h := range hours { for _, h := range hours {
if err := validators.Validate.Struct(&h); err != nil { if err := validators.Validate.Struct(&h); err != nil {
log.Printf("Failed to process request: %v", err) log.Printf("Failed to process request: %v", err)
http.Error(w, "Invalid request", http.StatusBadRequest) http.Error(w, "Invalid request", http.StatusBadRequest)
return return
} }
} }
@@ -96,10 +98,10 @@ func UpdateDefaultHours(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
weekdays := make([]int, len(hours)) weekdays := make([]int, len(hours))
startTimes := make([]string, len(hours)) startTimes := make([]string, len(hours))
@@ -294,7 +296,9 @@ func GetWorkingHours(w http.ResponseWriter, r *http.Request) {
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(results) if err := json.NewEncoder(w).Encode(results); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// isValidTime15Min checks that a time string (HH:MM or HH:MM:SS) has minutes in {00, 15, 30, 45}. // isValidTime15Min checks that a time string (HH:MM or HH:MM:SS) has minutes in {00, 15, 30, 45}.
@@ -610,7 +614,9 @@ func GetAvailableHours(w http.ResponseWriter, r *http.Request) {
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(results) if err := json.NewEncoder(w).Encode(results); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// normalizeTime strips seconds from HH:MM:SS to HH:MM for consistent string // normalizeTime strips seconds from HH:MM:SS to HH:MM for consistent string
@@ -119,8 +119,9 @@ func ListExceptionalGroups(w http.ResponseWriter, r *http.Request) {
} }
} }
if err := json.NewEncoder(w).Encode(groups); err != nil {
_ = json.NewEncoder(w).Encode(groups) log.Printf("Failed to encode JSON response: %v", err)
}
} }
// --- Create Group with Hours and Applications (bulk) --- // --- Create Group with Hours and Applications (bulk) ---
@@ -187,10 +188,10 @@ func CreateExceptionalGroup(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
// Create group // Create group
err = tx.QueryRow(r.Context(), ` err = tx.QueryRow(r.Context(), `
@@ -236,9 +237,10 @@ func CreateExceptionalGroup(w http.ResponseWriter, r *http.Request) {
return return
} }
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(g) if err := json.NewEncoder(w).Encode(g); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// --- Delete Group (cascades to hours and applications) --- // --- Delete Group (cascades to hours and applications) ---
@@ -264,10 +266,10 @@ func DeleteExceptionalGroup(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
result, err := tx.Exec(r.Context(), ` result, err := tx.Exec(r.Context(), `
DELETE FROM exceptional_working_hours_groups WHERE id=$1 DELETE FROM exceptional_working_hours_groups WHERE id=$1
@@ -333,10 +335,10 @@ func UpdateExceptionalApplications(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
// Delete existing applications for this group // Delete existing applications for this group
_, err = tx.Exec(r.Context(), ` _, err = tx.Exec(r.Context(), `
+63 -59
View File
@@ -9,8 +9,8 @@ import (
"net/http" "net/http"
"time" "time"
"crussell/db"
"crussell/clock" "crussell/clock"
"crussell/db"
"crussell/internal/validators" "crussell/internal/validators"
"crussell/mw" "crussell/mw"
@@ -113,7 +113,9 @@ func ListTimeBlockers(w http.ResponseWriter, r *http.Request) {
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(blockers) if err := json.NewEncoder(w).Encode(blockers); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// --- Create Time Blocker --- // --- Create Time Blocker ---
@@ -153,10 +155,10 @@ func CreateTimeBlocker(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
// Insert the time blocker // Insert the time blocker
var blocker TimeBlocker var blocker TimeBlocker
@@ -180,7 +182,9 @@ func CreateTimeBlocker(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(blocker) if err := json.NewEncoder(w).Encode(blocker); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// --- Delete Time Blocker --- // --- Delete Time Blocker ---
@@ -198,10 +202,10 @@ func DeleteTimeBlocker(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
result, err := tx.Exec(r.Context(), ` result, err := tx.Exec(r.Context(), `
DELETE FROM time_blockers WHERE id = $1 DELETE FROM time_blockers WHERE id = $1
@@ -361,14 +365,14 @@ func CheckTimeBlockerOverlap(ctx context.Context, startTime, endTime time.Time,
} }
// CleanupOldReservations deletes expired reservations: // CleanupOldReservations deletes expired reservations:
// - Logged-in (RESERVATION:user): older than 1 hour // - Logged-in (RESERVATION:user): older than 1 hour
// - Anonymous (RESERVATION:anon): older than 10 minutes // - Anonymous (RESERVATION:anon): older than 10 minutes
// - Admin walk-in (RESERVATION:admin:walkin:%): older than 15 minutes // - Admin walk-in (RESERVATION:admin:walkin:%): older than 15 minutes
// - Admin call-in (RESERVATION:admin:callin:%): older than 15 minutes // - Admin call-in (RESERVATION:admin:callin:%): older than 15 minutes
// - Edit request (RESERVATION:edit_request:%): older than 24 hours // - Edit request (RESERVATION:edit_request:%): older than 24 hours
// - Payment in-flight (PAYMENT_IN_FLIGHT:%): TTL via duration_minutes column // - Payment in-flight (PAYMENT_IN_FLIGHT:%): TTL via duration_minutes column
// (AcquirePaymentLock sets duration_minutes = PaymentLockDuration = 5min and // (AcquirePaymentLock sets duration_minutes = PaymentLockDuration = 5min and
// start_time = NOW(), so the condition evaluates to "cleanup after 5 minutes". // start_time = NOW(), so the condition evaluates to "cleanup after 5 minutes".
func CleanupOldReservations(ctx context.Context) (int, error) { func CleanupOldReservations(ctx context.Context) (int, error) {
oneHourAgo := clock.Now().Add(-1 * time.Hour) oneHourAgo := clock.Now().Add(-1 * time.Hour)
tenMinutesAgo := clock.Now().Add(-10 * time.Minute) tenMinutesAgo := clock.Now().Add(-10 * time.Minute)
@@ -380,10 +384,10 @@ func CleanupOldReservations(ctx context.Context) (int, error) {
return 0, fmt.Errorf("failed to begin transaction: %w", err) return 0, fmt.Errorf("failed to begin transaction: %w", err)
} }
defer func() { defer func() {
if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
tag, err := tx.Exec(ctx, ` tag, err := tx.Exec(ctx, `
DELETE FROM time_blockers DELETE FROM time_blockers
@@ -411,10 +415,10 @@ func AnonymizeStaleGuestAccounts(ctx context.Context) (int, error) {
return 0, fmt.Errorf("failed to begin transaction: %w", err) return 0, fmt.Errorf("failed to begin transaction: %w", err)
} }
defer func() { defer func() {
if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
var totalRows int var totalRows int
@@ -507,10 +511,10 @@ func CleanupExpiredLoyaltyRedemptions(ctx context.Context) (int, error) {
return 0, fmt.Errorf("failed to begin transaction: %w", err) return 0, fmt.Errorf("failed to begin transaction: %w", err)
} }
defer func() { defer func() {
if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
tag, err := tx.Exec(ctx, ` tag, err := tx.Exec(ctx, `
DELETE FROM loyalty_redemptions DELETE FROM loyalty_redemptions
@@ -555,10 +559,10 @@ func CleanupExpiredFinancialRecords(ctx context.Context) (int, error) {
return 0, fmt.Errorf("failed to begin transaction: %w", err) return 0, fmt.Errorf("failed to begin transaction: %w", err)
} }
defer func() { defer func() {
if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
var totalRows int var totalRows int
@@ -584,7 +588,7 @@ func CleanupExpiredFinancialRecords(ctx context.Context) (int, error) {
LEFT JOIN bookings b ON p.booking_id = b.id LEFT JOIN bookings b ON p.booking_id = b.id
LEFT JOIN users u ON b.user_id = u.id LEFT JOIN users u ON b.user_id = u.id
WHERE p.created_at < NOW() - INTERVAL '7 years' WHERE p.created_at < NOW() - INTERVAL '7 years'
` + retentionFilter + ` `+retentionFilter+`
GROUP BY DATE_TRUNC('month', p.created_at)::date GROUP BY DATE_TRUNC('month', p.created_at)::date
ON CONFLICT (month) DO UPDATE SET ON CONFLICT (month) DO UPDATE SET
total_payments = financial_aggregates.total_payments + EXCLUDED.total_payments, total_payments = financial_aggregates.total_payments + EXCLUDED.total_payments,
@@ -617,7 +621,7 @@ func CleanupExpiredFinancialRecords(ctx context.Context) (int, error) {
LEFT JOIN bookings b ON p.booking_id = b.id LEFT JOIN bookings b ON p.booking_id = b.id
LEFT JOIN users u ON b.user_id = u.id LEFT JOIN users u ON b.user_id = u.id
WHERE p.created_at < NOW() - INTERVAL '7 years' WHERE p.created_at < NOW() - INTERVAL '7 years'
` + retentionFilter + ` `+retentionFilter+`
GROUP BY DATE_TRUNC('month', r.created_at)::date GROUP BY DATE_TRUNC('month', r.created_at)::date
ON CONFLICT (month) DO UPDATE SET ON CONFLICT (month) DO UPDATE SET
total_refunds = financial_aggregates.total_refunds + EXCLUDED.total_refunds total_refunds = financial_aggregates.total_refunds + EXCLUDED.total_refunds
@@ -633,7 +637,7 @@ func CleanupExpiredFinancialRecords(ctx context.Context) (int, error) {
LEFT JOIN users u ON b.user_id = u.id LEFT JOIN users u ON b.user_id = u.id
WHERE p.booking_id = b.id WHERE p.booking_id = b.id
AND p.created_at < NOW() - INTERVAL '7 years' AND p.created_at < NOW() - INTERVAL '7 years'
` + retentionFilter + ` `+retentionFilter+`
`) `)
if err != nil { if err != nil {
return 0, fmt.Errorf("failed to delete expired payments: %w", err) return 0, fmt.Errorf("failed to delete expired payments: %w", err)
@@ -647,7 +651,7 @@ func CleanupExpiredFinancialRecords(ctx context.Context) (int, error) {
LEFT JOIN users u ON b.user_id = u.id LEFT JOIN users u ON b.user_id = u.id
WHERE r.payment_id = p.id WHERE r.payment_id = p.id
AND p.created_at < NOW() - INTERVAL '7 years' AND p.created_at < NOW() - INTERVAL '7 years'
` + retentionFilter + ` `+retentionFilter+`
`) `)
if err != nil { if err != nil {
return 0, fmt.Errorf("failed to delete expired refunds: %w", err) return 0, fmt.Errorf("failed to delete expired refunds: %w", err)
@@ -671,10 +675,10 @@ func CleanupExpiredDeposits(ctx context.Context) (int, error) {
return 0, fmt.Errorf("failed to begin transaction: %w", err) return 0, fmt.Errorf("failed to begin transaction: %w", err)
} }
defer func() { defer func() {
if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
// Collect all evicted booking IDs from both updates so we can notify // Collect all evicted booking IDs from both updates so we can notify
// and clean up in one pass instead of re-scanning via updated_at = NOW(). // and clean up in one pass instead of re-scanning via updated_at = NOW().
@@ -803,10 +807,10 @@ func CleanupExpiredGiftCards(ctx context.Context) (int, error) {
return 0, fmt.Errorf("failed to begin transaction: %w", err) return 0, fmt.Errorf("failed to begin transaction: %w", err)
} }
defer func() { defer func() {
if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
rows, err := tx.Query(ctx, ` rows, err := tx.Query(ctx, `
SELECT id, amount_remaining SELECT id, amount_remaining
@@ -905,10 +909,10 @@ func CleanupIdleAccounts(ctx context.Context) (int, error) {
return 0, fmt.Errorf("failed to begin transaction: %w", err) return 0, fmt.Errorf("failed to begin transaction: %w", err)
} }
defer func() { defer func() {
if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
rowsWithBalance, err := tx.Query(ctx, ` rowsWithBalance, err := tx.Query(ctx, `
SELECT u.id, COALESCE(b.balance, 0) as balance SELECT u.id, COALESCE(b.balance, 0) as balance
@@ -1014,10 +1018,10 @@ func CleanupOldIdempotencyKeys(ctx context.Context) (int, error) {
return 0, fmt.Errorf("failed to begin transaction: %w", err) return 0, fmt.Errorf("failed to begin transaction: %w", err)
} }
defer func() { defer func() {
if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
var totalRows int var totalRows int
@@ -1069,10 +1073,10 @@ func CleanupOldNameHistory(ctx context.Context) (int, error) {
return 0, fmt.Errorf("failed to begin transaction: %w", err) return 0, fmt.Errorf("failed to begin transaction: %w", err)
} }
defer func() { defer func() {
if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(ctx); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
tag, err := tx.Exec(ctx, ` tag, err := tx.Exec(ctx, `
DELETE FROM name_history DELETE FROM name_history
+20 -16
View File
@@ -67,10 +67,10 @@ func ToggleService(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
query := "UPDATE services SET is_active = NOT is_active WHERE id = $1" query := "UPDATE services SET is_active = NOT is_active WHERE id = $1"
result, err := tx.Exec(r.Context(), query, serviceID) result, err := tx.Exec(r.Context(), query, serviceID)
@@ -90,10 +90,12 @@ func ToggleService(w http.ResponseWriter, r *http.Request) {
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]any{ if err := json.NewEncoder(w).Encode(map[string]any{
"message": "Service toggled successfully", "message": "Service toggled successfully",
"id": serviceID, "id": serviceID,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// POST /api/admin/services // POST /api/admin/services
@@ -145,10 +147,10 @@ func CreateServiceHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
query := ` query := `
INSERT INTO services ( INSERT INTO services (
@@ -229,10 +231,10 @@ func DeleteServiceHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
query := "UPDATE services SET is_active = FALSE WHERE id = $1" query := "UPDATE services SET is_active = FALSE WHERE id = $1"
result, err := tx.Exec(r.Context(), query, serviceID) result, err := tx.Exec(r.Context(), query, serviceID)
@@ -252,10 +254,12 @@ func DeleteServiceHandler(w http.ResponseWriter, r *http.Request) {
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]any{ if err := json.NewEncoder(w).Encode(map[string]any{
"message": "Service deleted successfully", "message": "Service deleted successfully",
"id": serviceID, "id": serviceID,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// ServicesHandler returns all services from the database // ServicesHandler returns all services from the database
+11 -9
View File
@@ -103,10 +103,10 @@ func GetCurrentAndNextHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
_, err = tx.Exec(r.Context(), ` _, err = tx.Exec(r.Context(), `
UPDATE bookings UPDATE bookings
@@ -715,10 +715,10 @@ func GetTodayAppointmentsHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
_, err = tx.Exec(r.Context(), ` _, err = tx.Exec(r.Context(), `
UPDATE bookings UPDATE bookings
@@ -799,7 +799,9 @@ func GetTodayAppointmentsHandler(w http.ResponseWriter, r *http.Request) {
rows.Close() rows.Close()
if len(raw) == 0 { if len(raw) == 0 {
_ = json.NewEncoder(w).Encode(TodayAppointmentsResponse{Appointments: []TodayAppointment{}}) if err := json.NewEncoder(w).Encode(TodayAppointmentsResponse{Appointments: []TodayAppointment{}}); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
return return
} }
+11 -7
View File
@@ -115,10 +115,10 @@ func CreateGuestUserHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
var userID string var userID string
err = tx.QueryRow(r.Context(), ` err = tx.QueryRow(r.Context(), `
@@ -144,7 +144,9 @@ func CreateGuestUserHandler(w http.ResponseWriter, r *http.Request) {
// Note: We intentionally don't sync to CardDAV - guests don't need calendar contacts // Note: We intentionally don't sync to CardDAV - guests don't need calendar contacts
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(CreateGuestUserResponse{ID: userID, Role: "guest"}) if err := json.NewEncoder(w).Encode(CreateGuestUserResponse{ID: userID, Role: "guest"}); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// GET /api/check-email?email=...&firstName=...&lastName=...&phone=... // GET /api/check-email?email=...&firstName=...&lastName=...&phone=...
@@ -199,7 +201,9 @@ func CheckEmailHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(map[string]any{ if err := json.NewEncoder(w).Encode(map[string]any{
"suggestion": suggestion, "suggestion": suggestion,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
+4 -2
View File
@@ -2,6 +2,7 @@ package user
import ( import (
"encoding/json" "encoding/json"
"log"
"net/http" "net/http"
"crussell/db" "crussell/db"
@@ -29,6 +30,7 @@ func GetLoyaltyHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
if err := json.NewEncoder(w).Encode(loyalty); err != nil {
_ = json.NewEncoder(w).Encode(loyalty) log.Printf("Failed to encode JSON response: %v", err)
}
} }
+30 -18
View File
@@ -170,7 +170,9 @@ func GetProfileHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
_ = json.NewEncoder(w).Encode(user) if err := json.NewEncoder(w).Encode(user); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// PUT /api/user/profile // PUT /api/user/profile
@@ -320,8 +322,8 @@ func UpdateProfileHandler(w http.ResponseWriter, r *http.Request) {
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
// If first or last name changed (via user edit), track the old names in history // If first or last name changed (via user edit), track the old names in history
@@ -730,8 +732,8 @@ func ChangePasswordHandler(w http.ResponseWriter, r *http.Request) {
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
_, err = tx.Exec(r.Context(), `UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2`, string(newHash), userID) _, err = tx.Exec(r.Context(), `UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2`, string(newHash), userID)
@@ -815,7 +817,9 @@ func GetEligiblePatchTestServicesHandler(w http.ResponseWriter, r *http.Request)
services = []ServiceForPatchTest{} services = []ServiceForPatchTest{}
} }
_ = json.NewEncoder(w).Encode(services) if err := json.NewEncoder(w).Encode(services); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
type AddPatchTestRequest struct { type AddPatchTestRequest struct {
@@ -869,8 +873,8 @@ func AddPatchTestHandler(w http.ResponseWriter, r *http.Request) {
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
_, err = tx.Exec(r.Context(), ` _, err = tx.Exec(r.Context(), `
@@ -940,7 +944,9 @@ func GetUserPatchTestsHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(tests) if err := json.NewEncoder(w).Encode(tests); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// DELETE /api/admin/users/{user_id}/patch-tests/{test_id} // DELETE /api/admin/users/{user_id}/patch-tests/{test_id}
@@ -964,8 +970,8 @@ func DeletePatchTestHandler(w http.ResponseWriter, r *http.Request) {
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
result, err := tx.Exec(r.Context(), ` result, err := tx.Exec(r.Context(), `
@@ -1079,8 +1085,8 @@ func UploadProfilePictureHandler(w http.ResponseWriter, r *http.Request) {
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
_, err = tx.Exec(r.Context(), `UPDATE users SET profile_pic_url = $1 WHERE id = $2`, url, userID) _, err = tx.Exec(r.Context(), `UPDATE users SET profile_pic_url = $1 WHERE id = $2`, url, userID)
@@ -1096,7 +1102,9 @@ func UploadProfilePictureHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = json.NewEncoder(w).Encode(UploadProfilePicResponse{URL: url}) if err := json.NewEncoder(w).Encode(UploadProfilePicResponse{URL: url}); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
func processProfileImage(data []byte) ([]byte, error) { func processProfileImage(data []byte) ([]byte, error) {
@@ -1165,7 +1173,9 @@ func GetNotificationPreferencesHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
_ = json.NewEncoder(w).Encode(prefs) if err := json.NewEncoder(w).Encode(prefs); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// PUT /api/user/notification-preferences // PUT /api/user/notification-preferences
@@ -1200,8 +1210,8 @@ func UpdateNotificationPreferencesHandler(w http.ResponseWriter, r *http.Request
} }
defer func() { defer func() {
if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" {
slog.Error("failed to rollback transaction", "err", err) slog.Error("failed to rollback transaction", "err", err)
} }
}() }()
if exists { if exists {
@@ -1258,5 +1268,7 @@ func GetContactInfoHandler(w http.ResponseWriter, r *http.Request) {
contact.Role = "Owner / Beauty Specialist" contact.Role = "Owner / Beauty Specialist"
_ = json.NewEncoder(w).Encode(contact) if err := json.NewEncoder(w).Encode(contact); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
+4 -2
View File
@@ -151,10 +151,12 @@ func healthCheckHandler(w http.ResponseWriter, r *http.Request) {
} else { } else {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
} }
_ = json.NewEncoder(w).Encode(map[string]any{ if err := json.NewEncoder(w).Encode(map[string]any{
"status": status, "status": status,
"services": services, "services": services,
}) }); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
func main() { func main() {
+7 -3
View File
@@ -2,6 +2,7 @@ package mw
import ( import (
"encoding/json" "encoding/json"
"log"
"net/http" "net/http"
) )
@@ -11,7 +12,9 @@ import (
func RespondJSON(w http.ResponseWriter, status int, data any) { func RespondJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status) w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(data) if err := json.NewEncoder(w).Encode(data); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
} }
// RespondError sends a JSON error response with the given status code and message. // RespondError sends a JSON error response with the given status code and message.
@@ -19,6 +22,7 @@ func RespondJSON(w http.ResponseWriter, status int, data any) {
func RespondError(w http.ResponseWriter, status int, msg string) { func RespondError(w http.ResponseWriter, status int, msg string) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status) w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]string{"error": msg}) if err := json.NewEncoder(w).Encode(map[string]string{"error": msg}); err != nil {
log.Printf("Failed to encode error response: %v", err)
}
} }