Move billing policy to booking level with override modes and audit logs
All checks were successful
build-and-deploy / build-deploy (push) Successful in 36s
All checks were successful
build-and-deploy / build-deploy (push) Successful in 36s
This commit is contained in:
@@ -44,8 +44,9 @@ class BookingBalances(
|
||||
val expected = computeExpectedPay(
|
||||
roomStayRepo.findByBookingId(bookingId),
|
||||
booking.property.timezone,
|
||||
booking.property.billingCheckinTime,
|
||||
booking.property.billingCheckoutTime
|
||||
booking.billingMode,
|
||||
booking.billingCheckinTime,
|
||||
booking.billingCheckoutTime
|
||||
)
|
||||
val charges = chargeRepo.sumAmountByBookingId(bookingId)
|
||||
val collected = paymentRepo.sumAmountByBookingId(bookingId)
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.android.trisolarisserver.controller.dto.booking.BookingCheckOutReques
|
||||
import com.android.trisolarisserver.controller.dto.booking.BookingCreateRequest
|
||||
import com.android.trisolarisserver.controller.dto.booking.BookingCreateResponse
|
||||
import com.android.trisolarisserver.controller.dto.booking.BookingDetailResponse
|
||||
import com.android.trisolarisserver.controller.dto.booking.BookingBillingPolicyUpdateRequest
|
||||
import com.android.trisolarisserver.controller.dto.booking.BookingExpectedDatesUpdateRequest
|
||||
import com.android.trisolarisserver.controller.dto.booking.BookingLinkGuestRequest
|
||||
import com.android.trisolarisserver.controller.dto.booking.BookingNoShowRequest
|
||||
@@ -24,7 +25,9 @@ import com.android.trisolarisserver.repo.guest.GuestDocumentRepo
|
||||
import com.android.trisolarisserver.repo.guest.GuestRepo
|
||||
import com.android.trisolarisserver.repo.guest.GuestRatingRepo
|
||||
import com.android.trisolarisserver.models.booking.BookingStatus
|
||||
import com.android.trisolarisserver.models.booking.BookingBillingMode
|
||||
import com.android.trisolarisserver.models.booking.BookingRoomRequestStatus
|
||||
import com.android.trisolarisserver.models.booking.BookingBillingPolicyAuditLog
|
||||
import com.android.trisolarisserver.models.booking.Charge
|
||||
import com.android.trisolarisserver.models.booking.ChargeType
|
||||
import com.android.trisolarisserver.models.booking.MemberRelation
|
||||
@@ -38,6 +41,7 @@ import com.android.trisolarisserver.repo.property.AppUserRepo
|
||||
import com.android.trisolarisserver.repo.guest.GuestVehicleRepo
|
||||
import com.android.trisolarisserver.repo.booking.PaymentRepo
|
||||
import com.android.trisolarisserver.repo.booking.BookingRoomRequestRepo
|
||||
import com.android.trisolarisserver.repo.booking.BookingBillingPolicyAuditLogRepo
|
||||
import com.android.trisolarisserver.repo.booking.ChargeRepo
|
||||
import com.android.trisolarisserver.repo.property.PropertyRepo
|
||||
import com.android.trisolarisserver.repo.property.PropertyCancellationPolicyRepo
|
||||
@@ -58,7 +62,10 @@ 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.LocalTime
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.DateTimeParseException
|
||||
import kotlin.math.abs
|
||||
import java.util.UUID
|
||||
|
||||
@@ -82,6 +89,7 @@ class BookingFlow(
|
||||
private val bookingSnapshotBuilder: BookingSnapshotBuilder,
|
||||
private val roomStayAuditLogRepo: RoomStayAuditLogRepo,
|
||||
private val bookingRoomRequestRepo: BookingRoomRequestRepo,
|
||||
private val bookingBillingPolicyAuditLogRepo: BookingBillingPolicyAuditLogRepo,
|
||||
private val propertyCancellationPolicyRepo: PropertyCancellationPolicyRepo
|
||||
) {
|
||||
|
||||
@@ -112,6 +120,12 @@ class BookingFlow(
|
||||
val fromCity = request.fromCity?.trim()?.ifBlank { null }
|
||||
val toCity = request.toCity?.trim()?.ifBlank { null }
|
||||
val memberRelation = parseMemberRelation(request.memberRelation)
|
||||
val (billingMode, billingCheckinTime, billingCheckoutTime) = resolveBookingBillingPolicy(
|
||||
property = property,
|
||||
modeRaw = request.billingMode,
|
||||
billingCheckinTimeRaw = request.billingCheckinTime,
|
||||
billingCheckoutTimeRaw = request.billingCheckoutTime
|
||||
)
|
||||
val hasGuestCounts = request.maleCount != null || request.femaleCount != null || request.childCount != null
|
||||
val adultCount = if (hasGuestCounts) {
|
||||
(request.maleCount ?: 0) + (request.femaleCount ?: 0)
|
||||
@@ -131,6 +145,9 @@ class BookingFlow(
|
||||
checkinAt = null,
|
||||
expectedCheckinAt = expectedCheckInAt,
|
||||
expectedCheckoutAt = expectedCheckOutAt,
|
||||
billingMode = billingMode,
|
||||
billingCheckinTime = billingCheckinTime,
|
||||
billingCheckoutTime = billingCheckoutTime,
|
||||
transportMode = request.transportMode?.let {
|
||||
val mode = parseTransportMode(it)
|
||||
if (!isTransportModeAllowed(property, mode)) {
|
||||
@@ -157,6 +174,9 @@ class BookingFlow(
|
||||
return BookingCreateResponse(
|
||||
id = saved.id!!,
|
||||
status = saved.status.name,
|
||||
billingMode = saved.billingMode.name,
|
||||
billingCheckinTime = saved.billingCheckinTime,
|
||||
billingCheckoutTime = saved.billingCheckoutTime,
|
||||
guestId = guest.id,
|
||||
checkInAt = saved.checkinAt?.toString(),
|
||||
expectedCheckInAt = saved.expectedCheckinAt?.toString(),
|
||||
@@ -223,8 +243,9 @@ class BookingFlow(
|
||||
computeExpectedPay(
|
||||
stays,
|
||||
property.timezone,
|
||||
property.billingCheckinTime,
|
||||
property.billingCheckoutTime
|
||||
booking.billingMode,
|
||||
booking.billingCheckinTime,
|
||||
booking.billingCheckoutTime
|
||||
)
|
||||
}
|
||||
val collected = paymentsByBooking[booking.id] ?: 0L
|
||||
@@ -238,6 +259,9 @@ class BookingFlow(
|
||||
guestPhone = guest?.phoneE164,
|
||||
roomNumbers = roomNumbersByBooking[booking.id] ?: emptyList(),
|
||||
source = booking.source,
|
||||
billingMode = booking.billingMode.name,
|
||||
billingCheckinTime = booking.billingCheckinTime,
|
||||
billingCheckoutTime = booking.billingCheckoutTime,
|
||||
expectedCheckInAt = booking.expectedCheckinAt?.toString(),
|
||||
expectedCheckOutAt = booking.expectedCheckoutAt?.toString(),
|
||||
checkInAt = booking.checkinAt?.toString(),
|
||||
@@ -447,6 +471,58 @@ class BookingFlow(
|
||||
bookingEvents.emit(propertyId, bookingId)
|
||||
}
|
||||
|
||||
@PostMapping("/{bookingId}/billing-policy")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@Transactional
|
||||
fun updateBillingPolicy(
|
||||
@PathVariable propertyId: UUID,
|
||||
@PathVariable bookingId: UUID,
|
||||
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||
@RequestBody request: BookingBillingPolicyUpdateRequest
|
||||
) {
|
||||
val actor = requireActor(propertyId, principal)
|
||||
val booking = requireBooking(propertyId, bookingId)
|
||||
when (booking.status) {
|
||||
BookingStatus.CHECKED_OUT,
|
||||
BookingStatus.CANCELLED,
|
||||
BookingStatus.NO_SHOW -> throw ResponseStatusException(HttpStatus.CONFLICT, "Booking closed")
|
||||
else -> {}
|
||||
}
|
||||
|
||||
val oldMode = booking.billingMode
|
||||
val oldCheckin = booking.billingCheckinTime
|
||||
val oldCheckout = booking.billingCheckoutTime
|
||||
val (newMode, newCheckin, newCheckout) = resolveBookingBillingPolicy(
|
||||
property = booking.property,
|
||||
modeRaw = request.billingMode,
|
||||
billingCheckinTimeRaw = request.billingCheckinTime,
|
||||
billingCheckoutTimeRaw = request.billingCheckoutTime
|
||||
)
|
||||
booking.billingMode = newMode
|
||||
booking.billingCheckinTime = newCheckin
|
||||
booking.billingCheckoutTime = newCheckout
|
||||
booking.updatedAt = OffsetDateTime.now()
|
||||
bookingRepo.save(booking)
|
||||
|
||||
if (oldMode != newMode || oldCheckin != newCheckin || oldCheckout != newCheckout) {
|
||||
bookingBillingPolicyAuditLogRepo.save(
|
||||
BookingBillingPolicyAuditLog(
|
||||
property = booking.property,
|
||||
booking = booking,
|
||||
actor = actor,
|
||||
oldBillingMode = oldMode,
|
||||
newBillingMode = newMode,
|
||||
oldBillingCheckinTime = oldCheckin,
|
||||
newBillingCheckinTime = newCheckin,
|
||||
oldBillingCheckoutTime = oldCheckout,
|
||||
newBillingCheckoutTime = newCheckout
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
bookingEvents.emit(propertyId, bookingId)
|
||||
}
|
||||
|
||||
@PostMapping("/{bookingId}/check-out")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@Transactional
|
||||
@@ -471,8 +547,9 @@ class BookingFlow(
|
||||
ensureLedgerToleranceForCheckout(
|
||||
bookingId = bookingId,
|
||||
timezone = booking.property.timezone,
|
||||
billingCheckinTime = booking.property.billingCheckinTime,
|
||||
billingCheckoutTime = booking.property.billingCheckoutTime,
|
||||
billingMode = booking.billingMode,
|
||||
billingCheckinTime = booking.billingCheckinTime,
|
||||
billingCheckoutTime = booking.billingCheckoutTime,
|
||||
stays = stays,
|
||||
checkOutOverrides = stays.associate { it.id!! to checkOutAt },
|
||||
restrictToClosedStaysOnly = false
|
||||
@@ -543,8 +620,9 @@ class BookingFlow(
|
||||
ensureLedgerToleranceForCheckout(
|
||||
bookingId = bookingId,
|
||||
timezone = booking.property.timezone,
|
||||
billingCheckinTime = booking.property.billingCheckinTime,
|
||||
billingCheckoutTime = booking.property.billingCheckoutTime,
|
||||
billingMode = booking.billingMode,
|
||||
billingCheckinTime = booking.billingCheckinTime,
|
||||
billingCheckoutTime = booking.billingCheckoutTime,
|
||||
stays = staysForBooking,
|
||||
checkOutOverrides = mapOf(stay.id!! to checkOutAt),
|
||||
restrictToClosedStaysOnly = true
|
||||
@@ -798,6 +876,7 @@ class BookingFlow(
|
||||
private fun ensureLedgerToleranceForCheckout(
|
||||
bookingId: UUID,
|
||||
timezone: String?,
|
||||
billingMode: BookingBillingMode,
|
||||
billingCheckinTime: String?,
|
||||
billingCheckoutTime: String?,
|
||||
stays: List<RoomStay>,
|
||||
@@ -822,6 +901,7 @@ class BookingFlow(
|
||||
stay.fromAt,
|
||||
endAt,
|
||||
timezone,
|
||||
billingMode,
|
||||
billingCheckinTime,
|
||||
billingCheckoutTime
|
||||
)
|
||||
@@ -899,4 +979,53 @@ class BookingFlow(
|
||||
response.setHeader("X-Accel-Buffering", "no")
|
||||
}
|
||||
|
||||
private fun resolveBookingBillingPolicy(
|
||||
property: com.android.trisolarisserver.models.property.Property,
|
||||
modeRaw: String?,
|
||||
billingCheckinTimeRaw: String?,
|
||||
billingCheckoutTimeRaw: String?
|
||||
): Triple<BookingBillingMode, String, String> {
|
||||
val mode = parseBillingMode(modeRaw)
|
||||
return when (mode) {
|
||||
BookingBillingMode.PROPERTY_POLICY -> Triple(
|
||||
BookingBillingMode.PROPERTY_POLICY,
|
||||
property.billingCheckinTime,
|
||||
property.billingCheckoutTime
|
||||
)
|
||||
BookingBillingMode.CUSTOM_WINDOW -> Triple(
|
||||
BookingBillingMode.CUSTOM_WINDOW,
|
||||
normalizeBillingTime(billingCheckinTimeRaw, "billingCheckinTime"),
|
||||
normalizeBillingTime(billingCheckoutTimeRaw, "billingCheckoutTime")
|
||||
)
|
||||
BookingBillingMode.FULL_24H -> Triple(
|
||||
BookingBillingMode.FULL_24H,
|
||||
"00:00",
|
||||
"23:59"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseBillingMode(raw: String?): BookingBillingMode {
|
||||
if (raw.isNullOrBlank()) return BookingBillingMode.PROPERTY_POLICY
|
||||
return try {
|
||||
BookingBillingMode.valueOf(raw.trim().uppercase())
|
||||
} catch (_: IllegalArgumentException) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown billing mode")
|
||||
}
|
||||
}
|
||||
|
||||
private fun normalizeBillingTime(raw: String?, fieldName: String): String {
|
||||
val value = raw?.trim()?.takeIf { it.isNotEmpty() }
|
||||
?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "$fieldName required")
|
||||
return try {
|
||||
LocalTime.parse(value, BILLING_TIME_FORMATTER).format(BILLING_TIME_FORMATTER)
|
||||
} catch (_: DateTimeParseException) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "$fieldName must be HH:mm")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val BILLING_TIME_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -54,14 +54,16 @@ class BookingSnapshotBuilder(
|
||||
stays,
|
||||
booking.expectedCheckoutAt,
|
||||
booking.property.timezone,
|
||||
booking.property.billingCheckinTime,
|
||||
booking.property.billingCheckoutTime
|
||||
booking.billingMode,
|
||||
booking.billingCheckinTime,
|
||||
booking.billingCheckoutTime
|
||||
)
|
||||
val accruedPay = computeExpectedPay(
|
||||
stays,
|
||||
booking.property.timezone,
|
||||
booking.property.billingCheckinTime,
|
||||
booking.property.billingCheckoutTime
|
||||
booking.billingMode,
|
||||
booking.billingCheckinTime,
|
||||
booking.billingCheckoutTime
|
||||
)
|
||||
val extraCharges = chargeRepo.sumAmountByBookingId(bookingId)
|
||||
val amountCollected = paymentRepo.sumAmountByBookingId(bookingId)
|
||||
@@ -80,6 +82,9 @@ class BookingSnapshotBuilder(
|
||||
vehicleNumbers = vehicleNumbers,
|
||||
roomNumbers = roomNumbers,
|
||||
source = booking.source,
|
||||
billingMode = booking.billingMode.name,
|
||||
billingCheckinTime = booking.billingCheckinTime,
|
||||
billingCheckoutTime = booking.billingCheckoutTime,
|
||||
fromCity = booking.fromCity,
|
||||
toCity = booking.toCity,
|
||||
memberRelation = booking.memberRelation?.name,
|
||||
|
||||
Reference in New Issue
Block a user