Add admin/manager booking profile update API
All checks were successful
build-and-deploy / build-deploy (push) Successful in 37s
All checks were successful
build-and-deploy / build-deploy (push) Successful in 37s
This commit is contained in:
@@ -1879,6 +1879,56 @@ BOOKING APIS
|
|||||||
- 409 Conflict (booking closed)
|
- 409 Conflict (booking closed)
|
||||||
|
|
||||||
|
|
||||||
|
- Update booking profile API is this one:
|
||||||
|
|
||||||
|
POST /properties/{propertyId}/bookings/{bookingId}/profile
|
||||||
|
|
||||||
|
What it does:
|
||||||
|
|
||||||
|
- Updates booking profile fields at any booking stage (OPEN, CHECKED_IN, CHECKED_OUT, CANCELLED, NO_SHOW).
|
||||||
|
- Supports partial updates for these fields only:
|
||||||
|
- transportMode
|
||||||
|
- childCount
|
||||||
|
- maleCount
|
||||||
|
- femaleCount
|
||||||
|
- fromCity
|
||||||
|
- toCity
|
||||||
|
- memberRelation
|
||||||
|
- If a provided field value is null, that field is cleared.
|
||||||
|
- Recomputes adultCount and totalGuestCount when any of maleCount/femaleCount/childCount is updated.
|
||||||
|
|
||||||
|
Request body:
|
||||||
|
|
||||||
|
{
|
||||||
|
"transportMode": "CAR",
|
||||||
|
"childCount": 1,
|
||||||
|
"maleCount": 1,
|
||||||
|
"femaleCount": 1,
|
||||||
|
"fromCity": "Varanasi",
|
||||||
|
"toCity": "Lucknow",
|
||||||
|
"memberRelation": "SELF"
|
||||||
|
}
|
||||||
|
|
||||||
|
- Allowed roles: ADMIN, MANAGER
|
||||||
|
- superAdmin can also access.
|
||||||
|
|
||||||
|
Error Codes
|
||||||
|
|
||||||
|
- 400 Bad Request
|
||||||
|
- Invalid request body
|
||||||
|
- Unknown fields
|
||||||
|
- At least one field is required
|
||||||
|
- Unknown transport mode
|
||||||
|
- Transport mode disabled
|
||||||
|
- Unknown member relation
|
||||||
|
- childCount/maleCount/femaleCount must be integer or null
|
||||||
|
- childCount/maleCount/femaleCount must be >= 0
|
||||||
|
- fromCity/toCity/memberRelation/transportMode type invalid
|
||||||
|
- 401 Unauthorized
|
||||||
|
- 403 Forbidden
|
||||||
|
- 404 Not Found
|
||||||
|
|
||||||
|
|
||||||
- Booking check-out API is this one:
|
- Booking check-out API is this one:
|
||||||
|
|
||||||
POST /properties/{propertyId}/bookings/{bookingId}/check-out
|
POST /properties/{propertyId}/bookings/{bookingId}/check-out
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ import com.android.trisolarisserver.repo.room.RoomRepo
|
|||||||
import com.android.trisolarisserver.repo.room.RoomStayAuditLogRepo
|
import com.android.trisolarisserver.repo.room.RoomStayAuditLogRepo
|
||||||
import com.android.trisolarisserver.repo.room.RoomStayRepo
|
import com.android.trisolarisserver.repo.room.RoomStayRepo
|
||||||
import com.android.trisolarisserver.security.MyPrincipal
|
import com.android.trisolarisserver.security.MyPrincipal
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode
|
||||||
import jakarta.servlet.http.HttpServletResponse
|
import jakarta.servlet.http.HttpServletResponse
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||||
@@ -635,6 +636,87 @@ class BookingFlow(
|
|||||||
bookingEvents.emit(propertyId, bookingId)
|
bookingEvents.emit(propertyId, bookingId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{bookingId}/profile")
|
||||||
|
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||||
|
@Transactional
|
||||||
|
fun updateBookingProfile(
|
||||||
|
@PathVariable propertyId: UUID,
|
||||||
|
@PathVariable bookingId: UUID,
|
||||||
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||||
|
@RequestBody request: JsonNode
|
||||||
|
) {
|
||||||
|
requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER)
|
||||||
|
if (!request.isObject) {
|
||||||
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
val allowedFields = setOf(
|
||||||
|
"transportMode",
|
||||||
|
"childCount",
|
||||||
|
"maleCount",
|
||||||
|
"femaleCount",
|
||||||
|
"fromCity",
|
||||||
|
"toCity",
|
||||||
|
"memberRelation"
|
||||||
|
)
|
||||||
|
val fieldNames = request.fieldNames().asSequence().toList()
|
||||||
|
val unknownFields = fieldNames.filter { it !in allowedFields }
|
||||||
|
if (unknownFields.isNotEmpty()) {
|
||||||
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown fields: ${unknownFields.joinToString(",")}")
|
||||||
|
}
|
||||||
|
if (fieldNames.isEmpty()) {
|
||||||
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "At least one field is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
val booking = requireBooking(propertyId, bookingId)
|
||||||
|
|
||||||
|
if (request.has("transportMode")) {
|
||||||
|
val transportNode = request.get("transportMode")
|
||||||
|
booking.transportMode = parseNullableTransportMode(booking, transportNode)
|
||||||
|
}
|
||||||
|
if (request.has("fromCity")) {
|
||||||
|
booking.fromCity = parseNullableText(request.get("fromCity"), "fromCity")
|
||||||
|
}
|
||||||
|
if (request.has("toCity")) {
|
||||||
|
booking.toCity = parseNullableText(request.get("toCity"), "toCity")
|
||||||
|
}
|
||||||
|
if (request.has("memberRelation")) {
|
||||||
|
booking.memberRelation = parseNullableMemberRelation(request.get("memberRelation"))
|
||||||
|
}
|
||||||
|
|
||||||
|
val hasGuestCountPatch = request.has("maleCount") || request.has("femaleCount") || request.has("childCount")
|
||||||
|
if (hasGuestCountPatch) {
|
||||||
|
val maleCount = if (request.has("maleCount")) {
|
||||||
|
parseNullableNonNegativeInt(request.get("maleCount"), "maleCount")
|
||||||
|
} else {
|
||||||
|
booking.maleCount
|
||||||
|
}
|
||||||
|
val femaleCount = if (request.has("femaleCount")) {
|
||||||
|
parseNullableNonNegativeInt(request.get("femaleCount"), "femaleCount")
|
||||||
|
} else {
|
||||||
|
booking.femaleCount
|
||||||
|
}
|
||||||
|
val childCount = if (request.has("childCount")) {
|
||||||
|
parseNullableNonNegativeInt(request.get("childCount"), "childCount")
|
||||||
|
} else {
|
||||||
|
booking.childCount
|
||||||
|
}
|
||||||
|
val hasAnyGuestCount = maleCount != null || femaleCount != null || childCount != null
|
||||||
|
val adultCount = if (hasAnyGuestCount) (maleCount ?: 0) + (femaleCount ?: 0) else null
|
||||||
|
val totalGuestCount = if (hasAnyGuestCount) (maleCount ?: 0) + (femaleCount ?: 0) + (childCount ?: 0) else null
|
||||||
|
|
||||||
|
booking.maleCount = maleCount
|
||||||
|
booking.femaleCount = femaleCount
|
||||||
|
booking.childCount = childCount
|
||||||
|
booking.adultCount = adultCount
|
||||||
|
booking.totalGuestCount = totalGuestCount
|
||||||
|
}
|
||||||
|
|
||||||
|
booking.updatedAt = OffsetDateTime.now()
|
||||||
|
bookingRepo.save(booking)
|
||||||
|
bookingEvents.emit(propertyId, bookingId)
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/{bookingId}/check-out")
|
@PostMapping("/{bookingId}/check-out")
|
||||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -952,6 +1034,23 @@ class BookingFlow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun parseNullableTransportMode(
|
||||||
|
booking: com.android.trisolarisserver.models.booking.Booking,
|
||||||
|
node: JsonNode?
|
||||||
|
): TransportMode? {
|
||||||
|
if (node == null || node.isNull) return null
|
||||||
|
if (!node.isTextual) {
|
||||||
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "transportMode must be string or null")
|
||||||
|
}
|
||||||
|
val raw = node.asText().trim()
|
||||||
|
if (raw.isBlank()) return null
|
||||||
|
val mode = parseTransportMode(raw.uppercase())
|
||||||
|
if (!isTransportModeAllowed(booking.property, mode)) {
|
||||||
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Transport mode disabled")
|
||||||
|
}
|
||||||
|
return mode
|
||||||
|
}
|
||||||
|
|
||||||
private fun parseMemberRelation(value: String?): MemberRelation? {
|
private fun parseMemberRelation(value: String?): MemberRelation? {
|
||||||
if (value.isNullOrBlank()) return null
|
if (value.isNullOrBlank()) return null
|
||||||
return try {
|
return try {
|
||||||
@@ -961,6 +1060,34 @@ class BookingFlow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun parseNullableMemberRelation(node: JsonNode?): MemberRelation? {
|
||||||
|
if (node == null || node.isNull) return null
|
||||||
|
if (!node.isTextual) {
|
||||||
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "memberRelation must be string or null")
|
||||||
|
}
|
||||||
|
return parseMemberRelation(node.asText())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseNullableText(node: JsonNode?, fieldName: String): String? {
|
||||||
|
if (node == null || node.isNull) return null
|
||||||
|
if (!node.isTextual) {
|
||||||
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "$fieldName must be string or null")
|
||||||
|
}
|
||||||
|
return node.asText().trim().ifBlank { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseNullableNonNegativeInt(node: JsonNode?, fieldName: String): Int? {
|
||||||
|
if (node == null || node.isNull) return null
|
||||||
|
if (!node.isIntegralNumber) {
|
||||||
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "$fieldName must be integer or null")
|
||||||
|
}
|
||||||
|
val value = node.asInt()
|
||||||
|
if (value < 0) {
|
||||||
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "$fieldName must be >= 0")
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
private fun parseRateSource(value: String?): RateSource? {
|
private fun parseRateSource(value: String?): RateSource? {
|
||||||
if (value.isNullOrBlank()) return null
|
if (value.isNullOrBlank()) return null
|
||||||
return try {
|
return try {
|
||||||
|
|||||||
Reference in New Issue
Block a user