135 lines
5.4 KiB
Kotlin
135 lines
5.4 KiB
Kotlin
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<ActiveRoomStayResponse> {
|
|
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<Role>): 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 }
|
|
}
|
|
}
|