From 2950af333237923d3c7c5fb988f66a46828a6e55 Mon Sep 17 00:00:00 2001 From: androidlover5842 Date: Wed, 4 Feb 2026 17:22:28 +0530 Subject: [PATCH] Add forecast occupancy logic for room availability range APIs --- docs/API_REFERENCE.txt | 3 ++ .../trisolarisserver/controller/room/Rooms.kt | 21 ++++++++++- .../repo/room/RoomStayRepo.kt | 37 +++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/docs/API_REFERENCE.txt b/docs/API_REFERENCE.txt index 1e84033..9714d2d 100644 --- a/docs/API_REFERENCE.txt +++ b/docs/API_REFERENCE.txt @@ -1004,6 +1004,8 @@ ROOM TYPE + ROOMS + ROOM IMAGES What it does: - Returns free rooms in date range. + - Uses forecast logic for active stays: if `toAt` is null, occupancy is considered up to booking `expectedCheckOutAt` (if present). + - Active stays with no expected checkout are treated as occupied. Request body: @@ -1031,6 +1033,7 @@ ROOM TYPE + ROOMS + ROOM IMAGES What it does: - Returns available rooms with average rate over date range. + - Uses same forecast occupancy logic as availability-range. Request body: diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt b/src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt index 00b07ab..80f68b1 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt @@ -204,7 +204,7 @@ class Rooms( val toAt = toDate.atStartOfDay(zone).toOffsetDateTime() val rooms = roomRepo.findByPropertyIdOrderByRoomNumber(propertyId) - val occupiedRoomIds = roomStayRepo.findOccupiedRoomIdsBetween(propertyId, fromAt, toAt).toHashSet() + val occupiedRoomIds = findForecastOccupiedRoomIds(propertyId, fromAt, toAt) val freeRooms = rooms.filter { it.active && !it.maintenance && !occupiedRoomIds.contains(it.id) } val grouped = freeRooms.groupBy { it.roomType.name } @@ -242,7 +242,7 @@ class Rooms( val toAt = toDate.atStartOfDay(zone).toOffsetDateTime() val rooms = roomRepo.findByPropertyIdOrderByRoomNumber(propertyId) - val occupiedRoomIds = roomStayRepo.findOccupiedRoomIdsBetween(propertyId, fromAt, toAt).toHashSet() + val occupiedRoomIds = findForecastOccupiedRoomIds(propertyId, fromAt, toAt) val freeRooms = rooms.filter { it.active && !it.maintenance && !occupiedRoomIds.contains(it.id) } if (freeRooms.isEmpty()) return emptyList() @@ -447,6 +447,23 @@ class Rooms( .mapValues { (_, cards) -> cards.maxBy { it.expiresAt }.expiresAt } } + private fun findForecastOccupiedRoomIds( + propertyId: UUID, + fromAt: OffsetDateTime, + toAt: OffsetDateTime + ): Set { + val occupied = roomStayRepo.findOccupiedRoomIdsBetweenClosed(propertyId, fromAt, toAt).toMutableSet() + val activeRows = roomStayRepo.findActiveForecastRows(propertyId, toAt) + activeRows + .asSequence() + .filter { row -> + val expectedCheckoutAt = row.expectedCheckoutAt + expectedCheckoutAt == null || expectedCheckoutAt.isAfter(fromAt) + } + .mapTo(occupied) { it.roomId } + return occupied + } + private fun prepareSse(response: HttpServletResponse) { response.setHeader("Cache-Control", "no-cache") response.setHeader("Connection", "keep-alive") diff --git a/src/main/kotlin/com/android/trisolarisserver/repo/room/RoomStayRepo.kt b/src/main/kotlin/com/android/trisolarisserver/repo/room/RoomStayRepo.kt index e1eab0c..103e433 100644 --- a/src/main/kotlin/com/android/trisolarisserver/repo/room/RoomStayRepo.kt +++ b/src/main/kotlin/com/android/trisolarisserver/repo/room/RoomStayRepo.kt @@ -30,6 +30,37 @@ interface RoomStayRepo : JpaRepository { @Param("toAt") toAt: java.time.OffsetDateTime ): List + @Query(""" + select distinct rs.room.id + from RoomStay rs + where rs.property.id = :propertyId + and rs.isVoided = false + and rs.toAt is not null + and rs.fromAt < :toAt + and rs.toAt > :fromAt + """) + fun findOccupiedRoomIdsBetweenClosed( + @Param("propertyId") propertyId: UUID, + @Param("fromAt") fromAt: java.time.OffsetDateTime, + @Param("toAt") toAt: java.time.OffsetDateTime + ): List + + @Query(""" + select rs.room.id as roomId, + rs.fromAt as fromAt, + b.expectedCheckoutAt as expectedCheckoutAt + from RoomStay rs + join rs.booking b + where rs.property.id = :propertyId + and rs.isVoided = false + and rs.toAt is null + and rs.fromAt < :toAt + """) + fun findActiveForecastRows( + @Param("propertyId") propertyId: UUID, + @Param("toAt") toAt: java.time.OffsetDateTime + ): List + @Query(""" select rs from RoomStay rs @@ -163,3 +194,9 @@ interface BookingRoomNumberRow { val bookingId: UUID val roomNumber: Int } + +interface ActiveRoomStayForecastRow { + val roomId: UUID + val fromAt: java.time.OffsetDateTime + val expectedCheckoutAt: java.time.OffsetDateTime? +}