package com.android.trisolarisserver.controller import com.android.trisolarisserver.models.booking.Payment import com.android.trisolarisserver.models.booking.PaymentMethod import com.android.trisolarisserver.models.payment.PayuPaymentAttempt import com.android.trisolarisserver.models.payment.PayuWebhookLog import com.android.trisolarisserver.db.repo.BookingRepo import com.android.trisolarisserver.repo.PayuPaymentAttemptRepo import com.android.trisolarisserver.repo.PayuWebhookLogRepo import com.android.trisolarisserver.repo.PaymentRepo import com.android.trisolarisserver.repo.PropertyRepo import jakarta.servlet.http.HttpServletRequest import org.springframework.http.HttpStatus 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 org.springframework.transaction.annotation.Transactional import java.math.BigDecimal import java.net.URLDecoder import java.time.OffsetDateTime import java.time.LocalDateTime import java.time.ZoneId import java.time.ZoneOffset import java.time.format.DateTimeFormatter import java.util.UUID @RestController @RequestMapping("/properties/{propertyId}/payu/webhook") class PayuWebhookCapture( private val propertyRepo: PropertyRepo, private val bookingRepo: BookingRepo, private val paymentRepo: PaymentRepo, private val payuPaymentAttemptRepo: PayuPaymentAttemptRepo, private val payuWebhookLogRepo: PayuWebhookLogRepo ) { @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 headers = request.headerNames.toList().associateWith { request.getHeader(it) } val headersText = headers.entries.joinToString("\n") { (k, v) -> "$k: $v" } payuWebhookLogRepo.save( PayuWebhookLog( property = property, headers = headersText, payload = body, contentType = request.contentType, receivedAt = OffsetDateTime.now() ) ) if (body.isNullOrBlank()) return val data = parseFormBody(body) val status = data["status"]?.lowercase() ?: data["unmappedstatus"]?.lowercase() val isSuccess = status == "success" || status == "captured" val isRefund = status == "refund" || status == "refunded" val bookingId = data["udf1"]?.let { runCatching { UUID.fromString(it) }.getOrNull() } val booking = bookingId?.let { bookingRepo.findById(it).orElse(null) } if (booking != null && booking.property.id != propertyId) return val amountRaw = data["amount"]?.ifBlank { null } ?: data["net_amount_debit"]?.ifBlank { null } val amount = parseAmount(amountRaw) val gatewayPaymentId = data["mihpayid"]?.ifBlank { null } val gatewayTxnId = data["txnid"]?.ifBlank { null } val bankRef = data["bank_ref_num"]?.ifBlank { null } ?: data["bank_ref_no"]?.ifBlank { null } val mode = data["mode"]?.ifBlank { null } val pgType = data["PG_TYPE"]?.ifBlank { null } val payerVpa = data["field3"]?.ifBlank { null } val payerName = data["field6"]?.ifBlank { null } val paymentSource = data["payment_source"]?.ifBlank { null } val errorCode = data["error"]?.ifBlank { null } val errorMessage = data["error_Message"]?.ifBlank { null } val receivedAt = parseAddedOn(data["addedon"], booking?.property?.timezone) payuPaymentAttemptRepo.save( PayuPaymentAttempt( property = property, booking = booking, status = status, unmappedStatus = data["unmappedstatus"]?.ifBlank { null }, amount = amount, currency = booking?.property?.currency ?: property.currency, gatewayPaymentId = gatewayPaymentId, gatewayTxnId = gatewayTxnId, bankRefNum = bankRef, mode = mode, pgType = pgType, payerVpa = payerVpa, payerName = payerName, paymentSource = paymentSource, errorCode = errorCode, errorMessage = errorMessage, payload = body, receivedAt = receivedAt ) ) if (!isSuccess && !isRefund) return if (booking == null) return if (gatewayPaymentId != null && paymentRepo.findByGatewayPaymentId(gatewayPaymentId) != null) return if (gatewayPaymentId == null && gatewayTxnId != null && paymentRepo.findByGatewayTxnId(gatewayTxnId) != null) return val signedAmount = amount?.let { if (isRefund) -it else it } ?: return val notes = buildString { append("payu status=").append(status) gatewayTxnId?.let { append(" txnid=").append(it) } bankRef?.let { append(" bank_ref=").append(it) } } paymentRepo.save( Payment( property = booking.property, booking = booking, amount = signedAmount, currency = booking.property.currency, method = PaymentMethod.ONLINE, gatewayPaymentId = gatewayPaymentId, gatewayTxnId = gatewayTxnId, bankRefNum = bankRef, mode = mode, pgType = pgType, payerVpa = payerVpa, payerName = payerName, paymentSource = paymentSource, reference = gatewayPaymentId?.let { "payu:$it" } ?: gatewayTxnId?.let { "payu:$it" }, notes = notes, receivedAt = receivedAt ) ) } private fun parseFormBody(body: String): Map { return body.split("&") .mapNotNull { pair -> val idx = pair.indexOf("=") if (idx <= 0) return@mapNotNull null val key = URLDecoder.decode(pair.substring(0, idx), "UTF-8") val value = URLDecoder.decode(pair.substring(idx + 1), "UTF-8") key to value } .toMap() } private fun parseAmount(value: String?): Long? { if (value.isNullOrBlank()) return null return try { val bd = BigDecimal(value.trim()).setScale(0, java.math.RoundingMode.HALF_UP) bd.longValueExact() } catch (_: Exception) { null } } private fun parseAddedOn(value: String?, timezone: String?): OffsetDateTime { if (value.isNullOrBlank()) return OffsetDateTime.now() return try { val local = LocalDateTime.parse(value.trim(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) val zone = try { if (timezone.isNullOrBlank()) ZoneId.of("Asia/Kolkata") else ZoneId.of(timezone) } catch (_: Exception) { ZoneId.of("Asia/Kolkata") } local.atZone(zone).toOffsetDateTime() } catch (_: Exception) { OffsetDateTime.now(ZoneOffset.UTC) } } }