Add room-stay void/checkout controls and audit logs
All checks were successful
build-and-deploy / build-deploy (push) Successful in 39s
All checks were successful
build-and-deploy / build-deploy (push) Successful in 39s
This commit is contained in:
@@ -196,6 +196,10 @@ Notes / constraints
|
||||
- Property code is auto-generated (7-char random, no fixed prefix). Property create no longer accepts `code` in request. Join-by-code uses property code, not propertyId.
|
||||
- Property access codes: 6-digit PIN, 1-minute expiry, single-use. Admin generates; staff joins with property code + PIN.
|
||||
- Property user disable is property-scoped (not global); hierarchy applies for who can disable.
|
||||
- Room stay lifecycle: `RoomStay` now supports soft void (`is_voided`), and room-stay audit events are written to `room_stay_audit_log`.
|
||||
- Checkout supports both booking-level and specific room-stay checkout; specific checkout endpoint: `POST /properties/{propertyId}/bookings/{bookingId}/room-stays/{roomStayId}/check-out`.
|
||||
- Staff can change/void stays only before first payment on booking; manager/admin can act after payments.
|
||||
- Checkout validation: nightly rate must be within +/-20% of room type default rate (when default rate exists), and minimum stay duration must be at least 1 hour.
|
||||
|
||||
Operational notes
|
||||
- Payment provider migrated: PayU removed; Razorpay now used for settings, QR, payment links, and webhooks.
|
||||
|
||||
@@ -27,6 +27,7 @@ import com.android.trisolarisserver.repo.guest.GuestRatingRepo
|
||||
import com.android.trisolarisserver.models.booking.BookingStatus
|
||||
import com.android.trisolarisserver.models.booking.MemberRelation
|
||||
import com.android.trisolarisserver.models.booking.TransportMode
|
||||
import com.android.trisolarisserver.models.room.RoomStayAuditLog
|
||||
import com.android.trisolarisserver.models.room.RoomStay
|
||||
import com.android.trisolarisserver.models.room.RateSource
|
||||
import com.android.trisolarisserver.models.property.Role
|
||||
@@ -35,6 +36,7 @@ import com.android.trisolarisserver.repo.guest.GuestVehicleRepo
|
||||
import com.android.trisolarisserver.repo.booking.PaymentRepo
|
||||
import com.android.trisolarisserver.repo.property.PropertyRepo
|
||||
import com.android.trisolarisserver.repo.room.RoomRepo
|
||||
import com.android.trisolarisserver.repo.room.RoomStayAuditLogRepo
|
||||
import com.android.trisolarisserver.repo.room.RoomStayRepo
|
||||
import com.android.trisolarisserver.security.MyPrincipal
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
@@ -69,7 +71,8 @@ class BookingFlow(
|
||||
private val guestRatingRepo: GuestRatingRepo,
|
||||
private val guestDocumentRepo: GuestDocumentRepo,
|
||||
private val paymentRepo: PaymentRepo,
|
||||
private val bookingSnapshotBuilder: BookingSnapshotBuilder
|
||||
private val bookingSnapshotBuilder: BookingSnapshotBuilder,
|
||||
private val roomStayAuditLogRepo: RoomStayAuditLogRepo
|
||||
) {
|
||||
|
||||
@PostMapping
|
||||
@@ -511,7 +514,7 @@ class BookingFlow(
|
||||
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||
@RequestBody request: BookingCheckOutRequest
|
||||
) {
|
||||
requireActor(propertyId, principal)
|
||||
val actor = requireActor(propertyId, principal)
|
||||
val booking = requireBooking(propertyId, bookingId)
|
||||
if (booking.status != BookingStatus.CHECKED_IN) {
|
||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Booking not checked in")
|
||||
@@ -520,8 +523,24 @@ class BookingFlow(
|
||||
val checkOutAt = parseOffset(request.checkOutAt) ?: now
|
||||
|
||||
val stays = roomStayRepo.findActiveByBookingId(bookingId)
|
||||
if (stays.any { !isCheckoutAmountValid(it) || !isMinimumStayDurationValid(it, checkOutAt) }) {
|
||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Room stay amount is outside allowed range")
|
||||
}
|
||||
val oldStates = stays.associate { it.id!! to it.toAt }
|
||||
stays.forEach { it.toAt = checkOutAt }
|
||||
roomStayRepo.saveAll(stays)
|
||||
stays.forEach {
|
||||
logStayAudit(
|
||||
stay = it,
|
||||
action = "CHECK_OUT",
|
||||
actor = actor,
|
||||
oldToAt = oldStates[it.id],
|
||||
oldIsVoided = it.isVoided,
|
||||
newToAt = checkOutAt,
|
||||
newIsVoided = false,
|
||||
reason = request.notes
|
||||
)
|
||||
}
|
||||
|
||||
booking.status = BookingStatus.CHECKED_OUT
|
||||
booking.checkoutAt = checkOutAt
|
||||
@@ -532,6 +551,62 @@ class BookingFlow(
|
||||
bookingEvents.emit(propertyId, bookingId)
|
||||
}
|
||||
|
||||
@PostMapping("/{bookingId}/room-stays/{roomStayId}/check-out")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@Transactional
|
||||
fun checkOutRoomStay(
|
||||
@PathVariable propertyId: UUID,
|
||||
@PathVariable bookingId: UUID,
|
||||
@PathVariable roomStayId: UUID,
|
||||
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||
@RequestBody request: BookingCheckOutRequest
|
||||
) {
|
||||
val actor = requireActor(propertyId, principal)
|
||||
val booking = requireBooking(propertyId, bookingId)
|
||||
if (booking.status != BookingStatus.CHECKED_IN) {
|
||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Booking not checked in")
|
||||
}
|
||||
val stay = roomStayRepo.findByIdAndBookingId(roomStayId, bookingId)
|
||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room stay not found for booking")
|
||||
if (stay.toAt != null) {
|
||||
return
|
||||
}
|
||||
|
||||
val now = OffsetDateTime.now()
|
||||
val checkOutAt = parseOffset(request.checkOutAt) ?: now
|
||||
if (!isCheckoutAmountValid(stay)) {
|
||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Room stay amount is outside allowed range")
|
||||
}
|
||||
if (!isMinimumStayDurationValid(stay, checkOutAt)) {
|
||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Minimum stay duration is 1 hour")
|
||||
}
|
||||
|
||||
val oldToAt = stay.toAt
|
||||
val oldIsVoided = stay.isVoided
|
||||
stay.toAt = checkOutAt
|
||||
roomStayRepo.save(stay)
|
||||
logStayAudit(
|
||||
stay = stay,
|
||||
action = "CHECK_OUT",
|
||||
actor = actor,
|
||||
oldToAt = oldToAt,
|
||||
oldIsVoided = oldIsVoided,
|
||||
newToAt = checkOutAt,
|
||||
newIsVoided = false,
|
||||
reason = request.notes
|
||||
)
|
||||
|
||||
val remainingActive = roomStayRepo.findActiveByBookingId(bookingId)
|
||||
if (remainingActive.isEmpty()) {
|
||||
booking.status = BookingStatus.CHECKED_OUT
|
||||
booking.checkoutAt = checkOutAt
|
||||
booking.updatedAt = now
|
||||
bookingRepo.save(booking)
|
||||
}
|
||||
roomBoardEvents.emit(propertyId)
|
||||
bookingEvents.emit(propertyId, bookingId)
|
||||
}
|
||||
|
||||
private fun resolveGuestForBooking(
|
||||
propertyId: UUID,
|
||||
property: com.android.trisolarisserver.models.property.Property,
|
||||
@@ -706,6 +781,44 @@ class BookingFlow(
|
||||
}
|
||||
}
|
||||
|
||||
private fun isCheckoutAmountValid(stay: RoomStay): Boolean {
|
||||
val base = stay.room.roomType.defaultRate ?: return true
|
||||
val nightly = stay.nightlyRate ?: return false
|
||||
val low = (base * 80L) / 100L
|
||||
val high = (base * 120L) / 100L
|
||||
return nightly in low..high
|
||||
}
|
||||
|
||||
private fun isMinimumStayDurationValid(stay: RoomStay, checkOutAt: OffsetDateTime): Boolean {
|
||||
return java.time.Duration.between(stay.fromAt, checkOutAt).toMinutes() >= 60
|
||||
}
|
||||
|
||||
private fun logStayAudit(
|
||||
stay: RoomStay,
|
||||
action: String,
|
||||
actor: com.android.trisolarisserver.models.property.AppUser?,
|
||||
oldToAt: OffsetDateTime?,
|
||||
oldIsVoided: Boolean,
|
||||
newToAt: OffsetDateTime?,
|
||||
newIsVoided: Boolean,
|
||||
reason: String?
|
||||
) {
|
||||
roomStayAuditLogRepo.save(
|
||||
RoomStayAuditLog(
|
||||
property = stay.property,
|
||||
booking = stay.booking,
|
||||
roomStay = stay,
|
||||
action = action,
|
||||
oldToAt = oldToAt,
|
||||
newToAt = newToAt,
|
||||
oldIsVoided = oldIsVoided,
|
||||
newIsVoided = newIsVoided,
|
||||
reason = reason,
|
||||
actor = actor
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun isTransportModeAllowed(
|
||||
property: com.android.trisolarisserver.models.property.Property,
|
||||
mode: TransportMode
|
||||
|
||||
@@ -58,7 +58,7 @@ internal fun requireRoomStayForProperty(
|
||||
val stay = roomStayRepo.findById(roomStayId).orElseThrow {
|
||||
ResponseStatusException(HttpStatus.NOT_FOUND, "Room stay not found")
|
||||
}
|
||||
if (stay.property.id != propertyId) {
|
||||
if (stay.property.id != propertyId || stay.isVoided) {
|
||||
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room stay not found for property")
|
||||
}
|
||||
return stay
|
||||
@@ -108,6 +108,7 @@ internal fun computeExpectedPay(stays: List<RoomStay>, timezone: String?): Long
|
||||
val now = nowForProperty(timezone)
|
||||
var total = 0L
|
||||
stays.forEach { stay ->
|
||||
if (stay.isVoided) return@forEach
|
||||
val rate = stay.nightlyRate ?: 0L
|
||||
if (rate == 0L) return@forEach
|
||||
val start = stay.fromAt.toLocalDate()
|
||||
@@ -128,6 +129,7 @@ internal fun computeExpectedPayTotal(
|
||||
val now = nowForProperty(timezone)
|
||||
var total = 0L
|
||||
stays.forEach { stay ->
|
||||
if (stay.isVoided) return@forEach
|
||||
val rate = stay.nightlyRate ?: 0L
|
||||
if (rate == 0L) return@forEach
|
||||
val start = stay.fromAt.toLocalDate()
|
||||
|
||||
@@ -161,6 +161,10 @@ data class RoomStayPreAssignRequest(
|
||||
val notes: String? = null
|
||||
)
|
||||
|
||||
data class RoomStayVoidRequest(
|
||||
val reason: String? = null
|
||||
)
|
||||
|
||||
data class IssueCardRequest(
|
||||
val cardId: String,
|
||||
val cardIndex: Int,
|
||||
|
||||
@@ -4,14 +4,19 @@ import com.android.trisolarisserver.controller.common.requireMember
|
||||
import com.android.trisolarisserver.controller.common.requireRoomStayForProperty
|
||||
|
||||
import com.android.trisolarisserver.component.auth.PropertyAccess
|
||||
import com.android.trisolarisserver.controller.dto.booking.RoomStayVoidRequest
|
||||
import com.android.trisolarisserver.controller.dto.room.ActiveRoomStayResponse
|
||||
import com.android.trisolarisserver.controller.dto.rate.RoomStayRateChangeRequest
|
||||
import com.android.trisolarisserver.controller.dto.rate.RoomStayRateChangeResponse
|
||||
import com.android.trisolarisserver.models.property.Role
|
||||
import com.android.trisolarisserver.models.room.RateSource
|
||||
import com.android.trisolarisserver.models.room.RoomStayAuditLog
|
||||
import com.android.trisolarisserver.models.room.RoomStay
|
||||
import com.android.trisolarisserver.repo.booking.BookingRepo
|
||||
import com.android.trisolarisserver.repo.booking.PaymentRepo
|
||||
import com.android.trisolarisserver.repo.property.AppUserRepo
|
||||
import com.android.trisolarisserver.repo.property.PropertyUserRepo
|
||||
import com.android.trisolarisserver.repo.room.RoomStayAuditLogRepo
|
||||
import com.android.trisolarisserver.repo.room.RoomStayRepo
|
||||
import com.android.trisolarisserver.security.MyPrincipal
|
||||
import org.springframework.http.HttpStatus
|
||||
@@ -29,7 +34,10 @@ import java.util.UUID
|
||||
class RoomStays(
|
||||
private val propertyAccess: PropertyAccess,
|
||||
private val propertyUserRepo: PropertyUserRepo,
|
||||
private val appUserRepo: AppUserRepo,
|
||||
private val bookingRepo: BookingRepo,
|
||||
private val paymentRepo: PaymentRepo,
|
||||
private val roomStayAuditLogRepo: RoomStayAuditLogRepo,
|
||||
private val roomStayRepo: RoomStayRepo
|
||||
) {
|
||||
|
||||
@@ -110,6 +118,61 @@ class RoomStays(
|
||||
)
|
||||
}
|
||||
|
||||
@PostMapping("/properties/{propertyId}/room-stays/{roomStayId}/void")
|
||||
fun voidRoomStay(
|
||||
@PathVariable propertyId: UUID,
|
||||
@PathVariable roomStayId: UUID,
|
||||
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||
@RequestBody request: RoomStayVoidRequest
|
||||
) {
|
||||
val actor = requireMember(propertyAccess, propertyId, principal)
|
||||
val stay = requireRoomStayForProperty(roomStayRepo, propertyId, roomStayId)
|
||||
if (stay.isVoided) return
|
||||
if (stay.toAt != null) {
|
||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Cannot void checked-out room stay")
|
||||
}
|
||||
val roles = propertyUserRepo.findRolesByPropertyAndUser(propertyId, actor.userId)
|
||||
val hasPrivilegedRole = roles.contains(Role.ADMIN) || roles.contains(Role.MANAGER)
|
||||
val hasStaffRole = roles.contains(Role.STAFF)
|
||||
if (!hasPrivilegedRole && !hasStaffRole) {
|
||||
throw ResponseStatusException(HttpStatus.FORBIDDEN, "Missing role")
|
||||
}
|
||||
if (!hasPrivilegedRole && paymentRepo.existsByBookingId(stay.booking.id!!)) {
|
||||
throw ResponseStatusException(HttpStatus.FORBIDDEN, "Cannot void stay after first payment")
|
||||
}
|
||||
|
||||
val appUser = appUserRepo.findById(actor.userId).orElse(null)
|
||||
val oldToAt = stay.toAt
|
||||
stay.isVoided = true
|
||||
stay.toAt = OffsetDateTime.now()
|
||||
roomStayRepo.save(stay)
|
||||
roomStayAuditLogRepo.save(
|
||||
RoomStayAuditLog(
|
||||
property = stay.property,
|
||||
booking = stay.booking,
|
||||
roomStay = stay,
|
||||
action = "VOID",
|
||||
oldToAt = oldToAt,
|
||||
newToAt = stay.toAt,
|
||||
oldIsVoided = false,
|
||||
newIsVoided = true,
|
||||
reason = request.reason,
|
||||
actor = appUser
|
||||
)
|
||||
)
|
||||
|
||||
val booking = bookingRepo.findById(stay.booking.id!!).orElse(null)
|
||||
if (booking != null && booking.status == com.android.trisolarisserver.models.booking.BookingStatus.CHECKED_IN) {
|
||||
val active = roomStayRepo.findActiveByBookingId(booking.id!!)
|
||||
if (active.isEmpty()) {
|
||||
booking.status = com.android.trisolarisserver.models.booking.BookingStatus.CHECKED_OUT
|
||||
booking.checkoutAt = OffsetDateTime.now()
|
||||
booking.updatedAt = OffsetDateTime.now()
|
||||
bookingRepo.save(booking)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun splitStay(stay: RoomStay, effectiveAt: OffsetDateTime, request: RoomStayRateChangeRequest): RoomStay {
|
||||
val oldToAt = stay.toAt
|
||||
stay.toAt = effectiveAt
|
||||
|
||||
@@ -33,6 +33,9 @@ class RoomStay(
|
||||
@Column(name = "to_at", columnDefinition = "timestamptz")
|
||||
var toAt: OffsetDateTime? = null, // null = active
|
||||
|
||||
@Column(name = "is_voided", nullable = false)
|
||||
var isVoided: Boolean = false,
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "rate_source")
|
||||
var rateSource: RateSource? = null,
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.android.trisolarisserver.models.room
|
||||
|
||||
import com.android.trisolarisserver.models.booking.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.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 = "room_stay_audit_log")
|
||||
class RoomStayAuditLog(
|
||||
@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, optional = false)
|
||||
@JoinColumn(name = "room_stay_id", nullable = false)
|
||||
var roomStay: RoomStay,
|
||||
|
||||
@Column(name = "action", nullable = false)
|
||||
var action: String,
|
||||
|
||||
@Column(name = "old_to_at", columnDefinition = "timestamptz")
|
||||
var oldToAt: OffsetDateTime? = null,
|
||||
|
||||
@Column(name = "new_to_at", columnDefinition = "timestamptz")
|
||||
var newToAt: OffsetDateTime? = null,
|
||||
|
||||
@Column(name = "old_is_voided", nullable = false)
|
||||
var oldIsVoided: Boolean = false,
|
||||
|
||||
@Column(name = "new_is_voided", nullable = false)
|
||||
var newIsVoided: Boolean = false,
|
||||
|
||||
@Column(name = "reason")
|
||||
var reason: String? = null,
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "actor_user_id")
|
||||
var actor: AppUser? = null,
|
||||
|
||||
@Column(name = "created_at", nullable = false, columnDefinition = "timestamptz")
|
||||
val createdAt: OffsetDateTime = OffsetDateTime.now()
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.android.trisolarisserver.repo.room
|
||||
|
||||
import com.android.trisolarisserver.models.room.RoomStayAuditLog
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import java.util.UUID
|
||||
|
||||
interface RoomStayAuditLogRepo : JpaRepository<RoomStayAuditLog, UUID>
|
||||
@@ -12,6 +12,7 @@ interface RoomStayRepo : JpaRepository<RoomStay, UUID> {
|
||||
from RoomStay rs
|
||||
where rs.property.id = :propertyId
|
||||
and rs.toAt is null
|
||||
and rs.isVoided = false
|
||||
""")
|
||||
fun findOccupiedRoomIds(@Param("propertyId") propertyId: UUID): List<UUID>
|
||||
|
||||
@@ -19,6 +20,7 @@ interface RoomStayRepo : JpaRepository<RoomStay, UUID> {
|
||||
select distinct rs.room.id
|
||||
from RoomStay rs
|
||||
where rs.property.id = :propertyId
|
||||
and rs.isVoided = false
|
||||
and rs.fromAt < :toAt
|
||||
and (rs.toAt is null or rs.toAt > :fromAt)
|
||||
""")
|
||||
@@ -33,6 +35,7 @@ interface RoomStayRepo : JpaRepository<RoomStay, UUID> {
|
||||
from RoomStay rs
|
||||
where rs.booking.id = :bookingId
|
||||
and rs.toAt is null
|
||||
and rs.isVoided = false
|
||||
""")
|
||||
fun findActiveByBookingId(@Param("bookingId") bookingId: UUID): List<RoomStay>
|
||||
|
||||
@@ -40,6 +43,7 @@ interface RoomStayRepo : JpaRepository<RoomStay, UUID> {
|
||||
select rs
|
||||
from RoomStay rs
|
||||
where rs.booking.id = :bookingId
|
||||
and rs.isVoided = false
|
||||
""")
|
||||
fun findByBookingId(@Param("bookingId") bookingId: UUID): List<RoomStay>
|
||||
|
||||
@@ -48,6 +52,7 @@ interface RoomStayRepo : JpaRepository<RoomStay, UUID> {
|
||||
from RoomStay rs
|
||||
join fetch rs.room r
|
||||
where rs.booking.id = :bookingId
|
||||
and rs.isVoided = false
|
||||
""")
|
||||
fun findByBookingIdWithRoom(@Param("bookingId") bookingId: UUID): List<RoomStay>
|
||||
|
||||
@@ -55,6 +60,7 @@ interface RoomStayRepo : JpaRepository<RoomStay, UUID> {
|
||||
select rs
|
||||
from RoomStay rs
|
||||
where rs.booking.id in :bookingIds
|
||||
and rs.isVoided = false
|
||||
""")
|
||||
fun findByBookingIdIn(@Param("bookingIds") bookingIds: List<UUID>): List<RoomStay>
|
||||
|
||||
@@ -64,6 +70,7 @@ interface RoomStayRepo : JpaRepository<RoomStay, UUID> {
|
||||
where rs.property.id = :propertyId
|
||||
and rs.room.id in :roomIds
|
||||
and rs.toAt is null
|
||||
and rs.isVoided = false
|
||||
""")
|
||||
fun findActiveRoomIds(
|
||||
@Param("propertyId") propertyId: UUID,
|
||||
@@ -75,6 +82,7 @@ interface RoomStayRepo : JpaRepository<RoomStay, UUID> {
|
||||
from RoomStay rs
|
||||
where rs.property.id = :propertyId
|
||||
and rs.room.id = :roomId
|
||||
and rs.isVoided = false
|
||||
and rs.fromAt < :toAt
|
||||
and (rs.toAt is null or rs.toAt > :fromAt)
|
||||
""")
|
||||
@@ -94,6 +102,7 @@ interface RoomStayRepo : JpaRepository<RoomStay, UUID> {
|
||||
left join fetch b.primaryGuest g
|
||||
where rs.property.id = :propertyId
|
||||
and rs.toAt is null
|
||||
and rs.isVoided = false
|
||||
order by r.roomNumber
|
||||
""")
|
||||
fun findActiveByPropertyIdWithDetails(@Param("propertyId") propertyId: UUID): List<RoomStay>
|
||||
@@ -111,10 +120,25 @@ interface RoomStayRepo : JpaRepository<RoomStay, UUID> {
|
||||
join rs.room r
|
||||
where rs.booking.id in :bookingIds
|
||||
and rs.toAt is null
|
||||
and rs.isVoided = false
|
||||
""")
|
||||
fun findActiveRoomNumbersByBookingIds(
|
||||
@Param("bookingIds") bookingIds: List<UUID>
|
||||
): List<BookingRoomNumberRow>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
select rs
|
||||
from RoomStay rs
|
||||
where rs.id = :roomStayId
|
||||
and rs.booking.id = :bookingId
|
||||
and rs.isVoided = false
|
||||
"""
|
||||
)
|
||||
fun findByIdAndBookingId(
|
||||
@Param("roomStayId") roomStayId: UUID,
|
||||
@Param("bookingId") bookingId: UUID
|
||||
): RoomStay?
|
||||
}
|
||||
|
||||
interface BookingRoomNumberRow {
|
||||
|
||||
Reference in New Issue
Block a user