From 14c86210c21045920f5d8d9e5a71f082fc3ab260 Mon Sep 17 00:00:00 2001 From: androidlover5842 Date: Sun, 1 Feb 2026 15:34:46 +0530 Subject: [PATCH] Add Razorpay refund endpoint --- .../controller/RazorpayRefundsController.kt | 135 ++++++++++++++++++ .../controller/dto/RazorpayDtos.kt | 14 ++ 2 files changed, 149 insertions(+) create mode 100644 src/main/kotlin/com/android/trisolarisserver/controller/RazorpayRefundsController.kt diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayRefundsController.kt b/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayRefundsController.kt new file mode 100644 index 0000000..e84eb2f --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayRefundsController.kt @@ -0,0 +1,135 @@ +package com.android.trisolarisserver.controller + +import com.android.trisolarisserver.component.PropertyAccess +import com.android.trisolarisserver.controller.dto.RazorpayRefundRequest +import com.android.trisolarisserver.controller.dto.RazorpayRefundResponse +import com.android.trisolarisserver.db.repo.BookingRepo +import com.android.trisolarisserver.models.property.Role +import com.android.trisolarisserver.repo.PaymentRepo +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.ResponseEntity +import org.springframework.http.HttpStatus +import org.springframework.security.core.annotation.AuthenticationPrincipal +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 +@RequestMapping("/properties/{propertyId}/bookings/{bookingId}/payments/razorpay") +class RazorpayRefundsController( + private val propertyAccess: PropertyAccess, + private val bookingRepo: BookingRepo, + private val paymentRepo: PaymentRepo, + private val settingsRepo: RazorpaySettingsRepo, + private val restTemplate: RestTemplate, + private val objectMapper: ObjectMapper +) { + + @PostMapping("/refund") + fun refund( + @PathVariable propertyId: UUID, + @PathVariable bookingId: UUID, + @RequestBody request: RazorpayRefundRequest, + @AuthenticationPrincipal principal: MyPrincipal? + ): RazorpayRefundResponse { + requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER, Role.FINANCE) + 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 paymentId = request.paymentId + val razorpayPaymentId = request.razorpayPaymentId?.trim()?.ifBlank { null } + if ((paymentId == null && razorpayPaymentId == null) || (paymentId != null && razorpayPaymentId != null)) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Provide exactly one of paymentId or razorpayPaymentId") + } + + val gatewayPaymentId = if (razorpayPaymentId != null) { + razorpayPaymentId + } else { + val payment = paymentRepo.findById(paymentId!!).orElseThrow { + ResponseStatusException(HttpStatus.NOT_FOUND, "Payment not found") + } + if (payment.booking.id != bookingId || payment.property.id != propertyId) { + throw ResponseStatusException(HttpStatus.NOT_FOUND, "Payment not found for booking") + } + payment.gatewayPaymentId + ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Payment is missing gateway id") + } + + val settings = settingsRepo.findByPropertyId(propertyId) + ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Razorpay settings not configured") + + val payload = linkedMapOf() + request.amount?.let { + if (it <= 0) throw ResponseStatusException(HttpStatus.BAD_REQUEST, "amount must be > 0") + payload["amount"] = it * 100 + } + request.notes?.trim()?.takeIf { it.isNotBlank() }?.let { payload["notes"] = mapOf("note" to it) } + + val response = postJson(resolveBaseUrl(settings.isTest) + "/payments/$gatewayPaymentId/refund", settings, objectMapper.writeValueAsString(payload)) + if (!response.statusCode.is2xxSuccessful) { + throw ResponseStatusException(HttpStatus.BAD_GATEWAY, "Razorpay refund request failed") + } + val body = response.body ?: "{}" + val node = objectMapper.readTree(body) + val refundId = node.path("id").asText(null) + val status = node.path("status").asText(null) + val amount = node.path("amount").asLong(0).let { if (it == 0L) null else it / 100 } + val currency = node.path("currency").asText(null) + return RazorpayRefundResponse( + refundId = refundId, + status = status, + amount = amount, + currency = currency + ) + } + + private fun postJson( + url: String, + settings: com.android.trisolarisserver.models.payment.RazorpaySettings, + json: String + ): ResponseEntity { + val (keyId, keySecret) = requireKeys(settings) + val headers = HttpHeaders() + headers.contentType = MediaType.APPLICATION_JSON + headers.set(HttpHeaders.AUTHORIZATION, basicAuth(keyId, 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" + } + + private fun requireKeys(settings: com.android.trisolarisserver.models.payment.RazorpaySettings): Pair { + val keyId = settings.resolveKeyId() + ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Razorpay test keys not configured") + val keySecret = settings.resolveKeySecret() + ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Razorpay test keys not configured") + return keyId to keySecret + } +} 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 3bd81b8..8583916 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/dto/RazorpayDtos.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/dto/RazorpayDtos.kt @@ -106,3 +106,17 @@ data class RazorpayPaymentRequestCloseResponse( val paymentLinkId: String? = null, val status: String? = null ) + +data class RazorpayRefundRequest( + val paymentId: UUID? = null, + val razorpayPaymentId: String? = null, + val amount: Long? = null, + val notes: String? = null +) + +data class RazorpayRefundResponse( + val refundId: String?, + val status: String?, + val amount: Long?, + val currency: String? +)