Files
TrisolarisServer/src/main/kotlin/com/android/trisolarisserver/controller/RatePlans.kt
androidlover5842 97f09f6f75
All checks were successful
build-and-deploy / build-deploy (push) Successful in 36s
Default timezone to India; switch rate calendar to range API
2026-01-29 05:47:47 +05:30

241 lines
10 KiB
Kotlin

package com.android.trisolarisserver.controller
import com.android.trisolarisserver.component.PropertyAccess
import com.android.trisolarisserver.controller.dto.RateCalendarResponse
import com.android.trisolarisserver.controller.dto.RateCalendarRangeUpsertRequest
import com.android.trisolarisserver.controller.dto.RatePlanCreateRequest
import com.android.trisolarisserver.controller.dto.RatePlanResponse
import com.android.trisolarisserver.controller.dto.RatePlanUpdateRequest
import com.android.trisolarisserver.repo.PropertyRepo
import com.android.trisolarisserver.repo.RateCalendarRepo
import com.android.trisolarisserver.repo.RatePlanRepo
import com.android.trisolarisserver.repo.RoomTypeRepo
import com.android.trisolarisserver.models.property.Role
import com.android.trisolarisserver.models.room.RateCalendar
import com.android.trisolarisserver.models.room.RatePlan
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.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ResponseStatusException
import java.time.LocalDate
import java.time.OffsetDateTime
import java.util.UUID
@RestController
@RequestMapping("/properties/{propertyId}/rate-plans")
class RatePlans(
private val propertyAccess: PropertyAccess,
private val propertyRepo: PropertyRepo,
private val roomTypeRepo: RoomTypeRepo,
private val ratePlanRepo: RatePlanRepo,
private val rateCalendarRepo: RateCalendarRepo
) {
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun create(
@PathVariable propertyId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?,
@RequestBody request: RatePlanCreateRequest
): RatePlanResponse {
requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER)
val property = propertyRepo.findById(propertyId).orElseThrow {
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
}
val roomType = roomTypeRepo.findByPropertyIdAndCodeIgnoreCase(propertyId, request.roomTypeCode)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room type not found")
if (ratePlanRepo.existsByPropertyIdAndRoomTypeIdAndCode(propertyId, roomType.id!!, request.code.trim())) {
throw ResponseStatusException(HttpStatus.CONFLICT, "Rate plan code already exists for room type")
}
val plan = RatePlan(
property = property,
roomType = roomType,
code = request.code.trim(),
name = request.name.trim(),
baseRate = request.baseRate,
currency = request.currency ?: property.currency,
updatedAt = OffsetDateTime.now()
)
return ratePlanRepo.save(plan).toResponse()
}
@GetMapping
fun list(
@PathVariable propertyId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?,
@RequestParam(required = false) roomTypeCode: String?
): List<RatePlanResponse> {
requireMember(propertyAccess, propertyId, principal)
val plans = if (roomTypeCode.isNullOrBlank()) {
ratePlanRepo.findByPropertyIdOrderByCode(propertyId)
} else {
ratePlanRepo.findByPropertyIdOrderByCode(propertyId)
.filter { it.roomType.code.equals(roomTypeCode, ignoreCase = true) }
}
return plans.map { it.toResponse() }
}
@PutMapping("/{ratePlanId}")
fun update(
@PathVariable propertyId: UUID,
@PathVariable ratePlanId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?,
@RequestBody request: RatePlanUpdateRequest
): RatePlanResponse {
requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER)
val plan = ratePlanRepo.findByIdAndPropertyId(ratePlanId, propertyId)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Rate plan not found")
plan.name = request.name.trim()
plan.baseRate = request.baseRate
plan.currency = request.currency ?: plan.currency
plan.updatedAt = OffsetDateTime.now()
return ratePlanRepo.save(plan).toResponse()
}
@DeleteMapping("/{ratePlanId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Transactional
fun delete(
@PathVariable propertyId: UUID,
@PathVariable ratePlanId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?
) {
requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER)
val plan = ratePlanRepo.findByIdAndPropertyId(ratePlanId, propertyId)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Rate plan not found")
rateCalendarRepo.findByRatePlanId(plan.id!!).forEach { rateCalendarRepo.delete(it) }
ratePlanRepo.delete(plan)
}
@PostMapping("/{ratePlanId}/calendar")
@ResponseStatus(HttpStatus.CREATED)
@Transactional
fun upsertCalendar(
@PathVariable propertyId: UUID,
@PathVariable ratePlanId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?,
@RequestBody request: RateCalendarRangeUpsertRequest
): List<RateCalendarResponse> {
requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER)
val plan = ratePlanRepo.findByIdAndPropertyId(ratePlanId, propertyId)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Rate plan not found")
val fromDate = parseDate(request.from)
val toDate = parseDate(request.to)
if (toDate.isBefore(fromDate)) {
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "to must be on/after from")
}
val existing = rateCalendarRepo.findByRatePlanIdAndRateDateBetweenOrderByRateDateAsc(
plan.id!!,
fromDate,
toDate
).associateBy { it.rateDate }
val updates = mutableListOf<RateCalendar>()
for (date in datesBetween(fromDate, toDate)) {
val row = existing[date]
if (row != null) {
row.rate = request.rate
updates.add(row)
} else {
updates.add(
RateCalendar(
property = plan.property,
ratePlan = plan,
rateDate = date,
rate = request.rate
)
)
}
}
return rateCalendarRepo.saveAll(updates).map { it.toResponse() }
}
@GetMapping("/{ratePlanId}/calendar")
fun listCalendar(
@PathVariable propertyId: UUID,
@PathVariable ratePlanId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?,
@RequestParam from: String,
@RequestParam to: String
): List<RateCalendarResponse> {
requireMember(propertyAccess, propertyId, principal)
val plan = ratePlanRepo.findByIdAndPropertyId(ratePlanId, propertyId)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Rate plan not found")
val fromDate = parseDate(from)
val toDate = parseDate(to)
val items = rateCalendarRepo.findByRatePlanIdAndRateDateBetweenOrderByRateDateAsc(plan.id!!, fromDate, toDate)
return items.map { it.toResponse() }
}
@DeleteMapping("/{ratePlanId}/calendar/{rateDate}")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun deleteCalendar(
@PathVariable propertyId: UUID,
@PathVariable ratePlanId: UUID,
@PathVariable rateDate: String,
@AuthenticationPrincipal principal: MyPrincipal?
) {
requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER)
val plan = ratePlanRepo.findByIdAndPropertyId(ratePlanId, propertyId)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Rate plan not found")
val date = parseDate(rateDate)
val existing = rateCalendarRepo.findByRatePlanIdAndRateDate(plan.id!!, date)
?: return
rateCalendarRepo.delete(existing)
}
private fun parseDate(value: String): LocalDate {
return try {
LocalDate.parse(value.trim())
} catch (_: Exception) {
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid date")
}
}
private fun datesBetween(from: LocalDate, to: LocalDate): Sequence<LocalDate> {
return generateSequence(from) { current ->
val next = current.plusDays(1)
if (next.isAfter(to)) null else next
}
}
}
private fun RatePlan.toResponse(): RatePlanResponse {
val id = id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Rate plan id missing")
val propertyId = property.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Property id missing")
val roomTypeId = roomType.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Room type id missing")
return RatePlanResponse(
id = id,
propertyId = propertyId,
roomTypeId = roomTypeId,
roomTypeCode = roomType.code,
code = code,
name = name,
baseRate = baseRate,
currency = currency
)
}
private fun RateCalendar.toResponse(): RateCalendarResponse {
val id = id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Rate calendar id missing")
val planId = ratePlan.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Rate plan id missing")
return RateCalendarResponse(
id = id,
ratePlanId = planId,
rateDate = rateDate,
rate = rate
)
}