diff --git a/.sisyphus/plans/cron-jobs-package.md b/.sisyphus/plans/cron-jobs-package.md new file mode 100644 index 0000000..9cacca1 --- /dev/null +++ b/.sisyphus/plans/cron-jobs-package.md @@ -0,0 +1,431 @@ +# Plan: Consolidated `jobs` Package for Background Cron Tasks + +**Goal:** Extract 9 side-effect cleanup functions from `GetAvailableHours` HTTP handler into a dedicated `jobs` package with cron-scheduled, parallel execution. Remove the lazy-cleanup antipattern. + +**Current state:** +- 9 cleanup functions in `backend/handlers/scheduling/time-blockers.go` — all called synchronously in `default-hours.go:364-407` on every `GET /available-hours` request +- Only `CleanupOldReservations` ALSO runs on a background goroutine (`main.go:432-450`, 5min ticker) +- 4 other ad-hoc background goroutines exist (JWT cleanup, GDPR cache, login state, rate limiter) +- `robfig/cron/v3` v3.0.1 is already a `go.mod` dependency (currently used only for parsing time-blocker cron expressions) + +--- + +## Phase 1: Create `backend/internal/jobs/` Package + +**New package:** `crussell/internal/jobs` + +### File: `scheduler.go` + +Core scheduler abstraction built on `robfig/cron/v3`: + +```go +package jobs + +import ( + "context" + "log" + "time" + "github.com/robfig/cron/v3" +) + +// Job defines a single periodic task. +type Job struct { + Name string // Human-readable name (for logging) + Schedule string // Standard cron expression ("*/5 * * * *") + Timeout time.Duration // Per-execution timeout + Concurrency int // Max concurrent runs (0 = unlimited, 1 = serial) + Handler func(context.Context) error // The actual work +} + +// Scheduler manages all registered cron jobs with parallel execution. +type Scheduler struct { + cron *cron.Cron + entries []cron.EntryID + registry []Job + baseCtx context.Context + cancel context.CancelFunc + semaphores map[string]chan struct{} // Per-job concurrency limit +} +``` + +**Key design decisions:** +1. Each job runs in its own goroutine (cron v3 default) — parallel by nature +2. Per-job concurrency control via channel semaphore — prevents overlapping runs of the same job +3. Panic recovery wrapper — matches existing pattern in `jwt.go`, `gdpr_export.go`, `auth/local.go` +4. Timeout via `context.WithTimeout` — matches existing `main.go` pattern +5. Graceful shutdown via `baseCtx` — the cron scheduler is stopped, then active jobs drain + +```go +func New() *Scheduler { + ctx, cancel := context.WithCancel(context.Background()) + return &Scheduler{ + cron: cron.New(cron.WithLocation(londonLocation)), + baseCtx: ctx, + cancel: cancel, + semaphores: make(map[string]chan struct{}), + } +} + +func (s *Scheduler) Register(job Job) { + // Validate cron expression at registration time + parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow) + if _, err := parser.Parse(job.Schedule); err != nil { + log.Fatalf("jobs: invalid cron schedule %q for job %q: %v", job.Schedule, job.Name, err) + } + s.registry = append(s.registry, job) +} + +func (s *Scheduler) Start() { + for _, job := range s.registry { + j := job // capture + entryID, err := s.cron.AddFunc(j.Schedule, s.wrapJob(j)) + if err != nil { + log.Fatalf("jobs: failed to register %q: %v", j.Name, err) + } + s.entries = append(s.entries, entryID) + } + s.cron.Start() +} + +func (s *Scheduler) Shutdown() <-chan struct{} { + ctx := s.cron.Stop() // Stop scheduler (returns ctx that completes when all jobs finish) + s.cancel() // Cancel base context so in-flight jobs know to stop + return ctx.Done() // Caller can wait for this +} + +// wrapJob adds panic recovery, timeout, concurrency control, and logging. +func (s *Scheduler) wrapJob(job Job) func() { + // Set up semaphore if concurrency limited + var sem chan struct{} + if job.Concurrency > 0 { + sem = make(chan struct{}, job.Concurrency) + sem <- struct{}{} // Initial slot filled + } + + return func() { + // Concurrency guard + if sem != nil { + select { + case <-sem: + // Acquired — proceed + default: + log.Printf("jobs: %q skipped (previous run still in progress)", job.Name) + return + } + defer func() { sem <- struct{}{} }() + } + + // Panic recovery + defer func() { + if r := recover(); r != nil { + log.Printf("jobs: panic recovered in %q: %v", job.Name, r) + } + }() + + // Timeout + ctx, cancel := context.WithTimeout(s.baseCtx, job.Timeout) + defer cancel() + + start := time.Now() + if err := job.Handler(ctx); err != nil { + log.Printf("jobs: %q failed: %v (duration: %v)", job.Name, err, time.Since(start)) + } else { + log.Printf("jobs: %q completed (duration: %v)", job.Name, time.Since(start)) + } + } +} +``` + +### File: `cleanup.go` + +Registration of all 9 scheduling cleanup functions + JWT cleanup. + +```go +package jobs + +import ( + "context" + "crussell/auth" + "crussell/handlers/scheduling" + "time" +) + +// RegisterAll registers every background job. +func RegisterAll(s *Scheduler) { + // === HIGH FREQUENCY (every 5 min) === + + s.Register(Job{ + Name: "cleanup-reservations", + Schedule: "*/5 * * * *", + Timeout: 30 * time.Second, + Concurrency: 1, + Handler: scheduling.CleanupOldReservations, + }) + + s.Register(Job{ + Name: "cleanup-expired-deposits", + Schedule: "*/5 * * * *", + Timeout: 30 * time.Second, + Concurrency: 1, + Handler: scheduling.CleanupExpiredDeposits, + }) + + // === MID FREQUENCY (hourly) === + + s.Register(Job{ + Name: "cleanup-expired-loyalty-redemptions", + Schedule: "0 * * * *", + Timeout: 30 * time.Second, + Concurrency: 1, + Handler: scheduling.CleanupExpiredLoyaltyRedemptions, + }) + + s.Register(Job{ + Name: "cleanup-old-idempotency-keys", + Schedule: "0 * * * *", + Timeout: 30 * time.Second, + Concurrency: 1, + Handler: scheduling.CleanupOldIdempotencyKeys, + }) + + s.Register(Job{ + Name: "cleanup-revoked-jtis", + Schedule: "0 * * * *", + Timeout: 30 * time.Second, + Concurrency: 1, + Handler: func(ctx context.Context) error { + auth.CleanupRevokedJTIs(ctx) + return nil + }, + }) + + // === LOW FREQUENCY (daily, off-peak) === + + s.Register(Job{ + Name: "anonymize-stale-guest-accounts", + Schedule: "0 3 * * *", // 3am + Timeout: 5 * time.Minute, + Concurrency: 1, + Handler: scheduling.AnonymizeStaleGuestAccounts, + }) + + s.Register(Job{ + Name: "cleanup-expired-financial-records", + Schedule: "0 4 * * *", // 4am + Timeout: 10 * time.Minute, + Concurrency: 1, + Handler: scheduling.CleanupExpiredFinancialRecords, + }) + + s.Register(Job{ + Name: "cleanup-expired-gift-cards", + Schedule: "0 5 * * *", // 5am + Timeout: 5 * time.Minute, + Concurrency: 1, + Handler: scheduling.CleanupExpiredGiftCards, + }) + + s.Register(Job{ + Name: "cleanup-idle-accounts", + Schedule: "30 3 * * *", // 3:30am (offset from financial) + Timeout: 5 * time.Minute, + Concurrency: 1, + Handler: scheduling.CleanupIdleAccounts, + }) + + s.Register(Job{ + Name: "cleanup-old-name-history", + Schedule: "30 4 * * *", // 4:30am + Timeout: 30 * time.Second, + Concurrency: 1, + Handler: scheduling.CleanupOldNameHistory, + }) +} +``` + +**Staggering rationale:** Daily jobs at 3am/3:30am/4am/4:30am/5am spread the load so heavy operations (financial aggregation, idle account scans) don't contend with each other or with the hourly jobs. High-frequency jobs (reservations, deposits) are decoupled on their own 5-min schedules. + +--- + +## Phase 2: Wire into `main.go` + +### Before: +```go +// main.go:142 +auth.StartJTICleanup() + +// main.go:432-450 (background goroutine for reservations) +cleanupCtx, cleanupStop := context.WithCancel(context.Background()) +go func() { + ticker := time.NewTicker(reservationCleanupInterval) + ... +}() + +// main.go:457-468 (shutdown) +cleanupStop() +``` + +### After: +```go +// At package level, after constants but before init() +var sched *jobs.Scheduler + +// In main(), after initDB()/initSquare()/etc but before r := chi.NewRouter(): +sched = jobs.New() +jobs.RegisterAll(sched) +sched.Start() + +// In main(), shutdown block (replace the old cleanupStop() call): +quit := make(chan os.Signal, 1) +signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT) +go func() { + <-quit + log.Println("Shutting down server...") + + // Graceful HTTP shutdown + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + log.Printf("Server forced to shutdown: %v", err) + } + + // Graceful job scheduler shutdown + <-sched.Shutdown() + log.Println("Background jobs stopped") +}() +``` + +**Remove:** +- `reservationCleanupInterval` and `reservationCleanupTimeout` constants (`main.go:71-72`) +- `cleanupCtx`/`cleanupStop` variables (`main.go:433`) +- The goroutine block (`main.go:434-450`) +- Import `scheduling` from `main.go` (no longer needed there) +- `auth.StartJTICleanup()` call (`main.go:142`) + +--- + +## Phase 3: Remove Side-Effects from `GetAvailableHours` + +**File: `backend/handlers/scheduling/default-hours.go`** + +Delete lines 364-407 (the entire cleanup block): + +```diff +- // Clean up old reservations (older than 1 hour) +- if err := CleanupOldReservations(r.Context()); err != nil { +- log.Printf("Failed to cleanup old reservations: %v", err) +- } +- +- // Anonymize stale guest accounts (6+ months after last booking) +- if err := AnonymizeStaleGuestAccounts(r.Context()); err != nil { +- log.Printf("Failed to anonymize stale guest accounts: %v", err) +- } +- +- // Clean up expired loyalty redemptions (pending past expires_at) +- if err := CleanupExpiredLoyaltyRedemptions(r.Context()); err != nil { +- log.Printf("Failed to cleanup expired loyalty redemptions: %v", err) +- } +- +- // Clean up expired financial records (aggregate + delete granular data) +- if err := CleanupExpiredFinancialRecords(r.Context()); err != nil { +- log.Printf("Failed to cleanup expired financial records: %v", err) +- } +- +- // Clean up bookings past deposit deadline (no deposit paid) +- if err := CleanupExpiredDeposits(r.Context()); err != nil { +- log.Printf("Failed to cleanup expired deposits: %v", err) +- } +- +- // Clean up expired gift cards (unused for 24+ months) +- if err := CleanupExpiredGiftCards(r.Context()); err != nil { +- log.Printf("Failed to cleanup expired gift cards: %v", err) +- } +- +- // Clean up idle accounts (2yr no money, 5yr with money) +- if err := CleanupIdleAccounts(r.Context()); err != nil { +- log.Printf("Failed to cleanup idle accounts: %v", err) +- } +- +- // Clean up old idempotency keys (24h+ and non-pending) +- if err := CleanupOldIdempotencyKeys(r.Context()); err != nil { +- log.Printf("Failed to cleanup old idempotency keys: %v", err) +- } +- +- // Clean up old name history (6+ months) +- if err := CleanupOldNameHistory(r.Context()); err != nil { +- log.Printf("Failed to cleanup old name history: %v", err) +- } +``` + +Also remove `"log"` import from `default-hours.go` if it becomes unused after removal. + +--- + +## Phase 4: Remove `auth.StartJTICleanup()` (Optional but Recommended) + +**File: `backend/auth/jwt.go`** + +- Keep `CleanupRevokedJTIs(ctx)` — it's used by tests and now by the jobs package +- Delete `StartJTICleanup()` entirely (no longer needed, replaced by the jobs package) +- Remove the ticker goroutine + +--- + +## Phase 5: Update Documentation + +**File: `obsidian/Crussell/Technical Manual.md`** +- Update lines ~545 and ~999: change "lazy cleanup triggered on availability fetch" to "all cleanup runs on cron schedules via the `jobs` package" +- Remove the lazy-cleanup justification +- Add a new section documenting the `jobs` package and its job registry + +**File: `obsidian/Crussell/Future Work - Gap Backlog.md`** +- Mark item #2 as fully complete (strikethrough) +- Remove the note about remaining functions still running on `GET /api/availability` + +**File: `README.md`** (optional) +- Add a line about the background job scheduler + +--- + +## Effort Estimate + +| Step | Files Changed | Effort | +|------|--------------|--------| +| Phase 1a: `scheduler.go` | 1 new file | 2-3h | +| Phase 1b: `cleanup.go` | 1 new file | 1h | +| Phase 2: Wire `main.go` | 1 file | 30min | +| Phase 3: Remove side-effects from handler | 1 file | 15min | +| Phase 4: Remove `StartJTICleanup` | 1 file | 15min | +| Phase 5: Update docs | 2-3 files | 30min | +| Testing & go vet | — | 1h | +| **Total** | **4-5 files (+2 new)** | **~6h** | + +--- + +## Risks & Mitigations + +| Risk | Mitigation | +|------|------------| +| **Concurrent DB load** from multiple jobs at the same cron tick | Stagger daily jobs across 3am-5am range. High-frequency jobs (5min) are lightweight. Per-job `Concurrency: 1` prevents overlapping runs. | +| **Job takes longer than interval** (e.g., financial aggregation >5min while scheduled every 5min) | `Concurrency: 1` + skip-logic: if previous run still in-flight, the new invocation is skipped and logged. | +| **Cron expression parsing differs** from existing time-blocker parser | Both use `cron.NewParser(cron.Minute \| cron.Hour \| cron.Dom \| cron.Month \| cron.Dow)` — identical. | +| **Tests expect cleanup side-effects** from `GetAvailableHours` | Search for tests that rely on the side-effect calls. Some may need an explicit cleanup call before assertions. | +| **`CleanupExpiredDeposits` timing gap** — deposit-lapsed slots not freed until next cron tick | 5-min frequency is acceptable. The previous behavior freed on the *next* availability fetch, which could be minutes or hours apart depending on user activity. 5-min max latency is actually *better* than the lazy approach during quiet periods. | +| **Panic in one job takes down the scheduler** | `wrapJob` has defer-recover. One job's panic cannot affect others (separate goroutines). | + +--- + +## Edge Cases & Exclusions + +**Not in scope (Phase 1):** +- `gdpr_export.go:init()` — GDPR cache cleanup (5min ticker). Tightly coupled to package-level state. Leave as-is. +- `auth/local.go:init()` — Login state cleanup. Tightly coupled. Leave as-is. +- `mw/ratelimit.go` goroutines — Rate limiter cleanup. Leave as-is. +- These can be migrated to the scheduler in a follow-up if desired, but require more refactoring. + +**Not a concern:** +- `backend/handlers/scheduling/time-blockers.go` already uses `robfig/cron/v3` — no new dependency, no version conflict. +- All 9 cleanup functions have signature `func(context.Context) error` — perfectly uniform for the `Job.Handler` type. +- The `scheduling` package is already imported in `main.go` — no new import needed for that. + +**Key invariant:** All cleanup functions are idempotent (documented in code). Running them on cron instead of on-demand has zero correctness impact — they produce the same result regardless of how often they run. diff --git a/backend/auth/jwt.go b/backend/auth/jwt.go index 8ee5627..8dd1a8e 100644 --- a/backend/auth/jwt.go +++ b/backend/auth/jwt.go @@ -121,7 +121,7 @@ func GenerateToken(userID string, role string) (string, string, error) { return "", "", err } - _, tokenString, err := TokenAuth.Encode(map[string]interface{}{ + _, tokenString, err := TokenAuth.Encode(map[string]any{ "user_id": userID, "role": role, "jti": jti, @@ -137,7 +137,7 @@ func VerifyToken(tokenString string, ctx context.Context) (userID string, role s return "", "", "", err } - var uidVal interface{} + var uidVal any if err := token.Get("user_id", &uidVal); err != nil { return "", "", "", fmt.Errorf("invalid user_id claim") } @@ -146,7 +146,7 @@ func VerifyToken(tokenString string, ctx context.Context) (userID string, role s return "", "", "", fmt.Errorf("invalid user_id claim") } - var roleVal interface{} + var roleVal any if err := token.Get("role", &roleVal); err != nil { return "", "", "", fmt.Errorf("invalid role claim") } @@ -155,7 +155,7 @@ func VerifyToken(tokenString string, ctx context.Context) (userID string, role s return "", "", "", fmt.Errorf("invalid role claim") } - var jtiVal interface{} + var jtiVal any if err := token.Get("jti", &jtiVal); err != nil { return "", "", "", fmt.Errorf("invalid jti claim") } diff --git a/backend/auth/jwt_test.go b/backend/auth/jwt_test.go index 2f62c01..7ad6fe2 100644 --- a/backend/auth/jwt_test.go +++ b/backend/auth/jwt_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package auth diff --git a/backend/auth/testmain_test.go b/backend/auth/testmain_test.go index 37dcf38..88f91c5 100644 --- a/backend/auth/testmain_test.go +++ b/backend/auth/testmain_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package auth diff --git a/backend/db/db.go b/backend/db/db.go index 366b374..468f775 100644 --- a/backend/db/db.go +++ b/backend/db/db.go @@ -1,5 +1,4 @@ //go:build !dev -// +build !dev package db diff --git a/backend/db/db_dev.go b/backend/db/db_dev.go index ab8ce6f..d32ed45 100644 --- a/backend/db/db_dev.go +++ b/backend/db/db_dev.go @@ -1,5 +1,4 @@ //go:build dev -// +build dev package db diff --git a/backend/db/db_test.go b/backend/db/db_test.go index d9c7e3f..9a70270 100644 --- a/backend/db/db_test.go +++ b/backend/db/db_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package db @@ -178,5 +177,3 @@ func TestGetEnv_ReturnsEmptyWhenUnset(t *testing.T) { // ============================================================================= // Clean-up — restore env after all tests // ============================================================================= - - diff --git a/backend/db/db_test_init.go b/backend/db/db_test_init.go index 0cbdb6e..b8b2e49 100644 --- a/backend/db/db_test_init.go +++ b/backend/db/db_test_init.go @@ -1,5 +1,4 @@ //go:build test -// +build test package db diff --git a/backend/db/testmain_test.go b/backend/db/testmain_test.go index 6868eb9..aa1e5c0 100644 --- a/backend/db/testmain_test.go +++ b/backend/db/testmain_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package db diff --git a/backend/handlers/admin/bookings_extra_test.go b/backend/handlers/admin/bookings_extra_test.go index 1cc5e56..4a6de29 100644 --- a/backend/handlers/admin/bookings_extra_test.go +++ b/backend/handlers/admin/bookings_extra_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package admin @@ -9,8 +8,8 @@ import ( "time" "crussell/clock" - "crussell/testutils" "crussell/handlers/bookings" + "crussell/testutils" "crussell/testutils/fixtures" ) diff --git a/backend/handlers/admin/bookings_fields_test.go b/backend/handlers/admin/bookings_fields_test.go index 78abff7..826cab2 100644 --- a/backend/handlers/admin/bookings_fields_test.go +++ b/backend/handlers/admin/bookings_fields_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package admin diff --git a/backend/handlers/admin/bookings_test.go b/backend/handlers/admin/bookings_test.go index 344b167..169097d 100644 --- a/backend/handlers/admin/bookings_test.go +++ b/backend/handlers/admin/bookings_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package admin @@ -32,9 +31,9 @@ import ( "crussell/clock" "crussell/db" - "crussell/testutils" "crussell/handlers/bookings" "crussell/mw" + "crussell/testutils" "crussell/testutils/fixtures" "github.com/go-chi/chi/v5" @@ -2247,7 +2246,6 @@ func TestAdminBookings_Create_WalkIn(t *testing.T) { } // Seed working hours so we have a valid booking window - // Try to create booking with walk-in time (30 minutes from now - less than 1h requirement) // Regular users would be rejected, but admin should succeed @@ -2291,7 +2289,6 @@ func TestAdminBookings_Create_WalkInWithDeposits(t *testing.T) { } // Seed working hours - // Set user to have outstanding deposits _, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 3 WHERE id = $1", userID) @@ -2346,7 +2343,6 @@ func TestAdminBookings_Confirm_CompletesWithPayment_ReducesDeposits(t *testing.T t.Fatalf("failed to create test service: %v", err) } - // Set user to have deposits_required = 2 _, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 2 WHERE id = $1", userID) if err != nil { @@ -2427,7 +2423,6 @@ func TestAdminBookings_Confirm_CompletesWithoutPayment_NoReduction(t *testing.T) t.Fatalf("failed to create test service: %v", err) } - // Set user to have deposits_required = 2 _, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 2 WHERE id = $1", userID) if err != nil { @@ -2502,7 +2497,6 @@ func TestAdminBookings_Create_EnforceDepositsFalse_BypassesLimit(t *testing.T) { t.Fatalf("failed to create test service: %v", err) } - // Set user to have deposits_required = 3 _, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 3 WHERE id = $1", userID) if err != nil { @@ -2564,9 +2558,7 @@ func TestAdminBookings_Create_EnforceDepositsFalse_Within24h(t *testing.T) { t.Fatalf("failed to create test service: %v", err) } - // Seed working hours - // Set user to have deposits_required = 3 _, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 3 WHERE id = $1", userID) @@ -2619,7 +2611,6 @@ func TestAdminBookings_Create_WalkInGuestUser(t *testing.T) { } // Seed working hours - // Create booking for tomorrow tomorrow := clock.Now().Add(24 * time.Hour).Truncate(time.Second) @@ -2661,7 +2652,6 @@ func TestAdminBookings_Create_WalkInGuestUser(t *testing.T) { func TestGetBookingsByCreatedRange(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - _, err := fixtures.CreateTestAdminUser(tx) if err != nil { @@ -2744,7 +2734,6 @@ func TestGetBookingsByCreatedRange(t *testing.T) { func TestGetBookingsByCreatedRange_Empty(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - _, err := fixtures.CreateTestAdminUser(tx) if err != nil { @@ -2824,7 +2813,6 @@ func TestGetBookingsByCreatedRange_InvalidFormat(t *testing.T) { func TestGetBookingsByCreatedRange_OrderedByCreatedAt(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - _, err := fixtures.CreateTestAdminUser(tx) if err != nil { @@ -2912,7 +2900,6 @@ func TestGetBookingsByCreatedRange_OrderedByCreatedAt(t *testing.T) { func TestGetAdminBooking_WithDiscounts(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -3007,7 +2994,6 @@ func createCompletedBookingWithTimeForAdmin(t *testing.T, ctx context.Context, t func TestAdminBookings_CreateWithCustomServices(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - _, err := fixtures.CreateTestAdminUser(tx) if err != nil { @@ -3084,7 +3070,6 @@ func TestAdminBookings_CreateWithCustomServices(t *testing.T) { func TestAdminBookings_CreateWithCustomAndRegularServices(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - _, err := fixtures.CreateTestAdminUser(tx) if err != nil { @@ -3208,7 +3193,6 @@ func TestAdminBookings_Create_CustomServiceValidation(t *testing.T) { // Test 3: custom_service_ids is provided (should succeed — need working hours) t.Run("provides custom_service_ids only", func(t *testing.T) { - customServiceID, err := fixtures.CreateTestCustomService(tx) if err != nil { @@ -3322,7 +3306,6 @@ func TestAdminBookings_Confirm_WithCustomOverrides(t *testing.T) { func TestAdminBookings_CreateWithCustomServicesAndOverrides(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - _, err := fixtures.CreateTestAdminUser(tx) if err != nil { @@ -3401,7 +3384,6 @@ func TestAdminBookings_CreateWithCustomServicesAndOverrides(t *testing.T) { func TestAdminBookings_AdminReserve_WithCustomServices(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - adminID, err := fixtures.CreateTestAdminUser(tx) if err != nil { diff --git a/backend/handlers/admin/custom_services.go b/backend/handlers/admin/custom_services.go index d83effe..a7cb359 100644 --- a/backend/handlers/admin/custom_services.go +++ b/backend/handlers/admin/custom_services.go @@ -9,6 +9,7 @@ import ( "errors" "net/http" "strconv" + "strings" "time" "github.com/go-chi/chi/v5" @@ -107,7 +108,7 @@ func GetCustomServices(w http.ResponseWriter, r *http.Request) { } var dataQuery string - var dataArgs []interface{} + var dataArgs []any if q != "" { dataQuery = ` @@ -115,7 +116,7 @@ func GetCustomServices(w http.ResponseWriter, r *http.Request) { FROM custom_services WHERE name ILIKE $1 OR description ILIKE $1 ` - dataArgs = []interface{}{"%" + q + "%"} + dataArgs = []any{"%" + q + "%"} dataQuery += " ORDER BY created_at DESC, id DESC LIMIT $" + strconv.Itoa(len(dataArgs)+1) dataArgs = append(dataArgs, perPage+1) @@ -318,7 +319,7 @@ func UpdateCustomService(w http.ResponseWriter, r *http.Request) { return } - updates := make(map[string]interface{}) + updates := make(map[string]any) if req.Name != nil { updates["name"] = *req.Name } @@ -361,7 +362,7 @@ func UpdateCustomService(w http.ResponseWriter, r *http.Request) { } setClauses := make([]string, 0, len(updates)) - args := make([]interface{}, 0, len(updates)+1) + args := make([]any, 0, len(updates)+1) argIdx := 1 for field, val := range updates { setClauses = append(setClauses, field+" = $"+strconv.Itoa(argIdx)) @@ -530,9 +531,10 @@ func joinStrings(strs []string, sep string) string { if len(strs) == 0 { return "" } - result := strs[0] + var result strings.Builder + result.WriteString(strs[0]) for _, s := range strs[1:] { - result += sep + s + result.WriteString(sep + s) } - return result + return result.String() } diff --git a/backend/handlers/admin/custom_services_test.go b/backend/handlers/admin/custom_services_test.go index a3fef48..a02cbfb 100644 --- a/backend/handlers/admin/custom_services_test.go +++ b/backend/handlers/admin/custom_services_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package admin @@ -24,8 +23,8 @@ import ( "strings" "testing" - "crussell/testutils" "crussell/mw" + "crussell/testutils" "crussell/testutils/fixtures" "github.com/go-chi/chi/v5" diff --git a/backend/handlers/admin/discount_campaigns.go b/backend/handlers/admin/discount_campaigns.go index f3a9250..5ce42b3 100644 --- a/backend/handlers/admin/discount_campaigns.go +++ b/backend/handlers/admin/discount_campaigns.go @@ -195,7 +195,6 @@ func GetDiscountCampaigns(w http.ResponseWriter, r *http.Request) { return } - w.WriteHeader(http.StatusOK) if campaigns == nil { @@ -384,7 +383,6 @@ func CreateDiscountCampaign(w http.ResponseWriter, r *http.Request) { campaign.CreatedBy = &createdByDB.String } - w.WriteHeader(http.StatusCreated) if err := json.NewEncoder(w).Encode(campaign); err != nil { log.Printf("Error encoding campaign: %v", err) @@ -424,7 +422,7 @@ func UpdateDiscountCampaign(w http.ResponseWriter, r *http.Request) { // Build dynamic update query query := "UPDATE discount_campaigns SET updated_at = NOW()" - args := []interface{}{} + args := []any{} argNum := 1 if req.Name != nil { @@ -603,7 +601,6 @@ func UpdateDiscountCampaign(w http.ResponseWriter, r *http.Request) { campaign.CreatedBy = &createdBy.String } - w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(campaign); err != nil { log.Printf("Error encoding campaign: %v", err) @@ -658,9 +655,8 @@ func DeleteDiscountCampaign(w http.ResponseWriter, r *http.Request) { return } - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "message": "Campaign deleted successfully", "id": campaignID, }) @@ -774,7 +770,6 @@ func GetCampaignStats(w http.ResponseWriter, r *http.Request) { BookingCount: bookingCount, } - w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(stats); err != nil { log.Printf("Error encoding stats: %v", err) diff --git a/backend/handlers/admin/discount_campaigns_test.go b/backend/handlers/admin/discount_campaigns_test.go index bffd73c..7587ef3 100644 --- a/backend/handlers/admin/discount_campaigns_test.go +++ b/backend/handlers/admin/discount_campaigns_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package admin @@ -15,8 +14,8 @@ import ( "crussell/clock" "crussell/db" - "crussell/testutils" "crussell/mw" + "crussell/testutils" "crussell/testutils/fixtures" "github.com/go-chi/chi/v5" @@ -24,7 +23,6 @@ import ( var testAdminID string - func makeCampaignRequest(handler http.Handler, method, path string, body interface{}, ctx context.Context, adminID string) *httptest.ResponseRecorder { var req *http.Request if body != nil { diff --git a/backend/handlers/admin/patch_tests.go b/backend/handlers/admin/patch_tests.go index c6ed710..afe1117 100644 --- a/backend/handlers/admin/patch_tests.go +++ b/backend/handlers/admin/patch_tests.go @@ -138,7 +138,7 @@ func UpdatePatchTest(w http.ResponseWriter, r *http.Request) { } query := "UPDATE patch_tests SET " - args := []interface{}{} + args := []any{} i := 1 if req.Name != nil { diff --git a/backend/handlers/admin/patch_tests_test.go b/backend/handlers/admin/patch_tests_test.go index ff9e614..dcf6f2b 100644 --- a/backend/handlers/admin/patch_tests_test.go +++ b/backend/handlers/admin/patch_tests_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package admin diff --git a/backend/handlers/admin/services_test.go b/backend/handlers/admin/services_test.go index 2f4fc3b..b54b730 100644 --- a/backend/handlers/admin/services_test.go +++ b/backend/handlers/admin/services_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package admin @@ -19,9 +18,9 @@ import ( "net/http" "testing" - "crussell/testutils" "crussell/handlers/services" "crussell/mw" + "crussell/testutils" ) // TestAdminServices_Create verifies that an admin can create a new service diff --git a/backend/handlers/admin/settings.go b/backend/handlers/admin/settings.go index 9a1300a..43cad2d 100644 --- a/backend/handlers/admin/settings.go +++ b/backend/handlers/admin/settings.go @@ -8,6 +8,7 @@ import ( "net/http" "net/url" "strconv" + "strings" ) type BusinessSettings struct { @@ -67,7 +68,6 @@ func GetPublicBusinessInfo(w http.ResponseWriter, r *http.Request) { return } - json.NewEncoder(w).Encode(info) } @@ -90,7 +90,6 @@ func GetBusinessSettings(w http.ResponseWriter, r *http.Request) { return } - json.NewEncoder(w).Encode(s) } @@ -172,7 +171,7 @@ func UpdateBusinessSettings(w http.ResponseWriter, r *http.Request) { } setClauses := []string{} - args := []interface{}{} + args := []any{} argIdx := 1 if req.BusinessName != nil { @@ -231,12 +230,13 @@ func UpdateBusinessSettings(w http.ResponseWriter, r *http.Request) { return } - query := "UPDATE business_settings SET " + var query strings.Builder + query.WriteString("UPDATE business_settings SET ") for i, clause := range setClauses { if i > 0 { - query += ", " + query.WriteString(", ") } - query += clause + query.WriteString(clause) } tx, err := db.Conn.Begin(r.Context()) @@ -247,7 +247,7 @@ func UpdateBusinessSettings(w http.ResponseWriter, r *http.Request) { } defer tx.Rollback(r.Context()) - _, err = tx.Exec(r.Context(), query, args...) + _, err = tx.Exec(r.Context(), query.String(), args...) if err != nil { log.Printf("Failed to update business settings: %v", err) http.Error(w, "Failed to update settings", http.StatusInternalServerError) diff --git a/backend/handlers/admin/settings_test.go b/backend/handlers/admin/settings_test.go index 25608cb..041faea 100644 --- a/backend/handlers/admin/settings_test.go +++ b/backend/handlers/admin/settings_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package admin diff --git a/backend/handlers/admin/test_helpers.go b/backend/handlers/admin/test_helpers.go index 6ccc100..1259c1f 100644 --- a/backend/handlers/admin/test_helpers.go +++ b/backend/handlers/admin/test_helpers.go @@ -1,5 +1,4 @@ //go:build test -// +build test package admin diff --git a/backend/handlers/admin/testmain_test.go b/backend/handlers/admin/testmain_test.go index 038c45d..8b4dd53 100644 --- a/backend/handlers/admin/testmain_test.go +++ b/backend/handlers/admin/testmain_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package admin diff --git a/backend/handlers/admin/today_test.go b/backend/handlers/admin/today_test.go index 4548304..d6108f4 100644 --- a/backend/handlers/admin/today_test.go +++ b/backend/handlers/admin/today_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package admin @@ -24,10 +23,10 @@ import ( "time" "crussell/clock" - "crussell/testutils" "crussell/handlers/notifications" "crussell/handlers/today" "crussell/mw" + "crussell/testutils" ) // TestAdminToday_CurrentNext verifies that an admin can retrieve the currently diff --git a/backend/handlers/admin/update_booking_services_test.go b/backend/handlers/admin/update_booking_services_test.go index ae5eaee..40dc350 100644 --- a/backend/handlers/admin/update_booking_services_test.go +++ b/backend/handlers/admin/update_booking_services_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package admin @@ -22,8 +21,8 @@ import ( "crussell/clock" "crussell/db" - "crussell/testutils" "crussell/handlers/bookings" + "crussell/testutils" "crussell/testutils/fixtures" ) @@ -80,7 +79,6 @@ func createSecondService(t *testing.T, tx db.Querier, ctx context.Context, name func TestAdminBookings_UpdateServices_ReplaceServices(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - _, err := fixtures.CreateTestAdminUser(tx) if err != nil { @@ -129,7 +127,6 @@ func TestAdminBookings_UpdateServices_ReplaceServices(t *testing.T) { func TestAdminBookings_UpdateServices_AddService(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - _, err := fixtures.CreateTestAdminUser(tx) if err != nil { @@ -175,7 +172,6 @@ func TestAdminBookings_UpdateServices_AddService(t *testing.T) { func TestAdminBookings_UpdateServices_RemoveService(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - _, err := fixtures.CreateTestAdminUser(tx) if err != nil { @@ -233,7 +229,6 @@ func TestAdminBookings_UpdateServices_RemoveService(t *testing.T) { func TestAdminBookings_UpdateServices_WithPriceOverride(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - _, err := fixtures.CreateTestAdminUser(tx) if err != nil { @@ -286,7 +281,6 @@ func TestAdminBookings_UpdateServices_WithPriceOverride(t *testing.T) { func TestAdminBookings_UpdateServices_WithDurationOverride(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - _, err := fixtures.CreateTestAdminUser(tx) if err != nil { @@ -339,7 +333,6 @@ func TestAdminBookings_UpdateServices_WithDurationOverride(t *testing.T) { func TestAdminBookings_UpdateServices_WithBothOverrides(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - _, err := fixtures.CreateTestAdminUser(tx) if err != nil { @@ -393,7 +386,6 @@ func TestAdminBookings_UpdateServices_WithBothOverrides(t *testing.T) { func TestAdminBookings_UpdateServices_UpdateNotes(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - _, err := fixtures.CreateTestAdminUser(tx) if err != nil { @@ -438,7 +430,6 @@ func TestAdminBookings_UpdateServices_UpdateNotes(t *testing.T) { func TestAdminBookings_UpdateServices_MultipleOverrides(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - _, err := fixtures.CreateTestAdminUser(tx) if err != nil { @@ -881,7 +872,6 @@ func TestAdminBookings_UpdateServices_WeCancelledBookingRejected(t *testing.T) { func TestAdminBookings_UpdateServices_OverlapWithNextBooking(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - _, err := fixtures.CreateTestAdminUser(tx) if err != nil { @@ -927,7 +917,6 @@ func TestAdminBookings_UpdateServices_OverlapWithNextBooking(t *testing.T) { func TestAdminBookings_UpdateServices_NoOverlapSucceeds(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - _, err := fixtures.CreateTestAdminUser(tx) if err != nil { @@ -972,7 +961,6 @@ func TestAdminBookings_UpdateServices_NoOverlapSucceeds(t *testing.T) { func TestAdminBookings_UpdateServices_NoNextBookingSucceeds(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - _, err := fixtures.CreateTestAdminUser(tx) if err != nil { @@ -1014,7 +1002,6 @@ func TestAdminBookings_UpdateServices_NoNextBookingSucceeds(t *testing.T) { func TestAdminBookings_UpdateServices_ResponseShape(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - _, err := fixtures.CreateTestAdminUser(tx) if err != nil { @@ -1082,7 +1069,6 @@ func TestAdminBookings_UpdateServices_ResponseShape(t *testing.T) { func TestAdminBookings_UpdateServices_PendingBooking(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - _, err := fixtures.CreateTestAdminUser(tx) if err != nil { @@ -1117,7 +1103,6 @@ func TestAdminBookings_UpdateServices_PendingBooking(t *testing.T) { func TestAdminBookings_UpdateServices_InProgressBooking(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - _, err := fixtures.CreateTestAdminUser(tx) if err != nil { @@ -1152,7 +1137,6 @@ func TestAdminBookings_UpdateServices_InProgressBooking(t *testing.T) { func TestAdminBookings_UpdateServices_ClearNotes(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - _, err := fixtures.CreateTestAdminUser(tx) if err != nil { diff --git a/backend/handlers/admin/users_test.go b/backend/handlers/admin/users_test.go index 73bc1a2..a14ccbb 100644 --- a/backend/handlers/admin/users_test.go +++ b/backend/handlers/admin/users_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package admin @@ -21,9 +20,9 @@ import ( "testing" "time" - "crussell/testutils" "crussell/handlers/user" "crussell/mw" + "crussell/testutils" ) // TestAdminUsers_List verifies that an admin can list all users in the diff --git a/backend/handlers/auth/auth_test.go b/backend/handlers/auth/auth_test.go index ca827ce..4803a7f 100644 --- a/backend/handlers/auth/auth_test.go +++ b/backend/handlers/auth/auth_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package auth @@ -357,7 +356,6 @@ func TestLogin_Success(t *testing.T) { } defer fixtures.DeleteUser(tx, userID) - body := LoginRequest{ Email: "user@test.com", Password: "testpassword123", @@ -1396,7 +1394,6 @@ func TestLoginResponse_IncludesJTI(t *testing.T) { } defer fixtures.DeleteUser(tx, userID) - body := LoginRequest{ Email: "jti-test@test.com", Password: "testpassword123", @@ -1599,7 +1596,6 @@ func TestLogin_AccountLockout_ResetsOnSuccess(t *testing.T) { t.Fatalf("failed to set failed_attempts: %v", err) } - body := LoginRequest{ Email: "lockout-reset@test.com", Password: "testpassword123", @@ -1764,7 +1760,6 @@ func TestLogin_ResponseIncludesRefreshToken(t *testing.T) { } defer fixtures.DeleteUser(tx, userID) - body := LoginRequest{ Email: "refresh-check@test.com", Password: "testpassword123", @@ -1933,9 +1928,9 @@ func TestCleanupStaleLoginEntries_Mixed(t *testing.T) { loginStateMu.Lock() saved := loginInProgress loginInProgress = map[string]time.Time{ - "stale-user": clock.Now().Add(-60 * time.Second), - "recent-user": clock.Now().Add(-5 * time.Second), - "borderline": clock.Now().Add(-29 * time.Second), // Just under 30s threshold + "stale-user": clock.Now().Add(-60 * time.Second), + "recent-user": clock.Now().Add(-5 * time.Second), + "borderline": clock.Now().Add(-29 * time.Second), // Just under 30s threshold } loginStateMu.Unlock() defer func() { diff --git a/backend/handlers/auth/testmain_test.go b/backend/handlers/auth/testmain_test.go index 93bb8bc..1f2f6d8 100644 --- a/backend/handlers/auth/testmain_test.go +++ b/backend/handlers/auth/testmain_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package auth diff --git a/backend/handlers/bookings/admin_cancel_reservation_test.go b/backend/handlers/bookings/admin_cancel_reservation_test.go index 052939a..05af69a 100644 --- a/backend/handlers/bookings/admin_cancel_reservation_test.go +++ b/backend/handlers/bookings/admin_cancel_reservation_test.go @@ -1,5 +1,4 @@ //go:build test && dev -// +build test,dev package bookings diff --git a/backend/handlers/bookings/admin_reserve_test.go b/backend/handlers/bookings/admin_reserve_test.go index e24b774..a7de0bf 100644 --- a/backend/handlers/bookings/admin_reserve_test.go +++ b/backend/handlers/bookings/admin_reserve_test.go @@ -1,5 +1,4 @@ //go:build test && dev -// +build test,dev package bookings @@ -20,8 +19,8 @@ import ( "time" "crussell/clock" - "crussell/testutils" "crussell/mw" + "crussell/testutils" "crussell/testutils/fixtures" "github.com/go-chi/chi/v5" @@ -68,7 +67,6 @@ func TestAdminReserveSlot_WalkIn_Success(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - adminID, err := fixtures.CreateTestAdminUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) @@ -126,7 +124,6 @@ func TestAdminReserveSlot_CallIn_Success(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - adminID, err := fixtures.CreateTestAdminUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) @@ -202,8 +199,6 @@ func TestAdminReserveSlot_WalkIn_MissingDuration(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - - adminID, err := fixtures.CreateTestAdminUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) @@ -241,8 +236,6 @@ func TestAdminReserveSlot_CallIn_MissingServices(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - - adminID, err := fixtures.CreateTestAdminUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) @@ -283,8 +276,6 @@ func TestAdminReserveSlot_InvalidReservationType(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - - adminID, err := fixtures.CreateTestAdminUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) @@ -321,8 +312,6 @@ func TestAdminReserveSlot_SlotOverlap(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - - // Create test admin user (for the booking) adminID, err := fixtures.CreateTestAdminUser(tx) if err != nil { @@ -394,8 +383,6 @@ func TestAdminReserveSlot_ReplacesExisting(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - - adminID, err := fixtures.CreateTestAdminUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) @@ -483,8 +470,6 @@ func TestAdminReserveSlot_WalkIn_PastStart(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - - adminID, err := fixtures.CreateTestAdminUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) diff --git a/backend/handlers/bookings/bookings.go b/backend/handlers/bookings/bookings.go index 6642af4..7410503 100644 --- a/backend/handlers/bookings/bookings.go +++ b/backend/handlers/bookings/bookings.go @@ -2,8 +2,8 @@ package bookings import ( "context" - "crussell/db" "crussell/clock" + "crussell/db" "crussell/handlers/notifications" "crussell/handlers/payments" "crussell/handlers/scheduling" @@ -254,20 +254,20 @@ type AdminBookingSummary struct { } type UserSummary struct { - ID string `json:"id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - FullName string `json:"full_name"` - Email *string `json:"email,omitempty"` - Phone *string `json:"phone,omitempty"` - ProfilePicURL *string `json:"profile_pic_url,omitempty"` - DateOfBirth *string `json:"date_of_birth,omitempty"` - AccountRole string `json:"account_role"` - LoyaltyStamps *int `json:"loyalty_stamps,omitempty"` - ReferralCode *string `json:"referral_code,omitempty"` - ReferralCodeUses *int `json:"referral_code_uses,omitempty"` - CreatedAt string `json:"created_at"` - Notes *string `json:"notes,omitempty" validate:"omitempty,max=1000000"` + ID string `json:"id"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + FullName string `json:"full_name"` + Email *string `json:"email,omitempty"` + Phone *string `json:"phone,omitempty"` + ProfilePicURL *string `json:"profile_pic_url,omitempty"` + DateOfBirth *string `json:"date_of_birth,omitempty"` + AccountRole string `json:"account_role"` + LoyaltyStamps *int `json:"loyalty_stamps,omitempty"` + ReferralCode *string `json:"referral_code,omitempty"` + ReferralCodeUses *int `json:"referral_code_uses,omitempty"` + CreatedAt string `json:"created_at"` + Notes *string `json:"notes,omitempty" validate:"omitempty,max=1000000"` PreviousFirstName *string `json:"previous_first_name,omitempty"` PreviousLastName *string `json:"previous_last_name,omitempty"` } @@ -387,7 +387,7 @@ func GetAllUserBookingsHandler(w http.ResponseWriter, r *http.Request) { // Build WHERE conditions shared between count and data queries. whereClause := " WHERE b.user_id = $1" - whereArgs := []interface{}{userID} + whereArgs := []any{userID} paramCount := 2 if req.Status != nil { @@ -447,7 +447,7 @@ func GetAllUserBookingsHandler(w http.ResponseWriter, r *http.Request) { WHERE booking_id = b.id ) pt ON true` + whereClause - dataArgs := make([]interface{}, len(whereArgs)) + dataArgs := make([]any, len(whereArgs)) copy(dataArgs, whereArgs) dataParamCount := paramCount @@ -683,7 +683,7 @@ func GetAllAdminBookingsHandler(w http.ResponseWriter, r *http.Request) { ) pre_pay ON true ` - var args []interface{} + var args []any paramCount := 1 addWhereClause := func(condition string) { @@ -750,7 +750,7 @@ func GetAllAdminBookingsHandler(w http.ResponseWriter, r *http.Request) { var total int { countWhere := "" - var countArgs []interface{} + var countArgs []any cp := 1 addCountWhere := func(cond string) { if cp == 1 { @@ -954,7 +954,7 @@ func GetAllBookingsByUserHandler(w http.ResponseWriter, r *http.Request) { FROM bookings b WHERE b.user_id = $1 ` - var args []interface{} + var args []any args = append(args, userID) paramCount := 2 @@ -1793,7 +1793,7 @@ func SearchAdminBookingsHandler(w http.ResponseWriter, r *http.Request) { WHERE bcs.booking_id = b.id AND cs.name ILIKE $1 ESCAPE '\' ) ` - var args []interface{} + var args []any args = append(args, searchPattern) paramCount := 2 @@ -2639,9 +2639,9 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) { if currentStatus == "completed" { log.Printf("Booking %s is already completed — skipping duplicate completion", bookingID) } else { - // Collect patch test IDs first so the rows are consumed before INSERT operations. - var patchTestIDs []string - ptRows, err := tx.Query(r.Context(), ` + // Collect patch test IDs first so the rows are consumed before INSERT operations. + var patchTestIDs []string + ptRows, err := tx.Query(r.Context(), ` SELECT DISTINCT pt.id FROM patch_tests pt JOIN booking_services bs ON bs.booking_id = $1 @@ -2649,42 +2649,42 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) { SELECT pt_inner.id FROM patch_tests pt_inner WHERE bs.service_id = ANY(pt_inner.service_ids) ) `, bookingID) - if err != nil { - log.Printf("Failed to fetch patch tests for booking %s: %v", bookingID, err) - } else { - for ptRows.Next() { - var ptID string - if err := ptRows.Scan(&ptID); err == nil { - patchTestIDs = append(patchTestIDs, ptID) + if err != nil { + log.Printf("Failed to fetch patch tests for booking %s: %v", bookingID, err) + } else { + for ptRows.Next() { + var ptID string + if err := ptRows.Scan(&ptID); err == nil { + patchTestIDs = append(patchTestIDs, ptID) + } } + ptRows.Close() } - ptRows.Close() - } - for _, ptID := range patchTestIDs { - if _, err := tx.Exec(r.Context(), ` + for _, ptID := range patchTestIDs { + if _, err := tx.Exec(r.Context(), ` INSERT INTO user_patch_tests (user_id, patch_test_id, tested_at) VALUES ($1, $2, NOW()) ON CONFLICT (user_id, patch_test_id) DO UPDATE SET tested_at = NOW() `, booking.User.ID, ptID); err != nil { - log.Printf("Failed to update patch test validity for user %s, patch test %s: %v", booking.User.ID, ptID, err) + log.Printf("Failed to update patch test validity for user %s, patch test %s: %v", booking.User.ID, ptID, err) + } } - } - var bookingTotal float64 - if err := tx.QueryRow(r.Context(), ` + var bookingTotal float64 + if err := tx.QueryRow(r.Context(), ` SELECT total_amount FROM bookings WHERE id = $1 `, bookingID).Scan(&bookingTotal); err != nil { - log.Printf("Failed to calculate booking total for %s: %v", bookingID, err) - } + log.Printf("Failed to calculate booking total for %s: %v", bookingID, err) + } - // 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) + // 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) - var newStampCount int - if bookingTotal > 0 && !loyaltyAppliedOnThisBooking { - if err := tx.QueryRow(r.Context(), ` + var newStampCount int + if bookingTotal > 0 && !loyaltyAppliedOnThisBooking { + if err := tx.QueryRow(r.Context(), ` UPDATE users SET loyalty_stamps = loyalty_stamps + 1 WHERE id = $1 @@ -2697,108 +2697,108 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) { ) RETURNING loyalty_stamps `, booking.User.ID, bookingID).Scan(&newStampCount); err != nil { - if !errors.Is(err, pgx.ErrNoRows) { - log.Printf("Failed to add loyalty stamp for booking %s: %v", bookingID, err) + if !errors.Is(err, pgx.ErrNoRows) { + log.Printf("Failed to add loyalty stamp for booking %s: %v", bookingID, err) + } } } - } - // Create pending redemption when stamps reach LoyaltyStampCost - if newStampCount == payments.LoyaltyStampCost { - _, err = tx.Exec(r.Context(), ` + // Create pending redemption when stamps reach LoyaltyStampCost + if newStampCount == payments.LoyaltyStampCost { + _, err = tx.Exec(r.Context(), ` INSERT INTO loyalty_redemptions (user_id, stamps_redeemed, status, redeemed_at) VALUES ($1, $2, 'pending', NOW()) `, booking.User.ID, payments.LoyaltyStampCost) - if err != nil { - log.Printf("Failed to create loyalty redemption for user %s: %v", booking.User.ID, err) + if err != nil { + log.Printf("Failed to create loyalty redemption for user %s: %v", booking.User.ID, err) + } } - } - // 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) - if bookingTotal > 0 && !timeBasedApplied { - var campaignID string - var campaignPercent float64 - if err := tx.QueryRow(r.Context(), ` + // 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) + if bookingTotal > 0 && !timeBasedApplied { + var campaignID string + var campaignPercent float64 + if err := tx.QueryRow(r.Context(), ` SELECT id, discount_percent FROM discount_campaigns WHERE status = 'active' AND campaign_type = 'time_based' AND start_date <= NOW() AND end_date >= NOW() AND (max_redemptions IS NULL OR times_redeemed < max_redemptions) ORDER BY discount_percent DESC LIMIT 1 `).Scan(&campaignID, &campaignPercent); err == nil && campaignID != "" { - discountAmount := roundTo2(bookingTotal * campaignPercent / 100) + discountAmount := roundTo2(bookingTotal * campaignPercent / 100) - if _, err := tx.Exec(r.Context(), ` + if _, err := tx.Exec(r.Context(), ` 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, 'campaign', $3, 'time_based', NULL, $4, $5, $6) `, bookingID, booking.User.ID, campaignID, campaignPercent, bookingTotal, discountAmount); err != nil { - log.Printf("ALERT: failed to insert booking discount: %v", err) - } + log.Printf("ALERT: failed to insert booking discount: %v", err) + } - if _, err := tx.Exec(r.Context(), ` + if _, err := tx.Exec(r.Context(), ` INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_by) VALUES ($1, 'partial', 'discount', $2, 'completed', $3) `, bookingID, discountAmount, booking.User.ID); err != nil { - log.Printf("ALERT: failed to insert payment record: %v", err) - } + log.Printf("ALERT: failed to insert payment record: %v", err) + } - if _, err := tx.Exec(r.Context(), ` + if _, err := tx.Exec(r.Context(), ` UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = $1 `, campaignID); err != nil { - log.Printf("ALERT: failed to update discount campaign usage: %v", err) + log.Printf("ALERT: failed to update discount campaign usage: %v", err) + } } } - } - if bookingTotal > 0 { - var userBookingCount int - _ = tx.QueryRow(r.Context(), `SELECT COUNT(*) FROM bookings WHERE user_id = $1 AND status = 'completed'`, booking.User.ID).Scan(&userBookingCount) + if bookingTotal > 0 { + var userBookingCount int + _ = 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 - _ = tx.QueryRow(r.Context(), ` + var milestoneCampaignID string + var milestonePercent float64 + _ = 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' AND milestone_value = $1 AND NOT EXISTS (SELECT 1 FROM booking_discounts WHERE user_id = $2 AND source_id = discount_campaigns.id) `, userBookingCount, booking.User.ID).Scan(&milestoneCampaignID, &milestonePercent) - if milestoneCampaignID != "" { - discountAmount := roundTo2(bookingTotal * milestonePercent / 100) - if _, err := tx.Exec(r.Context(), ` + if milestoneCampaignID != "" { + discountAmount := roundTo2(bookingTotal * milestonePercent / 100) + if _, err := tx.Exec(r.Context(), ` 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, 'campaign', $3, 'milestone', 'per_user_booking_count', $4, $5, $6) `, bookingID, booking.User.ID, milestoneCampaignID, milestonePercent, bookingTotal, discountAmount); err != nil { - log.Printf("ALERT: failed to insert booking discount: %v", err) - } - if _, err := tx.Exec(r.Context(), ` + log.Printf("ALERT: failed to insert booking discount: %v", err) + } + if _, err := tx.Exec(r.Context(), ` INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_by) VALUES ($1, 'partial', 'discount', $2, 'completed', $3) `, bookingID, discountAmount, booking.User.ID); err != nil { - log.Printf("ALERT: failed to insert payment record: %v", err) - } - if _, err := tx.Exec(r.Context(), ` + log.Printf("ALERT: failed to insert payment record: %v", err) + } + if _, err := tx.Exec(r.Context(), ` UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = $1 `, milestoneCampaignID); err != nil { - log.Printf("ALERT: failed to insert payment record: %v", err) + log.Printf("ALERT: failed to insert payment record: %v", err) + } } - } - 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) - if !globalMilestoneApplied { - var globalCount int - _ = tx.QueryRow(r.Context(), `SELECT COUNT(*) FROM bookings WHERE status = 'completed'`).Scan(&globalCount) + 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) + if !globalMilestoneApplied { + var globalCount int + _ = tx.QueryRow(r.Context(), `SELECT COUNT(*) FROM bookings WHERE status = 'completed'`).Scan(&globalCount) - var hasInPersonPayment bool - tx.QueryRow(r.Context(), ` + var hasInPersonPayment bool + 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 - _ = tx.QueryRow(r.Context(), ` + if hasInPersonPayment { + var globalCampaignID string + var globalPercent float64 + _ = tx.QueryRow(r.Context(), ` SELECT id, discount_percent FROM discount_campaigns WHERE status = 'active' AND campaign_type = 'milestone' AND milestone_type = 'global_booking_count' AND milestone_value <= $1 @@ -2806,126 +2806,126 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) { ORDER BY milestone_value DESC LIMIT 1 `, globalCount).Scan(&globalCampaignID, &globalPercent) - if globalCampaignID != "" { - discountAmount := roundTo2(bookingTotal * globalPercent / 100) - if _, err := tx.Exec(r.Context(), ` + if globalCampaignID != "" { + discountAmount := roundTo2(bookingTotal * globalPercent / 100) + if _, err := tx.Exec(r.Context(), ` 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, 'campaign', $3, 'milestone', 'global_booking_count', $4, $5, $6) `, bookingID, booking.User.ID, globalCampaignID, globalPercent, bookingTotal, discountAmount); err != nil { - log.Printf("ALERT: failed to insert booking discount: %v", err) - } - if _, err := tx.Exec(r.Context(), ` + log.Printf("ALERT: failed to insert booking discount: %v", err) + } + if _, err := tx.Exec(r.Context(), ` INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_by) VALUES ($1, 'partial', 'discount', $2, 'completed', $3) `, bookingID, discountAmount, booking.User.ID); err != nil { - log.Printf("ALERT: failed to insert payment record: %v", err) - } - if _, err := tx.Exec(r.Context(), ` + log.Printf("ALERT: failed to insert payment record: %v", err) + } + if _, err := tx.Exec(r.Context(), ` UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = $1 `, globalCampaignID); err != nil { - log.Printf("ALERT: failed to insert payment record: %v", err) + log.Printf("ALERT: failed to insert payment record: %v", err) + } } } } - } - var firstVisitDate time.Time - _ = 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(), ` + var firstVisitDate time.Time + _ = 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(), ` SELECT id, discount_percent, milestone_value, milestone_unit FROM discount_campaigns WHERE status = 'active' AND campaign_type = 'milestone' AND milestone_type = 'anniversary' AND NOT EXISTS (SELECT 1 FROM booking_discounts WHERE user_id = $1 AND source_id = discount_campaigns.id AND milestone_type = 'anniversary') `, booking.User.ID) - if err == nil { - // Collect anniversary campaigns first to avoid interleaving rows with writes. - type annCampaign struct { - id string - pct float64 - value int - unit string - } - var campaigns []annCampaign - for annRows.Next() { - var c annCampaign - if annRows.Scan(&c.id, &c.pct, &c.value, &c.unit) == nil { - campaigns = append(campaigns, c) + if err == nil { + // Collect anniversary campaigns first to avoid interleaving rows with writes. + type annCampaign struct { + id string + pct float64 + value int + unit string } - } - annRows.Close() + var campaigns []annCampaign + for annRows.Next() { + var c annCampaign + if annRows.Scan(&c.id, &c.pct, &c.value, &c.unit) == nil { + campaigns = append(campaigns, c) + } + } + annRows.Close() - // Sort by milestone_value descending so we apply the longest anniversary only - sort.Slice(campaigns, func(i, j int) bool { - return campaigns[i].value > campaigns[j].value - }) - for _, c := range campaigns { - var matches bool - elapsed := time.Since(firstVisitDate) - switch c.unit { - case "months": - months := int(elapsed.Hours() / (30 * 24)) - matches = months >= c.value - case "years": - years := int(elapsed.Hours() / (365.25 * 24)) - matches = years >= c.value - } - if matches { - discountAmount := roundTo2(bookingTotal * c.pct / 100) - if _, err := tx.Exec(r.Context(), ` + // Sort by milestone_value descending so we apply the longest anniversary only + sort.Slice(campaigns, func(i, j int) bool { + return campaigns[i].value > campaigns[j].value + }) + for _, c := range campaigns { + var matches bool + elapsed := time.Since(firstVisitDate) + switch c.unit { + case "months": + months := int(elapsed.Hours() / (30 * 24)) + matches = months >= c.value + case "years": + years := int(elapsed.Hours() / (365.25 * 24)) + matches = years >= c.value + } + if matches { + discountAmount := roundTo2(bookingTotal * c.pct / 100) + if _, err := tx.Exec(r.Context(), ` 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, 'campaign', $3, 'milestone', 'anniversary', $4, $5, $6) `, bookingID, booking.User.ID, c.id, c.pct, bookingTotal, discountAmount); err != nil { - log.Printf("ALERT: failed to insert booking discount: %v", err) - } - if _, err := tx.Exec(r.Context(), ` + log.Printf("ALERT: failed to insert booking discount: %v", err) + } + if _, err := tx.Exec(r.Context(), ` INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_by) VALUES ($1, 'partial', 'discount', $2, 'completed', $3) `, bookingID, discountAmount, booking.User.ID); err != nil { - log.Printf("ALERT: failed to insert payment record: %v", err) - } - if _, err := tx.Exec(r.Context(), ` + log.Printf("ALERT: failed to insert payment record: %v", err) + } + if _, err := tx.Exec(r.Context(), ` UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = $1 `, c.id); err != nil { - log.Printf("ALERT: failed to insert payment record: %v", err) + log.Printf("ALERT: failed to insert payment record: %v", err) + } + break // apply longest matching only } - break // apply longest matching only } } } } - } - var paymentExists bool - if err := tx.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM payments WHERE booking_id = $1)`, bookingID).Scan(&paymentExists); err == nil && paymentExists { - var newDepositsRequired int - if err := tx.QueryRow(r.Context(), ` + var paymentExists bool + if err := tx.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM payments WHERE booking_id = $1)`, bookingID).Scan(&paymentExists); err == nil && paymentExists { + var newDepositsRequired int + if err := tx.QueryRow(r.Context(), ` UPDATE users SET deposits_required = GREATEST(0, deposits_required - 1) WHERE id = $1 RETURNING deposits_required `, booking.User.ID).Scan(&newDepositsRequired); err != nil { - log.Printf("ALERT: failed to update deposits_required: %v", err) - } else if newDepositsRequired == 0 { - // After 3 paid bookings, forget no-shows so the counter resets. - if _, err := tx.Exec(r.Context(), ` + log.Printf("ALERT: failed to update deposits_required: %v", err) + } else if newDepositsRequired == 0 { + // After 3 paid bookings, forget no-shows so the counter resets. + if _, err := tx.Exec(r.Context(), ` INSERT INTO forgiven_no_shows (booking_id) SELECT id FROM bookings WHERE user_id = $1 AND status = 'no_show' AND start_time >= NOW() - INTERVAL '6 months' AND NOT EXISTS (SELECT 1 FROM forgiven_no_shows WHERE booking_id = bookings.id) `, booking.User.ID); err != nil { - log.Printf("ALERT: failed to auto-forgive no-shows: %v", err) + log.Printf("ALERT: failed to auto-forgive no-shows: %v", err) + } } } - } - // Consume unconsumed name_history entries — this booking is the "first post-name-change - // booking" that completes. After this, we no longer show "formerly" on displays. - if _, err := tx.Exec(r.Context(), ` + // Consume unconsumed name_history entries — this booking is the "first post-name-change + // booking" that completes. After this, we no longer show "formerly" on displays. + if _, err := tx.Exec(r.Context(), ` UPDATE name_history SET booking_id = $1 WHERE user_id = $2 AND booking_id IS NULL `, bookingID, booking.User.ID); err != nil { - log.Printf("Failed to consume name_history for user %s: %v", booking.User.ID, err) - } - } // close the else from alreadyCompleted check + log.Printf("Failed to consume name_history for user %s: %v", booking.User.ID, err) + } + } // close the else from alreadyCompleted check } if err := tx.Commit(r.Context()); err != nil { @@ -3288,7 +3288,7 @@ func DeleteBookingHandler(w http.ResponseWriter, r *http.Request) { } } - resp := map[string]interface{}{ + resp := map[string]any{ "message": "Booking cancelled successfully", "id": bookingID, "status": req.Reason, @@ -3336,7 +3336,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]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "message": "Booking deleted successfully", "id": bookingID, }) diff --git a/backend/handlers/bookings/bookings_test.go b/backend/handlers/bookings/bookings_test.go index 44c034d..7056df0 100644 --- a/backend/handlers/bookings/bookings_test.go +++ b/backend/handlers/bookings/bookings_test.go @@ -1,5 +1,4 @@ //go:build test && dev -// +build test,dev package bookings @@ -30,10 +29,10 @@ import ( "crussell/clock" "crussell/db" - "crussell/testutils" "crussell/handlers/user" "crussell/internal/validators" "crussell/mw" + "crussell/testutils" "crussell/testutils/fixtures" "crussell/testutils/jwt" @@ -217,7 +216,6 @@ func TestBookings_Create(t *testing.T) { ctx, tx := testutils.SetupTestTx(t) // Seed working hours for booking tests - // Create test user and service userID, err := fixtures.CreateTestUser(tx) @@ -1099,10 +1097,8 @@ func TestBookings_Delete_NotFound(t *testing.T) { // - Cancellation < 24 hours before appointment: treated as no-show (deposits = 3) // - Cancellation >= 24 hours before appointment: treated as late_cancellation func TestBookings_Delete_NoShow24hThreshold(t *testing.T) { - - ctx, tx := testutils.SetupTestTx(t) - + ctx, tx := testutils.SetupTestTx(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -1162,10 +1158,8 @@ func TestBookings_Delete_NoShow24hThreshold(t *testing.T) { // TestBookings_Delete_NoShow_WithForgiveness tests that admin can forgive a no-show func TestBookings_Delete_NoShow_WithForgiveness(t *testing.T) { - - ctx, tx := testutils.SetupTestTx(t) - + ctx, tx := testutils.SetupTestTx(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -1459,7 +1453,6 @@ func TestBookings_Create_MinimumAdvance(t *testing.T) { ctx, tx := testutils.SetupTestTx(t) // Seed working hours for booking tests - userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -1513,7 +1506,6 @@ func TestBookings_Create_WithNotes_StatusPending(t *testing.T) { ctx, tx := testutils.SetupTestTx(t) // Seed working hours - userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -1567,7 +1559,6 @@ func TestBookings_Create_WithoutNotes_StatusConfirmed(t *testing.T) { ctx, tx := testutils.SetupTestTx(t) // Seed working hours - userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -1615,7 +1606,6 @@ func TestBookings_Create_Within1Hour_ShouldFail(t *testing.T) { ctx, tx := testutils.SetupTestTx(t) // Seed working hours - userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -1654,7 +1644,6 @@ func TestBookings_Create_MultipleServices(t *testing.T) { ctx, tx := testutils.SetupTestTx(t) // Seed working hours for booking tests - userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -1715,10 +1704,8 @@ func TestBookings_Create_MultipleServices(t *testing.T) { // booking is cancelled within 24 hours (with no forgiveness), the system // overrides the cancellation to "no_show" and deposits_required stays 0. func TestDeleteBooking_NoShowUnder24h_SetsDepositsTo3(t *testing.T) { - - ctx, tx := testutils.SetupTestTx(t) - + ctx, tx := testutils.SetupTestTx(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -1798,10 +1785,8 @@ func TestDeleteBooking_NoShowUnder24h_SetsDepositsTo3(t *testing.T) { // cancelled with more than 24 hours notice (client_cancelled), no deposit penalty // is applied and deposits_required remains 0. func TestDeleteBooking_CancelOver24h_NoDepositPenalty(t *testing.T) { - - ctx, tx := testutils.SetupTestTx(t) - + ctx, tx := testutils.SetupTestTx(t) // Create test user with deposits_required = 0 userID, err := fixtures.CreateTestUser(tx) @@ -1883,10 +1868,8 @@ func TestDeleteBooking_CancelOver24h_NoDepositPenalty(t *testing.T) { // booking is cancelled within 24 hours with forgiveness, the system overrides to // "client_cancelled" (no no-show penalty). func TestDeleteBooking_NoShowWithForgiveness_NoPenalty(t *testing.T) { - - ctx, tx := testutils.SetupTestTx(t) - + ctx, tx := testutils.SetupTestTx(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -1968,10 +1951,8 @@ func TestDeleteBooking_NoShowWithForgiveness_NoPenalty(t *testing.T) { // no-show, the deposits_required stays at 3 (not 6). The handler sets deposits to 3 // on the first no-show and doesn't increment on subsequent no-shows. func TestDeleteBooking_SecondNoShow_StaysAt3(t *testing.T) { - - ctx, tx := testutils.SetupTestTx(t) - + ctx, tx := testutils.SetupTestTx(t) // Create test user with deposits_required = 0 userID, err := fixtures.CreateTestUser(tx) @@ -2858,8 +2839,6 @@ func TestCreateEditRequest_WithTimeChange(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create test user: %v", err) @@ -3274,8 +3253,6 @@ func TestAdminApproveEditRequest_DeletesTimeBlocker(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create test user: %v", err) @@ -3373,8 +3350,6 @@ func TestAdminRejectEditRequest_DeletesTimeBlocker(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create test user: %v", err) @@ -3471,8 +3446,6 @@ func TestDeleteEditRequest_DeletesTimeBlocker(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create test user: %v", err) @@ -3552,8 +3525,6 @@ func TestAdminApproveEditRequest_TimeBlockerOverlap(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create test user: %v", err) @@ -3765,8 +3736,6 @@ func TestBookings_Create_PatchTestRequired_NoRecord(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - - // Create user and service with patch test requirement userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -3815,8 +3784,6 @@ func TestBookings_Create_PatchTestRequired_WithinNoticePeriod(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create test user: %v", err) @@ -3872,8 +3839,6 @@ func TestBookings_Create_PatchTestRequired_Expired(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create test user: %v", err) @@ -3926,8 +3891,6 @@ func TestBookings_Create_PatchTestRequired_ValidRecord(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create test user: %v", err) @@ -3992,8 +3955,6 @@ func TestBookings_Create_DepositRequired_WithinAdvanceWindow(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create test user: %v", err) @@ -4040,8 +4001,6 @@ func TestBookings_Create_DepositRequired_After48Hours(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create test user: %v", err) @@ -4092,10 +4051,8 @@ func TestBookings_Create_DepositRequired_After48Hours(t *testing.T) { // TestBookings_Create_NoDepositRequired_Within48Hours verifies that a user with deposits_required=0 // can book at any time (no 48h restriction). func TestBookings_Create_NoDepositRequired_Within48Hours(t *testing.T) { - - ctx, tx := testutils.SetupTestTx(t) - + ctx, tx := testutils.SetupTestTx(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -4143,8 +4100,6 @@ func TestBookings_Create_DepositSnapshot(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create test user: %v", err) @@ -4214,8 +4169,6 @@ func TestBookings_Create_DepositRequired_OneActiveBookingLimit(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create test user: %v", err) @@ -4276,8 +4229,6 @@ func TestBookings_Get_DepositFieldsReturned(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create test user: %v", err) @@ -4356,7 +4307,6 @@ func TestBookings_Get_DepositFieldsReturned(t *testing.T) { func TestBookings_Get_ServicesReturned(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -4427,7 +4377,6 @@ func TestBookings_Get_ServicesReturned(t *testing.T) { func TestBookings_Get_CustomServicesReturned(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -4511,7 +4460,6 @@ func TestBookings_Get_CustomServicesReturned(t *testing.T) { func TestBookings_Get_EmptyServices(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -4562,8 +4510,6 @@ func TestBookings_Edit_ClosedDay_UserBlocked(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create test user: %v", err) @@ -4651,8 +4597,6 @@ func TestBookings_Edit_OpenDay_UserAllowed(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create test user: %v", err) @@ -4739,8 +4683,6 @@ func TestBookings_Create_OverlappingBlocker_UserBlocked(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create test user: %v", err) @@ -4760,7 +4702,7 @@ func TestBookings_Create_OverlappingBlocker_UserBlocked(t *testing.T) { defer fixtures.DeleteService(tx, serviceID) // Create a time blocker for a specific time - blockerTime := time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC) + blockerTime := time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC) _, err = tx.Exec(ctx, ` INSERT INTO time_blockers (start_time, duration_minutes, description, created_by) VALUES ($1, 60, 'Staff meeting', NULL) @@ -4808,8 +4750,6 @@ func TestBookings_Edit_OverlappingBlocker_UserBlocked(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create test user: %v", err) @@ -4836,7 +4776,7 @@ func TestBookings_Edit_OverlappingBlocker_UserBlocked(t *testing.T) { defer fixtures.DeleteBooking(tx, bookingID) // Create a time blocker for a specific time - blockerTime := time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC) + blockerTime := time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC) _, err = tx.Exec(ctx, ` INSERT INTO time_blockers (start_time, duration_minutes, description, created_by) VALUES ($1, 60, 'Staff meeting', NULL) @@ -4985,7 +4925,6 @@ func TestGuestUser_Create_RegisteredEmailCollision(t *testing.T) { func TestGuestBooking_Create_Success(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - // Create guest user guestReq := map[string]string{ @@ -5081,7 +5020,6 @@ func TestGuestBooking_Create_NonGuestUserID(t *testing.T) { func TestGuestBooking_SkipsDepositCheck(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - // Create guest user guestReq := map[string]string{ @@ -5123,9 +5061,8 @@ func TestGuestBooking_SkipsDepositCheck(t *testing.T) { } func TestGuestBooking_BypassesAdvanceWindow(t *testing.T) { - + ctx, tx := testutils.SetupTestTx(t) - // Create guest user guestReq := map[string]string{ @@ -5158,8 +5095,6 @@ func TestGuestBooking_BypassesAdvanceWindow(t *testing.T) { } nearTime := midday.Truncate(time.Second) - - req := CreateBookingRequest{ StartTime: nearTime, ServiceIDs: []string{serviceID}, @@ -5181,7 +5116,6 @@ func TestGuestBooking_BypassesAdvanceWindow(t *testing.T) { func TestCreateBooking_Notifications_NewBookingAlwaysCreated(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -5236,7 +5170,6 @@ func TestCreateBooking_Notifications_NewBookingAlwaysCreated(t *testing.T) { func TestCreateBooking_Notifications_PendingBookingWithNotes(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -5303,7 +5236,6 @@ func TestCreateBooking_Notifications_PendingBookingWithNotes(t *testing.T) { func TestCreateBooking_Notifications_NoPendingBookingWithoutNotes(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -6016,7 +5948,6 @@ func TestProgressBooking_DailyStampCap_SQLSubquery(t *testing.T) { func TestCreateBooking_OneHourAdvanceCheck(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -6071,7 +6002,6 @@ func TestCreateBooking_OneHourAdvanceCheck(t *testing.T) { func TestCreateBooking_ActiveBookingLimit(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -6177,7 +6107,6 @@ func TestNextWeekdayHelper(t *testing.T) { func TestCreateBooking_DepositSnapshot(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -6282,7 +6211,6 @@ func TestCreateBooking_DepositSnapshot(t *testing.T) { func TestGetBooking_WithDiscounts(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -6373,7 +6301,6 @@ func createCompletedBookingWithTime(t *testing.T, tx db.Querier, ctx context.Con func TestBookings_Confirm_WithCustomServiceOverrides(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -6495,7 +6422,6 @@ func TestBookings_Confirm_WithCustomServiceOverrides(t *testing.T) { func TestBookings_GetBooking_WithCustomServices(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -6576,7 +6502,6 @@ func TestBookings_GetBooking_WithCustomServices(t *testing.T) { func TestBookings_Confirm_CustomOverrideValidation(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -6665,7 +6590,6 @@ func TestBookings_Confirm_CustomOverrideValidation(t *testing.T) { func TestBookings_Confirm_CustomServiceNotInBooking(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -6744,7 +6668,6 @@ func TestBookings_Confirm_CustomServiceNotInBooking(t *testing.T) { func TestBookings_Progress_WithCustomServices(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -6919,10 +6842,8 @@ func TestDeleteBooking_WithPayments_ProcessesRefund(t *testing.T) { // TestDeleteBooking_NoPayments_HardDelete verifies that when a booking has no // payments, cancelling performs a hard delete (removes the row entirely). func TestDeleteBooking_NoPayments_HardDelete(t *testing.T) { - - ctx, tx := testutils.SetupTestTx(t) - + ctx, tx := testutils.SetupTestTx(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -7066,8 +6987,6 @@ func TestRequestEditHandler_AutoApproves_NoPayments_FarFuture(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create test user: %v", err) @@ -7163,8 +7082,6 @@ func TestRequestEditHandler_AutoApproves_WithTimeChange(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create test user: %v", err) @@ -7241,8 +7158,6 @@ func TestRequestEditHandler_NoAutoApproval_WithPayments(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create test user: %v", err) @@ -7312,8 +7227,6 @@ func TestRequestEditHandler_NoAutoApproval_Within48h(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create test user: %v", err) @@ -7372,7 +7285,6 @@ func TestRequestEditHandler_NoAutoApproval_Within48h(t *testing.T) { func TestCreateEditRequest_DiscountsBlockAutoApprove(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -7441,7 +7353,6 @@ func TestCreateEditRequest_DiscountsBlockAutoApprove(t *testing.T) { func TestCreateEditRequest_NoDiscountsStillAutoApproves(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -7506,7 +7417,6 @@ func TestCreateEditRequest_NoDiscountsStillAutoApproves(t *testing.T) { func TestGetAllUserBookings_CursorNotSetOnLastPage(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -7628,7 +7538,6 @@ func TestGetAllUserBookings_CursorNotSetOnLastPage(t *testing.T) { func TestGetAllUserBookings_TotalCountMatches(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { diff --git a/backend/handlers/bookings/cancel_reservation_test.go b/backend/handlers/bookings/cancel_reservation_test.go index 4abeb55..02e1d51 100644 --- a/backend/handlers/bookings/cancel_reservation_test.go +++ b/backend/handlers/bookings/cancel_reservation_test.go @@ -1,5 +1,4 @@ //go:build test && dev -// +build test,dev package bookings diff --git a/backend/handlers/bookings/closing_time_test.go b/backend/handlers/bookings/closing_time_test.go index 6421f5b..18cc9a2 100644 --- a/backend/handlers/bookings/closing_time_test.go +++ b/backend/handlers/bookings/closing_time_test.go @@ -1,5 +1,4 @@ //go:build test && dev -// +build test,dev package bookings @@ -65,8 +64,8 @@ func TestCheckClosingHours_InvalidFormat(t *testing.T) { localEnd := time.Date(2026, 6, 24, 14, 0, 0, 0, london) tests := []struct { - name string - closeStr string + name string + closeStr string }{ {"empty string", ""}, {"no colon", "1700"}, diff --git a/backend/handlers/bookings/dedup_test.go b/backend/handlers/bookings/dedup_test.go index 7e9e325..977924d 100644 --- a/backend/handlers/bookings/dedup_test.go +++ b/backend/handlers/bookings/dedup_test.go @@ -1,5 +1,4 @@ //go:build test && dev -// +build test,dev package bookings @@ -22,7 +21,6 @@ import ( func setupDedupTest(t *testing.T, tx db.Querier, ctx context.Context) (string, string, string) { t.Helper() - userID := createTestUser(t, 0, tx, ctx) serviceID := createTestService(t, 50.00, tx, ctx) @@ -139,7 +137,6 @@ func TestProgressBooking_UserMilestoneDedup(t *testing.T) { func TestNoShowApplyDepositsIfNeeded(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID := createTestUser(t, 0, tx, ctx) serviceID := createTestService(t, 50.00, tx, ctx) @@ -170,7 +167,6 @@ func TestNoShowApplyDepositsIfNeeded(t *testing.T) { func TestNoShowSingleNoShowDoesNotTrigger(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID := createTestUser(t, 0, tx, ctx) serviceID := createTestService(t, 50.00, tx, ctx) @@ -194,7 +190,6 @@ func TestNoShowSingleNoShowDoesNotTrigger(t *testing.T) { func TestNoShowOldNoShowsExcluded(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID := createTestUser(t, 0, tx, ctx) serviceID := createTestService(t, 50.00, tx, ctx) @@ -222,7 +217,6 @@ func TestNoShowOldNoShowsExcluded(t *testing.T) { func TestNoShowForgivenExcluded(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID := createTestUser(t, 0, tx, ctx) serviceID := createTestService(t, 50.00, tx, ctx) @@ -252,7 +246,6 @@ func TestNoShowForgivenExcluded(t *testing.T) { func TestThreePaidBookingsClearNoShows(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID := createTestUser(t, 0, tx, ctx) serviceID := createTestService(t, 50.00, tx, ctx) diff --git a/backend/handlers/bookings/deposit_test.go b/backend/handlers/bookings/deposit_test.go index 825e157..2b0da66 100644 --- a/backend/handlers/bookings/deposit_test.go +++ b/backend/handlers/bookings/deposit_test.go @@ -1,5 +1,4 @@ //go:build test && dev -// +build test,dev package bookings @@ -14,9 +13,9 @@ import ( "crussell/clock" "crussell/db" - "crussell/testutils" "crussell/handlers/payments" "crussell/mw" + "crussell/testutils" "crussell/testutils/fixtures" "crussell/testutils/jwt" ) diff --git a/backend/handlers/bookings/discount_test.go b/backend/handlers/bookings/discount_test.go index e6b2032..fa20b5c 100644 --- a/backend/handlers/bookings/discount_test.go +++ b/backend/handlers/bookings/discount_test.go @@ -1,5 +1,4 @@ //go:build test && dev -// +build test,dev package bookings @@ -15,9 +14,9 @@ import ( "crussell/clock" "crussell/db" - "crussell/testutils" "crussell/handlers/payments" "crussell/mw" + "crussell/testutils" "crussell/testutils/fixtures" "github.com/go-chi/chi/v5" diff --git a/backend/handlers/bookings/edit_requests_test.go b/backend/handlers/bookings/edit_requests_test.go index 521a277..f50fbaf 100644 --- a/backend/handlers/bookings/edit_requests_test.go +++ b/backend/handlers/bookings/edit_requests_test.go @@ -1,5 +1,4 @@ //go:build test && dev -// +build test,dev package bookings @@ -34,8 +33,8 @@ import ( "crussell/clock" "crussell/db" - "crussell/testutils" "crussell/mw" + "crussell/testutils" "crussell/testutils/fixtures" "crussell/testutils/jwt" @@ -1610,8 +1609,6 @@ func TestAdminApproveEditRequestHandler_WithNotesOnly(t *testing.T) { func TestAdminApproveEditRequestHandler_OverlapWithBooking(t *testing.T) { ctx, tx := testutils.SetupTestTx(t) - - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) @@ -1883,8 +1880,6 @@ func TestUserCancelBookingHandler_CleansUpEditRequest(t *testing.T) { func TestAdminApproveEditRequestHandler_BlockedByExceptionalClosedHours(t *testing.T) { ctx, tx := testutils.SetupTestTx(t) - - _, serviceID, bookingID, token := setupEditRequestTest(t, ctx, tx) _ = serviceID diff --git a/backend/handlers/bookings/manage.go b/backend/handlers/bookings/manage.go index 3a33077..c8437bc 100644 --- a/backend/handlers/bookings/manage.go +++ b/backend/handlers/bookings/manage.go @@ -2,9 +2,8 @@ package bookings import ( "context" - "crussell/db" "crussell/clock" - "github.com/jackc/pgx/v5" + "crussell/db" "crussell/handlers/notifications" "crussell/handlers/payments" "crussell/handlers/scheduling" @@ -14,6 +13,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/jackc/pgx/v5" "log" "net/http" "strings" @@ -279,7 +279,7 @@ func AdminCancelBookingHandler(w http.ResponseWriter, r *http.Request) { } if refundFailed || (refundResult != nil && refundResult.RefundableAmount > 0) { - resp := map[string]interface{}{ + resp := map[string]any{ "message": "Booking cancelled", } if refundResult != nil && refundResult.RefundableAmount > 0 { @@ -932,7 +932,7 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) { } - response := map[string]interface{}{ + response := map[string]any{ "booking": booking, @@ -988,10 +988,10 @@ type EditSnapshot struct { } type EditUserSummary struct { - ID string `json:"id"` - FullName string `json:"full_name"` - Email *string `json:"email,omitempty"` - Phone *string `json:"phone,omitempty"` + ID string `json:"id"` + FullName string `json:"full_name"` + Email *string `json:"email,omitempty"` + Phone *string `json:"phone,omitempty"` PreviousFirstName *string `json:"previous_first_name,omitempty"` PreviousLastName *string `json:"previous_last_name,omitempty"` } @@ -1462,12 +1462,12 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) { durMinutes = 60 } } else { - if err := tx.QueryRow(r.Context(), ` + if err := tx.QueryRow(r.Context(), ` SELECT total_duration_minutes FROM bookings WHERE id = $1 `, bookingID).Scan(&durMinutes); err != nil { - log.Printf("Failed to get duration for existing services: %v", err) - durMinutes = 60 - } + log.Printf("Failed to get duration for existing services: %v", err) + durMinutes = 60 + } } if durMinutes <= 0 { durMinutes = 60 @@ -1504,7 +1504,7 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) { // Update booking start_time, notes, and services directly if req.NewStartTime != nil || req.Notes != nil { var setClauses []string - var args []interface{} + var args []any argNum := 1 if req.NewStartTime != nil { setClauses = append(setClauses, fmt.Sprintf("start_time = $%d", argNum)) @@ -1574,7 +1574,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]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "auto_approved": true, "edit_request": editReq, }) @@ -1684,7 +1684,7 @@ func AdminListEditRequestsHandler(w http.ResponseWriter, r *http.Request) { JOIN users u ON ber.requested_by = u.id ` - var args []interface{} + var args []any baseQuery += " ORDER BY ber.updated_at DESC" @@ -1747,7 +1747,7 @@ func AdminListEditRequestsHandler(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "requests": requests, "total": total, }) @@ -1939,7 +1939,7 @@ func AdminApproveEditRequestHandler(w http.ResponseWriter, r *http.Request) { // setClauses contains only hardcoded column name assignments ("start_time = $N", "notes = $N"). // Column names are never derived from user input. User values are in args and always parameterised. var setClauses []string - var args []interface{} + var args []any argNum := 1 if newStartTime != nil { @@ -2153,7 +2153,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]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "edit_request": nil, }) return @@ -2172,7 +2172,7 @@ func GetMyEditRequestHandler(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "edit_request": enriched, }) } @@ -2236,7 +2236,7 @@ func GetMyEditRequestsHandler(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "edit_requests": enrichedRequests, }) } @@ -2293,7 +2293,7 @@ func AdminListAllEditRequestsHandler(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "edit_requests": enrichedRequests, }) } @@ -2342,7 +2342,7 @@ func AdminGetBookingEditRequestHandler(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "edit_request": enriched, }) } diff --git a/backend/handlers/bookings/overlap_test.go b/backend/handlers/bookings/overlap_test.go index abfb521..fd8bdd2 100644 --- a/backend/handlers/bookings/overlap_test.go +++ b/backend/handlers/bookings/overlap_test.go @@ -1,5 +1,4 @@ //go:build test && dev -// +build test,dev package bookings @@ -1301,7 +1300,7 @@ func TestAdminCreateBooking_Weekday_BST_Boundary(t *testing.T) { // Compute weekStart the same way the handler does: from the booking time's // London weekday, find Monday's date, store as UTC midnight. bkLondon := sunday2330UTC.In(londonLocation) // 00:30 BST Monday - daysToMonday := int(bkLondon.Weekday()) // Monday in Go = 1 + daysToMonday := int(bkLondon.Weekday()) // Monday in Go = 1 if daysToMonday == 0 { daysToMonday = 7 } @@ -2182,10 +2181,10 @@ func TestAdminReserveSlot_CleansUpAnonReservation(t *testing.T) { w := makeIPRequest(http.HandlerFunc(AdminReserveSlotHandler), "POST", "/api/admin/bookings/reserve", &AdminReserveSlotRequest{ - StartTime: future, - ServiceIDs: []string{serviceID}, - DurationMinutes: 60, - ReservationType: "walkin", + StartTime: future, + ServiceIDs: []string{serviceID}, + DurationMinutes: 60, + ReservationType: "walkin", }, token, testIP, adminID, ctx) if w.Code != http.StatusCreated { diff --git a/backend/handlers/bookings/reserve.go b/backend/handlers/bookings/reserve.go index 1e2264c..0108be8 100644 --- a/backend/handlers/bookings/reserve.go +++ b/backend/handlers/bookings/reserve.go @@ -80,8 +80,8 @@ func ReserveSlotHandler(w http.ResponseWriter, r *http.Request) { hasAuth := hasUser && userID != "" if !hasAuth { authHeader := r.Header.Get("Authorization") - if strings.HasPrefix(authHeader, "Bearer ") { - tokenString := strings.TrimPrefix(authHeader, "Bearer ") + if after, ok := strings.CutPrefix(authHeader, "Bearer "); ok { + tokenString := after var err error userID, _, _, err = auth.VerifyToken(tokenString, r.Context()) if err != nil { diff --git a/backend/handlers/bookings/reserve_test.go b/backend/handlers/bookings/reserve_test.go index f5ca538..1c5dd28 100644 --- a/backend/handlers/bookings/reserve_test.go +++ b/backend/handlers/bookings/reserve_test.go @@ -1,5 +1,4 @@ //go:build test && dev -// +build test,dev package bookings diff --git a/backend/handlers/bookings/testmain_test.go b/backend/handlers/bookings/testmain_test.go index ed84da8..6aad8d1 100644 --- a/backend/handlers/bookings/testmain_test.go +++ b/backend/handlers/bookings/testmain_test.go @@ -1,5 +1,4 @@ //go:build test && dev -// +build test,dev package bookings diff --git a/backend/handlers/bookings/trigger_test.go b/backend/handlers/bookings/trigger_test.go index 1fbeeb7..a8fd41b 100644 --- a/backend/handlers/bookings/trigger_test.go +++ b/backend/handlers/bookings/trigger_test.go @@ -1,5 +1,4 @@ //go:build test && dev -// +build test,dev package bookings diff --git a/backend/handlers/notifications/notifications.go b/backend/handlers/notifications/notifications.go index 8f33e91..859068e 100644 --- a/backend/handlers/notifications/notifications.go +++ b/backend/handlers/notifications/notifications.go @@ -201,7 +201,6 @@ func GetNotifications(w http.ResponseWriter, r *http.Request) { NextCursor: nextCursor, } - if err := json.NewEncoder(w).Encode(resp); err != nil { log.Printf("Failed to encode response: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) @@ -222,7 +221,6 @@ func GetUnreadCount(w http.ResponseWriter, r *http.Request) { return } - if err := json.NewEncoder(w).Encode(map[string]int{"count": count}); err != nil { log.Printf("Failed to encode response: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) @@ -269,13 +267,12 @@ func AcknowledgeNotification(w http.ResponseWriter, r *http.Request) { return } - json.NewEncoder(w).Encode(map[string]string{ "status": "ok", }) } -func AcknowledgePendingBookingNotification(tx interface{}, ctx context.Context, bookingID string) error { +func AcknowledgePendingBookingNotification(tx any, ctx context.Context, bookingID string) error { query := ` UPDATE admin_notifications SET acknowledged_at = NOW() @@ -284,7 +281,7 @@ func AcknowledgePendingBookingNotification(tx interface{}, ctx context.Context, // Use type assertion to get the Exec method - pgx.Tx satisfies this interface execer, ok := tx.(interface { - Exec(ctx context.Context, sql string, arguments ...interface{}) (pgconn.CommandTag, error) + Exec(ctx context.Context, sql string, arguments ...any) (pgconn.CommandTag, error) }) if !ok { log.Printf("Warning: cannot acknowledge notification - tx does not satisfy Execer interface for booking %s", bookingID) diff --git a/backend/handlers/notifications/notifications_extended_test.go b/backend/handlers/notifications/notifications_extended_test.go index d0141c9..d89b362 100644 --- a/backend/handlers/notifications/notifications_extended_test.go +++ b/backend/handlers/notifications/notifications_extended_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package notifications @@ -14,8 +13,8 @@ import ( "time" "crussell/db" - "crussell/testutils" "crussell/mw" + "crussell/testutils" "github.com/go-chi/chi/v5" ) @@ -571,5 +570,3 @@ func createNotificationWithBooking(t *testing.T, ctx context.Context, q db.Queri } return notificationID } - - diff --git a/backend/handlers/notifications/notifications_test.go b/backend/handlers/notifications/notifications_test.go index b8037e7..c4b15b3 100644 --- a/backend/handlers/notifications/notifications_test.go +++ b/backend/handlers/notifications/notifications_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package notifications @@ -25,8 +24,8 @@ import ( "time" "crussell/clock" - "crussell/testutils" "crussell/mw" + "crussell/testutils" "crussell/testutils/fixtures" "github.com/go-chi/chi/v5" diff --git a/backend/handlers/notifications/testmain_test.go b/backend/handlers/notifications/testmain_test.go index 1d05e7b..2562600 100644 --- a/backend/handlers/notifications/testmain_test.go +++ b/backend/handlers/notifications/testmain_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package notifications diff --git a/backend/handlers/payments/discount_preview_test.go b/backend/handlers/payments/discount_preview_test.go index 18e7270..c6e1e34 100644 --- a/backend/handlers/payments/discount_preview_test.go +++ b/backend/handlers/payments/discount_preview_test.go @@ -1,5 +1,4 @@ //go:build test && dev -// +build test,dev package payments @@ -13,8 +12,8 @@ import ( "crussell/clock" "crussell/db" - "crussell/testutils" "crussell/mw" + "crussell/testutils" "crussell/testutils/fixtures" "crussell/testutils/jwt" diff --git a/backend/handlers/payments/giftcards.go b/backend/handlers/payments/giftcards.go index 41dec6a..6637ce4 100644 --- a/backend/handlers/payments/giftcards.go +++ b/backend/handlers/payments/giftcards.go @@ -11,8 +11,8 @@ import ( "strings" "time" - "crussell/db" "crussell/clock" + "crussell/db" "crussell/internal/square" "crussell/internal/validators" "crussell/mw" @@ -160,7 +160,7 @@ func GetGiftCards(w http.ResponseWriter, r *http.Request) { } var gcTotal int - var gcListArgs []interface{} + var gcListArgs []any gcListQuery := fmt.Sprintf(` SELECT id, total_funds_added, amount_remaining, created_at, is_inventory @@ -170,7 +170,7 @@ func GetGiftCards(w http.ResponseWriter, r *http.Request) { if searchTerm != "" { searchPattern := "%" + searchTerm + "%" - gcListArgs = []interface{}{searchPattern} + gcListArgs = []any{searchPattern} if cursorStr != "" { cursorCreatedAt, cursorID, err := validators.ParseCursor(cursorStr) @@ -202,7 +202,7 @@ func GetGiftCards(w http.ResponseWriter, r *http.Request) { // transaction (single connection). gcTotal = 0 if whereSQL != "" { - countArgs := []interface{}{} + countArgs := []any{} if searchTerm != "" { countArgs = append(countArgs, "%"+searchTerm+"%") } @@ -243,7 +243,7 @@ func GetGiftCards(w http.ResponseWriter, r *http.Request) { var ubTotal int var ubListQuery string - var ubListArgs []interface{} + var ubListArgs []any if searchTerm != "" { searchPattern := "%" + searchTerm + "%" @@ -258,7 +258,7 @@ func GetGiftCards(w http.ResponseWriter, r *http.Request) { OR u.email ILIKE $1 ORDER BY b.updated_at DESC ` - ubListArgs = []interface{}{searchPattern} + ubListArgs = []any{searchPattern} } else { ubListQuery = ` SELECT @@ -267,7 +267,7 @@ func GetGiftCards(w http.ResponseWriter, r *http.Request) { JOIN users u ON b.user_id = u.id ORDER BY b.updated_at DESC ` - ubListArgs = []interface{}{} + ubListArgs = []any{} } ubRows, err := db.Conn.Query(ctx, ubListQuery, ubListArgs...) @@ -736,7 +736,7 @@ func RedeemGiftCard(w http.ResponseWriter, r *http.Request) { } w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "status": "success", "amount_redeemed": amountRemaining, }) @@ -1065,7 +1065,7 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) { } w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "status": "success", "code": cardID, "amount": amountPounds, @@ -1137,7 +1137,7 @@ func GetExpiredBalances(w http.ResponseWriter, r *http.Request) { balances = []ExpiredBalance{} } - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "expired_balances": balances, "total": len(balances), }) diff --git a/backend/handlers/payments/giftcards_test.go b/backend/handlers/payments/giftcards_test.go index f1bc837..3e710c5 100644 --- a/backend/handlers/payments/giftcards_test.go +++ b/backend/handlers/payments/giftcards_test.go @@ -1,5 +1,4 @@ //go:build test && dev -// +build test,dev package payments @@ -23,7 +22,6 @@ import ( "github.com/jackc/pgx/v5" ) - func TestAdminCreateGiftCard(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) diff --git a/backend/handlers/payments/handlers.go b/backend/handlers/payments/handlers.go index 71cd23f..0d95fc0 100644 --- a/backend/handlers/payments/handlers.go +++ b/backend/handlers/payments/handlers.go @@ -2,8 +2,8 @@ package payments import ( "context" - "crussell/db" "crussell/clock" + "crussell/db" "crussell/internal/square" "crussell/internal/validators" "crussell/mw" @@ -1943,7 +1943,7 @@ func AcquirePaymentLock(w http.ResponseWriter, r *http.Request) { return } - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "status": "locked", "ttl_min": PaymentLockDuration, "bookingID": bookingID, diff --git a/backend/handlers/payments/loyalty.go b/backend/handlers/payments/loyalty.go index 7346922..3893106 100644 --- a/backend/handlers/payments/loyalty.go +++ b/backend/handlers/payments/loyalty.go @@ -150,8 +150,7 @@ func ApplyLoyaltyRedemption(w http.ResponseWriter, r *http.Request) { return } - - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "success": true, "discount_amount": discountAmount, }) diff --git a/backend/handlers/payments/loyalty_test.go b/backend/handlers/payments/loyalty_test.go index e5334b6..8ced6f1 100644 --- a/backend/handlers/payments/loyalty_test.go +++ b/backend/handlers/payments/loyalty_test.go @@ -1,5 +1,4 @@ //go:build test && dev -// +build test,dev package payments @@ -13,8 +12,8 @@ import ( "crussell/clock" "crussell/db" - "crussell/testutils" "crussell/mw" + "crussell/testutils" "crussell/testutils/fixtures" "crussell/testutils/jwt" diff --git a/backend/handlers/payments/payment_status_test.go b/backend/handlers/payments/payment_status_test.go index 5d87755..6ee91b3 100644 --- a/backend/handlers/payments/payment_status_test.go +++ b/backend/handlers/payments/payment_status_test.go @@ -1,5 +1,4 @@ //go:build test && dev -// +build test,dev package payments @@ -11,8 +10,8 @@ import ( "time" "crussell/db" - "crussell/testutils" "crussell/mw" + "crussell/testutils" "crussell/testutils/fixtures" "crussell/testutils/jwt" diff --git a/backend/handlers/payments/payments_test.go b/backend/handlers/payments/payments_test.go index 21bb20f..5aa02b4 100644 --- a/backend/handlers/payments/payments_test.go +++ b/backend/handlers/payments/payments_test.go @@ -1,5 +1,4 @@ //go:build test && dev -// +build test,dev package payments @@ -16,8 +15,8 @@ import ( "crussell/clock" "crussell/db" - "crussell/testutils" "crussell/mw" + "crussell/testutils" "crussell/testutils/fixtures" "crussell/testutils/jwt" diff --git a/backend/handlers/payments/refund_exclude_test.go b/backend/handlers/payments/refund_exclude_test.go index 3f34c77..cb813ee 100644 --- a/backend/handlers/payments/refund_exclude_test.go +++ b/backend/handlers/payments/refund_exclude_test.go @@ -1,5 +1,4 @@ //go:build test && dev -// +build test,dev package payments diff --git a/backend/handlers/payments/refunds_test.go b/backend/handlers/payments/refunds_test.go index 6721b3e..294b3f2 100644 --- a/backend/handlers/payments/refunds_test.go +++ b/backend/handlers/payments/refunds_test.go @@ -1,5 +1,4 @@ //go:build test && dev -// +build test,dev package payments @@ -154,7 +153,7 @@ func TestProcessCancellationRefund_CreatesRefundRecords(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) + userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } @@ -210,7 +209,7 @@ func TestProcessCancellationRefund_NoRefundWhenNotNeeded(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) + userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } @@ -246,7 +245,7 @@ func TestProcessCancellationRefund_NoPaymentsNoop(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) + userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } @@ -289,7 +288,7 @@ func TestProcessCancellationRefund_GiftCardCreditsUserBalance(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) + userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } @@ -377,7 +376,7 @@ func TestProcessCancellationRefund_CashCreditsUserBalance(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) + userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } @@ -432,7 +431,7 @@ func TestProcessCancellationRefund_CardSquareRefundWithoutBalanceCredit(t *testi t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) + userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } @@ -507,7 +506,7 @@ func TestProcessCancellationRefund_DiscountPaymentSkipped(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) + userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } @@ -562,7 +561,7 @@ func TestProcessCancellationRefund_OnTheHousePaymentSkipped(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) + userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } @@ -621,7 +620,7 @@ func TestProcessCancellationRefund_MissingUserID_LogsWarning(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) + userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } @@ -687,7 +686,7 @@ func TestProcessCancellationRefund_GuestGiftcardDoesNotCreditBalance(t *testing. ctx, tx := testutils.SetupTestTx(t) // Create a user and promote them to guest role. - userID, err := fixtures.CreateTestUser(tx) + userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } @@ -758,7 +757,7 @@ func TestProcessCancellationRefund_GuestCashDoesNotCreditBalance(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) + userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } @@ -824,7 +823,7 @@ func TestProcessCancellationRefund_SplitPayment_DeduplicatesSquareRefund(t *test t.Parallel() ctx, tx := testutils.SetupTestTx(t) - userID, err := fixtures.CreateTestUser(tx) + userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } diff --git a/backend/handlers/payments/testmain_test.go b/backend/handlers/payments/testmain_test.go index ce50f0c..4f23455 100644 --- a/backend/handlers/payments/testmain_test.go +++ b/backend/handlers/payments/testmain_test.go @@ -1,5 +1,4 @@ //go:build test && dev -// +build test,dev package payments diff --git a/backend/handlers/payments/till_test.go b/backend/handlers/payments/till_test.go index be20d05..4baf09b 100644 --- a/backend/handlers/payments/till_test.go +++ b/backend/handlers/payments/till_test.go @@ -1,5 +1,4 @@ //go:build test && dev -// +build test,dev package payments @@ -25,7 +24,6 @@ func TestCreateTillSale_OnTheHouse(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - adminID, err := fixtures.CreateTestAdminUser(tx) if err != nil { t.Fatalf("failed to create admin user: %v", err) @@ -104,7 +102,6 @@ func TestCreateTillSale_Idempotency(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - adminID, err := fixtures.CreateTestAdminUser(tx) if err != nil { t.Fatalf("failed to create admin user: %v", err) @@ -192,7 +189,6 @@ func TestCreateTillSale_CreatesGiftCardTransaction(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - adminID, err := fixtures.CreateTestAdminUser(tx) if err != nil { t.Fatalf("failed to create admin user: %v", err) @@ -294,7 +290,6 @@ func TestCreateTillSale_TopupOnRedeemedCard(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) - adminID, err := fixtures.CreateTestAdminUser(tx) if err != nil { t.Fatalf("failed to create admin user: %v", err) diff --git a/backend/handlers/payments/vat_test.go b/backend/handlers/payments/vat_test.go index ad791fa..3e17abb 100644 --- a/backend/handlers/payments/vat_test.go +++ b/backend/handlers/payments/vat_test.go @@ -1,5 +1,4 @@ //go:build test && dev -// +build test,dev package payments diff --git a/backend/handlers/portfolio/images.go b/backend/handlers/portfolio/images.go index aa8af32..011833c 100644 --- a/backend/handlers/portfolio/images.go +++ b/backend/handlers/portfolio/images.go @@ -3,8 +3,8 @@ package portfolio import ( "bytes" "context" - "crussell/db" "crussell/clock" + "crussell/db" "crussell/internal/images" "crussell/internal/s3" "crussell/internal/validators" @@ -165,8 +165,8 @@ func ListImages(w http.ResponseWriter, r *http.Request) { return } - filterClauses := "" - filterArgs := []interface{}{} + var filterClauses strings.Builder + filterArgs := []any{} for key, values := range r.URL.Query() { if len(values) == 0 || values[0] == "" { @@ -190,14 +190,14 @@ func ListImages(w http.ResponseWriter, r *http.Request) { return } - filterClauses += fmt.Sprintf(" AND $%d::text = ANY(tag_names)", len(filterArgs)+1) + filterClauses.WriteString(fmt.Sprintf(" AND $%d::text = ANY(tag_names)", len(filterArgs)+1)) filterArgs = append(filterArgs, category+":"+value) } } } var query string - var args []interface{} + var args []any const formatCols = `, full_avif_url, full_webp_url, full_jpg_url, full_jxl_url, thumb_avif_url, thumb_webp_url, thumb_jpg_url` @@ -226,16 +226,16 @@ func ListImages(w http.ResponseWriter, r *http.Request) { FROM images, unnest(tag_names) as t WHERE %s%s GROUP BY id - `, formatCols, similaritySum, whereClause, filterClauses) + `, formatCols, similaritySum, whereClause, filterClauses.String()) - var cursorArgs []interface{} + var cursorArgs []any if cursorStr != "" { cursorCreatedAt, cursorID, err := validators.ParseCursor(cursorStr) if err != nil { http.Error(w, "Invalid cursor: "+err.Error(), http.StatusBadRequest) return } - cursorArgs = []interface{}{cursorCreatedAt, cursorID} + cursorArgs = []any{cursorCreatedAt, cursorID} havingIdx := len(cleanTags) + len(filterArgs) + 1 query += fmt.Sprintf(" HAVING (created_at, id) < ($%d, $%d)", havingIdx, havingIdx+1) } @@ -244,7 +244,7 @@ func ListImages(w http.ResponseWriter, r *http.Request) { argOffset = len(filterArgs) + len(cleanTags) + len(cursorArgs) query += fmt.Sprintf(" LIMIT $%d", argOffset+1) - queryArgs := make([]interface{}, len(filterArgs)+len(cleanTags)+len(cursorArgs)+1) + queryArgs := make([]any, len(filterArgs)+len(cleanTags)+len(cursorArgs)+1) copy(queryArgs, filterArgs) for i, t := range cleanTags { queryArgs[len(filterArgs)+i] = t @@ -264,9 +264,9 @@ func ListImages(w http.ResponseWriter, r *http.Request) { similarity(t, $%d) as relevance FROM images, unnest(tag_names) as t WHERE 1=1%s AND t ILIKE '%%' || $%d || '%%' - `, formatCols, searchIdx, searchIdx, filterClauses, searchIdx) + `, formatCols, searchIdx, searchIdx, filterClauses.String(), searchIdx) - cursorArgs := []interface{}{} + cursorArgs := []any{} if cursorStr != "" { cursorCreatedAt, cursorID, err := validators.ParseCursor(cursorStr) if err != nil { @@ -280,7 +280,7 @@ func ListImages(w http.ResponseWriter, r *http.Request) { query += " ORDER BY match_priority DESC, relevance DESC, created_at DESC, id DESC" query += fmt.Sprintf(" LIMIT $%d", searchIdx+1+len(cursorArgs)) - queryArgs := make([]interface{}, searchIdx+1+len(cursorArgs)) + queryArgs := make([]any, searchIdx+1+len(cursorArgs)) copy(queryArgs[:argOffset], filterArgs) queryArgs[argOffset] = searchPattern for i, ca := range cursorArgs { @@ -294,7 +294,7 @@ func ListImages(w http.ResponseWriter, r *http.Request) { SELECT id, url, thumbnail_url, tag_names, created_at%s, 0 as match_count, 0.0 as relevance FROM images WHERE 1=1%s - `, formatCols, filterClauses) + `, formatCols, filterClauses.String()) if cursorStr != "" { cursorCreatedAt, cursorID, err := validators.ParseCursor(cursorStr) @@ -310,7 +310,7 @@ func ListImages(w http.ResponseWriter, r *http.Request) { query += " ORDER BY created_at DESC, id DESC" query += fmt.Sprintf(" LIMIT $%d", argOffset+1) - queryArgs := make([]interface{}, len(filterArgs)+1) + queryArgs := make([]any, len(filterArgs)+1) copy(queryArgs, filterArgs) queryArgs[len(filterArgs)] = limit args = queryArgs @@ -394,7 +394,7 @@ func ListTags(w http.ResponseWriter, r *http.Request) { } var query string - var args []interface{} + var args []any // Query tags from images.tag_names column (stored as array) if q != "" { @@ -409,7 +409,7 @@ func ListTags(w http.ResponseWriter, r *http.Request) { ORDER BY tag LIMIT 20 ` - args = []interface{}{q} + args = []any{q} } else { query = ` SELECT DISTINCT tag @@ -484,7 +484,7 @@ func ListFilters(w http.ResponseWriter, r *http.Request) { // Build base query baseQuery := "SELECT DISTINCT id FROM images WHERE 1=1" - args := []interface{}{} + args := []any{} argNum := 1 if tagFilter != "" { @@ -507,7 +507,7 @@ func ListFilters(w http.ResponseWriter, r *http.Request) { // Build category filters for OTHER categories otherFilters := make([]string, 0) - otherArgs := make([]interface{}, len(args)) + otherArgs := make([]any, len(args)) copy(otherArgs, args) otherArgNum := argNum for cat, val := range selectedCategories { @@ -709,7 +709,7 @@ func UploadImage(w http.ResponseWriter, r *http.Request) { tagsStr := r.FormValue("tags") tags := []string{} if tagsStr != "" { - for _, t := range strings.Split(tagsStr, ",") { + for t := range strings.SplitSeq(tagsStr, ",") { if trimmed := strings.TrimSpace(t); trimmed != "" { tags = append(tags, trimmed) } @@ -1034,11 +1034,11 @@ func extractKey(url string) string { // URL format: https://endpoint/bucket/portfolio/1234567890.jpg // Need to return: portfolio/1234567890.jpg // Find the bucket segment: skip past scheme://endpoint/ - idx := strings.Index(url, "://") - if idx == -1 { + _, after, ok := strings.Cut(url, "://") + if !ok { return url } - rest := url[idx+3:] // skip "://" + rest := after // skip "://" // Now rest = "endpoint/bucket/portfolio/1234567890.jpg" // Skip first path segment (endpoint) slashIdx := strings.Index(rest, "/") diff --git a/backend/handlers/portfolio/images_test.go b/backend/handlers/portfolio/images_test.go index fb1f2be..dcec670 100644 --- a/backend/handlers/portfolio/images_test.go +++ b/backend/handlers/portfolio/images_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test // Package portfolio contains tests for portfolio image management endpoints. // @@ -27,8 +26,8 @@ import ( "strings" "testing" - "crussell/testutils" "crussell/mw" + "crussell/testutils" "github.com/go-chi/chi/v5" "github.com/kovidgoyal/imaging" diff --git a/backend/handlers/portfolio/testmain_test.go b/backend/handlers/portfolio/testmain_test.go index 71a7551..1b3166a 100644 --- a/backend/handlers/portfolio/testmain_test.go +++ b/backend/handlers/portfolio/testmain_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package portfolio diff --git a/backend/handlers/scheduling/default-hours.go b/backend/handlers/scheduling/default-hours.go index 9052055..5cda33c 100644 --- a/backend/handlers/scheduling/default-hours.go +++ b/backend/handlers/scheduling/default-hours.go @@ -8,8 +8,8 @@ import ( "strings" "time" - "crussell/db" "crussell/clock" + "crussell/db" "crussell/internal/validators" "crussell/mw" "log" @@ -308,17 +308,17 @@ func isValidTime15Min(t string) bool { } // --- helper: sqlIn generates IN queries dynamically for Postgres --- -func sqlIn(query string, args []int) (string, []interface{}, error) { - inArgs := []interface{}{} - placeholders := "" +func sqlIn(query string, args []int) (string, []any, error) { + inArgs := []any{} + var placeholders strings.Builder for i, arg := range args { if i > 0 { - placeholders += "," + placeholders.WriteString(",") } - placeholders += fmt.Sprintf("$%d", i+1) + placeholders.WriteString(fmt.Sprintf("$%d", i+1)) inArgs = append(inArgs, arg) } - query = fmt.Sprintf(query, placeholders) + query = fmt.Sprintf(query, placeholders.String()) return query, inArgs, nil } diff --git a/backend/handlers/scheduling/scheduled_cleanup_test.go b/backend/handlers/scheduling/scheduled_cleanup_test.go index dd3724a..4921149 100644 --- a/backend/handlers/scheduling/scheduled_cleanup_test.go +++ b/backend/handlers/scheduling/scheduled_cleanup_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package scheduling diff --git a/backend/handlers/scheduling/scheduling_test.go b/backend/handlers/scheduling/scheduling_test.go index 3c8ffbe..9fd1a3f 100644 --- a/backend/handlers/scheduling/scheduling_test.go +++ b/backend/handlers/scheduling/scheduling_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package scheduling @@ -1023,7 +1022,7 @@ func TestScheduling_GetAvailableHours_WithBlocker_NonAdmin(t *testing.T) { ctx, tx := resetTestData(t) // Create a time blocker for 2026-03-16 10:00-11:00 (Monday - an open day) - blockerTime := time.Date(2026, 3, 16, 10, 0, 0, 0, time.UTC) + blockerTime := time.Date(2026, 3, 16, 10, 0, 0, 0, time.UTC) _, err := tx.Exec(ctx, ` INSERT INTO time_blockers (start_time, duration_minutes, description, created_by) VALUES ($1, 60, 'Staff Meeting', NULL) @@ -1091,7 +1090,7 @@ func TestScheduling_GetAvailableHours_WithBlocker_Admin(t *testing.T) { ctx, tx := resetTestData(t) // Create a time blocker for 2026-03-16 10:00-11:00 (Monday - open day) - blockerTime := time.Date(2026, 3, 16, 10, 0, 0, 0, time.UTC) + blockerTime := time.Date(2026, 3, 16, 10, 0, 0, 0, time.UTC) _, err := tx.Exec(ctx, ` INSERT INTO time_blockers (start_time, duration_minutes, description, created_by) VALUES ($1, 60, 'Staff Meeting', NULL) @@ -1309,7 +1308,7 @@ func getWorkingHoursForDate(t *testing.T, ctx context.Context, date string) (sta func TestScheduling_GetAvailableHours_WithBlocker_Admin_MultipleBlockers(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - + // Create two blockers on Tuesday 2026-03-17 (open 09:00-17:00): // 10:00-11:00 (Staff Meeting) and 14:00-15:00 (Training) b1 := time.Date(2026, 3, 17, 10, 0, 0, 0, time.UTC) @@ -1353,7 +1352,7 @@ func TestScheduling_GetAvailableHours_WithBlocker_Admin_MultipleBlockers(t *test func TestScheduling_GetAvailableHours_WithBlocker_Admin_BlockerAndBooking(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - + // Tuesday 2026-03-17 (open 09:00-17:00) // Create a booking at 11:00-12:00 and a blocker at 14:00-15:00 bookingStart := time.Date(2026, 3, 17, 11, 0, 0, 0, time.UTC) @@ -1413,7 +1412,7 @@ func TestScheduling_GetAvailableHours_WithBlocker_Admin_BlockerAndBooking(t *tes func TestScheduling_GetAvailableHours_WithBlocker_Admin_AllDayBlocker(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - + // Tuesday 2026-03-17 (open 09:00-17:00) — block entire open period blockerTime := time.Date(2026, 3, 17, 9, 0, 0, 0, time.UTC) tx.Exec(ctx, `INSERT INTO time_blockers (start_time, duration_minutes, description, created_by) VALUES ($1, 480, 'All day closure', NULL)`, blockerTime) @@ -1440,7 +1439,7 @@ func TestScheduling_GetAvailableHours_WithBlocker_Admin_AllDayBlocker(t *testing func TestScheduling_GetAvailableHours_WithBlocker_Admin_NonOverlappingBlocker(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - + // Tuesday 2026-03-17 (open 09:00-17:00) — blocker at 17:00-18:00 (after close) blockerTime := time.Date(2026, 3, 17, 17, 0, 0, 0, time.UTC) tx.Exec(ctx, `INSERT INTO time_blockers (start_time, duration_minutes, description, created_by) VALUES ($1, 60, 'After hours cleaning', NULL)`, blockerTime) @@ -1476,7 +1475,7 @@ func TestScheduling_GetAvailableHours_WithBlocker_Admin_NonOverlappingBlocker(t func TestScheduling_GetAvailableHours_WithBlocker_Admin_MultiDay(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - + // Blockers on Tue 2026-03-17 10:00-11:00 and Wed 2026-03-18 14:00-15:00 b1 := time.Date(2026, 3, 17, 10, 0, 0, 0, time.UTC) b2 := time.Date(2026, 3, 18, 14, 0, 0, 0, time.UTC) @@ -1525,7 +1524,7 @@ func TestScheduling_GetAvailableHours_WithBlocker_Admin_MultiDay(t *testing.T) { func TestScheduling_GetAvailableHours_WithBlocker_Admin_BoundaryStart(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - + // Tuesday 2026-03-17 (open 09:00-17:00) — blocker at 09:00-10:00 (start of day) blockerTime := time.Date(2026, 3, 17, 9, 0, 0, 0, time.UTC) tx.Exec(ctx, `INSERT INTO time_blockers (start_time, duration_minutes, description, created_by) VALUES ($1, 60, 'Morning setup', NULL)`, blockerTime) @@ -1561,7 +1560,7 @@ func TestScheduling_GetAvailableHours_WithBlocker_Admin_BoundaryStart(t *testing func TestScheduling_GetAvailableHours_WithBlocker_Admin_BoundaryEnd(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - + // Tuesday 2026-03-17 (open 09:00-17:00) — blocker at 16:00-17:00 (end of day) blockerTime := time.Date(2026, 3, 17, 16, 0, 0, 0, time.UTC) tx.Exec(ctx, `INSERT INTO time_blockers (start_time, duration_minutes, description, created_by) VALUES ($1, 60, 'End of day cleanup', NULL)`, blockerTime) @@ -1597,7 +1596,7 @@ func TestScheduling_GetAvailableHours_WithBlocker_Admin_BoundaryEnd(t *testing.T func TestScheduling_GetAvailableHours_WithBlocker_Admin_OutOfHours(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - + // Tuesday 2026-03-17 — blocker at 10:00-11:00 blockerTime := time.Date(2026, 3, 17, 10, 0, 0, 0, time.UTC) tx.Exec(ctx, `INSERT INTO time_blockers (start_time, duration_minutes, description, created_by) VALUES ($1, 60, 'Morning Meeting', NULL)`, blockerTime) @@ -1639,7 +1638,7 @@ func TestScheduling_GetAvailableHours_WithBlocker_Admin_OutOfHours(t *testing.T) func TestScheduling_GetAvailableHours_WithBlocker_NonAdmin_Regression(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - + // Tuesday 2026-03-17 — blocker at 10:00-11:00 blockerTime := time.Date(2026, 3, 17, 10, 0, 0, 0, time.UTC) tx.Exec(ctx, `INSERT INTO time_blockers (start_time, duration_minutes, description, created_by) VALUES ($1, 60, 'Staff Meeting', NULL)`, blockerTime) @@ -1686,7 +1685,7 @@ func TestScheduling_GetAvailableHours_WithBlocker_Admin_Recurring(t *testing.T) // No t.Parallel() — GetAvailableHours cleanup operations can deadlock with // concurrent test transactions on the shared test database. ctx, tx := resetTestData(t) - + // Daily recurring blocker 12:00-13:00 starting Mon 2026-03-16 startTime := time.Date(2026, 3, 16, 12, 0, 0, 0, time.UTC) cronExpr := "0 12 * * *" // Every day at 12:00 @@ -1730,7 +1729,7 @@ func TestScheduling_GetAvailableHours_WithBlocker_Admin_Recurring(t *testing.T) // RESERVATION:admin time_blocker entries are also subtracted from admin slots. func TestScheduling_GetAvailableHours_WithBlocker_Admin_Reservation(t *testing.T) { ctx, tx := resetTestData(t) - + // Create a real admin user to satisfy FK constraint, then simulate a reservation adminID, err := fixtures.CreateTestAdminUser(tx) if err != nil { @@ -1774,7 +1773,7 @@ func TestScheduling_GetAvailableHours_WithBlocker_Admin_Reservation(t *testing.T func TestScheduling_GetAvailableHours_WithBlocker_Admin_Reservation_NonAdmin(t *testing.T) { // Not parallel (see above) ctx, tx := resetTestData(t) - + reservationTime := time.Date(2026, 3, 17, 10, 0, 0, 0, time.UTC) tx.Exec(ctx, ` INSERT INTO time_blockers (start_time, duration_minutes, description, created_by) @@ -1812,7 +1811,7 @@ func TestScheduling_GetAvailableHours_WithBlocker_Admin_Reservation_NonAdmin(t * func TestScheduling_GetAvailableHours_WithBlocker_Admin_OverlappingBlockers(t *testing.T) { ctx, tx := resetTestData(t) - + // Two overlapping blockers on Tue 2026-03-17: 10:00-12:00 and 11:00-13:00 b1 := time.Date(2026, 3, 17, 10, 0, 0, 0, time.UTC) b2 := time.Date(2026, 3, 17, 11, 0, 0, 0, time.UTC) @@ -1854,7 +1853,7 @@ func TestScheduling_GetAvailableHours_WithBlocker_Admin_OverlappingBlockers(t *t func TestScheduling_GetAvailableHours_WithBlocker_Admin_AdjacentBoundaries(t *testing.T) { ctx, tx := resetTestData(t) - + // Tue 2026-03-17: booking 10:00-11:00, blocker 11:00-12:00 (adjacent) bookingStart := time.Date(2026, 3, 17, 10, 0, 0, 0, time.UTC) bookingEnd := bookingStart.Add(60 * time.Minute) @@ -1899,7 +1898,7 @@ func TestScheduling_GetAvailableHours_WithBlocker_Admin_AdjacentBoundaries(t *te func TestScheduling_GetAvailableHours_WithBlocker_Admin_MidnightBlocker(t *testing.T) { ctx, tx := resetTestData(t) - + // Use the first open day found and the following day tueStart, tueEnd, tueOpen := getWorkingHoursForDate(t, ctx, "2026-03-17") wedStart, _, wedOpen := getWorkingHoursForDate(t, ctx, "2026-03-18") @@ -1987,7 +1986,7 @@ func TestScheduling_GetAvailableHours_WithBlocker_Admin_NoBlockers(t *testing.T) func TestScheduling_GetAvailableHours_WithBlocker_Admin_ClosedDayBlocker(t *testing.T) { ctx, tx := resetTestData(t) - + // Sunday 2026-03-22 is closed. Blocker at 10:00-11:00. blockerTime := time.Date(2026, 3, 22, 10, 0, 0, 0, time.UTC) tx.Exec(ctx, `INSERT INTO time_blockers (start_time, duration_minutes, description, created_by) VALUES ($1, 60, 'Sunday Maintenance', NULL)`, blockerTime) @@ -2017,7 +2016,7 @@ func TestScheduling_GetAvailableHours_WithBlocker_Admin_ClosedDayBlocker(t *test func TestScheduling_GetAvailableHours_WithBlocker_Admin_MultiDayRangePartial(t *testing.T) { ctx, tx := resetTestData(t) - + // Blocker only on Tuesday (2026-03-17) at 10:00-11:00 blockerTime := time.Date(2026, 3, 17, 10, 0, 0, 0, time.UTC) tx.Exec(ctx, `INSERT INTO time_blockers (start_time, duration_minutes, description, created_by) VALUES ($1, 60, 'Tue Only Blocker', NULL)`, blockerTime) @@ -2065,7 +2064,7 @@ func TestScheduling_GetAvailableHours_WithBlocker_Admin_MultiDayRangePartial(t * func TestScheduling_GetAvailableHours_WithBlocker_Admin_ExceptionalHours(t *testing.T) { ctx, tx := resetTestData(t) - + // Monday 2026-03-16 is normally CLOSED. Add exceptional hours: 10:00-16:00. // Also add a blocker at 12:00-13:00. // First create the exceptional group @@ -2124,7 +2123,7 @@ func TestScheduling_GetAvailableHours_WithBlocker_Admin_ExceptionalHours(t *test func TestScheduling_GetAvailableHours_WithBlocker_NonAdmin_ClosedDay(t *testing.T) { ctx, tx := resetTestData(t) - + // Sunday 2026-03-22 closed, blocker at 10:00-11:00 blockerTime := time.Date(2026, 3, 22, 10, 0, 0, 0, time.UTC) tx.Exec(ctx, `INSERT INTO time_blockers (start_time, duration_minutes, description, created_by) VALUES ($1, 60, 'Weekend Maintenance', NULL)`, blockerTime) @@ -2163,7 +2162,7 @@ func TestScheduling_GetAvailableHours_WithBlocker_NonAdmin_ClosedDay(t *testing. func TestScheduling_GetAvailableHours_WithBlocker_NonAdmin_BookingAdjacent(t *testing.T) { ctx, tx := resetTestData(t) - + bookingStart := time.Date(2026, 3, 17, 10, 0, 0, 0, time.UTC) bookingEnd := bookingStart.Add(60 * time.Minute) userID, err := fixtures.CreateTestUser(tx) @@ -2417,7 +2416,7 @@ func TestNormalizeTime_Regression_RealWorldFormats(t *testing.T) { // slots from each affected day. func TestScheduling_GetAvailableHours_CrossDayBlocker(t *testing.T) { ctx, tx := resetTestData(t) - + // Use Tue 2026-03-17 and Wed 2026-03-18 — both open days _, tueEnd, tueOpen := getWorkingHoursForDate(t, ctx, "2026-03-17") _, _, wedOpen := getWorkingHoursForDate(t, ctx, "2026-03-18") diff --git a/backend/handlers/scheduling/testmain_test.go b/backend/handlers/scheduling/testmain_test.go index e253dfa..6414141 100644 --- a/backend/handlers/scheduling/testmain_test.go +++ b/backend/handlers/scheduling/testmain_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package scheduling diff --git a/backend/handlers/scheduling/time_blockers_test.go b/backend/handlers/scheduling/time_blockers_test.go index 0fd9d45..b59d3ba 100644 --- a/backend/handlers/scheduling/time_blockers_test.go +++ b/backend/handlers/scheduling/time_blockers_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package scheduling @@ -430,7 +429,6 @@ func TestGetTimeBlockersInRange(t *testing.T) { t.Fatalf("failed to create blockers: %v", err) } - // Query range that includes blocker1 and blocker2 but not blocker3 start := time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC) end := time.Date(2026, 3, 16, 23, 59, 59, 0, time.UTC) @@ -468,7 +466,6 @@ func TestGetTimeBlockersInRange_Empty(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - // Create a blocker blockerTime := time.Date(2026, 3, 15, 10, 0, 0, 0, time.UTC) _, err := tx.Exec(ctx, ` @@ -479,7 +476,6 @@ func TestGetTimeBlockersInRange_Empty(t *testing.T) { t.Fatalf("failed to create blocker: %v", err) } - // Query range with no blockers start := time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC) end := time.Date(2026, 4, 30, 23, 59, 59, 0, time.UTC) @@ -514,7 +510,6 @@ func TestGetTimeBlockersInRange_IncludesRecurring(t *testing.T) { t.Fatalf("failed to create blockers: %v", err) } - // Query range: March 1-31, 2026 start := time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC) end := time.Date(2026, 3, 31, 23, 59, 59, 0, time.UTC) @@ -559,7 +554,6 @@ func TestCleanupOldReservations(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - // Create fixture users for the test oldUserID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -666,7 +660,6 @@ func TestCleanupOldReservations_AdminWalkIn(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - // Create old walk-in reservation (>15 min old) oldTime := clock.Now().Add(-16 * time.Minute) _, err := tx.Exec(ctx, ` @@ -722,7 +715,6 @@ func TestCleanupOldReservations_AdminCallIn(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - // Create old call-in reservation (>15 min old) oldTime := clock.Now().Add(-16 * time.Minute) _, err := tx.Exec(ctx, ` @@ -778,7 +770,6 @@ func TestCleanupOldReservations_MixedTypes(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - // Create old user reservation (>1 hour old) oldUserTime := clock.Now().Add(-2 * time.Hour) _, err := tx.Exec(ctx, ` @@ -925,7 +916,6 @@ func TestGetTimeBlockersInRange_IncludesReservations(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - // Create a regular blocker for tomorrow at 10:00 tomorrow := clock.Now().Add(24 * time.Hour) blockerTime := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 10, 0, 0, 0, time.UTC) @@ -1130,7 +1120,6 @@ func TestAnonymizeStaleGuestAccounts_Exactly6Months(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - // Create guest user guestID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -1183,7 +1172,6 @@ func TestAnonymizeStaleGuestAccounts_ActiveBooking_Skipped(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - // Create guest user guestID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -1240,7 +1228,6 @@ func TestAnonymizeStaleGuestAccounts_NoBookings(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - // Create guest user with no bookings guestID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -1280,7 +1267,6 @@ func TestCleanupExpiredFinancialRecords_PaymentOlderThan7Years(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) @@ -1363,7 +1349,6 @@ func TestCleanupExpiredFinancialRecords_AnonUserWithin1YearBuffer(t *testing.T) t.Parallel() ctx, tx := resetTestData(t) - guestID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) @@ -1434,7 +1419,6 @@ func TestCleanupExpiredFinancialRecords_PaymentOlderThan9Years(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) @@ -1502,7 +1486,6 @@ func TestCleanupExpiredFinancialRecords_AggregationCorrectTotals(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) @@ -1601,7 +1584,6 @@ func TestCleanupExpiredFinancialRecords_Idempotent(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) @@ -1701,7 +1683,6 @@ func TestCleanupExpiredFinancialRecords_ActiveUserWithin7Years(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) @@ -1763,7 +1744,6 @@ func TestCleanupExpiredFinancialRecords_AnonUserBothThresholdsElapsed(t *testing t.Parallel() ctx, tx := resetTestData(t) - guestID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) @@ -1839,7 +1819,6 @@ func TestCleanupExpiredFinancialRecords_RefundAggregatedAndDeleted(t *testing.T) t.Parallel() ctx, tx := resetTestData(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) @@ -1934,7 +1913,6 @@ func TestAnonymizeStaleGuestAccounts(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - // Guest 1: last booking 7 months ago — should be anonymized guest1ID, _ := fixtures.CreateTestUser(tx) tx.Exec(ctx, `UPDATE users SET account_role = 'guest' WHERE id = $1`, guest1ID) @@ -2002,7 +1980,6 @@ func TestCleanupOldReservations_EditRequest(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - // Create old edit_request reservation (>24 hours old) oldTime := clock.Now().Add(-25 * time.Hour) _, err := tx.Exec(ctx, ` @@ -2056,7 +2033,6 @@ func TestCleanupExpiredDeposits_ExpiredConfirmed(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) @@ -2118,7 +2094,6 @@ func TestCleanupExpiredDeposits_ExpiredPending(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) @@ -2182,7 +2157,6 @@ func TestCleanupExpiredDeposits_PaidDepositPreserved(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) @@ -2251,7 +2225,6 @@ func TestCleanupExpiredDeposits_FutureDeadlinePreserved(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) @@ -2313,7 +2286,6 @@ func TestCleanupExpiredGiftCards(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - // Make account_id nullable for this test — CleanupExpiredGiftCards inserts NULL // for unredeemed gift cards (no user account to reference). _, err := tx.Exec(ctx, `ALTER TABLE gift_card_expired_balances ALTER COLUMN account_id DROP NOT NULL`) @@ -2385,7 +2357,6 @@ func TestCleanupExpiredGiftCards_SkipRecentlyUsed(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - // Make account_id nullable for this test — CleanupExpiredGiftCards inserts NULL // for unredeemed gift cards (no user account to reference). _, err := tx.Exec(ctx, `ALTER TABLE gift_card_expired_balances ALTER COLUMN account_id DROP NOT NULL`) @@ -2437,7 +2408,6 @@ func TestCleanupExpiredGiftCards_SkipRedeemed(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - // Make account_id nullable for this test — CleanupExpiredGiftCards inserts NULL // for unredeemed gift cards (no user account to reference). Even though this // card is redeemed, the function may also match other cards; ensure schema allows it. @@ -2498,7 +2468,6 @@ func TestCleanupIdleAccounts_WithBalance(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - // Create a user userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -2567,7 +2536,6 @@ func TestCleanupIdleAccounts_NoBalance(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - // Create a user userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -2603,7 +2571,6 @@ func TestCleanupIdleAccounts_SkipActive(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - // Create a user with recent last_login userID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -2639,7 +2606,6 @@ func TestCleanupIdleAccounts_SkipAdminGuest(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - // Create admin user with old last_login adminID, err := fixtures.CreateTestUser(tx) if err != nil { @@ -2799,7 +2765,7 @@ func TestCleanupOldIdempotencyKeys_ClearsOldPayments(t *testing.T) { } func TestCleanupOldIdempotencyKeys_ClearsOldTillSales(t *testing.T) { - + ctx, tx := resetTestData(t) // Create an admin user for till_sales.created_by diff --git a/backend/handlers/services/services.go b/backend/handlers/services/services.go index f46dc31..21e7f43 100644 --- a/backend/handlers/services/services.go +++ b/backend/handlers/services/services.go @@ -4,12 +4,12 @@ import ( "context" "crussell/clock" "crussell/db" - "github.com/jackc/pgx/v5" "crussell/internal/validators" "crussell/mw" "database/sql" "encoding/json" "errors" + "github.com/jackc/pgx/v5" "net/http" "time" @@ -84,7 +84,7 @@ func ToggleService(w http.ResponseWriter, r *http.Request) { } w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "message": "Service toggled successfully", "id": serviceID, }) @@ -247,7 +247,7 @@ func DeleteServiceHandler(w http.ResponseWriter, r *http.Request) { } w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "message": "Service deleted successfully", "id": serviceID, }) @@ -305,7 +305,6 @@ func ServicesHandler(w http.ResponseWriter, r *http.Request) { return } - w.WriteHeader(http.StatusOK) if services == nil { @@ -402,7 +401,6 @@ func ServicesHandler(w http.ResponseWriter, r *http.Request) { // Combine: eligible + ineligible services = append(services, ineligibleServices...) - w.WriteHeader(http.StatusOK) if services == nil { @@ -509,7 +507,6 @@ func ServicesEligibleForUserHandler(w http.ResponseWriter, r *http.Request) { // Sort and combine: valid first, then grayed out services = append(services, grayedOutServices...) - w.WriteHeader(http.StatusOK) if services == nil { diff --git a/backend/handlers/services/services_test.go b/backend/handlers/services/services_test.go index c2217e3..facb28b 100644 --- a/backend/handlers/services/services_test.go +++ b/backend/handlers/services/services_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test // Package services contains tests for service listing and eligibility endpoints. // @@ -22,8 +21,8 @@ import ( "testing" "crussell/db" - "crussell/testutils" "crussell/handlers/user" + "crussell/testutils" "github.com/go-chi/chi/v5" ) diff --git a/backend/handlers/services/testmain_test.go b/backend/handlers/services/testmain_test.go index 2a9e9a9..9d4d030 100644 --- a/backend/handlers/services/testmain_test.go +++ b/backend/handlers/services/testmain_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package services diff --git a/backend/handlers/today/testmain_test.go b/backend/handlers/today/testmain_test.go index d785650..bd38b7f 100644 --- a/backend/handlers/today/testmain_test.go +++ b/backend/handlers/today/testmain_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package today diff --git a/backend/handlers/today/today.go b/backend/handlers/today/today.go index 612dfac..e875b54 100644 --- a/backend/handlers/today/today.go +++ b/backend/handlers/today/today.go @@ -10,8 +10,8 @@ import ( "strings" "time" - "crussell/db" "crussell/clock" + "crussell/db" "github.com/jackc/pgx/v5" ) @@ -31,11 +31,11 @@ type ServiceInfo struct { } type UserInfo struct { - ID string `json:"id"` - FullName string `json:"full_name"` - Phone *string `json:"phone,omitempty"` - Email *string `json:"email,omitempty"` - ProfilePicURL *string `json:"profile_pic_url,omitempty"` + ID string `json:"id"` + FullName string `json:"full_name"` + Phone *string `json:"phone,omitempty"` + Email *string `json:"email,omitempty"` + ProfilePicURL *string `json:"profile_pic_url,omitempty"` PreviousFirstName *string `json:"previous_first_name,omitempty"` PreviousLastName *string `json:"previous_last_name,omitempty"` } @@ -552,7 +552,7 @@ func getClosingTime(r *http.Request, date time.Time) string { } // Helper function to fetch a single appointment with all details -func fetchAppointment(r *http.Request, query string, args ...interface{}) (*AppointmentInfo, error) { +func fetchAppointment(r *http.Request, query string, args ...any) (*AppointmentInfo, error) { var bookingID string var startTime time.Time var status string @@ -681,15 +681,15 @@ func fetchAppointment(r *http.Request, query string, args ...interface{}) (*Appo } type TodayAppointment struct { - ID string `json:"id"` - StartTime string `json:"start_time"` - Status string `json:"status"` - UserName string `json:"user_name"` - UserID string `json:"user_id"` - Services []string `json:"services"` - DurationMinutes int `json:"duration_minutes"` - PreviousFirstName *string `json:"previous_first_name,omitempty"` - PreviousLastName *string `json:"previous_last_name,omitempty"` + ID string `json:"id"` + StartTime string `json:"start_time"` + Status string `json:"status"` + UserName string `json:"user_name"` + UserID string `json:"user_id"` + Services []string `json:"services"` + DurationMinutes int `json:"duration_minutes"` + PreviousFirstName *string `json:"previous_first_name,omitempty"` + PreviousLastName *string `json:"previous_last_name,omitempty"` } type TodayAppointmentsResponse struct { @@ -903,15 +903,15 @@ func GetTodayAppointmentsHandler(w http.ResponseWriter, r *http.Request) { } type PendingApproval struct { - ID string `json:"id"` - StartTime string `json:"start_time"` - UserID string `json:"user_id"` - UserName string `json:"user_name"` - PreviousFirstName *string `json:"previous_first_name,omitempty"` - PreviousLastName *string `json:"previous_last_name,omitempty"` - Services []string `json:"services"` - DurationMinutes int `json:"duration_minutes"` - CreatedAt string `json:"created_at"` + ID string `json:"id"` + StartTime string `json:"start_time"` + UserID string `json:"user_id"` + UserName string `json:"user_name"` + PreviousFirstName *string `json:"previous_first_name,omitempty"` + PreviousLastName *string `json:"previous_last_name,omitempty"` + Services []string `json:"services"` + DurationMinutes int `json:"duration_minutes"` + CreatedAt string `json:"created_at"` } type PendingApprovalsResponse struct { diff --git a/backend/handlers/today/today_test.go b/backend/handlers/today/today_test.go index beefba3..08571bb 100644 --- a/backend/handlers/today/today_test.go +++ b/backend/handlers/today/today_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package today diff --git a/backend/handlers/user/customer_relationship_test.go b/backend/handlers/user/customer_relationship_test.go index 7620f9e..3dee265 100644 --- a/backend/handlers/user/customer_relationship_test.go +++ b/backend/handlers/user/customer_relationship_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package user diff --git a/backend/handlers/user/gdpr_test.go b/backend/handlers/user/gdpr_test.go index 625d6b4..b1a7948 100644 --- a/backend/handlers/user/gdpr_test.go +++ b/backend/handlers/user/gdpr_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package user @@ -12,8 +11,8 @@ import ( "time" "crussell/clock" - "crussell/testutils" "crussell/mw" + "crussell/testutils" "crussell/testutils/fixtures" "crussell/testutils/jwt" ) @@ -1284,8 +1283,8 @@ func TestCleanupGDPRExportCache_Mixed(t *testing.T) { gdprExportCacheMu.Lock() gdprExportCache = map[string]*gdprCacheEntry{ - "user-expired": {expiresAt: clock.Now().Add(-2 * time.Hour)}, - "user-valid": {expiresAt: clock.Now().Add(2 * time.Hour)}, + "user-expired": {expiresAt: clock.Now().Add(-2 * time.Hour)}, + "user-valid": {expiresAt: clock.Now().Add(2 * time.Hour)}, "user-expired2": {expiresAt: clock.Now().Add(-30 * time.Minute)}, } gdprExportCacheMu.Unlock() diff --git a/backend/handlers/user/guest.go b/backend/handlers/user/guest.go index 92d4862..a2f89e9 100644 --- a/backend/handlers/user/guest.go +++ b/backend/handlers/user/guest.go @@ -136,7 +136,6 @@ 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"}) } @@ -192,8 +191,7 @@ func CheckEmailHandler(w http.ResponseWriter, r *http.Request) { return } - - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "suggestion": suggestion, }) } diff --git a/backend/handlers/user/guest_test.go b/backend/handlers/user/guest_test.go index 6a832bb..4657f0c 100644 --- a/backend/handlers/user/guest_test.go +++ b/backend/handlers/user/guest_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package user diff --git a/backend/handlers/user/patch_tests_test.go b/backend/handlers/user/patch_tests_test.go index 50ce5ca..899e21d 100644 --- a/backend/handlers/user/patch_tests_test.go +++ b/backend/handlers/user/patch_tests_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package user @@ -10,8 +9,8 @@ import ( "strings" "testing" - "crussell/testutils" "crussell/mw" + "crussell/testutils" "crussell/testutils/fixtures" "github.com/go-chi/chi/v5" @@ -174,5 +173,3 @@ func TestDeletePatchTest_InvalidTestID(t *testing.T) { t.Errorf("expected 404 for invalid test ID, got %d", w.Code) } } - - diff --git a/backend/handlers/user/profile.go b/backend/handlers/user/profile.go index a17b71a..5fcdab5 100644 --- a/backend/handlers/user/profile.go +++ b/backend/handlers/user/profile.go @@ -21,14 +21,14 @@ import ( "golang.org/x/text/cases" "golang.org/x/text/language" - "crussell/db" "crussell/clock" - "github.com/jackc/pgx/v5" + "crussell/db" "crussell/handlers/auth" "crussell/internal/images" "crussell/internal/s3" "crussell/internal/validators" "crussell/mw" + "github.com/jackc/pgx/v5" ) func getEnv(key, fallback string) string { @@ -41,19 +41,19 @@ func getEnv(key, fallback string) string { var titleCaser = cases.Title(language.English) type UserProfile struct { - ID string `json:"id"` - Email string `json:"email"` - FirstName string `json:"firstName"` - LastName string `json:"lastName"` - Phone *string `json:"phone,omitempty"` - DateOfBirth *string `json:"dateOfBirth,omitempty"` - Role string `json:"role"` - LoyaltyStamps int `json:"loyaltyStamps"` - ReferralCode string `json:"referralCode"` - ReferralCodeUses int `json:"referralCodeUses"` - ReferralSavings float64 `json:"referralSavings"` - ProfilePicURL *string `json:"profilePicUrl,omitempty"` - DepositsRequired int `json:"deposits_required"` + ID string `json:"id"` + Email string `json:"email"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + Phone *string `json:"phone,omitempty"` + DateOfBirth *string `json:"dateOfBirth,omitempty"` + Role string `json:"role"` + LoyaltyStamps int `json:"loyaltyStamps"` + ReferralCode string `json:"referralCode"` + ReferralCodeUses int `json:"referralCodeUses"` + ReferralSavings float64 `json:"referralSavings"` + ProfilePicURL *string `json:"profilePicUrl,omitempty"` + DepositsRequired int `json:"deposits_required"` PreviousFirstName *string `json:"previousFirstName,omitempty"` PreviousLastName *string `json:"previousLastName,omitempty"` } @@ -103,15 +103,15 @@ type SocialLogin struct { } type UserListItem struct { - ID string `json:"id"` - FullName string `json:"fullName"` - Email *string `json:"email,omitempty"` - Phone *string `json:"phone,omitempty"` - AccountRole string `json:"account_role"` - CreatedAt time.Time `json:"created_at"` - PreviousFirstName *string `json:"previousFirstName,omitempty"` - PreviousLastName *string `json:"previousLastName,omitempty"` - CompletedCount int `json:"completed_count"` + ID string `json:"id"` + FullName string `json:"fullName"` + Email *string `json:"email,omitempty"` + Phone *string `json:"phone,omitempty"` + AccountRole string `json:"account_role"` + CreatedAt time.Time `json:"created_at"` + PreviousFirstName *string `json:"previousFirstName,omitempty"` + PreviousLastName *string `json:"previousLastName,omitempty"` + CompletedCount int `json:"completed_count"` } type UserListResponse struct { @@ -511,7 +511,7 @@ func ListAdminUsersHandler(w http.ResponseWriter, r *http.Request) { ` var listQuery string - var listArgs []interface{} + var listArgs []any if searchTerm != "" { searchPattern := "%" + searchTerm + "%" @@ -520,7 +520,7 @@ func ListAdminUsersHandler(w http.ResponseWriter, r *http.Request) { WHERE (u.fn ILIKE $1 OR u.email ILIKE $1 OR u.phone ILIKE $1) ) sub` - listArgs = []interface{}{searchPattern} + listArgs = []any{searchPattern} if cursorStr != "" { cursorCount, cursorCreatedAt, cursorID, err := validators.ParseCursor3(cursorStr) diff --git a/backend/handlers/user/profile_test.go b/backend/handlers/user/profile_test.go index 20919f6..6df411b 100644 --- a/backend/handlers/user/profile_test.go +++ b/backend/handlers/user/profile_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package user @@ -25,8 +24,8 @@ import ( "net/http/httptest" "testing" - "crussell/testutils" "crussell/mw" + "crussell/testutils" "crussell/testutils/fixtures" "crussell/testutils/jwt" ) diff --git a/backend/handlers/user/testmain_test.go b/backend/handlers/user/testmain_test.go index 169b90d..ccc4cec 100644 --- a/backend/handlers/user/testmain_test.go +++ b/backend/handlers/user/testmain_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package user diff --git a/backend/handlers/webhooks/webhooks_test.go b/backend/handlers/webhooks/webhooks_test.go index 6a4d1eb..552cb97 100644 --- a/backend/handlers/webhooks/webhooks_test.go +++ b/backend/handlers/webhooks/webhooks_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package webhooks @@ -194,7 +193,7 @@ func TestHandleSquareWebhook_BodyTooLarge(t *testing.T) { } func TestHandleSquareWebhook_ValidSignatureWithEnvKey(t *testing.T) { - + body := []byte(`{"type":"payment.updated","event_id":"evt_1"}`) key := "env-signing-key" notificationURL := "http://localhost:8080/webhooks/square" @@ -213,7 +212,7 @@ func TestHandleSquareWebhook_ValidSignatureWithEnvKey(t *testing.T) { } func TestHandleSquareWebhook_InvalidSignatureWithEnvKey(t *testing.T) { - + body := []byte(`{"type":"payment.updated","event_id":"evt_1"}`) t.Setenv("SQUARE_WEBHOOK_SIGNATURE_KEY", "env-signing-key") @@ -225,7 +224,7 @@ func TestHandleSquareWebhook_InvalidSignatureWithEnvKey(t *testing.T) { } func TestHandleSquareWebhook_NoSignatureWhenKeySet(t *testing.T) { - + body := []byte(`{"type":"payment.updated","event_id":"evt_1"}`) t.Setenv("SQUARE_WEBHOOK_SIGNATURE_KEY", "env-signing-key") @@ -238,7 +237,7 @@ func TestHandleSquareWebhook_NoSignatureWhenKeySet(t *testing.T) { } func TestHandleSquareWebhook_SignatureSkippedWhenKeyEmpty(t *testing.T) { - + t.Setenv("SQUARE_WEBHOOK_SIGNATURE_KEY", "") body := []byte(`{"type":"payment.updated","event_id":"evt_1"}`) diff --git a/backend/internal/dav/service_dev.go b/backend/internal/dav/service_dev.go index 3d4d7a5..509aebe 100644 --- a/backend/internal/dav/service_dev.go +++ b/backend/internal/dav/service_dev.go @@ -1,5 +1,4 @@ //go:build dev -// +build dev package dav diff --git a/backend/internal/dav/service_prod.go b/backend/internal/dav/service_prod.go index 204c387..84ad4cc 100644 --- a/backend/internal/dav/service_prod.go +++ b/backend/internal/dav/service_prod.go @@ -1,5 +1,4 @@ //go:build !dev -// +build !dev package dav diff --git a/backend/internal/dav/shared.go b/backend/internal/dav/shared.go index 275ad08..c8f299f 100644 --- a/backend/internal/dav/shared.go +++ b/backend/internal/dav/shared.go @@ -1,8 +1,8 @@ package dav import ( - "crussell/clock" "context" + "crussell/clock" "fmt" "strings" "time" @@ -131,7 +131,7 @@ func (s *BaseService) ListRecentContacts(days int) ([]Contact, error) { // Helper Methods // ============================================================================ -func (s *BaseService) queryEventsWithContacts(query string, args ...interface{}) ([]CalendarEvent, error) { +func (s *BaseService) queryEventsWithContacts(query string, args ...any) ([]CalendarEvent, error) { rows, err := s.db.Query(context.Background(), query, args...) if err != nil { return nil, err @@ -155,7 +155,7 @@ func (s *BaseService) queryEventsWithContacts(query string, args ...interface{}) func extractContactURIsFromICalendar(icalData string) []string { var uris []string - for _, line := range strings.Split(icalData, "\n") { + for line := range strings.SplitSeq(icalData, "\n") { line = strings.TrimSpace(line) if strings.HasPrefix(line, "ATTENDEE") { parts := strings.Split(line, ":") diff --git a/backend/internal/dav/types.go b/backend/internal/dav/types.go index ccb848e..dd55f29 100644 --- a/backend/internal/dav/types.go +++ b/backend/internal/dav/types.go @@ -3,6 +3,7 @@ package dav import ( "crussell/clock" "fmt" + "strings" "time" ) @@ -102,9 +103,9 @@ func GenerateICalEvent(input EventInput) string { } // Build attendees section - attendees := "" + var attendees strings.Builder for _, contactURI := range input.ContactURIs { - attendees += fmt.Sprintf("ATTENDEE;CN=%s:%s\n", contactURI, contactURI) + attendees.WriteString(fmt.Sprintf("ATTENDEE;CN=%s:%s\n", contactURI, contactURI)) } ical := fmt.Sprintf(`BEGIN:VCALENDAR @@ -127,7 +128,7 @@ END:VCALENDAR`, uid, dtstamp, dtstart, dtend, escapeICalText(input.Summary), escapeICalText(input.Description), escapeICalText(input.Location), - attendees) + attendees.String()) return ical } @@ -164,13 +165,13 @@ func escapeICalText(text string) string { } func replaceAll(s, old, new string) string { - result := "" + var result strings.Builder for _, char := range s { if string(char) == old { - result += new + result.WriteString(new) } else { - result += string(char) + result.WriteString(string(char)) } } - return result + return result.String() } diff --git a/backend/internal/images/validate_test.go b/backend/internal/images/validate_test.go index 2ed4d6d..e835c09 100644 --- a/backend/internal/images/validate_test.go +++ b/backend/internal/images/validate_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package images diff --git a/backend/internal/s3/s3.go b/backend/internal/s3/s3.go index 567da22..fa3556b 100644 --- a/backend/internal/s3/s3.go +++ b/backend/internal/s3/s3.go @@ -1,5 +1,4 @@ //go:build !dev -// +build !dev package s3 diff --git a/backend/internal/s3/s3_dev.go b/backend/internal/s3/s3_dev.go index 3aeb712..1703b68 100644 --- a/backend/internal/s3/s3_dev.go +++ b/backend/internal/s3/s3_dev.go @@ -1,5 +1,4 @@ //go:build dev -// +build dev package s3 diff --git a/backend/internal/square/square.go b/backend/internal/square/square.go index e123d29..4da1a17 100644 --- a/backend/internal/square/square.go +++ b/backend/internal/square/square.go @@ -1,5 +1,4 @@ //go:build !dev -// +build !dev package square diff --git a/backend/internal/square/square_dev.go b/backend/internal/square/square_dev.go index a2979df..2e9a969 100644 --- a/backend/internal/square/square_dev.go +++ b/backend/internal/square/square_dev.go @@ -1,11 +1,10 @@ //go:build dev -// +build dev package square import ( - "crussell/clock" "context" + "crussell/clock" "fmt" "log" "os" diff --git a/backend/internal/square/square_dev_test.go b/backend/internal/square/square_dev_test.go index 15d9dbf..b23971e 100644 --- a/backend/internal/square/square_dev_test.go +++ b/backend/internal/square/square_dev_test.go @@ -1,5 +1,4 @@ //go:build test && dev -// +build test,dev package square diff --git a/backend/internal/validators/email.go b/backend/internal/validators/email.go index 54a6cb5..1b70dc7 100644 --- a/backend/internal/validators/email.go +++ b/backend/internal/validators/email.go @@ -30,11 +30,11 @@ func ValidateEmail(email string) error { // NormalizeGiftCardCode strips non-alphanumeric characters and upper-cases the code. func NormalizeGiftCardCode(code string) string { - clean := "" + var clean strings.Builder for _, char := range code { if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') { - clean += string(char) + clean.WriteString(string(char)) } } - return clean + return clean.String() } diff --git a/backend/internal/validators/email_test.go b/backend/internal/validators/email_test.go index f716b60..d4f238b 100644 --- a/backend/internal/validators/email_test.go +++ b/backend/internal/validators/email_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package validators diff --git a/backend/main.go b/backend/main.go index dfa29af..66963a5 100644 --- a/backend/main.go +++ b/backend/main.go @@ -25,8 +25,8 @@ import ( "crussell/db" "crussell/mw" - authHandlers "crussell/handlers/auth" "crussell/handlers/admin" + authHandlers "crussell/handlers/auth" "crussell/handlers/bookings" "crussell/handlers/notifications" "crussell/handlers/payments" @@ -68,9 +68,9 @@ func limitBody(limit int64) func(http.Handler) http.Handler { } const ( - defaultBodyLimit int64 = 1 * 1024 * 1024 // 1MB - uploadBodyLimit int64 = 15 * 1024 * 1024 // 15MB - portfolioBodyLimit int64 = 40 * 1024 * 1024 // 40MB (7 variants from 20MB source) + defaultBodyLimit int64 = 1 * 1024 * 1024 // 1MB + uploadBodyLimit int64 = 15 * 1024 * 1024 // 15MB + portfolioBodyLimit int64 = 40 * 1024 * 1024 // 40MB (7 variants from 20MB source) ) // nColor / bColor — Chi-style ANSI colors for request logging. @@ -78,11 +78,11 @@ type nColor string type bColor string var ( - reset = nColor(logutil.Reset) - nGreen = nColor(logutil.Green) - nYellow = nColor(logutil.Yellow) - nCyan = nColor(logutil.Cyan) - nRed = nColor(logutil.Red) + 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 +153,7 @@ func healthCheckHandler(w http.ResponseWriter, r *http.Request) { } else { w.WriteHeader(http.StatusOK) } - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "status": status, "services": services, }) @@ -440,15 +440,15 @@ func main() { r.Post("/{id}/edit-requests/{request_id}/deny", bookings.AdminRejectEditRequestHandler) }) -r.Route("/admin/users", func(r chi.Router) { - r.Get("/", user.ListAdminUsersHandler) - r.Get("/{id}", user.GetAdminUserHandler) - r.Get("/{id}/relationship", user.GetCustomerRelationshipHandler) - r.Get("/{id}/patch-tests/eligible", user.GetEligiblePatchTestServicesHandler) - r.Post("/{id}/patch-tests", user.AddPatchTestHandler) - r.Get("/{id}/giftcard-balance", payments.GetUserGiftCardBalanceAdmin) - r.Get("/{id}/payment-methods", payments.AdminGetUserPaymentMethods) - }) + r.Route("/admin/users", func(r chi.Router) { + r.Get("/", user.ListAdminUsersHandler) + r.Get("/{id}", user.GetAdminUserHandler) + r.Get("/{id}/relationship", user.GetCustomerRelationshipHandler) + r.Get("/{id}/patch-tests/eligible", user.GetEligiblePatchTestServicesHandler) + r.Post("/{id}/patch-tests", user.AddPatchTestHandler) + r.Get("/{id}/giftcard-balance", payments.GetUserGiftCardBalanceAdmin) + r.Get("/{id}/payment-methods", payments.AdminGetUserPaymentMethods) + }) r.Route("/admin/today", func(r chi.Router) { r.Get("/current-next", today.GetCurrentAndNextHandler) @@ -456,11 +456,11 @@ r.Route("/admin/users", func(r chi.Router) { r.Get("/pending-approvals", today.GetPendingApprovalsHandler) }) - r.Route("/admin/notifications", func(r chi.Router) { - r.Get("/", notifications.GetNotifications) - r.Get("/unread-count", notifications.GetUnreadCount) - r.Post("/{id}/acknowledge", notifications.AcknowledgeNotification) - }) + r.Route("/admin/notifications", func(r chi.Router) { + r.Get("/", notifications.GetNotifications) + r.Get("/unread-count", notifications.GetUnreadCount) + r.Post("/{id}/acknowledge", notifications.AcknowledgeNotification) + }) r.Route("/admin/time-blockers", func(r chi.Router) { r.Get("/", scheduling.ListTimeBlockers) diff --git a/backend/main_test.go b/backend/main_test.go index b8bd866..c9f0834 100644 --- a/backend/main_test.go +++ b/backend/main_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package main diff --git a/backend/mw/auth.go b/backend/mw/auth.go index 18c32e6..2ff7bc9 100644 --- a/backend/mw/auth.go +++ b/backend/mw/auth.go @@ -4,6 +4,7 @@ import ( "context" "log" "net/http" + "slices" "strings" "crussell/auth" @@ -77,13 +78,7 @@ func RequireRole(allowedRoles ...string) func(http.Handler) http.Handler { } // Check if user has one of the allowed roles - hasRole := false - for _, allowedRole := range allowedRoles { - if role == allowedRole { - hasRole = true - break - } - } + hasRole := slices.Contains(allowedRoles, role) if !hasRole { http.Error(w, "forbidden", http.StatusForbidden) diff --git a/backend/mw/auth_test.go b/backend/mw/auth_test.go index f0ccf1d..747f063 100644 --- a/backend/mw/auth_test.go +++ b/backend/mw/auth_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package mw diff --git a/backend/mw/contenttype_test.go b/backend/mw/contenttype_test.go index 94fe45d..a20f5a1 100644 --- a/backend/mw/contenttype_test.go +++ b/backend/mw/contenttype_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package mw diff --git a/backend/mw/ratelimit.go b/backend/mw/ratelimit.go index c1a7efe..a782056 100644 --- a/backend/mw/ratelimit.go +++ b/backend/mw/ratelimit.go @@ -1,5 +1,4 @@ //go:build !dev -// +build !dev package mw diff --git a/backend/mw/ratelimit_dev.go b/backend/mw/ratelimit_dev.go index 634704a..6de24ec 100644 --- a/backend/mw/ratelimit_dev.go +++ b/backend/mw/ratelimit_dev.go @@ -1,5 +1,4 @@ //go:build dev -// +build dev package mw diff --git a/backend/mw/ratelimit_dev_test.go b/backend/mw/ratelimit_dev_test.go index 1d47c0e..90ff6ca 100644 --- a/backend/mw/ratelimit_dev_test.go +++ b/backend/mw/ratelimit_dev_test.go @@ -1,5 +1,4 @@ //go:build test && dev -// +build test,dev package mw diff --git a/backend/mw/ratelimit_prod_test.go b/backend/mw/ratelimit_prod_test.go index c4ed514..f87f949 100644 --- a/backend/mw/ratelimit_prod_test.go +++ b/backend/mw/ratelimit_prod_test.go @@ -1,5 +1,4 @@ //go:build test && !dev -// +build test,!dev package mw diff --git a/backend/mw/ratelimit_test.go b/backend/mw/ratelimit_test.go index 3db7543..e5df86c 100644 --- a/backend/mw/ratelimit_test.go +++ b/backend/mw/ratelimit_test.go @@ -1,5 +1,4 @@ //go:build test && !dev -// +build test,!dev package mw diff --git a/backend/testmain_test.go b/backend/testmain_test.go index e9431aa..a97d384 100644 --- a/backend/testmain_test.go +++ b/backend/testmain_test.go @@ -1,5 +1,4 @@ //go:build test -// +build test package main