Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 22 additions & 19 deletions backend/docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ definitions:
example: Dao Ho
type: string
type: object
FilterRoomsRequest:
properties:
cursor:
type: string
floors:
items:
type: integer
type: array
limit:
type: integer
type: object
GenerateRequestInput:
properties:
hotel_id:
Expand Down Expand Up @@ -919,30 +930,22 @@ paths:
tags:
- requests
/rooms:
get:
description: Retrieves rooms optionally filtered by floor, with any active guest
bookings
post:
consumes:
- application/json
description: Retrieves rooms with optional floor filters and cursor pagination,
including any active guest bookings
parameters:
- description: Hotel ID (UUID)
in: header
name: X-Hotel-ID
required: true
type: string
- collectionFormat: csv
description: floors
in: query
items:
type: integer
name: floors
type: array
- description: Opaque cursor for the next page
in: query
name: cursor
type: string
- description: Number of items per page (1-100, default 20)
in: query
name: limit
type: integer
- description: Filters and pagination
in: body
name: body
schema:
$ref: '#/definitions/FilterRoomsRequest'
produces:
- application/json
responses:
Expand All @@ -964,7 +967,7 @@ paths:
type: object
security:
- BearerAuth: []
summary: Get Rooms By Floor
summary: List rooms with filters
tags:
- rooms
/rooms/floors:
Expand Down
33 changes: 18 additions & 15 deletions backend/internal/handler/rooms.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,35 +23,38 @@ func NewRoomsHandler(repo RoomsRepository) *RoomsHandler {
return &RoomsHandler{repo: repo}
}

// GetRoomsByFloor godoc
// @Summary Get Rooms By Floor
// @Description Retrieves rooms optionally filtered by floor, with any active guest bookings
// FilterRooms godoc
// @Summary List rooms with filters
// @Description Retrieves rooms with optional floor filters and cursor pagination, including any active guest bookings
// @Tags rooms
// @Accept json
// @Produce json
// @Param X-Hotel-ID header string true "Hotel ID (UUID)"
// @Param floors query []int false "floors"
// @Param cursor query string false "Opaque cursor for the next page"
// @Param limit query int false "Number of items per page (1-100, default 20)"
// @Param X-Hotel-ID header string true "Hotel ID (UUID)"
// @Param body body models.FilterRoomsRequest false "Filters and pagination"
// @Success 200 {object} utils.CursorPage[models.RoomWithOptionalGuestBooking]
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security BearerAuth
// @Router /rooms [get]
func (h *RoomsHandler) GetRoomsByFloor(c *fiber.Ctx) error {
// @Router /rooms [post]
func (h *RoomsHandler) FilterRooms(c *fiber.Ctx) error {
hotelID, err := hotelIDFromHeader(c)
if err != nil {
return err
}

filter := new(models.RoomFilters)
if err := c.QueryParser(filter); err != nil {
return errs.BadRequest("invalid filters")
var body models.FilterRoomsRequest
if err := c.BodyParser(&body); err != nil {
return errs.InvalidJSON()
}

filter := &models.RoomFilters{
Floors: body.Floors,
Limit: body.Limit,
}

cursor := c.Query("cursor", "")
cursorRoomNumber := 0
if cursor != "" {
cursorRoomNumber, err = strconv.Atoi(cursor)
if body.Cursor != "" {
cursorRoomNumber, err = strconv.Atoi(body.Cursor)
if err != nil {
return errs.BadRequest("invalid cursor")
}
Expand Down
67 changes: 48 additions & 19 deletions backend/internal/handler/rooms_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"io"
"net/http/httptest"
"strings"
"testing"

"github.com/generate/selfserve/internal/errs"
Expand All @@ -31,7 +32,7 @@ var _ RoomsRepository = (*mockRoomsRepository)(nil)

const testHotelID = "00000000-0000-0000-0000-000000000001"

func TestRoomsHandler_GetRoomsByFloor(t *testing.T) {
func TestRoomsHandler_FilterRooms(t *testing.T) {
t.Parallel()

t.Run("returns 200 with rooms and no guests when rooms are vacant", func(t *testing.T) {
Expand All @@ -50,9 +51,10 @@ func TestRoomsHandler_GetRoomsByFloor(t *testing.T) {

app := fiber.New()
h := NewRoomsHandler(mock)
app.Get("/rooms", h.GetRoomsByFloor)
app.Post("/rooms", h.FilterRooms)

req := httptest.NewRequest("GET", "/rooms", nil)
req := httptest.NewRequest("POST", "/rooms", strings.NewReader(`{}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set(hotelIDHeader, testHotelID)
resp, err := app.Test(req)
require.NoError(t, err)
Expand Down Expand Up @@ -94,9 +96,10 @@ func TestRoomsHandler_GetRoomsByFloor(t *testing.T) {

app := fiber.New()
h := NewRoomsHandler(mock)
app.Get("/rooms", h.GetRoomsByFloor)
app.Post("/rooms", h.FilterRooms)

req := httptest.NewRequest("GET", "/rooms?floors=2", nil)
req := httptest.NewRequest("POST", "/rooms", strings.NewReader(`{"floors":[2]}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set(hotelIDHeader, testHotelID)
resp, err := app.Test(req)
require.NoError(t, err)
Expand All @@ -122,9 +125,10 @@ func TestRoomsHandler_GetRoomsByFloor(t *testing.T) {

app := fiber.New()
h := NewRoomsHandler(mock)
app.Get("/rooms", h.GetRoomsByFloor)
app.Post("/rooms", h.FilterRooms)

req := httptest.NewRequest("GET", "/rooms?floors=99", nil)
req := httptest.NewRequest("POST", "/rooms", strings.NewReader(`{"floors":[99]}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set(hotelIDHeader, testHotelID)
resp, err := app.Test(req)
require.NoError(t, err)
Expand Down Expand Up @@ -154,9 +158,10 @@ func TestRoomsHandler_GetRoomsByFloor(t *testing.T) {

app := fiber.New()
h := NewRoomsHandler(mock)
app.Get("/rooms", h.GetRoomsByFloor)
app.Post("/rooms", h.FilterRooms)

req := httptest.NewRequest("GET", "/rooms?limit=5", nil)
req := httptest.NewRequest("POST", "/rooms", strings.NewReader(`{"limit":5}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set(hotelIDHeader, testHotelID)
resp, err := app.Test(req)
require.NoError(t, err)
Expand All @@ -171,8 +176,6 @@ func TestRoomsHandler_GetRoomsByFloor(t *testing.T) {
t.Run("passes cursor, filter, and hotelID to repository", func(t *testing.T) {
t.Parallel()

cursor := "200"

var capturedFilter *models.RoomFilters
var capturedHotelID string
var capturedCursor int
Expand All @@ -187,9 +190,10 @@ func TestRoomsHandler_GetRoomsByFloor(t *testing.T) {

app := fiber.New()
h := NewRoomsHandler(mock)
app.Get("/rooms", h.GetRoomsByFloor)
app.Post("/rooms", h.FilterRooms)

req := httptest.NewRequest("GET", "/rooms?cursor="+cursor+"&limit=10", nil)
req := httptest.NewRequest("POST", "/rooms", strings.NewReader(`{"cursor":"200","limit":10}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set(hotelIDHeader, testHotelID)
resp, err := app.Test(req)
require.NoError(t, err)
Expand All @@ -212,9 +216,10 @@ func TestRoomsHandler_GetRoomsByFloor(t *testing.T) {

app := fiber.New(fiber.Config{ErrorHandler: errs.ErrorHandler})
h := NewRoomsHandler(mock)
app.Get("/rooms", h.GetRoomsByFloor)
app.Post("/rooms", h.FilterRooms)

req := httptest.NewRequest("GET", "/rooms", nil)
req := httptest.NewRequest("POST", "/rooms", strings.NewReader(`{}`))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
require.NoError(t, err)

Expand All @@ -232,9 +237,10 @@ func TestRoomsHandler_GetRoomsByFloor(t *testing.T) {

app := fiber.New(fiber.Config{ErrorHandler: errs.ErrorHandler})
h := NewRoomsHandler(mock)
app.Get("/rooms", h.GetRoomsByFloor)
app.Post("/rooms", h.FilterRooms)

req := httptest.NewRequest("GET", "/rooms", nil)
req := httptest.NewRequest("POST", "/rooms", strings.NewReader(`{}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set(hotelIDHeader, "not-a-uuid")
resp, err := app.Test(req)
require.NoError(t, err)
Expand All @@ -255,15 +261,38 @@ func TestRoomsHandler_GetRoomsByFloor(t *testing.T) {

app := fiber.New(fiber.Config{ErrorHandler: errs.ErrorHandler})
h := NewRoomsHandler(mock)
app.Get("/rooms", h.GetRoomsByFloor)
app.Post("/rooms", h.FilterRooms)

req := httptest.NewRequest("GET", "/rooms", nil)
req := httptest.NewRequest("POST", "/rooms", strings.NewReader(`{}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set(hotelIDHeader, testHotelID)
resp, err := app.Test(req)
require.NoError(t, err)

assert.Equal(t, 500, resp.StatusCode)
})

t.Run("returns 400 when request body is invalid json", func(t *testing.T) {
t.Parallel()

mock := &mockRoomsRepository{
findRoomsFunc: func(ctx context.Context, filter *models.RoomFilters, hotelID string, cursorRoomNumber int) ([]*models.RoomWithOptionalGuestBooking, error) {
return nil, nil
},
}

app := fiber.New(fiber.Config{ErrorHandler: errs.ErrorHandler})
h := NewRoomsHandler(mock)
app.Post("/rooms", h.FilterRooms)

req := httptest.NewRequest("POST", "/rooms", strings.NewReader(`{`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set(hotelIDHeader, testHotelID)
resp, err := app.Test(req)
require.NoError(t, err)

assert.Equal(t, 400, resp.StatusCode)
})
}

func TestRoomsHandler_GetFloors(t *testing.T) {
Expand Down
6 changes: 6 additions & 0 deletions backend/internal/models/rooms.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ type RoomFilters struct {
Limit int `query:"limit"`
}

type FilterRoomsRequest struct {
Floors *[]int `json:"floors,omitempty"`
Limit int `json:"limit,omitempty"`
Cursor string `json:"cursor,omitempty"`
} //@name FilterRoomsRequest

// Read model for rooms page on the frontend
type RoomWithOptionalGuestBooking struct {
Room
Expand Down
2 changes: 1 addition & 1 deletion backend/internal/service/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ func setupRoutes(app *fiber.App, repo *storage.Repository, genkitInstance *aiflo

// rooms routes
api.Route("/rooms", func(r fiber.Router) {
r.Get("/", roomsHandler.GetRoomsByFloor)
r.Post("/", roomsHandler.FilterRooms)
r.Get("/floors", roomsHandler.GetFloors)
})

Expand Down
4 changes: 2 additions & 2 deletions backend/scripts/seed.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ echo ""
echo " export HOTEL_ID='a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'"
echo ""
echo " # All rooms:"
echo " curl -s -H \"X-Hotel-ID: \$HOTEL_ID\" 'http://localhost:${APP_PORT}/api/v1/rooms' | jq"
echo " curl -s -X POST -H \"Content-Type: application/json\" -H \"X-Hotel-ID: \$HOTEL_ID\" -d '{}' 'http://localhost:${APP_PORT}/api/v1/rooms' | jq"
echo ""
echo " # Floor 1 only:"
echo " curl -s -H \"X-Hotel-ID: \$HOTEL_ID\" 'http://localhost:${APP_PORT}/api/v1/rooms?floors=1' | jq"
echo " curl -s -X POST -H \"Content-Type: application/json\" -H \"X-Hotel-ID: \$HOTEL_ID\" -d '{\"floors\":[1]}' 'http://localhost:${APP_PORT}/api/v1/rooms' | jq"
echo ""
echo " # All guests:"
echo " curl -s -H \"X-Hotel-ID: \$HOTEL_ID\" 'http://localhost:${APP_PORT}/api/v1/guests' | jq"
Loading