Add booking billable nights preview API and detail field
All checks were successful
build-and-deploy / build-deploy (push) Successful in 35s

This commit is contained in:
androidlover5842
2026-02-04 13:52:39 +05:30
parent ff911661a4
commit 0694cf0b8a
4 changed files with 164 additions and 0 deletions

View File

@@ -10,6 +10,8 @@ import com.android.trisolarisserver.component.booking.BookingEvents
import com.android.trisolarisserver.component.auth.PropertyAccess
import com.android.trisolarisserver.component.room.RoomBoardEvents
import com.android.trisolarisserver.controller.dto.booking.BookingCancelRequest
import com.android.trisolarisserver.controller.dto.booking.BookingBillableNightsRequest
import com.android.trisolarisserver.controller.dto.booking.BookingBillableNightsResponse
import com.android.trisolarisserver.controller.dto.booking.BookingBulkCheckInRequest
import com.android.trisolarisserver.controller.dto.booking.BookingCheckOutRequest
import com.android.trisolarisserver.controller.dto.booking.BookingCreateRequest
@@ -298,6 +300,44 @@ class BookingFlow(
return bookingSnapshotBuilder.build(propertyId, bookingId)
}
@PostMapping("/{bookingId}/billable-nights")
fun previewBillableNights(
@PathVariable propertyId: UUID,
@PathVariable bookingId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?,
@RequestBody(required = false) request: BookingBillableNightsRequest?
): BookingBillableNightsResponse {
requireRole(
propertyAccess,
propertyId,
principal,
Role.ADMIN,
Role.MANAGER,
Role.STAFF,
Role.HOUSEKEEPING,
Role.FINANCE
)
val booking = requireBooking(propertyId, bookingId)
val resolvedRequest = request ?: BookingBillableNightsRequest()
val (startAt, endAt) = resolveBillableNightsWindow(booking, resolvedRequest)
if (!endAt.isAfter(startAt)) {
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid date range")
}
val nights = billableNights(
startAt = startAt,
endAt = endAt,
timezone = booking.property.timezone,
billingMode = booking.billingMode,
billingCheckinTime = booking.billingCheckinTime,
billingCheckoutTime = booking.billingCheckoutTime
)
return BookingBillableNightsResponse(
bookingId = bookingId,
status = booking.status.name,
billableNights = nights
)
}
@GetMapping("/{bookingId}/stream")
fun streamBooking(
@PathVariable propertyId: UUID,
@@ -753,6 +793,43 @@ class BookingFlow(
}
}
private fun resolveBillableNightsWindow(
booking: com.android.trisolarisserver.models.booking.Booking,
request: BookingBillableNightsRequest
): Pair<OffsetDateTime, OffsetDateTime> {
return when (booking.status) {
BookingStatus.OPEN -> {
val expectedCheckIn = parseOffset(request.expectedCheckInAt)
?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "expectedCheckInAt required for OPEN booking")
val expectedCheckOut = parseOffset(request.expectedCheckOutAt)
?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "expectedCheckOutAt required for OPEN booking")
Pair(expectedCheckIn, expectedCheckOut)
}
BookingStatus.CHECKED_IN -> {
if (request.expectedCheckInAt != null) {
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "expectedCheckInAt not allowed for CHECKED_IN booking")
}
val checkInAt = booking.checkinAt ?: booking.expectedCheckinAt
?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "checkInAt missing for booking")
val expectedCheckOut = parseOffset(request.expectedCheckOutAt)
?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "expectedCheckOutAt required for CHECKED_IN booking")
Pair(checkInAt, expectedCheckOut)
}
BookingStatus.CHECKED_OUT,
BookingStatus.CANCELLED,
BookingStatus.NO_SHOW -> {
if (request.expectedCheckInAt != null || request.expectedCheckOutAt != null) {
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "expected dates not accepted for closed booking")
}
val startAt = booking.checkinAt ?: booking.expectedCheckinAt
?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "checkInAt missing for booking")
val endAt = booking.checkoutAt ?: booking.expectedCheckoutAt
?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "checkOutAt missing for booking")
Pair(startAt, endAt)
}
}
}
private fun parseTransportMode(value: String): TransportMode {
return try {
TransportMode.valueOf(value)