diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/PayuWebhookCapture.kt b/src/main/kotlin/com/android/trisolarisserver/controller/PayuWebhookCapture.kt index 38389ed..ec5a76a 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/PayuWebhookCapture.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/PayuWebhookCapture.kt @@ -1,7 +1,11 @@ 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.PayuWebhookLog +import com.android.trisolarisserver.db.repo.BookingRepo 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 @@ -12,18 +16,28 @@ 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 payuWebhookLogRepo: PayuWebhookLogRepo ) { @PostMapping @ResponseStatus(HttpStatus.NO_CONTENT) + @Transactional fun capture( @PathVariable propertyId: UUID, @RequestBody(required = false) body: String?, @@ -43,5 +57,82 @@ class PayuWebhookCapture( 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" + if (!isSuccess && !isRefund) return + + val bookingId = data["udf1"]?.let { runCatching { UUID.fromString(it) }.getOrNull() } + if (bookingId == null) return + val booking = bookingRepo.findById(bookingId).orElse(null) ?: return + if (booking.property.id != propertyId) return + + val referenceId = data["mihpayid"]?.ifBlank { null } ?: data["txnid"]?.ifBlank { null } + val reference = referenceId?.let { "payu:$it" } + if (reference != null && paymentRepo.findByReference(reference) != null) return + + val amountRaw = data["amount"]?.ifBlank { null } ?: data["net_amount_debit"]?.ifBlank { null } + val amount = parseAmount(amountRaw) ?: return + val signedAmount = if (isRefund) -amount else amount + + val receivedAt = parseAddedOn(data["addedon"], booking.property.timezone) + val notes = buildString { + append("payu status=").append(status) + data["txnid"]?.let { append(" txnid=").append(it) } + data["bank_ref_num"]?.let { append(" bank_ref=").append(it) } + } + + paymentRepo.save( + Payment( + property = booking.property, + booking = booking, + amount = signedAmount, + currency = booking.property.currency, + method = PaymentMethod.ONLINE, + reference = reference, + 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) + } } } diff --git a/src/main/kotlin/com/android/trisolarisserver/repo/PaymentRepo.kt b/src/main/kotlin/com/android/trisolarisserver/repo/PaymentRepo.kt index bd1caf0..1f36ed3 100644 --- a/src/main/kotlin/com/android/trisolarisserver/repo/PaymentRepo.kt +++ b/src/main/kotlin/com/android/trisolarisserver/repo/PaymentRepo.kt @@ -8,6 +8,7 @@ import java.util.UUID interface PaymentRepo : JpaRepository { fun findByBookingIdOrderByReceivedAtDesc(bookingId: UUID): List + fun findByReference(reference: String): Payment? @Query( """