Add booking SSE stream and emit on updates
All checks were successful
build-and-deploy / build-deploy (push) Successful in 32s
All checks were successful
build-and-deploy / build-deploy (push) Successful in 32s
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
package com.android.trisolarisserver.component
|
||||
|
||||
import com.android.trisolarisserver.controller.BookingSnapshotBuilder
|
||||
import com.android.trisolarisserver.controller.dto.BookingDetailResponse
|
||||
import org.springframework.scheduling.annotation.Scheduled
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter
|
||||
import java.util.UUID
|
||||
|
||||
@Component
|
||||
class BookingEvents(
|
||||
private val bookingSnapshotBuilder: BookingSnapshotBuilder
|
||||
) {
|
||||
private val hub = SseHub<BookingKey>("booking") { key ->
|
||||
bookingSnapshotBuilder.build(key.propertyId, key.bookingId)
|
||||
}
|
||||
|
||||
fun subscribe(propertyId: UUID, bookingId: UUID): SseEmitter {
|
||||
return hub.subscribe(BookingKey(propertyId, bookingId))
|
||||
}
|
||||
|
||||
fun emit(propertyId: UUID, bookingId: UUID) {
|
||||
hub.emit(BookingKey(propertyId, bookingId))
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelayString = "25000")
|
||||
fun heartbeat() {
|
||||
hub.heartbeat()
|
||||
}
|
||||
}
|
||||
|
||||
private data class BookingKey(
|
||||
val propertyId: UUID,
|
||||
val bookingId: UUID
|
||||
)
|
||||
@@ -19,7 +19,8 @@ class DocumentExtractionService(
|
||||
private val propertyRepo: PropertyRepo,
|
||||
private val paddleOcrClient: PaddleOcrClient,
|
||||
private val bookingRepo: BookingRepo,
|
||||
private val pincodeResolver: PincodeResolver
|
||||
private val pincodeResolver: PincodeResolver,
|
||||
private val bookingEvents: BookingEvents
|
||||
) {
|
||||
private val logger = LoggerFactory.getLogger(DocumentExtractionService::class.java)
|
||||
|
||||
@@ -476,6 +477,9 @@ class DocumentExtractionService(
|
||||
if (updated) {
|
||||
booking.updatedAt = OffsetDateTime.now()
|
||||
bookingRepo.save(booking)
|
||||
val propertyId = booking.property.id ?: return
|
||||
val bookingId = booking.id ?: return
|
||||
bookingEvents.emit(propertyId, bookingId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.android.trisolarisserver.controller
|
||||
|
||||
import com.android.trisolarisserver.component.BookingEvents
|
||||
import com.android.trisolarisserver.component.PropertyAccess
|
||||
import com.android.trisolarisserver.component.RoomBoardEvents
|
||||
import com.android.trisolarisserver.controller.dto.BookingCancelRequest
|
||||
@@ -57,10 +58,12 @@ class BookingFlow(
|
||||
private val appUserRepo: AppUserRepo,
|
||||
private val propertyRepo: PropertyRepo,
|
||||
private val roomBoardEvents: RoomBoardEvents,
|
||||
private val bookingEvents: BookingEvents,
|
||||
private val guestVehicleRepo: GuestVehicleRepo,
|
||||
private val guestRatingRepo: GuestRatingRepo,
|
||||
private val guestDocumentRepo: GuestDocumentRepo,
|
||||
private val paymentRepo: PaymentRepo
|
||||
private val paymentRepo: PaymentRepo,
|
||||
private val bookingSnapshotBuilder: BookingSnapshotBuilder
|
||||
) {
|
||||
|
||||
@PostMapping
|
||||
@@ -131,6 +134,7 @@ class BookingFlow(
|
||||
)
|
||||
|
||||
val saved = bookingRepo.save(booking)
|
||||
bookingEvents.emit(propertyId, saved.id!!)
|
||||
return BookingCreateResponse(
|
||||
id = saved.id!!,
|
||||
status = saved.status.name,
|
||||
@@ -237,57 +241,27 @@ class BookingFlow(
|
||||
Role.FINANCE
|
||||
)
|
||||
val booking = requireBooking(propertyId, bookingId)
|
||||
val stays = roomStayRepo.findByBookingId(bookingId)
|
||||
val activeRooms = stays.filter { it.toAt == null }
|
||||
val roomsToShow = if (activeRooms.isNotEmpty()) activeRooms else stays
|
||||
val roomNumbers = roomsToShow.map { it.room.roomNumber }
|
||||
.distinct()
|
||||
.sorted()
|
||||
return bookingSnapshotBuilder.build(propertyId, bookingId)
|
||||
}
|
||||
|
||||
val guest = booking.primaryGuest
|
||||
val signatureUrl = guest?.signaturePath?.let {
|
||||
"/properties/$propertyId/guests/${guest.id}/signature/file"
|
||||
}
|
||||
|
||||
val totalNightlyRate = roomsToShow.sumOf { it.nightlyRate ?: 0L }
|
||||
val expectedPay = computeExpectedPayTotal(stays, booking.expectedCheckoutAt, booking.property.timezone)
|
||||
val accruedPay = computeExpectedPay(stays, booking.property.timezone)
|
||||
val amountCollected = paymentRepo.sumAmountByBookingId(bookingId)
|
||||
val pending = accruedPay - amountCollected
|
||||
|
||||
return BookingDetailResponse(
|
||||
id = booking.id!!,
|
||||
status = booking.status.name,
|
||||
guestId = guest?.id,
|
||||
guestName = guest?.name,
|
||||
guestPhone = guest?.phoneE164,
|
||||
guestNationality = guest?.nationality,
|
||||
guestAddressText = guest?.addressText,
|
||||
guestSignatureUrl = signatureUrl,
|
||||
roomNumbers = roomNumbers,
|
||||
source = booking.source,
|
||||
fromCity = booking.fromCity,
|
||||
toCity = booking.toCity,
|
||||
memberRelation = booking.memberRelation?.name,
|
||||
transportMode = booking.transportMode?.name,
|
||||
checkInAt = booking.checkinAt?.toString(),
|
||||
checkOutAt = booking.checkoutAt?.toString(),
|
||||
expectedCheckInAt = booking.expectedCheckinAt?.toString(),
|
||||
expectedCheckOutAt = booking.expectedCheckoutAt?.toString(),
|
||||
adultCount = booking.adultCount,
|
||||
childCount = booking.childCount,
|
||||
maleCount = booking.maleCount,
|
||||
femaleCount = booking.femaleCount,
|
||||
totalGuestCount = booking.totalGuestCount,
|
||||
expectedGuestCount = booking.expectedGuestCount,
|
||||
notes = booking.notes,
|
||||
registeredByName = booking.createdBy?.name,
|
||||
registeredByPhone = booking.createdBy?.phoneE164,
|
||||
totalNightlyRate = totalNightlyRate,
|
||||
expectedPay = expectedPay,
|
||||
amountCollected = amountCollected,
|
||||
pending = pending
|
||||
@GetMapping("/{bookingId}/stream")
|
||||
fun streamBooking(
|
||||
@PathVariable propertyId: UUID,
|
||||
@PathVariable bookingId: UUID,
|
||||
@AuthenticationPrincipal principal: MyPrincipal?
|
||||
): org.springframework.web.servlet.mvc.method.annotation.SseEmitter {
|
||||
requireRole(
|
||||
propertyAccess,
|
||||
propertyId,
|
||||
principal,
|
||||
Role.ADMIN,
|
||||
Role.MANAGER,
|
||||
Role.STAFF,
|
||||
Role.HOUSEKEEPING,
|
||||
Role.FINANCE
|
||||
)
|
||||
requireBooking(propertyId, bookingId)
|
||||
return bookingEvents.subscribe(propertyId, bookingId)
|
||||
}
|
||||
|
||||
@PostMapping("/{bookingId}/link-guest")
|
||||
@@ -311,6 +285,7 @@ class BookingFlow(
|
||||
booking.primaryGuest = guest
|
||||
booking.updatedAt = OffsetDateTime.now()
|
||||
bookingRepo.save(booking)
|
||||
bookingEvents.emit(propertyId, bookingId)
|
||||
if (previous != null && previous.id != guest.id && isPlaceholderGuest(previous) && isSafeToDelete(previous)) {
|
||||
guestRepo.delete(previous)
|
||||
}
|
||||
@@ -382,6 +357,7 @@ class BookingFlow(
|
||||
booking.updatedAt = now
|
||||
bookingRepo.save(booking)
|
||||
roomBoardEvents.emit(propertyId)
|
||||
bookingEvents.emit(propertyId, bookingId)
|
||||
}
|
||||
|
||||
@PostMapping("/{bookingId}/check-in/bulk")
|
||||
@@ -469,6 +445,7 @@ class BookingFlow(
|
||||
booking.updatedAt = now
|
||||
bookingRepo.save(booking)
|
||||
roomBoardEvents.emit(propertyId)
|
||||
bookingEvents.emit(propertyId, bookingId)
|
||||
}
|
||||
|
||||
@PostMapping("/{bookingId}/expected-dates")
|
||||
@@ -514,6 +491,7 @@ class BookingFlow(
|
||||
|
||||
booking.updatedAt = OffsetDateTime.now()
|
||||
bookingRepo.save(booking)
|
||||
bookingEvents.emit(propertyId, bookingId)
|
||||
}
|
||||
|
||||
@PostMapping("/{bookingId}/check-out")
|
||||
@@ -543,6 +521,7 @@ class BookingFlow(
|
||||
booking.updatedAt = now
|
||||
bookingRepo.save(booking)
|
||||
roomBoardEvents.emit(propertyId)
|
||||
bookingEvents.emit(propertyId, bookingId)
|
||||
}
|
||||
|
||||
private fun resolveGuestForBooking(
|
||||
@@ -603,6 +582,7 @@ class BookingFlow(
|
||||
if (request.reason != null) booking.notes = request.reason
|
||||
booking.updatedAt = OffsetDateTime.now()
|
||||
bookingRepo.save(booking)
|
||||
bookingEvents.emit(propertyId, bookingId)
|
||||
}
|
||||
|
||||
@PostMapping("/{bookingId}/no-show")
|
||||
@@ -623,6 +603,7 @@ class BookingFlow(
|
||||
if (request.reason != null) booking.notes = request.reason
|
||||
booking.updatedAt = OffsetDateTime.now()
|
||||
bookingRepo.save(booking)
|
||||
bookingEvents.emit(propertyId, bookingId)
|
||||
}
|
||||
|
||||
@PostMapping("/{bookingId}/room-stays")
|
||||
@@ -671,6 +652,7 @@ class BookingFlow(
|
||||
createdBy = actor
|
||||
)
|
||||
roomStayRepo.save(stay)
|
||||
bookingEvents.emit(propertyId, bookingId)
|
||||
}
|
||||
|
||||
private fun requireBooking(propertyId: UUID, bookingId: UUID): com.android.trisolarisserver.models.booking.Booking {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.android.trisolarisserver.controller
|
||||
|
||||
import com.android.trisolarisserver.controller.dto.BookingDetailResponse
|
||||
import com.android.trisolarisserver.db.repo.BookingRepo
|
||||
import com.android.trisolarisserver.repo.PaymentRepo
|
||||
import com.android.trisolarisserver.repo.RoomStayRepo
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import java.util.UUID
|
||||
|
||||
@Component
|
||||
class BookingSnapshotBuilder(
|
||||
private val bookingRepo: BookingRepo,
|
||||
private val roomStayRepo: RoomStayRepo,
|
||||
private val paymentRepo: PaymentRepo
|
||||
) {
|
||||
fun build(propertyId: UUID, bookingId: UUID): BookingDetailResponse {
|
||||
val booking = bookingRepo.findById(bookingId).orElseThrow {
|
||||
ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found")
|
||||
}
|
||||
if (booking.property.id != propertyId) {
|
||||
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found for property")
|
||||
}
|
||||
|
||||
val stays = roomStayRepo.findByBookingId(bookingId)
|
||||
val activeRooms = stays.filter { it.toAt == null }
|
||||
val roomsToShow = activeRooms.ifEmpty { stays }
|
||||
val roomNumbers = roomsToShow.map { it.room.roomNumber }
|
||||
.distinct()
|
||||
.sorted()
|
||||
|
||||
val guest = booking.primaryGuest
|
||||
val signatureUrl = guest?.signaturePath?.let {
|
||||
"/properties/$propertyId/guests/${guest.id}/signature/file"
|
||||
}
|
||||
|
||||
val totalNightlyRate = roomsToShow.sumOf { it.nightlyRate ?: 0L }
|
||||
val expectedPay = computeExpectedPayTotal(stays, booking.expectedCheckoutAt, booking.property.timezone)
|
||||
val accruedPay = computeExpectedPay(stays, booking.property.timezone)
|
||||
val amountCollected = paymentRepo.sumAmountByBookingId(bookingId)
|
||||
val pending = accruedPay - amountCollected
|
||||
|
||||
return BookingDetailResponse(
|
||||
id = booking.id!!,
|
||||
status = booking.status.name,
|
||||
guestId = guest?.id,
|
||||
guestName = guest?.name,
|
||||
guestPhone = guest?.phoneE164,
|
||||
guestNationality = guest?.nationality,
|
||||
guestAddressText = guest?.addressText,
|
||||
guestSignatureUrl = signatureUrl,
|
||||
roomNumbers = roomNumbers,
|
||||
source = booking.source,
|
||||
fromCity = booking.fromCity,
|
||||
toCity = booking.toCity,
|
||||
memberRelation = booking.memberRelation?.name,
|
||||
transportMode = booking.transportMode?.name,
|
||||
checkInAt = booking.checkinAt?.toString(),
|
||||
checkOutAt = booking.checkoutAt?.toString(),
|
||||
expectedCheckInAt = booking.expectedCheckinAt?.toString(),
|
||||
expectedCheckOutAt = booking.expectedCheckoutAt?.toString(),
|
||||
adultCount = booking.adultCount,
|
||||
childCount = booking.childCount,
|
||||
maleCount = booking.maleCount,
|
||||
femaleCount = booking.femaleCount,
|
||||
totalGuestCount = booking.totalGuestCount,
|
||||
expectedGuestCount = booking.expectedGuestCount,
|
||||
notes = booking.notes,
|
||||
registeredByName = booking.createdBy?.name,
|
||||
registeredByPhone = booking.createdBy?.phoneE164,
|
||||
totalNightlyRate = totalNightlyRate,
|
||||
expectedPay = expectedPay,
|
||||
amountCollected = amountCollected,
|
||||
pending = pending
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.android.trisolarisserver.controller
|
||||
|
||||
import com.android.trisolarisserver.component.BookingEvents
|
||||
import com.android.trisolarisserver.component.PropertyAccess
|
||||
import com.android.trisolarisserver.controller.dto.ChargeCreateRequest
|
||||
import com.android.trisolarisserver.controller.dto.ChargeResponse
|
||||
@@ -29,7 +30,8 @@ class Charges(
|
||||
private val propertyAccess: PropertyAccess,
|
||||
private val bookingRepo: BookingRepo,
|
||||
private val chargeRepo: ChargeRepo,
|
||||
private val appUserRepo: AppUserRepo
|
||||
private val appUserRepo: AppUserRepo,
|
||||
private val bookingEvents: BookingEvents
|
||||
) {
|
||||
|
||||
@PostMapping
|
||||
@@ -65,7 +67,9 @@ class Charges(
|
||||
occurredAt = occurredAt,
|
||||
createdBy = createdBy
|
||||
)
|
||||
return chargeRepo.save(charge).toResponse()
|
||||
val saved = chargeRepo.save(charge).toResponse()
|
||||
bookingEvents.emit(propertyId, bookingId)
|
||||
return saved
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.android.trisolarisserver.controller
|
||||
|
||||
import com.android.trisolarisserver.component.BookingEvents
|
||||
import com.android.trisolarisserver.component.PropertyAccess
|
||||
import com.android.trisolarisserver.controller.dto.PaymentCreateRequest
|
||||
import com.android.trisolarisserver.controller.dto.PaymentResponse
|
||||
@@ -34,7 +35,8 @@ class Payments(
|
||||
private val propertyRepo: PropertyRepo,
|
||||
private val bookingRepo: BookingRepo,
|
||||
private val paymentRepo: PaymentRepo,
|
||||
private val appUserRepo: AppUserRepo
|
||||
private val appUserRepo: AppUserRepo,
|
||||
private val bookingEvents: BookingEvents
|
||||
) {
|
||||
|
||||
@PostMapping
|
||||
@@ -72,7 +74,9 @@ class Payments(
|
||||
receivedAt = receivedAt,
|
||||
receivedBy = appUserRepo.findById(actor.userId).orElse(null)
|
||||
)
|
||||
return paymentRepo.save(payment).toResponse()
|
||||
val saved = paymentRepo.save(payment).toResponse()
|
||||
bookingEvents.emit(propertyId, bookingId)
|
||||
return saved
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@@ -122,6 +126,7 @@ class Payments(
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Only CASH payments can be deleted")
|
||||
}
|
||||
paymentRepo.delete(payment)
|
||||
bookingEvents.emit(propertyId, bookingId)
|
||||
}
|
||||
|
||||
private fun parseMethod(value: String): PaymentMethod {
|
||||
|
||||
Reference in New Issue
Block a user