Add property billing policy times and policy-based night calculation
All checks were successful
build-and-deploy / build-deploy (push) Successful in 35s
All checks were successful
build-and-deploy / build-deploy (push) Successful in 35s
This commit is contained in:
@@ -43,7 +43,9 @@ class BookingBalances(
|
|||||||
}
|
}
|
||||||
val expected = computeExpectedPay(
|
val expected = computeExpectedPay(
|
||||||
roomStayRepo.findByBookingId(bookingId),
|
roomStayRepo.findByBookingId(bookingId),
|
||||||
booking.property.timezone
|
booking.property.timezone,
|
||||||
|
booking.property.billingCheckinTime,
|
||||||
|
booking.property.billingCheckoutTime
|
||||||
)
|
)
|
||||||
val charges = chargeRepo.sumAmountByBookingId(bookingId)
|
val charges = chargeRepo.sumAmountByBookingId(bookingId)
|
||||||
val collected = paymentRepo.sumAmountByBookingId(bookingId)
|
val collected = paymentRepo.sumAmountByBookingId(bookingId)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
package com.android.trisolarisserver.controller.booking
|
package com.android.trisolarisserver.controller.booking
|
||||||
|
import com.android.trisolarisserver.controller.common.billableNights
|
||||||
import com.android.trisolarisserver.controller.common.computeExpectedPay
|
import com.android.trisolarisserver.controller.common.computeExpectedPay
|
||||||
import com.android.trisolarisserver.controller.common.nowForProperty
|
import com.android.trisolarisserver.controller.common.nowForProperty
|
||||||
import com.android.trisolarisserver.controller.common.parseOffset
|
import com.android.trisolarisserver.controller.common.parseOffset
|
||||||
@@ -58,6 +59,7 @@ import org.springframework.web.bind.annotation.ResponseStatus
|
|||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
import org.springframework.web.server.ResponseStatusException
|
import org.springframework.web.server.ResponseStatusException
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
import kotlin.math.abs
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -218,7 +220,12 @@ class BookingFlow(
|
|||||||
val expectedPay = if (stays.isEmpty()) {
|
val expectedPay = if (stays.isEmpty()) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
computeExpectedPay(stays, property.timezone)
|
computeExpectedPay(
|
||||||
|
stays,
|
||||||
|
property.timezone,
|
||||||
|
property.billingCheckinTime,
|
||||||
|
property.billingCheckoutTime
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val collected = paymentsByBooking[booking.id] ?: 0L
|
val collected = paymentsByBooking[booking.id] ?: 0L
|
||||||
val extraCharges = chargesByBooking[booking.id] ?: 0L
|
val extraCharges = chargesByBooking[booking.id] ?: 0L
|
||||||
@@ -461,6 +468,15 @@ class BookingFlow(
|
|||||||
if (stays.any { !isCheckoutAmountValid(it) || !isMinimumStayDurationValid(it, checkOutAt) }) {
|
if (stays.any { !isCheckoutAmountValid(it) || !isMinimumStayDurationValid(it, checkOutAt) }) {
|
||||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Room stay amount is outside allowed range")
|
throw ResponseStatusException(HttpStatus.CONFLICT, "Room stay amount is outside allowed range")
|
||||||
}
|
}
|
||||||
|
ensureLedgerToleranceForCheckout(
|
||||||
|
bookingId = bookingId,
|
||||||
|
timezone = booking.property.timezone,
|
||||||
|
billingCheckinTime = booking.property.billingCheckinTime,
|
||||||
|
billingCheckoutTime = booking.property.billingCheckoutTime,
|
||||||
|
stays = stays,
|
||||||
|
checkOutOverrides = stays.associate { it.id!! to checkOutAt },
|
||||||
|
restrictToClosedStaysOnly = false
|
||||||
|
)
|
||||||
val oldStates = stays.associate { it.id!! to it.toAt }
|
val oldStates = stays.associate { it.id!! to it.toAt }
|
||||||
stays.forEach { it.toAt = checkOutAt }
|
stays.forEach { it.toAt = checkOutAt }
|
||||||
roomStayRepo.saveAll(stays)
|
roomStayRepo.saveAll(stays)
|
||||||
@@ -524,6 +540,15 @@ class BookingFlow(
|
|||||||
if (!isMinimumStayDurationValid(stay, checkOutAt)) {
|
if (!isMinimumStayDurationValid(stay, checkOutAt)) {
|
||||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Minimum stay duration is 1 hour")
|
throw ResponseStatusException(HttpStatus.CONFLICT, "Minimum stay duration is 1 hour")
|
||||||
}
|
}
|
||||||
|
ensureLedgerToleranceForCheckout(
|
||||||
|
bookingId = bookingId,
|
||||||
|
timezone = booking.property.timezone,
|
||||||
|
billingCheckinTime = booking.property.billingCheckinTime,
|
||||||
|
billingCheckoutTime = booking.property.billingCheckoutTime,
|
||||||
|
stays = staysForBooking,
|
||||||
|
checkOutOverrides = mapOf(stay.id!! to checkOutAt),
|
||||||
|
restrictToClosedStaysOnly = true
|
||||||
|
)
|
||||||
|
|
||||||
val oldToAt = stay.toAt
|
val oldToAt = stay.toAt
|
||||||
val oldIsVoided = stay.isVoided
|
val oldIsVoided = stay.isVoided
|
||||||
@@ -770,6 +795,51 @@ class BookingFlow(
|
|||||||
return java.time.Duration.between(stay.fromAt, checkOutAt).toMinutes() >= 60
|
return java.time.Duration.between(stay.fromAt, checkOutAt).toMinutes() >= 60
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ensureLedgerToleranceForCheckout(
|
||||||
|
bookingId: UUID,
|
||||||
|
timezone: String?,
|
||||||
|
billingCheckinTime: String?,
|
||||||
|
billingCheckoutTime: String?,
|
||||||
|
stays: List<RoomStay>,
|
||||||
|
checkOutOverrides: Map<UUID, OffsetDateTime>,
|
||||||
|
restrictToClosedStaysOnly: Boolean
|
||||||
|
) {
|
||||||
|
val now = nowForProperty(timezone)
|
||||||
|
val expectedStayAmount = stays
|
||||||
|
.asSequence()
|
||||||
|
.filter { !it.isVoided }
|
||||||
|
.mapNotNull { stay ->
|
||||||
|
val effectiveToAt = checkOutOverrides[stay.id] ?: stay.toAt
|
||||||
|
if (restrictToClosedStaysOnly && effectiveToAt == null) {
|
||||||
|
return@mapNotNull null
|
||||||
|
}
|
||||||
|
val endAt = effectiveToAt ?: now
|
||||||
|
val rate = stay.nightlyRate ?: 0L
|
||||||
|
if (rate <= 0L) {
|
||||||
|
return@mapNotNull 0L
|
||||||
|
}
|
||||||
|
rate * billableNights(
|
||||||
|
stay.fromAt,
|
||||||
|
endAt,
|
||||||
|
timezone,
|
||||||
|
billingCheckinTime,
|
||||||
|
billingCheckoutTime
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.sum()
|
||||||
|
val extraCharges = chargeRepo.sumAmountByBookingId(bookingId)
|
||||||
|
val collected = paymentRepo.sumAmountByBookingId(bookingId)
|
||||||
|
val expectedTotal = expectedStayAmount + extraCharges
|
||||||
|
val tolerance = (expectedTotal * 20L) / 100L
|
||||||
|
val delta = collected - expectedTotal
|
||||||
|
if (abs(delta) > tolerance) {
|
||||||
|
throw ResponseStatusException(
|
||||||
|
HttpStatus.CONFLICT,
|
||||||
|
"Ledger mismatch: collected amount must be within 20% of expected amount before checkout"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun logStayAudit(
|
private fun logStayAudit(
|
||||||
stay: RoomStay,
|
stay: RoomStay,
|
||||||
action: String,
|
action: String,
|
||||||
|
|||||||
@@ -50,8 +50,19 @@ class BookingSnapshotBuilder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val totalNightlyRate = roomsToShow.sumOf { it.nightlyRate ?: 0L }
|
val totalNightlyRate = roomsToShow.sumOf { it.nightlyRate ?: 0L }
|
||||||
val expectedPay = computeExpectedPayTotal(stays, booking.expectedCheckoutAt, booking.property.timezone)
|
val expectedPay = computeExpectedPayTotal(
|
||||||
val accruedPay = computeExpectedPay(stays, booking.property.timezone)
|
stays,
|
||||||
|
booking.expectedCheckoutAt,
|
||||||
|
booking.property.timezone,
|
||||||
|
booking.property.billingCheckinTime,
|
||||||
|
booking.property.billingCheckoutTime
|
||||||
|
)
|
||||||
|
val accruedPay = computeExpectedPay(
|
||||||
|
stays,
|
||||||
|
booking.property.timezone,
|
||||||
|
booking.property.billingCheckinTime,
|
||||||
|
booking.property.billingCheckoutTime
|
||||||
|
)
|
||||||
val extraCharges = chargeRepo.sumAmountByBookingId(bookingId)
|
val extraCharges = chargeRepo.sumAmountByBookingId(bookingId)
|
||||||
val amountCollected = paymentRepo.sumAmountByBookingId(bookingId)
|
val amountCollected = paymentRepo.sumAmountByBookingId(bookingId)
|
||||||
val pending = accruedPay + extraCharges - amountCollected
|
val pending = accruedPay + extraCharges - amountCollected
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import com.android.trisolarisserver.repo.room.RoomStayRepo
|
|||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
import org.springframework.web.server.ResponseStatusException
|
import org.springframework.web.server.ResponseStatusException
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
import java.time.LocalTime
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@@ -103,27 +104,11 @@ internal fun nowForProperty(timezone: String?): OffsetDateTime {
|
|||||||
return OffsetDateTime.now(zone)
|
return OffsetDateTime.now(zone)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun computeExpectedPay(stays: List<RoomStay>, timezone: String?): Long {
|
internal fun computeExpectedPay(
|
||||||
if (stays.isEmpty()) return 0
|
|
||||||
val now = nowForProperty(timezone)
|
|
||||||
var total = 0L
|
|
||||||
stays.forEach { stay ->
|
|
||||||
if (stay.isVoided) return@forEach
|
|
||||||
val rate = stay.nightlyRate ?: 0L
|
|
||||||
if (rate == 0L) return@forEach
|
|
||||||
val start = stay.fromAt.toLocalDate()
|
|
||||||
val endAt = stay.toAt ?: now
|
|
||||||
val end = endAt.toLocalDate()
|
|
||||||
val nights = daysBetweenInclusive(start, end)
|
|
||||||
total += rate * nights
|
|
||||||
}
|
|
||||||
return total
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun computeExpectedPayTotal(
|
|
||||||
stays: List<RoomStay>,
|
stays: List<RoomStay>,
|
||||||
expectedCheckoutAt: OffsetDateTime?,
|
timezone: String?,
|
||||||
timezone: String?
|
billingCheckinTime: String?,
|
||||||
|
billingCheckoutTime: String?
|
||||||
): Long {
|
): Long {
|
||||||
if (stays.isEmpty()) return 0
|
if (stays.isEmpty()) return 0
|
||||||
val now = nowForProperty(timezone)
|
val now = nowForProperty(timezone)
|
||||||
@@ -132,15 +117,87 @@ internal fun computeExpectedPayTotal(
|
|||||||
if (stay.isVoided) return@forEach
|
if (stay.isVoided) return@forEach
|
||||||
val rate = stay.nightlyRate ?: 0L
|
val rate = stay.nightlyRate ?: 0L
|
||||||
if (rate == 0L) return@forEach
|
if (rate == 0L) return@forEach
|
||||||
val start = stay.fromAt.toLocalDate()
|
val endAt = stay.toAt ?: now
|
||||||
val endAt = stay.toAt ?: expectedCheckoutAt ?: now
|
val nights = billableNights(
|
||||||
val end = endAt.toLocalDate()
|
stay.fromAt,
|
||||||
val nights = daysBetweenInclusive(start, end)
|
endAt,
|
||||||
|
timezone,
|
||||||
|
billingCheckinTime,
|
||||||
|
billingCheckoutTime
|
||||||
|
)
|
||||||
total += rate * nights
|
total += rate * nights
|
||||||
}
|
}
|
||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun computeExpectedPayTotal(
|
||||||
|
stays: List<RoomStay>,
|
||||||
|
expectedCheckoutAt: OffsetDateTime?,
|
||||||
|
timezone: String?,
|
||||||
|
billingCheckinTime: String?,
|
||||||
|
billingCheckoutTime: String?
|
||||||
|
): Long {
|
||||||
|
if (stays.isEmpty()) return 0
|
||||||
|
val now = nowForProperty(timezone)
|
||||||
|
var total = 0L
|
||||||
|
stays.forEach { stay ->
|
||||||
|
if (stay.isVoided) return@forEach
|
||||||
|
val rate = stay.nightlyRate ?: 0L
|
||||||
|
if (rate == 0L) return@forEach
|
||||||
|
val endAt = stay.toAt ?: expectedCheckoutAt ?: now
|
||||||
|
val nights = billableNights(
|
||||||
|
stay.fromAt,
|
||||||
|
endAt,
|
||||||
|
timezone,
|
||||||
|
billingCheckinTime,
|
||||||
|
billingCheckoutTime
|
||||||
|
)
|
||||||
|
total += rate * nights
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val DEFAULT_BILLING_CHECKIN_TIME = "12:00"
|
||||||
|
private const val DEFAULT_BILLING_CHECKOUT_TIME = "11:00"
|
||||||
|
|
||||||
|
internal fun billableNights(
|
||||||
|
startAt: OffsetDateTime,
|
||||||
|
endAt: OffsetDateTime,
|
||||||
|
timezone: String?,
|
||||||
|
billingCheckinTime: String?,
|
||||||
|
billingCheckoutTime: String?
|
||||||
|
): Long {
|
||||||
|
if (!endAt.isAfter(startAt)) return 1L
|
||||||
|
val zone = try {
|
||||||
|
if (timezone.isNullOrBlank()) ZoneId.of("Asia/Kolkata") else ZoneId.of(timezone)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
ZoneId.of("Asia/Kolkata")
|
||||||
|
}
|
||||||
|
val checkinPolicy = parseBillingTimeOrDefault(billingCheckinTime, DEFAULT_BILLING_CHECKIN_TIME)
|
||||||
|
val checkoutPolicy = parseBillingTimeOrDefault(billingCheckoutTime, DEFAULT_BILLING_CHECKOUT_TIME)
|
||||||
|
val localStart = startAt.atZoneSameInstant(zone)
|
||||||
|
val localEnd = endAt.atZoneSameInstant(zone)
|
||||||
|
var billStartDate = localStart.toLocalDate()
|
||||||
|
if (localStart.toLocalTime().isBefore(checkinPolicy)) {
|
||||||
|
billStartDate = billStartDate.minusDays(1)
|
||||||
|
}
|
||||||
|
var billEndDate = localEnd.toLocalDate()
|
||||||
|
if (localEnd.toLocalTime().isAfter(checkoutPolicy)) {
|
||||||
|
billEndDate = billEndDate.plusDays(1)
|
||||||
|
}
|
||||||
|
val diff = billEndDate.toEpochDay() - billStartDate.toEpochDay()
|
||||||
|
return if (diff <= 0L) 1L else diff
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseBillingTimeOrDefault(raw: String?, fallback: String): LocalTime {
|
||||||
|
val value = raw?.trim()?.takeIf { it.isNotEmpty() } ?: fallback
|
||||||
|
return try {
|
||||||
|
LocalTime.parse(value)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
LocalTime.parse(fallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal fun daysBetweenInclusive(start: LocalDate, end: LocalDate): Long {
|
internal fun daysBetweenInclusive(start: LocalDate, end: LocalDate): Long {
|
||||||
val diff = end.toEpochDay() - start.toEpochDay()
|
val diff = end.toEpochDay() - start.toEpochDay()
|
||||||
return if (diff <= 0) 1L else diff
|
return if (diff <= 0) 1L else diff
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ data class PropertyCreateRequest(
|
|||||||
val addressText: String? = null,
|
val addressText: String? = null,
|
||||||
val timezone: String? = null,
|
val timezone: String? = null,
|
||||||
val currency: String? = null,
|
val currency: String? = null,
|
||||||
|
val billingCheckinTime: String? = null,
|
||||||
|
val billingCheckoutTime: String? = null,
|
||||||
val active: Boolean? = null,
|
val active: Boolean? = null,
|
||||||
val otaAliases: Set<String>? = null,
|
val otaAliases: Set<String>? = null,
|
||||||
val emailAddresses: Set<String>? = null,
|
val emailAddresses: Set<String>? = null,
|
||||||
@@ -19,6 +21,8 @@ data class PropertyUpdateRequest(
|
|||||||
val addressText: String? = null,
|
val addressText: String? = null,
|
||||||
val timezone: String? = null,
|
val timezone: String? = null,
|
||||||
val currency: String? = null,
|
val currency: String? = null,
|
||||||
|
val billingCheckinTime: String? = null,
|
||||||
|
val billingCheckoutTime: String? = null,
|
||||||
val active: Boolean? = null,
|
val active: Boolean? = null,
|
||||||
val otaAliases: Set<String>? = null,
|
val otaAliases: Set<String>? = null,
|
||||||
val emailAddresses: Set<String>? = null,
|
val emailAddresses: Set<String>? = null,
|
||||||
@@ -32,6 +36,8 @@ data class PropertyResponse(
|
|||||||
val addressText: String?,
|
val addressText: String?,
|
||||||
val timezone: String,
|
val timezone: String,
|
||||||
val currency: String,
|
val currency: String,
|
||||||
|
val billingCheckinTime: String,
|
||||||
|
val billingCheckoutTime: String,
|
||||||
val active: Boolean,
|
val active: Boolean,
|
||||||
val otaAliases: Set<String>,
|
val otaAliases: Set<String>,
|
||||||
val emailAddresses: Set<String>,
|
val emailAddresses: Set<String>,
|
||||||
@@ -42,6 +48,17 @@ data class PropertyCodeResponse(
|
|||||||
val code: String
|
val code: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class PropertyBillingPolicyRequest(
|
||||||
|
val billingCheckinTime: String,
|
||||||
|
val billingCheckoutTime: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PropertyBillingPolicyResponse(
|
||||||
|
val propertyId: UUID,
|
||||||
|
val billingCheckinTime: String,
|
||||||
|
val billingCheckoutTime: String
|
||||||
|
)
|
||||||
|
|
||||||
data class GuestResponse(
|
data class GuestResponse(
|
||||||
val id: UUID,
|
val id: UUID,
|
||||||
val name: String?,
|
val name: String?,
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import com.android.trisolarisserver.controller.common.requireUser
|
|||||||
|
|
||||||
import com.android.trisolarisserver.component.auth.PropertyAccess
|
import com.android.trisolarisserver.component.auth.PropertyAccess
|
||||||
import com.android.trisolarisserver.controller.dto.property.PropertyCodeResponse
|
import com.android.trisolarisserver.controller.dto.property.PropertyCodeResponse
|
||||||
|
import com.android.trisolarisserver.controller.dto.property.PropertyBillingPolicyRequest
|
||||||
|
import com.android.trisolarisserver.controller.dto.property.PropertyBillingPolicyResponse
|
||||||
import com.android.trisolarisserver.controller.dto.property.PropertyCreateRequest
|
import com.android.trisolarisserver.controller.dto.property.PropertyCreateRequest
|
||||||
import com.android.trisolarisserver.controller.dto.property.PropertyResponse
|
import com.android.trisolarisserver.controller.dto.property.PropertyResponse
|
||||||
import com.android.trisolarisserver.controller.dto.property.PropertyUserDisableRequest
|
import com.android.trisolarisserver.controller.dto.property.PropertyUserDisableRequest
|
||||||
@@ -31,6 +33,9 @@ import org.springframework.web.bind.annotation.RequestBody
|
|||||||
import org.springframework.web.bind.annotation.ResponseStatus
|
import org.springframework.web.bind.annotation.ResponseStatus
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
import org.springframework.web.server.ResponseStatusException
|
import org.springframework.web.server.ResponseStatusException
|
||||||
|
import java.time.LocalTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.time.format.DateTimeParseException
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -58,6 +63,8 @@ class Properties(
|
|||||||
addressText = request.addressText,
|
addressText = request.addressText,
|
||||||
timezone = request.timezone ?: "Asia/Kolkata",
|
timezone = request.timezone ?: "Asia/Kolkata",
|
||||||
currency = request.currency ?: "INR",
|
currency = request.currency ?: "INR",
|
||||||
|
billingCheckinTime = validateBillingTime(request.billingCheckinTime, "billingCheckinTime", "12:00"),
|
||||||
|
billingCheckoutTime = validateBillingTime(request.billingCheckoutTime, "billingCheckoutTime", "11:00"),
|
||||||
active = request.active ?: true,
|
active = request.active ?: true,
|
||||||
otaAliases = request.otaAliases?.toMutableSet() ?: mutableSetOf(),
|
otaAliases = request.otaAliases?.toMutableSet() ?: mutableSetOf(),
|
||||||
emailAddresses = request.emailAddresses?.toMutableSet() ?: mutableSetOf(),
|
emailAddresses = request.emailAddresses?.toMutableSet() ?: mutableSetOf(),
|
||||||
@@ -107,6 +114,53 @@ class Properties(
|
|||||||
return PropertyCodeResponse(code = property.code)
|
return PropertyCodeResponse(code = property.code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/properties/{propertyId}/billing-policy")
|
||||||
|
fun getBillingPolicy(
|
||||||
|
@PathVariable propertyId: UUID,
|
||||||
|
@AuthenticationPrincipal principal: MyPrincipal?
|
||||||
|
): PropertyBillingPolicyResponse {
|
||||||
|
requirePrincipal(principal)
|
||||||
|
propertyAccess.requireMember(propertyId, principal!!.userId)
|
||||||
|
val property = propertyRepo.findById(propertyId).orElseThrow {
|
||||||
|
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
|
||||||
|
}
|
||||||
|
return PropertyBillingPolicyResponse(
|
||||||
|
propertyId = property.id!!,
|
||||||
|
billingCheckinTime = property.billingCheckinTime,
|
||||||
|
billingCheckoutTime = property.billingCheckoutTime
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/properties/{propertyId}/billing-policy")
|
||||||
|
fun updateBillingPolicy(
|
||||||
|
@PathVariable propertyId: UUID,
|
||||||
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||||
|
@RequestBody request: PropertyBillingPolicyRequest
|
||||||
|
): PropertyBillingPolicyResponse {
|
||||||
|
requirePrincipal(principal)
|
||||||
|
propertyAccess.requireMember(propertyId, principal!!.userId)
|
||||||
|
propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN)
|
||||||
|
val property = propertyRepo.findById(propertyId).orElseThrow {
|
||||||
|
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
|
||||||
|
}
|
||||||
|
property.billingCheckinTime = validateBillingTime(
|
||||||
|
request.billingCheckinTime,
|
||||||
|
"billingCheckinTime",
|
||||||
|
property.billingCheckinTime
|
||||||
|
)
|
||||||
|
property.billingCheckoutTime = validateBillingTime(
|
||||||
|
request.billingCheckoutTime,
|
||||||
|
"billingCheckoutTime",
|
||||||
|
property.billingCheckoutTime
|
||||||
|
)
|
||||||
|
val saved = propertyRepo.save(property)
|
||||||
|
return PropertyBillingPolicyResponse(
|
||||||
|
propertyId = saved.id!!,
|
||||||
|
billingCheckinTime = saved.billingCheckinTime,
|
||||||
|
billingCheckoutTime = saved.billingCheckoutTime
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/properties/{propertyId}/users")
|
@GetMapping("/properties/{propertyId}/users")
|
||||||
fun listPropertyUsers(
|
fun listPropertyUsers(
|
||||||
@PathVariable propertyId: UUID,
|
@PathVariable propertyId: UUID,
|
||||||
@@ -270,6 +324,16 @@ class Properties(
|
|||||||
property.addressText = request.addressText ?: property.addressText
|
property.addressText = request.addressText ?: property.addressText
|
||||||
property.timezone = request.timezone ?: property.timezone
|
property.timezone = request.timezone ?: property.timezone
|
||||||
property.currency = request.currency ?: property.currency
|
property.currency = request.currency ?: property.currency
|
||||||
|
property.billingCheckinTime = validateBillingTime(
|
||||||
|
request.billingCheckinTime,
|
||||||
|
"billingCheckinTime",
|
||||||
|
property.billingCheckinTime
|
||||||
|
)
|
||||||
|
property.billingCheckoutTime = validateBillingTime(
|
||||||
|
request.billingCheckoutTime,
|
||||||
|
"billingCheckoutTime",
|
||||||
|
property.billingCheckoutTime
|
||||||
|
)
|
||||||
property.active = request.active ?: property.active
|
property.active = request.active ?: property.active
|
||||||
if (request.otaAliases != null) {
|
if (request.otaAliases != null) {
|
||||||
property.otaAliases = request.otaAliases.toMutableSet()
|
property.otaAliases = request.otaAliases.toMutableSet()
|
||||||
@@ -292,6 +356,15 @@ class Properties(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun validateBillingTime(value: String?, fieldName: String, fallback: String): String {
|
||||||
|
val candidate = value?.trim()?.takeIf { it.isNotEmpty() } ?: fallback
|
||||||
|
return try {
|
||||||
|
LocalTime.parse(candidate, BILLING_TIME_FORMATTER).format(BILLING_TIME_FORMATTER)
|
||||||
|
} catch (_: DateTimeParseException) {
|
||||||
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "$fieldName must be HH:mm")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun generatePropertyCode(): String {
|
private fun generatePropertyCode(): String {
|
||||||
repeat(10) {
|
repeat(10) {
|
||||||
val code = buildString(7) {
|
val code = buildString(7) {
|
||||||
@@ -319,6 +392,10 @@ class Properties(
|
|||||||
Role.AGENT -> 100
|
Role.AGENT -> 100
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val BILLING_TIME_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Property.toResponse(): PropertyResponse {
|
private fun Property.toResponse(): PropertyResponse {
|
||||||
@@ -330,6 +407,8 @@ private fun Property.toResponse(): PropertyResponse {
|
|||||||
addressText = addressText,
|
addressText = addressText,
|
||||||
timezone = timezone,
|
timezone = timezone,
|
||||||
currency = currency,
|
currency = currency,
|
||||||
|
billingCheckinTime = billingCheckinTime,
|
||||||
|
billingCheckoutTime = billingCheckoutTime,
|
||||||
active = active,
|
active = active,
|
||||||
otaAliases = otaAliases.toSet(),
|
otaAliases = otaAliases.toSet(),
|
||||||
emailAddresses = emailAddresses.toSet(),
|
emailAddresses = emailAddresses.toSet(),
|
||||||
|
|||||||
@@ -38,6 +38,12 @@ class Property(
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
var currency: String = "INR",
|
var currency: String = "INR",
|
||||||
|
|
||||||
|
@Column(name = "billing_checkin_time", nullable = false)
|
||||||
|
var billingCheckinTime: String = "12:00",
|
||||||
|
|
||||||
|
@Column(name = "billing_checkout_time", nullable = false)
|
||||||
|
var billingCheckoutTime: String = "11:00",
|
||||||
|
|
||||||
@Column(name = "is_active", nullable = false)
|
@Column(name = "is_active", nullable = false)
|
||||||
var active: Boolean = true,
|
var active: Boolean = true,
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user