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 } }