diff --git a/src/main/kotlin/com/android/trisolarisserver/component/RazorpayQrEvents.kt b/src/main/kotlin/com/android/trisolarisserver/component/RazorpayQrEvents.kt new file mode 100644 index 0000000..c9535cd --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/component/RazorpayQrEvents.kt @@ -0,0 +1,48 @@ +package com.android.trisolarisserver.component + +import com.android.trisolarisserver.controller.dto.RazorpayQrEventResponse +import com.android.trisolarisserver.repo.RazorpayQrRequestRepo +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter +import java.time.OffsetDateTime +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap + +@Component +class RazorpayQrEvents( + private val qrRequestRepo: RazorpayQrRequestRepo +) { + private val latestEvents: MutableMap = ConcurrentHashMap() + private val hub = SseHub("qr") { key -> + latestEvents[key] ?: run { + val latest = qrRequestRepo.findTopByQrIdOrderByCreatedAtDesc(key.qrId) + RazorpayQrEventResponse( + event = "snapshot", + qrId = key.qrId, + status = latest?.status, + receivedAt = (latest?.createdAt ?: OffsetDateTime.now()).toString() + ) + } + } + + fun subscribe(propertyId: UUID, qrId: String): SseEmitter { + return hub.subscribe(QrKey(propertyId, qrId)) + } + + fun emit(propertyId: UUID, qrId: String, event: RazorpayQrEventResponse) { + val key = QrKey(propertyId, qrId) + latestEvents[key] = event + hub.emit(key) + } + + @Scheduled(fixedDelayString = "25000") + fun heartbeat() { + hub.heartbeat() + } +} + +private data class QrKey( + val propertyId: UUID, + val qrId: String +) diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayQrPayments.kt b/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayQrPayments.kt index e22fec4..09d9ca4 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayQrPayments.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayQrPayments.kt @@ -1,6 +1,7 @@ package com.android.trisolarisserver.controller import com.android.trisolarisserver.component.PropertyAccess +import com.android.trisolarisserver.component.RazorpayQrEvents import com.android.trisolarisserver.controller.dto.RazorpayQrGenerateRequest import com.android.trisolarisserver.controller.dto.RazorpayQrEventResponse import com.android.trisolarisserver.controller.dto.RazorpayQrGenerateResponse @@ -30,6 +31,7 @@ import org.springframework.web.client.RestTemplate import org.springframework.web.server.ResponseStatusException import org.springframework.http.HttpStatus import org.springframework.transaction.annotation.Transactional +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter import java.time.OffsetDateTime import java.util.Base64 import java.util.UUID @@ -42,6 +44,7 @@ class RazorpayQrPayments( private val settingsRepo: RazorpaySettingsRepo, private val qrRequestRepo: RazorpayQrRequestRepo, private val webhookLogRepo: RazorpayWebhookLogRepo, + private val qrEvents: RazorpayQrEvents, private val restTemplate: RestTemplate, private val objectMapper: ObjectMapper ) { @@ -259,6 +262,17 @@ class RazorpayQrPayments( return out } + @GetMapping("/qr/{qrId}/events/stream", produces = [MediaType.TEXT_EVENT_STREAM_VALUE]) + fun streamQrEvents( + @PathVariable propertyId: UUID, + @PathVariable bookingId: UUID, + @PathVariable qrId: String, + @AuthenticationPrincipal principal: MyPrincipal? + ): SseEmitter { + requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER, Role.STAFF) + return qrEvents.subscribe(propertyId, qrId) + } + @GetMapping("/qr") fun listQr( @PathVariable propertyId: UUID, diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayWebhookCapture.kt b/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayWebhookCapture.kt index 271f7ea..7246854 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayWebhookCapture.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayWebhookCapture.kt @@ -5,6 +5,8 @@ import com.android.trisolarisserver.models.booking.Payment import com.android.trisolarisserver.models.booking.PaymentMethod import com.android.trisolarisserver.models.payment.RazorpayPaymentAttempt import com.android.trisolarisserver.models.payment.RazorpayWebhookLog +import com.android.trisolarisserver.component.RazorpayQrEvents +import com.android.trisolarisserver.controller.dto.RazorpayQrEventResponse import com.android.trisolarisserver.repo.PaymentRepo import com.android.trisolarisserver.repo.PropertyRepo import com.android.trisolarisserver.repo.RazorpayPaymentAttemptRepo @@ -37,6 +39,7 @@ class RazorpayWebhookCapture( private val razorpayPaymentAttemptRepo: RazorpayPaymentAttemptRepo, private val razorpayQrRequestRepo: RazorpayQrRequestRepo, private val razorpayWebhookLogRepo: RazorpayWebhookLogRepo, + private val razorpayQrEvents: RazorpayQrEvents, private val objectMapper: ObjectMapper ) { @@ -102,6 +105,16 @@ class RazorpayWebhookCapture( razorpayQrRequestRepo.save(existingQr) } } + razorpayQrEvents.emit( + propertyId, + qrId, + RazorpayQrEventResponse( + event = event, + qrId = qrId, + status = qrStatus, + receivedAt = OffsetDateTime.now().toString() + ) + ) } razorpayPaymentAttemptRepo.save(