fix: resolve golangci-lint violations (errcheck, unused, gosimple, ineffassign)

errcheck: add proper error handling with slog.Error for tx.Rollback, key generation, and s3/dav operations. Add nolint comments for intentionally discarded DB scan errors and HTTP write errors.
unused: remove dead code (svcRow type, processImage, nonDepositPaymentType, generateSecureCode, colorBold, nGreen, nRed)
gosimple S1021: merge var declaration with assignment in manage.go
ineffassign: remove dead assignments in settings.go, till.go, images.go

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-07-09 18:53:51 +01:00
co-authored by Sisyphus
parent b26bf14419
commit ed9cb1489c
34 changed files with 766 additions and 345 deletions
+21 -4
View File
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"log"
"log/slog"
"time"
"crussell/clock"
@@ -47,7 +48,11 @@ func RevokeJTI(ctx context.Context, jti string, expiresAt time.Time) {
log.Printf("WARN: Failed to begin transaction for JTI revocation: %v", err)
return
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
_, err = tx.Exec(ctx,
`INSERT INTO revoked_jtis (jti, expires_at) VALUES ($1, $2)
@@ -92,7 +97,11 @@ func CleanupRevokedJTIs(ctx context.Context) (int, error) {
log.Printf("WARN: Failed to begin transaction for JTI cleanup: %v", err)
return 0, err
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
tag, err := tx.Exec(ctx,
`DELETE FROM revoked_jtis WHERE expires_at < NOW()`)
@@ -198,7 +207,11 @@ func GenerateRefreshToken(ctx context.Context, userID string, role string) (stri
if err != nil {
return "", fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
var tokenID int64
err = tx.QueryRow(ctx, query, userID, token, role).Scan(&tokenID)
@@ -227,7 +240,11 @@ func VerifyRefreshToken(ctx context.Context, tokenString string) (userID string,
if err != nil {
return "", "", fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
err = tx.QueryRow(ctx, query, tokenString).Scan(&userID, &role)
if err != nil {
+27 -12
View File
@@ -7,6 +7,7 @@ import (
"database/sql"
"encoding/json"
"errors"
"log/slog"
"net/http"
"strconv"
"strings"
@@ -103,7 +104,7 @@ func GetCustomServices(w http.ResponseWriter, r *http.Request) {
if services == nil {
services = []CustomService{}
}
json.NewEncoder(w).Encode(services)
_ = json.NewEncoder(w).Encode(services)
return
}
@@ -166,11 +167,13 @@ func GetCustomServices(w http.ResponseWriter, r *http.Request) {
// so pgx does not return "conn busy" on the same transaction.
if q != "" {
var countTotal int64
db.Conn.QueryRow(r.Context(), "SELECT COUNT(*) FROM custom_services WHERE name ILIKE $1 OR description ILIKE $1", "%"+q+"%").Scan(&countTotal)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = db.Conn.QueryRow(r.Context(), "SELECT COUNT(*) FROM custom_services WHERE name ILIKE $1 OR description ILIKE $1", "%"+q+"%").Scan(&countTotal)
total = countTotal
} else {
var countTotal int64
db.Conn.QueryRow(r.Context(), "SELECT COUNT(*) FROM custom_services").Scan(&countTotal)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = db.Conn.QueryRow(r.Context(), "SELECT COUNT(*) FROM custom_services").Scan(&countTotal)
total = countTotal
}
@@ -186,7 +189,7 @@ func GetCustomServices(w http.ResponseWriter, r *http.Request) {
if services == nil {
services = []CustomService{}
}
json.NewEncoder(w).Encode(CustomServiceListResponse{
_ = json.NewEncoder(w).Encode(CustomServiceListResponse{
Services: services,
Total: total,
PerPage: perPage,
@@ -259,7 +262,7 @@ func CreateCustomService(w http.ResponseWriter, r *http.Request) {
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(cs)
_ = json.NewEncoder(w).Encode(cs)
}
func GetCustomService(w http.ResponseWriter, r *http.Request) {
@@ -299,7 +302,7 @@ func GetCustomService(w http.ResponseWriter, r *http.Request) {
cs.LastUsedAt = &lastUsedAt.Time
}
json.NewEncoder(w).Encode(cs)
_ = json.NewEncoder(w).Encode(cs)
}
func UpdateCustomService(w http.ResponseWriter, r *http.Request) {
@@ -378,7 +381,11 @@ func UpdateCustomService(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
result, err := tx.Exec(r.Context(), query, args...)
if err != nil {
@@ -395,7 +402,7 @@ func UpdateCustomService(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(map[string]string{"message": "Custom service updated"})
_ = json.NewEncoder(w).Encode(map[string]string{"message": "Custom service updated"})
}
func PromoteCustomService(w http.ResponseWriter, r *http.Request) {
@@ -410,7 +417,11 @@ func PromoteCustomService(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Failed to start transaction", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
var name, desc, notes sql.NullString
var price float64
@@ -473,7 +484,7 @@ func PromoteCustomService(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(map[string]string{
_ = json.NewEncoder(w).Encode(map[string]string{
"message": "Custom service promoted to regular service",
"new_service_id": newServiceID,
"custom_service_id": id,
@@ -507,7 +518,11 @@ func DeleteCustomService(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
result, err := tx.Exec(r.Context(), `DELETE FROM custom_services WHERE id = $1`, id)
if err != nil {
@@ -524,7 +539,7 @@ func DeleteCustomService(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(map[string]string{"message": "Custom service deleted"})
_ = json.NewEncoder(w).Encode(map[string]string{"message": "Custom service deleted"})
}
func joinStrings(strs []string, sep string) string {
+17 -4
View File
@@ -8,6 +8,7 @@ import (
"encoding/json"
"errors"
"log"
"log/slog"
"net/http"
"strconv"
"time"
@@ -279,7 +280,11 @@ func CreateDiscountCampaign(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
// Insert new campaign
query := `
@@ -512,7 +517,11 @@ func UpdateDiscountCampaign(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
_, err = tx.Exec(r.Context(), query, args...)
if err != nil {
@@ -636,7 +645,11 @@ func DeleteDiscountCampaign(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
result, err := tx.Exec(r.Context(), query, campaignID)
if err != nil {
@@ -656,7 +669,7 @@ func DeleteDiscountCampaign(w http.ResponseWriter, r *http.Request) {
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]any{
_ = json.NewEncoder(w).Encode(map[string]any{
"message": "Campaign deleted successfully",
"id": campaignID,
})
+18 -5
View File
@@ -5,6 +5,7 @@ import (
"crussell/internal/validators"
"database/sql"
"encoding/json"
"log/slog"
"net/http"
"strconv"
@@ -73,7 +74,7 @@ func GetPatchTests(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(patchTests)
_ = json.NewEncoder(w).Encode(patchTests)
}
// CreatePatchTest handles POST /api/admin/patch-tests
@@ -100,7 +101,11 @@ func CreatePatchTest(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
var id string
err = tx.QueryRow(r.Context(), query, req.Name, req.Description, req.NoticeDurationHours, req.ExpiryMonths, req.ServiceIDs).Scan(&id)
@@ -115,7 +120,7 @@ func CreatePatchTest(w http.ResponseWriter, r *http.Request) {
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{"id": id})
_ = json.NewEncoder(w).Encode(map[string]string{"id": id})
}
// UpdatePatchTest handles PUT /api/admin/patch-tests/{id}
@@ -176,7 +181,11 @@ func UpdatePatchTest(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
_, err = tx.Exec(r.Context(), query, args...)
if err != nil {
@@ -205,7 +214,11 @@ func DeletePatchTest(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
_, err = tx.Exec(r.Context(), "DELETE FROM patch_tests WHERE id = $1", id)
if err != nil {
+8 -4
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"log"
"log/slog"
"net/http"
"net/url"
"strconv"
@@ -68,7 +69,7 @@ func GetPublicBusinessInfo(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(info)
_ = json.NewEncoder(w).Encode(info)
}
func GetBusinessSettings(w http.ResponseWriter, r *http.Request) {
@@ -90,7 +91,7 @@ func GetBusinessSettings(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(s)
_ = json.NewEncoder(w).Encode(s)
}
type UpdateBusinessSettingsRequest struct {
@@ -222,7 +223,6 @@ func UpdateBusinessSettings(w http.ResponseWriter, r *http.Request) {
if req.VoucherType != nil {
setClauses = append(setClauses, "voucher_type = $"+strconv.Itoa(argIdx))
args = append(args, *req.VoucherType)
argIdx++
}
if len(setClauses) == 0 {
@@ -245,7 +245,11 @@ func UpdateBusinessSettings(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Failed to update settings", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
_, err = tx.Exec(r.Context(), query.String(), args...)
if err != nil {
+28 -20
View File
@@ -9,11 +9,11 @@ import (
"crussell/internal/validators"
"crussell/internal/zxcvbnjs"
"crussell/mw"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"log"
"log/slog"
"net/http"
"github.com/jackc/pgx/v5"
@@ -219,7 +219,11 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
now := clock.Now()
@@ -376,7 +380,11 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
err = tx.QueryRow(r.Context(), `
UPDATE users
@@ -421,7 +429,11 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
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)
if err != nil {
@@ -451,7 +463,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(auth.AuthResponse{
_ = json.NewEncoder(w).Encode(auth.AuthResponse{
Token: tokenString,
JTI: jti,
RefreshToken: refreshToken,
@@ -500,7 +512,7 @@ func RefreshTokenHandler(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(auth.AuthResponse{
_ = json.NewEncoder(w).Encode(auth.AuthResponse{
Token: newToken,
JTI: jti,
RefreshToken: refreshToken,
@@ -518,7 +530,7 @@ func LogoutHandler(w http.ResponseWriter, r *http.Request) {
// Revoke the JTI — match the access token lifetime (1 hour)
auth.RevokeJTI(r.Context(), jti, clock.Now().Add(1*time.Hour))
json.NewEncoder(w).Encode(map[string]bool{"success": true})
_ = json.NewEncoder(w).Encode(map[string]bool{"success": true})
}
type VerificationCodeRequest struct {
@@ -557,7 +569,7 @@ func GenerateVerificationCodeHandler(w http.ResponseWriter, r *http.Request) {
).Scan(&userID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
json.NewEncoder(w).Encode(VerificationResponse{Success: true, Message: "If the email exists, a verification code will be sent"})
_ = json.NewEncoder(w).Encode(VerificationResponse{Success: true, Message: "If the email exists, a verification code will be sent"})
return
}
log.Printf("Failed to look up user: %v", err)
@@ -578,7 +590,7 @@ func GenerateVerificationCodeHandler(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(VerificationResponse{Success: true, Message: "Verification code generated"})
_ = json.NewEncoder(w).Encode(VerificationResponse{Success: true, Message: "Verification code generated"})
}
func VerifyCodeHandler(w http.ResponseWriter, r *http.Request) {
@@ -639,7 +651,11 @@ func VerifyCodeHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
_, err = tx.Exec(r.Context(),
`UPDATE verification_codes SET used_at = NOW() WHERE code = $1`,
@@ -669,15 +685,7 @@ func VerifyCodeHandler(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(VerificationResponse{Success: true, Message: "Email verified successfully"})
_ = json.NewEncoder(w).Encode(VerificationResponse{Success: true, Message: "Email verified successfully"})
}
// TODO(M8): Replace crypto/rand fallback with proper error handling - time-based fallback is predictable
func generateSecureCode(length int) string {
bytes := make([]byte, length)
if _, err := rand.Read(bytes); err != nil {
log.Printf("Failed to generate random code: %v", err)
return strings.ToLower(fmt.Sprintf("%x", clock.Now().UnixNano()))
}
return strings.ToLower(fmt.Sprintf("%x", bytes))
}
+6 -1
View File
@@ -12,6 +12,7 @@ import (
"fmt"
"github.com/jackc/pgx/v5"
"log"
"log/slog"
"net"
"net/http"
"time"
@@ -176,7 +177,11 @@ func AdminReserveSlotHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
// Check booking overlap inside transaction
// pending_release is excluded — those bookings are evicted at creation time
+65 -21
View File
@@ -15,6 +15,7 @@ import (
"errors"
"fmt"
"log"
"log/slog"
"math"
"net/http"
"sort"
@@ -1447,7 +1448,11 @@ func UpdateBookingServicesHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
// Evict any pending_release bookings that overlap this slot.
if _, evictErr := EvictPendingReleaseOverlapping(r.Context(), tx, startTime, newEndTime); evictErr != nil {
@@ -1993,12 +1998,13 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) {
// Get payment info
var preStartPaid float64
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'`, existingBooking.ID).Scan(&preStartPaid)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = 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'`, existingBooking.ID).Scan(&preStartPaid)
populateDepositFields(&existingBooking, existingBooking.DepositRequired, preStartPaid)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(existingBooking)
_ = json.NewEncoder(w).Encode(existingBooking)
return
}
// If err is sql.ErrNoRows, proceed with creation
@@ -2223,7 +2229,11 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
// Evict any pending_release bookings that overlap this slot.
if _, err := EvictPendingReleaseOverlapping(r.Context(), tx, req.StartTime, endTime); err != nil {
@@ -2429,7 +2439,11 @@ func EditBookingHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
var durationMinutes int
if err := tx.QueryRow(r.Context(), `
@@ -2583,7 +2597,11 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
// Read current status before updating to validate the transition
var currentStatus string
@@ -2680,7 +2698,8 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
// Don't award a stamp if this booking already used a loyalty redemption
// (take or receive, never both).
var loyaltyAppliedOnThisBooking bool
tx.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND discount_source = 'loyalty')`, bookingID).Scan(&loyaltyAppliedOnThisBooking)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = tx.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND discount_source = 'loyalty')`, bookingID).Scan(&loyaltyAppliedOnThisBooking)
var newStampCount int
if bookingTotal > 0 && !loyaltyAppliedOnThisBooking {
@@ -2716,7 +2735,8 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
// Skip time-based campaign if already applied at payment time
var timeBasedApplied bool
tx.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND discount_source = 'campaign' AND campaign_type = 'time_based')`, bookingID).Scan(&timeBasedApplied)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = tx.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND discount_source = 'campaign' AND campaign_type = 'time_based')`, bookingID).Scan(&timeBasedApplied)
if bookingTotal > 0 && !timeBasedApplied {
var campaignID string
var campaignPercent float64
@@ -2753,10 +2773,12 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
if bookingTotal > 0 {
var userBookingCount int
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = tx.QueryRow(r.Context(), `SELECT COUNT(*) FROM bookings WHERE user_id = $1 AND status = 'completed'`, booking.User.ID).Scan(&userBookingCount)
var milestoneCampaignID string
var milestonePercent float64
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = tx.QueryRow(r.Context(), `
SELECT id, discount_percent FROM discount_campaigns
WHERE status = 'active' AND campaign_type = 'milestone' AND milestone_type = 'per_user_booking_count'
@@ -2786,18 +2808,22 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
}
var globalMilestoneApplied bool
tx.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND discount_source = 'campaign' AND campaign_type = 'milestone' AND milestone_type = 'global_booking_count')`, bookingID).Scan(&globalMilestoneApplied)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = tx.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND discount_source = 'campaign' AND campaign_type = 'milestone' AND milestone_type = 'global_booking_count')`, bookingID).Scan(&globalMilestoneApplied)
if !globalMilestoneApplied {
var globalCount int
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = tx.QueryRow(r.Context(), `SELECT COUNT(*) FROM bookings WHERE status = 'completed'`).Scan(&globalCount)
var hasInPersonPayment bool
tx.QueryRow(r.Context(), `
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = tx.QueryRow(r.Context(), `
SELECT EXISTS(SELECT 1 FROM payments WHERE booking_id = $1 AND payment_method = 'in_person_card')`, bookingID).Scan(&hasInPersonPayment)
if hasInPersonPayment {
var globalCampaignID string
var globalPercent float64
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = tx.QueryRow(r.Context(), `
SELECT id, discount_percent FROM discount_campaigns
WHERE status = 'active' AND campaign_type = 'milestone' AND milestone_type = 'global_booking_count'
@@ -2830,6 +2856,7 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
}
var firstVisitDate time.Time
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = tx.QueryRow(r.Context(), `SELECT MIN(start_time) FROM bookings WHERE user_id = $1 AND status = 'completed'`, booking.User.ID).Scan(&firstVisitDate)
if !firstVisitDate.IsZero() {
annRows, err := tx.Query(r.Context(), `
@@ -2989,7 +3016,11 @@ func ConfirmBookingHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
var bkStart time.Time
var dur int
@@ -3121,13 +3152,14 @@ func ConfirmBookingHandler(w http.ResponseWriter, r *http.Request) {
if dav.Service != nil {
var durationMinutes int
db.Conn.QueryRow(r.Context(), `
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = db.Conn.QueryRow(r.Context(), `
SELECT total_duration_minutes FROM bookings WHERE id = $1
`, bookingID).Scan(&durationMinutes)
if durationMinutes == 0 {
durationMinutes = 60
}
dav.Service.CreateEvent(1, dav.EventInput{
_ = dav.Service.CreateEvent(1, dav.EventInput{
Summary: "Crussell Booking",
Start: booking.StartTime,
End: booking.StartTime.Add(time.Duration(durationMinutes) * time.Minute),
@@ -3209,7 +3241,7 @@ func DeleteBookingHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("Refund processing failed for booking %s — cancellation aborted: %v", bookingID, calcErr)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{
_ = json.NewEncoder(w).Encode(map[string]string{
"error": "Refund processing failed — cancellation aborted. Please try again or contact support.",
})
return
@@ -3223,7 +3255,11 @@ func DeleteBookingHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
result, err := tx.Exec(r.Context(), `
UPDATE bookings SET status = $1, updated_at = NOW() WHERE id = $2 AND user_id = $3
@@ -3299,7 +3335,7 @@ func DeleteBookingHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(resp)
_ = json.NewEncoder(w).Encode(resp)
return
}
@@ -3310,7 +3346,11 @@ func DeleteBookingHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
if _, err := tx.Exec(r.Context(), `DELETE FROM admin_notifications WHERE booking_id = $1`, bookingID); err != nil {
log.Printf("Failed to delete admin notifications for booking %s: %v", bookingID, err)
@@ -3336,7 +3376,7 @@ func DeleteBookingHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]any{
_ = json.NewEncoder(w).Encode(map[string]any{
"message": "Booking deleted successfully",
"id": bookingID,
})
@@ -3602,7 +3642,7 @@ func GetBookingCalendarHandler(w http.ResponseWriter, r *http.Request) {
for rows.Next() {
var name string
var price float64
rows.Scan(&name, &price)
_ = rows.Scan(&name, &price)
services = append(services, name)
totalPrice += price
}
@@ -3617,7 +3657,7 @@ func GetBookingCalendarHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/calendar; charset=utf-8")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"booking-%s.ics\"", bookingID))
w.WriteHeader(http.StatusOK)
w.Write([]byte(icalContent))
_, _ = w.Write([]byte(icalContent))
}
func sanitizeICS(s string) string {
@@ -4144,7 +4184,11 @@ func AdminRescheduleBookingHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
forgiveNoShow := req.ForgiveNoShow != nil && *req.ForgiveNoShow
if forgiveNoShow && bookingUserID != "" {
+68 -32
View File
@@ -15,6 +15,7 @@ import (
"fmt"
"github.com/jackc/pgx/v5"
"log"
"log/slog"
"net/http"
"strings"
"time"
@@ -43,7 +44,11 @@ func UserCancelBookingHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
// Get current status before updating
var originalStatus string
@@ -166,7 +171,11 @@ func AdminCancelBookingHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
// Get current status and user ID — use FOR UPDATE to lock the row so
// the refund and status change are atomic.
@@ -289,7 +298,7 @@ func AdminCancelBookingHandler(w http.ResponseWriter, r *http.Request) {
resp["refund_failed"] = true
resp["warning"] = "Booking was cancelled but refund processing failed — please process refund manually or retry"
}
json.NewEncoder(w).Encode(resp)
_ = json.NewEncoder(w).Encode(resp)
return
}
@@ -445,13 +454,15 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) {
// Get deposit info
var depositRequired bool
var preStartPaid float64
db.Conn.QueryRow(r.Context(), `SELECT deposit_required FROM bookings WHERE id = $1`, existingID).Scan(&depositRequired)
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)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = db.Conn.QueryRow(r.Context(), `SELECT deposit_required FROM bookings WHERE id = $1`, existingID).Scan(&depositRequired)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = 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)
populateDepositFields(&existingBooking, depositRequired, preStartPaid)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(existingBooking)
_ = json.NewEncoder(w).Encode(existingBooking)
return
}
}
@@ -644,8 +655,7 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) {
if !req.OutOfHours {
// Check if there's an exceptional hours entry that makes this time unavailable
var isClosed bool
var checkErr error
checkErr = db.Conn.QueryRow(r.Context(), `
checkErr := db.Conn.QueryRow(r.Context(), `
SELECT EXISTS (
SELECT 1 FROM exceptional_working_hours ewh
JOIN exceptional_group_applications ega ON ewh.group_id = ega.group_id
@@ -698,7 +708,11 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
// Evict any pending_release bookings that overlap this slot.
if _, evictErr := EvictPendingReleaseOverlapping(r.Context(), tx, req.StartTime, newEnd); evictErr != nil {
@@ -1240,7 +1254,11 @@ func DeleteEditRequestHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
// Delete the edit request for this booking
res, err := tx.Exec(r.Context(), `
@@ -1361,11 +1379,13 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
// Query payment and timing info (used for validation AND auto-approval later)
var hasPayments bool
hoursUntilCurrent := currentStartTime.Sub(clock.Now()).Hours()
db.Conn.QueryRow(r.Context(), "SELECT EXISTS(SELECT 1 FROM payments WHERE booking_id = $1 AND status = 'completed')", bookingID).Scan(&hasPayments)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = db.Conn.QueryRow(r.Context(), "SELECT EXISTS(SELECT 1 FROM payments WHERE booking_id = $1 AND status = 'completed')", bookingID).Scan(&hasPayments)
// Check if booking has discounts (affects auto-approval decisions)
var hasDiscounts bool
db.Conn.QueryRow(r.Context(), "SELECT EXISTS(SELECT 1 FROM booking_discounts WHERE booking_id = $1)", bookingID).Scan(&hasDiscounts)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = db.Conn.QueryRow(r.Context(), "SELECT EXISTS(SELECT 1 FROM booking_discounts WHERE booking_id = $1)", bookingID).Scan(&hasDiscounts)
if req.NewStartTime != nil && !req.NewStartTime.Equal(currentStartTime) {
if hasPayments && hoursUntilCurrent < 72 {
@@ -1412,7 +1432,11 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
// Delete any existing edit request for this booking (upsert behavior)
_, err = tx.Exec(r.Context(), `
@@ -1574,7 +1598,7 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]any{
_ = json.NewEncoder(w).Encode(map[string]any{
"auto_approved": true,
"edit_request": editReq,
})
@@ -1591,6 +1615,7 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
var durationMinutes int
if len(req.NewServices) > 0 {
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = tx.QueryRow(r.Context(), `
SELECT COALESCE(SUM(dur), 60) FROM (
SELECT s.duration_minutes AS dur
@@ -1603,6 +1628,7 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
) sub
`, req.NewServices).Scan(&durationMinutes)
} else {
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = tx.QueryRow(r.Context(), `
SELECT total_duration_minutes FROM bookings WHERE id = $1
`, bookingID).Scan(&durationMinutes)
@@ -1668,7 +1694,7 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(editReq)
_ = json.NewEncoder(w).Encode(editReq)
}
// AdminListEditRequestsHandler returns all edit requests
@@ -1691,7 +1717,8 @@ func AdminListEditRequestsHandler(w http.ResponseWriter, r *http.Request) {
var total int
// Count query (no ORDER BY needed).
db.Conn.QueryRow(r.Context(), "SELECT COUNT(*) FROM booking_edit_requests").Scan(&total)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = db.Conn.QueryRow(r.Context(), "SELECT COUNT(*) FROM booking_edit_requests").Scan(&total)
rows, err := db.Conn.Query(r.Context(), baseQuery, args...)
if err != nil {
@@ -1747,7 +1774,7 @@ func AdminListEditRequestsHandler(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
_ = json.NewEncoder(w).Encode(map[string]any{
"requests": requests,
"total": total,
})
@@ -1773,7 +1800,11 @@ func AdminApproveEditRequestHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
// Get the edit request
var bookingID string
@@ -2058,7 +2089,11 @@ func AdminRejectEditRequestHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
// Delete the edit request
_, err = tx.Exec(r.Context(), `
@@ -2153,7 +2188,7 @@ func GetMyEditRequestHandler(w http.ResponseWriter, r *http.Request) {
if errors.Is(err, pgx.ErrNoRows) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]any{
_ = json.NewEncoder(w).Encode(map[string]any{
"edit_request": nil,
})
return
@@ -2172,7 +2207,7 @@ func GetMyEditRequestHandler(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
_ = json.NewEncoder(w).Encode(map[string]any{
"edit_request": enriched,
})
}
@@ -2236,7 +2271,7 @@ func GetMyEditRequestsHandler(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
_ = json.NewEncoder(w).Encode(map[string]any{
"edit_requests": enrichedRequests,
})
}
@@ -2293,7 +2328,7 @@ func AdminListAllEditRequestsHandler(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
_ = json.NewEncoder(w).Encode(map[string]any{
"edit_requests": enrichedRequests,
})
}
@@ -2335,16 +2370,17 @@ func AdminGetBookingEditRequestHandler(w http.ResponseWriter, r *http.Request) {
editReq.NewServices = newServices
enriched, err := buildEnrichedEditRequest(r.Context(), &editReq)
if err != nil {
log.Printf("Failed to build enriched edit request: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
if err != nil {
log.Printf("Failed to build enriched edit request: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"edit_request": enriched,
})
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]any{
"edit_request": enriched,
})
}
// ========================================
+12 -3
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"log"
"log/slog"
"net/http"
"strings"
"time"
@@ -179,7 +180,11 @@ func ReserveSlotHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
// Check booking overlap inside transaction (TOCTOU fix)
// pending_release is excluded — those bookings are evicted at creation time.
@@ -249,7 +254,11 @@ func ReserveSlotHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
// Check anon rate cap inside transaction
tenMinutesAgo := clock.Now().Add(-10 * time.Minute)
@@ -333,5 +342,5 @@ func ReserveSlotHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(response)
_ = json.NewEncoder(w).Encode(response)
}
@@ -8,6 +8,7 @@ import (
"encoding/json"
"fmt"
"log"
"log/slog"
"net/http"
"strconv"
"time"
@@ -125,7 +126,8 @@ func GetNotifications(w http.ResponseWriter, r *http.Request) {
if !includeAcknowledged {
countWhere += " WHERE an.acknowledged_at IS NULL"
}
db.Conn.QueryRow(r.Context(), "SELECT COUNT(*) FROM admin_notifications an"+countWhere).Scan(&total)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = db.Conn.QueryRow(r.Context(), "SELECT COUNT(*) FROM admin_notifications an"+countWhere).Scan(&total)
// Query
rows, err := db.Conn.Query(r.Context(), baseQuery, args...)
@@ -241,7 +243,11 @@ func AcknowledgeNotification(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
query := `
UPDATE admin_notifications
@@ -267,7 +273,7 @@ func AcknowledgeNotification(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(map[string]string{
_ = json.NewEncoder(w).Encode(map[string]string{
"status": "ok",
})
}
+57 -24
View File
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"log"
"log/slog"
"net/http"
"strconv"
"strings"
@@ -206,9 +207,11 @@ func GetGiftCards(w http.ResponseWriter, r *http.Request) {
if searchTerm != "" {
countArgs = append(countArgs, "%"+searchTerm+"%")
}
db.Conn.QueryRow(ctx, "SELECT COUNT(*) FROM gift_cards "+whereSQL, countArgs...).Scan(&gcTotal)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = db.Conn.QueryRow(ctx, "SELECT COUNT(*) FROM gift_cards "+whereSQL, countArgs...).Scan(&gcTotal)
} else {
db.Conn.QueryRow(ctx, "SELECT COUNT(*) FROM gift_cards").Scan(&gcTotal)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = db.Conn.QueryRow(ctx, "SELECT COUNT(*) FROM gift_cards").Scan(&gcTotal)
}
gcRows, err := db.Conn.Query(ctx, gcListQuery, gcListArgs...)
@@ -280,11 +283,13 @@ func GetGiftCards(w http.ResponseWriter, r *http.Request) {
// Count query for user balances.
if searchTerm != "" {
db.Conn.QueryRow(ctx, `SELECT COUNT(*) FROM user_giftcard_balances b
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = db.Conn.QueryRow(ctx, `SELECT COUNT(*) FROM user_giftcard_balances b
JOIN users u ON b.user_id = u.id
WHERE u.n_first_name ILIKE $1 OR u.n_last_name ILIKE $1 OR u.email ILIKE $1`, "%"+searchTerm+"%").Scan(&ubTotal)
} else {
db.Conn.QueryRow(ctx, "SELECT COUNT(*) FROM user_giftcard_balances").Scan(&ubTotal)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = db.Conn.QueryRow(ctx, "SELECT COUNT(*) FROM user_giftcard_balances").Scan(&ubTotal)
}
for ubRows.Next() {
@@ -322,7 +327,7 @@ func GetGiftCards(w http.ResponseWriter, r *http.Request) {
}
resp.TotalPages = totalPages
json.NewEncoder(w).Encode(resp)
_ = json.NewEncoder(w).Encode(resp)
}
func CreateGiftCard(w http.ResponseWriter, r *http.Request) {
@@ -350,7 +355,11 @@ func CreateGiftCard(w http.ResponseWriter, r *http.Request) {
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
var gc GiftCard
var lastUsedAt sql.NullTime
@@ -412,7 +421,7 @@ func CreateGiftCard(w http.ResponseWriter, r *http.Request) {
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(gc)
_ = json.NewEncoder(w).Encode(gc)
}
func TopUpGiftCard(w http.ResponseWriter, r *http.Request) {
@@ -453,7 +462,11 @@ func TopUpGiftCard(w http.ResponseWriter, r *http.Request) {
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
var redeemedBy sql.NullString
var isInventory bool
@@ -524,7 +537,7 @@ func TopUpGiftCard(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(gc)
_ = json.NewEncoder(w).Encode(gc)
}
func TransferGiftCard(w http.ResponseWriter, r *http.Request) {
@@ -563,7 +576,11 @@ func TransferGiftCard(w http.ResponseWriter, r *http.Request) {
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
var fromRedeemedBy, toRedeemedBy sql.NullString
var fromRemaining, toRemaining float64
@@ -632,7 +649,7 @@ func TransferGiftCard(w http.ResponseWriter, r *http.Request) {
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
_ = json.NewEncoder(w).Encode(map[string]string{"status": "success"})
}
// --- User Handlers ---
@@ -663,7 +680,11 @@ func RedeemGiftCard(w http.ResponseWriter, r *http.Request) {
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
var amountRemaining float64
var redeemedBy sql.NullString
@@ -736,7 +757,7 @@ func RedeemGiftCard(w http.ResponseWriter, r *http.Request) {
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]any{
_ = json.NewEncoder(w).Encode(map[string]any{
"status": "success",
"amount_redeemed": amountRemaining,
})
@@ -754,7 +775,7 @@ 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)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
json.NewEncoder(w).Encode(map[string]float64{"balance": 0.00})
_ = json.NewEncoder(w).Encode(map[string]float64{"balance": 0.00})
return
}
log.Printf("Failed to query user balance: %v", err)
@@ -762,7 +783,7 @@ func GetGiftCardBalance(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(map[string]float64{"balance": balance})
_ = json.NewEncoder(w).Encode(map[string]float64{"balance": balance})
}
// GetUserGiftCardBalanceAdmin Handler returns any user's balance for the admin.
@@ -780,7 +801,7 @@ 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)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
json.NewEncoder(w).Encode(map[string]float64{"balance": 0.00})
_ = json.NewEncoder(w).Encode(map[string]float64{"balance": 0.00})
return
}
log.Printf("Failed to query user balance: %v", err)
@@ -794,7 +815,11 @@ func GetUserGiftCardBalanceAdmin(w http.ResponseWriter, r *http.Request) {
if err != nil {
log.Printf("Failed to begin transaction: %v", err)
} else {
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
if _, err := tx.Exec(ctx, `
INSERT INTO admin_audit_log (admin_id, action_type, target_user_id, details)
@@ -808,7 +833,7 @@ func GetUserGiftCardBalanceAdmin(w http.ResponseWriter, r *http.Request) {
}
}
json.NewEncoder(w).Encode(map[string]float64{"balance": balance})
_ = json.NewEncoder(w).Encode(map[string]float64{"balance": balance})
}
func BuyGiftCard(w http.ResponseWriter, r *http.Request) {
@@ -849,7 +874,7 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) {
log.Printf("Failed to check idempotency: %v", err)
}
if existing != nil {
json.NewEncoder(w).Encode(existing)
_ = json.NewEncoder(w).Encode(existing)
return
}
}
@@ -901,7 +926,11 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) {
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
var buyPaymentID string
fees := paymentService.CalculateFees(req.Amount, "online")
@@ -1065,7 +1094,7 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) {
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]any{
_ = json.NewEncoder(w).Encode(map[string]any{
"status": "success",
"code": cardID,
"amount": amountPounds,
@@ -1137,7 +1166,7 @@ func GetExpiredBalances(w http.ResponseWriter, r *http.Request) {
balances = []ExpiredBalance{}
}
json.NewEncoder(w).Encode(map[string]any{
_ = json.NewEncoder(w).Encode(map[string]any{
"expired_balances": balances,
"total": len(balances),
})
@@ -1173,7 +1202,11 @@ func ClaimExpiredBalance(w http.ResponseWriter, r *http.Request) {
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
var existingClaimedAt sql.NullTime
err = tx.QueryRow(ctx, `
@@ -1212,5 +1245,5 @@ func ClaimExpiredBalance(w http.ResponseWriter, r *http.Request) {
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "claimed"})
_ = json.NewEncoder(w).Encode(map[string]string{"status": "claimed"})
}
+118 -81
View File
@@ -12,6 +12,7 @@ import (
"errors"
"fmt"
"log"
"log/slog"
"math"
"net/http"
"strconv"
@@ -129,7 +130,7 @@ func GetDiscountPreviewHandler(w http.ResponseWriter, r *http.Request) {
preview := calculateDiscountPreview(r.Context(), bookingID, userID)
json.NewEncoder(w).Encode(preview)
_ = json.NewEncoder(w).Encode(preview)
}
// calculateDiscountPreview runs the same queries as applyEligibleCampaignsAtPayment
@@ -140,7 +141,8 @@ func calculateDiscountPreview(ctx context.Context, bookingID string, userID stri
}
var bookingTotal float64
db.Conn.QueryRow(ctx, `
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = db.Conn.QueryRow(ctx, `
SELECT total_amount FROM bookings WHERE id = $1
`, bookingID).Scan(&bookingTotal)
@@ -162,7 +164,8 @@ func calculateDiscountPreview(ctx context.Context, bookingID string, userID stri
ORDER BY discount_percent DESC LIMIT 1
`).Scan(&campaignID, &campaignPercent, &campaignName); err == nil && campaignID != "" {
var exists int
db.Conn.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, campaignID).Scan(&exists)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = db.Conn.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, campaignID).Scan(&exists)
if exists == 0 {
amount := roundTo2(bookingTotal * campaignPercent / 100)
resp.Discounts = append(resp.Discounts, DiscountPreview{
@@ -176,12 +179,14 @@ func calculateDiscountPreview(ctx context.Context, bookingID string, userID stri
}
var userBookingCount int
db.Conn.QueryRow(ctx, `SELECT COUNT(*) FROM bookings WHERE user_id = $1 AND status = 'completed'`, userID).Scan(&userBookingCount)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = db.Conn.QueryRow(ctx, `SELECT COUNT(*) FROM bookings WHERE user_id = $1 AND status = 'completed'`, userID).Scan(&userBookingCount)
var milestoneCampaignID string
var milestonePercent float64
var milestoneName string
db.Conn.QueryRow(ctx, `
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = db.Conn.QueryRow(ctx, `
SELECT id, discount_percent, name FROM discount_campaigns
WHERE status = 'active' AND campaign_type = 'milestone' AND milestone_type = 'per_user_booking_count'
AND milestone_value = $1
@@ -190,7 +195,8 @@ func calculateDiscountPreview(ctx context.Context, bookingID string, userID stri
if milestoneCampaignID != "" {
var exists int
db.Conn.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, milestoneCampaignID).Scan(&exists)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = db.Conn.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, milestoneCampaignID).Scan(&exists)
if exists == 0 {
amount := roundTo2(bookingTotal * milestonePercent / 100)
resp.Discounts = append(resp.Discounts, DiscountPreview{
@@ -204,7 +210,8 @@ func calculateDiscountPreview(ctx context.Context, bookingID string, userID stri
}
var firstVisitDate time.Time
db.Conn.QueryRow(ctx, `SELECT MIN(start_time) FROM bookings WHERE user_id = $1 AND status = 'completed'`, userID).Scan(&firstVisitDate)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = db.Conn.QueryRow(ctx, `SELECT MIN(start_time) FROM bookings WHERE user_id = $1 AND status = 'completed'`, userID).Scan(&firstVisitDate)
if !firstVisitDate.IsZero() {
type annCamp struct {
id string
@@ -230,7 +237,8 @@ func calculateDiscountPreview(ctx context.Context, bookingID string, userID stri
for _, c := range campaigns {
var exists int
db.Conn.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, c.id).Scan(&exists)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = db.Conn.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, c.id).Scan(&exists)
if exists > 0 {
continue
}
@@ -266,7 +274,8 @@ func calculateDiscountPreview(ctx context.Context, bookingID string, userID stri
LIMIT 1
`, userID).Scan(&rdID, &rdPercent); err == nil && rdID != "" {
exists := 0
db.Conn.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND discount_source = 'referral' AND source_id = $2`, bookingID, rdID).Scan(&exists)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = db.Conn.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND discount_source = 'referral' AND source_id = $2`, bookingID, rdID).Scan(&exists)
if exists == 0 {
amount := roundTo2(bookingTotal * rdPercent / 100)
resp.Discounts = append(resp.Discounts, DiscountPreview{
@@ -341,7 +350,11 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) {
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
// Check booking status inside the transaction.
var status string
@@ -365,7 +378,7 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) {
if err := tx.QueryRow(r.Context(), `
SELECT id, status FROM payments WHERE booking_id = $1 AND idempotency_key = $2
`, bookingID, idempotencyKey).Scan(&existingID, &existingStatus); err == nil {
json.NewEncoder(w).Encode(CheckoutResponse{
_ = json.NewEncoder(w).Encode(CheckoutResponse{
CheckoutID: existingID,
Status: existingStatus,
})
@@ -507,7 +520,7 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(CheckoutResponse{
_ = json.NewEncoder(w).Encode(CheckoutResponse{
CheckoutID: paymentID,
Status: "COMPLETED",
})
@@ -537,7 +550,7 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) {
log.Printf("Failed to check idempotency: %v", err)
}
if existingPayment != nil {
json.NewEncoder(w).Encode(CheckoutResponse{
_ = json.NewEncoder(w).Encode(CheckoutResponse{
CheckoutID: existingPayment.ID,
Status: existingPayment.Status,
})
@@ -559,11 +572,10 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(CheckoutResponse{
_ = json.NewEncoder(w).Encode(CheckoutResponse{
CheckoutID: checkout.ID,
Status: checkout.Status,
})
_ = adminID
}
func GetCheckoutStatus(w http.ResponseWriter, r *http.Request) {
@@ -590,7 +602,7 @@ func GetCheckoutStatus(w http.ResponseWriter, r *http.Request) {
paymentResult, err := SquareClient.GetCheckout(r.Context(), checkoutID)
if err != nil {
if err.Error() == "checkout pending" {
json.NewEncoder(w).Encode(PaymentStatusResponse{Status: "PENDING"})
_ = json.NewEncoder(w).Encode(PaymentStatusResponse{Status: "PENDING"})
return
}
log.Printf("Failed to get checkout status: %v", err)
@@ -610,7 +622,11 @@ func GetCheckoutStatus(w http.ResponseWriter, r *http.Request) {
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
// Check for existing payment inside the transaction.
var existingID string
@@ -620,10 +636,10 @@ func GetCheckoutStatus(w http.ResponseWriter, r *http.Request) {
WHERE booking_id = $1 AND idempotency_key = $2
`, bookingID, idempotencyKey).Scan(&existingID, &existingSquarePayID); err == nil {
if existingSquarePayID.Valid && existingSquarePayID.String == paymentResult.SquarePayID {
json.NewEncoder(w).Encode(PaymentStatusResponse{
Status: "COMPLETED",
PaymentID: existingID,
Amount: paymentResult.Amount,
_ = json.NewEncoder(w).Encode(PaymentStatusResponse{
Status: "COMPLETED",
PaymentID: existingID,
Amount: paymentResult.Amount,
CardBrand: paymentResult.CardBrand,
CardLast4: paymentResult.CardLast4,
ReceiptURL: paymentResult.ReceiptURL,
@@ -661,7 +677,7 @@ func GetCheckoutStatus(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(PaymentStatusResponse{
_ = json.NewEncoder(w).Encode(PaymentStatusResponse{
Status: "COMPLETED",
PaymentID: paymentID,
Amount: paymentResult.Amount,
@@ -805,7 +821,11 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) {
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
var status string
if err := tx.QueryRow(r.Context(), `SELECT status FROM bookings WHERE id = $1`, bookingID).Scan(&status); err != nil {
@@ -840,7 +860,7 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) {
FROM payments
WHERE booking_id = $1 AND idempotency_key = $2
`, bookingID, req.IdempotencyKey).Scan(&existingID, &existingBookingID, &existingPaymentType, &existingStatus, &existingAmount, &existingCreatedAt); err == nil {
json.NewEncoder(w).Encode(PaymentResponse{
_ = json.NewEncoder(w).Encode(PaymentResponse{
ID: existingID.String,
BookingID: existingBookingID.String,
PaymentType: existingPaymentType.String,
@@ -1001,7 +1021,8 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) {
// Promote deposit to confirmed if total paid meets the 20% threshold.
// Check is inside the transaction so it sees the just-inserted payments.
var depositMet bool
tx.QueryRow(r.Context(), `
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = tx.QueryRow(r.Context(), `
WITH booking_total AS (
SELECT total_amount * 100 AS total_cents FROM bookings WHERE id = $1
),
@@ -1034,7 +1055,7 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(PaymentResponse{
_ = json.NewEncoder(w).Encode(PaymentResponse{
ID: primaryPaymentID,
BookingID: bookingID,
PaymentType: req.PaymentType,
@@ -1056,7 +1077,8 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) {
// would create a credit balance or require a refund.
func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingID string, userID string) {
var existingPayment int
q.QueryRow(ctx, `
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = q.QueryRow(ctx, `
SELECT COUNT(*) FROM payments
WHERE booking_id = $1 AND status = 'completed' AND payment_method NOT IN ('discount', 'on_the_house')
`, bookingID).Scan(&existingPayment)
@@ -1088,7 +1110,8 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingI
ORDER BY discount_percent DESC LIMIT 1
`).Scan(&campaignID, &campaignPercent); err == nil && campaignID != "" {
var exists int
q.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, campaignID).Scan(&exists)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = q.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, campaignID).Scan(&exists)
if exists == 0 {
discountAmount := roundTo2(bookingTotal * campaignPercent / 100)
if _, err := q.Exec(ctx, `
@@ -1097,11 +1120,13 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingI
`, bookingID, userID, campaignID, campaignPercent, bookingTotal, discountAmount); err != nil {
log.Printf("Failed to insert time-based campaign discount: %v", err)
} else {
q.Exec(ctx, `
//nolint:errcheck // exec errors are non-critical; best-effort inserts/updates
_, _ = q.Exec(ctx, `
INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_by)
VALUES ($1, 'partial', 'discount', $2, 'completed', $3)
`, bookingID, discountAmount, userID)
q.Exec(ctx, `
//nolint:errcheck // exec errors are non-critical; best-effort inserts/updates
_, _ = q.Exec(ctx, `
UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = $1
`, campaignID)
}
@@ -1109,11 +1134,13 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingI
}
var userBookingCount int
q.QueryRow(ctx, `SELECT COUNT(*) FROM bookings WHERE user_id = $1 AND status = 'completed'`, userID).Scan(&userBookingCount)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = q.QueryRow(ctx, `SELECT COUNT(*) FROM bookings WHERE user_id = $1 AND status = 'completed'`, userID).Scan(&userBookingCount)
var milestoneCampaignID string
var milestonePercent float64
q.QueryRow(ctx, `
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = q.QueryRow(ctx, `
SELECT id, discount_percent FROM discount_campaigns
WHERE status = 'active' AND campaign_type = 'milestone' AND milestone_type = 'per_user_booking_count'
AND milestone_value = $1
@@ -1122,7 +1149,8 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingI
if milestoneCampaignID != "" {
var exists int
q.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, milestoneCampaignID).Scan(&exists)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = q.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, milestoneCampaignID).Scan(&exists)
if exists == 0 {
discountAmount := roundTo2(bookingTotal * milestonePercent / 100)
if _, err := q.Exec(ctx, `
@@ -1131,11 +1159,13 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingI
`, bookingID, userID, milestoneCampaignID, milestonePercent, bookingTotal, discountAmount); err != nil {
log.Printf("Failed to insert per-user milestone discount: %v", err)
} else {
q.Exec(ctx, `
//nolint:errcheck // exec errors are non-critical; best-effort inserts/updates
_, _ = q.Exec(ctx, `
INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_by)
VALUES ($1, 'partial', 'discount', $2, 'completed', $3)
`, bookingID, discountAmount, userID)
q.Exec(ctx, `
//nolint:errcheck // exec errors are non-critical; best-effort inserts/updates
_, _ = q.Exec(ctx, `
UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = $1
`, milestoneCampaignID)
}
@@ -1143,7 +1173,8 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingI
}
var firstVisitDate time.Time
q.QueryRow(ctx, `SELECT MIN(start_time) FROM bookings WHERE user_id = $1 AND status = 'completed'`, userID).Scan(&firstVisitDate)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = q.QueryRow(ctx, `SELECT MIN(start_time) FROM bookings WHERE user_id = $1 AND status = 'completed'`, userID).Scan(&firstVisitDate)
if !firstVisitDate.IsZero() {
annRows, err := q.Query(ctx, `
SELECT id, discount_percent, milestone_value, milestone_unit FROM discount_campaigns
@@ -1168,7 +1199,8 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingI
for _, c := range campaigns {
var exists int
q.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, c.id).Scan(&exists)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = q.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, c.id).Scan(&exists)
if exists > 0 {
continue
}
@@ -1191,11 +1223,13 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingI
`, bookingID, userID, c.id, c.pct, bookingTotal, discountAmount); err != nil {
log.Printf("Failed to insert anniversary discount: %v", err)
} else {
q.Exec(ctx, `
//nolint:errcheck // exec errors are non-critical; best-effort inserts/updates
_, _ = q.Exec(ctx, `
INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_by)
VALUES ($1, 'partial', 'discount', $2, 'completed', $3)
`, bookingID, discountAmount, userID)
q.Exec(ctx, `
//nolint:errcheck // exec errors are non-critical; best-effort inserts/updates
_, _ = q.Exec(ctx, `
UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = $1
`, c.id)
}
@@ -1212,11 +1246,13 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingI
SELECT payment_method FROM payments WHERE booking_id = $1 AND payment_method NOT IN ('discount', 'on_the_house') ORDER BY created_at ASC LIMIT 1
`, bookingID).Scan(&firstPaymentMethod); err == nil && firstPaymentMethod == "in_person_card" {
var globalCount int
q.QueryRow(ctx, `SELECT COUNT(*) FROM bookings WHERE status = 'completed'`).Scan(&globalCount)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = q.QueryRow(ctx, `SELECT COUNT(*) FROM bookings WHERE status = 'completed'`).Scan(&globalCount)
var globalCampaignID string
var globalPercent float64
q.QueryRow(ctx, `
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = q.QueryRow(ctx, `
SELECT id, discount_percent FROM discount_campaigns
WHERE status = 'active' AND campaign_type = 'milestone' AND milestone_type = 'global_booking_count'
AND milestone_value <= $1
@@ -1227,7 +1263,8 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingI
if globalCampaignID != "" {
var exists int
q.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, globalCampaignID).Scan(&exists)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = q.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, globalCampaignID).Scan(&exists)
if exists == 0 {
discountAmount := roundTo2(bookingTotal * globalPercent / 100)
if _, err := q.Exec(ctx, `
@@ -1236,13 +1273,15 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingI
`, bookingID, userID, globalCampaignID, globalPercent, bookingTotal, discountAmount); err != nil {
log.Printf("Failed to insert global milestone discount: %v", err)
} else {
q.Exec(ctx, `
INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_by)
VALUES ($1, 'partial', 'discount', $2, 'completed', $3)
`, bookingID, discountAmount, userID)
q.Exec(ctx, `
UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = $1
`, globalCampaignID)
//nolint:errcheck // exec errors are non-critical; best-effort inserts/updates
_, _ = q.Exec(ctx, `
INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_by)
VALUES ($1, 'partial', 'discount', $2, 'completed', $3)
`, bookingID, discountAmount, userID)
//nolint:errcheck // exec errors are non-critical; best-effort inserts/updates
_, _ = q.Exec(ctx, `
UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = $1
`, globalCampaignID)
}
}
}
@@ -1258,18 +1297,21 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingI
LIMIT 1
`, userID).Scan(&rdID, &rdPercent); err == nil && rdID != "" {
exists := 0
q.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND discount_source = 'referral' AND source_id = $2`, bookingID, rdID).Scan(&exists)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = q.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND discount_source = 'referral' AND source_id = $2`, bookingID, rdID).Scan(&exists)
if exists == 0 {
discountAmount := roundTo2(bookingTotal * rdPercent / 100)
if _, err := q.Exec(ctx, `
INSERT INTO booking_discounts (booking_id, user_id, discount_source, source_id, campaign_type, milestone_type, discount_percent, original_total, discount_amount)
VALUES ($1, $2, 'referral', $3, NULL, NULL, $4, $5, $6)
`, bookingID, userID, rdID, rdPercent, bookingTotal, discountAmount); err == nil {
q.Exec(ctx, `
//nolint:errcheck // exec errors are non-critical; best-effort inserts/updates
_, _ = q.Exec(ctx, `
INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_by)
VALUES ($1, 'partial', 'discount', $2, 'completed', $3)
`, bookingID, discountAmount, userID)
q.Exec(ctx, `
//nolint:errcheck // exec errors are non-critical; best-effort inserts/updates
_, _ = q.Exec(ctx, `
UPDATE referral_discounts SET used = TRUE, used_at = NOW() WHERE id = $1
`, rdID)
}
@@ -1376,23 +1418,6 @@ func buildSplitRecords(primary PaymentRecord, reqPaymentType string, info *Booki
return records
}
// nonDepositPaymentType picks the right label for the non-deposit portion of a
// split payment, following the same rules as the frontend's handlePayFull:
// 'balance' when some payment already exists, 'full' when covering everything,
// 'partial' when leaving a remainder.
func nonDepositPaymentType(reqType string, totalPaidAfterThis float64, thisPortion float64, bookingTotal float64) string {
if totalPaidAfterThis >= bookingTotal {
if totalPaidAfterThis-thisPortion > 0 {
return "balance"
}
return "full"
}
if reqType == "full" || reqType == "deposit" {
return "partial"
}
return "partial"
}
func GetUserPaymentMethods(w http.ResponseWriter, r *http.Request) {
userID, ok := r.Context().Value(mw.UserIDKey).(string)
if !ok || userID == "" {
@@ -1408,7 +1433,7 @@ func GetUserPaymentMethods(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(cards)
_ = json.NewEncoder(w).Encode(cards)
}
func AdminGetUserPaymentMethods(w http.ResponseWriter, r *http.Request) {
@@ -1426,7 +1451,7 @@ func AdminGetUserPaymentMethods(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(cards)
_ = json.NewEncoder(w).Encode(cards)
}
func DeletePaymentMethod(w http.ResponseWriter, r *http.Request) {
@@ -1450,7 +1475,7 @@ func DeletePaymentMethod(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(map[string]string{"status": "deleted"})
_ = json.NewEncoder(w).Encode(map[string]string{"status": "deleted"})
}
type CreatePaymentMethodRequest struct {
@@ -1497,7 +1522,7 @@ func CreatePaymentMethod(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(card)
_ = json.NewEncoder(w).Encode(card)
}
func RefundPayment(w http.ResponseWriter, r *http.Request) {
@@ -1575,7 +1600,11 @@ func RefundPayment(w http.ResponseWriter, r *http.Request) {
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
refundReq := square.RefundPaymentReq{
PaymentID: *payment.SquarePaymentID,
@@ -1619,7 +1648,7 @@ func RefundPayment(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(RefundResponse{
_ = json.NewEncoder(w).Encode(RefundResponse{
ID: refundID,
PaymentID: paymentID,
Amount: req.Amount,
@@ -1723,7 +1752,7 @@ func CreateTipPayment(w http.ResponseWriter, r *http.Request) {
paymentID, err := service.CreatePaymentRecordTx(r.Context(), tx, record, nil)
if err != nil {
tx.Rollback(r.Context())
_ = tx.Rollback(r.Context())
log.Printf("Failed to create payment record: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
@@ -1768,7 +1797,7 @@ func CreateTipPayment(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(PaymentResponse{
_ = json.NewEncoder(w).Encode(PaymentResponse{
ID: paymentID,
BookingID: bookingID,
PaymentType: "tip",
@@ -1843,7 +1872,7 @@ func GetBookingPaymentSummary(w http.ResponseWriter, r *http.Request) {
}
}
json.NewEncoder(w).Encode(PaymentSummaryResponse{
_ = json.NewEncoder(w).Encode(PaymentSummaryResponse{
TotalAmount: int64(summary.TotalAmount * 100),
PaidAmount: int64(summary.PaidAmount * 100),
RefundedAmount: int64(summary.RefundedAmount * 100),
@@ -1917,7 +1946,11 @@ func AcquirePaymentLock(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
if _, err := tx.Exec(r.Context(), `
DELETE FROM time_blockers
@@ -1943,7 +1976,7 @@ func AcquirePaymentLock(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(map[string]any{
_ = json.NewEncoder(w).Encode(map[string]any{
"status": "locked",
"ttl_min": PaymentLockDuration,
"bookingID": bookingID,
@@ -1964,7 +1997,11 @@ func ReleasePaymentLock(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
if _, err := tx.Exec(r.Context(), `
DELETE FROM time_blockers
+7 -2
View File
@@ -8,6 +8,7 @@ import (
"encoding/json"
"errors"
"log"
"log/slog"
"math"
"net/http"
@@ -96,7 +97,11 @@ func ApplyLoyaltyRedemption(w http.ResponseWriter, r *http.Request) {
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
var bookingTotal float64
if err := tx.QueryRow(r.Context(), `
@@ -150,7 +155,7 @@ func ApplyLoyaltyRedemption(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(map[string]any{
_ = json.NewEncoder(w).Encode(map[string]any{
"success": true,
"discount_amount": discountAmount,
})
+6 -1
View File
@@ -3,6 +3,7 @@ package payments
import (
"context"
"log"
"log/slog"
"math"
"time"
@@ -291,7 +292,11 @@ func ProcessCancellationRefund(
log.Printf("Failed to begin transaction for cancellation refund: %v", err)
return &calc, nil
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
// Get the booking's user info for refund routing.
var bookingUserID string
+6 -1
View File
@@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"log"
"log/slog"
"strconv"
"strings"
"time"
@@ -468,7 +469,11 @@ func (s *PaymentService) DeletePaymentMethod(ctx context.Context, cardID, userID
log.Printf("Failed to begin transaction: %v", err)
return err
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
retainedUntil := clock.Now().Add(7 * 365 * 24 * time.Hour)
_, err = tx.Exec(ctx, `
+22 -14
View File
@@ -11,6 +11,7 @@ import (
"errors"
"fmt"
"log"
"log/slog"
"net/http"
"github.com/go-chi/chi/v5"
@@ -96,12 +97,12 @@ 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)
if err == nil {
// Existing sale found — return it (idempotent)
json.NewEncoder(w).Encode(TillSaleResponse{
ID: existingID,
ItemType: req.ItemType,
TotalAmount: req.Amount,
PaymentMethod: req.PaymentMethod,
Status: "completed",
_ = json.NewEncoder(w).Encode(TillSaleResponse{
ID: existingID,
ItemType: req.ItemType,
TotalAmount: req.Amount,
PaymentMethod: req.PaymentMethod,
Status: "completed",
})
return
}
@@ -115,7 +116,11 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) {
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
var giftCardID string
if req.Action == "create" {
@@ -438,11 +443,10 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) {
}
saleStatus = "completed"
squarePaymentID = &paymentResult.SquarePayID
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(TillSaleResponse{
_ = json.NewEncoder(w).Encode(TillSaleResponse{
ID: tillSaleID,
ItemType: req.ItemType,
ItemID: &giftCardID,
@@ -477,7 +481,7 @@ func GetTillCheckoutStatus(w http.ResponseWriter, r *http.Request) {
}
if currentStatus == "completed" {
json.NewEncoder(w).Encode(PaymentStatusResponse{
_ = json.NewEncoder(w).Encode(PaymentStatusResponse{
Status: "COMPLETED",
})
return
@@ -486,7 +490,7 @@ func GetTillCheckoutStatus(w http.ResponseWriter, r *http.Request) {
paymentResult, err := SquareClient.GetCheckout(r.Context(), checkoutID)
if err != nil {
if err.Error() == "checkout pending" {
json.NewEncoder(w).Encode(PaymentStatusResponse{Status: "PENDING"})
_ = json.NewEncoder(w).Encode(PaymentStatusResponse{Status: "PENDING"})
return
}
log.Printf("Failed to get checkout status: %v", err)
@@ -501,7 +505,11 @@ func GetTillCheckoutStatus(w http.ResponseWriter, r *http.Request) {
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
_, err = tx.Exec(r.Context(), `
UPDATE till_sales
@@ -523,7 +531,7 @@ func GetTillCheckoutStatus(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(PaymentStatusResponse{
_ = json.NewEncoder(w).Encode(PaymentStatusResponse{
Status: "COMPLETED",
PaymentID: tillSaleID,
Amount: paymentResult.Amount,
@@ -534,5 +542,5 @@ func GetTillCheckoutStatus(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(PaymentStatusResponse{Status: "PENDING"})
_ = json.NewEncoder(w).Encode(PaymentStatusResponse{Status: "PENDING"})
}
+25 -34
View File
@@ -14,6 +14,7 @@ import (
"fmt"
"io"
"log"
"log/slog"
"net/http"
"regexp"
"sort"
@@ -22,7 +23,6 @@ import (
"time"
"github.com/go-chi/chi/v5"
"github.com/kovidgoyal/imaging"
)
const MaxInputLength = 256
@@ -42,25 +42,6 @@ func mimeTypeForField(fieldName string) string {
}
}
// processImage strips metadata and auto-orients the image
func processImage(data []byte, quality int) ([]byte, error) {
// Decode the image - this automatically applies EXIF orientation
// and strips all metadata (EXIF, GPS, etc.)
img, err := imaging.Decode(bytes.NewReader(data), imaging.AutoOrientation(true))
if err != nil {
return nil, fmt.Errorf("failed to decode image: %w", err)
}
// Encode to JPEG without any metadata
var buf bytes.Buffer
err = imaging.Encode(&buf, img, imaging.JPEG, imaging.JPEGQuality(quality))
if err != nil {
return nil, fmt.Errorf("failed to encode image: %w", err)
}
return buf.Bytes(), nil
}
// validateInputLength returns an error if input exceeds max length
func validateInputLength(input string) error {
@@ -218,7 +199,6 @@ func ListImages(w http.ResponseWriter, r *http.Request) {
}
similaritySum := strings.Join(similarityCalls, " + ")
argOffset := len(filterArgs)
query = fmt.Sprintf(`
SELECT id, url, thumbnail_url, tag_names, created_at%s,
COUNT(t) as match_count,
@@ -241,7 +221,7 @@ func ListImages(w http.ResponseWriter, r *http.Request) {
}
query += " ORDER BY match_count DESC, relevance DESC, created_at DESC, id DESC"
argOffset = len(filterArgs) + len(cleanTags) + len(cursorArgs)
argOffset := len(filterArgs) + len(cleanTags) + len(cursorArgs)
query += fmt.Sprintf(" LIMIT $%d", argOffset+1)
queryArgs := make([]any, len(filterArgs)+len(cleanTags)+len(cursorArgs)+1)
@@ -381,9 +361,9 @@ func ListImages(w http.ResponseWriter, r *http.Request) {
nextCursor = &cursor
}
json.NewEncoder(w).Encode(ImageListResponse{
Images: images,
NextCursor: nextCursor,
_ = json.NewEncoder(w).Encode(ImageListResponse{
Images: images,
NextCursor: nextCursor,
})
}
@@ -450,7 +430,7 @@ func ListTags(w http.ResponseWriter, r *http.Request) {
tags = []Tag{}
}
json.NewEncoder(w).Encode(tags)
_ = json.NewEncoder(w).Encode(tags)
}
type FilterCategory struct {
@@ -622,7 +602,7 @@ func ListFilters(w http.ResponseWriter, r *http.Request) {
return sumI > sumJ
})
json.NewEncoder(w).Encode(filters)
_ = json.NewEncoder(w).Encode(filters)
return
}
@@ -676,7 +656,7 @@ func ListFilters(w http.ResponseWriter, r *http.Request) {
}
filters = uniqueFilters
json.NewEncoder(w).Encode(filters)
_ = json.NewEncoder(w).Encode(filters)
}
func UploadImage(w http.ResponseWriter, r *http.Request) {
@@ -704,7 +684,8 @@ func UploadImage(w http.ResponseWriter, r *http.Request) {
return
}
r.ParseMultipartForm(50 << 20)
//nolint:errcheck // parse errors are non-fatal; form values may still be available
_ = r.ParseMultipartForm(50 << 20)
tagsStr := r.FormValue("tags")
tags := []string{}
@@ -885,7 +866,11 @@ func UploadImage(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
var imgID string
err = tx.QueryRow(r.Context(), `
@@ -919,7 +904,7 @@ func UploadImage(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(Image{
_ = json.NewEncoder(w).Encode(Image{
ID: imgID,
URL: fullURLs.Avif,
ThumbnailURL: thumbURLs.Webp,
@@ -1002,7 +987,9 @@ func DeleteImage(w http.ResponseWriter, r *http.Request) {
continue
}
key := extractKey(u)
s3.Client.Delete(context.Background(), "crussell", key)
if err := s3.Client.Delete(context.Background(), "crussell", key); err != nil {
slog.Warn("failed to delete S3 object", "err", err)
}
}
}
@@ -1012,7 +999,11 @@ func DeleteImage(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
_, err = tx.Exec(r.Context(), `DELETE FROM images WHERE id = $1`, imageID)
if err != nil {
@@ -1119,5 +1110,5 @@ func GetImage(w http.ResponseWriter, r *http.Request) {
img.Thumb.Jpg = thumbJpg.String
}
json.NewEncoder(w).Encode(img)
_ = json.NewEncoder(w).Encode(img)
}
+9 -4
View File
@@ -13,6 +13,7 @@ import (
"crussell/internal/validators"
"crussell/mw"
"log"
"log/slog"
)
var londonLocation = clock.London
@@ -57,7 +58,7 @@ func GetDefaultHours(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(hours)
_ = json.NewEncoder(w).Encode(hours)
}
func UpdateDefaultHours(w http.ResponseWriter, r *http.Request) {
@@ -93,7 +94,11 @@ func UpdateDefaultHours(w http.ResponseWriter, r *http.Request) {
http.Error(w, "failed to start tx", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
weekdays := make([]int, len(hours))
startTimes := make([]string, len(hours))
@@ -288,7 +293,7 @@ func GetWorkingHours(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(results)
_ = json.NewEncoder(w).Encode(results)
}
// isValidTime15Min checks that a time string (HH:MM or HH:MM:SS) has minutes in {00, 15, 30, 45}.
@@ -604,7 +609,7 @@ func GetAvailableHours(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(results)
_ = json.NewEncoder(w).Encode(results)
}
// normalizeTime strips seconds from HH:MM:SS to HH:MM for consistent string
@@ -2,6 +2,7 @@ package scheduling
import (
"encoding/json"
"log/slog"
"net/http"
"strconv"
"time"
@@ -118,7 +119,7 @@ func ListExceptionalGroups(w http.ResponseWriter, r *http.Request) {
}
json.NewEncoder(w).Encode(groups)
_ = json.NewEncoder(w).Encode(groups)
}
// --- Create Group with Hours and Applications (bulk) ---
@@ -183,7 +184,11 @@ func CreateExceptionalGroup(w http.ResponseWriter, r *http.Request) {
http.Error(w, "failed to start transaction", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
// Create group
err = tx.QueryRow(r.Context(), `
@@ -231,7 +236,7 @@ func CreateExceptionalGroup(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(g)
_ = json.NewEncoder(w).Encode(g)
}
// --- Delete Group (cascades to hours and applications) ---
@@ -256,7 +261,11 @@ func DeleteExceptionalGroup(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
result, err := tx.Exec(r.Context(), `
DELETE FROM exceptional_working_hours_groups WHERE id=$1
@@ -320,7 +329,11 @@ func UpdateExceptionalApplications(w http.ResponseWriter, r *http.Request) {
http.Error(w, "failed to start transaction", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
// Delete existing applications for this group
_, err = tx.Exec(r.Context(), `
@@ -3,6 +3,7 @@ package scheduling
import (
"context"
"fmt"
"log/slog"
"crussell/db"
)
@@ -15,7 +16,11 @@ func NotifyUnpaidOneWeek(ctx context.Context) (int, error) {
if err != nil {
return 0, fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
rows, err := tx.Query(ctx, `
SELECT b.id, b.user_id
@@ -80,7 +85,11 @@ func NotifyUnpaidOneMonth(ctx context.Context) (int, error) {
if err != nil {
return 0, fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
rows, err := tx.Query(ctx, `
SELECT b.id, b.user_id
@@ -145,7 +154,11 @@ func TransitionDiscountCampaigns(ctx context.Context) (int, error) {
if err != nil {
return 0, fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
result, err := tx.Exec(ctx, `
UPDATE discount_campaigns
@@ -187,7 +200,11 @@ func CleanupExpiredVerificationCodes(ctx context.Context) (int, error) {
if err != nil {
return 0, fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
result, err := tx.Exec(ctx, `
DELETE FROM verification_codes
@@ -212,7 +229,11 @@ func CleanupExpiredRefreshTokens(ctx context.Context) (int, error) {
if err != nil {
return 0, fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
result, err := tx.Exec(ctx, `
DELETE FROM refresh_tokens
+58 -13
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"log"
"log/slog"
"net/http"
"time"
@@ -112,7 +113,7 @@ func ListTimeBlockers(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(blockers)
_ = json.NewEncoder(w).Encode(blockers)
}
// --- Create Time Blocker ---
@@ -150,7 +151,11 @@ func CreateTimeBlocker(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
// Insert the time blocker
var blocker TimeBlocker
@@ -174,7 +179,7 @@ func CreateTimeBlocker(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(blocker)
_ = json.NewEncoder(w).Encode(blocker)
}
// --- Delete Time Blocker ---
@@ -191,7 +196,11 @@ func DeleteTimeBlocker(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
result, err := tx.Exec(r.Context(), `
DELETE FROM time_blockers WHERE id = $1
@@ -369,7 +378,11 @@ func CleanupOldReservations(ctx context.Context) (int, error) {
if err != nil {
return 0, fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
tag, err := tx.Exec(ctx, `
DELETE FROM time_blockers
@@ -396,7 +409,11 @@ func AnonymizeStaleGuestAccounts(ctx context.Context) (int, error) {
if err != nil {
return 0, fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
var totalRows int
@@ -488,7 +505,11 @@ func CleanupExpiredLoyaltyRedemptions(ctx context.Context) (int, error) {
if err != nil {
return 0, fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
tag, err := tx.Exec(ctx, `
DELETE FROM loyalty_redemptions
@@ -532,7 +553,11 @@ func CleanupExpiredFinancialRecords(ctx context.Context) (int, error) {
if err != nil {
return 0, fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
var totalRows int
@@ -644,7 +669,11 @@ func CleanupExpiredDeposits(ctx context.Context) (int, error) {
if err != nil {
return 0, fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
// 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().
@@ -772,7 +801,11 @@ func CleanupExpiredGiftCards(ctx context.Context) (int, error) {
if err != nil {
return 0, fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
rows, err := tx.Query(ctx, `
SELECT id, amount_remaining
@@ -870,7 +903,11 @@ func CleanupIdleAccounts(ctx context.Context) (int, error) {
if err != nil {
return 0, fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
rowsWithBalance, err := tx.Query(ctx, `
SELECT u.id, COALESCE(b.balance, 0) as balance
@@ -975,7 +1012,11 @@ func CleanupOldIdempotencyKeys(ctx context.Context) (int, error) {
if err != nil {
return 0, fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
var totalRows int
@@ -1026,7 +1067,11 @@ func CleanupOldNameHistory(ctx context.Context) (int, error) {
if err != nil {
return 0, fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
tag, err := tx.Exec(ctx, `
DELETE FROM name_history
+18 -5
View File
@@ -10,6 +10,7 @@ import (
"encoding/json"
"errors"
"github.com/jackc/pgx/v5"
"log/slog"
"net/http"
"time"
@@ -64,7 +65,11 @@ func ToggleService(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
query := "UPDATE services SET is_active = NOT is_active WHERE id = $1"
result, err := tx.Exec(r.Context(), query, serviceID)
@@ -84,7 +89,7 @@ func ToggleService(w http.ResponseWriter, r *http.Request) {
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]any{
_ = json.NewEncoder(w).Encode(map[string]any{
"message": "Service toggled successfully",
"id": serviceID,
})
@@ -137,7 +142,11 @@ func CreateServiceHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
query := `
INSERT INTO services (
@@ -227,7 +236,11 @@ func DeleteServiceHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
query := "UPDATE services SET is_active = FALSE WHERE id = $1"
result, err := tx.Exec(r.Context(), query, serviceID)
@@ -247,7 +260,7 @@ func DeleteServiceHandler(w http.ResponseWriter, r *http.Request) {
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]any{
_ = json.NewEncoder(w).Encode(map[string]any{
"message": "Service deleted successfully",
"id": serviceID,
})
+14 -10
View File
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"log"
"log/slog"
"net/http"
"strings"
"time"
@@ -101,7 +102,11 @@ func GetCurrentAndNextHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("Failed to begin transaction: %v", err)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
_, err = tx.Exec(r.Context(), `
UPDATE bookings
@@ -386,9 +391,9 @@ func findNewBookingServices(r *http.Request, rangeStart time.Time) []ServiceBook
if err == nil {
parts := strings.Split(closeTimeStr, ":")
h, m := 0, 0
fmt.Sscanf(parts[0], "%d", &h)
_, _ = fmt.Sscanf(parts[0], "%d", &h)
if len(parts) > 1 {
fmt.Sscanf(parts[1], "%d", &m)
_, _ = fmt.Sscanf(parts[1], "%d", &m)
}
lastClose = time.Date(dayDate.Year(), dayDate.Month(), dayDate.Day(), h, m, 0, 0, londonLocation).UTC()
} else {
@@ -707,7 +712,11 @@ func GetTodayAppointmentsHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("Failed to begin transaction: %v", err)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
_, err = tx.Exec(r.Context(), `
UPDATE bookings
@@ -788,7 +797,7 @@ func GetTodayAppointmentsHandler(w http.ResponseWriter, r *http.Request) {
rows.Close()
if len(raw) == 0 {
json.NewEncoder(w).Encode(TodayAppointmentsResponse{Appointments: []TodayAppointment{}})
_ = json.NewEncoder(w).Encode(TodayAppointmentsResponse{Appointments: []TodayAppointment{}})
return
}
@@ -797,11 +806,6 @@ func GetTodayAppointmentsHandler(w http.ResponseWriter, r *http.Request) {
for i, a := range raw {
batchIDs[i] = a.ID
}
type svcRow struct {
BookingID string
Name string
Duration int
}
svcRows, err := db.Conn.Query(r.Context(), `
SELECT bs.booking_id, s.name, COALESCE(bs.override_duration_minutes, s.duration_minutes)
FROM booking_services bs
+11 -2
View File
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"log"
"log/slog"
"net/http"
"os"
@@ -103,7 +104,11 @@ func DeleteAccountHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
_, err = tx.Exec(ctx, `SELECT delete_guest_user($1)`, userID)
if err != nil {
@@ -124,7 +129,11 @@ func DeleteAccountHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
_, err = tx.Exec(ctx, `SELECT anonymize_user($1)`, userID)
if err != nil {
+3 -3
View File
@@ -53,12 +53,12 @@ func GetGDPRExportHandler(w http.ResponseWriter, r *http.Request) {
if entry.generating {
gdprExportCacheMu.Unlock()
w.Header().Set("X-Cache", "GENERATING")
w.Write([]byte(`{"status":"generating"}`))
_, _ = w.Write([]byte(`{"status":"generating"}`))
return
}
gdprExportCacheMu.Unlock()
w.Header().Set("X-Cache", "HIT")
w.Write(entry.data)
_, _ = w.Write(entry.data)
return
}
@@ -90,5 +90,5 @@ func GetGDPRExportHandler(w http.ResponseWriter, r *http.Request) {
}()
w.Header().Set("X-Cache", "MISS")
w.Write([]byte(`{"status":"generating"}`))
_, _ = w.Write([]byte(`{"status":"generating"}`))
}
+8 -3
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"log"
"log/slog"
"net/http"
"regexp"
"strings"
@@ -111,7 +112,11 @@ func CreateGuestUserHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
var userID string
err = tx.QueryRow(r.Context(), `
@@ -137,7 +142,7 @@ func CreateGuestUserHandler(w http.ResponseWriter, r *http.Request) {
// Note: We intentionally don't sync to CardDAV - guests don't need calendar contacts
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(CreateGuestUserResponse{ID: userID, Role: "guest"})
_ = json.NewEncoder(w).Encode(CreateGuestUserResponse{ID: userID, Role: "guest"})
}
// GET /api/check-email?email=...&firstName=...&lastName=...&phone=...
@@ -191,7 +196,7 @@ func CheckEmailHandler(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(map[string]any{
_ = json.NewEncoder(w).Encode(map[string]any{
"suggestion": suggestion,
})
}
+1 -1
View File
@@ -30,5 +30,5 @@ func GetLoyaltyHandler(w http.ResponseWriter, r *http.Request) {
}
json.NewEncoder(w).Encode(loyalty)
_ = json.NewEncoder(w).Encode(loyalty)
}
+43 -15
View File
@@ -8,6 +8,7 @@ import (
"fmt"
"io"
"log"
"log/slog"
"net/http"
"os"
"regexp"
@@ -169,7 +170,7 @@ func GetProfileHandler(w http.ResponseWriter, r *http.Request) {
}
}
json.NewEncoder(w).Encode(user)
_ = json.NewEncoder(w).Encode(user)
}
// PUT /api/user/profile
@@ -316,7 +317,11 @@ func UpdateProfileHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "failed to begin transaction", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
// If first or last name changed (via user edit), track the old names in history
nameChanged := currentFirstName != req.FirstName || currentLastName != req.LastName
@@ -560,11 +565,13 @@ func ListAdminUsersHandler(w http.ResponseWriter, r *http.Request) {
// Run BEFORE the data query to avoid "conn busy" errors when routing
// through a per-test transaction (pgx.Tx does not support concurrent queries).
if searchTerm != "" {
db.Conn.QueryRow(r.Context(), `SELECT COUNT(DISTINCT u.id) FROM users u
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = db.Conn.QueryRow(r.Context(), `SELECT COUNT(DISTINCT u.id) FROM users u
LEFT JOIN bookings b ON u.id = b.user_id
WHERE u.fn ILIKE $1 OR u.email ILIKE $1 OR u.phone ILIKE $1`, "%"+searchTerm+"%").Scan(&total)
} else {
db.Conn.QueryRow(r.Context(), "SELECT COUNT(*) FROM users").Scan(&total)
//nolint:errcheck // zero value is acceptable fallback on scan failure
_ = db.Conn.QueryRow(r.Context(), "SELECT COUNT(*) FROM users").Scan(&total)
}
// Get users list
@@ -717,7 +724,11 @@ func ChangePasswordHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
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)
if err != nil {
@@ -800,7 +811,7 @@ func GetEligiblePatchTestServicesHandler(w http.ResponseWriter, r *http.Request)
services = []ServiceForPatchTest{}
}
json.NewEncoder(w).Encode(services)
_ = json.NewEncoder(w).Encode(services)
}
type AddPatchTestRequest struct {
@@ -851,7 +862,11 @@ func AddPatchTestHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
_, err = tx.Exec(r.Context(), `
INSERT INTO user_patch_tests (user_id, patch_test_id, tested_at)
@@ -920,7 +935,7 @@ func GetUserPatchTestsHandler(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(tests)
_ = json.NewEncoder(w).Encode(tests)
}
// DELETE /api/admin/users/{user_id}/patch-tests/{test_id}
@@ -942,7 +957,11 @@ func DeletePatchTestHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
result, err := tx.Exec(r.Context(), `
DELETE FROM user_patch_tests WHERE id = $1 AND user_id = $2
@@ -983,7 +1002,8 @@ func UploadProfilePictureHandler(w http.ResponseWriter, r *http.Request) {
return
}
r.ParseMultipartForm(10 << 20)
//nolint:errcheck // parse errors are non-fatal; form values may still be available
_ = r.ParseMultipartForm(10 << 20)
file, _, err := r.FormFile("file")
if err != nil {
@@ -1043,7 +1063,11 @@ func UploadProfilePictureHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
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)
if err != nil {
@@ -1058,7 +1082,7 @@ func UploadProfilePictureHandler(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(UploadProfilePicResponse{URL: url})
_ = json.NewEncoder(w).Encode(UploadProfilePicResponse{URL: url})
}
func processProfileImage(data []byte) ([]byte, error) {
@@ -1127,7 +1151,7 @@ func GetNotificationPreferencesHandler(w http.ResponseWriter, r *http.Request) {
}
}
json.NewEncoder(w).Encode(prefs)
_ = json.NewEncoder(w).Encode(prefs)
}
// PUT /api/user/notification-preferences
@@ -1160,7 +1184,11 @@ func UpdateNotificationPreferencesHandler(w http.ResponseWriter, r *http.Request
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
defer func() {
if err := tx.Rollback(r.Context()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
if exists {
_, err = tx.Exec(r.Context(), `
@@ -1216,5 +1244,5 @@ func GetContactInfoHandler(w http.ResponseWriter, r *http.Request) {
contact.Role = "Owner / Beauty Specialist"
json.NewEncoder(w).Encode(contact)
_ = json.NewEncoder(w).Encode(contact)
}
+1 -1
View File
@@ -74,7 +74,7 @@ func HandleSquareWebhook(w http.ResponseWriter, r *http.Request) {
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
_, _ = w.Write([]byte("ok"))
}
func verifySquareSignature(body []byte, signature, signingKey, notificationURL string) bool {
+31 -6
View File
@@ -4,6 +4,7 @@ import (
"context"
"crussell/clock"
"fmt"
"log/slog"
"strings"
"time"
@@ -181,7 +182,11 @@ func (s *BaseService) CreateContact(addressBookID int, userID string, input Cont
if err != nil {
return err
}
defer tx.Rollback(context.Background())
defer func() {
if err := tx.Rollback(context.Background()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
query := `
INSERT INTO dav_cards (addressbookid, uri, carddata, lastmodified, etag, size)
@@ -211,7 +216,11 @@ func (s *BaseService) UpdateContact(addressBookID int, uri string, input Contact
if err != nil {
return err
}
defer tx.Rollback(context.Background())
defer func() {
if err := tx.Rollback(context.Background()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
query := `
UPDATE dav_cards
@@ -232,7 +241,11 @@ func (s *BaseService) DeleteContact(addressBookID int, uri string) error {
if err != nil {
return err
}
defer tx.Rollback(context.Background())
defer func() {
if err := tx.Rollback(context.Background()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
query := `DELETE FROM dav_cards WHERE addressbookid = $1 AND uri = $2`
_, err = tx.Exec(context.Background(), query, addressBookID, uri)
@@ -253,7 +266,11 @@ func (s *BaseService) CreateEvent(calendarID int, input EventInput) error {
if err != nil {
return err
}
defer tx.Rollback(context.Background())
defer func() {
if err := tx.Rollback(context.Background()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
query := `
INSERT INTO dav_calendarobjects
@@ -288,7 +305,11 @@ func (s *BaseService) UpdateEvent(calendarID int, uid string, input EventInput)
if err != nil {
return err
}
defer tx.Rollback(context.Background())
defer func() {
if err := tx.Rollback(context.Background()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
query := `
UPDATE dav_calendarobjects
@@ -314,7 +335,11 @@ func (s *BaseService) DeleteEvent(calendarID int, uid string) error {
if err != nil {
return err
}
defer tx.Rollback(context.Background())
defer func() {
if err := tx.Rollback(context.Background()); err != nil {
slog.Error("failed to rollback transaction", "err", err)
}
}()
query := `DELETE FROM dav_calendarobjects WHERE calendarid = $1 AND uid = $2`
_, err = tx.Exec(context.Background(), query, calendarID, uid)
+3 -2
View File
@@ -40,7 +40,9 @@ func init() {
hostname = h
var buf [12]byte
rand.Read(buf[:])
if _, err := rand.Read(buf[:]); err != nil {
panic("crypto/rand.Read failed: " + err.Error())
}
b64 := base64.StdEncoding.EncodeToString(buf[:])
b64 = strings.NewReplacer("+", "", "/", "").Replace(b64)
randomPrefix = b64[0:10]
@@ -53,7 +55,6 @@ func Hostname() string { return hostname }
// Shorthand references to logutil constants (avoids package-qualified noise).
var (
colorReset = logutil.Reset
colorBold = logutil.Bold
colorDim = logutil.Dim
colorCyan = logutil.Cyan
colorGreen = logutil.Green
+1 -3
View File
@@ -79,10 +79,8 @@ type bColor string
var (
reset = nColor(logutil.Reset)
nGreen = nColor(logutil.Green)
nYellow = nColor(logutil.Yellow)
nCyan = nColor(logutil.Cyan)
nRed = nColor(logutil.Red)
bGreen = bColor(logutil.BoldGreen)
bYellow = bColor(logutil.BoldYellow)
@@ -153,7 +151,7 @@ func healthCheckHandler(w http.ResponseWriter, r *http.Request) {
} else {
w.WriteHeader(http.StatusOK)
}
json.NewEncoder(w).Encode(map[string]any{
_ = json.NewEncoder(w).Encode(map[string]any{
"status": status,
"services": services,
})
+1 -1
View File
@@ -11,7 +11,7 @@ import (
func RespondJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
_ = json.NewEncoder(w).Encode(data)
}
// RespondError writes a JSON error response with the given status code and message.