From 5e9e0d07422d0d6c28bce7ea89e98e8593977722 Mon Sep 17 00:00:00 2001 From: androidlover5842 Date: Sun, 1 Feb 2026 14:25:50 +0530 Subject: [PATCH] Add unified close endpoint for Razorpay requests --- .../RazorpayPaymentRequestsController.kt | 107 +++++++++++++++++- .../controller/dto/RazorpayDtos.kt | 12 ++ .../repo/RazorpayPaymentLinkRequestRepo.kt | 2 + 3 files changed, 120 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayPaymentRequestsController.kt b/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayPaymentRequestsController.kt index 02d4cf0..32d29d8 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayPaymentRequestsController.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayPaymentRequestsController.kt @@ -1,19 +1,31 @@ package com.android.trisolarisserver.controller import com.android.trisolarisserver.component.PropertyAccess +import com.android.trisolarisserver.controller.dto.RazorpayPaymentRequestCloseRequest +import com.android.trisolarisserver.controller.dto.RazorpayPaymentRequestCloseResponse import com.android.trisolarisserver.controller.dto.RazorpayPaymentRequestResponse import com.android.trisolarisserver.db.repo.BookingRepo import com.android.trisolarisserver.models.property.Role import com.android.trisolarisserver.repo.RazorpayPaymentLinkRequestRepo import com.android.trisolarisserver.repo.RazorpayQrRequestRepo +import com.android.trisolarisserver.repo.RazorpaySettingsRepo import com.android.trisolarisserver.security.MyPrincipal +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod +import org.springframework.http.MediaType import org.springframework.http.HttpStatus import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController +import org.springframework.web.client.RestTemplate import org.springframework.web.server.ResponseStatusException +import java.util.Base64 import java.util.UUID @RestController @@ -22,7 +34,10 @@ class RazorpayPaymentRequestsController( private val propertyAccess: PropertyAccess, private val bookingRepo: BookingRepo, private val qrRequestRepo: RazorpayQrRequestRepo, - private val paymentLinkRequestRepo: RazorpayPaymentLinkRequestRepo + private val paymentLinkRequestRepo: RazorpayPaymentLinkRequestRepo, + private val settingsRepo: RazorpaySettingsRepo, + private val restTemplate: RestTemplate, + private val objectMapper: ObjectMapper ) { @GetMapping("/requests") @@ -68,4 +83,94 @@ class RazorpayPaymentRequestsController( return (qrItems + linkItems).sortedByDescending { it.createdAt } } + + @PostMapping("/close") + fun closeRequest( + @PathVariable propertyId: UUID, + @PathVariable bookingId: UUID, + @RequestBody request: RazorpayPaymentRequestCloseRequest, + @AuthenticationPrincipal principal: MyPrincipal? + ): RazorpayPaymentRequestCloseResponse { + requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER, Role.STAFF) + 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 qrId = request.qrId?.trim()?.ifBlank { null } + val linkId = request.paymentLinkId?.trim()?.ifBlank { null } + if ((qrId == null && linkId == null) || (qrId != null && linkId != null)) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Provide exactly one of qrId or paymentLinkId") + } + + val settings = settingsRepo.findByPropertyId(propertyId) + ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Razorpay settings not configured") + + if (qrId != null) { + val record = qrRequestRepo.findTopByQrIdOrderByCreatedAtDesc(qrId) + ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "QR not found") + if (record.booking.id != bookingId) { + throw ResponseStatusException(HttpStatus.NOT_FOUND, "QR not found for booking") + } + val response = postJson(resolveBaseUrl(settings.isTest) + "/payments/qr_codes/$qrId/close", settings, "{}") + if (!response.statusCode.is2xxSuccessful) { + throw ResponseStatusException(HttpStatus.BAD_GATEWAY, "Razorpay close request failed") + } + record.status = "closed" + qrRequestRepo.save(record) + return RazorpayPaymentRequestCloseResponse( + type = "QR", + qrId = qrId, + status = "closed" + ) + } + + val paymentLinkId = linkId!! + val record = paymentLinkRequestRepo.findTopByPaymentLinkIdOrderByCreatedAtDesc(paymentLinkId) + ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Payment link not found") + if (record.booking.id != bookingId) { + throw ResponseStatusException(HttpStatus.NOT_FOUND, "Payment link not found for booking") + } + val response = postJson(resolveBaseUrl(settings.isTest) + "/payment_links/$paymentLinkId/cancel", settings, "{}") + if (!response.statusCode.is2xxSuccessful) { + throw ResponseStatusException(HttpStatus.BAD_GATEWAY, "Razorpay cancel request failed") + } + val body = response.body ?: "{}" + val status = runCatching { objectMapper.readTree(body).path("status").asText(null) }.getOrNull() + ?: "cancelled" + record.status = status + paymentLinkRequestRepo.save(record) + return RazorpayPaymentRequestCloseResponse( + type = "PAYMENT_LINK", + paymentLinkId = paymentLinkId, + status = status + ) + } + + private fun postJson( + url: String, + settings: com.android.trisolarisserver.models.payment.RazorpaySettings, + json: String + ): org.springframework.http.ResponseEntity { + val headers = HttpHeaders() + headers.contentType = MediaType.APPLICATION_JSON + headers.set(HttpHeaders.AUTHORIZATION, basicAuth(settings.keyId, settings.keySecret)) + return restTemplate.exchange(url, HttpMethod.POST, HttpEntity(json, headers), String::class.java) + } + + private fun resolveBaseUrl(isTest: Boolean): String { + return if (isTest) { + "https://api.razorpay.com/v1" + } else { + "https://api.razorpay.com/v1" + } + } + + private fun basicAuth(keyId: String, keySecret: String): String { + val raw = "$keyId:$keySecret" + val encoded = Base64.getEncoder().encodeToString(raw.toByteArray()) + return "Basic $encoded" + } } diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/dto/RazorpayDtos.kt b/src/main/kotlin/com/android/trisolarisserver/controller/dto/RazorpayDtos.kt index d553821..08707d9 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/dto/RazorpayDtos.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/dto/RazorpayDtos.kt @@ -88,3 +88,15 @@ data class RazorpayPaymentRequestResponse( val paymentLinkId: String? = null, val paymentLink: String? = null ) + +data class RazorpayPaymentRequestCloseRequest( + val qrId: String? = null, + val paymentLinkId: String? = null +) + +data class RazorpayPaymentRequestCloseResponse( + val type: String, + val qrId: String? = null, + val paymentLinkId: String? = null, + val status: String? = null +) diff --git a/src/main/kotlin/com/android/trisolarisserver/repo/RazorpayPaymentLinkRequestRepo.kt b/src/main/kotlin/com/android/trisolarisserver/repo/RazorpayPaymentLinkRequestRepo.kt index 4f8facf..a0f152e 100644 --- a/src/main/kotlin/com/android/trisolarisserver/repo/RazorpayPaymentLinkRequestRepo.kt +++ b/src/main/kotlin/com/android/trisolarisserver/repo/RazorpayPaymentLinkRequestRepo.kt @@ -13,4 +13,6 @@ interface RazorpayPaymentLinkRequestRepo : JpaRepository + + fun findTopByPaymentLinkIdOrderByCreatedAtDesc(paymentLinkId: String): RazorpayPaymentLinkRequest? }