Add rate plans, room stay rates, and payments ledger
All checks were successful
build-and-deploy / build-deploy (push) Successful in 34s
All checks were successful
build-and-deploy / build-deploy (push) Successful in 34s
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
package com.android.trisolarisserver.controller
|
||||
|
||||
import com.android.trisolarisserver.component.PropertyAccess
|
||||
import com.android.trisolarisserver.controller.dto.RateCalendarResponse
|
||||
import com.android.trisolarisserver.controller.dto.RateCalendarUpsertRequest
|
||||
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)
|
||||
if (ratePlanRepo.existsByPropertyIdAndCode(propertyId, request.code.trim())) {
|
||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Rate plan code already exists")
|
||||
}
|
||||
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")
|
||||
|
||||
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 requests: List<RateCalendarUpsertRequest>
|
||||
): 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")
|
||||
if (requests.isEmpty()) return emptyList()
|
||||
|
||||
val updates = requests.map { req ->
|
||||
val date = parseDate(req.rateDate)
|
||||
val existing = rateCalendarRepo.findByRatePlanIdAndRateDate(plan.id!!, date)
|
||||
if (existing != null) {
|
||||
existing.rate = req.rate
|
||||
existing
|
||||
} else {
|
||||
RateCalendar(
|
||||
property = plan.property,
|
||||
ratePlan = plan,
|
||||
rateDate = date,
|
||||
rate = req.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 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
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user