Add room-stay void/checkout controls and audit logs
All checks were successful
build-and-deploy / build-deploy (push) Successful in 39s

This commit is contained in:
androidlover5842
2026-02-02 08:19:40 +05:30
parent 240e8fca25
commit e77ae6396e
9 changed files with 284 additions and 3 deletions

View File

@@ -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 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 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. - 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 Operational notes
- Payment provider migrated: PayU removed; Razorpay now used for settings, QR, payment links, and webhooks. - Payment provider migrated: PayU removed; Razorpay now used for settings, QR, payment links, and webhooks.

View File

@@ -27,6 +27,7 @@ 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.MemberRelation import com.android.trisolarisserver.models.booking.MemberRelation
import com.android.trisolarisserver.models.booking.TransportMode 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.RoomStay
import com.android.trisolarisserver.models.room.RateSource import com.android.trisolarisserver.models.room.RateSource
import com.android.trisolarisserver.models.property.Role 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.booking.PaymentRepo
import com.android.trisolarisserver.repo.property.PropertyRepo import com.android.trisolarisserver.repo.property.PropertyRepo
import com.android.trisolarisserver.repo.room.RoomRepo 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.repo.room.RoomStayRepo
import com.android.trisolarisserver.security.MyPrincipal import com.android.trisolarisserver.security.MyPrincipal
import jakarta.servlet.http.HttpServletResponse import jakarta.servlet.http.HttpServletResponse
@@ -69,7 +71,8 @@ class BookingFlow(
private val guestRatingRepo: GuestRatingRepo, private val guestRatingRepo: GuestRatingRepo,
private val guestDocumentRepo: GuestDocumentRepo, private val guestDocumentRepo: GuestDocumentRepo,
private val paymentRepo: PaymentRepo, private val paymentRepo: PaymentRepo,
private val bookingSnapshotBuilder: BookingSnapshotBuilder private val bookingSnapshotBuilder: BookingSnapshotBuilder,
private val roomStayAuditLogRepo: RoomStayAuditLogRepo
) { ) {
@PostMapping @PostMapping
@@ -511,7 +514,7 @@ class BookingFlow(
@AuthenticationPrincipal principal: MyPrincipal?, @AuthenticationPrincipal principal: MyPrincipal?,
@RequestBody request: BookingCheckOutRequest @RequestBody request: BookingCheckOutRequest
) { ) {
requireActor(propertyId, principal) val actor = requireActor(propertyId, principal)
val booking = requireBooking(propertyId, bookingId) val booking = requireBooking(propertyId, bookingId)
if (booking.status != BookingStatus.CHECKED_IN) { if (booking.status != BookingStatus.CHECKED_IN) {
throw ResponseStatusException(HttpStatus.CONFLICT, "Booking not checked in") throw ResponseStatusException(HttpStatus.CONFLICT, "Booking not checked in")
@@ -520,8 +523,24 @@ class BookingFlow(
val checkOutAt = parseOffset(request.checkOutAt) ?: now val checkOutAt = parseOffset(request.checkOutAt) ?: now
val stays = roomStayRepo.findActiveByBookingId(bookingId) 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 } stays.forEach { it.toAt = checkOutAt }
roomStayRepo.saveAll(stays) 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.status = BookingStatus.CHECKED_OUT
booking.checkoutAt = checkOutAt booking.checkoutAt = checkOutAt
@@ -532,6 +551,62 @@ class BookingFlow(
bookingEvents.emit(propertyId, bookingId) 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( private fun resolveGuestForBooking(
propertyId: UUID, propertyId: UUID,
property: com.android.trisolarisserver.models.property.Property, 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( private fun isTransportModeAllowed(
property: com.android.trisolarisserver.models.property.Property, property: com.android.trisolarisserver.models.property.Property,
mode: TransportMode mode: TransportMode

View File

@@ -58,7 +58,7 @@ internal fun requireRoomStayForProperty(
val stay = roomStayRepo.findById(roomStayId).orElseThrow { val stay = roomStayRepo.findById(roomStayId).orElseThrow {
ResponseStatusException(HttpStatus.NOT_FOUND, "Room stay not found") 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") throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room stay not found for property")
} }
return stay return stay
@@ -108,6 +108,7 @@ internal fun computeExpectedPay(stays: List<RoomStay>, timezone: String?): Long
val now = nowForProperty(timezone) val now = nowForProperty(timezone)
var total = 0L var total = 0L
stays.forEach { stay -> stays.forEach { stay ->
if (stay.isVoided) return@forEach
val rate = stay.nightlyRate ?: 0L val rate = stay.nightlyRate ?: 0L
if (rate == 0L) return@forEach if (rate == 0L) return@forEach
val start = stay.fromAt.toLocalDate() val start = stay.fromAt.toLocalDate()
@@ -128,6 +129,7 @@ internal fun computeExpectedPayTotal(
val now = nowForProperty(timezone) val now = nowForProperty(timezone)
var total = 0L var total = 0L
stays.forEach { stay -> stays.forEach { stay ->
if (stay.isVoided) return@forEach
val rate = stay.nightlyRate ?: 0L val rate = stay.nightlyRate ?: 0L
if (rate == 0L) return@forEach if (rate == 0L) return@forEach
val start = stay.fromAt.toLocalDate() val start = stay.fromAt.toLocalDate()

View File

@@ -161,6 +161,10 @@ data class RoomStayPreAssignRequest(
val notes: String? = null val notes: String? = null
) )
data class RoomStayVoidRequest(
val reason: String? = null
)
data class IssueCardRequest( data class IssueCardRequest(
val cardId: String, val cardId: String,
val cardIndex: Int, val cardIndex: Int,

View File

@@ -4,14 +4,19 @@ import com.android.trisolarisserver.controller.common.requireMember
import com.android.trisolarisserver.controller.common.requireRoomStayForProperty import com.android.trisolarisserver.controller.common.requireRoomStayForProperty
import com.android.trisolarisserver.component.auth.PropertyAccess 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.room.ActiveRoomStayResponse
import com.android.trisolarisserver.controller.dto.rate.RoomStayRateChangeRequest import com.android.trisolarisserver.controller.dto.rate.RoomStayRateChangeRequest
import com.android.trisolarisserver.controller.dto.rate.RoomStayRateChangeResponse import com.android.trisolarisserver.controller.dto.rate.RoomStayRateChangeResponse
import com.android.trisolarisserver.models.property.Role import com.android.trisolarisserver.models.property.Role
import com.android.trisolarisserver.models.room.RateSource 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.models.room.RoomStay
import com.android.trisolarisserver.repo.booking.BookingRepo
import com.android.trisolarisserver.repo.booking.PaymentRepo 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.property.PropertyUserRepo
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 org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
@@ -29,7 +34,10 @@ import java.util.UUID
class RoomStays( class RoomStays(
private val propertyAccess: PropertyAccess, private val propertyAccess: PropertyAccess,
private val propertyUserRepo: PropertyUserRepo, private val propertyUserRepo: PropertyUserRepo,
private val appUserRepo: AppUserRepo,
private val bookingRepo: BookingRepo,
private val paymentRepo: PaymentRepo, private val paymentRepo: PaymentRepo,
private val roomStayAuditLogRepo: RoomStayAuditLogRepo,
private val roomStayRepo: RoomStayRepo 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 { private fun splitStay(stay: RoomStay, effectiveAt: OffsetDateTime, request: RoomStayRateChangeRequest): RoomStay {
val oldToAt = stay.toAt val oldToAt = stay.toAt
stay.toAt = effectiveAt stay.toAt = effectiveAt

View File

@@ -33,6 +33,9 @@ class RoomStay(
@Column(name = "to_at", columnDefinition = "timestamptz") @Column(name = "to_at", columnDefinition = "timestamptz")
var toAt: OffsetDateTime? = null, // null = active var toAt: OffsetDateTime? = null, // null = active
@Column(name = "is_voided", nullable = false)
var isVoided: Boolean = false,
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
@Column(name = "rate_source") @Column(name = "rate_source")
var rateSource: RateSource? = null, var rateSource: RateSource? = null,

View File

@@ -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()
)

View File

@@ -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>

View File

@@ -12,6 +12,7 @@ interface RoomStayRepo : JpaRepository<RoomStay, UUID> {
from RoomStay rs from RoomStay rs
where rs.property.id = :propertyId where rs.property.id = :propertyId
and rs.toAt is null and rs.toAt is null
and rs.isVoided = false
""") """)
fun findOccupiedRoomIds(@Param("propertyId") propertyId: UUID): List<UUID> fun findOccupiedRoomIds(@Param("propertyId") propertyId: UUID): List<UUID>
@@ -19,6 +20,7 @@ interface RoomStayRepo : JpaRepository<RoomStay, UUID> {
select distinct rs.room.id select distinct rs.room.id
from RoomStay rs from RoomStay rs
where rs.property.id = :propertyId where rs.property.id = :propertyId
and rs.isVoided = false
and rs.fromAt < :toAt and rs.fromAt < :toAt
and (rs.toAt is null or rs.toAt > :fromAt) and (rs.toAt is null or rs.toAt > :fromAt)
""") """)
@@ -33,6 +35,7 @@ interface RoomStayRepo : JpaRepository<RoomStay, UUID> {
from RoomStay rs from RoomStay rs
where rs.booking.id = :bookingId where rs.booking.id = :bookingId
and rs.toAt is null and rs.toAt is null
and rs.isVoided = false
""") """)
fun findActiveByBookingId(@Param("bookingId") bookingId: UUID): List<RoomStay> fun findActiveByBookingId(@Param("bookingId") bookingId: UUID): List<RoomStay>
@@ -40,6 +43,7 @@ interface RoomStayRepo : JpaRepository<RoomStay, UUID> {
select rs select rs
from RoomStay rs from RoomStay rs
where rs.booking.id = :bookingId where rs.booking.id = :bookingId
and rs.isVoided = false
""") """)
fun findByBookingId(@Param("bookingId") bookingId: UUID): List<RoomStay> fun findByBookingId(@Param("bookingId") bookingId: UUID): List<RoomStay>
@@ -48,6 +52,7 @@ interface RoomStayRepo : JpaRepository<RoomStay, UUID> {
from RoomStay rs from RoomStay rs
join fetch rs.room r join fetch rs.room r
where rs.booking.id = :bookingId where rs.booking.id = :bookingId
and rs.isVoided = false
""") """)
fun findByBookingIdWithRoom(@Param("bookingId") bookingId: UUID): List<RoomStay> fun findByBookingIdWithRoom(@Param("bookingId") bookingId: UUID): List<RoomStay>
@@ -55,6 +60,7 @@ interface RoomStayRepo : JpaRepository<RoomStay, UUID> {
select rs select rs
from RoomStay rs from RoomStay rs
where rs.booking.id in :bookingIds where rs.booking.id in :bookingIds
and rs.isVoided = false
""") """)
fun findByBookingIdIn(@Param("bookingIds") bookingIds: List<UUID>): List<RoomStay> fun findByBookingIdIn(@Param("bookingIds") bookingIds: List<UUID>): List<RoomStay>
@@ -64,6 +70,7 @@ interface RoomStayRepo : JpaRepository<RoomStay, UUID> {
where rs.property.id = :propertyId where rs.property.id = :propertyId
and rs.room.id in :roomIds and rs.room.id in :roomIds
and rs.toAt is null and rs.toAt is null
and rs.isVoided = false
""") """)
fun findActiveRoomIds( fun findActiveRoomIds(
@Param("propertyId") propertyId: UUID, @Param("propertyId") propertyId: UUID,
@@ -75,6 +82,7 @@ interface RoomStayRepo : JpaRepository<RoomStay, UUID> {
from RoomStay rs from RoomStay rs
where rs.property.id = :propertyId where rs.property.id = :propertyId
and rs.room.id = :roomId and rs.room.id = :roomId
and rs.isVoided = false
and rs.fromAt < :toAt and rs.fromAt < :toAt
and (rs.toAt is null or rs.toAt > :fromAt) and (rs.toAt is null or rs.toAt > :fromAt)
""") """)
@@ -94,6 +102,7 @@ interface RoomStayRepo : JpaRepository<RoomStay, UUID> {
left join fetch b.primaryGuest g left join fetch b.primaryGuest g
where rs.property.id = :propertyId where rs.property.id = :propertyId
and rs.toAt is null and rs.toAt is null
and rs.isVoided = false
order by r.roomNumber order by r.roomNumber
""") """)
fun findActiveByPropertyIdWithDetails(@Param("propertyId") propertyId: UUID): List<RoomStay> fun findActiveByPropertyIdWithDetails(@Param("propertyId") propertyId: UUID): List<RoomStay>
@@ -111,10 +120,25 @@ interface RoomStayRepo : JpaRepository<RoomStay, UUID> {
join rs.room r join rs.room r
where rs.booking.id in :bookingIds where rs.booking.id in :bookingIds
and rs.toAt is null and rs.toAt is null
and rs.isVoided = false
""") """)
fun findActiveRoomNumbersByBookingIds( fun findActiveRoomNumbersByBookingIds(
@Param("bookingIds") bookingIds: List<UUID> @Param("bookingIds") bookingIds: List<UUID>
): List<BookingRoomNumberRow> ): 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 { interface BookingRoomNumberRow {