160 lines
7.1 KiB
Kotlin
160 lines
7.1 KiB
Kotlin
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.RazorpayQrRequestRepo
|
|
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 razorpayQrRequestRepo: RazorpayQrRequestRepo,
|
|
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 qrEntity = root.path("payload").path("qr_code").path("entity")
|
|
val paymentId = paymentEntity.path("id").asText(null)
|
|
val orderId = paymentEntity.path("order_id").asText(null)?.takeIf { it.isNotBlank() }
|
|
?: orderEntity.path("id").asText(null)?.takeIf { it.isNotBlank() }
|
|
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
|
|
|
|
val qrId = qrEntity.path("id").asText(null)
|
|
val qrStatus = qrEntity.path("status").asText(null)
|
|
if (event != null && event.startsWith("qr_code.") && qrId != null) {
|
|
val existingQr = razorpayQrRequestRepo.findTopByQrIdOrderByCreatedAtDesc(qrId)
|
|
if (existingQr != null) {
|
|
if (!qrStatus.isNullOrBlank()) {
|
|
existingQr.status = qrStatus
|
|
razorpayQrRequestRepo.save(existingQr)
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|