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(
|
val expected = computeExpectedPay(
|
||||||
roomStayRepo.findByBookingId(bookingId),
|
roomStayRepo.findByBookingId(bookingId),
|
||||||
booking.property.timezone,
|
booking.property.timezone,
|
||||||
booking.property.billingCheckinTime,
|
booking.billingMode,
|
||||||
booking.property.billingCheckoutTime
|
booking.billingCheckinTime,
|
||||||
|
booking.billingCheckoutTime
|
||||||
)
|
)
|
||||||
val charges = chargeRepo.sumAmountByBookingId(bookingId)
|
val charges = chargeRepo.sumAmountByBookingId(bookingId)
|
||||||
val collected = paymentRepo.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.BookingCreateRequest
|
||||||
import com.android.trisolarisserver.controller.dto.booking.BookingCreateResponse
|
import com.android.trisolarisserver.controller.dto.booking.BookingCreateResponse
|
||||||
import com.android.trisolarisserver.controller.dto.booking.BookingDetailResponse
|
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.BookingExpectedDatesUpdateRequest
|
||||||
import com.android.trisolarisserver.controller.dto.booking.BookingLinkGuestRequest
|
import com.android.trisolarisserver.controller.dto.booking.BookingLinkGuestRequest
|
||||||
import com.android.trisolarisserver.controller.dto.booking.BookingNoShowRequest
|
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.GuestRepo
|
||||||
import com.android.trisolarisserver.repo.guest.GuestRatingRepo
|
import com.android.trisolarisserver.repo.guest.GuestRatingRepo
|
||||||
import com.android.trisolarisserver.models.booking.BookingStatus
|
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.BookingRoomRequestStatus
|
||||||
|
import com.android.trisolarisserver.models.booking.BookingBillingPolicyAuditLog
|
||||||
import com.android.trisolarisserver.models.booking.Charge
|
import com.android.trisolarisserver.models.booking.Charge
|
||||||
import com.android.trisolarisserver.models.booking.ChargeType
|
import com.android.trisolarisserver.models.booking.ChargeType
|
||||||
import com.android.trisolarisserver.models.booking.MemberRelation
|
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.guest.GuestVehicleRepo
|
||||||
import com.android.trisolarisserver.repo.booking.PaymentRepo
|
import com.android.trisolarisserver.repo.booking.PaymentRepo
|
||||||
import com.android.trisolarisserver.repo.booking.BookingRoomRequestRepo
|
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.booking.ChargeRepo
|
||||||
import com.android.trisolarisserver.repo.property.PropertyRepo
|
import com.android.trisolarisserver.repo.property.PropertyRepo
|
||||||
import com.android.trisolarisserver.repo.property.PropertyCancellationPolicyRepo
|
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.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.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.time.format.DateTimeParseException
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@@ -82,6 +89,7 @@ class BookingFlow(
|
|||||||
private val bookingSnapshotBuilder: BookingSnapshotBuilder,
|
private val bookingSnapshotBuilder: BookingSnapshotBuilder,
|
||||||
private val roomStayAuditLogRepo: RoomStayAuditLogRepo,
|
private val roomStayAuditLogRepo: RoomStayAuditLogRepo,
|
||||||
private val bookingRoomRequestRepo: BookingRoomRequestRepo,
|
private val bookingRoomRequestRepo: BookingRoomRequestRepo,
|
||||||
|
private val bookingBillingPolicyAuditLogRepo: BookingBillingPolicyAuditLogRepo,
|
||||||
private val propertyCancellationPolicyRepo: PropertyCancellationPolicyRepo
|
private val propertyCancellationPolicyRepo: PropertyCancellationPolicyRepo
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -112,6 +120,12 @@ class BookingFlow(
|
|||||||
val fromCity = request.fromCity?.trim()?.ifBlank { null }
|
val fromCity = request.fromCity?.trim()?.ifBlank { null }
|
||||||
val toCity = request.toCity?.trim()?.ifBlank { null }
|
val toCity = request.toCity?.trim()?.ifBlank { null }
|
||||||
val memberRelation = parseMemberRelation(request.memberRelation)
|
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 hasGuestCounts = request.maleCount != null || request.femaleCount != null || request.childCount != null
|
||||||
val adultCount = if (hasGuestCounts) {
|
val adultCount = if (hasGuestCounts) {
|
||||||
(request.maleCount ?: 0) + (request.femaleCount ?: 0)
|
(request.maleCount ?: 0) + (request.femaleCount ?: 0)
|
||||||
@@ -131,6 +145,9 @@ class BookingFlow(
|
|||||||
checkinAt = null,
|
checkinAt = null,
|
||||||
expectedCheckinAt = expectedCheckInAt,
|
expectedCheckinAt = expectedCheckInAt,
|
||||||
expectedCheckoutAt = expectedCheckOutAt,
|
expectedCheckoutAt = expectedCheckOutAt,
|
||||||
|
billingMode = billingMode,
|
||||||
|
billingCheckinTime = billingCheckinTime,
|
||||||
|
billingCheckoutTime = billingCheckoutTime,
|
||||||
transportMode = request.transportMode?.let {
|
transportMode = request.transportMode?.let {
|
||||||
val mode = parseTransportMode(it)
|
val mode = parseTransportMode(it)
|
||||||
if (!isTransportModeAllowed(property, mode)) {
|
if (!isTransportModeAllowed(property, mode)) {
|
||||||
@@ -157,6 +174,9 @@ class BookingFlow(
|
|||||||
return BookingCreateResponse(
|
return BookingCreateResponse(
|
||||||
id = saved.id!!,
|
id = saved.id!!,
|
||||||
status = saved.status.name,
|
status = saved.status.name,
|
||||||
|
billingMode = saved.billingMode.name,
|
||||||
|
billingCheckinTime = saved.billingCheckinTime,
|
||||||
|
billingCheckoutTime = saved.billingCheckoutTime,
|
||||||
guestId = guest.id,
|
guestId = guest.id,
|
||||||
checkInAt = saved.checkinAt?.toString(),
|
checkInAt = saved.checkinAt?.toString(),
|
||||||
expectedCheckInAt = saved.expectedCheckinAt?.toString(),
|
expectedCheckInAt = saved.expectedCheckinAt?.toString(),
|
||||||
@@ -223,8 +243,9 @@ class BookingFlow(
|
|||||||
computeExpectedPay(
|
computeExpectedPay(
|
||||||
stays,
|
stays,
|
||||||
property.timezone,
|
property.timezone,
|
||||||
property.billingCheckinTime,
|
booking.billingMode,
|
||||||
property.billingCheckoutTime
|
booking.billingCheckinTime,
|
||||||
|
booking.billingCheckoutTime
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val collected = paymentsByBooking[booking.id] ?: 0L
|
val collected = paymentsByBooking[booking.id] ?: 0L
|
||||||
@@ -238,6 +259,9 @@ class BookingFlow(
|
|||||||
guestPhone = guest?.phoneE164,
|
guestPhone = guest?.phoneE164,
|
||||||
roomNumbers = roomNumbersByBooking[booking.id] ?: emptyList(),
|
roomNumbers = roomNumbersByBooking[booking.id] ?: emptyList(),
|
||||||
source = booking.source,
|
source = booking.source,
|
||||||
|
billingMode = booking.billingMode.name,
|
||||||
|
billingCheckinTime = booking.billingCheckinTime,
|
||||||
|
billingCheckoutTime = booking.billingCheckoutTime,
|
||||||
expectedCheckInAt = booking.expectedCheckinAt?.toString(),
|
expectedCheckInAt = booking.expectedCheckinAt?.toString(),
|
||||||
expectedCheckOutAt = booking.expectedCheckoutAt?.toString(),
|
expectedCheckOutAt = booking.expectedCheckoutAt?.toString(),
|
||||||
checkInAt = booking.checkinAt?.toString(),
|
checkInAt = booking.checkinAt?.toString(),
|
||||||
@@ -447,6 +471,58 @@ class BookingFlow(
|
|||||||
bookingEvents.emit(propertyId, bookingId)
|
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")
|
@PostMapping("/{bookingId}/check-out")
|
||||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -471,8 +547,9 @@ class BookingFlow(
|
|||||||
ensureLedgerToleranceForCheckout(
|
ensureLedgerToleranceForCheckout(
|
||||||
bookingId = bookingId,
|
bookingId = bookingId,
|
||||||
timezone = booking.property.timezone,
|
timezone = booking.property.timezone,
|
||||||
billingCheckinTime = booking.property.billingCheckinTime,
|
billingMode = booking.billingMode,
|
||||||
billingCheckoutTime = booking.property.billingCheckoutTime,
|
billingCheckinTime = booking.billingCheckinTime,
|
||||||
|
billingCheckoutTime = booking.billingCheckoutTime,
|
||||||
stays = stays,
|
stays = stays,
|
||||||
checkOutOverrides = stays.associate { it.id!! to checkOutAt },
|
checkOutOverrides = stays.associate { it.id!! to checkOutAt },
|
||||||
restrictToClosedStaysOnly = false
|
restrictToClosedStaysOnly = false
|
||||||
@@ -543,8 +620,9 @@ class BookingFlow(
|
|||||||
ensureLedgerToleranceForCheckout(
|
ensureLedgerToleranceForCheckout(
|
||||||
bookingId = bookingId,
|
bookingId = bookingId,
|
||||||
timezone = booking.property.timezone,
|
timezone = booking.property.timezone,
|
||||||
billingCheckinTime = booking.property.billingCheckinTime,
|
billingMode = booking.billingMode,
|
||||||
billingCheckoutTime = booking.property.billingCheckoutTime,
|
billingCheckinTime = booking.billingCheckinTime,
|
||||||
|
billingCheckoutTime = booking.billingCheckoutTime,
|
||||||
stays = staysForBooking,
|
stays = staysForBooking,
|
||||||
checkOutOverrides = mapOf(stay.id!! to checkOutAt),
|
checkOutOverrides = mapOf(stay.id!! to checkOutAt),
|
||||||
restrictToClosedStaysOnly = true
|
restrictToClosedStaysOnly = true
|
||||||
@@ -798,6 +876,7 @@ class BookingFlow(
|
|||||||
private fun ensureLedgerToleranceForCheckout(
|
private fun ensureLedgerToleranceForCheckout(
|
||||||
bookingId: UUID,
|
bookingId: UUID,
|
||||||
timezone: String?,
|
timezone: String?,
|
||||||
|
billingMode: BookingBillingMode,
|
||||||
billingCheckinTime: String?,
|
billingCheckinTime: String?,
|
||||||
billingCheckoutTime: String?,
|
billingCheckoutTime: String?,
|
||||||
stays: List<RoomStay>,
|
stays: List<RoomStay>,
|
||||||
@@ -822,6 +901,7 @@ class BookingFlow(
|
|||||||
stay.fromAt,
|
stay.fromAt,
|
||||||
endAt,
|
endAt,
|
||||||
timezone,
|
timezone,
|
||||||
|
billingMode,
|
||||||
billingCheckinTime,
|
billingCheckinTime,
|
||||||
billingCheckoutTime
|
billingCheckoutTime
|
||||||
)
|
)
|
||||||
@@ -899,4 +979,53 @@ class BookingFlow(
|
|||||||
response.setHeader("X-Accel-Buffering", "no")
|
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,
|
stays,
|
||||||
booking.expectedCheckoutAt,
|
booking.expectedCheckoutAt,
|
||||||
booking.property.timezone,
|
booking.property.timezone,
|
||||||
booking.property.billingCheckinTime,
|
booking.billingMode,
|
||||||
booking.property.billingCheckoutTime
|
booking.billingCheckinTime,
|
||||||
|
booking.billingCheckoutTime
|
||||||
)
|
)
|
||||||
val accruedPay = computeExpectedPay(
|
val accruedPay = computeExpectedPay(
|
||||||
stays,
|
stays,
|
||||||
booking.property.timezone,
|
booking.property.timezone,
|
||||||
booking.property.billingCheckinTime,
|
booking.billingMode,
|
||||||
booking.property.billingCheckoutTime
|
booking.billingCheckinTime,
|
||||||
|
booking.billingCheckoutTime
|
||||||
)
|
)
|
||||||
val extraCharges = chargeRepo.sumAmountByBookingId(bookingId)
|
val extraCharges = chargeRepo.sumAmountByBookingId(bookingId)
|
||||||
val amountCollected = paymentRepo.sumAmountByBookingId(bookingId)
|
val amountCollected = paymentRepo.sumAmountByBookingId(bookingId)
|
||||||
@@ -80,6 +82,9 @@ class BookingSnapshotBuilder(
|
|||||||
vehicleNumbers = vehicleNumbers,
|
vehicleNumbers = vehicleNumbers,
|
||||||
roomNumbers = roomNumbers,
|
roomNumbers = roomNumbers,
|
||||||
source = booking.source,
|
source = booking.source,
|
||||||
|
billingMode = booking.billingMode.name,
|
||||||
|
billingCheckinTime = booking.billingCheckinTime,
|
||||||
|
billingCheckoutTime = booking.billingCheckoutTime,
|
||||||
fromCity = booking.fromCity,
|
fromCity = booking.fromCity,
|
||||||
toCity = booking.toCity,
|
toCity = booking.toCity,
|
||||||
memberRelation = booking.memberRelation?.name,
|
memberRelation = booking.memberRelation?.name,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import com.android.trisolarisserver.controller.common.requireRoomStayForProperty
|
|||||||
|
|
||||||
import com.android.trisolarisserver.repo.guest.GuestRepo
|
import com.android.trisolarisserver.repo.guest.GuestRepo
|
||||||
import com.android.trisolarisserver.models.booking.Guest
|
import com.android.trisolarisserver.models.booking.Guest
|
||||||
|
import com.android.trisolarisserver.models.booking.BookingBillingMode
|
||||||
import com.android.trisolarisserver.models.property.Property
|
import com.android.trisolarisserver.models.property.Property
|
||||||
import com.android.trisolarisserver.models.room.RoomStay
|
import com.android.trisolarisserver.models.room.RoomStay
|
||||||
import com.android.trisolarisserver.repo.property.PropertyRepo
|
import com.android.trisolarisserver.repo.property.PropertyRepo
|
||||||
@@ -107,6 +108,7 @@ internal fun nowForProperty(timezone: String?): OffsetDateTime {
|
|||||||
internal fun computeExpectedPay(
|
internal fun computeExpectedPay(
|
||||||
stays: List<RoomStay>,
|
stays: List<RoomStay>,
|
||||||
timezone: String?,
|
timezone: String?,
|
||||||
|
billingMode: BookingBillingMode,
|
||||||
billingCheckinTime: String?,
|
billingCheckinTime: String?,
|
||||||
billingCheckoutTime: String?
|
billingCheckoutTime: String?
|
||||||
): Long {
|
): Long {
|
||||||
@@ -122,6 +124,7 @@ internal fun computeExpectedPay(
|
|||||||
stay.fromAt,
|
stay.fromAt,
|
||||||
endAt,
|
endAt,
|
||||||
timezone,
|
timezone,
|
||||||
|
billingMode,
|
||||||
billingCheckinTime,
|
billingCheckinTime,
|
||||||
billingCheckoutTime
|
billingCheckoutTime
|
||||||
)
|
)
|
||||||
@@ -134,6 +137,7 @@ internal fun computeExpectedPayTotal(
|
|||||||
stays: List<RoomStay>,
|
stays: List<RoomStay>,
|
||||||
expectedCheckoutAt: OffsetDateTime?,
|
expectedCheckoutAt: OffsetDateTime?,
|
||||||
timezone: String?,
|
timezone: String?,
|
||||||
|
billingMode: BookingBillingMode,
|
||||||
billingCheckinTime: String?,
|
billingCheckinTime: String?,
|
||||||
billingCheckoutTime: String?
|
billingCheckoutTime: String?
|
||||||
): Long {
|
): Long {
|
||||||
@@ -149,6 +153,7 @@ internal fun computeExpectedPayTotal(
|
|||||||
stay.fromAt,
|
stay.fromAt,
|
||||||
endAt,
|
endAt,
|
||||||
timezone,
|
timezone,
|
||||||
|
billingMode,
|
||||||
billingCheckinTime,
|
billingCheckinTime,
|
||||||
billingCheckoutTime
|
billingCheckoutTime
|
||||||
)
|
)
|
||||||
@@ -164,10 +169,19 @@ internal fun billableNights(
|
|||||||
startAt: OffsetDateTime,
|
startAt: OffsetDateTime,
|
||||||
endAt: OffsetDateTime,
|
endAt: OffsetDateTime,
|
||||||
timezone: String?,
|
timezone: String?,
|
||||||
|
billingMode: BookingBillingMode,
|
||||||
billingCheckinTime: String?,
|
billingCheckinTime: String?,
|
||||||
billingCheckoutTime: String?
|
billingCheckoutTime: String?
|
||||||
): Long {
|
): Long {
|
||||||
if (!endAt.isAfter(startAt)) return 1L
|
if (!endAt.isAfter(startAt)) return 1L
|
||||||
|
if (billingMode == BookingBillingMode.FULL_24H) {
|
||||||
|
val minutes = java.time.Duration.between(startAt, endAt).toMinutes().coerceAtLeast(0L)
|
||||||
|
if (minutes == 0L) return 1L
|
||||||
|
val fullDays = minutes / (24L * 60L)
|
||||||
|
val remainder = minutes % (24L * 60L)
|
||||||
|
val extraNight = if (remainder > 120L) 1L else 0L
|
||||||
|
return (fullDays + extraNight).coerceAtLeast(1L)
|
||||||
|
}
|
||||||
val zone = try {
|
val zone = try {
|
||||||
if (timezone.isNullOrBlank()) ZoneId.of("Asia/Kolkata") else ZoneId.of(timezone)
|
if (timezone.isNullOrBlank()) ZoneId.of("Asia/Kolkata") else ZoneId.of(timezone)
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ data class BookingCreateRequest(
|
|||||||
val source: String? = null,
|
val source: String? = null,
|
||||||
val expectedCheckInAt: String,
|
val expectedCheckInAt: String,
|
||||||
val expectedCheckOutAt: String,
|
val expectedCheckOutAt: String,
|
||||||
|
val billingMode: String? = null,
|
||||||
|
val billingCheckinTime: String? = null,
|
||||||
|
val billingCheckoutTime: String? = null,
|
||||||
val guestPhoneE164: String? = null,
|
val guestPhoneE164: String? = null,
|
||||||
val fromCity: String? = null,
|
val fromCity: String? = null,
|
||||||
val toCity: String? = null,
|
val toCity: String? = null,
|
||||||
@@ -39,6 +42,9 @@ data class BookingCreateRequest(
|
|||||||
data class BookingCreateResponse(
|
data class BookingCreateResponse(
|
||||||
val id: UUID,
|
val id: UUID,
|
||||||
val status: String,
|
val status: String,
|
||||||
|
val billingMode: String,
|
||||||
|
val billingCheckinTime: String,
|
||||||
|
val billingCheckoutTime: String,
|
||||||
val guestId: UUID?,
|
val guestId: UUID?,
|
||||||
val checkInAt: String?,
|
val checkInAt: String?,
|
||||||
val expectedCheckInAt: String?,
|
val expectedCheckInAt: String?,
|
||||||
@@ -53,6 +59,9 @@ data class BookingListItem(
|
|||||||
val guestPhone: String?,
|
val guestPhone: String?,
|
||||||
val roomNumbers: List<Int>,
|
val roomNumbers: List<Int>,
|
||||||
val source: String?,
|
val source: String?,
|
||||||
|
val billingMode: String,
|
||||||
|
val billingCheckinTime: String,
|
||||||
|
val billingCheckoutTime: String,
|
||||||
val expectedCheckInAt: String?,
|
val expectedCheckInAt: String?,
|
||||||
val expectedCheckOutAt: String?,
|
val expectedCheckOutAt: String?,
|
||||||
val checkInAt: String?,
|
val checkInAt: String?,
|
||||||
@@ -80,6 +89,9 @@ data class BookingDetailResponse(
|
|||||||
val vehicleNumbers: List<String>,
|
val vehicleNumbers: List<String>,
|
||||||
val roomNumbers: List<Int>,
|
val roomNumbers: List<Int>,
|
||||||
val source: String?,
|
val source: String?,
|
||||||
|
val billingMode: String,
|
||||||
|
val billingCheckinTime: String,
|
||||||
|
val billingCheckoutTime: String,
|
||||||
val fromCity: String?,
|
val fromCity: String?,
|
||||||
val toCity: String?,
|
val toCity: String?,
|
||||||
val memberRelation: String?,
|
val memberRelation: String?,
|
||||||
@@ -112,6 +124,12 @@ data class BookingExpectedDatesUpdateRequest(
|
|||||||
val expectedCheckOutAt: String? = null
|
val expectedCheckOutAt: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class BookingBillingPolicyUpdateRequest(
|
||||||
|
val billingMode: String,
|
||||||
|
val billingCheckinTime: String? = null,
|
||||||
|
val billingCheckoutTime: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
data class BookingCheckOutRequest(
|
data class BookingCheckOutRequest(
|
||||||
val checkOutAt: String? = null,
|
val checkOutAt: String? = null,
|
||||||
val notes: String? = null
|
val notes: String? = null
|
||||||
|
|||||||
@@ -49,6 +49,16 @@ class Booking(
|
|||||||
@Column(name = "expected_checkout_at", columnDefinition = "timestamptz")
|
@Column(name = "expected_checkout_at", columnDefinition = "timestamptz")
|
||||||
var expectedCheckoutAt: OffsetDateTime? = null,
|
var expectedCheckoutAt: OffsetDateTime? = null,
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "billing_mode", nullable = false)
|
||||||
|
var billingMode: BookingBillingMode = BookingBillingMode.PROPERTY_POLICY,
|
||||||
|
|
||||||
|
@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 = "email_audit_pdf_url")
|
@Column(name = "email_audit_pdf_url")
|
||||||
var emailAuditPdfUrl: String? = null,
|
var emailAuditPdfUrl: String? = null,
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.android.trisolarisserver.models.booking
|
||||||
|
|
||||||
|
enum class BookingBillingMode {
|
||||||
|
PROPERTY_POLICY,
|
||||||
|
CUSTOM_WINDOW,
|
||||||
|
FULL_24H
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.android.trisolarisserver.models.booking
|
||||||
|
|
||||||
|
import com.android.trisolarisserver.models.property.AppUser
|
||||||
|
import com.android.trisolarisserver.models.property.Property
|
||||||
|
import jakarta.persistence.Column
|
||||||
|
import jakarta.persistence.Entity
|
||||||
|
import jakarta.persistence.EnumType
|
||||||
|
import jakarta.persistence.Enumerated
|
||||||
|
import jakarta.persistence.FetchType
|
||||||
|
import jakarta.persistence.GeneratedValue
|
||||||
|
import jakarta.persistence.Id
|
||||||
|
import jakarta.persistence.JoinColumn
|
||||||
|
import jakarta.persistence.ManyToOne
|
||||||
|
import jakarta.persistence.Table
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "booking_billing_policy_audit_log")
|
||||||
|
class BookingBillingPolicyAuditLog(
|
||||||
|
@Id
|
||||||
|
@GeneratedValue
|
||||||
|
@Column(columnDefinition = "uuid")
|
||||||
|
val id: UUID? = null,
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
@JoinColumn(name = "property_id", nullable = false)
|
||||||
|
var property: Property,
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
@JoinColumn(name = "booking_id", nullable = false)
|
||||||
|
var booking: Booking,
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "actor_user_id")
|
||||||
|
var actor: AppUser? = null,
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "old_billing_mode", nullable = false)
|
||||||
|
var oldBillingMode: BookingBillingMode,
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "new_billing_mode", nullable = false)
|
||||||
|
var newBillingMode: BookingBillingMode,
|
||||||
|
|
||||||
|
@Column(name = "old_billing_checkin_time", nullable = false)
|
||||||
|
var oldBillingCheckinTime: String,
|
||||||
|
|
||||||
|
@Column(name = "new_billing_checkin_time", nullable = false)
|
||||||
|
var newBillingCheckinTime: String,
|
||||||
|
|
||||||
|
@Column(name = "old_billing_checkout_time", nullable = false)
|
||||||
|
var oldBillingCheckoutTime: String,
|
||||||
|
|
||||||
|
@Column(name = "new_billing_checkout_time", nullable = false)
|
||||||
|
var newBillingCheckoutTime: String,
|
||||||
|
|
||||||
|
@Column(name = "event", nullable = false)
|
||||||
|
var event: String = "POLICY_MODIFIED",
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false, columnDefinition = "timestamptz")
|
||||||
|
val createdAt: OffsetDateTime = OffsetDateTime.now()
|
||||||
|
)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.android.trisolarisserver.repo.booking
|
||||||
|
|
||||||
|
import com.android.trisolarisserver.models.booking.BookingBillingPolicyAuditLog
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
interface BookingBillingPolicyAuditLogRepo : JpaRepository<BookingBillingPolicyAuditLog, UUID>
|
||||||
@@ -6,6 +6,7 @@ import com.android.trisolarisserver.repo.booking.BookingRepo
|
|||||||
import com.android.trisolarisserver.repo.guest.GuestRepo
|
import com.android.trisolarisserver.repo.guest.GuestRepo
|
||||||
import com.android.trisolarisserver.repo.email.InboundEmailRepo
|
import com.android.trisolarisserver.repo.email.InboundEmailRepo
|
||||||
import com.android.trisolarisserver.models.booking.Booking
|
import com.android.trisolarisserver.models.booking.Booking
|
||||||
|
import com.android.trisolarisserver.models.booking.BookingBillingMode
|
||||||
import com.android.trisolarisserver.models.booking.BookingStatus
|
import com.android.trisolarisserver.models.booking.BookingStatus
|
||||||
import com.android.trisolarisserver.models.booking.Guest
|
import com.android.trisolarisserver.models.booking.Guest
|
||||||
import com.android.trisolarisserver.models.booking.InboundEmail
|
import com.android.trisolarisserver.models.booking.InboundEmail
|
||||||
@@ -186,6 +187,9 @@ class EmailIngestionService(
|
|||||||
sourceBookingId = sourceBookingId,
|
sourceBookingId = sourceBookingId,
|
||||||
expectedCheckinAt = checkin,
|
expectedCheckinAt = checkin,
|
||||||
expectedCheckoutAt = checkout,
|
expectedCheckoutAt = checkout,
|
||||||
|
billingMode = BookingBillingMode.PROPERTY_POLICY,
|
||||||
|
billingCheckinTime = property.billingCheckinTime,
|
||||||
|
billingCheckoutTime = property.billingCheckoutTime,
|
||||||
emailAuditPdfUrl = emailAuditPdfUrl
|
emailAuditPdfUrl = emailAuditPdfUrl
|
||||||
)
|
)
|
||||||
return bookingRepo.save(booking)
|
return bookingRepo.save(booking)
|
||||||
|
|||||||
Reference in New Issue
Block a user