Add property billing policy times and policy-based night calculation
All checks were successful
build-and-deploy / build-deploy (push) Successful in 35s

This commit is contained in:
androidlover5842
2026-02-02 10:17:00 +05:30
parent 734591807f
commit 776ed6dc4e
7 changed files with 270 additions and 28 deletions

View File

@@ -43,7 +43,9 @@ class BookingBalances(
}
val expected = computeExpectedPay(
roomStayRepo.findByBookingId(bookingId),
booking.property.timezone
booking.property.timezone,
booking.property.billingCheckinTime,
booking.property.billingCheckoutTime
)
val charges = chargeRepo.sumAmountByBookingId(bookingId)
val collected = paymentRepo.sumAmountByBookingId(bookingId)

View File

@@ -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,

View File

@@ -50,8 +50,19 @@ class BookingSnapshotBuilder(
}
val totalNightlyRate = roomsToShow.sumOf { it.nightlyRate ?: 0L }
val expectedPay = computeExpectedPayTotal(stays, booking.expectedCheckoutAt, booking.property.timezone)
val accruedPay = computeExpectedPay(stays, booking.property.timezone)
val expectedPay = computeExpectedPayTotal(
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 amountCollected = paymentRepo.sumAmountByBookingId(bookingId)
val pending = accruedPay + extraCharges - amountCollected