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:
@@ -1,4 +1,5 @@
|
||||
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.nowForProperty
|
||||
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.server.ResponseStatusException
|
||||
import java.time.OffsetDateTime
|
||||
import kotlin.math.abs
|
||||
import java.util.UUID
|
||||
|
||||
@RestController
|
||||
@@ -218,7 +220,12 @@ class BookingFlow(
|
||||
val expectedPay = if (stays.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
computeExpectedPay(stays, property.timezone)
|
||||
computeExpectedPay(
|
||||
stays,
|
||||
property.timezone,
|
||||
property.billingCheckinTime,
|
||||
property.billingCheckoutTime
|
||||
)
|
||||
}
|
||||
val collected = paymentsByBooking[booking.id] ?: 0L
|
||||
val extraCharges = chargesByBooking[booking.id] ?: 0L
|
||||
@@ -461,6 +468,15 @@ class BookingFlow(
|
||||
if (stays.any { !isCheckoutAmountValid(it) || !isMinimumStayDurationValid(it, checkOutAt) }) {
|
||||
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 }
|
||||
stays.forEach { it.toAt = checkOutAt }
|
||||
roomStayRepo.saveAll(stays)
|
||||
@@ -524,6 +540,15 @@ class BookingFlow(
|
||||
if (!isMinimumStayDurationValid(stay, checkOutAt)) {
|
||||
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 oldIsVoided = stay.isVoided
|
||||
@@ -770,6 +795,51 @@ class BookingFlow(
|
||||
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(
|
||||
stay: RoomStay,
|
||||
action: String,
|
||||
|
||||
Reference in New Issue
Block a user