chore: run go fix for Go 1.26 modernization
CI / Go vulnerabilities (push) Successful in 1m10s
CI / Build & Vet (push) Successful in 1m39s
CI / Frontend build (gate) (push) Successful in 1m42s
CI / Frontend QC (audit) (push) Successful in 56s
CI / Frontend QC (typecheck) (push) Successful in 1m36s
CI / Frontend QC (lint) (push) Successful in 1m51s
CI / Tests (prod) (push) Has been cancelled
CI / Tests (dev) (push) Has been cancelled
CI / Race (prod) (push) Has been cancelled
CI / Race (dev) (push) Has been cancelled
CI / Go vulnerabilities (push) Successful in 1m10s
CI / Build & Vet (push) Successful in 1m39s
CI / Frontend build (gate) (push) Successful in 1m42s
CI / Frontend QC (audit) (push) Successful in 56s
CI / Frontend QC (typecheck) (push) Successful in 1m36s
CI / Frontend QC (lint) (push) Successful in 1m51s
CI / Tests (prod) (push) Has been cancelled
CI / Tests (dev) (push) Has been cancelled
CI / Race (prod) (push) Has been cancelled
CI / Race (dev) (push) Has been cancelled
106 files: interface{}→any, strings.Split→SplitSeq, CutPrefix/Cut, strings.Builder, slices.Contains, remove redundant // +build directives, gofmt import ordering and indentation.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -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.
|
||||||
+4
-4
@@ -121,7 +121,7 @@ func GenerateToken(userID string, role string) (string, string, error) {
|
|||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, tokenString, err := TokenAuth.Encode(map[string]interface{}{
|
_, tokenString, err := TokenAuth.Encode(map[string]any{
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
"role": role,
|
"role": role,
|
||||||
"jti": jti,
|
"jti": jti,
|
||||||
@@ -137,7 +137,7 @@ func VerifyToken(tokenString string, ctx context.Context) (userID string, role s
|
|||||||
return "", "", "", err
|
return "", "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var uidVal interface{}
|
var uidVal any
|
||||||
if err := token.Get("user_id", &uidVal); err != nil {
|
if err := token.Get("user_id", &uidVal); err != nil {
|
||||||
return "", "", "", fmt.Errorf("invalid user_id claim")
|
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")
|
return "", "", "", fmt.Errorf("invalid user_id claim")
|
||||||
}
|
}
|
||||||
|
|
||||||
var roleVal interface{}
|
var roleVal any
|
||||||
if err := token.Get("role", &roleVal); err != nil {
|
if err := token.Get("role", &roleVal); err != nil {
|
||||||
return "", "", "", fmt.Errorf("invalid role claim")
|
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")
|
return "", "", "", fmt.Errorf("invalid role claim")
|
||||||
}
|
}
|
||||||
|
|
||||||
var jtiVal interface{}
|
var jtiVal any
|
||||||
if err := token.Get("jti", &jtiVal); err != nil {
|
if err := token.Get("jti", &jtiVal); err != nil {
|
||||||
return "", "", "", fmt.Errorf("invalid jti claim")
|
return "", "", "", fmt.Errorf("invalid jti claim")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build !dev
|
//go:build !dev
|
||||||
// +build !dev
|
|
||||||
|
|
||||||
package db
|
package db
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build dev
|
//go:build dev
|
||||||
// +build dev
|
|
||||||
|
|
||||||
package db
|
package db
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package db
|
package db
|
||||||
|
|
||||||
@@ -178,5 +177,3 @@ func TestGetEnv_ReturnsEmptyWhenUnset(t *testing.T) {
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Clean-up — restore env after all tests
|
// Clean-up — restore env after all tests
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package db
|
package db
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package db
|
package db
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package admin
|
package admin
|
||||||
|
|
||||||
@@ -9,8 +8,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"crussell/clock"
|
"crussell/clock"
|
||||||
"crussell/testutils"
|
|
||||||
"crussell/handlers/bookings"
|
"crussell/handlers/bookings"
|
||||||
|
"crussell/testutils"
|
||||||
"crussell/testutils/fixtures"
|
"crussell/testutils/fixtures"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package admin
|
package admin
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package admin
|
package admin
|
||||||
|
|
||||||
@@ -32,9 +31,9 @@ import (
|
|||||||
|
|
||||||
"crussell/clock"
|
"crussell/clock"
|
||||||
"crussell/db"
|
"crussell/db"
|
||||||
"crussell/testutils"
|
|
||||||
"crussell/handlers/bookings"
|
"crussell/handlers/bookings"
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
|
"crussell/testutils"
|
||||||
"crussell/testutils/fixtures"
|
"crussell/testutils/fixtures"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"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
|
// 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)
|
// 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
|
// Regular users would be rejected, but admin should succeed
|
||||||
@@ -2291,7 +2289,6 @@ func TestAdminBookings_Create_WalkInWithDeposits(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Seed working hours
|
// Seed working hours
|
||||||
|
|
||||||
|
|
||||||
// Set user to have outstanding deposits
|
// Set user to have outstanding deposits
|
||||||
_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 3 WHERE id = $1", userID)
|
_, 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)
|
t.Fatalf("failed to create test service: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Set user to have deposits_required = 2
|
// Set user to have deposits_required = 2
|
||||||
_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 2 WHERE id = $1", userID)
|
_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 2 WHERE id = $1", userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2427,7 +2423,6 @@ func TestAdminBookings_Confirm_CompletesWithoutPayment_NoReduction(t *testing.T)
|
|||||||
t.Fatalf("failed to create test service: %v", err)
|
t.Fatalf("failed to create test service: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Set user to have deposits_required = 2
|
// Set user to have deposits_required = 2
|
||||||
_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 2 WHERE id = $1", userID)
|
_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 2 WHERE id = $1", userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2502,7 +2497,6 @@ func TestAdminBookings_Create_EnforceDepositsFalse_BypassesLimit(t *testing.T) {
|
|||||||
t.Fatalf("failed to create test service: %v", err)
|
t.Fatalf("failed to create test service: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Set user to have deposits_required = 3
|
// Set user to have deposits_required = 3
|
||||||
_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 3 WHERE id = $1", userID)
|
_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 3 WHERE id = $1", userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2564,9 +2558,7 @@ func TestAdminBookings_Create_EnforceDepositsFalse_Within24h(t *testing.T) {
|
|||||||
t.Fatalf("failed to create test service: %v", err)
|
t.Fatalf("failed to create test service: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Seed working hours
|
// Seed working hours
|
||||||
|
|
||||||
|
|
||||||
// Set user to have deposits_required = 3
|
// Set user to have deposits_required = 3
|
||||||
_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 3 WHERE id = $1", userID)
|
_, 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
|
// Seed working hours
|
||||||
|
|
||||||
|
|
||||||
// Create booking for tomorrow
|
// Create booking for tomorrow
|
||||||
tomorrow := clock.Now().Add(24 * time.Hour).Truncate(time.Second)
|
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) {
|
func TestGetBookingsByCreatedRange(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
_, err := fixtures.CreateTestAdminUser(tx)
|
_, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2744,7 +2734,6 @@ func TestGetBookingsByCreatedRange(t *testing.T) {
|
|||||||
func TestGetBookingsByCreatedRange_Empty(t *testing.T) {
|
func TestGetBookingsByCreatedRange_Empty(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
_, err := fixtures.CreateTestAdminUser(tx)
|
_, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2824,7 +2813,6 @@ func TestGetBookingsByCreatedRange_InvalidFormat(t *testing.T) {
|
|||||||
func TestGetBookingsByCreatedRange_OrderedByCreatedAt(t *testing.T) {
|
func TestGetBookingsByCreatedRange_OrderedByCreatedAt(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
_, err := fixtures.CreateTestAdminUser(tx)
|
_, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2912,7 +2900,6 @@ func TestGetBookingsByCreatedRange_OrderedByCreatedAt(t *testing.T) {
|
|||||||
func TestGetAdminBooking_WithDiscounts(t *testing.T) {
|
func TestGetAdminBooking_WithDiscounts(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -3007,7 +2994,6 @@ func createCompletedBookingWithTimeForAdmin(t *testing.T, ctx context.Context, t
|
|||||||
func TestAdminBookings_CreateWithCustomServices(t *testing.T) {
|
func TestAdminBookings_CreateWithCustomServices(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
_, err := fixtures.CreateTestAdminUser(tx)
|
_, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -3084,7 +3070,6 @@ func TestAdminBookings_CreateWithCustomServices(t *testing.T) {
|
|||||||
func TestAdminBookings_CreateWithCustomAndRegularServices(t *testing.T) {
|
func TestAdminBookings_CreateWithCustomAndRegularServices(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
_, err := fixtures.CreateTestAdminUser(tx)
|
_, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
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)
|
// Test 3: custom_service_ids is provided (should succeed — need working hours)
|
||||||
t.Run("provides custom_service_ids only", func(t *testing.T) {
|
t.Run("provides custom_service_ids only", func(t *testing.T) {
|
||||||
|
|
||||||
|
|
||||||
customServiceID, err := fixtures.CreateTestCustomService(tx)
|
customServiceID, err := fixtures.CreateTestCustomService(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -3322,7 +3306,6 @@ func TestAdminBookings_Confirm_WithCustomOverrides(t *testing.T) {
|
|||||||
func TestAdminBookings_CreateWithCustomServicesAndOverrides(t *testing.T) {
|
func TestAdminBookings_CreateWithCustomServicesAndOverrides(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
_, err := fixtures.CreateTestAdminUser(tx)
|
_, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -3401,7 +3384,6 @@ func TestAdminBookings_CreateWithCustomServicesAndOverrides(t *testing.T) {
|
|||||||
func TestAdminBookings_AdminReserve_WithCustomServices(t *testing.T) {
|
func TestAdminBookings_AdminReserve_WithCustomServices(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
@@ -107,7 +108,7 @@ func GetCustomServices(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var dataQuery string
|
var dataQuery string
|
||||||
var dataArgs []interface{}
|
var dataArgs []any
|
||||||
|
|
||||||
if q != "" {
|
if q != "" {
|
||||||
dataQuery = `
|
dataQuery = `
|
||||||
@@ -115,7 +116,7 @@ func GetCustomServices(w http.ResponseWriter, r *http.Request) {
|
|||||||
FROM custom_services
|
FROM custom_services
|
||||||
WHERE name ILIKE $1 OR description ILIKE $1
|
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)
|
dataQuery += " ORDER BY created_at DESC, id DESC LIMIT $" + strconv.Itoa(len(dataArgs)+1)
|
||||||
dataArgs = append(dataArgs, perPage+1)
|
dataArgs = append(dataArgs, perPage+1)
|
||||||
@@ -318,7 +319,7 @@ func UpdateCustomService(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
updates := make(map[string]interface{})
|
updates := make(map[string]any)
|
||||||
if req.Name != nil {
|
if req.Name != nil {
|
||||||
updates["name"] = *req.Name
|
updates["name"] = *req.Name
|
||||||
}
|
}
|
||||||
@@ -361,7 +362,7 @@ func UpdateCustomService(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setClauses := make([]string, 0, len(updates))
|
setClauses := make([]string, 0, len(updates))
|
||||||
args := make([]interface{}, 0, len(updates)+1)
|
args := make([]any, 0, len(updates)+1)
|
||||||
argIdx := 1
|
argIdx := 1
|
||||||
for field, val := range updates {
|
for field, val := range updates {
|
||||||
setClauses = append(setClauses, field+" = $"+strconv.Itoa(argIdx))
|
setClauses = append(setClauses, field+" = $"+strconv.Itoa(argIdx))
|
||||||
@@ -530,9 +531,10 @@ func joinStrings(strs []string, sep string) string {
|
|||||||
if len(strs) == 0 {
|
if len(strs) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
result := strs[0]
|
var result strings.Builder
|
||||||
|
result.WriteString(strs[0])
|
||||||
for _, s := range strs[1:] {
|
for _, s := range strs[1:] {
|
||||||
result += sep + s
|
result.WriteString(sep + s)
|
||||||
}
|
}
|
||||||
return result
|
return result.String()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package admin
|
package admin
|
||||||
|
|
||||||
@@ -24,8 +23,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"crussell/testutils"
|
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
|
"crussell/testutils"
|
||||||
"crussell/testutils/fixtures"
|
"crussell/testutils/fixtures"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|||||||
@@ -195,7 +195,6 @@ func GetDiscountCampaigns(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
if campaigns == nil {
|
if campaigns == nil {
|
||||||
@@ -384,7 +383,6 @@ func CreateDiscountCampaign(w http.ResponseWriter, r *http.Request) {
|
|||||||
campaign.CreatedBy = &createdByDB.String
|
campaign.CreatedBy = &createdByDB.String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
if err := json.NewEncoder(w).Encode(campaign); err != nil {
|
if err := json.NewEncoder(w).Encode(campaign); err != nil {
|
||||||
log.Printf("Error encoding campaign: %v", err)
|
log.Printf("Error encoding campaign: %v", err)
|
||||||
@@ -424,7 +422,7 @@ func UpdateDiscountCampaign(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Build dynamic update query
|
// Build dynamic update query
|
||||||
query := "UPDATE discount_campaigns SET updated_at = NOW()"
|
query := "UPDATE discount_campaigns SET updated_at = NOW()"
|
||||||
args := []interface{}{}
|
args := []any{}
|
||||||
argNum := 1
|
argNum := 1
|
||||||
|
|
||||||
if req.Name != nil {
|
if req.Name != nil {
|
||||||
@@ -603,7 +601,6 @@ func UpdateDiscountCampaign(w http.ResponseWriter, r *http.Request) {
|
|||||||
campaign.CreatedBy = &createdBy.String
|
campaign.CreatedBy = &createdBy.String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
if err := json.NewEncoder(w).Encode(campaign); err != nil {
|
if err := json.NewEncoder(w).Encode(campaign); err != nil {
|
||||||
log.Printf("Error encoding campaign: %v", err)
|
log.Printf("Error encoding campaign: %v", err)
|
||||||
@@ -658,9 +655,8 @@ func DeleteDiscountCampaign(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
"message": "Campaign deleted successfully",
|
"message": "Campaign deleted successfully",
|
||||||
"id": campaignID,
|
"id": campaignID,
|
||||||
})
|
})
|
||||||
@@ -774,7 +770,6 @@ func GetCampaignStats(w http.ResponseWriter, r *http.Request) {
|
|||||||
BookingCount: bookingCount,
|
BookingCount: bookingCount,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
if err := json.NewEncoder(w).Encode(stats); err != nil {
|
if err := json.NewEncoder(w).Encode(stats); err != nil {
|
||||||
log.Printf("Error encoding stats: %v", err)
|
log.Printf("Error encoding stats: %v", err)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package admin
|
package admin
|
||||||
|
|
||||||
@@ -15,8 +14,8 @@ import (
|
|||||||
|
|
||||||
"crussell/clock"
|
"crussell/clock"
|
||||||
"crussell/db"
|
"crussell/db"
|
||||||
"crussell/testutils"
|
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
|
"crussell/testutils"
|
||||||
"crussell/testutils/fixtures"
|
"crussell/testutils/fixtures"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
@@ -24,7 +23,6 @@ import (
|
|||||||
|
|
||||||
var testAdminID string
|
var testAdminID string
|
||||||
|
|
||||||
|
|
||||||
func makeCampaignRequest(handler http.Handler, method, path string, body interface{}, ctx context.Context, adminID string) *httptest.ResponseRecorder {
|
func makeCampaignRequest(handler http.Handler, method, path string, body interface{}, ctx context.Context, adminID string) *httptest.ResponseRecorder {
|
||||||
var req *http.Request
|
var req *http.Request
|
||||||
if body != nil {
|
if body != nil {
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ func UpdatePatchTest(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
query := "UPDATE patch_tests SET "
|
query := "UPDATE patch_tests SET "
|
||||||
args := []interface{}{}
|
args := []any{}
|
||||||
i := 1
|
i := 1
|
||||||
|
|
||||||
if req.Name != nil {
|
if req.Name != nil {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package admin
|
package admin
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package admin
|
package admin
|
||||||
|
|
||||||
@@ -19,9 +18,9 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"crussell/testutils"
|
|
||||||
"crussell/handlers/services"
|
"crussell/handlers/services"
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
|
"crussell/testutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestAdminServices_Create verifies that an admin can create a new service
|
// TestAdminServices_Create verifies that an admin can create a new service
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BusinessSettings struct {
|
type BusinessSettings struct {
|
||||||
@@ -67,7 +68,6 @@ func GetPublicBusinessInfo(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(info)
|
json.NewEncoder(w).Encode(info)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +90,6 @@ func GetBusinessSettings(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(s)
|
json.NewEncoder(w).Encode(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +171,7 @@ func UpdateBusinessSettings(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setClauses := []string{}
|
setClauses := []string{}
|
||||||
args := []interface{}{}
|
args := []any{}
|
||||||
argIdx := 1
|
argIdx := 1
|
||||||
|
|
||||||
if req.BusinessName != nil {
|
if req.BusinessName != nil {
|
||||||
@@ -231,12 +230,13 @@ func UpdateBusinessSettings(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
query := "UPDATE business_settings SET "
|
var query strings.Builder
|
||||||
|
query.WriteString("UPDATE business_settings SET ")
|
||||||
for i, clause := range setClauses {
|
for i, clause := range setClauses {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
query += ", "
|
query.WriteString(", ")
|
||||||
}
|
}
|
||||||
query += clause
|
query.WriteString(clause)
|
||||||
}
|
}
|
||||||
|
|
||||||
tx, err := db.Conn.Begin(r.Context())
|
tx, err := db.Conn.Begin(r.Context())
|
||||||
@@ -247,7 +247,7 @@ func UpdateBusinessSettings(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
defer tx.Rollback(r.Context())
|
defer tx.Rollback(r.Context())
|
||||||
|
|
||||||
_, err = tx.Exec(r.Context(), query, args...)
|
_, err = tx.Exec(r.Context(), query.String(), args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to update business settings: %v", err)
|
log.Printf("Failed to update business settings: %v", err)
|
||||||
http.Error(w, "Failed to update settings", http.StatusInternalServerError)
|
http.Error(w, "Failed to update settings", http.StatusInternalServerError)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package admin
|
package admin
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package admin
|
package admin
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package admin
|
package admin
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package admin
|
package admin
|
||||||
|
|
||||||
@@ -24,10 +23,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"crussell/clock"
|
"crussell/clock"
|
||||||
"crussell/testutils"
|
|
||||||
"crussell/handlers/notifications"
|
"crussell/handlers/notifications"
|
||||||
"crussell/handlers/today"
|
"crussell/handlers/today"
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
|
"crussell/testutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestAdminToday_CurrentNext verifies that an admin can retrieve the currently
|
// TestAdminToday_CurrentNext verifies that an admin can retrieve the currently
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package admin
|
package admin
|
||||||
|
|
||||||
@@ -22,8 +21,8 @@ import (
|
|||||||
|
|
||||||
"crussell/clock"
|
"crussell/clock"
|
||||||
"crussell/db"
|
"crussell/db"
|
||||||
"crussell/testutils"
|
|
||||||
"crussell/handlers/bookings"
|
"crussell/handlers/bookings"
|
||||||
|
"crussell/testutils"
|
||||||
"crussell/testutils/fixtures"
|
"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) {
|
func TestAdminBookings_UpdateServices_ReplaceServices(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
_, err := fixtures.CreateTestAdminUser(tx)
|
_, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -129,7 +127,6 @@ func TestAdminBookings_UpdateServices_ReplaceServices(t *testing.T) {
|
|||||||
func TestAdminBookings_UpdateServices_AddService(t *testing.T) {
|
func TestAdminBookings_UpdateServices_AddService(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
_, err := fixtures.CreateTestAdminUser(tx)
|
_, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -175,7 +172,6 @@ func TestAdminBookings_UpdateServices_AddService(t *testing.T) {
|
|||||||
func TestAdminBookings_UpdateServices_RemoveService(t *testing.T) {
|
func TestAdminBookings_UpdateServices_RemoveService(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
_, err := fixtures.CreateTestAdminUser(tx)
|
_, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -233,7 +229,6 @@ func TestAdminBookings_UpdateServices_RemoveService(t *testing.T) {
|
|||||||
func TestAdminBookings_UpdateServices_WithPriceOverride(t *testing.T) {
|
func TestAdminBookings_UpdateServices_WithPriceOverride(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
_, err := fixtures.CreateTestAdminUser(tx)
|
_, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -286,7 +281,6 @@ func TestAdminBookings_UpdateServices_WithPriceOverride(t *testing.T) {
|
|||||||
func TestAdminBookings_UpdateServices_WithDurationOverride(t *testing.T) {
|
func TestAdminBookings_UpdateServices_WithDurationOverride(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
_, err := fixtures.CreateTestAdminUser(tx)
|
_, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -339,7 +333,6 @@ func TestAdminBookings_UpdateServices_WithDurationOverride(t *testing.T) {
|
|||||||
func TestAdminBookings_UpdateServices_WithBothOverrides(t *testing.T) {
|
func TestAdminBookings_UpdateServices_WithBothOverrides(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
_, err := fixtures.CreateTestAdminUser(tx)
|
_, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -393,7 +386,6 @@ func TestAdminBookings_UpdateServices_WithBothOverrides(t *testing.T) {
|
|||||||
func TestAdminBookings_UpdateServices_UpdateNotes(t *testing.T) {
|
func TestAdminBookings_UpdateServices_UpdateNotes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
_, err := fixtures.CreateTestAdminUser(tx)
|
_, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -438,7 +430,6 @@ func TestAdminBookings_UpdateServices_UpdateNotes(t *testing.T) {
|
|||||||
func TestAdminBookings_UpdateServices_MultipleOverrides(t *testing.T) {
|
func TestAdminBookings_UpdateServices_MultipleOverrides(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
_, err := fixtures.CreateTestAdminUser(tx)
|
_, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -881,7 +872,6 @@ func TestAdminBookings_UpdateServices_WeCancelledBookingRejected(t *testing.T) {
|
|||||||
func TestAdminBookings_UpdateServices_OverlapWithNextBooking(t *testing.T) {
|
func TestAdminBookings_UpdateServices_OverlapWithNextBooking(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
_, err := fixtures.CreateTestAdminUser(tx)
|
_, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -927,7 +917,6 @@ func TestAdminBookings_UpdateServices_OverlapWithNextBooking(t *testing.T) {
|
|||||||
func TestAdminBookings_UpdateServices_NoOverlapSucceeds(t *testing.T) {
|
func TestAdminBookings_UpdateServices_NoOverlapSucceeds(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
_, err := fixtures.CreateTestAdminUser(tx)
|
_, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -972,7 +961,6 @@ func TestAdminBookings_UpdateServices_NoOverlapSucceeds(t *testing.T) {
|
|||||||
func TestAdminBookings_UpdateServices_NoNextBookingSucceeds(t *testing.T) {
|
func TestAdminBookings_UpdateServices_NoNextBookingSucceeds(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
_, err := fixtures.CreateTestAdminUser(tx)
|
_, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1014,7 +1002,6 @@ func TestAdminBookings_UpdateServices_NoNextBookingSucceeds(t *testing.T) {
|
|||||||
func TestAdminBookings_UpdateServices_ResponseShape(t *testing.T) {
|
func TestAdminBookings_UpdateServices_ResponseShape(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
_, err := fixtures.CreateTestAdminUser(tx)
|
_, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1082,7 +1069,6 @@ func TestAdminBookings_UpdateServices_ResponseShape(t *testing.T) {
|
|||||||
func TestAdminBookings_UpdateServices_PendingBooking(t *testing.T) {
|
func TestAdminBookings_UpdateServices_PendingBooking(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
_, err := fixtures.CreateTestAdminUser(tx)
|
_, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1117,7 +1103,6 @@ func TestAdminBookings_UpdateServices_PendingBooking(t *testing.T) {
|
|||||||
func TestAdminBookings_UpdateServices_InProgressBooking(t *testing.T) {
|
func TestAdminBookings_UpdateServices_InProgressBooking(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
_, err := fixtures.CreateTestAdminUser(tx)
|
_, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1152,7 +1137,6 @@ func TestAdminBookings_UpdateServices_InProgressBooking(t *testing.T) {
|
|||||||
func TestAdminBookings_UpdateServices_ClearNotes(t *testing.T) {
|
func TestAdminBookings_UpdateServices_ClearNotes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
_, err := fixtures.CreateTestAdminUser(tx)
|
_, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package admin
|
package admin
|
||||||
|
|
||||||
@@ -21,9 +20,9 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"crussell/testutils"
|
|
||||||
"crussell/handlers/user"
|
"crussell/handlers/user"
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
|
"crussell/testutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestAdminUsers_List verifies that an admin can list all users in the
|
// TestAdminUsers_List verifies that an admin can list all users in the
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
@@ -357,7 +356,6 @@ func TestLogin_Success(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer fixtures.DeleteUser(tx, userID)
|
defer fixtures.DeleteUser(tx, userID)
|
||||||
|
|
||||||
|
|
||||||
body := LoginRequest{
|
body := LoginRequest{
|
||||||
Email: "user@test.com",
|
Email: "user@test.com",
|
||||||
Password: "testpassword123",
|
Password: "testpassword123",
|
||||||
@@ -1396,7 +1394,6 @@ func TestLoginResponse_IncludesJTI(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer fixtures.DeleteUser(tx, userID)
|
defer fixtures.DeleteUser(tx, userID)
|
||||||
|
|
||||||
|
|
||||||
body := LoginRequest{
|
body := LoginRequest{
|
||||||
Email: "jti-test@test.com",
|
Email: "jti-test@test.com",
|
||||||
Password: "testpassword123",
|
Password: "testpassword123",
|
||||||
@@ -1599,7 +1596,6 @@ func TestLogin_AccountLockout_ResetsOnSuccess(t *testing.T) {
|
|||||||
t.Fatalf("failed to set failed_attempts: %v", err)
|
t.Fatalf("failed to set failed_attempts: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
body := LoginRequest{
|
body := LoginRequest{
|
||||||
Email: "lockout-reset@test.com",
|
Email: "lockout-reset@test.com",
|
||||||
Password: "testpassword123",
|
Password: "testpassword123",
|
||||||
@@ -1764,7 +1760,6 @@ func TestLogin_ResponseIncludesRefreshToken(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer fixtures.DeleteUser(tx, userID)
|
defer fixtures.DeleteUser(tx, userID)
|
||||||
|
|
||||||
|
|
||||||
body := LoginRequest{
|
body := LoginRequest{
|
||||||
Email: "refresh-check@test.com",
|
Email: "refresh-check@test.com",
|
||||||
Password: "testpassword123",
|
Password: "testpassword123",
|
||||||
@@ -1933,9 +1928,9 @@ func TestCleanupStaleLoginEntries_Mixed(t *testing.T) {
|
|||||||
loginStateMu.Lock()
|
loginStateMu.Lock()
|
||||||
saved := loginInProgress
|
saved := loginInProgress
|
||||||
loginInProgress = map[string]time.Time{
|
loginInProgress = map[string]time.Time{
|
||||||
"stale-user": clock.Now().Add(-60 * time.Second),
|
"stale-user": clock.Now().Add(-60 * time.Second),
|
||||||
"recent-user": clock.Now().Add(-5 * time.Second),
|
"recent-user": clock.Now().Add(-5 * time.Second),
|
||||||
"borderline": clock.Now().Add(-29 * time.Second), // Just under 30s threshold
|
"borderline": clock.Now().Add(-29 * time.Second), // Just under 30s threshold
|
||||||
}
|
}
|
||||||
loginStateMu.Unlock()
|
loginStateMu.Unlock()
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test && dev
|
//go:build test && dev
|
||||||
// +build test,dev
|
|
||||||
|
|
||||||
package bookings
|
package bookings
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test && dev
|
//go:build test && dev
|
||||||
// +build test,dev
|
|
||||||
|
|
||||||
package bookings
|
package bookings
|
||||||
|
|
||||||
@@ -20,8 +19,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"crussell/clock"
|
"crussell/clock"
|
||||||
"crussell/testutils"
|
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
|
"crussell/testutils"
|
||||||
"crussell/testutils/fixtures"
|
"crussell/testutils/fixtures"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
@@ -68,7 +67,6 @@ func TestAdminReserveSlot_WalkIn_Success(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create admin: %v", err)
|
t.Fatalf("failed to create admin: %v", err)
|
||||||
@@ -126,7 +124,6 @@ func TestAdminReserveSlot_CallIn_Success(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create admin: %v", err)
|
t.Fatalf("failed to create admin: %v", err)
|
||||||
@@ -202,8 +199,6 @@ func TestAdminReserveSlot_WalkIn_MissingDuration(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create admin: %v", err)
|
t.Fatalf("failed to create admin: %v", err)
|
||||||
@@ -241,8 +236,6 @@ func TestAdminReserveSlot_CallIn_MissingServices(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create admin: %v", err)
|
t.Fatalf("failed to create admin: %v", err)
|
||||||
@@ -283,8 +276,6 @@ func TestAdminReserveSlot_InvalidReservationType(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create admin: %v", err)
|
t.Fatalf("failed to create admin: %v", err)
|
||||||
@@ -321,8 +312,6 @@ func TestAdminReserveSlot_SlotOverlap(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Create test admin user (for the booking)
|
// Create test admin user (for the booking)
|
||||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -394,8 +383,6 @@ func TestAdminReserveSlot_ReplacesExisting(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create admin: %v", err)
|
t.Fatalf("failed to create admin: %v", err)
|
||||||
@@ -483,8 +470,6 @@ func TestAdminReserveSlot_WalkIn_PastStart(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create admin: %v", err)
|
t.Fatalf("failed to create admin: %v", err)
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package bookings
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crussell/db"
|
|
||||||
"crussell/clock"
|
"crussell/clock"
|
||||||
|
"crussell/db"
|
||||||
"crussell/handlers/notifications"
|
"crussell/handlers/notifications"
|
||||||
"crussell/handlers/payments"
|
"crussell/handlers/payments"
|
||||||
"crussell/handlers/scheduling"
|
"crussell/handlers/scheduling"
|
||||||
@@ -254,20 +254,20 @@ type AdminBookingSummary struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UserSummary struct {
|
type UserSummary struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
FirstName string `json:"first_name"`
|
FirstName string `json:"first_name"`
|
||||||
LastName string `json:"last_name"`
|
LastName string `json:"last_name"`
|
||||||
FullName string `json:"full_name"`
|
FullName string `json:"full_name"`
|
||||||
Email *string `json:"email,omitempty"`
|
Email *string `json:"email,omitempty"`
|
||||||
Phone *string `json:"phone,omitempty"`
|
Phone *string `json:"phone,omitempty"`
|
||||||
ProfilePicURL *string `json:"profile_pic_url,omitempty"`
|
ProfilePicURL *string `json:"profile_pic_url,omitempty"`
|
||||||
DateOfBirth *string `json:"date_of_birth,omitempty"`
|
DateOfBirth *string `json:"date_of_birth,omitempty"`
|
||||||
AccountRole string `json:"account_role"`
|
AccountRole string `json:"account_role"`
|
||||||
LoyaltyStamps *int `json:"loyalty_stamps,omitempty"`
|
LoyaltyStamps *int `json:"loyalty_stamps,omitempty"`
|
||||||
ReferralCode *string `json:"referral_code,omitempty"`
|
ReferralCode *string `json:"referral_code,omitempty"`
|
||||||
ReferralCodeUses *int `json:"referral_code_uses,omitempty"`
|
ReferralCodeUses *int `json:"referral_code_uses,omitempty"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
Notes *string `json:"notes,omitempty" validate:"omitempty,max=1000000"`
|
Notes *string `json:"notes,omitempty" validate:"omitempty,max=1000000"`
|
||||||
PreviousFirstName *string `json:"previous_first_name,omitempty"`
|
PreviousFirstName *string `json:"previous_first_name,omitempty"`
|
||||||
PreviousLastName *string `json:"previous_last_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.
|
// Build WHERE conditions shared between count and data queries.
|
||||||
whereClause := " WHERE b.user_id = $1"
|
whereClause := " WHERE b.user_id = $1"
|
||||||
whereArgs := []interface{}{userID}
|
whereArgs := []any{userID}
|
||||||
paramCount := 2
|
paramCount := 2
|
||||||
|
|
||||||
if req.Status != nil {
|
if req.Status != nil {
|
||||||
@@ -447,7 +447,7 @@ func GetAllUserBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
WHERE booking_id = b.id
|
WHERE booking_id = b.id
|
||||||
) pt ON true` + whereClause
|
) pt ON true` + whereClause
|
||||||
|
|
||||||
dataArgs := make([]interface{}, len(whereArgs))
|
dataArgs := make([]any, len(whereArgs))
|
||||||
copy(dataArgs, whereArgs)
|
copy(dataArgs, whereArgs)
|
||||||
dataParamCount := paramCount
|
dataParamCount := paramCount
|
||||||
|
|
||||||
@@ -683,7 +683,7 @@ func GetAllAdminBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
) pre_pay ON true
|
) pre_pay ON true
|
||||||
`
|
`
|
||||||
|
|
||||||
var args []interface{}
|
var args []any
|
||||||
paramCount := 1
|
paramCount := 1
|
||||||
|
|
||||||
addWhereClause := func(condition string) {
|
addWhereClause := func(condition string) {
|
||||||
@@ -750,7 +750,7 @@ func GetAllAdminBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
var total int
|
var total int
|
||||||
{
|
{
|
||||||
countWhere := ""
|
countWhere := ""
|
||||||
var countArgs []interface{}
|
var countArgs []any
|
||||||
cp := 1
|
cp := 1
|
||||||
addCountWhere := func(cond string) {
|
addCountWhere := func(cond string) {
|
||||||
if cp == 1 {
|
if cp == 1 {
|
||||||
@@ -954,7 +954,7 @@ func GetAllBookingsByUserHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
FROM bookings b
|
FROM bookings b
|
||||||
WHERE b.user_id = $1
|
WHERE b.user_id = $1
|
||||||
`
|
`
|
||||||
var args []interface{}
|
var args []any
|
||||||
args = append(args, userID)
|
args = append(args, userID)
|
||||||
paramCount := 2
|
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 '\'
|
WHERE bcs.booking_id = b.id AND cs.name ILIKE $1 ESCAPE '\'
|
||||||
)
|
)
|
||||||
`
|
`
|
||||||
var args []interface{}
|
var args []any
|
||||||
args = append(args, searchPattern)
|
args = append(args, searchPattern)
|
||||||
paramCount := 2
|
paramCount := 2
|
||||||
|
|
||||||
@@ -2639,9 +2639,9 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if currentStatus == "completed" {
|
if currentStatus == "completed" {
|
||||||
log.Printf("Booking %s is already completed — skipping duplicate completion", bookingID)
|
log.Printf("Booking %s is already completed — skipping duplicate completion", bookingID)
|
||||||
} else {
|
} else {
|
||||||
// Collect patch test IDs first so the rows are consumed before INSERT operations.
|
// Collect patch test IDs first so the rows are consumed before INSERT operations.
|
||||||
var patchTestIDs []string
|
var patchTestIDs []string
|
||||||
ptRows, err := tx.Query(r.Context(), `
|
ptRows, err := tx.Query(r.Context(), `
|
||||||
SELECT DISTINCT pt.id
|
SELECT DISTINCT pt.id
|
||||||
FROM patch_tests pt
|
FROM patch_tests pt
|
||||||
JOIN booking_services bs ON bs.booking_id = $1
|
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)
|
SELECT pt_inner.id FROM patch_tests pt_inner WHERE bs.service_id = ANY(pt_inner.service_ids)
|
||||||
)
|
)
|
||||||
`, bookingID)
|
`, bookingID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to fetch patch tests for booking %s: %v", bookingID, err)
|
log.Printf("Failed to fetch patch tests for booking %s: %v", bookingID, err)
|
||||||
} else {
|
} else {
|
||||||
for ptRows.Next() {
|
for ptRows.Next() {
|
||||||
var ptID string
|
var ptID string
|
||||||
if err := ptRows.Scan(&ptID); err == nil {
|
if err := ptRows.Scan(&ptID); err == nil {
|
||||||
patchTestIDs = append(patchTestIDs, ptID)
|
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)
|
INSERT INTO user_patch_tests (user_id, patch_test_id, tested_at)
|
||||||
VALUES ($1, $2, NOW())
|
VALUES ($1, $2, NOW())
|
||||||
ON CONFLICT (user_id, patch_test_id) DO UPDATE SET tested_at = NOW()
|
ON CONFLICT (user_id, patch_test_id) DO UPDATE SET tested_at = NOW()
|
||||||
`, booking.User.ID, ptID); err != nil {
|
`, 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
|
var bookingTotal float64
|
||||||
if err := tx.QueryRow(r.Context(), `
|
if err := tx.QueryRow(r.Context(), `
|
||||||
SELECT total_amount FROM bookings WHERE id = $1
|
SELECT total_amount FROM bookings WHERE id = $1
|
||||||
`, bookingID).Scan(&bookingTotal); err != nil {
|
`, 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
|
// Don't award a stamp if this booking already used a loyalty redemption
|
||||||
// (take or receive, never both).
|
// (take or receive, never both).
|
||||||
var loyaltyAppliedOnThisBooking bool
|
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)
|
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
|
var newStampCount int
|
||||||
if bookingTotal > 0 && !loyaltyAppliedOnThisBooking {
|
if bookingTotal > 0 && !loyaltyAppliedOnThisBooking {
|
||||||
if err := tx.QueryRow(r.Context(), `
|
if err := tx.QueryRow(r.Context(), `
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET loyalty_stamps = loyalty_stamps + 1
|
SET loyalty_stamps = loyalty_stamps + 1
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
@@ -2697,108 +2697,108 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
)
|
)
|
||||||
RETURNING loyalty_stamps
|
RETURNING loyalty_stamps
|
||||||
`, booking.User.ID, bookingID).Scan(&newStampCount); err != nil {
|
`, booking.User.ID, bookingID).Scan(&newStampCount); err != nil {
|
||||||
if !errors.Is(err, pgx.ErrNoRows) {
|
if !errors.Is(err, pgx.ErrNoRows) {
|
||||||
log.Printf("Failed to add loyalty stamp for booking %s: %v", bookingID, err)
|
log.Printf("Failed to add loyalty stamp for booking %s: %v", bookingID, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Create pending redemption when stamps reach LoyaltyStampCost
|
// Create pending redemption when stamps reach LoyaltyStampCost
|
||||||
if newStampCount == payments.LoyaltyStampCost {
|
if newStampCount == payments.LoyaltyStampCost {
|
||||||
_, err = tx.Exec(r.Context(), `
|
_, err = tx.Exec(r.Context(), `
|
||||||
INSERT INTO loyalty_redemptions (user_id, stamps_redeemed, status, redeemed_at)
|
INSERT INTO loyalty_redemptions (user_id, stamps_redeemed, status, redeemed_at)
|
||||||
VALUES ($1, $2, 'pending', NOW())
|
VALUES ($1, $2, 'pending', NOW())
|
||||||
`, booking.User.ID, payments.LoyaltyStampCost)
|
`, booking.User.ID, payments.LoyaltyStampCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to create loyalty redemption for user %s: %v", booking.User.ID, err)
|
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
|
// Skip time-based campaign if already applied at payment time
|
||||||
var timeBasedApplied bool
|
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)
|
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 {
|
if bookingTotal > 0 && !timeBasedApplied {
|
||||||
var campaignID string
|
var campaignID string
|
||||||
var campaignPercent float64
|
var campaignPercent float64
|
||||||
if err := tx.QueryRow(r.Context(), `
|
if err := tx.QueryRow(r.Context(), `
|
||||||
SELECT id, discount_percent FROM discount_campaigns
|
SELECT id, discount_percent FROM discount_campaigns
|
||||||
WHERE status = 'active' AND campaign_type = 'time_based'
|
WHERE status = 'active' AND campaign_type = 'time_based'
|
||||||
AND start_date <= NOW() AND end_date >= NOW()
|
AND start_date <= NOW() AND end_date >= NOW()
|
||||||
AND (max_redemptions IS NULL OR times_redeemed < max_redemptions)
|
AND (max_redemptions IS NULL OR times_redeemed < max_redemptions)
|
||||||
ORDER BY discount_percent DESC LIMIT 1
|
ORDER BY discount_percent DESC LIMIT 1
|
||||||
`).Scan(&campaignID, &campaignPercent); err == nil && campaignID != "" {
|
`).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)
|
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)
|
VALUES ($1, $2, 'campaign', $3, 'time_based', NULL, $4, $5, $6)
|
||||||
`, bookingID, booking.User.ID, campaignID, campaignPercent, bookingTotal, discountAmount); err != nil {
|
`, 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)
|
INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_by)
|
||||||
VALUES ($1, 'partial', 'discount', $2, 'completed', $3)
|
VALUES ($1, 'partial', 'discount', $2, 'completed', $3)
|
||||||
`, bookingID, discountAmount, booking.User.ID); err != nil {
|
`, 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
|
UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = $1
|
||||||
`, campaignID); err != nil {
|
`, 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 {
|
if bookingTotal > 0 {
|
||||||
var userBookingCount int
|
var userBookingCount int
|
||||||
_ = tx.QueryRow(r.Context(), `SELECT COUNT(*) FROM bookings WHERE user_id = $1 AND status = 'completed'`, booking.User.ID).Scan(&userBookingCount)
|
_ = tx.QueryRow(r.Context(), `SELECT COUNT(*) FROM bookings WHERE user_id = $1 AND status = 'completed'`, booking.User.ID).Scan(&userBookingCount)
|
||||||
|
|
||||||
var milestoneCampaignID string
|
var milestoneCampaignID string
|
||||||
var milestonePercent float64
|
var milestonePercent float64
|
||||||
_ = tx.QueryRow(r.Context(), `
|
_ = tx.QueryRow(r.Context(), `
|
||||||
SELECT id, discount_percent FROM discount_campaigns
|
SELECT id, discount_percent FROM discount_campaigns
|
||||||
WHERE status = 'active' AND campaign_type = 'milestone' AND milestone_type = 'per_user_booking_count'
|
WHERE status = 'active' AND campaign_type = 'milestone' AND milestone_type = 'per_user_booking_count'
|
||||||
AND milestone_value = $1
|
AND milestone_value = $1
|
||||||
AND NOT EXISTS (SELECT 1 FROM booking_discounts WHERE user_id = $2 AND source_id = discount_campaigns.id)
|
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)
|
`, userBookingCount, booking.User.ID).Scan(&milestoneCampaignID, &milestonePercent)
|
||||||
|
|
||||||
if milestoneCampaignID != "" {
|
if milestoneCampaignID != "" {
|
||||||
discountAmount := roundTo2(bookingTotal * milestonePercent / 100)
|
discountAmount := roundTo2(bookingTotal * milestonePercent / 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)
|
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)
|
VALUES ($1, $2, 'campaign', $3, 'milestone', 'per_user_booking_count', $4, $5, $6)
|
||||||
`, bookingID, booking.User.ID, milestoneCampaignID, milestonePercent, bookingTotal, discountAmount); err != nil {
|
`, bookingID, booking.User.ID, milestoneCampaignID, milestonePercent, 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)
|
INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_by)
|
||||||
VALUES ($1, 'partial', 'discount', $2, 'completed', $3)
|
VALUES ($1, 'partial', 'discount', $2, 'completed', $3)
|
||||||
`, bookingID, discountAmount, booking.User.ID); err != nil {
|
`, 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
|
UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = $1
|
||||||
`, milestoneCampaignID); err != nil {
|
`, 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
|
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)
|
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 {
|
if !globalMilestoneApplied {
|
||||||
var globalCount int
|
var globalCount int
|
||||||
_ = tx.QueryRow(r.Context(), `SELECT COUNT(*) FROM bookings WHERE status = 'completed'`).Scan(&globalCount)
|
_ = tx.QueryRow(r.Context(), `SELECT COUNT(*) FROM bookings WHERE status = 'completed'`).Scan(&globalCount)
|
||||||
|
|
||||||
var hasInPersonPayment bool
|
var hasInPersonPayment bool
|
||||||
tx.QueryRow(r.Context(), `
|
tx.QueryRow(r.Context(), `
|
||||||
SELECT EXISTS(SELECT 1 FROM payments WHERE booking_id = $1 AND payment_method = 'in_person_card')`, bookingID).Scan(&hasInPersonPayment)
|
SELECT EXISTS(SELECT 1 FROM payments WHERE booking_id = $1 AND payment_method = 'in_person_card')`, bookingID).Scan(&hasInPersonPayment)
|
||||||
|
|
||||||
if hasInPersonPayment {
|
if hasInPersonPayment {
|
||||||
var globalCampaignID string
|
var globalCampaignID string
|
||||||
var globalPercent float64
|
var globalPercent float64
|
||||||
_ = tx.QueryRow(r.Context(), `
|
_ = tx.QueryRow(r.Context(), `
|
||||||
SELECT id, discount_percent FROM discount_campaigns
|
SELECT id, discount_percent FROM discount_campaigns
|
||||||
WHERE status = 'active' AND campaign_type = 'milestone' AND milestone_type = 'global_booking_count'
|
WHERE status = 'active' AND campaign_type = 'milestone' AND milestone_type = 'global_booking_count'
|
||||||
AND milestone_value <= $1
|
AND milestone_value <= $1
|
||||||
@@ -2806,126 +2806,126 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
ORDER BY milestone_value DESC LIMIT 1
|
ORDER BY milestone_value DESC LIMIT 1
|
||||||
`, globalCount).Scan(&globalCampaignID, &globalPercent)
|
`, globalCount).Scan(&globalCampaignID, &globalPercent)
|
||||||
|
|
||||||
if globalCampaignID != "" {
|
if globalCampaignID != "" {
|
||||||
discountAmount := roundTo2(bookingTotal * globalPercent / 100)
|
discountAmount := roundTo2(bookingTotal * globalPercent / 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)
|
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)
|
VALUES ($1, $2, 'campaign', $3, 'milestone', 'global_booking_count', $4, $5, $6)
|
||||||
`, bookingID, booking.User.ID, globalCampaignID, globalPercent, bookingTotal, discountAmount); err != nil {
|
`, bookingID, booking.User.ID, globalCampaignID, globalPercent, 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)
|
INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_by)
|
||||||
VALUES ($1, 'partial', 'discount', $2, 'completed', $3)
|
VALUES ($1, 'partial', 'discount', $2, 'completed', $3)
|
||||||
`, bookingID, discountAmount, booking.User.ID); err != nil {
|
`, 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
|
UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = $1
|
||||||
`, globalCampaignID); err != nil {
|
`, 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
|
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)
|
_ = 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() {
|
if !firstVisitDate.IsZero() {
|
||||||
annRows, err := tx.Query(r.Context(), `
|
annRows, err := tx.Query(r.Context(), `
|
||||||
SELECT id, discount_percent, milestone_value, milestone_unit FROM discount_campaigns
|
SELECT id, discount_percent, milestone_value, milestone_unit FROM discount_campaigns
|
||||||
WHERE status = 'active' AND campaign_type = 'milestone' AND milestone_type = 'anniversary'
|
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')
|
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)
|
`, booking.User.ID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Collect anniversary campaigns first to avoid interleaving rows with writes.
|
// Collect anniversary campaigns first to avoid interleaving rows with writes.
|
||||||
type annCampaign struct {
|
type annCampaign struct {
|
||||||
id string
|
id string
|
||||||
pct float64
|
pct float64
|
||||||
value int
|
value int
|
||||||
unit string
|
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)
|
|
||||||
}
|
}
|
||||||
}
|
var campaigns []annCampaign
|
||||||
annRows.Close()
|
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 by milestone_value descending so we apply the longest anniversary only
|
||||||
sort.Slice(campaigns, func(i, j int) bool {
|
sort.Slice(campaigns, func(i, j int) bool {
|
||||||
return campaigns[i].value > campaigns[j].value
|
return campaigns[i].value > campaigns[j].value
|
||||||
})
|
})
|
||||||
for _, c := range campaigns {
|
for _, c := range campaigns {
|
||||||
var matches bool
|
var matches bool
|
||||||
elapsed := time.Since(firstVisitDate)
|
elapsed := time.Since(firstVisitDate)
|
||||||
switch c.unit {
|
switch c.unit {
|
||||||
case "months":
|
case "months":
|
||||||
months := int(elapsed.Hours() / (30 * 24))
|
months := int(elapsed.Hours() / (30 * 24))
|
||||||
matches = months >= c.value
|
matches = months >= c.value
|
||||||
case "years":
|
case "years":
|
||||||
years := int(elapsed.Hours() / (365.25 * 24))
|
years := int(elapsed.Hours() / (365.25 * 24))
|
||||||
matches = years >= c.value
|
matches = years >= c.value
|
||||||
}
|
}
|
||||||
if matches {
|
if matches {
|
||||||
discountAmount := roundTo2(bookingTotal * c.pct / 100)
|
discountAmount := roundTo2(bookingTotal * c.pct / 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)
|
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)
|
VALUES ($1, $2, 'campaign', $3, 'milestone', 'anniversary', $4, $5, $6)
|
||||||
`, bookingID, booking.User.ID, c.id, c.pct, bookingTotal, discountAmount); err != nil {
|
`, bookingID, booking.User.ID, c.id, c.pct, 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)
|
INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_by)
|
||||||
VALUES ($1, 'partial', 'discount', $2, 'completed', $3)
|
VALUES ($1, 'partial', 'discount', $2, 'completed', $3)
|
||||||
`, bookingID, discountAmount, booking.User.ID); err != nil {
|
`, 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
|
UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = $1
|
||||||
`, c.id); err != nil {
|
`, 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
|
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 {
|
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
|
var newDepositsRequired int
|
||||||
if err := tx.QueryRow(r.Context(), `
|
if err := tx.QueryRow(r.Context(), `
|
||||||
UPDATE users SET deposits_required = GREATEST(0, deposits_required - 1)
|
UPDATE users SET deposits_required = GREATEST(0, deposits_required - 1)
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
RETURNING deposits_required
|
RETURNING deposits_required
|
||||||
`, booking.User.ID).Scan(&newDepositsRequired); err != nil {
|
`, booking.User.ID).Scan(&newDepositsRequired); err != nil {
|
||||||
log.Printf("ALERT: failed to update deposits_required: %v", err)
|
log.Printf("ALERT: failed to update deposits_required: %v", err)
|
||||||
} else if newDepositsRequired == 0 {
|
} else if newDepositsRequired == 0 {
|
||||||
// After 3 paid bookings, forget no-shows so the counter resets.
|
// After 3 paid bookings, forget no-shows so the counter resets.
|
||||||
if _, err := tx.Exec(r.Context(), `
|
if _, err := tx.Exec(r.Context(), `
|
||||||
INSERT INTO forgiven_no_shows (booking_id)
|
INSERT INTO forgiven_no_shows (booking_id)
|
||||||
SELECT id FROM bookings
|
SELECT id FROM bookings
|
||||||
WHERE user_id = $1 AND status = 'no_show'
|
WHERE user_id = $1 AND status = 'no_show'
|
||||||
AND start_time >= NOW() - INTERVAL '6 months'
|
AND start_time >= NOW() - INTERVAL '6 months'
|
||||||
AND NOT EXISTS (SELECT 1 FROM forgiven_no_shows WHERE booking_id = bookings.id)
|
AND NOT EXISTS (SELECT 1 FROM forgiven_no_shows WHERE booking_id = bookings.id)
|
||||||
`, booking.User.ID); err != nil {
|
`, 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
|
||||||
// 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.
|
||||||
// booking" that completes. After this, we no longer show "formerly" on displays.
|
if _, err := tx.Exec(r.Context(), `
|
||||||
if _, err := tx.Exec(r.Context(), `
|
|
||||||
UPDATE name_history SET booking_id = $1
|
UPDATE name_history SET booking_id = $1
|
||||||
WHERE user_id = $2 AND booking_id IS NULL
|
WHERE user_id = $2 AND booking_id IS NULL
|
||||||
`, bookingID, booking.User.ID); err != nil {
|
`, bookingID, booking.User.ID); err != nil {
|
||||||
log.Printf("Failed to consume name_history for user %s: %v", booking.User.ID, err)
|
log.Printf("Failed to consume name_history for user %s: %v", booking.User.ID, err)
|
||||||
}
|
}
|
||||||
} // close the else from alreadyCompleted check
|
} // close the else from alreadyCompleted check
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit(r.Context()); err != nil {
|
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",
|
"message": "Booking cancelled successfully",
|
||||||
"id": bookingID,
|
"id": bookingID,
|
||||||
"status": req.Reason,
|
"status": req.Reason,
|
||||||
@@ -3336,7 +3336,7 @@ func DeleteBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
"message": "Booking deleted successfully",
|
"message": "Booking deleted successfully",
|
||||||
"id": bookingID,
|
"id": bookingID,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test && dev
|
//go:build test && dev
|
||||||
// +build test,dev
|
|
||||||
|
|
||||||
package bookings
|
package bookings
|
||||||
|
|
||||||
@@ -30,10 +29,10 @@ import (
|
|||||||
|
|
||||||
"crussell/clock"
|
"crussell/clock"
|
||||||
"crussell/db"
|
"crussell/db"
|
||||||
"crussell/testutils"
|
|
||||||
"crussell/handlers/user"
|
"crussell/handlers/user"
|
||||||
"crussell/internal/validators"
|
"crussell/internal/validators"
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
|
"crussell/testutils"
|
||||||
"crussell/testutils/fixtures"
|
"crussell/testutils/fixtures"
|
||||||
"crussell/testutils/jwt"
|
"crussell/testutils/jwt"
|
||||||
|
|
||||||
@@ -217,7 +216,6 @@ func TestBookings_Create(t *testing.T) {
|
|||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
// Seed working hours for booking tests
|
// Seed working hours for booking tests
|
||||||
|
|
||||||
|
|
||||||
// Create test user and service
|
// Create test user and service
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
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 no-show (deposits = 3)
|
||||||
// - Cancellation >= 24 hours before appointment: treated as late_cancellation
|
// - Cancellation >= 24 hours before appointment: treated as late_cancellation
|
||||||
func TestBookings_Delete_NoShow24hThreshold(t *testing.T) {
|
func TestBookings_Delete_NoShow24hThreshold(t *testing.T) {
|
||||||
|
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
|
||||||
|
|
||||||
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
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
|
// TestBookings_Delete_NoShow_WithForgiveness tests that admin can forgive a no-show
|
||||||
func TestBookings_Delete_NoShow_WithForgiveness(t *testing.T) {
|
func TestBookings_Delete_NoShow_WithForgiveness(t *testing.T) {
|
||||||
|
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
|
||||||
|
|
||||||
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1459,7 +1453,6 @@ func TestBookings_Create_MinimumAdvance(t *testing.T) {
|
|||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
// Seed working hours for booking tests
|
// Seed working hours for booking tests
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1513,7 +1506,6 @@ func TestBookings_Create_WithNotes_StatusPending(t *testing.T) {
|
|||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
// Seed working hours
|
// Seed working hours
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1567,7 +1559,6 @@ func TestBookings_Create_WithoutNotes_StatusConfirmed(t *testing.T) {
|
|||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
// Seed working hours
|
// Seed working hours
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1615,7 +1606,6 @@ func TestBookings_Create_Within1Hour_ShouldFail(t *testing.T) {
|
|||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
// Seed working hours
|
// Seed working hours
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1654,7 +1644,6 @@ func TestBookings_Create_MultipleServices(t *testing.T) {
|
|||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
// Seed working hours for booking tests
|
// Seed working hours for booking tests
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
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
|
// booking is cancelled within 24 hours (with no forgiveness), the system
|
||||||
// overrides the cancellation to "no_show" and deposits_required stays 0.
|
// overrides the cancellation to "no_show" and deposits_required stays 0.
|
||||||
func TestDeleteBooking_NoShowUnder24h_SetsDepositsTo3(t *testing.T) {
|
func TestDeleteBooking_NoShowUnder24h_SetsDepositsTo3(t *testing.T) {
|
||||||
|
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
|
||||||
|
|
||||||
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
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
|
// cancelled with more than 24 hours notice (client_cancelled), no deposit penalty
|
||||||
// is applied and deposits_required remains 0.
|
// is applied and deposits_required remains 0.
|
||||||
func TestDeleteBooking_CancelOver24h_NoDepositPenalty(t *testing.T) {
|
func TestDeleteBooking_CancelOver24h_NoDepositPenalty(t *testing.T) {
|
||||||
|
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
|
||||||
|
|
||||||
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
// Create test user with deposits_required = 0
|
// Create test user with deposits_required = 0
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
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
|
// booking is cancelled within 24 hours with forgiveness, the system overrides to
|
||||||
// "client_cancelled" (no no-show penalty).
|
// "client_cancelled" (no no-show penalty).
|
||||||
func TestDeleteBooking_NoShowWithForgiveness_NoPenalty(t *testing.T) {
|
func TestDeleteBooking_NoShowWithForgiveness_NoPenalty(t *testing.T) {
|
||||||
|
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
|
||||||
|
|
||||||
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
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
|
// 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.
|
// on the first no-show and doesn't increment on subsequent no-shows.
|
||||||
func TestDeleteBooking_SecondNoShow_StaysAt3(t *testing.T) {
|
func TestDeleteBooking_SecondNoShow_StaysAt3(t *testing.T) {
|
||||||
|
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
|
||||||
|
|
||||||
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
// Create test user with deposits_required = 0
|
// Create test user with deposits_required = 0
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
@@ -2858,8 +2839,6 @@ func TestCreateEditRequest_WithTimeChange(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create test user: %v", err)
|
t.Fatalf("failed to create test user: %v", err)
|
||||||
@@ -3274,8 +3253,6 @@ func TestAdminApproveEditRequest_DeletesTimeBlocker(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create test user: %v", err)
|
t.Fatalf("failed to create test user: %v", err)
|
||||||
@@ -3373,8 +3350,6 @@ func TestAdminRejectEditRequest_DeletesTimeBlocker(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create test user: %v", err)
|
t.Fatalf("failed to create test user: %v", err)
|
||||||
@@ -3471,8 +3446,6 @@ func TestDeleteEditRequest_DeletesTimeBlocker(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create test user: %v", err)
|
t.Fatalf("failed to create test user: %v", err)
|
||||||
@@ -3552,8 +3525,6 @@ func TestAdminApproveEditRequest_TimeBlockerOverlap(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create test user: %v", err)
|
t.Fatalf("failed to create test user: %v", err)
|
||||||
@@ -3765,8 +3736,6 @@ func TestBookings_Create_PatchTestRequired_NoRecord(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Create user and service with patch test requirement
|
// Create user and service with patch test requirement
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -3815,8 +3784,6 @@ func TestBookings_Create_PatchTestRequired_WithinNoticePeriod(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create test user: %v", err)
|
t.Fatalf("failed to create test user: %v", err)
|
||||||
@@ -3872,8 +3839,6 @@ func TestBookings_Create_PatchTestRequired_Expired(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create test user: %v", err)
|
t.Fatalf("failed to create test user: %v", err)
|
||||||
@@ -3926,8 +3891,6 @@ func TestBookings_Create_PatchTestRequired_ValidRecord(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create test user: %v", err)
|
t.Fatalf("failed to create test user: %v", err)
|
||||||
@@ -3992,8 +3955,6 @@ func TestBookings_Create_DepositRequired_WithinAdvanceWindow(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create test user: %v", err)
|
t.Fatalf("failed to create test user: %v", err)
|
||||||
@@ -4040,8 +4001,6 @@ func TestBookings_Create_DepositRequired_After48Hours(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create test user: %v", err)
|
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
|
// TestBookings_Create_NoDepositRequired_Within48Hours verifies that a user with deposits_required=0
|
||||||
// can book at any time (no 48h restriction).
|
// can book at any time (no 48h restriction).
|
||||||
func TestBookings_Create_NoDepositRequired_Within48Hours(t *testing.T) {
|
func TestBookings_Create_NoDepositRequired_Within48Hours(t *testing.T) {
|
||||||
|
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
|
||||||
|
|
||||||
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -4143,8 +4100,6 @@ func TestBookings_Create_DepositSnapshot(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create test user: %v", err)
|
t.Fatalf("failed to create test user: %v", err)
|
||||||
@@ -4214,8 +4169,6 @@ func TestBookings_Create_DepositRequired_OneActiveBookingLimit(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create test user: %v", err)
|
t.Fatalf("failed to create test user: %v", err)
|
||||||
@@ -4276,8 +4229,6 @@ func TestBookings_Get_DepositFieldsReturned(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create test user: %v", err)
|
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) {
|
func TestBookings_Get_ServicesReturned(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -4427,7 +4377,6 @@ func TestBookings_Get_ServicesReturned(t *testing.T) {
|
|||||||
func TestBookings_Get_CustomServicesReturned(t *testing.T) {
|
func TestBookings_Get_CustomServicesReturned(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -4511,7 +4460,6 @@ func TestBookings_Get_CustomServicesReturned(t *testing.T) {
|
|||||||
func TestBookings_Get_EmptyServices(t *testing.T) {
|
func TestBookings_Get_EmptyServices(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -4562,8 +4510,6 @@ func TestBookings_Edit_ClosedDay_UserBlocked(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create test user: %v", err)
|
t.Fatalf("failed to create test user: %v", err)
|
||||||
@@ -4651,8 +4597,6 @@ func TestBookings_Edit_OpenDay_UserAllowed(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create test user: %v", err)
|
t.Fatalf("failed to create test user: %v", err)
|
||||||
@@ -4739,8 +4683,6 @@ func TestBookings_Create_OverlappingBlocker_UserBlocked(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create test user: %v", err)
|
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)
|
defer fixtures.DeleteService(tx, serviceID)
|
||||||
|
|
||||||
// Create a time blocker for a specific time
|
// 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, `
|
_, err = tx.Exec(ctx, `
|
||||||
INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
|
INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
|
||||||
VALUES ($1, 60, 'Staff meeting', NULL)
|
VALUES ($1, 60, 'Staff meeting', NULL)
|
||||||
@@ -4808,8 +4750,6 @@ func TestBookings_Edit_OverlappingBlocker_UserBlocked(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create test user: %v", err)
|
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)
|
defer fixtures.DeleteBooking(tx, bookingID)
|
||||||
|
|
||||||
// Create a time blocker for a specific time
|
// 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, `
|
_, err = tx.Exec(ctx, `
|
||||||
INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
|
INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
|
||||||
VALUES ($1, 60, 'Staff meeting', NULL)
|
VALUES ($1, 60, 'Staff meeting', NULL)
|
||||||
@@ -4985,7 +4925,6 @@ func TestGuestUser_Create_RegisteredEmailCollision(t *testing.T) {
|
|||||||
func TestGuestBooking_Create_Success(t *testing.T) {
|
func TestGuestBooking_Create_Success(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
// Create guest user
|
// Create guest user
|
||||||
guestReq := map[string]string{
|
guestReq := map[string]string{
|
||||||
@@ -5081,7 +5020,6 @@ func TestGuestBooking_Create_NonGuestUserID(t *testing.T) {
|
|||||||
func TestGuestBooking_SkipsDepositCheck(t *testing.T) {
|
func TestGuestBooking_SkipsDepositCheck(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
// Create guest user
|
// Create guest user
|
||||||
guestReq := map[string]string{
|
guestReq := map[string]string{
|
||||||
@@ -5123,9 +5061,8 @@ func TestGuestBooking_SkipsDepositCheck(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGuestBooking_BypassesAdvanceWindow(t *testing.T) {
|
func TestGuestBooking_BypassesAdvanceWindow(t *testing.T) {
|
||||||
|
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
// Create guest user
|
// Create guest user
|
||||||
guestReq := map[string]string{
|
guestReq := map[string]string{
|
||||||
@@ -5158,8 +5095,6 @@ func TestGuestBooking_BypassesAdvanceWindow(t *testing.T) {
|
|||||||
}
|
}
|
||||||
nearTime := midday.Truncate(time.Second)
|
nearTime := midday.Truncate(time.Second)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
req := CreateBookingRequest{
|
req := CreateBookingRequest{
|
||||||
StartTime: nearTime,
|
StartTime: nearTime,
|
||||||
ServiceIDs: []string{serviceID},
|
ServiceIDs: []string{serviceID},
|
||||||
@@ -5181,7 +5116,6 @@ func TestGuestBooking_BypassesAdvanceWindow(t *testing.T) {
|
|||||||
func TestCreateBooking_Notifications_NewBookingAlwaysCreated(t *testing.T) {
|
func TestCreateBooking_Notifications_NewBookingAlwaysCreated(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -5236,7 +5170,6 @@ func TestCreateBooking_Notifications_NewBookingAlwaysCreated(t *testing.T) {
|
|||||||
func TestCreateBooking_Notifications_PendingBookingWithNotes(t *testing.T) {
|
func TestCreateBooking_Notifications_PendingBookingWithNotes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -5303,7 +5236,6 @@ func TestCreateBooking_Notifications_PendingBookingWithNotes(t *testing.T) {
|
|||||||
func TestCreateBooking_Notifications_NoPendingBookingWithoutNotes(t *testing.T) {
|
func TestCreateBooking_Notifications_NoPendingBookingWithoutNotes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -6016,7 +5948,6 @@ func TestProgressBooking_DailyStampCap_SQLSubquery(t *testing.T) {
|
|||||||
func TestCreateBooking_OneHourAdvanceCheck(t *testing.T) {
|
func TestCreateBooking_OneHourAdvanceCheck(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -6071,7 +6002,6 @@ func TestCreateBooking_OneHourAdvanceCheck(t *testing.T) {
|
|||||||
func TestCreateBooking_ActiveBookingLimit(t *testing.T) {
|
func TestCreateBooking_ActiveBookingLimit(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -6177,7 +6107,6 @@ func TestNextWeekdayHelper(t *testing.T) {
|
|||||||
func TestCreateBooking_DepositSnapshot(t *testing.T) {
|
func TestCreateBooking_DepositSnapshot(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -6282,7 +6211,6 @@ func TestCreateBooking_DepositSnapshot(t *testing.T) {
|
|||||||
func TestGetBooking_WithDiscounts(t *testing.T) {
|
func TestGetBooking_WithDiscounts(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
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) {
|
func TestBookings_Confirm_WithCustomServiceOverrides(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -6495,7 +6422,6 @@ func TestBookings_Confirm_WithCustomServiceOverrides(t *testing.T) {
|
|||||||
func TestBookings_GetBooking_WithCustomServices(t *testing.T) {
|
func TestBookings_GetBooking_WithCustomServices(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -6576,7 +6502,6 @@ func TestBookings_GetBooking_WithCustomServices(t *testing.T) {
|
|||||||
func TestBookings_Confirm_CustomOverrideValidation(t *testing.T) {
|
func TestBookings_Confirm_CustomOverrideValidation(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -6665,7 +6590,6 @@ func TestBookings_Confirm_CustomOverrideValidation(t *testing.T) {
|
|||||||
func TestBookings_Confirm_CustomServiceNotInBooking(t *testing.T) {
|
func TestBookings_Confirm_CustomServiceNotInBooking(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -6744,7 +6668,6 @@ func TestBookings_Confirm_CustomServiceNotInBooking(t *testing.T) {
|
|||||||
func TestBookings_Progress_WithCustomServices(t *testing.T) {
|
func TestBookings_Progress_WithCustomServices(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -6919,10 +6842,8 @@ func TestDeleteBooking_WithPayments_ProcessesRefund(t *testing.T) {
|
|||||||
// TestDeleteBooking_NoPayments_HardDelete verifies that when a booking has no
|
// TestDeleteBooking_NoPayments_HardDelete verifies that when a booking has no
|
||||||
// payments, cancelling performs a hard delete (removes the row entirely).
|
// payments, cancelling performs a hard delete (removes the row entirely).
|
||||||
func TestDeleteBooking_NoPayments_HardDelete(t *testing.T) {
|
func TestDeleteBooking_NoPayments_HardDelete(t *testing.T) {
|
||||||
|
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
|
||||||
|
|
||||||
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -7066,8 +6987,6 @@ func TestRequestEditHandler_AutoApproves_NoPayments_FarFuture(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create test user: %v", err)
|
t.Fatalf("failed to create test user: %v", err)
|
||||||
@@ -7163,8 +7082,6 @@ func TestRequestEditHandler_AutoApproves_WithTimeChange(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create test user: %v", err)
|
t.Fatalf("failed to create test user: %v", err)
|
||||||
@@ -7241,8 +7158,6 @@ func TestRequestEditHandler_NoAutoApproval_WithPayments(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create test user: %v", err)
|
t.Fatalf("failed to create test user: %v", err)
|
||||||
@@ -7312,8 +7227,6 @@ func TestRequestEditHandler_NoAutoApproval_Within48h(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create test user: %v", err)
|
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) {
|
func TestCreateEditRequest_DiscountsBlockAutoApprove(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -7441,7 +7353,6 @@ func TestCreateEditRequest_DiscountsBlockAutoApprove(t *testing.T) {
|
|||||||
func TestCreateEditRequest_NoDiscountsStillAutoApproves(t *testing.T) {
|
func TestCreateEditRequest_NoDiscountsStillAutoApproves(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -7506,7 +7417,6 @@ func TestCreateEditRequest_NoDiscountsStillAutoApproves(t *testing.T) {
|
|||||||
func TestGetAllUserBookings_CursorNotSetOnLastPage(t *testing.T) {
|
func TestGetAllUserBookings_CursorNotSetOnLastPage(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -7628,7 +7538,6 @@ func TestGetAllUserBookings_CursorNotSetOnLastPage(t *testing.T) {
|
|||||||
func TestGetAllUserBookings_TotalCountMatches(t *testing.T) {
|
func TestGetAllUserBookings_TotalCountMatches(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test && dev
|
//go:build test && dev
|
||||||
// +build test,dev
|
|
||||||
|
|
||||||
package bookings
|
package bookings
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test && dev
|
//go:build test && dev
|
||||||
// +build test,dev
|
|
||||||
|
|
||||||
package bookings
|
package bookings
|
||||||
|
|
||||||
@@ -65,8 +64,8 @@ func TestCheckClosingHours_InvalidFormat(t *testing.T) {
|
|||||||
localEnd := time.Date(2026, 6, 24, 14, 0, 0, 0, london)
|
localEnd := time.Date(2026, 6, 24, 14, 0, 0, 0, london)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
closeStr string
|
closeStr string
|
||||||
}{
|
}{
|
||||||
{"empty string", ""},
|
{"empty string", ""},
|
||||||
{"no colon", "1700"},
|
{"no colon", "1700"},
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test && dev
|
//go:build test && dev
|
||||||
// +build test,dev
|
|
||||||
|
|
||||||
package bookings
|
package bookings
|
||||||
|
|
||||||
@@ -22,7 +21,6 @@ import (
|
|||||||
|
|
||||||
func setupDedupTest(t *testing.T, tx db.Querier, ctx context.Context) (string, string, string) {
|
func setupDedupTest(t *testing.T, tx db.Querier, ctx context.Context) (string, string, string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|
||||||
userID := createTestUser(t, 0, tx, ctx)
|
userID := createTestUser(t, 0, tx, ctx)
|
||||||
serviceID := createTestService(t, 50.00, tx, ctx)
|
serviceID := createTestService(t, 50.00, tx, ctx)
|
||||||
@@ -139,7 +137,6 @@ func TestProgressBooking_UserMilestoneDedup(t *testing.T) {
|
|||||||
func TestNoShowApplyDepositsIfNeeded(t *testing.T) {
|
func TestNoShowApplyDepositsIfNeeded(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
userID := createTestUser(t, 0, tx, ctx)
|
userID := createTestUser(t, 0, tx, ctx)
|
||||||
serviceID := createTestService(t, 50.00, tx, ctx)
|
serviceID := createTestService(t, 50.00, tx, ctx)
|
||||||
@@ -170,7 +167,6 @@ func TestNoShowApplyDepositsIfNeeded(t *testing.T) {
|
|||||||
func TestNoShowSingleNoShowDoesNotTrigger(t *testing.T) {
|
func TestNoShowSingleNoShowDoesNotTrigger(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
userID := createTestUser(t, 0, tx, ctx)
|
userID := createTestUser(t, 0, tx, ctx)
|
||||||
serviceID := createTestService(t, 50.00, tx, ctx)
|
serviceID := createTestService(t, 50.00, tx, ctx)
|
||||||
@@ -194,7 +190,6 @@ func TestNoShowSingleNoShowDoesNotTrigger(t *testing.T) {
|
|||||||
func TestNoShowOldNoShowsExcluded(t *testing.T) {
|
func TestNoShowOldNoShowsExcluded(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
userID := createTestUser(t, 0, tx, ctx)
|
userID := createTestUser(t, 0, tx, ctx)
|
||||||
serviceID := createTestService(t, 50.00, tx, ctx)
|
serviceID := createTestService(t, 50.00, tx, ctx)
|
||||||
@@ -222,7 +217,6 @@ func TestNoShowOldNoShowsExcluded(t *testing.T) {
|
|||||||
func TestNoShowForgivenExcluded(t *testing.T) {
|
func TestNoShowForgivenExcluded(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
userID := createTestUser(t, 0, tx, ctx)
|
userID := createTestUser(t, 0, tx, ctx)
|
||||||
serviceID := createTestService(t, 50.00, tx, ctx)
|
serviceID := createTestService(t, 50.00, tx, ctx)
|
||||||
@@ -252,7 +246,6 @@ func TestNoShowForgivenExcluded(t *testing.T) {
|
|||||||
func TestThreePaidBookingsClearNoShows(t *testing.T) {
|
func TestThreePaidBookingsClearNoShows(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
userID := createTestUser(t, 0, tx, ctx)
|
userID := createTestUser(t, 0, tx, ctx)
|
||||||
serviceID := createTestService(t, 50.00, tx, ctx)
|
serviceID := createTestService(t, 50.00, tx, ctx)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test && dev
|
//go:build test && dev
|
||||||
// +build test,dev
|
|
||||||
|
|
||||||
package bookings
|
package bookings
|
||||||
|
|
||||||
@@ -14,9 +13,9 @@ import (
|
|||||||
|
|
||||||
"crussell/clock"
|
"crussell/clock"
|
||||||
"crussell/db"
|
"crussell/db"
|
||||||
"crussell/testutils"
|
|
||||||
"crussell/handlers/payments"
|
"crussell/handlers/payments"
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
|
"crussell/testutils"
|
||||||
"crussell/testutils/fixtures"
|
"crussell/testutils/fixtures"
|
||||||
"crussell/testutils/jwt"
|
"crussell/testutils/jwt"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test && dev
|
//go:build test && dev
|
||||||
// +build test,dev
|
|
||||||
|
|
||||||
package bookings
|
package bookings
|
||||||
|
|
||||||
@@ -15,9 +14,9 @@ import (
|
|||||||
|
|
||||||
"crussell/clock"
|
"crussell/clock"
|
||||||
"crussell/db"
|
"crussell/db"
|
||||||
"crussell/testutils"
|
|
||||||
"crussell/handlers/payments"
|
"crussell/handlers/payments"
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
|
"crussell/testutils"
|
||||||
"crussell/testutils/fixtures"
|
"crussell/testutils/fixtures"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test && dev
|
//go:build test && dev
|
||||||
// +build test,dev
|
|
||||||
|
|
||||||
package bookings
|
package bookings
|
||||||
|
|
||||||
@@ -34,8 +33,8 @@ import (
|
|||||||
|
|
||||||
"crussell/clock"
|
"crussell/clock"
|
||||||
"crussell/db"
|
"crussell/db"
|
||||||
"crussell/testutils"
|
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
|
"crussell/testutils"
|
||||||
"crussell/testutils/fixtures"
|
"crussell/testutils/fixtures"
|
||||||
"crussell/testutils/jwt"
|
"crussell/testutils/jwt"
|
||||||
|
|
||||||
@@ -1610,8 +1609,6 @@ func TestAdminApproveEditRequestHandler_WithNotesOnly(t *testing.T) {
|
|||||||
func TestAdminApproveEditRequestHandler_OverlapWithBooking(t *testing.T) {
|
func TestAdminApproveEditRequestHandler_OverlapWithBooking(t *testing.T) {
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create user: %v", err)
|
t.Fatalf("failed to create user: %v", err)
|
||||||
@@ -1883,8 +1880,6 @@ func TestUserCancelBookingHandler_CleansUpEditRequest(t *testing.T) {
|
|||||||
func TestAdminApproveEditRequestHandler_BlockedByExceptionalClosedHours(t *testing.T) {
|
func TestAdminApproveEditRequestHandler_BlockedByExceptionalClosedHours(t *testing.T) {
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
_, serviceID, bookingID, token := setupEditRequestTest(t, ctx, tx)
|
_, serviceID, bookingID, token := setupEditRequestTest(t, ctx, tx)
|
||||||
_ = serviceID
|
_ = serviceID
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ package bookings
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crussell/db"
|
|
||||||
"crussell/clock"
|
"crussell/clock"
|
||||||
"github.com/jackc/pgx/v5"
|
"crussell/db"
|
||||||
"crussell/handlers/notifications"
|
"crussell/handlers/notifications"
|
||||||
"crussell/handlers/payments"
|
"crussell/handlers/payments"
|
||||||
"crussell/handlers/scheduling"
|
"crussell/handlers/scheduling"
|
||||||
@@ -14,6 +13,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -279,7 +279,7 @@ func AdminCancelBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if refundFailed || (refundResult != nil && refundResult.RefundableAmount > 0) {
|
if refundFailed || (refundResult != nil && refundResult.RefundableAmount > 0) {
|
||||||
resp := map[string]interface{}{
|
resp := map[string]any{
|
||||||
"message": "Booking cancelled",
|
"message": "Booking cancelled",
|
||||||
}
|
}
|
||||||
if refundResult != nil && refundResult.RefundableAmount > 0 {
|
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,
|
"booking": booking,
|
||||||
|
|
||||||
@@ -988,10 +988,10 @@ type EditSnapshot struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type EditUserSummary struct {
|
type EditUserSummary struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
FullName string `json:"full_name"`
|
FullName string `json:"full_name"`
|
||||||
Email *string `json:"email,omitempty"`
|
Email *string `json:"email,omitempty"`
|
||||||
Phone *string `json:"phone,omitempty"`
|
Phone *string `json:"phone,omitempty"`
|
||||||
PreviousFirstName *string `json:"previous_first_name,omitempty"`
|
PreviousFirstName *string `json:"previous_first_name,omitempty"`
|
||||||
PreviousLastName *string `json:"previous_last_name,omitempty"`
|
PreviousLastName *string `json:"previous_last_name,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -1462,12 +1462,12 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
durMinutes = 60
|
durMinutes = 60
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := tx.QueryRow(r.Context(), `
|
if err := tx.QueryRow(r.Context(), `
|
||||||
SELECT total_duration_minutes FROM bookings WHERE id = $1
|
SELECT total_duration_minutes FROM bookings WHERE id = $1
|
||||||
`, bookingID).Scan(&durMinutes); err != nil {
|
`, bookingID).Scan(&durMinutes); err != nil {
|
||||||
log.Printf("Failed to get duration for existing services: %v", err)
|
log.Printf("Failed to get duration for existing services: %v", err)
|
||||||
durMinutes = 60
|
durMinutes = 60
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if durMinutes <= 0 {
|
if durMinutes <= 0 {
|
||||||
durMinutes = 60
|
durMinutes = 60
|
||||||
@@ -1504,7 +1504,7 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Update booking start_time, notes, and services directly
|
// Update booking start_time, notes, and services directly
|
||||||
if req.NewStartTime != nil || req.Notes != nil {
|
if req.NewStartTime != nil || req.Notes != nil {
|
||||||
var setClauses []string
|
var setClauses []string
|
||||||
var args []interface{}
|
var args []any
|
||||||
argNum := 1
|
argNum := 1
|
||||||
if req.NewStartTime != nil {
|
if req.NewStartTime != nil {
|
||||||
setClauses = append(setClauses, fmt.Sprintf("start_time = $%d", argNum))
|
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.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
"auto_approved": true,
|
"auto_approved": true,
|
||||||
"edit_request": editReq,
|
"edit_request": editReq,
|
||||||
})
|
})
|
||||||
@@ -1684,7 +1684,7 @@ func AdminListEditRequestsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
JOIN users u ON ber.requested_by = u.id
|
JOIN users u ON ber.requested_by = u.id
|
||||||
`
|
`
|
||||||
|
|
||||||
var args []interface{}
|
var args []any
|
||||||
|
|
||||||
baseQuery += " ORDER BY ber.updated_at DESC"
|
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")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
"requests": requests,
|
"requests": requests,
|
||||||
"total": total,
|
"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").
|
// 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.
|
// Column names are never derived from user input. User values are in args and always parameterised.
|
||||||
var setClauses []string
|
var setClauses []string
|
||||||
var args []interface{}
|
var args []any
|
||||||
argNum := 1
|
argNum := 1
|
||||||
|
|
||||||
if newStartTime != nil {
|
if newStartTime != nil {
|
||||||
@@ -2153,7 +2153,7 @@ func GetMyEditRequestHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if errors.Is(err, pgx.ErrNoRows) {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
"edit_request": nil,
|
"edit_request": nil,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -2172,7 +2172,7 @@ func GetMyEditRequestHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
"edit_request": enriched,
|
"edit_request": enriched,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -2236,7 +2236,7 @@ func GetMyEditRequestsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
"edit_requests": enrichedRequests,
|
"edit_requests": enrichedRequests,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -2293,7 +2293,7 @@ func AdminListAllEditRequestsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
"edit_requests": enrichedRequests,
|
"edit_requests": enrichedRequests,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -2342,7 +2342,7 @@ func AdminGetBookingEditRequestHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
"edit_request": enriched,
|
"edit_request": enriched,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test && dev
|
//go:build test && dev
|
||||||
// +build test,dev
|
|
||||||
|
|
||||||
package bookings
|
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
|
// Compute weekStart the same way the handler does: from the booking time's
|
||||||
// London weekday, find Monday's date, store as UTC midnight.
|
// London weekday, find Monday's date, store as UTC midnight.
|
||||||
bkLondon := sunday2330UTC.In(londonLocation) // 00:30 BST Monday
|
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 {
|
if daysToMonday == 0 {
|
||||||
daysToMonday = 7
|
daysToMonday = 7
|
||||||
}
|
}
|
||||||
@@ -2182,10 +2181,10 @@ func TestAdminReserveSlot_CleansUpAnonReservation(t *testing.T) {
|
|||||||
|
|
||||||
w := makeIPRequest(http.HandlerFunc(AdminReserveSlotHandler), "POST", "/api/admin/bookings/reserve",
|
w := makeIPRequest(http.HandlerFunc(AdminReserveSlotHandler), "POST", "/api/admin/bookings/reserve",
|
||||||
&AdminReserveSlotRequest{
|
&AdminReserveSlotRequest{
|
||||||
StartTime: future,
|
StartTime: future,
|
||||||
ServiceIDs: []string{serviceID},
|
ServiceIDs: []string{serviceID},
|
||||||
DurationMinutes: 60,
|
DurationMinutes: 60,
|
||||||
ReservationType: "walkin",
|
ReservationType: "walkin",
|
||||||
}, token, testIP, adminID, ctx)
|
}, token, testIP, adminID, ctx)
|
||||||
|
|
||||||
if w.Code != http.StatusCreated {
|
if w.Code != http.StatusCreated {
|
||||||
|
|||||||
@@ -80,8 +80,8 @@ func ReserveSlotHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
hasAuth := hasUser && userID != ""
|
hasAuth := hasUser && userID != ""
|
||||||
if !hasAuth {
|
if !hasAuth {
|
||||||
authHeader := r.Header.Get("Authorization")
|
authHeader := r.Header.Get("Authorization")
|
||||||
if strings.HasPrefix(authHeader, "Bearer ") {
|
if after, ok := strings.CutPrefix(authHeader, "Bearer "); ok {
|
||||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
tokenString := after
|
||||||
var err error
|
var err error
|
||||||
userID, _, _, err = auth.VerifyToken(tokenString, r.Context())
|
userID, _, _, err = auth.VerifyToken(tokenString, r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test && dev
|
//go:build test && dev
|
||||||
// +build test,dev
|
|
||||||
|
|
||||||
package bookings
|
package bookings
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test && dev
|
//go:build test && dev
|
||||||
// +build test,dev
|
|
||||||
|
|
||||||
package bookings
|
package bookings
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test && dev
|
//go:build test && dev
|
||||||
// +build test,dev
|
|
||||||
|
|
||||||
package bookings
|
package bookings
|
||||||
|
|
||||||
|
|||||||
@@ -201,7 +201,6 @@ func GetNotifications(w http.ResponseWriter, r *http.Request) {
|
|||||||
NextCursor: nextCursor,
|
NextCursor: nextCursor,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||||
log.Printf("Failed to encode response: %v", err)
|
log.Printf("Failed to encode response: %v", err)
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
@@ -222,7 +221,6 @@ func GetUnreadCount(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(map[string]int{"count": count}); err != nil {
|
if err := json.NewEncoder(w).Encode(map[string]int{"count": count}); err != nil {
|
||||||
log.Printf("Failed to encode response: %v", err)
|
log.Printf("Failed to encode response: %v", err)
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
@@ -269,13 +267,12 @@ func AcknowledgeNotification(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{
|
json.NewEncoder(w).Encode(map[string]string{
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func AcknowledgePendingBookingNotification(tx interface{}, ctx context.Context, bookingID string) error {
|
func AcknowledgePendingBookingNotification(tx any, ctx context.Context, bookingID string) error {
|
||||||
query := `
|
query := `
|
||||||
UPDATE admin_notifications
|
UPDATE admin_notifications
|
||||||
SET acknowledged_at = NOW()
|
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
|
// Use type assertion to get the Exec method - pgx.Tx satisfies this interface
|
||||||
execer, ok := tx.(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 {
|
if !ok {
|
||||||
log.Printf("Warning: cannot acknowledge notification - tx does not satisfy Execer interface for booking %s", bookingID)
|
log.Printf("Warning: cannot acknowledge notification - tx does not satisfy Execer interface for booking %s", bookingID)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package notifications
|
package notifications
|
||||||
|
|
||||||
@@ -14,8 +13,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"crussell/db"
|
"crussell/db"
|
||||||
"crussell/testutils"
|
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
|
"crussell/testutils"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
@@ -571,5 +570,3 @@ func createNotificationWithBooking(t *testing.T, ctx context.Context, q db.Queri
|
|||||||
}
|
}
|
||||||
return notificationID
|
return notificationID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package notifications
|
package notifications
|
||||||
|
|
||||||
@@ -25,8 +24,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"crussell/clock"
|
"crussell/clock"
|
||||||
"crussell/testutils"
|
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
|
"crussell/testutils"
|
||||||
"crussell/testutils/fixtures"
|
"crussell/testutils/fixtures"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package notifications
|
package notifications
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test && dev
|
//go:build test && dev
|
||||||
// +build test,dev
|
|
||||||
|
|
||||||
package payments
|
package payments
|
||||||
|
|
||||||
@@ -13,8 +12,8 @@ import (
|
|||||||
|
|
||||||
"crussell/clock"
|
"crussell/clock"
|
||||||
"crussell/db"
|
"crussell/db"
|
||||||
"crussell/testutils"
|
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
|
"crussell/testutils"
|
||||||
"crussell/testutils/fixtures"
|
"crussell/testutils/fixtures"
|
||||||
"crussell/testutils/jwt"
|
"crussell/testutils/jwt"
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"crussell/db"
|
|
||||||
"crussell/clock"
|
"crussell/clock"
|
||||||
|
"crussell/db"
|
||||||
"crussell/internal/square"
|
"crussell/internal/square"
|
||||||
"crussell/internal/validators"
|
"crussell/internal/validators"
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
@@ -160,7 +160,7 @@ func GetGiftCards(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var gcTotal int
|
var gcTotal int
|
||||||
var gcListArgs []interface{}
|
var gcListArgs []any
|
||||||
|
|
||||||
gcListQuery := fmt.Sprintf(`
|
gcListQuery := fmt.Sprintf(`
|
||||||
SELECT id, total_funds_added, amount_remaining, created_at, is_inventory
|
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 != "" {
|
if searchTerm != "" {
|
||||||
searchPattern := "%" + searchTerm + "%"
|
searchPattern := "%" + searchTerm + "%"
|
||||||
gcListArgs = []interface{}{searchPattern}
|
gcListArgs = []any{searchPattern}
|
||||||
|
|
||||||
if cursorStr != "" {
|
if cursorStr != "" {
|
||||||
cursorCreatedAt, cursorID, err := validators.ParseCursor(cursorStr)
|
cursorCreatedAt, cursorID, err := validators.ParseCursor(cursorStr)
|
||||||
@@ -202,7 +202,7 @@ func GetGiftCards(w http.ResponseWriter, r *http.Request) {
|
|||||||
// transaction (single connection).
|
// transaction (single connection).
|
||||||
gcTotal = 0
|
gcTotal = 0
|
||||||
if whereSQL != "" {
|
if whereSQL != "" {
|
||||||
countArgs := []interface{}{}
|
countArgs := []any{}
|
||||||
if searchTerm != "" {
|
if searchTerm != "" {
|
||||||
countArgs = append(countArgs, "%"+searchTerm+"%")
|
countArgs = append(countArgs, "%"+searchTerm+"%")
|
||||||
}
|
}
|
||||||
@@ -243,7 +243,7 @@ func GetGiftCards(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var ubTotal int
|
var ubTotal int
|
||||||
var ubListQuery string
|
var ubListQuery string
|
||||||
var ubListArgs []interface{}
|
var ubListArgs []any
|
||||||
|
|
||||||
if searchTerm != "" {
|
if searchTerm != "" {
|
||||||
searchPattern := "%" + searchTerm + "%"
|
searchPattern := "%" + searchTerm + "%"
|
||||||
@@ -258,7 +258,7 @@ func GetGiftCards(w http.ResponseWriter, r *http.Request) {
|
|||||||
OR u.email ILIKE $1
|
OR u.email ILIKE $1
|
||||||
ORDER BY b.updated_at DESC
|
ORDER BY b.updated_at DESC
|
||||||
`
|
`
|
||||||
ubListArgs = []interface{}{searchPattern}
|
ubListArgs = []any{searchPattern}
|
||||||
} else {
|
} else {
|
||||||
ubListQuery = `
|
ubListQuery = `
|
||||||
SELECT
|
SELECT
|
||||||
@@ -267,7 +267,7 @@ func GetGiftCards(w http.ResponseWriter, r *http.Request) {
|
|||||||
JOIN users u ON b.user_id = u.id
|
JOIN users u ON b.user_id = u.id
|
||||||
ORDER BY b.updated_at DESC
|
ORDER BY b.updated_at DESC
|
||||||
`
|
`
|
||||||
ubListArgs = []interface{}{}
|
ubListArgs = []any{}
|
||||||
}
|
}
|
||||||
|
|
||||||
ubRows, err := db.Conn.Query(ctx, ubListQuery, ubListArgs...)
|
ubRows, err := db.Conn.Query(ctx, ubListQuery, ubListArgs...)
|
||||||
@@ -736,7 +736,7 @@ func RedeemGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"amount_redeemed": amountRemaining,
|
"amount_redeemed": amountRemaining,
|
||||||
})
|
})
|
||||||
@@ -1065,7 +1065,7 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"code": cardID,
|
"code": cardID,
|
||||||
"amount": amountPounds,
|
"amount": amountPounds,
|
||||||
@@ -1137,7 +1137,7 @@ func GetExpiredBalances(w http.ResponseWriter, r *http.Request) {
|
|||||||
balances = []ExpiredBalance{}
|
balances = []ExpiredBalance{}
|
||||||
}
|
}
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
"expired_balances": balances,
|
"expired_balances": balances,
|
||||||
"total": len(balances),
|
"total": len(balances),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test && dev
|
//go:build test && dev
|
||||||
// +build test,dev
|
|
||||||
|
|
||||||
package payments
|
package payments
|
||||||
|
|
||||||
@@ -23,7 +22,6 @@ import (
|
|||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
func TestAdminCreateGiftCard(t *testing.T) {
|
func TestAdminCreateGiftCard(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package payments
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crussell/db"
|
|
||||||
"crussell/clock"
|
"crussell/clock"
|
||||||
|
"crussell/db"
|
||||||
"crussell/internal/square"
|
"crussell/internal/square"
|
||||||
"crussell/internal/validators"
|
"crussell/internal/validators"
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
@@ -1943,7 +1943,7 @@ func AcquirePaymentLock(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
"status": "locked",
|
"status": "locked",
|
||||||
"ttl_min": PaymentLockDuration,
|
"ttl_min": PaymentLockDuration,
|
||||||
"bookingID": bookingID,
|
"bookingID": bookingID,
|
||||||
|
|||||||
@@ -150,8 +150,7 @@ func ApplyLoyaltyRedemption(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
||||||
"success": true,
|
"success": true,
|
||||||
"discount_amount": discountAmount,
|
"discount_amount": discountAmount,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test && dev
|
//go:build test && dev
|
||||||
// +build test,dev
|
|
||||||
|
|
||||||
package payments
|
package payments
|
||||||
|
|
||||||
@@ -13,8 +12,8 @@ import (
|
|||||||
|
|
||||||
"crussell/clock"
|
"crussell/clock"
|
||||||
"crussell/db"
|
"crussell/db"
|
||||||
"crussell/testutils"
|
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
|
"crussell/testutils"
|
||||||
"crussell/testutils/fixtures"
|
"crussell/testutils/fixtures"
|
||||||
"crussell/testutils/jwt"
|
"crussell/testutils/jwt"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test && dev
|
//go:build test && dev
|
||||||
// +build test,dev
|
|
||||||
|
|
||||||
package payments
|
package payments
|
||||||
|
|
||||||
@@ -11,8 +10,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"crussell/db"
|
"crussell/db"
|
||||||
"crussell/testutils"
|
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
|
"crussell/testutils"
|
||||||
"crussell/testutils/fixtures"
|
"crussell/testutils/fixtures"
|
||||||
"crussell/testutils/jwt"
|
"crussell/testutils/jwt"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test && dev
|
//go:build test && dev
|
||||||
// +build test,dev
|
|
||||||
|
|
||||||
package payments
|
package payments
|
||||||
|
|
||||||
@@ -16,8 +15,8 @@ import (
|
|||||||
|
|
||||||
"crussell/clock"
|
"crussell/clock"
|
||||||
"crussell/db"
|
"crussell/db"
|
||||||
"crussell/testutils"
|
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
|
"crussell/testutils"
|
||||||
"crussell/testutils/fixtures"
|
"crussell/testutils/fixtures"
|
||||||
"crussell/testutils/jwt"
|
"crussell/testutils/jwt"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test && dev
|
//go:build test && dev
|
||||||
// +build test,dev
|
|
||||||
|
|
||||||
package payments
|
package payments
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test && dev
|
//go:build test && dev
|
||||||
// +build test,dev
|
|
||||||
|
|
||||||
package payments
|
package payments
|
||||||
|
|
||||||
@@ -154,7 +153,7 @@ func TestProcessCancellationRefund_CreatesRefundRecords(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create user: %v", err)
|
t.Fatalf("failed to create user: %v", err)
|
||||||
}
|
}
|
||||||
@@ -210,7 +209,7 @@ func TestProcessCancellationRefund_NoRefundWhenNotNeeded(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create user: %v", err)
|
t.Fatalf("failed to create user: %v", err)
|
||||||
}
|
}
|
||||||
@@ -246,7 +245,7 @@ func TestProcessCancellationRefund_NoPaymentsNoop(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create user: %v", err)
|
t.Fatalf("failed to create user: %v", err)
|
||||||
}
|
}
|
||||||
@@ -289,7 +288,7 @@ func TestProcessCancellationRefund_GiftCardCreditsUserBalance(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create user: %v", err)
|
t.Fatalf("failed to create user: %v", err)
|
||||||
}
|
}
|
||||||
@@ -377,7 +376,7 @@ func TestProcessCancellationRefund_CashCreditsUserBalance(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create user: %v", err)
|
t.Fatalf("failed to create user: %v", err)
|
||||||
}
|
}
|
||||||
@@ -432,7 +431,7 @@ func TestProcessCancellationRefund_CardSquareRefundWithoutBalanceCredit(t *testi
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create user: %v", err)
|
t.Fatalf("failed to create user: %v", err)
|
||||||
}
|
}
|
||||||
@@ -507,7 +506,7 @@ func TestProcessCancellationRefund_DiscountPaymentSkipped(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create user: %v", err)
|
t.Fatalf("failed to create user: %v", err)
|
||||||
}
|
}
|
||||||
@@ -562,7 +561,7 @@ func TestProcessCancellationRefund_OnTheHousePaymentSkipped(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create user: %v", err)
|
t.Fatalf("failed to create user: %v", err)
|
||||||
}
|
}
|
||||||
@@ -621,7 +620,7 @@ func TestProcessCancellationRefund_MissingUserID_LogsWarning(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create user: %v", err)
|
t.Fatalf("failed to create user: %v", err)
|
||||||
}
|
}
|
||||||
@@ -687,7 +686,7 @@ func TestProcessCancellationRefund_GuestGiftcardDoesNotCreditBalance(t *testing.
|
|||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
// Create a user and promote them to guest role.
|
// Create a user and promote them to guest role.
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create user: %v", err)
|
t.Fatalf("failed to create user: %v", err)
|
||||||
}
|
}
|
||||||
@@ -758,7 +757,7 @@ func TestProcessCancellationRefund_GuestCashDoesNotCreditBalance(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create user: %v", err)
|
t.Fatalf("failed to create user: %v", err)
|
||||||
}
|
}
|
||||||
@@ -824,7 +823,7 @@ func TestProcessCancellationRefund_SplitPayment_DeduplicatesSquareRefund(t *test
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create user: %v", err)
|
t.Fatalf("failed to create user: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test && dev
|
//go:build test && dev
|
||||||
// +build test,dev
|
|
||||||
|
|
||||||
package payments
|
package payments
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test && dev
|
//go:build test && dev
|
||||||
// +build test,dev
|
|
||||||
|
|
||||||
package payments
|
package payments
|
||||||
|
|
||||||
@@ -25,7 +24,6 @@ func TestCreateTillSale_OnTheHouse(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create admin user: %v", err)
|
t.Fatalf("failed to create admin user: %v", err)
|
||||||
@@ -104,7 +102,6 @@ func TestCreateTillSale_Idempotency(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create admin user: %v", err)
|
t.Fatalf("failed to create admin user: %v", err)
|
||||||
@@ -192,7 +189,6 @@ func TestCreateTillSale_CreatesGiftCardTransaction(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create admin user: %v", err)
|
t.Fatalf("failed to create admin user: %v", err)
|
||||||
@@ -294,7 +290,6 @@ func TestCreateTillSale_TopupOnRedeemedCard(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := testutils.SetupTestTx(t)
|
ctx, tx := testutils.SetupTestTx(t)
|
||||||
|
|
||||||
|
|
||||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create admin user: %v", err)
|
t.Fatalf("failed to create admin user: %v", err)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test && dev
|
//go:build test && dev
|
||||||
// +build test,dev
|
|
||||||
|
|
||||||
package payments
|
package payments
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package portfolio
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crussell/db"
|
|
||||||
"crussell/clock"
|
"crussell/clock"
|
||||||
|
"crussell/db"
|
||||||
"crussell/internal/images"
|
"crussell/internal/images"
|
||||||
"crussell/internal/s3"
|
"crussell/internal/s3"
|
||||||
"crussell/internal/validators"
|
"crussell/internal/validators"
|
||||||
@@ -165,8 +165,8 @@ func ListImages(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
filterClauses := ""
|
var filterClauses strings.Builder
|
||||||
filterArgs := []interface{}{}
|
filterArgs := []any{}
|
||||||
|
|
||||||
for key, values := range r.URL.Query() {
|
for key, values := range r.URL.Query() {
|
||||||
if len(values) == 0 || values[0] == "" {
|
if len(values) == 0 || values[0] == "" {
|
||||||
@@ -190,14 +190,14 @@ func ListImages(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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)
|
filterArgs = append(filterArgs, category+":"+value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var query string
|
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`
|
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
|
FROM images, unnest(tag_names) as t
|
||||||
WHERE %s%s
|
WHERE %s%s
|
||||||
GROUP BY id
|
GROUP BY id
|
||||||
`, formatCols, similaritySum, whereClause, filterClauses)
|
`, formatCols, similaritySum, whereClause, filterClauses.String())
|
||||||
|
|
||||||
var cursorArgs []interface{}
|
var cursorArgs []any
|
||||||
if cursorStr != "" {
|
if cursorStr != "" {
|
||||||
cursorCreatedAt, cursorID, err := validators.ParseCursor(cursorStr)
|
cursorCreatedAt, cursorID, err := validators.ParseCursor(cursorStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Invalid cursor: "+err.Error(), http.StatusBadRequest)
|
http.Error(w, "Invalid cursor: "+err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cursorArgs = []interface{}{cursorCreatedAt, cursorID}
|
cursorArgs = []any{cursorCreatedAt, cursorID}
|
||||||
havingIdx := len(cleanTags) + len(filterArgs) + 1
|
havingIdx := len(cleanTags) + len(filterArgs) + 1
|
||||||
query += fmt.Sprintf(" HAVING (created_at, id) < ($%d, $%d)", havingIdx, havingIdx+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)
|
argOffset = len(filterArgs) + len(cleanTags) + len(cursorArgs)
|
||||||
query += fmt.Sprintf(" LIMIT $%d", argOffset+1)
|
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)
|
copy(queryArgs, filterArgs)
|
||||||
for i, t := range cleanTags {
|
for i, t := range cleanTags {
|
||||||
queryArgs[len(filterArgs)+i] = t
|
queryArgs[len(filterArgs)+i] = t
|
||||||
@@ -264,9 +264,9 @@ func ListImages(w http.ResponseWriter, r *http.Request) {
|
|||||||
similarity(t, $%d) as relevance
|
similarity(t, $%d) as relevance
|
||||||
FROM images, unnest(tag_names) as t
|
FROM images, unnest(tag_names) as t
|
||||||
WHERE 1=1%s AND t ILIKE '%%' || $%d || '%%'
|
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 != "" {
|
if cursorStr != "" {
|
||||||
cursorCreatedAt, cursorID, err := validators.ParseCursor(cursorStr)
|
cursorCreatedAt, cursorID, err := validators.ParseCursor(cursorStr)
|
||||||
if err != nil {
|
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 += " ORDER BY match_priority DESC, relevance DESC, created_at DESC, id DESC"
|
||||||
query += fmt.Sprintf(" LIMIT $%d", searchIdx+1+len(cursorArgs))
|
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)
|
copy(queryArgs[:argOffset], filterArgs)
|
||||||
queryArgs[argOffset] = searchPattern
|
queryArgs[argOffset] = searchPattern
|
||||||
for i, ca := range cursorArgs {
|
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
|
SELECT id, url, thumbnail_url, tag_names, created_at%s, 0 as match_count, 0.0 as relevance
|
||||||
FROM images
|
FROM images
|
||||||
WHERE 1=1%s
|
WHERE 1=1%s
|
||||||
`, formatCols, filterClauses)
|
`, formatCols, filterClauses.String())
|
||||||
|
|
||||||
if cursorStr != "" {
|
if cursorStr != "" {
|
||||||
cursorCreatedAt, cursorID, err := validators.ParseCursor(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 += " ORDER BY created_at DESC, id DESC"
|
||||||
query += fmt.Sprintf(" LIMIT $%d", argOffset+1)
|
query += fmt.Sprintf(" LIMIT $%d", argOffset+1)
|
||||||
|
|
||||||
queryArgs := make([]interface{}, len(filterArgs)+1)
|
queryArgs := make([]any, len(filterArgs)+1)
|
||||||
copy(queryArgs, filterArgs)
|
copy(queryArgs, filterArgs)
|
||||||
queryArgs[len(filterArgs)] = limit
|
queryArgs[len(filterArgs)] = limit
|
||||||
args = queryArgs
|
args = queryArgs
|
||||||
@@ -394,7 +394,7 @@ func ListTags(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var query string
|
var query string
|
||||||
var args []interface{}
|
var args []any
|
||||||
|
|
||||||
// Query tags from images.tag_names column (stored as array)
|
// Query tags from images.tag_names column (stored as array)
|
||||||
if q != "" {
|
if q != "" {
|
||||||
@@ -409,7 +409,7 @@ func ListTags(w http.ResponseWriter, r *http.Request) {
|
|||||||
ORDER BY tag
|
ORDER BY tag
|
||||||
LIMIT 20
|
LIMIT 20
|
||||||
`
|
`
|
||||||
args = []interface{}{q}
|
args = []any{q}
|
||||||
} else {
|
} else {
|
||||||
query = `
|
query = `
|
||||||
SELECT DISTINCT tag
|
SELECT DISTINCT tag
|
||||||
@@ -484,7 +484,7 @@ func ListFilters(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Build base query
|
// Build base query
|
||||||
baseQuery := "SELECT DISTINCT id FROM images WHERE 1=1"
|
baseQuery := "SELECT DISTINCT id FROM images WHERE 1=1"
|
||||||
args := []interface{}{}
|
args := []any{}
|
||||||
argNum := 1
|
argNum := 1
|
||||||
|
|
||||||
if tagFilter != "" {
|
if tagFilter != "" {
|
||||||
@@ -507,7 +507,7 @@ func ListFilters(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Build category filters for OTHER categories
|
// Build category filters for OTHER categories
|
||||||
otherFilters := make([]string, 0)
|
otherFilters := make([]string, 0)
|
||||||
otherArgs := make([]interface{}, len(args))
|
otherArgs := make([]any, len(args))
|
||||||
copy(otherArgs, args)
|
copy(otherArgs, args)
|
||||||
otherArgNum := argNum
|
otherArgNum := argNum
|
||||||
for cat, val := range selectedCategories {
|
for cat, val := range selectedCategories {
|
||||||
@@ -709,7 +709,7 @@ func UploadImage(w http.ResponseWriter, r *http.Request) {
|
|||||||
tagsStr := r.FormValue("tags")
|
tagsStr := r.FormValue("tags")
|
||||||
tags := []string{}
|
tags := []string{}
|
||||||
if tagsStr != "" {
|
if tagsStr != "" {
|
||||||
for _, t := range strings.Split(tagsStr, ",") {
|
for t := range strings.SplitSeq(tagsStr, ",") {
|
||||||
if trimmed := strings.TrimSpace(t); trimmed != "" {
|
if trimmed := strings.TrimSpace(t); trimmed != "" {
|
||||||
tags = append(tags, trimmed)
|
tags = append(tags, trimmed)
|
||||||
}
|
}
|
||||||
@@ -1034,11 +1034,11 @@ func extractKey(url string) string {
|
|||||||
// URL format: https://endpoint/bucket/portfolio/1234567890.jpg
|
// URL format: https://endpoint/bucket/portfolio/1234567890.jpg
|
||||||
// Need to return: portfolio/1234567890.jpg
|
// Need to return: portfolio/1234567890.jpg
|
||||||
// Find the bucket segment: skip past scheme://endpoint/
|
// Find the bucket segment: skip past scheme://endpoint/
|
||||||
idx := strings.Index(url, "://")
|
_, after, ok := strings.Cut(url, "://")
|
||||||
if idx == -1 {
|
if !ok {
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
rest := url[idx+3:] // skip "://"
|
rest := after // skip "://"
|
||||||
// Now rest = "endpoint/bucket/portfolio/1234567890.jpg"
|
// Now rest = "endpoint/bucket/portfolio/1234567890.jpg"
|
||||||
// Skip first path segment (endpoint)
|
// Skip first path segment (endpoint)
|
||||||
slashIdx := strings.Index(rest, "/")
|
slashIdx := strings.Index(rest, "/")
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
// Package portfolio contains tests for portfolio image management endpoints.
|
// Package portfolio contains tests for portfolio image management endpoints.
|
||||||
//
|
//
|
||||||
@@ -27,8 +26,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"crussell/testutils"
|
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
|
"crussell/testutils"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/kovidgoyal/imaging"
|
"github.com/kovidgoyal/imaging"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package portfolio
|
package portfolio
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"crussell/db"
|
|
||||||
"crussell/clock"
|
"crussell/clock"
|
||||||
|
"crussell/db"
|
||||||
"crussell/internal/validators"
|
"crussell/internal/validators"
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
"log"
|
"log"
|
||||||
@@ -308,17 +308,17 @@ func isValidTime15Min(t string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- helper: sqlIn generates IN queries dynamically for Postgres ---
|
// --- helper: sqlIn generates IN queries dynamically for Postgres ---
|
||||||
func sqlIn(query string, args []int) (string, []interface{}, error) {
|
func sqlIn(query string, args []int) (string, []any, error) {
|
||||||
inArgs := []interface{}{}
|
inArgs := []any{}
|
||||||
placeholders := ""
|
var placeholders strings.Builder
|
||||||
for i, arg := range args {
|
for i, arg := range args {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
placeholders += ","
|
placeholders.WriteString(",")
|
||||||
}
|
}
|
||||||
placeholders += fmt.Sprintf("$%d", i+1)
|
placeholders.WriteString(fmt.Sprintf("$%d", i+1))
|
||||||
inArgs = append(inArgs, arg)
|
inArgs = append(inArgs, arg)
|
||||||
}
|
}
|
||||||
query = fmt.Sprintf(query, placeholders)
|
query = fmt.Sprintf(query, placeholders.String())
|
||||||
return query, inArgs, nil
|
return query, inArgs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package scheduling
|
package scheduling
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package scheduling
|
package scheduling
|
||||||
|
|
||||||
@@ -1023,7 +1022,7 @@ func TestScheduling_GetAvailableHours_WithBlocker_NonAdmin(t *testing.T) {
|
|||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
// Create a time blocker for 2026-03-16 10:00-11:00 (Monday - an open day)
|
// 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, `
|
_, err := tx.Exec(ctx, `
|
||||||
INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
|
INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
|
||||||
VALUES ($1, 60, 'Staff Meeting', NULL)
|
VALUES ($1, 60, 'Staff Meeting', NULL)
|
||||||
@@ -1091,7 +1090,7 @@ func TestScheduling_GetAvailableHours_WithBlocker_Admin(t *testing.T) {
|
|||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
// Create a time blocker for 2026-03-16 10:00-11:00 (Monday - open day)
|
// 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, `
|
_, err := tx.Exec(ctx, `
|
||||||
INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
|
INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
|
||||||
VALUES ($1, 60, 'Staff Meeting', NULL)
|
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) {
|
func TestScheduling_GetAvailableHours_WithBlocker_Admin_MultipleBlockers(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
// Create two blockers on Tuesday 2026-03-17 (open 09:00-17:00):
|
// 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)
|
// 10:00-11:00 (Staff Meeting) and 14:00-15:00 (Training)
|
||||||
b1 := time.Date(2026, 3, 17, 10, 0, 0, 0, time.UTC)
|
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) {
|
func TestScheduling_GetAvailableHours_WithBlocker_Admin_BlockerAndBooking(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
// Tuesday 2026-03-17 (open 09:00-17:00)
|
// 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
|
// 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)
|
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) {
|
func TestScheduling_GetAvailableHours_WithBlocker_Admin_AllDayBlocker(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
// Tuesday 2026-03-17 (open 09:00-17:00) — block entire open period
|
// 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)
|
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)
|
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) {
|
func TestScheduling_GetAvailableHours_WithBlocker_Admin_NonOverlappingBlocker(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
// Tuesday 2026-03-17 (open 09:00-17:00) — blocker at 17:00-18:00 (after close)
|
// 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)
|
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)
|
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) {
|
func TestScheduling_GetAvailableHours_WithBlocker_Admin_MultiDay(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
// Blockers on Tue 2026-03-17 10:00-11:00 and Wed 2026-03-18 14:00-15:00
|
// 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)
|
b1 := time.Date(2026, 3, 17, 10, 0, 0, 0, time.UTC)
|
||||||
b2 := time.Date(2026, 3, 18, 14, 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) {
|
func TestScheduling_GetAvailableHours_WithBlocker_Admin_BoundaryStart(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
// Tuesday 2026-03-17 (open 09:00-17:00) — blocker at 09:00-10:00 (start of day)
|
// 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)
|
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)
|
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) {
|
func TestScheduling_GetAvailableHours_WithBlocker_Admin_BoundaryEnd(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
// Tuesday 2026-03-17 (open 09:00-17:00) — blocker at 16:00-17:00 (end of day)
|
// 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)
|
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)
|
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) {
|
func TestScheduling_GetAvailableHours_WithBlocker_Admin_OutOfHours(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
// Tuesday 2026-03-17 — blocker at 10:00-11:00
|
// Tuesday 2026-03-17 — blocker at 10:00-11:00
|
||||||
blockerTime := time.Date(2026, 3, 17, 10, 0, 0, 0, time.UTC)
|
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)
|
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) {
|
func TestScheduling_GetAvailableHours_WithBlocker_NonAdmin_Regression(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
// Tuesday 2026-03-17 — blocker at 10:00-11:00
|
// Tuesday 2026-03-17 — blocker at 10:00-11:00
|
||||||
blockerTime := time.Date(2026, 3, 17, 10, 0, 0, 0, time.UTC)
|
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)
|
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
|
// No t.Parallel() — GetAvailableHours cleanup operations can deadlock with
|
||||||
// concurrent test transactions on the shared test database.
|
// concurrent test transactions on the shared test database.
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
// Daily recurring blocker 12:00-13:00 starting Mon 2026-03-16
|
// Daily recurring blocker 12:00-13:00 starting Mon 2026-03-16
|
||||||
startTime := time.Date(2026, 3, 16, 12, 0, 0, 0, time.UTC)
|
startTime := time.Date(2026, 3, 16, 12, 0, 0, 0, time.UTC)
|
||||||
cronExpr := "0 12 * * *" // Every day at 12:00
|
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.
|
// RESERVATION:admin time_blocker entries are also subtracted from admin slots.
|
||||||
func TestScheduling_GetAvailableHours_WithBlocker_Admin_Reservation(t *testing.T) {
|
func TestScheduling_GetAvailableHours_WithBlocker_Admin_Reservation(t *testing.T) {
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
// Create a real admin user to satisfy FK constraint, then simulate a reservation
|
// Create a real admin user to satisfy FK constraint, then simulate a reservation
|
||||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||||
if err != nil {
|
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) {
|
func TestScheduling_GetAvailableHours_WithBlocker_Admin_Reservation_NonAdmin(t *testing.T) {
|
||||||
// Not parallel (see above)
|
// Not parallel (see above)
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
reservationTime := time.Date(2026, 3, 17, 10, 0, 0, 0, time.UTC)
|
reservationTime := time.Date(2026, 3, 17, 10, 0, 0, 0, time.UTC)
|
||||||
tx.Exec(ctx, `
|
tx.Exec(ctx, `
|
||||||
INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
|
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) {
|
func TestScheduling_GetAvailableHours_WithBlocker_Admin_OverlappingBlockers(t *testing.T) {
|
||||||
|
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
// Two overlapping blockers on Tue 2026-03-17: 10:00-12:00 and 11:00-13:00
|
// 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)
|
b1 := time.Date(2026, 3, 17, 10, 0, 0, 0, time.UTC)
|
||||||
b2 := time.Date(2026, 3, 17, 11, 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) {
|
func TestScheduling_GetAvailableHours_WithBlocker_Admin_AdjacentBoundaries(t *testing.T) {
|
||||||
|
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
// Tue 2026-03-17: booking 10:00-11:00, blocker 11:00-12:00 (adjacent)
|
// 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)
|
bookingStart := time.Date(2026, 3, 17, 10, 0, 0, 0, time.UTC)
|
||||||
bookingEnd := bookingStart.Add(60 * time.Minute)
|
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) {
|
func TestScheduling_GetAvailableHours_WithBlocker_Admin_MidnightBlocker(t *testing.T) {
|
||||||
|
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
// Use the first open day found and the following day
|
// Use the first open day found and the following day
|
||||||
tueStart, tueEnd, tueOpen := getWorkingHoursForDate(t, ctx, "2026-03-17")
|
tueStart, tueEnd, tueOpen := getWorkingHoursForDate(t, ctx, "2026-03-17")
|
||||||
wedStart, _, wedOpen := getWorkingHoursForDate(t, ctx, "2026-03-18")
|
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) {
|
func TestScheduling_GetAvailableHours_WithBlocker_Admin_ClosedDayBlocker(t *testing.T) {
|
||||||
|
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
// Sunday 2026-03-22 is closed. Blocker at 10:00-11:00.
|
// Sunday 2026-03-22 is closed. Blocker at 10:00-11:00.
|
||||||
blockerTime := time.Date(2026, 3, 22, 10, 0, 0, 0, time.UTC)
|
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)
|
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) {
|
func TestScheduling_GetAvailableHours_WithBlocker_Admin_MultiDayRangePartial(t *testing.T) {
|
||||||
|
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
// Blocker only on Tuesday (2026-03-17) at 10:00-11:00
|
// Blocker only on Tuesday (2026-03-17) at 10:00-11:00
|
||||||
blockerTime := time.Date(2026, 3, 17, 10, 0, 0, 0, time.UTC)
|
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)
|
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) {
|
func TestScheduling_GetAvailableHours_WithBlocker_Admin_ExceptionalHours(t *testing.T) {
|
||||||
|
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
// Monday 2026-03-16 is normally CLOSED. Add exceptional hours: 10:00-16:00.
|
// Monday 2026-03-16 is normally CLOSED. Add exceptional hours: 10:00-16:00.
|
||||||
// Also add a blocker at 12:00-13:00.
|
// Also add a blocker at 12:00-13:00.
|
||||||
// First create the exceptional group
|
// 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) {
|
func TestScheduling_GetAvailableHours_WithBlocker_NonAdmin_ClosedDay(t *testing.T) {
|
||||||
|
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
// Sunday 2026-03-22 closed, blocker at 10:00-11:00
|
// Sunday 2026-03-22 closed, blocker at 10:00-11:00
|
||||||
blockerTime := time.Date(2026, 3, 22, 10, 0, 0, 0, time.UTC)
|
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)
|
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) {
|
func TestScheduling_GetAvailableHours_WithBlocker_NonAdmin_BookingAdjacent(t *testing.T) {
|
||||||
|
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
bookingStart := time.Date(2026, 3, 17, 10, 0, 0, 0, time.UTC)
|
bookingStart := time.Date(2026, 3, 17, 10, 0, 0, 0, time.UTC)
|
||||||
bookingEnd := bookingStart.Add(60 * time.Minute)
|
bookingEnd := bookingStart.Add(60 * time.Minute)
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
@@ -2417,7 +2416,7 @@ func TestNormalizeTime_Regression_RealWorldFormats(t *testing.T) {
|
|||||||
// slots from each affected day.
|
// slots from each affected day.
|
||||||
func TestScheduling_GetAvailableHours_CrossDayBlocker(t *testing.T) {
|
func TestScheduling_GetAvailableHours_CrossDayBlocker(t *testing.T) {
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
// Use Tue 2026-03-17 and Wed 2026-03-18 — both open days
|
// Use Tue 2026-03-17 and Wed 2026-03-18 — both open days
|
||||||
_, tueEnd, tueOpen := getWorkingHoursForDate(t, ctx, "2026-03-17")
|
_, tueEnd, tueOpen := getWorkingHoursForDate(t, ctx, "2026-03-17")
|
||||||
_, _, wedOpen := getWorkingHoursForDate(t, ctx, "2026-03-18")
|
_, _, wedOpen := getWorkingHoursForDate(t, ctx, "2026-03-18")
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package scheduling
|
package scheduling
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package scheduling
|
package scheduling
|
||||||
|
|
||||||
@@ -430,7 +429,6 @@ func TestGetTimeBlockersInRange(t *testing.T) {
|
|||||||
t.Fatalf("failed to create blockers: %v", err)
|
t.Fatalf("failed to create blockers: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Query range that includes blocker1 and blocker2 but not blocker3
|
// Query range that includes blocker1 and blocker2 but not blocker3
|
||||||
start := time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC)
|
start := time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC)
|
||||||
end := time.Date(2026, 3, 16, 23, 59, 59, 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()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
// Create a blocker
|
// Create a blocker
|
||||||
blockerTime := time.Date(2026, 3, 15, 10, 0, 0, 0, time.UTC)
|
blockerTime := time.Date(2026, 3, 15, 10, 0, 0, 0, time.UTC)
|
||||||
_, err := tx.Exec(ctx, `
|
_, err := tx.Exec(ctx, `
|
||||||
@@ -479,7 +476,6 @@ func TestGetTimeBlockersInRange_Empty(t *testing.T) {
|
|||||||
t.Fatalf("failed to create blocker: %v", err)
|
t.Fatalf("failed to create blocker: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Query range with no blockers
|
// Query range with no blockers
|
||||||
start := time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)
|
start := time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)
|
||||||
end := time.Date(2026, 4, 30, 23, 59, 59, 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)
|
t.Fatalf("failed to create blockers: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Query range: March 1-31, 2026
|
// Query range: March 1-31, 2026
|
||||||
start := time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC)
|
start := time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC)
|
||||||
end := time.Date(2026, 3, 31, 23, 59, 59, 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()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
// Create fixture users for the test
|
// Create fixture users for the test
|
||||||
oldUserID, err := fixtures.CreateTestUser(tx)
|
oldUserID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -666,7 +660,6 @@ func TestCleanupOldReservations_AdminWalkIn(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
// Create old walk-in reservation (>15 min old)
|
// Create old walk-in reservation (>15 min old)
|
||||||
oldTime := clock.Now().Add(-16 * time.Minute)
|
oldTime := clock.Now().Add(-16 * time.Minute)
|
||||||
_, err := tx.Exec(ctx, `
|
_, err := tx.Exec(ctx, `
|
||||||
@@ -722,7 +715,6 @@ func TestCleanupOldReservations_AdminCallIn(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
// Create old call-in reservation (>15 min old)
|
// Create old call-in reservation (>15 min old)
|
||||||
oldTime := clock.Now().Add(-16 * time.Minute)
|
oldTime := clock.Now().Add(-16 * time.Minute)
|
||||||
_, err := tx.Exec(ctx, `
|
_, err := tx.Exec(ctx, `
|
||||||
@@ -778,7 +770,6 @@ func TestCleanupOldReservations_MixedTypes(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
// Create old user reservation (>1 hour old)
|
// Create old user reservation (>1 hour old)
|
||||||
oldUserTime := clock.Now().Add(-2 * time.Hour)
|
oldUserTime := clock.Now().Add(-2 * time.Hour)
|
||||||
_, err := tx.Exec(ctx, `
|
_, err := tx.Exec(ctx, `
|
||||||
@@ -925,7 +916,6 @@ func TestGetTimeBlockersInRange_IncludesReservations(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
// Create a regular blocker for tomorrow at 10:00
|
// Create a regular blocker for tomorrow at 10:00
|
||||||
tomorrow := clock.Now().Add(24 * time.Hour)
|
tomorrow := clock.Now().Add(24 * time.Hour)
|
||||||
blockerTime := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 10, 0, 0, 0, time.UTC)
|
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()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
// Create guest user
|
// Create guest user
|
||||||
guestID, err := fixtures.CreateTestUser(tx)
|
guestID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1183,7 +1172,6 @@ func TestAnonymizeStaleGuestAccounts_ActiveBooking_Skipped(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
// Create guest user
|
// Create guest user
|
||||||
guestID, err := fixtures.CreateTestUser(tx)
|
guestID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1240,7 +1228,6 @@ func TestAnonymizeStaleGuestAccounts_NoBookings(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
// Create guest user with no bookings
|
// Create guest user with no bookings
|
||||||
guestID, err := fixtures.CreateTestUser(tx)
|
guestID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1280,7 +1267,6 @@ func TestCleanupExpiredFinancialRecords_PaymentOlderThan7Years(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create user: %v", err)
|
t.Fatalf("failed to create user: %v", err)
|
||||||
@@ -1363,7 +1349,6 @@ func TestCleanupExpiredFinancialRecords_AnonUserWithin1YearBuffer(t *testing.T)
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
guestID, err := fixtures.CreateTestUser(tx)
|
guestID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create user: %v", err)
|
t.Fatalf("failed to create user: %v", err)
|
||||||
@@ -1434,7 +1419,6 @@ func TestCleanupExpiredFinancialRecords_PaymentOlderThan9Years(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create user: %v", err)
|
t.Fatalf("failed to create user: %v", err)
|
||||||
@@ -1502,7 +1486,6 @@ func TestCleanupExpiredFinancialRecords_AggregationCorrectTotals(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create user: %v", err)
|
t.Fatalf("failed to create user: %v", err)
|
||||||
@@ -1601,7 +1584,6 @@ func TestCleanupExpiredFinancialRecords_Idempotent(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create user: %v", err)
|
t.Fatalf("failed to create user: %v", err)
|
||||||
@@ -1701,7 +1683,6 @@ func TestCleanupExpiredFinancialRecords_ActiveUserWithin7Years(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create user: %v", err)
|
t.Fatalf("failed to create user: %v", err)
|
||||||
@@ -1763,7 +1744,6 @@ func TestCleanupExpiredFinancialRecords_AnonUserBothThresholdsElapsed(t *testing
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
guestID, err := fixtures.CreateTestUser(tx)
|
guestID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create user: %v", err)
|
t.Fatalf("failed to create user: %v", err)
|
||||||
@@ -1839,7 +1819,6 @@ func TestCleanupExpiredFinancialRecords_RefundAggregatedAndDeleted(t *testing.T)
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create user: %v", err)
|
t.Fatalf("failed to create user: %v", err)
|
||||||
@@ -1934,7 +1913,6 @@ func TestAnonymizeStaleGuestAccounts(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
// Guest 1: last booking 7 months ago — should be anonymized
|
// Guest 1: last booking 7 months ago — should be anonymized
|
||||||
guest1ID, _ := fixtures.CreateTestUser(tx)
|
guest1ID, _ := fixtures.CreateTestUser(tx)
|
||||||
tx.Exec(ctx, `UPDATE users SET account_role = 'guest' WHERE id = $1`, guest1ID)
|
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()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
// Create old edit_request reservation (>24 hours old)
|
// Create old edit_request reservation (>24 hours old)
|
||||||
oldTime := clock.Now().Add(-25 * time.Hour)
|
oldTime := clock.Now().Add(-25 * time.Hour)
|
||||||
_, err := tx.Exec(ctx, `
|
_, err := tx.Exec(ctx, `
|
||||||
@@ -2056,7 +2033,6 @@ func TestCleanupExpiredDeposits_ExpiredConfirmed(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create user: %v", err)
|
t.Fatalf("failed to create user: %v", err)
|
||||||
@@ -2118,7 +2094,6 @@ func TestCleanupExpiredDeposits_ExpiredPending(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create user: %v", err)
|
t.Fatalf("failed to create user: %v", err)
|
||||||
@@ -2182,7 +2157,6 @@ func TestCleanupExpiredDeposits_PaidDepositPreserved(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create user: %v", err)
|
t.Fatalf("failed to create user: %v", err)
|
||||||
@@ -2251,7 +2225,6 @@ func TestCleanupExpiredDeposits_FutureDeadlinePreserved(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create user: %v", err)
|
t.Fatalf("failed to create user: %v", err)
|
||||||
@@ -2313,7 +2286,6 @@ func TestCleanupExpiredGiftCards(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
// Make account_id nullable for this test — CleanupExpiredGiftCards inserts NULL
|
// Make account_id nullable for this test — CleanupExpiredGiftCards inserts NULL
|
||||||
// for unredeemed gift cards (no user account to reference).
|
// 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`)
|
_, 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()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
// Make account_id nullable for this test — CleanupExpiredGiftCards inserts NULL
|
// Make account_id nullable for this test — CleanupExpiredGiftCards inserts NULL
|
||||||
// for unredeemed gift cards (no user account to reference).
|
// 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`)
|
_, 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()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
// Make account_id nullable for this test — CleanupExpiredGiftCards inserts NULL
|
// Make account_id nullable for this test — CleanupExpiredGiftCards inserts NULL
|
||||||
// for unredeemed gift cards (no user account to reference). Even though this
|
// 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.
|
// 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()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
// Create a user
|
// Create a user
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2567,7 +2536,6 @@ func TestCleanupIdleAccounts_NoBalance(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
// Create a user
|
// Create a user
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2603,7 +2571,6 @@ func TestCleanupIdleAccounts_SkipActive(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
// Create a user with recent last_login
|
// Create a user with recent last_login
|
||||||
userID, err := fixtures.CreateTestUser(tx)
|
userID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2639,7 +2606,6 @@ func TestCleanupIdleAccounts_SkipAdminGuest(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
|
|
||||||
// Create admin user with old last_login
|
// Create admin user with old last_login
|
||||||
adminID, err := fixtures.CreateTestUser(tx)
|
adminID, err := fixtures.CreateTestUser(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2799,7 +2765,7 @@ func TestCleanupOldIdempotencyKeys_ClearsOldPayments(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCleanupOldIdempotencyKeys_ClearsOldTillSales(t *testing.T) {
|
func TestCleanupOldIdempotencyKeys_ClearsOldTillSales(t *testing.T) {
|
||||||
|
|
||||||
ctx, tx := resetTestData(t)
|
ctx, tx := resetTestData(t)
|
||||||
|
|
||||||
// Create an admin user for till_sales.created_by
|
// Create an admin user for till_sales.created_by
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crussell/clock"
|
"crussell/clock"
|
||||||
"crussell/db"
|
"crussell/db"
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
"crussell/internal/validators"
|
"crussell/internal/validators"
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ func ToggleService(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
"message": "Service toggled successfully",
|
"message": "Service toggled successfully",
|
||||||
"id": serviceID,
|
"id": serviceID,
|
||||||
})
|
})
|
||||||
@@ -247,7 +247,7 @@ func DeleteServiceHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
"message": "Service deleted successfully",
|
"message": "Service deleted successfully",
|
||||||
"id": serviceID,
|
"id": serviceID,
|
||||||
})
|
})
|
||||||
@@ -305,7 +305,6 @@ func ServicesHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
if services == nil {
|
if services == nil {
|
||||||
@@ -402,7 +401,6 @@ func ServicesHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Combine: eligible + ineligible
|
// Combine: eligible + ineligible
|
||||||
services = append(services, ineligibleServices...)
|
services = append(services, ineligibleServices...)
|
||||||
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
if services == nil {
|
if services == nil {
|
||||||
@@ -509,7 +507,6 @@ func ServicesEligibleForUserHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Sort and combine: valid first, then grayed out
|
// Sort and combine: valid first, then grayed out
|
||||||
services = append(services, grayedOutServices...)
|
services = append(services, grayedOutServices...)
|
||||||
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
if services == nil {
|
if services == nil {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
// Package services contains tests for service listing and eligibility endpoints.
|
// Package services contains tests for service listing and eligibility endpoints.
|
||||||
//
|
//
|
||||||
@@ -22,8 +21,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"crussell/db"
|
"crussell/db"
|
||||||
"crussell/testutils"
|
|
||||||
"crussell/handlers/user"
|
"crussell/handlers/user"
|
||||||
|
"crussell/testutils"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package services
|
package services
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package today
|
package today
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"crussell/db"
|
|
||||||
"crussell/clock"
|
"crussell/clock"
|
||||||
|
"crussell/db"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,11 +31,11 @@ type ServiceInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UserInfo struct {
|
type UserInfo struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
FullName string `json:"full_name"`
|
FullName string `json:"full_name"`
|
||||||
Phone *string `json:"phone,omitempty"`
|
Phone *string `json:"phone,omitempty"`
|
||||||
Email *string `json:"email,omitempty"`
|
Email *string `json:"email,omitempty"`
|
||||||
ProfilePicURL *string `json:"profile_pic_url,omitempty"`
|
ProfilePicURL *string `json:"profile_pic_url,omitempty"`
|
||||||
PreviousFirstName *string `json:"previous_first_name,omitempty"`
|
PreviousFirstName *string `json:"previous_first_name,omitempty"`
|
||||||
PreviousLastName *string `json:"previous_last_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
|
// 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 bookingID string
|
||||||
var startTime time.Time
|
var startTime time.Time
|
||||||
var status string
|
var status string
|
||||||
@@ -681,15 +681,15 @@ func fetchAppointment(r *http.Request, query string, args ...interface{}) (*Appo
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TodayAppointment struct {
|
type TodayAppointment struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
StartTime string `json:"start_time"`
|
StartTime string `json:"start_time"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
UserName string `json:"user_name"`
|
UserName string `json:"user_name"`
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
Services []string `json:"services"`
|
Services []string `json:"services"`
|
||||||
DurationMinutes int `json:"duration_minutes"`
|
DurationMinutes int `json:"duration_minutes"`
|
||||||
PreviousFirstName *string `json:"previous_first_name,omitempty"`
|
PreviousFirstName *string `json:"previous_first_name,omitempty"`
|
||||||
PreviousLastName *string `json:"previous_last_name,omitempty"`
|
PreviousLastName *string `json:"previous_last_name,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TodayAppointmentsResponse struct {
|
type TodayAppointmentsResponse struct {
|
||||||
@@ -903,15 +903,15 @@ func GetTodayAppointmentsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PendingApproval struct {
|
type PendingApproval struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
StartTime string `json:"start_time"`
|
StartTime string `json:"start_time"`
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
UserName string `json:"user_name"`
|
UserName string `json:"user_name"`
|
||||||
PreviousFirstName *string `json:"previous_first_name,omitempty"`
|
PreviousFirstName *string `json:"previous_first_name,omitempty"`
|
||||||
PreviousLastName *string `json:"previous_last_name,omitempty"`
|
PreviousLastName *string `json:"previous_last_name,omitempty"`
|
||||||
Services []string `json:"services"`
|
Services []string `json:"services"`
|
||||||
DurationMinutes int `json:"duration_minutes"`
|
DurationMinutes int `json:"duration_minutes"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PendingApprovalsResponse struct {
|
type PendingApprovalsResponse struct {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package today
|
package today
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package user
|
package user
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package user
|
package user
|
||||||
|
|
||||||
@@ -12,8 +11,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"crussell/clock"
|
"crussell/clock"
|
||||||
"crussell/testutils"
|
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
|
"crussell/testutils"
|
||||||
"crussell/testutils/fixtures"
|
"crussell/testutils/fixtures"
|
||||||
"crussell/testutils/jwt"
|
"crussell/testutils/jwt"
|
||||||
)
|
)
|
||||||
@@ -1284,8 +1283,8 @@ func TestCleanupGDPRExportCache_Mixed(t *testing.T) {
|
|||||||
|
|
||||||
gdprExportCacheMu.Lock()
|
gdprExportCacheMu.Lock()
|
||||||
gdprExportCache = map[string]*gdprCacheEntry{
|
gdprExportCache = map[string]*gdprCacheEntry{
|
||||||
"user-expired": {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-valid": {expiresAt: clock.Now().Add(2 * time.Hour)},
|
||||||
"user-expired2": {expiresAt: clock.Now().Add(-30 * time.Minute)},
|
"user-expired2": {expiresAt: clock.Now().Add(-30 * time.Minute)},
|
||||||
}
|
}
|
||||||
gdprExportCacheMu.Unlock()
|
gdprExportCacheMu.Unlock()
|
||||||
|
|||||||
@@ -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
|
// Note: We intentionally don't sync to CardDAV - guests don't need calendar contacts
|
||||||
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
json.NewEncoder(w).Encode(CreateGuestUserResponse{ID: userID, Role: "guest"})
|
json.NewEncoder(w).Encode(CreateGuestUserResponse{ID: userID, Role: "guest"})
|
||||||
}
|
}
|
||||||
@@ -192,8 +191,7 @@ func CheckEmailHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
||||||
"suggestion": suggestion,
|
"suggestion": suggestion,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package user
|
package user
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package user
|
package user
|
||||||
|
|
||||||
@@ -10,8 +9,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"crussell/testutils"
|
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
|
"crussell/testutils"
|
||||||
"crussell/testutils/fixtures"
|
"crussell/testutils/fixtures"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"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)
|
t.Errorf("expected 404 for invalid test ID, got %d", w.Code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,14 +21,14 @@ import (
|
|||||||
"golang.org/x/text/cases"
|
"golang.org/x/text/cases"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"crussell/db"
|
|
||||||
"crussell/clock"
|
"crussell/clock"
|
||||||
"github.com/jackc/pgx/v5"
|
"crussell/db"
|
||||||
"crussell/handlers/auth"
|
"crussell/handlers/auth"
|
||||||
"crussell/internal/images"
|
"crussell/internal/images"
|
||||||
"crussell/internal/s3"
|
"crussell/internal/s3"
|
||||||
"crussell/internal/validators"
|
"crussell/internal/validators"
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getEnv(key, fallback string) string {
|
func getEnv(key, fallback string) string {
|
||||||
@@ -41,19 +41,19 @@ func getEnv(key, fallback string) string {
|
|||||||
var titleCaser = cases.Title(language.English)
|
var titleCaser = cases.Title(language.English)
|
||||||
|
|
||||||
type UserProfile struct {
|
type UserProfile struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
FirstName string `json:"firstName"`
|
FirstName string `json:"firstName"`
|
||||||
LastName string `json:"lastName"`
|
LastName string `json:"lastName"`
|
||||||
Phone *string `json:"phone,omitempty"`
|
Phone *string `json:"phone,omitempty"`
|
||||||
DateOfBirth *string `json:"dateOfBirth,omitempty"`
|
DateOfBirth *string `json:"dateOfBirth,omitempty"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
LoyaltyStamps int `json:"loyaltyStamps"`
|
LoyaltyStamps int `json:"loyaltyStamps"`
|
||||||
ReferralCode string `json:"referralCode"`
|
ReferralCode string `json:"referralCode"`
|
||||||
ReferralCodeUses int `json:"referralCodeUses"`
|
ReferralCodeUses int `json:"referralCodeUses"`
|
||||||
ReferralSavings float64 `json:"referralSavings"`
|
ReferralSavings float64 `json:"referralSavings"`
|
||||||
ProfilePicURL *string `json:"profilePicUrl,omitempty"`
|
ProfilePicURL *string `json:"profilePicUrl,omitempty"`
|
||||||
DepositsRequired int `json:"deposits_required"`
|
DepositsRequired int `json:"deposits_required"`
|
||||||
PreviousFirstName *string `json:"previousFirstName,omitempty"`
|
PreviousFirstName *string `json:"previousFirstName,omitempty"`
|
||||||
PreviousLastName *string `json:"previousLastName,omitempty"`
|
PreviousLastName *string `json:"previousLastName,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -103,15 +103,15 @@ type SocialLogin struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UserListItem struct {
|
type UserListItem struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
FullName string `json:"fullName"`
|
FullName string `json:"fullName"`
|
||||||
Email *string `json:"email,omitempty"`
|
Email *string `json:"email,omitempty"`
|
||||||
Phone *string `json:"phone,omitempty"`
|
Phone *string `json:"phone,omitempty"`
|
||||||
AccountRole string `json:"account_role"`
|
AccountRole string `json:"account_role"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
PreviousFirstName *string `json:"previousFirstName,omitempty"`
|
PreviousFirstName *string `json:"previousFirstName,omitempty"`
|
||||||
PreviousLastName *string `json:"previousLastName,omitempty"`
|
PreviousLastName *string `json:"previousLastName,omitempty"`
|
||||||
CompletedCount int `json:"completed_count"`
|
CompletedCount int `json:"completed_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserListResponse struct {
|
type UserListResponse struct {
|
||||||
@@ -511,7 +511,7 @@ func ListAdminUsersHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
`
|
`
|
||||||
|
|
||||||
var listQuery string
|
var listQuery string
|
||||||
var listArgs []interface{}
|
var listArgs []any
|
||||||
|
|
||||||
if searchTerm != "" {
|
if searchTerm != "" {
|
||||||
searchPattern := "%" + 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)
|
WHERE (u.fn ILIKE $1 OR u.email ILIKE $1 OR u.phone ILIKE $1)
|
||||||
) sub`
|
) sub`
|
||||||
|
|
||||||
listArgs = []interface{}{searchPattern}
|
listArgs = []any{searchPattern}
|
||||||
|
|
||||||
if cursorStr != "" {
|
if cursorStr != "" {
|
||||||
cursorCount, cursorCreatedAt, cursorID, err := validators.ParseCursor3(cursorStr)
|
cursorCount, cursorCreatedAt, cursorID, err := validators.ParseCursor3(cursorStr)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package user
|
package user
|
||||||
|
|
||||||
@@ -25,8 +24,8 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"crussell/testutils"
|
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
|
"crussell/testutils"
|
||||||
"crussell/testutils/fixtures"
|
"crussell/testutils/fixtures"
|
||||||
"crussell/testutils/jwt"
|
"crussell/testutils/jwt"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package user
|
package user
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package webhooks
|
package webhooks
|
||||||
|
|
||||||
@@ -194,7 +193,7 @@ func TestHandleSquareWebhook_BodyTooLarge(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandleSquareWebhook_ValidSignatureWithEnvKey(t *testing.T) {
|
func TestHandleSquareWebhook_ValidSignatureWithEnvKey(t *testing.T) {
|
||||||
|
|
||||||
body := []byte(`{"type":"payment.updated","event_id":"evt_1"}`)
|
body := []byte(`{"type":"payment.updated","event_id":"evt_1"}`)
|
||||||
key := "env-signing-key"
|
key := "env-signing-key"
|
||||||
notificationURL := "http://localhost:8080/webhooks/square"
|
notificationURL := "http://localhost:8080/webhooks/square"
|
||||||
@@ -213,7 +212,7 @@ func TestHandleSquareWebhook_ValidSignatureWithEnvKey(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandleSquareWebhook_InvalidSignatureWithEnvKey(t *testing.T) {
|
func TestHandleSquareWebhook_InvalidSignatureWithEnvKey(t *testing.T) {
|
||||||
|
|
||||||
body := []byte(`{"type":"payment.updated","event_id":"evt_1"}`)
|
body := []byte(`{"type":"payment.updated","event_id":"evt_1"}`)
|
||||||
|
|
||||||
t.Setenv("SQUARE_WEBHOOK_SIGNATURE_KEY", "env-signing-key")
|
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) {
|
func TestHandleSquareWebhook_NoSignatureWhenKeySet(t *testing.T) {
|
||||||
|
|
||||||
body := []byte(`{"type":"payment.updated","event_id":"evt_1"}`)
|
body := []byte(`{"type":"payment.updated","event_id":"evt_1"}`)
|
||||||
|
|
||||||
t.Setenv("SQUARE_WEBHOOK_SIGNATURE_KEY", "env-signing-key")
|
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) {
|
func TestHandleSquareWebhook_SignatureSkippedWhenKeyEmpty(t *testing.T) {
|
||||||
|
|
||||||
t.Setenv("SQUARE_WEBHOOK_SIGNATURE_KEY", "")
|
t.Setenv("SQUARE_WEBHOOK_SIGNATURE_KEY", "")
|
||||||
|
|
||||||
body := []byte(`{"type":"payment.updated","event_id":"evt_1"}`)
|
body := []byte(`{"type":"payment.updated","event_id":"evt_1"}`)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build dev
|
//go:build dev
|
||||||
// +build dev
|
|
||||||
|
|
||||||
package dav
|
package dav
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build !dev
|
//go:build !dev
|
||||||
// +build !dev
|
|
||||||
|
|
||||||
package dav
|
package dav
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package dav
|
package dav
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crussell/clock"
|
|
||||||
"context"
|
"context"
|
||||||
|
"crussell/clock"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -131,7 +131,7 @@ func (s *BaseService) ListRecentContacts(days int) ([]Contact, error) {
|
|||||||
// Helper Methods
|
// 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...)
|
rows, err := s.db.Query(context.Background(), query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -155,7 +155,7 @@ func (s *BaseService) queryEventsWithContacts(query string, args ...interface{})
|
|||||||
|
|
||||||
func extractContactURIsFromICalendar(icalData string) []string {
|
func extractContactURIsFromICalendar(icalData string) []string {
|
||||||
var uris []string
|
var uris []string
|
||||||
for _, line := range strings.Split(icalData, "\n") {
|
for line := range strings.SplitSeq(icalData, "\n") {
|
||||||
line = strings.TrimSpace(line)
|
line = strings.TrimSpace(line)
|
||||||
if strings.HasPrefix(line, "ATTENDEE") {
|
if strings.HasPrefix(line, "ATTENDEE") {
|
||||||
parts := strings.Split(line, ":")
|
parts := strings.Split(line, ":")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package dav
|
|||||||
import (
|
import (
|
||||||
"crussell/clock"
|
"crussell/clock"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -102,9 +103,9 @@ func GenerateICalEvent(input EventInput) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build attendees section
|
// Build attendees section
|
||||||
attendees := ""
|
var attendees strings.Builder
|
||||||
for _, contactURI := range input.ContactURIs {
|
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
|
ical := fmt.Sprintf(`BEGIN:VCALENDAR
|
||||||
@@ -127,7 +128,7 @@ END:VCALENDAR`, uid, dtstamp, dtstart, dtend,
|
|||||||
escapeICalText(input.Summary),
|
escapeICalText(input.Summary),
|
||||||
escapeICalText(input.Description),
|
escapeICalText(input.Description),
|
||||||
escapeICalText(input.Location),
|
escapeICalText(input.Location),
|
||||||
attendees)
|
attendees.String())
|
||||||
|
|
||||||
return ical
|
return ical
|
||||||
}
|
}
|
||||||
@@ -164,13 +165,13 @@ func escapeICalText(text string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func replaceAll(s, old, new string) string {
|
func replaceAll(s, old, new string) string {
|
||||||
result := ""
|
var result strings.Builder
|
||||||
for _, char := range s {
|
for _, char := range s {
|
||||||
if string(char) == old {
|
if string(char) == old {
|
||||||
result += new
|
result.WriteString(new)
|
||||||
} else {
|
} else {
|
||||||
result += string(char)
|
result.WriteString(string(char))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result.String()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package images
|
package images
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build !dev
|
//go:build !dev
|
||||||
// +build !dev
|
|
||||||
|
|
||||||
package s3
|
package s3
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build dev
|
//go:build dev
|
||||||
// +build dev
|
|
||||||
|
|
||||||
package s3
|
package s3
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build !dev
|
//go:build !dev
|
||||||
// +build !dev
|
|
||||||
|
|
||||||
package square
|
package square
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
//go:build dev
|
//go:build dev
|
||||||
// +build dev
|
|
||||||
|
|
||||||
package square
|
package square
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crussell/clock"
|
|
||||||
"context"
|
"context"
|
||||||
|
"crussell/clock"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test && dev
|
//go:build test && dev
|
||||||
// +build test,dev
|
|
||||||
|
|
||||||
package square
|
package square
|
||||||
|
|
||||||
|
|||||||
@@ -30,11 +30,11 @@ func ValidateEmail(email string) error {
|
|||||||
|
|
||||||
// NormalizeGiftCardCode strips non-alphanumeric characters and upper-cases the code.
|
// NormalizeGiftCardCode strips non-alphanumeric characters and upper-cases the code.
|
||||||
func NormalizeGiftCardCode(code string) string {
|
func NormalizeGiftCardCode(code string) string {
|
||||||
clean := ""
|
var clean strings.Builder
|
||||||
for _, char := range code {
|
for _, char := range code {
|
||||||
if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') {
|
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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package validators
|
package validators
|
||||||
|
|
||||||
|
|||||||
+24
-24
@@ -25,8 +25,8 @@ import (
|
|||||||
"crussell/db"
|
"crussell/db"
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
|
|
||||||
authHandlers "crussell/handlers/auth"
|
|
||||||
"crussell/handlers/admin"
|
"crussell/handlers/admin"
|
||||||
|
authHandlers "crussell/handlers/auth"
|
||||||
"crussell/handlers/bookings"
|
"crussell/handlers/bookings"
|
||||||
"crussell/handlers/notifications"
|
"crussell/handlers/notifications"
|
||||||
"crussell/handlers/payments"
|
"crussell/handlers/payments"
|
||||||
@@ -68,9 +68,9 @@ func limitBody(limit int64) func(http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultBodyLimit int64 = 1 * 1024 * 1024 // 1MB
|
defaultBodyLimit int64 = 1 * 1024 * 1024 // 1MB
|
||||||
uploadBodyLimit int64 = 15 * 1024 * 1024 // 15MB
|
uploadBodyLimit int64 = 15 * 1024 * 1024 // 15MB
|
||||||
portfolioBodyLimit int64 = 40 * 1024 * 1024 // 40MB (7 variants from 20MB source)
|
portfolioBodyLimit int64 = 40 * 1024 * 1024 // 40MB (7 variants from 20MB source)
|
||||||
)
|
)
|
||||||
|
|
||||||
// nColor / bColor — Chi-style ANSI colors for request logging.
|
// nColor / bColor — Chi-style ANSI colors for request logging.
|
||||||
@@ -78,11 +78,11 @@ type nColor string
|
|||||||
type bColor string
|
type bColor string
|
||||||
|
|
||||||
var (
|
var (
|
||||||
reset = nColor(logutil.Reset)
|
reset = nColor(logutil.Reset)
|
||||||
nGreen = nColor(logutil.Green)
|
nGreen = nColor(logutil.Green)
|
||||||
nYellow = nColor(logutil.Yellow)
|
nYellow = nColor(logutil.Yellow)
|
||||||
nCyan = nColor(logutil.Cyan)
|
nCyan = nColor(logutil.Cyan)
|
||||||
nRed = nColor(logutil.Red)
|
nRed = nColor(logutil.Red)
|
||||||
|
|
||||||
bGreen = bColor(logutil.BoldGreen)
|
bGreen = bColor(logutil.BoldGreen)
|
||||||
bYellow = bColor(logutil.BoldYellow)
|
bYellow = bColor(logutil.BoldYellow)
|
||||||
@@ -153,7 +153,7 @@ func healthCheckHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
} else {
|
} else {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
"status": status,
|
"status": status,
|
||||||
"services": services,
|
"services": services,
|
||||||
})
|
})
|
||||||
@@ -440,15 +440,15 @@ func main() {
|
|||||||
r.Post("/{id}/edit-requests/{request_id}/deny", bookings.AdminRejectEditRequestHandler)
|
r.Post("/{id}/edit-requests/{request_id}/deny", bookings.AdminRejectEditRequestHandler)
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Route("/admin/users", func(r chi.Router) {
|
r.Route("/admin/users", func(r chi.Router) {
|
||||||
r.Get("/", user.ListAdminUsersHandler)
|
r.Get("/", user.ListAdminUsersHandler)
|
||||||
r.Get("/{id}", user.GetAdminUserHandler)
|
r.Get("/{id}", user.GetAdminUserHandler)
|
||||||
r.Get("/{id}/relationship", user.GetCustomerRelationshipHandler)
|
r.Get("/{id}/relationship", user.GetCustomerRelationshipHandler)
|
||||||
r.Get("/{id}/patch-tests/eligible", user.GetEligiblePatchTestServicesHandler)
|
r.Get("/{id}/patch-tests/eligible", user.GetEligiblePatchTestServicesHandler)
|
||||||
r.Post("/{id}/patch-tests", user.AddPatchTestHandler)
|
r.Post("/{id}/patch-tests", user.AddPatchTestHandler)
|
||||||
r.Get("/{id}/giftcard-balance", payments.GetUserGiftCardBalanceAdmin)
|
r.Get("/{id}/giftcard-balance", payments.GetUserGiftCardBalanceAdmin)
|
||||||
r.Get("/{id}/payment-methods", payments.AdminGetUserPaymentMethods)
|
r.Get("/{id}/payment-methods", payments.AdminGetUserPaymentMethods)
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Route("/admin/today", func(r chi.Router) {
|
r.Route("/admin/today", func(r chi.Router) {
|
||||||
r.Get("/current-next", today.GetCurrentAndNextHandler)
|
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.Get("/pending-approvals", today.GetPendingApprovalsHandler)
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Route("/admin/notifications", func(r chi.Router) {
|
r.Route("/admin/notifications", func(r chi.Router) {
|
||||||
r.Get("/", notifications.GetNotifications)
|
r.Get("/", notifications.GetNotifications)
|
||||||
r.Get("/unread-count", notifications.GetUnreadCount)
|
r.Get("/unread-count", notifications.GetUnreadCount)
|
||||||
r.Post("/{id}/acknowledge", notifications.AcknowledgeNotification)
|
r.Post("/{id}/acknowledge", notifications.AcknowledgeNotification)
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Route("/admin/time-blockers", func(r chi.Router) {
|
r.Route("/admin/time-blockers", func(r chi.Router) {
|
||||||
r.Get("/", scheduling.ListTimeBlockers)
|
r.Get("/", scheduling.ListTimeBlockers)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|||||||
+2
-7
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"crussell/auth"
|
"crussell/auth"
|
||||||
@@ -77,13 +78,7 @@ func RequireRole(allowedRoles ...string) func(http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if user has one of the allowed roles
|
// Check if user has one of the allowed roles
|
||||||
hasRole := false
|
hasRole := slices.Contains(allowedRoles, role)
|
||||||
for _, allowedRole := range allowedRoles {
|
|
||||||
if role == allowedRole {
|
|
||||||
hasRole = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hasRole {
|
if !hasRole {
|
||||||
http.Error(w, "forbidden", http.StatusForbidden)
|
http.Error(w, "forbidden", http.StatusForbidden)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
|
||||||
|
|
||||||
package mw
|
package mw
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user