package bookings import ( "context" "encoding/json" "errors" "strconv" "strings" "time" "crussell/db" ) // ErrPastClosing is returned by checkClosingHours when the booking's end time // exceeds the working hours closing time for that day. var ErrPastClosing = errors.New("booking extends beyond closing hours") // checkClosingHours verifies that a booking end time (in Europe/London wall-clock // 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 // 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 { parts := strings.Split(closeStr, ":") if len(parts) < 2 { return errors.New("invalid closing time format") } closeHour, err := strconv.Atoi(parts[0]) if err != nil { return errors.New("invalid closing time format") } closeMin, err := strconv.Atoi(parts[1]) if err != nil { return errors.New("invalid closing time format") } if localEnd.Hour() > closeHour || (localEnd.Hour() == closeHour && localEnd.Minute() > closeMin) { return ErrPastClosing } return nil }