Add Razorpay refund endpoint
All checks were successful
build-and-deploy / build-deploy (push) Successful in 35s
All checks were successful
build-and-deploy / build-deploy (push) Successful in 35s
This commit is contained in:
@@ -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<String, Any>()
|
||||||
|
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<String> {
|
||||||
|
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<String, String> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -106,3 +106,17 @@ data class RazorpayPaymentRequestCloseResponse(
|
|||||||
val paymentLinkId: String? = null,
|
val paymentLinkId: String? = null,
|
||||||
val status: 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?
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user