Replace PayU integration with Razorpay
All checks were successful
build-and-deploy / build-deploy (push) Successful in 33s
All checks were successful
build-and-deploy / build-deploy (push) Successful in 33s
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
package com.android.trisolarisserver.controller
|
||||
|
||||
import com.android.trisolarisserver.db.repo.BookingRepo
|
||||
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.repo.PaymentRepo
|
||||
import com.android.trisolarisserver.repo.PropertyRepo
|
||||
import com.android.trisolarisserver.repo.RazorpayPaymentAttemptRepo
|
||||
import com.android.trisolarisserver.repo.RazorpaySettingsRepo
|
||||
import com.android.trisolarisserver.repo.RazorpayWebhookLogRepo
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
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.ResponseStatus
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/properties/{propertyId}/razorpay/webhook")
|
||||
class RazorpayWebhookCapture(
|
||||
private val propertyRepo: PropertyRepo,
|
||||
private val bookingRepo: BookingRepo,
|
||||
private val paymentRepo: PaymentRepo,
|
||||
private val settingsRepo: RazorpaySettingsRepo,
|
||||
private val razorpayPaymentAttemptRepo: RazorpayPaymentAttemptRepo,
|
||||
private val razorpayWebhookLogRepo: RazorpayWebhookLogRepo,
|
||||
private val objectMapper: ObjectMapper
|
||||
) {
|
||||
|
||||
@PostMapping
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@Transactional
|
||||
fun capture(
|
||||
@PathVariable propertyId: UUID,
|
||||
@RequestBody(required = false) body: String?,
|
||||
request: HttpServletRequest
|
||||
) {
|
||||
val property = propertyRepo.findById(propertyId).orElseThrow {
|
||||
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
|
||||
}
|
||||
val settings = settingsRepo.findByPropertyId(propertyId)
|
||||
?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Razorpay settings not configured")
|
||||
|
||||
val headers = request.headerNames.toList().associateWith { request.getHeader(it) }
|
||||
val headersText = headers.entries.joinToString("\n") { (k, v) -> "$k: $v" }
|
||||
razorpayWebhookLogRepo.save(
|
||||
RazorpayWebhookLog(
|
||||
property = property,
|
||||
headers = headersText,
|
||||
payload = body,
|
||||
contentType = request.contentType,
|
||||
receivedAt = OffsetDateTime.now()
|
||||
)
|
||||
)
|
||||
|
||||
if (body.isNullOrBlank()) return
|
||||
val signature = request.getHeader("X-Razorpay-Signature")
|
||||
?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing signature")
|
||||
val secret = settings.webhookSecret
|
||||
?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Webhook secret not configured")
|
||||
if (!verifySignature(body, secret, signature)) {
|
||||
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid signature")
|
||||
}
|
||||
|
||||
val root = objectMapper.readTree(body)
|
||||
val event = root.path("event").asText(null)
|
||||
val paymentEntity = root.path("payload").path("payment").path("entity")
|
||||
val orderEntity = root.path("payload").path("order").path("entity")
|
||||
val paymentId = paymentEntity.path("id").asText(null)
|
||||
val orderId = paymentEntity.path("order_id").asText(null).ifBlank { null }
|
||||
?: orderEntity.path("id").asText(null)
|
||||
val status = paymentEntity.path("status").asText(null)
|
||||
val amountPaise = paymentEntity.path("amount").asLong(0)
|
||||
val currency = paymentEntity.path("currency").asText(property.currency)
|
||||
val notes = paymentEntity.path("notes")
|
||||
val bookingId = notes.path("bookingId").asText(null)?.let { runCatching { UUID.fromString(it) }.getOrNull() }
|
||||
?: orderEntity.path("receipt").asText(null)?.let { runCatching { UUID.fromString(it) }.getOrNull() }
|
||||
val booking = bookingId?.let { bookingRepo.findById(it).orElse(null) }
|
||||
if (booking != null && booking.property.id != propertyId) return
|
||||
|
||||
razorpayPaymentAttemptRepo.save(
|
||||
RazorpayPaymentAttempt(
|
||||
property = property,
|
||||
booking = booking,
|
||||
event = event,
|
||||
status = status,
|
||||
amount = paiseToAmount(amountPaise),
|
||||
currency = currency,
|
||||
paymentId = paymentId,
|
||||
orderId = orderId,
|
||||
payload = body,
|
||||
receivedAt = OffsetDateTime.now()
|
||||
)
|
||||
)
|
||||
|
||||
if (event == null || paymentId == null || booking == null) return
|
||||
if (event != "payment.captured" && event != "refund.processed") return
|
||||
|
||||
if (paymentRepo.findByGatewayPaymentId(paymentId) != null) return
|
||||
|
||||
val signedAmount = if (event == "refund.processed") -paiseToAmount(amountPaise) else paiseToAmount(amountPaise)
|
||||
val notesText = "razorpay event=$event status=$status order_id=$orderId"
|
||||
paymentRepo.save(
|
||||
Payment(
|
||||
property = booking.property,
|
||||
booking = booking,
|
||||
amount = signedAmount,
|
||||
currency = booking.property.currency,
|
||||
method = PaymentMethod.ONLINE,
|
||||
gatewayPaymentId = paymentId,
|
||||
gatewayTxnId = orderId,
|
||||
reference = "razorpay:$paymentId",
|
||||
notes = notesText,
|
||||
receivedAt = OffsetDateTime.now()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun verifySignature(payload: String, secret: String, signature: String): Boolean {
|
||||
return try {
|
||||
val mac = Mac.getInstance("HmacSHA256")
|
||||
mac.init(SecretKeySpec(secret.toByteArray(), "HmacSHA256"))
|
||||
val hash = mac.doFinal(payload.toByteArray()).joinToString("") { "%02x".format(it) }
|
||||
hash.equals(signature, ignoreCase = true)
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun paiseToAmount(paise: Long): Long {
|
||||
return paise / 100
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user