package com.android.trisolarisserver.controller.room import com.android.trisolarisserver.controller.common.parseOffset import com.android.trisolarisserver.controller.common.requireOpenRoomStayForProperty import com.android.trisolarisserver.controller.common.requireRole import com.android.trisolarisserver.component.auth.PropertyAccess import com.android.trisolarisserver.component.room.RoomBoardEvents import com.android.trisolarisserver.controller.dto.booking.RoomChangeRequest import com.android.trisolarisserver.controller.dto.booking.RoomChangeResponse import com.android.trisolarisserver.models.room.RoomStay import com.android.trisolarisserver.models.room.RoomStayChange import com.android.trisolarisserver.models.property.Role import com.android.trisolarisserver.repo.property.AppUserRepo import com.android.trisolarisserver.repo.room.RoomRepo import com.android.trisolarisserver.repo.room.RoomStayChangeRepo import com.android.trisolarisserver.repo.room.RoomStayRepo 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.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.time.OffsetDateTime import java.util.UUID @RestController @RequestMapping("/properties/{propertyId}/room-stays") class RoomStayFlow( private val propertyAccess: PropertyAccess, private val roomStayRepo: RoomStayRepo, private val roomStayChangeRepo: RoomStayChangeRepo, private val roomRepo: RoomRepo, private val appUserRepo: AppUserRepo, private val roomBoardEvents: RoomBoardEvents ) { @PostMapping("/{roomStayId}/change-room") @ResponseStatus(HttpStatus.CREATED) @Transactional fun changeRoom( @PathVariable propertyId: UUID, @PathVariable roomStayId: UUID, @AuthenticationPrincipal principal: MyPrincipal?, @RequestBody request: RoomChangeRequest ): RoomChangeResponse { val actor = requireActor(propertyId, principal) val stay = requireOpenRoomStayForProperty( roomStayRepo, propertyId, roomStayId, "Room stay already closed" ) if (request.idempotencyKey.isBlank()) { throw ResponseStatusException(HttpStatus.BAD_REQUEST, "idempotencyKey required") } if (request.newRoomId == stay.room.id) { throw ResponseStatusException(HttpStatus.BAD_REQUEST, "New room is same as current") } val existing = roomStayChangeRepo.findByRoomStayIdAndIdempotencyKey(roomStayId, request.idempotencyKey) if (existing != null) { return toResponse(existing) } val newRoom = roomRepo.findByIdAndPropertyId(request.newRoomId, propertyId) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room not found") if (!newRoom.active || newRoom.maintenance) { throw ResponseStatusException(HttpStatus.CONFLICT, "Room not available") } val occupied = roomStayRepo.findActiveRoomIds(propertyId, listOf(request.newRoomId)) if (occupied.isNotEmpty()) { throw ResponseStatusException(HttpStatus.CONFLICT, "Room already occupied") } val movedAt = parseOffset(request.movedAt) ?: OffsetDateTime.now() stay.toAt = movedAt roomStayRepo.save(stay) val newStay = RoomStay( property = stay.property, booking = stay.booking, room = newRoom, fromAt = movedAt, toAt = null, rateSource = stay.rateSource, nightlyRate = stay.nightlyRate, ratePlanCode = stay.ratePlanCode, currency = stay.currency, createdBy = actor ) val savedNewStay = roomStayRepo.save(newStay) val change = RoomStayChange( property = stay.property, roomStay = stay, newRoomStay = savedNewStay, idempotencyKey = request.idempotencyKey ) val savedChange = roomStayChangeRepo.save(change) roomBoardEvents.emit(propertyId) return toResponse(savedChange) } private fun toResponse(change: RoomStayChange): RoomChangeResponse { return RoomChangeResponse( oldRoomStayId = change.roomStay.id!!, newRoomStayId = change.newRoomStay.id!!, oldRoomId = change.roomStay.room.id!!, newRoomId = change.newRoomStay.room.id!!, movedAt = change.newRoomStay.fromAt.toString() ) } private fun requireActor(propertyId: UUID, principal: MyPrincipal?): com.android.trisolarisserver.models.property.AppUser { val resolved = requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER, Role.STAFF) return appUserRepo.findById(resolved.userId).orElseThrow { ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found") } } }