feat: add getClosingTimeForDate to check staged default hours for bookings
Add getClosingTimeForDate that queries default_hours_scheduled_changes for a pending staged change whose effective_date <= the booking date. Falls back to current working_hours if no staged change applies. Also checks the staged schedule to see if a weekday is closed (returns 00:00).
This commit is contained in:
@@ -1,10 +1,14 @@
|
|||||||
package bookings
|
package bookings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"crussell/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrPastClosing is returned by checkClosingHours when the booking's end time
|
// ErrPastClosing is returned by checkClosingHours when the booking's end time
|
||||||
@@ -15,6 +19,52 @@ var ErrPastClosing = errors.New("booking extends beyond closing hours")
|
|||||||
// time) does not exceed the closing time stored as "HH:MM" in the working_hours
|
// time) does not exceed the closing time stored as "HH:MM" in the working_hours
|
||||||
// table. Returns ErrPastClosing if the booking runs past close, or a generic
|
// table. Returns ErrPastClosing if the booking runs past close, or a generic
|
||||||
// error if closeStr cannot be parsed.
|
// error if closeStr cannot be parsed.
|
||||||
|
// getClosingTimeForDate resolves the closing time for a booking on a given
|
||||||
|
// date/weekday. It first checks for a pending default_hours_scheduled_changes
|
||||||
|
// whose effective_date <= the booking date; if found, the staged hours' closing
|
||||||
|
// time for that weekday is returned. Otherwise falls back to the current
|
||||||
|
// working_hours table. Returns the closing time as "HH:MM" (or "HH:MM:SS").
|
||||||
|
func getClosingTimeForDate(ctx context.Context, q db.Querier, weekday int, bookingDate time.Time) (string, error) {
|
||||||
|
// Check for a pending staged change that applies to this date
|
||||||
|
type stagedHour struct {
|
||||||
|
Weekday int `json:"weekday"`
|
||||||
|
StartTime string `json:"startTime"`
|
||||||
|
EndTime string `json:"endTime"`
|
||||||
|
IsOpen bool `json:"isOpen"`
|
||||||
|
}
|
||||||
|
var hoursJSON string
|
||||||
|
err := q.QueryRow(ctx, `
|
||||||
|
SELECT hours::text
|
||||||
|
FROM default_hours_scheduled_changes
|
||||||
|
WHERE effective_date <= $1::date
|
||||||
|
AND applied_at IS NULL
|
||||||
|
AND cancelled_at IS NULL
|
||||||
|
ORDER BY effective_date DESC
|
||||||
|
LIMIT 1
|
||||||
|
`, bookingDate.Format("2006-01-02")).Scan(&hoursJSON)
|
||||||
|
if err == nil && hoursJSON != "" {
|
||||||
|
var staged []stagedHour
|
||||||
|
if json.Unmarshal([]byte(hoursJSON), &staged) == nil {
|
||||||
|
for _, h := range staged {
|
||||||
|
if h.Weekday == weekday && h.IsOpen {
|
||||||
|
return h.EndTime, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Staged change found but weekday is closed — return "00:00" so the
|
||||||
|
// caller can reject the booking (the day would be closed under the
|
||||||
|
// staged schedule).
|
||||||
|
return "00:00", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to current working_hours table
|
||||||
|
var closeStr string
|
||||||
|
if err := q.QueryRow(ctx, `SELECT end_time::text FROM working_hours WHERE weekday = $1`, weekday).Scan(&closeStr); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return closeStr, nil
|
||||||
|
}
|
||||||
|
|
||||||
func checkClosingHours(localEnd time.Time, closeStr string) error {
|
func checkClosingHours(localEnd time.Time, closeStr string) error {
|
||||||
parts := strings.Split(closeStr, ":")
|
parts := strings.Split(closeStr, ":")
|
||||||
if len(parts) < 2 {
|
if len(parts) < 2 {
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ package bookings
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"crussell/clock"
|
||||||
|
"crussell/testutils"
|
||||||
|
"crussell/testutils/fixtures"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCheckClosingHours_WithinHours(t *testing.T) {
|
func TestCheckClosingHours_WithinHours(t *testing.T) {
|
||||||
@@ -91,3 +95,107 @@ func TestCheckClosingHours_InvalidFormat(t *testing.T) {
|
|||||||
func IsPastClosing(err error) bool {
|
func IsPastClosing(err error) bool {
|
||||||
return err == ErrPastClosing
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user