package com.android.trisolarisserver.controller import com.android.trisolarisserver.component.PropertyAccess import com.android.trisolarisserver.controller.dto.ActiveRoomStayResponse import com.android.trisolarisserver.controller.dto.RoomStayRateChangeRequest import com.android.trisolarisserver.controller.dto.RoomStayRateChangeResponse import com.android.trisolarisserver.models.property.Role import com.android.trisolarisserver.models.room.RateSource import com.android.trisolarisserver.models.room.RoomStay import com.android.trisolarisserver.repo.PropertyUserRepo import com.android.trisolarisserver.repo.RoomStayRepo import com.android.trisolarisserver.security.MyPrincipal import org.springframework.http.HttpStatus import org.springframework.security.core.annotation.AuthenticationPrincipal 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.RestController import org.springframework.web.server.ResponseStatusException import java.time.OffsetDateTime import java.util.UUID @RestController class RoomStays( private val propertyAccess: PropertyAccess, private val propertyUserRepo: PropertyUserRepo, private val roomStayRepo: RoomStayRepo ) { @GetMapping("/properties/{propertyId}/room-stays/active") fun listActiveRoomStays( @PathVariable propertyId: UUID, @AuthenticationPrincipal principal: MyPrincipal? ): List { if (principal == null) { throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal") } propertyAccess.requireMember(propertyId, principal.userId) val roles = propertyUserRepo.findRolesByPropertyAndUser(propertyId, principal.userId) if (isAgentOnly(roles)) { throw ResponseStatusException(HttpStatus.FORBIDDEN, "Agents cannot view active stays") } return roomStayRepo.findActiveByPropertyIdWithDetails(propertyId).map { stay -> val booking = stay.booking val guest = booking.primaryGuest val room = stay.room val roomType = room.roomType ActiveRoomStayResponse( roomStayId = stay.id!!, bookingId = booking.id!!, guestId = guest?.id, guestName = guest?.name, guestPhone = guest?.phoneE164, roomId = room.id!!, roomNumber = room.roomNumber, roomTypeName = roomType.name, fromAt = stay.fromAt.toString(), checkinAt = booking.checkinAt?.toString(), expectedCheckoutAt = booking.expectedCheckoutAt?.toString() ) } } @PostMapping("/properties/{propertyId}/room-stays/{roomStayId}/change-rate") fun changeRate( @PathVariable propertyId: UUID, @PathVariable roomStayId: UUID, @AuthenticationPrincipal principal: MyPrincipal?, @RequestBody request: RoomStayRateChangeRequest ): RoomStayRateChangeResponse { requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER) val stay = requireRoomStayForProperty(roomStayRepo, propertyId, roomStayId) val effectiveAt = parseOffset(request.effectiveAt) ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "effectiveAt required") if (!effectiveAt.isAfter(stay.fromAt)) { throw ResponseStatusException(HttpStatus.BAD_REQUEST, "effectiveAt must be after fromAt") } if (stay.toAt != null && !effectiveAt.isBefore(stay.toAt)) { throw ResponseStatusException(HttpStatus.CONFLICT, "effectiveAt outside stay range") } val newStay = splitStay(stay, effectiveAt, request) roomStayRepo.save(stay) roomStayRepo.save(newStay) return RoomStayRateChangeResponse( oldRoomStayId = stay.id!!, newRoomStayId = newStay.id!!, effectiveAt = effectiveAt.toString() ) } private fun splitStay(stay: RoomStay, effectiveAt: OffsetDateTime, request: RoomStayRateChangeRequest): RoomStay { val oldToAt = stay.toAt stay.toAt = effectiveAt return RoomStay( property = stay.property, booking = stay.booking, room = stay.room, fromAt = effectiveAt, toAt = oldToAt, rateSource = parseRateSource(request.rateSource), nightlyRate = request.nightlyRate, ratePlanCode = request.ratePlanCode, currency = request.currency ?: stay.property.currency, createdBy = stay.createdBy ) } private fun parseRateSource(value: String): RateSource { return try { RateSource.valueOf(value.trim().uppercase()) } catch (_: IllegalArgumentException) { throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown rate source") } } private fun isAgentOnly(roles: Set): Boolean { if (!roles.contains(Role.AGENT)) return false val privileged = setOf( Role.ADMIN, Role.MANAGER, Role.STAFF, Role.HOUSEKEEPING, Role.FINANCE, Role.SUPERVISOR, Role.GUIDE ) return roles.none { it in privileged } } }