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 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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user