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.component.RoomBoardEvents
|
||||||
import com.android.trisolarisserver.controller.dto.BookingCancelRequest
|
import com.android.trisolarisserver.controller.dto.BookingCancelRequest
|
||||||
import com.android.trisolarisserver.controller.dto.BookingCheckInRequest
|
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.BookingCheckOutRequest
|
||||||
import com.android.trisolarisserver.controller.dto.BookingCreateRequest
|
import com.android.trisolarisserver.controller.dto.BookingCreateRequest
|
||||||
import com.android.trisolarisserver.controller.dto.BookingCreateResponse
|
import com.android.trisolarisserver.controller.dto.BookingCreateResponse
|
||||||
@@ -267,6 +268,93 @@ class BookingFlow(
|
|||||||
roomBoardEvents.emit(propertyId)
|
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")
|
@PostMapping("/{bookingId}/check-out")
|
||||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||||
@Transactional
|
@Transactional
|
||||||
|
|||||||
@@ -13,6 +13,22 @@ data class BookingCheckInRequest(
|
|||||||
val notes: String? = null
|
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(
|
data class BookingCreateRequest(
|
||||||
val source: String? = null,
|
val source: String? = null,
|
||||||
val expectedCheckInAt: String,
|
val expectedCheckInAt: String,
|
||||||
|
|||||||
Reference in New Issue
Block a user