Add bulk check-in with per-room rates
All checks were successful
build-and-deploy / build-deploy (push) Successful in 1m35s
All checks were successful
build-and-deploy / build-deploy (push) Successful in 1m35s
This commit is contained in:
@@ -4,6 +4,7 @@ import com.android.trisolarisserver.component.PropertyAccess
|
||||
import com.android.trisolarisserver.component.RoomBoardEvents
|
||||
import com.android.trisolarisserver.controller.dto.BookingCancelRequest
|
||||
import com.android.trisolarisserver.controller.dto.BookingCheckInRequest
|
||||
import com.android.trisolarisserver.controller.dto.BookingBulkCheckInRequest
|
||||
import com.android.trisolarisserver.controller.dto.BookingCheckOutRequest
|
||||
import com.android.trisolarisserver.controller.dto.BookingCreateRequest
|
||||
import com.android.trisolarisserver.controller.dto.BookingCreateResponse
|
||||
@@ -267,6 +268,93 @@ class BookingFlow(
|
||||
roomBoardEvents.emit(propertyId)
|
||||
}
|
||||
|
||||
@PostMapping("/{bookingId}/check-in/bulk")
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
@Transactional
|
||||
fun bulkCheckIn(
|
||||
@PathVariable propertyId: UUID,
|
||||
@PathVariable bookingId: UUID,
|
||||
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||
@RequestBody request: BookingBulkCheckInRequest
|
||||
) {
|
||||
val actor = requireActor(propertyId, principal)
|
||||
if (request.stays.isEmpty()) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "stays required")
|
||||
}
|
||||
|
||||
val booking = requireBooking(propertyId, bookingId)
|
||||
if (booking.status != BookingStatus.OPEN) {
|
||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Booking not open")
|
||||
}
|
||||
|
||||
val roomIds = request.stays.map { it.roomId }
|
||||
if (roomIds.distinct().size != roomIds.size) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Duplicate roomId in stays")
|
||||
}
|
||||
|
||||
val rooms = request.stays.associate { stay ->
|
||||
val room = roomRepo.findByIdAndPropertyId(stay.roomId, propertyId)
|
||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room not found")
|
||||
if (!room.active || room.maintenance) {
|
||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Room not available")
|
||||
}
|
||||
stay.roomId to room
|
||||
}
|
||||
|
||||
val occupied = roomStayRepo.findActiveRoomIds(propertyId, roomIds)
|
||||
if (occupied.isNotEmpty()) {
|
||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Room already occupied")
|
||||
}
|
||||
|
||||
val now = OffsetDateTime.now()
|
||||
val checkInTimes = mutableListOf<OffsetDateTime>()
|
||||
val checkOutTimes = mutableListOf<OffsetDateTime>()
|
||||
request.stays.forEach { stay ->
|
||||
val checkInAt = parseOffset(stay.checkInAt) ?: now
|
||||
checkInTimes.add(checkInAt)
|
||||
val checkOutAt = parseOffset(stay.checkOutAt)
|
||||
if (checkOutAt != null) {
|
||||
if (!checkOutAt.isAfter(checkInAt)) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid date range for stay")
|
||||
}
|
||||
checkOutTimes.add(checkOutAt)
|
||||
}
|
||||
val room = rooms.getValue(stay.roomId)
|
||||
val newStay = RoomStay(
|
||||
property = booking.property,
|
||||
booking = booking,
|
||||
room = room,
|
||||
fromAt = checkInAt,
|
||||
toAt = null,
|
||||
rateSource = parseRateSource(stay.rateSource),
|
||||
nightlyRate = stay.nightlyRate,
|
||||
ratePlanCode = stay.ratePlanCode,
|
||||
currency = stay.currency ?: booking.property.currency,
|
||||
createdBy = actor
|
||||
)
|
||||
roomStayRepo.save(newStay)
|
||||
}
|
||||
|
||||
val bookingCheckInAt = checkInTimes.minOrNull() ?: now
|
||||
val bookingExpectedCheckout = checkOutTimes.maxOrNull()
|
||||
booking.status = BookingStatus.CHECKED_IN
|
||||
booking.checkinAt = bookingCheckInAt
|
||||
if (bookingExpectedCheckout != null) {
|
||||
booking.expectedCheckoutAt = bookingExpectedCheckout
|
||||
}
|
||||
booking.transportMode = request.transportMode?.let {
|
||||
val mode = parseTransportMode(it)
|
||||
if (!isTransportModeAllowed(booking.property, mode)) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Transport mode disabled")
|
||||
}
|
||||
mode
|
||||
}
|
||||
if (request.notes != null) booking.notes = request.notes
|
||||
booking.updatedAt = now
|
||||
bookingRepo.save(booking)
|
||||
roomBoardEvents.emit(propertyId)
|
||||
}
|
||||
|
||||
@PostMapping("/{bookingId}/check-out")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@Transactional
|
||||
|
||||
@@ -13,6 +13,22 @@ data class BookingCheckInRequest(
|
||||
val notes: String? = null
|
||||
)
|
||||
|
||||
data class BookingCheckInStayRequest(
|
||||
val roomId: UUID,
|
||||
val checkInAt: String? = null,
|
||||
val checkOutAt: String? = null,
|
||||
val nightlyRate: Long? = null,
|
||||
val rateSource: String? = null,
|
||||
val ratePlanCode: String? = null,
|
||||
val currency: String? = null
|
||||
)
|
||||
|
||||
data class BookingBulkCheckInRequest(
|
||||
val stays: List<BookingCheckInStayRequest>,
|
||||
val transportMode: String? = null,
|
||||
val notes: String? = null
|
||||
)
|
||||
|
||||
data class BookingCreateRequest(
|
||||
val source: String? = null,
|
||||
val expectedCheckInAt: String,
|
||||
|
||||
Reference in New Issue
Block a user