diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/BookingFlow.kt b/src/main/kotlin/com/android/trisolarisserver/controller/BookingFlow.kt index 36c4845..6ff9b9e 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/BookingFlow.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/BookingFlow.kt @@ -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() + val checkOutTimes = mutableListOf() + 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 diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/dto/BookingDtos.kt b/src/main/kotlin/com/android/trisolarisserver/controller/dto/BookingDtos.kt index 697cbac..499ff26 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/dto/BookingDtos.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/dto/BookingDtos.kt @@ -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, + val transportMode: String? = null, + val notes: String? = null +) + data class BookingCreateRequest( val source: String? = null, val expectedCheckInAt: String,