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 { 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 { 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() 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 { 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 { 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 ) }