From 4168835d472130e5da8549cedacd9bb01865b365 Mon Sep 17 00:00:00 2001 From: androidlover5842 Date: Fri, 30 Jan 2026 05:48:09 +0530 Subject: [PATCH] Add PayU webhook capture per property --- .../config/PayuWebhookLogSchemaFix.kt | 36 ++++++++++++++ .../controller/PayuWebhookCapture.kt | 47 +++++++++++++++++++ .../models/payment/PayuWebhookLog.kt | 38 +++++++++++++++ .../repo/PayuWebhookLogRepo.kt | 7 +++ .../security/PublicEndpoints.kt | 2 + 5 files changed, 130 insertions(+) create mode 100644 src/main/kotlin/com/android/trisolarisserver/config/PayuWebhookLogSchemaFix.kt create mode 100644 src/main/kotlin/com/android/trisolarisserver/controller/PayuWebhookCapture.kt create mode 100644 src/main/kotlin/com/android/trisolarisserver/models/payment/PayuWebhookLog.kt create mode 100644 src/main/kotlin/com/android/trisolarisserver/repo/PayuWebhookLogRepo.kt diff --git a/src/main/kotlin/com/android/trisolarisserver/config/PayuWebhookLogSchemaFix.kt b/src/main/kotlin/com/android/trisolarisserver/config/PayuWebhookLogSchemaFix.kt new file mode 100644 index 0000000..9e2430d --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/config/PayuWebhookLogSchemaFix.kt @@ -0,0 +1,36 @@ +package com.android.trisolarisserver.config + +import org.springframework.jdbc.core.JdbcTemplate +import org.springframework.stereotype.Component + +@Component +class PayuWebhookLogSchemaFix( + private val jdbcTemplate: JdbcTemplate +) : PostgresSchemaFix(jdbcTemplate) { + + override fun runPostgres(jdbcTemplate: JdbcTemplate) { + val hasTable = jdbcTemplate.queryForObject( + """ + select count(*) + from information_schema.tables + where table_name = 'payu_webhook_log' + """.trimIndent(), + Int::class.java + ) ?: 0 + if (hasTable == 0) { + logger.info("Creating payu_webhook_log table") + jdbcTemplate.execute( + """ + create table payu_webhook_log ( + id uuid primary key, + property_id uuid not null references property(id) on delete cascade, + headers text, + payload text, + content_type varchar, + received_at timestamptz not null + ) + """.trimIndent() + ) + } + } +} diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/PayuWebhookCapture.kt b/src/main/kotlin/com/android/trisolarisserver/controller/PayuWebhookCapture.kt new file mode 100644 index 0000000..38389ed --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/controller/PayuWebhookCapture.kt @@ -0,0 +1,47 @@ +package com.android.trisolarisserver.controller + +import com.android.trisolarisserver.models.payment.PayuWebhookLog +import com.android.trisolarisserver.repo.PayuWebhookLogRepo +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 java.time.OffsetDateTime +import java.util.UUID + +@RestController +@RequestMapping("/properties/{propertyId}/payu/webhook") +class PayuWebhookCapture( + private val propertyRepo: PropertyRepo, + private val payuWebhookLogRepo: PayuWebhookLogRepo +) { + + @PostMapping + @ResponseStatus(HttpStatus.NO_CONTENT) + 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() + ) + ) + } +} diff --git a/src/main/kotlin/com/android/trisolarisserver/models/payment/PayuWebhookLog.kt b/src/main/kotlin/com/android/trisolarisserver/models/payment/PayuWebhookLog.kt new file mode 100644 index 0000000..cd2df42 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/models/payment/PayuWebhookLog.kt @@ -0,0 +1,38 @@ +package com.android.trisolarisserver.models.payment + +import com.android.trisolarisserver.models.property.Property +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.Table +import java.time.OffsetDateTime +import java.util.UUID + +@Entity +@Table(name = "payu_webhook_log") +class PayuWebhookLog( + @Id + @GeneratedValue + @Column(columnDefinition = "uuid") + val id: UUID? = null, + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "property_id", nullable = false) + var property: Property, + + @Column(name = "headers", columnDefinition = "text") + var headers: String? = null, + + @Column(name = "payload", columnDefinition = "text") + var payload: String? = null, + + @Column(name = "content_type") + var contentType: String? = null, + + @Column(name = "received_at", nullable = false, columnDefinition = "timestamptz") + val receivedAt: OffsetDateTime = OffsetDateTime.now() +) diff --git a/src/main/kotlin/com/android/trisolarisserver/repo/PayuWebhookLogRepo.kt b/src/main/kotlin/com/android/trisolarisserver/repo/PayuWebhookLogRepo.kt new file mode 100644 index 0000000..15f62f9 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/repo/PayuWebhookLogRepo.kt @@ -0,0 +1,7 @@ +package com.android.trisolarisserver.repo + +import com.android.trisolarisserver.models.payment.PayuWebhookLog +import org.springframework.data.jpa.repository.JpaRepository +import java.util.UUID + +interface PayuWebhookLogRepo : JpaRepository diff --git a/src/main/kotlin/com/android/trisolarisserver/security/PublicEndpoints.kt b/src/main/kotlin/com/android/trisolarisserver/security/PublicEndpoints.kt index 0af8a3f..3d78caa 100644 --- a/src/main/kotlin/com/android/trisolarisserver/security/PublicEndpoints.kt +++ b/src/main/kotlin/com/android/trisolarisserver/security/PublicEndpoints.kt @@ -10,6 +10,7 @@ internal object PublicEndpoints { private val roomTypes = Regex("^/properties/[^/]+/room-types$") private val roomTypeImages = Regex("^/properties/[^/]+/room-types/[^/]+/images$") private val iconPngFile = Regex("^/icons/png/[^/]+$") + private val payuWebhook = Regex("^/properties/[^/]+/payu/webhook$") fun isPublic(request: HttpServletRequest): Boolean { val path = request.requestURI @@ -23,6 +24,7 @@ internal object PublicEndpoints { || (roomsByType.matches(path) && method == "GET") || (roomTypes.matches(path) && method == "GET") || roomTypeImages.matches(path) + || (payuWebhook.matches(path) && method == "POST") || (path == "/image-tags" && method == "GET") || path == "/icons/png" || iconPngFile.matches(path)