feat(bookings): add out_of_hours field to Booking struct and admin queries

Include out_of_hours in GetAllAdminBookings, GetAdminBooking, and SearchAdminBookings queries. Add tests verifying out_of_hours appears in single and list booking responses.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-06-22 12:54:44 +01:00
co-authored by Sisyphus
parent b4c1e98176
commit 260c760971
2 changed files with 386 additions and 3 deletions
+376
View File
@@ -3325,3 +3325,379 @@ func TestAdminBookings_AdminReserve_WithCustomServices(t *testing.T) {
t.Errorf("expected description to start with 'RESERVATION:admin:callin:', got %s", desc)
}
}
// =============================================================================
// GetAdminBookingHandler — out_of_hours field
// =============================================================================
func TestGetAdminBooking_OutOfHoursField(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
userID, err := fixtures.CreateTestUser(tx)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
serviceID, err := fixtures.CreateTestService(tx)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
futureTime := time.Now().Add(72 * time.Hour).Truncate(time.Second)
var bookingID string
err = tx.QueryRow(ctx, `
INSERT INTO bookings (user_id, start_time, status, out_of_hours)
VALUES ($1, $2, 'confirmed', true)
RETURNING id
`, userID, futureTime).Scan(&bookingID)
if err != nil {
t.Fatalf("failed to create out-of-hours booking: %v", err)
}
_, err = tx.Exec(ctx, `
INSERT INTO booking_services (booking_id, service_id)
VALUES ($1, $2)
`, bookingID, serviceID)
if err != nil {
t.Fatalf("failed to link service: %v", err)
}
handler := http.HandlerFunc(bookings.GetAdminBookingHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/bookings/"+bookingID, nil, ctx)
if w.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var booking bookings.Booking
if err := parseResponseBody(w, &booking); err != nil {
t.Fatalf("failed to parse booking: %v", err)
}
if !booking.OutOfHours {
t.Error("expected out_of_hours=true in booking response")
}
}
func TestGetAdminBooking_OutOfHoursFalseByDefault(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
userID, err := fixtures.CreateTestUser(tx)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
serviceID, err := fixtures.CreateTestService(tx)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
var bookingID string
err = tx.QueryRow(ctx, `
INSERT INTO bookings (user_id, start_time, status)
VALUES ($1, $2, 'confirmed')
RETURNING id
`, userID, time.Now().Add(72*time.Hour).Truncate(time.Second)).Scan(&bookingID)
if err != nil {
t.Fatalf("failed to create booking: %v", err)
}
_, err = tx.Exec(ctx, `
INSERT INTO booking_services (booking_id, service_id)
VALUES ($1, $2)
`, bookingID, serviceID)
if err != nil {
t.Fatalf("failed to link service: %v", err)
}
handler := http.HandlerFunc(bookings.GetAdminBookingHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/bookings/"+bookingID, nil, ctx)
if w.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var booking bookings.Booking
if err := parseResponseBody(w, &booking); err != nil {
t.Fatalf("failed to parse booking: %v", err)
}
if booking.OutOfHours {
t.Error("expected out_of_hours=false (default) for normal booking")
}
}
// =============================================================================
// GetAllAdminBookingsHandler — out_of_hours field
// =============================================================================
func TestAdminBookings_List_OutOfHoursField(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
_, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
userID, err := fixtures.CreateTestUser(tx)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
serviceID, err := fixtures.CreateTestService(tx)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
futureTime := time.Now().Add(72 * time.Hour).Truncate(time.Second)
var bookingID string
err = tx.QueryRow(ctx, `
INSERT INTO bookings (user_id, start_time, status, out_of_hours)
VALUES ($1, $2, 'confirmed', true)
RETURNING id
`, userID, futureTime).Scan(&bookingID)
if err != nil {
t.Fatalf("failed to create out-of-hours booking: %v", err)
}
_, err = tx.Exec(ctx, `
INSERT INTO booking_services (booking_id, service_id)
VALUES ($1, $2)
`, bookingID, serviceID)
if err != nil {
t.Fatalf("failed to link service: %v", err)
}
handler := http.HandlerFunc(bookings.GetAllAdminBookingsHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/bookings", nil, ctx)
if w.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var resp bookings.BookingListResponse
if err := parseResponseBody(w, &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if len(resp.Bookings) == 0 {
t.Fatal("expected at least 1 booking in list response")
}
var found bool
for _, b := range resp.Bookings {
if b.ID == bookingID {
if !b.OutOfHours {
t.Error("expected out_of_hours=true for the out-of-hours booking in list response")
}
found = true
break
}
}
if !found {
t.Error("expected out-of-hours booking to appear in list response")
}
}
func TestAdminBookings_List_OutOfHoursFalseByDefault(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
_, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
userID, err := fixtures.CreateTestUser(tx)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
serviceID, err := fixtures.CreateTestService(tx)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
var bookingID string
err = tx.QueryRow(ctx, `
INSERT INTO bookings (user_id, start_time, status)
VALUES ($1, $2, 'confirmed')
RETURNING id
`, userID, time.Now().Add(72*time.Hour).Truncate(time.Second)).Scan(&bookingID)
if err != nil {
t.Fatalf("failed to create booking: %v", err)
}
_, err = tx.Exec(ctx, `
INSERT INTO booking_services (booking_id, service_id)
VALUES ($1, $2)
`, bookingID, serviceID)
if err != nil {
t.Fatalf("failed to link service: %v", err)
}
handler := http.HandlerFunc(bookings.GetAllAdminBookingsHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/bookings", nil, ctx)
if w.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var resp bookings.BookingListResponse
if err := parseResponseBody(w, &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if len(resp.Bookings) == 0 {
t.Fatal("expected at least 1 booking in list response")
}
var found bool
for _, b := range resp.Bookings {
if b.ID == bookingID {
if b.OutOfHours {
t.Error("expected out_of_hours=false for normal booking in list response")
}
found = true
break
}
}
if !found {
t.Error("expected normal booking to appear in list response")
}
}
// =============================================================================
// AdminCreateBookingForUserHandler — out_of_hours field
// =============================================================================
func TestAdminBookings_Create_OutOfHours(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
_, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
userID, err := fixtures.CreateTestUser(tx)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
serviceID, err := fixtures.CreateTestService(tx)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
if err != nil {
t.Fatalf("failed to set deposits_required: %v", err)
}
futureTime := time.Now().Add(72 * time.Hour).Truncate(time.Second)
req := bookings.AdminCreateBookingForUserRequest{
UserID: userID,
StartTime: futureTime,
ServiceIDs: []string{serviceID},
OutOfHours: true,
}
handler := http.HandlerFunc(bookings.AdminCreateBookingForUserHandler)
w := makeAdminRequest(handler, "POST", "/api/admin/bookings", req, ctx)
if w.Code != http.StatusCreated {
t.Fatalf("expected status 201, got %d. body: %s", w.Code, w.Body.String())
}
var response map[string]interface{}
if err := parseResponseBody(w, &response); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
bookingData, ok := response["booking"].(map[string]interface{})
if !ok {
t.Fatal("expected booking in response")
}
outOfHours, ok := bookingData["out_of_hours"].(bool)
if !ok {
t.Fatal("expected out_of_hours field in booking response")
}
if !outOfHours {
t.Error("expected out_of_hours=true in create booking response")
}
bookingID, ok := bookingData["id"].(string)
if !ok || bookingID == "" {
t.Fatal("expected booking id in response")
}
var dbOutOfHours bool
err = tx.QueryRow(ctx, "SELECT out_of_hours FROM bookings WHERE id = $1", bookingID).Scan(&dbOutOfHours)
if err != nil {
t.Fatalf("failed to query booking out_of_hours: %v", err)
}
if !dbOutOfHours {
t.Error("expected out_of_hours=true in database")
}
}
func TestAdminBookings_Create_OutOfHoursFalseByDefault(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
_, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
userID, err := fixtures.CreateTestUser(tx)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
serviceID, err := fixtures.CreateTestService(tx)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
if err != nil {
t.Fatalf("failed to set deposits_required: %v", err)
}
futureTime := time.Now().Add(72 * time.Hour).Truncate(time.Second)
req := bookings.AdminCreateBookingForUserRequest{
UserID: userID,
StartTime: futureTime,
ServiceIDs: []string{serviceID},
}
handler := http.HandlerFunc(bookings.AdminCreateBookingForUserHandler)
w := makeAdminRequest(handler, "POST", "/api/admin/bookings", req, ctx)
if w.Code != http.StatusCreated {
t.Fatalf("expected status 201, got %d. body: %s", w.Code, w.Body.String())
}
var response map[string]interface{}
if err := parseResponseBody(w, &response); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
bookingData, ok := response["booking"].(map[string]interface{})
if !ok {
t.Fatal("expected booking in response")
}
outOfHours, ok := bookingData["out_of_hours"].(bool)
if !ok {
t.Fatal("expected out_of_hours field in booking response")
}
if outOfHours {
t.Error("expected out_of_hours=false (default) in create booking response")
}
}