Skip to content

Commit ee783a4

Browse files
feat: get floors endpoint (#189)
1 parent 388c9a0 commit ee783a4

6 files changed

Lines changed: 181 additions & 4 deletions

File tree

backend/docs/swagger.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,6 +927,33 @@ paths:
927927
summary: Get Rooms By Floor
928928
tags:
929929
- rooms
930+
/rooms/floors:
931+
get:
932+
description: Retrieves all distinct floor numbers
933+
parameters:
934+
- description: Hotel ID (UUID)
935+
in: header
936+
name: X-Hotel-ID
937+
required: true
938+
type: string
939+
produces:
940+
- application/json
941+
responses:
942+
"200":
943+
description: OK
944+
schema:
945+
items:
946+
type: integer
947+
type: array
948+
"500":
949+
description: Internal Server Error
950+
schema:
951+
additionalProperties:
952+
type: string
953+
type: object
954+
summary: Get Floors
955+
tags:
956+
- rooms
930957
/users:
931958
post:
932959
consumes:

backend/internal/handler/rooms.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
type RoomsRepository interface {
1414
FindRoomsWithOptionalGuestBookingsByFloor(ctx context.Context, filter *models.RoomFilters, hotelID string, cursorRoomNumber int) ([]*models.RoomWithOptionalGuestBooking, error)
15+
FindAllFloors(ctx context.Context, hotelID string) ([]int, error)
1516
}
1617

1718
type RoomsHandler struct {
@@ -67,3 +68,26 @@ func (h *RoomsHandler) GetRoomsByFloor(c *fiber.Ctx) error {
6768

6869
return c.JSON(page)
6970
}
71+
72+
// GetFloors godoc
73+
// @Summary Get Floors
74+
// @Description Retrieves all distinct floor numbers
75+
// @Tags rooms
76+
// @Produce json
77+
// @Param X-Hotel-ID header string true "Hotel ID (UUID)"
78+
// @Success 200 {array} int
79+
// @Failure 500 {object} map[string]string
80+
// @Router /rooms/floors [get]
81+
func (h *RoomsHandler) GetFloors(c *fiber.Ctx) error {
82+
hotelID, err := hotelIDFromHeader(c)
83+
if err != nil {
84+
return err
85+
}
86+
87+
floors, err := h.repo.FindAllFloors(c.Context(), hotelID)
88+
if err != nil {
89+
return errs.InternalServerError()
90+
}
91+
92+
return c.JSON(floors)
93+
}

backend/internal/handler/rooms_test.go

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,18 @@ import (
1515
)
1616

1717
type mockRoomsRepository struct {
18-
findRoomsFunc func(ctx context.Context, filter *models.RoomFilters, hotelID string, cursorRoomNumber int) ([]*models.RoomWithOptionalGuestBooking, error)
18+
findRoomsFunc func(ctx context.Context, filter *models.RoomFilters, hotelID string, cursorRoomNumber int) ([]*models.RoomWithOptionalGuestBooking, error)
19+
findFloorsFunc func(ctx context.Context, hotelID string) ([]int, error)
1920
}
2021

2122
func (m *mockRoomsRepository) FindRoomsWithOptionalGuestBookingsByFloor(ctx context.Context, filter *models.RoomFilters, hotelID string, cursorRoomNumber int) ([]*models.RoomWithOptionalGuestBooking, error) {
2223
return m.findRoomsFunc(ctx, filter, hotelID, cursorRoomNumber)
2324
}
2425

26+
func (m *mockRoomsRepository) FindAllFloors(ctx context.Context, hotelID string) ([]int, error) {
27+
return m.findFloorsFunc(ctx, hotelID)
28+
}
29+
2530
var _ RoomsRepository = (*mockRoomsRepository)(nil)
2631

2732
const testHotelID = "00000000-0000-0000-0000-000000000001"
@@ -260,3 +265,94 @@ func TestRoomsHandler_GetRoomsByFloor(t *testing.T) {
260265
assert.Equal(t, 500, resp.StatusCode)
261266
})
262267
}
268+
269+
func TestRoomsHandler_GetFloors(t *testing.T) {
270+
t.Parallel()
271+
272+
t.Run("returns 200 with floors", func(t *testing.T) {
273+
t.Parallel()
274+
275+
mock := &mockRoomsRepository{
276+
findFloorsFunc: func(ctx context.Context, hotelID string) ([]int, error) {
277+
return []int{1, 2, 3}, nil
278+
},
279+
}
280+
281+
app := fiber.New(fiber.Config{ErrorHandler: errs.ErrorHandler})
282+
h := NewRoomsHandler(mock)
283+
app.Get("/rooms/floors", h.GetFloors)
284+
285+
req := httptest.NewRequest("GET", "/rooms/floors", nil)
286+
req.Header.Set(hotelIDHeader, testHotelID)
287+
resp, err := app.Test(req)
288+
require.NoError(t, err)
289+
290+
assert.Equal(t, 200, resp.StatusCode)
291+
body, _ := io.ReadAll(resp.Body)
292+
assert.Contains(t, string(body), `1`)
293+
assert.Contains(t, string(body), `2`)
294+
assert.Contains(t, string(body), `3`)
295+
})
296+
297+
t.Run("returns 400 when hotel_id header is missing", func(t *testing.T) {
298+
t.Parallel()
299+
300+
mock := &mockRoomsRepository{
301+
findFloorsFunc: func(ctx context.Context, hotelID string) ([]int, error) {
302+
return []int{}, nil
303+
},
304+
}
305+
306+
app := fiber.New(fiber.Config{ErrorHandler: errs.ErrorHandler})
307+
h := NewRoomsHandler(mock)
308+
app.Get("/rooms/floors", h.GetFloors)
309+
310+
req := httptest.NewRequest("GET", "/rooms/floors", nil)
311+
resp, err := app.Test(req)
312+
require.NoError(t, err)
313+
314+
assert.Equal(t, 400, resp.StatusCode)
315+
})
316+
317+
t.Run("returns 400 when hotel_id header is invalid", func(t *testing.T) {
318+
t.Parallel()
319+
320+
mock := &mockRoomsRepository{
321+
findFloorsFunc: func(ctx context.Context, hotelID string) ([]int, error) {
322+
return []int{}, nil
323+
},
324+
}
325+
326+
app := fiber.New(fiber.Config{ErrorHandler: errs.ErrorHandler})
327+
h := NewRoomsHandler(mock)
328+
app.Get("/rooms/floors", h.GetFloors)
329+
330+
req := httptest.NewRequest("GET", "/rooms/floors", nil)
331+
req.Header.Set(hotelIDHeader, "not-a-uuid")
332+
resp, err := app.Test(req)
333+
require.NoError(t, err)
334+
335+
assert.Equal(t, 400, resp.StatusCode)
336+
})
337+
338+
t.Run("returns 500 when repository fails", func(t *testing.T) {
339+
t.Parallel()
340+
341+
mock := &mockRoomsRepository{
342+
findFloorsFunc: func(ctx context.Context, hotelID string) ([]int, error) {
343+
return nil, errors.New("db error")
344+
},
345+
}
346+
347+
app := fiber.New(fiber.Config{ErrorHandler: errs.ErrorHandler})
348+
h := NewRoomsHandler(mock)
349+
app.Get("/rooms/floors", h.GetFloors)
350+
351+
req := httptest.NewRequest("GET", "/rooms/floors", nil)
352+
req.Header.Set(hotelIDHeader, testHotelID)
353+
resp, err := app.Test(req)
354+
require.NoError(t, err)
355+
356+
assert.Equal(t, 500, resp.StatusCode)
357+
})
358+
}

backend/internal/repository/rooms.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,30 @@ func (r *RoomsRepository) FindRoomsWithOptionalGuestBookingsByFloor(ctx context.
7575
}
7676
return rooms, nil
7777
}
78+
79+
func (r *RoomsRepository) FindAllFloors(ctx context.Context, hotelID string) ([]int, error) {
80+
rows, err := r.db.Query(ctx, `
81+
SELECT DISTINCT floor
82+
FROM rooms
83+
WHERE hotel_id = $1
84+
ORDER BY floor ASC`, hotelID)
85+
if err != nil {
86+
return nil, err
87+
}
88+
defer rows.Close()
89+
90+
var floors []int
91+
for rows.Next() {
92+
var floor int
93+
if err := rows.Scan(&floor); err != nil {
94+
return nil, err
95+
}
96+
floors = append(floors, floor)
97+
}
98+
99+
if err := rows.Err(); err != nil {
100+
return nil, err
101+
}
102+
103+
return floors, nil
104+
}

backend/internal/service/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ func setupRoutes(app *fiber.App, repo *storage.Repository, genkitInstance *aiflo
160160
// rooms routes
161161
api.Route("/rooms", func(r fiber.Router) {
162162
r.Get("/", roomsHandler.GetRoomsByFloor)
163+
r.Get("/floors", roomsHandler.GetFloors)
163164
})
164165

165166
// s3 routes

backend/internal/service/storage/postgres/repo_types.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,17 @@ type GuestsRepository interface {
2121

2222
type RequestsRepository interface {
2323
InsertRequest(ctx context.Context, req *models.Request) (*models.Request, error)
24-
2524
FindRequest(ctx context.Context, id string) (*models.Request, error)
26-
2725
FindRequests(ctx context.Context) ([]models.Request, error)
28-
2926
FindRequestsByStatusPaginated(ctx context.Context, cursor string, status string, hotelID string, pageSize int) ([]*models.Request, string, error)
3027
}
3128

3229
type HotelsRepository interface {
3330
FindByID(ctx context.Context, id string) (*models.Hotel, error)
3431
InsertHotel(ctx context.Context, hotel *models.CreateHotelRequest) (*models.Hotel, error)
3532
}
33+
34+
type RoomsRepository interface {
35+
FindRoomsWithOptionalGuestBookingsByFloor(ctx context.Context, filter *models.RoomFilters, hotelID string, cursorRoomNumber int) ([]*models.RoomWithOptionalGuestBooking, error)
36+
FindAllFloors(ctx context.Context, hotelID string) ([]int, error)
37+
}

0 commit comments

Comments
 (0)