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 propertyRepo: PropertyRepo,
|
||||||
private val paddleOcrClient: PaddleOcrClient,
|
private val paddleOcrClient: PaddleOcrClient,
|
||||||
private val bookingRepo: BookingRepo,
|
private val bookingRepo: BookingRepo,
|
||||||
private val pincodeResolver: PincodeResolver
|
private val pincodeResolver: PincodeResolver,
|
||||||
|
private val bookingEvents: BookingEvents
|
||||||
) {
|
) {
|
||||||
private val logger = LoggerFactory.getLogger(DocumentExtractionService::class.java)
|
private val logger = LoggerFactory.getLogger(DocumentExtractionService::class.java)
|
||||||
|
|
||||||
@@ -476,6 +477,9 @@ class DocumentExtractionService(
|
|||||||
if (updated) {
|
if (updated) {
|
||||||
booking.updatedAt = OffsetDateTime.now()
|
booking.updatedAt = OffsetDateTime.now()
|
||||||
bookingRepo.save(booking)
|
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
|
package com.android.trisolarisserver.controller
|
||||||
|
|
||||||
|
import com.android.trisolarisserver.component.BookingEvents
|
||||||
import com.android.trisolarisserver.component.PropertyAccess
|
import com.android.trisolarisserver.component.PropertyAccess
|
||||||
import com.android.trisolarisserver.component.RoomBoardEvents
|
import com.android.trisolarisserver.component.RoomBoardEvents
|
||||||
import com.android.trisolarisserver.controller.dto.BookingCancelRequest
|
import com.android.trisolarisserver.controller.dto.BookingCancelRequest
|
||||||
@@ -57,10 +58,12 @@ class BookingFlow(
|
|||||||
private val appUserRepo: AppUserRepo,
|
private val appUserRepo: AppUserRepo,
|
||||||
private val propertyRepo: PropertyRepo,
|
private val propertyRepo: PropertyRepo,
|
||||||
private val roomBoardEvents: RoomBoardEvents,
|
private val roomBoardEvents: RoomBoardEvents,
|
||||||
|
private val bookingEvents: BookingEvents,
|
||||||
private val guestVehicleRepo: GuestVehicleRepo,
|
private val guestVehicleRepo: GuestVehicleRepo,
|
||||||
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
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@@ -131,6 +134,7 @@ class BookingFlow(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val saved = bookingRepo.save(booking)
|
val saved = bookingRepo.save(booking)
|
||||||
|
bookingEvents.emit(propertyId, saved.id!!)
|
||||||
return BookingCreateResponse(
|
return BookingCreateResponse(
|
||||||
id = saved.id!!,
|
id = saved.id!!,
|
||||||
status = saved.status.name,
|
status = saved.status.name,
|
||||||
@@ -237,57 +241,27 @@ class BookingFlow(
|
|||||||
Role.FINANCE
|
Role.FINANCE
|
||||||
)
|
)
|
||||||
val booking = requireBooking(propertyId, bookingId)
|
val booking = requireBooking(propertyId, bookingId)
|
||||||
val stays = roomStayRepo.findByBookingId(bookingId)
|
return bookingSnapshotBuilder.build(propertyId, 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()
|
|
||||||
|
|
||||||
val guest = booking.primaryGuest
|
@GetMapping("/{bookingId}/stream")
|
||||||
val signatureUrl = guest?.signaturePath?.let {
|
fun streamBooking(
|
||||||
"/properties/$propertyId/guests/${guest.id}/signature/file"
|
@PathVariable propertyId: UUID,
|
||||||
}
|
@PathVariable bookingId: UUID,
|
||||||
|
@AuthenticationPrincipal principal: MyPrincipal?
|
||||||
val totalNightlyRate = roomsToShow.sumOf { it.nightlyRate ?: 0L }
|
): org.springframework.web.servlet.mvc.method.annotation.SseEmitter {
|
||||||
val expectedPay = computeExpectedPayTotal(stays, booking.expectedCheckoutAt, booking.property.timezone)
|
requireRole(
|
||||||
val accruedPay = computeExpectedPay(stays, booking.property.timezone)
|
propertyAccess,
|
||||||
val amountCollected = paymentRepo.sumAmountByBookingId(bookingId)
|
propertyId,
|
||||||
val pending = accruedPay - amountCollected
|
principal,
|
||||||
|
Role.ADMIN,
|
||||||
return BookingDetailResponse(
|
Role.MANAGER,
|
||||||
id = booking.id!!,
|
Role.STAFF,
|
||||||
status = booking.status.name,
|
Role.HOUSEKEEPING,
|
||||||
guestId = guest?.id,
|
Role.FINANCE
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
requireBooking(propertyId, bookingId)
|
||||||
|
return bookingEvents.subscribe(propertyId, bookingId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/{bookingId}/link-guest")
|
@PostMapping("/{bookingId}/link-guest")
|
||||||
@@ -311,6 +285,7 @@ class BookingFlow(
|
|||||||
booking.primaryGuest = guest
|
booking.primaryGuest = guest
|
||||||
booking.updatedAt = OffsetDateTime.now()
|
booking.updatedAt = OffsetDateTime.now()
|
||||||
bookingRepo.save(booking)
|
bookingRepo.save(booking)
|
||||||
|
bookingEvents.emit(propertyId, bookingId)
|
||||||
if (previous != null && previous.id != guest.id && isPlaceholderGuest(previous) && isSafeToDelete(previous)) {
|
if (previous != null && previous.id != guest.id && isPlaceholderGuest(previous) && isSafeToDelete(previous)) {
|
||||||
guestRepo.delete(previous)
|
guestRepo.delete(previous)
|
||||||
}
|
}
|
||||||
@@ -382,6 +357,7 @@ class BookingFlow(
|
|||||||
booking.updatedAt = now
|
booking.updatedAt = now
|
||||||
bookingRepo.save(booking)
|
bookingRepo.save(booking)
|
||||||
roomBoardEvents.emit(propertyId)
|
roomBoardEvents.emit(propertyId)
|
||||||
|
bookingEvents.emit(propertyId, bookingId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/{bookingId}/check-in/bulk")
|
@PostMapping("/{bookingId}/check-in/bulk")
|
||||||
@@ -469,6 +445,7 @@ class BookingFlow(
|
|||||||
booking.updatedAt = now
|
booking.updatedAt = now
|
||||||
bookingRepo.save(booking)
|
bookingRepo.save(booking)
|
||||||
roomBoardEvents.emit(propertyId)
|
roomBoardEvents.emit(propertyId)
|
||||||
|
bookingEvents.emit(propertyId, bookingId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/{bookingId}/expected-dates")
|
@PostMapping("/{bookingId}/expected-dates")
|
||||||
@@ -514,6 +491,7 @@ class BookingFlow(
|
|||||||
|
|
||||||
booking.updatedAt = OffsetDateTime.now()
|
booking.updatedAt = OffsetDateTime.now()
|
||||||
bookingRepo.save(booking)
|
bookingRepo.save(booking)
|
||||||
|
bookingEvents.emit(propertyId, bookingId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/{bookingId}/check-out")
|
@PostMapping("/{bookingId}/check-out")
|
||||||
@@ -543,6 +521,7 @@ class BookingFlow(
|
|||||||
booking.updatedAt = now
|
booking.updatedAt = now
|
||||||
bookingRepo.save(booking)
|
bookingRepo.save(booking)
|
||||||
roomBoardEvents.emit(propertyId)
|
roomBoardEvents.emit(propertyId)
|
||||||
|
bookingEvents.emit(propertyId, bookingId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolveGuestForBooking(
|
private fun resolveGuestForBooking(
|
||||||
@@ -603,6 +582,7 @@ class BookingFlow(
|
|||||||
if (request.reason != null) booking.notes = request.reason
|
if (request.reason != null) booking.notes = request.reason
|
||||||
booking.updatedAt = OffsetDateTime.now()
|
booking.updatedAt = OffsetDateTime.now()
|
||||||
bookingRepo.save(booking)
|
bookingRepo.save(booking)
|
||||||
|
bookingEvents.emit(propertyId, bookingId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/{bookingId}/no-show")
|
@PostMapping("/{bookingId}/no-show")
|
||||||
@@ -623,6 +603,7 @@ class BookingFlow(
|
|||||||
if (request.reason != null) booking.notes = request.reason
|
if (request.reason != null) booking.notes = request.reason
|
||||||
booking.updatedAt = OffsetDateTime.now()
|
booking.updatedAt = OffsetDateTime.now()
|
||||||
bookingRepo.save(booking)
|
bookingRepo.save(booking)
|
||||||
|
bookingEvents.emit(propertyId, bookingId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/{bookingId}/room-stays")
|
@PostMapping("/{bookingId}/room-stays")
|
||||||
@@ -671,6 +652,7 @@ class BookingFlow(
|
|||||||
createdBy = actor
|
createdBy = actor
|
||||||
)
|
)
|
||||||
roomStayRepo.save(stay)
|
roomStayRepo.save(stay)
|
||||||
|
bookingEvents.emit(propertyId, bookingId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requireBooking(propertyId: UUID, bookingId: UUID): com.android.trisolarisserver.models.booking.Booking {
|
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
|
package com.android.trisolarisserver.controller
|
||||||
|
|
||||||
|
import com.android.trisolarisserver.component.BookingEvents
|
||||||
import com.android.trisolarisserver.component.PropertyAccess
|
import com.android.trisolarisserver.component.PropertyAccess
|
||||||
import com.android.trisolarisserver.controller.dto.ChargeCreateRequest
|
import com.android.trisolarisserver.controller.dto.ChargeCreateRequest
|
||||||
import com.android.trisolarisserver.controller.dto.ChargeResponse
|
import com.android.trisolarisserver.controller.dto.ChargeResponse
|
||||||
@@ -29,7 +30,8 @@ class Charges(
|
|||||||
private val propertyAccess: PropertyAccess,
|
private val propertyAccess: PropertyAccess,
|
||||||
private val bookingRepo: BookingRepo,
|
private val bookingRepo: BookingRepo,
|
||||||
private val chargeRepo: ChargeRepo,
|
private val chargeRepo: ChargeRepo,
|
||||||
private val appUserRepo: AppUserRepo
|
private val appUserRepo: AppUserRepo,
|
||||||
|
private val bookingEvents: BookingEvents
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@@ -65,7 +67,9 @@ class Charges(
|
|||||||
occurredAt = occurredAt,
|
occurredAt = occurredAt,
|
||||||
createdBy = createdBy
|
createdBy = createdBy
|
||||||
)
|
)
|
||||||
return chargeRepo.save(charge).toResponse()
|
val saved = chargeRepo.save(charge).toResponse()
|
||||||
|
bookingEvents.emit(propertyId, bookingId)
|
||||||
|
return saved
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.android.trisolarisserver.controller
|
package com.android.trisolarisserver.controller
|
||||||
|
|
||||||
|
import com.android.trisolarisserver.component.BookingEvents
|
||||||
import com.android.trisolarisserver.component.PropertyAccess
|
import com.android.trisolarisserver.component.PropertyAccess
|
||||||
import com.android.trisolarisserver.controller.dto.PaymentCreateRequest
|
import com.android.trisolarisserver.controller.dto.PaymentCreateRequest
|
||||||
import com.android.trisolarisserver.controller.dto.PaymentResponse
|
import com.android.trisolarisserver.controller.dto.PaymentResponse
|
||||||
@@ -34,7 +35,8 @@ class Payments(
|
|||||||
private val propertyRepo: PropertyRepo,
|
private val propertyRepo: PropertyRepo,
|
||||||
private val bookingRepo: BookingRepo,
|
private val bookingRepo: BookingRepo,
|
||||||
private val paymentRepo: PaymentRepo,
|
private val paymentRepo: PaymentRepo,
|
||||||
private val appUserRepo: AppUserRepo
|
private val appUserRepo: AppUserRepo,
|
||||||
|
private val bookingEvents: BookingEvents
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@@ -72,7 +74,9 @@ class Payments(
|
|||||||
receivedAt = receivedAt,
|
receivedAt = receivedAt,
|
||||||
receivedBy = appUserRepo.findById(actor.userId).orElse(null)
|
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
|
@GetMapping
|
||||||
@@ -122,6 +126,7 @@ class Payments(
|
|||||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Only CASH payments can be deleted")
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Only CASH payments can be deleted")
|
||||||
}
|
}
|
||||||
paymentRepo.delete(payment)
|
paymentRepo.delete(payment)
|
||||||
|
bookingEvents.emit(propertyId, bookingId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseMethod(value: String): PaymentMethod {
|
private fun parseMethod(value: String): PaymentMethod {
|
||||||
|
|||||||
Reference in New Issue
Block a user