241 lines
10 KiB
Kotlin
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
|
|
)
|
|
}
|