diff --git a/docs/API_REFERENCE.txt b/docs/API_REFERENCE.txt index dcb22d1..6b726f0 100644 --- a/docs/API_REFERENCE.txt +++ b/docs/API_REFERENCE.txt @@ -1822,8 +1822,11 @@ BOOKING APIS What it does: - Updates planned dates on booking (expectedCheckInAt, expectedCheckOutAt), not actual checkout. + - Updates booking-level expected dates only; does not auto-change room_stay checkout timestamps. - For OPEN bookings: can update both expected check-in and expected check-out. + - For OPEN bookings with linked room stays: each stay start/end must remain inside expected booking window. - For CHECKED_IN: can update only expectedCheckOutAt (check-in change is blocked). + - For CHECKED_IN bookings with linked room stays: stay start must be >= booking check-in and < expected check-out; stay end cannot be after expected check-out. - For CHECKED_OUT / CANCELLED / NO_SHOW: returns conflict (Booking closed). - Validates range: if both are present, checkout must be after checkin. - Returns 204 No Content. @@ -1853,6 +1856,7 @@ BOOKING APIS - 409 Conflict - "Cannot change expected check-in after check-in" - "Booking closed" for CHECKED_OUT, CANCELLED, NO_SHOW + - Room stay bounds outside allowed expected window for OPEN/CHECKED_IN booking - Update booking billing policy API is this one: diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt b/src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt index a468de8..e2b349e 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt @@ -578,12 +578,65 @@ class BookingFlow( if (expectedIn != null && expectedOut != null && !expectedOut.isAfter(expectedIn)) { throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid date range") } + validateRoomStayBoundsForExpectedDatesUpdate(booking, expectedIn, expectedOut) booking.updatedAt = OffsetDateTime.now() bookingRepo.save(booking) bookingEvents.emit(propertyId, bookingId) } + private fun validateRoomStayBoundsForExpectedDatesUpdate( + booking: com.android.trisolarisserver.models.booking.Booking, + expectedIn: OffsetDateTime?, + expectedOut: OffsetDateTime? + ) { + if (expectedIn == null || expectedOut == null) return + val bookingId = booking.id ?: return + val stays = roomStayRepo.findByBookingId(bookingId) + when (booking.status) { + BookingStatus.OPEN -> { + stays.forEach { stay -> + if (stay.fromAt.isBefore(expectedIn) || stay.fromAt.isAfter(expectedOut)) { + throw ResponseStatusException( + HttpStatus.CONFLICT, + "Room stay start must be within expected check-in/check-out window for OPEN booking" + ) + } + val toAt = stay.toAt + if (toAt != null && (toAt.isBefore(expectedIn) || toAt.isAfter(expectedOut))) { + throw ResponseStatusException( + HttpStatus.CONFLICT, + "Room stay end must be within expected check-in/check-out window for OPEN booking" + ) + } + } + } + BookingStatus.CHECKED_IN -> { + val checkInAt = booking.checkinAt ?: expectedIn + stays.forEach { stay -> + if (stay.fromAt.isBefore(checkInAt) || !stay.fromAt.isBefore(expectedOut)) { + throw ResponseStatusException( + HttpStatus.CONFLICT, + "Room stay start must be >= check-in and < expected check-out for CHECKED_IN booking" + ) + } + val toAt = stay.toAt + if (toAt != null && toAt.isAfter(expectedOut)) { + throw ResponseStatusException( + HttpStatus.CONFLICT, + "Room stay end cannot be after expected check-out for CHECKED_IN booking" + ) + } + } + } + BookingStatus.CHECKED_OUT, + BookingStatus.CANCELLED, + BookingStatus.NO_SHOW -> { + // Already blocked by status guard in updateExpectedDates. + } + } + } + @PostMapping("/{bookingId}/billing-policy") @ResponseStatus(HttpStatus.NO_CONTENT) @Transactional