//go:build test && dev package bookings import ( "testing" "time" "crussell/clock" "crussell/testutils" "crussell/testutils/fixtures" ) func TestCheckClosingHours_WithinHours(t *testing.T) { // Closing at 17:00, booking ends at 16:30 — should pass. london, err := time.LoadLocation("Europe/London") if err != nil { t.Fatalf("failed to load location: %v", err) } localEnd := time.Date(2026, 6, 24, 16, 30, 0, 0, london) if err := checkClosingHours(localEnd, "17:00"); err != nil { t.Errorf("expected no error for 16:30 end vs 17:00 close, got: %v", err) } } func TestCheckClosingHours_AtClosing(t *testing.T) { // Closing at 17:00, booking ends exactly at 17:00 — should pass. london, err := time.LoadLocation("Europe/London") if err != nil { t.Fatalf("failed to load location: %v", err) } localEnd := time.Date(2026, 6, 24, 17, 0, 0, 0, london) if err := checkClosingHours(localEnd, "17:00"); err != nil { t.Errorf("expected no error for 17:00 end vs 17:00 close, got: %v", err) } } func TestCheckClosingHours_PastClosing(t *testing.T) { // Closing at 17:00, booking ends at 17:01 — should return ErrPastClosing. london, err := time.LoadLocation("Europe/London") if err != nil { t.Fatalf("failed to load location: %v", err) } localEnd := time.Date(2026, 6, 24, 17, 1, 0, 0, london) if err := checkClosingHours(localEnd, "17:00"); !IsPastClosing(err) { t.Errorf("expected ErrPastClosing for 17:01 end vs 17:00 close, got: %v", err) } } func TestCheckClosingHours_WellPastClosing(t *testing.T) { // Closing at 17:00, booking ends at 18:00 — should return ErrPastClosing. london, err := time.LoadLocation("Europe/London") if err != nil { t.Fatalf("failed to load location: %v", err) } localEnd := time.Date(2026, 6, 24, 18, 0, 0, 0, london) if err := checkClosingHours(localEnd, "17:00"); !IsPastClosing(err) { t.Errorf("expected ErrPastClosing for 18:00 end vs 17:00 close, got: %v", err) } } func TestCheckClosingHours_InvalidFormat(t *testing.T) { // Malformed closing time string — should return a generic error, not ErrPastClosing. london, err := time.LoadLocation("Europe/London") if err != nil { t.Fatalf("failed to load location: %v", err) } localEnd := time.Date(2026, 6, 24, 14, 0, 0, 0, london) tests := []struct { name string closeStr string }{ {"empty string", ""}, {"no colon", "1700"}, {"non-numeric hour", "ab:00"}, {"non-numeric minute", "17:ab"}, {"only hour", "17"}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { err := checkClosingHours(localEnd, tc.closeStr) if err == nil { t.Error("expected error for invalid format, got nil") } if IsPastClosing(err) { t.Errorf("expected non-past-closing error for invalid format %q, got ErrPastClosing", tc.closeStr) } }) } } // IsPastClosing reports whether err indicates the booking extends beyond closing hours. // Exported helper for tests. func IsPastClosing(err error) bool { return err == ErrPastClosing } func TestGetClosingTimeForDate_FallsBackToWorkingHours(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) closeStr, err := getClosingTimeForDate(ctx, tx, 1, time.Now()) if err != nil { t.Fatalf("unexpected error: %v", err) } if closeStr == "" { t.Error("expected non-empty closing time") } } func TestGetClosingTimeForDate_StagedChangeOpenDay(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestAdminUser(tx) if err != nil { t.Fatalf("failed to create admin user: %v", err) } defer fixtures.DeleteUser(tx, adminID) hoursJSON := `[{"weekday":0,"startTime":"10:00","endTime":"18:00","isOpen":true}]` tomorrow := time.Now().AddDate(0, 0, 1).Format("2006-01-02") _, err = tx.Exec(ctx, ` INSERT INTO default_hours_scheduled_changes (effective_date, created_by, hours) VALUES ($1, $2, $3) `, tomorrow, adminID, hoursJSON) if err != nil { t.Fatalf("failed to insert staged change: %v", err) } bookingDate := time.Now().AddDate(0, 0, 1) closeStr, err := getClosingTimeForDate(ctx, tx, 0, bookingDate) if err != nil { t.Fatalf("unexpected error: %v", err) } if closeStr != "18:00" { t.Errorf("expected 18:00 from staged change, got %q", closeStr) } } func TestGetClosingTimeForDate_StagedChangeClosedDay(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestAdminUser(tx) if err != nil { t.Fatalf("failed to create admin user: %v", err) } defer fixtures.DeleteUser(tx, adminID) hoursJSON := `[{"weekday":0,"startTime":"00:00","endTime":"00:00","isOpen":false}]` tomorrow := time.Now().AddDate(0, 0, 1).Format("2006-01-02") _, err = tx.Exec(ctx, ` INSERT INTO default_hours_scheduled_changes (effective_date, created_by, hours) VALUES ($1, $2, $3) `, tomorrow, adminID, hoursJSON) if err != nil { t.Fatalf("failed to insert staged change: %v", err) } tomorrowLondon := time.Now().AddDate(0, 0, 1).In(clock.London) weekday := int((tomorrowLondon.Weekday() + 6) % 7) closeStr, err := getClosingTimeForDate(ctx, tx, weekday, tomorrowLondon) if err != nil { t.Fatalf("unexpected error: %v", err) } if closeStr != "00:00" { t.Errorf("expected 00:00 for closed day, got %q", closeStr) } } func TestGetClosingTimeForDate_StagedChangeNotYetEffective(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestAdminUser(tx) if err != nil { t.Fatalf("failed to create admin user: %v", err) } defer fixtures.DeleteUser(tx, adminID) hoursJSON := `[{"weekday":0,"startTime":"10:00","endTime":"18:00","isOpen":true}]` tomorrow := time.Now().AddDate(0, 0, 1).Format("2006-01-02") _, err = tx.Exec(ctx, ` INSERT INTO default_hours_scheduled_changes (effective_date, created_by, hours) VALUES ($1, $2, $3) `, tomorrow, adminID, hoursJSON) if err != nil { t.Fatalf("failed to insert staged change: %v", err) } closeStr, err := getClosingTimeForDate(ctx, tx, 1, time.Now()) if err != nil { t.Fatalf("unexpected error: %v", err) } if closeStr == "" { t.Error("expected non-empty closing time") } }