Files
TrisolarisServer/src/main/kotlin/com/android/trisolarisserver/controller/RoomStays.kt
androidlover5842 ff01f0ab18
All checks were successful
build-and-deploy / build-deploy (push) Successful in 2m35s
Allow MANUAL and RATE_PLAN rate sources
2026-01-29 12:01:33 +05:30

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 }
}
}