Add expected checkout preview API using property billing policy defaults
All checks were successful
build-and-deploy / build-deploy (push) Successful in 36s

This commit is contained in:
androidlover5842
2026-02-04 14:27:15 +05:30
parent 002f11240a
commit 59a50d4313
3 changed files with 158 additions and 0 deletions

View File

@@ -1655,6 +1655,53 @@ BOOKING APIS
- 404 Not Found
- Preview expected checkout API is this one:
POST /properties/{propertyId}/bookings/expected-checkout-preview
What it does:
- Computes expected check-out timestamp from check-in + billing policy.
- `checkInAt` is required.
- `billableNights` is optional (defaults to 1).
- `billingMode` is optional (defaults to property policy mode behavior).
- `billingCheckinTime` and `billingCheckoutTime` are optional; defaults come from property billing policy.
- `propertyId` is required in path.
Request body (BookingExpectedCheckoutPreviewRequest):
{
"checkInAt": "2026-02-05T10:30:00+05:30",
"billableNights": 2,
"billingMode": "PROPERTY_POLICY",
"billingCheckinTime": "12:00",
"billingCheckoutTime": "11:00"
}
Response body (BookingExpectedCheckoutPreviewResponse):
{
"expectedCheckOutAt": "2026-02-07T11:00+05:30",
"billableNights": 2,
"billingMode": "PROPERTY_POLICY",
"billingCheckinTime": "12:00",
"billingCheckoutTime": "11:00"
}
- Allowed roles: ADMIN, MANAGER, STAFF, HOUSEKEEPING, FINANCE
Error Codes
- 400 Bad Request
- checkInAt required or invalid timestamp
- billableNights must be > 0
- Unknown billing mode
- billingCheckinTime/billingCheckoutTime invalid format
- 401 Unauthorized
- 403 Forbidden
- 404 Not Found (property)
- Booking stream API is this one:
GET /properties/{propertyId}/bookings/{bookingId}/stream

View File

@@ -12,6 +12,8 @@ import com.android.trisolarisserver.component.room.RoomBoardEvents
import com.android.trisolarisserver.controller.dto.booking.BookingCancelRequest
import com.android.trisolarisserver.controller.dto.booking.BookingBillableNightsRequest
import com.android.trisolarisserver.controller.dto.booking.BookingBillableNightsResponse
import com.android.trisolarisserver.controller.dto.booking.BookingExpectedCheckoutPreviewRequest
import com.android.trisolarisserver.controller.dto.booking.BookingExpectedCheckoutPreviewResponse
import com.android.trisolarisserver.controller.dto.booking.BookingBulkCheckInRequest
import com.android.trisolarisserver.controller.dto.booking.BookingCheckOutRequest
import com.android.trisolarisserver.controller.dto.booking.BookingCreateRequest
@@ -66,6 +68,7 @@ import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ResponseStatusException
import java.time.LocalTime
import java.time.OffsetDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException
import kotlin.math.abs
@@ -339,6 +342,60 @@ class BookingFlow(
)
}
@PostMapping("/expected-checkout-preview")
@Transactional(readOnly = true)
fun previewExpectedCheckout(
@PathVariable propertyId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?,
@RequestBody request: BookingExpectedCheckoutPreviewRequest
): BookingExpectedCheckoutPreviewResponse {
requireRole(
propertyAccess,
propertyId,
principal,
Role.ADMIN,
Role.MANAGER,
Role.STAFF,
Role.HOUSEKEEPING,
Role.FINANCE
)
val property = propertyRepo.findById(propertyId).orElseThrow {
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
}
val checkInAt = parseOffset(request.checkInAt)
?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "checkInAt required")
val nights = request.billableNights ?: 1L
if (nights <= 0L) {
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "billableNights must be > 0")
}
val mode = parseBillingMode(request.billingMode)
val billingCheckinTime = when (mode) {
BookingBillingMode.FULL_24H -> "00:00"
else -> normalizeBillingTime(request.billingCheckinTime ?: property.billingCheckinTime, "billingCheckinTime")
}
val billingCheckoutTime = when (mode) {
BookingBillingMode.FULL_24H -> "23:59"
else -> normalizeBillingTime(request.billingCheckoutTime ?: property.billingCheckoutTime, "billingCheckoutTime")
}
val expectedCheckoutAt = computeExpectedCheckoutAtFromPolicy(
checkInAt = checkInAt,
timezone = property.timezone,
billableNights = nights,
billingMode = mode,
billingCheckinTime = billingCheckinTime,
billingCheckoutTime = billingCheckoutTime
)
return BookingExpectedCheckoutPreviewResponse(
expectedCheckOutAt = expectedCheckoutAt.toString(),
billableNights = nights,
billingMode = mode.name,
billingCheckinTime = billingCheckinTime,
billingCheckoutTime = billingCheckoutTime
)
}
@GetMapping("/{bookingId}/stream")
fun streamBooking(
@PathVariable propertyId: UUID,
@@ -831,6 +888,44 @@ class BookingFlow(
}
}
private fun computeExpectedCheckoutAtFromPolicy(
checkInAt: OffsetDateTime,
timezone: String?,
billableNights: Long,
billingMode: BookingBillingMode,
billingCheckinTime: String,
billingCheckoutTime: String
): OffsetDateTime {
if (billingMode == BookingBillingMode.FULL_24H) {
return checkInAt.plusDays(billableNights)
}
val zone = resolveZoneId(timezone)
val localCheckIn = checkInAt.atZoneSameInstant(zone)
val checkinPolicy = LocalTime.parse(billingCheckinTime, BILLING_TIME_FORMATTER)
val checkoutPolicy = LocalTime.parse(billingCheckoutTime, BILLING_TIME_FORMATTER)
val billStartDate = if (localCheckIn.toLocalTime().isBefore(checkinPolicy)) {
localCheckIn.toLocalDate().minusDays(1)
} else {
localCheckIn.toLocalDate()
}
val targetDate = billStartDate.plusDays(billableNights)
val targetCheckout = java.time.ZonedDateTime.of(targetDate, checkoutPolicy, zone).toOffsetDateTime()
if (targetCheckout.isAfter(checkInAt)) {
return targetCheckout
}
return targetCheckout.plusDays(1)
}
private fun resolveZoneId(timezone: String?): ZoneId {
return try {
if (timezone.isNullOrBlank()) ZoneId.of("Asia/Kolkata") else ZoneId.of(timezone)
} catch (_: Exception) {
ZoneId.of("Asia/Kolkata")
}
}
private fun parseTransportMode(value: String): TransportMode {
return try {
TransportMode.valueOf(value)

View File

@@ -145,6 +145,22 @@ data class BookingBillableNightsResponse(
val billableNights: Long
)
data class BookingExpectedCheckoutPreviewRequest(
val checkInAt: String,
val billableNights: Long? = null,
val billingMode: String? = null,
val billingCheckinTime: String? = null,
val billingCheckoutTime: String? = null
)
data class BookingExpectedCheckoutPreviewResponse(
val expectedCheckOutAt: String,
val billableNights: Long,
val billingMode: String,
val billingCheckinTime: String,
val billingCheckoutTime: String
)
data class BookingCancelRequest(
val cancelledAt: String? = null,
val reason: String? = null