package com.android.trisolarisserver.controller.booking import com.android.trisolarisserver.component.auth.PropertyAccess import com.android.trisolarisserver.controller.common.parseOffset import com.android.trisolarisserver.controller.common.requireRole import com.android.trisolarisserver.controller.dto.booking.BookingRoomRequestCreateRequest import com.android.trisolarisserver.controller.dto.booking.BookingRoomRequestResponse import com.android.trisolarisserver.models.booking.BookingRoomRequest import com.android.trisolarisserver.models.booking.BookingRoomRequestStatus import com.android.trisolarisserver.models.booking.BookingStatus import com.android.trisolarisserver.models.property.Role import com.android.trisolarisserver.repo.booking.BookingRepo import com.android.trisolarisserver.repo.booking.BookingRoomRequestRepo import com.android.trisolarisserver.repo.property.AppUserRepo import com.android.trisolarisserver.repo.room.RoomRepo import com.android.trisolarisserver.repo.room.RoomStayRepo import com.android.trisolarisserver.repo.room.RoomTypeRepo import com.android.trisolarisserver.security.MyPrincipal import org.springframework.http.HttpStatus import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.transaction.annotation.Transactional import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController import org.springframework.web.server.ResponseStatusException import java.util.UUID @RestController @RequestMapping("/properties/{propertyId}/bookings/{bookingId}/room-requests") class BookingRoomRequests( private val propertyAccess: PropertyAccess, private val bookingRepo: BookingRepo, private val roomTypeRepo: RoomTypeRepo, private val roomRepo: RoomRepo, private val roomStayRepo: RoomStayRepo, private val appUserRepo: AppUserRepo, private val bookingRoomRequestRepo: BookingRoomRequestRepo ) { @PostMapping @ResponseStatus(HttpStatus.CREATED) @Transactional fun create( @PathVariable propertyId: UUID, @PathVariable bookingId: UUID, @AuthenticationPrincipal principal: MyPrincipal?, @RequestBody request: BookingRoomRequestCreateRequest ): BookingRoomRequestResponse { val actor = requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER, Role.STAFF) val booking = bookingRepo.findById(bookingId).orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found") } if (booking.property.id != propertyId) { throw ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found for property") } if (booking.status == BookingStatus.CANCELLED || booking.status == BookingStatus.NO_SHOW || booking.status == BookingStatus.CHECKED_OUT) { throw ResponseStatusException(HttpStatus.CONFLICT, "Booking closed") } if (request.quantity <= 0) { throw ResponseStatusException(HttpStatus.BAD_REQUEST, "quantity must be > 0") } val fromAt = parseOffset(request.fromAt) ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "fromAt required") val toAt = parseOffset(request.toAt) ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "toAt required") if (!toAt.isAfter(fromAt)) { throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid date range") } val roomType = roomTypeRepo.findByPropertyIdAndCodeIgnoreCase(propertyId, request.roomTypeCode) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room type not found") val capacity = roomRepo.countActiveSellableByType(propertyId, roomType.id!!) val occupied = roomStayRepo.countOccupiedByTypeInRange(propertyId, roomType.id!!, fromAt, toAt) val requested = bookingRoomRequestRepo.sumRemainingByTypeAndRange(propertyId, roomType.id!!, fromAt, toAt) val free = capacity - occupied - requested if (request.quantity.toLong() > free) { throw ResponseStatusException(HttpStatus.CONFLICT, "Insufficient room type availability") } val appUser = appUserRepo.findById(actor.userId).orElse(null) val saved = bookingRoomRequestRepo.save( BookingRoomRequest( property = booking.property, booking = booking, roomType = roomType, quantity = request.quantity, fromAt = fromAt, toAt = toAt, status = BookingRoomRequestStatus.ACTIVE, createdBy = appUser ) ) return saved.toResponse() } @GetMapping fun list( @PathVariable propertyId: UUID, @PathVariable bookingId: UUID, @AuthenticationPrincipal principal: MyPrincipal? ): List { requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER, Role.STAFF, Role.HOUSEKEEPING, Role.FINANCE) val booking = bookingRepo.findById(bookingId).orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found") } if (booking.property.id != propertyId) { throw ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found for property") } return bookingRoomRequestRepo.findByBookingIdOrderByCreatedAtAsc(bookingId).map { it.toResponse() } } @DeleteMapping("/{requestId}") @ResponseStatus(HttpStatus.NO_CONTENT) @Transactional fun cancel( @PathVariable propertyId: UUID, @PathVariable bookingId: UUID, @PathVariable requestId: UUID, @AuthenticationPrincipal principal: MyPrincipal? ) { requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER, Role.STAFF) val booking = bookingRepo.findById(bookingId).orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found") } if (booking.property.id != propertyId) { throw ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found for property") } val request = bookingRoomRequestRepo.findById(requestId).orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Room request not found") } if (request.booking.id != bookingId || request.property.id != propertyId) { throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room request not found for booking") } if (request.status == BookingRoomRequestStatus.FULFILLED) { throw ResponseStatusException(HttpStatus.CONFLICT, "Cannot cancel fulfilled room request") } request.status = BookingRoomRequestStatus.CANCELLED bookingRoomRequestRepo.save(request) } private fun BookingRoomRequest.toResponse(): BookingRoomRequestResponse { val remaining = (quantity - fulfilledQuantity).coerceAtLeast(0) return BookingRoomRequestResponse( id = id!!, bookingId = booking.id!!, roomTypeCode = roomType.code, quantity = quantity, fulfilledQuantity = fulfilledQuantity, remainingQuantity = remaining, fromAt = fromAt.toString(), toAt = toAt.toString(), status = status.name ) } }