diff --git a/src/main/kotlin/com/android/trisolarisserver/config/PaymentSchemaFix.kt b/src/main/kotlin/com/android/trisolarisserver/config/PaymentSchemaFix.kt new file mode 100644 index 0000000..631f754 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/config/PaymentSchemaFix.kt @@ -0,0 +1,37 @@ +package com.android.trisolarisserver.config + +import org.springframework.jdbc.core.JdbcTemplate +import org.springframework.stereotype.Component + +@Component +class PaymentSchemaFix( + private val jdbcTemplate: JdbcTemplate +) : PostgresSchemaFix(jdbcTemplate) { + + override fun runPostgres(jdbcTemplate: JdbcTemplate) { + ensureColumn("payment", "gateway_payment_id", "varchar") + ensureColumn("payment", "gateway_txn_id", "varchar") + ensureColumn("payment", "bank_ref_num", "varchar") + ensureColumn("payment", "mode", "varchar") + ensureColumn("payment", "pg_type", "varchar") + ensureColumn("payment", "payer_vpa", "varchar") + ensureColumn("payment", "payer_name", "varchar") + ensureColumn("payment", "payment_source", "varchar") + } + + private fun ensureColumn(table: String, column: String, type: String) { + val exists = jdbcTemplate.queryForObject( + """ + select count(*) + from information_schema.columns + where table_name = '$table' + and column_name = '$column' + """.trimIndent(), + Int::class.java + ) ?: 0 + if (exists == 0) { + logger.info("Adding $table.$column column") + jdbcTemplate.execute("alter table $table add column $column $type") + } + } +} diff --git a/src/main/kotlin/com/android/trisolarisserver/config/PayuPaymentAttemptSchemaFix.kt b/src/main/kotlin/com/android/trisolarisserver/config/PayuPaymentAttemptSchemaFix.kt new file mode 100644 index 0000000..3b3add6 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/config/PayuPaymentAttemptSchemaFix.kt @@ -0,0 +1,49 @@ +package com.android.trisolarisserver.config + +import org.springframework.jdbc.core.JdbcTemplate +import org.springframework.stereotype.Component + +@Component +class PayuPaymentAttemptSchemaFix( + 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_payment_attempt' + """.trimIndent(), + Int::class.java + ) ?: 0 + if (hasTable == 0) { + logger.info("Creating payu_payment_attempt table") + jdbcTemplate.execute( + """ + create table payu_payment_attempt ( + id uuid primary key, + property_id uuid not null references property(id) on delete cascade, + booking_id uuid references booking(id) on delete set null, + status varchar, + unmapped_status varchar, + amount bigint, + currency varchar, + gateway_payment_id varchar, + gateway_txn_id varchar, + bank_ref_num varchar, + mode varchar, + pg_type varchar, + payer_vpa varchar, + payer_name varchar, + payment_source varchar, + error_code varchar, + error_message varchar, + payload text, + 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 index ec5a76a..8860fa6 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/PayuWebhookCapture.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/PayuWebhookCapture.kt @@ -2,8 +2,10 @@ 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 @@ -32,6 +34,7 @@ class PayuWebhookCapture( private val propertyRepo: PropertyRepo, private val bookingRepo: BookingRepo, private val paymentRepo: PaymentRepo, + private val payuPaymentAttemptRepo: PayuPaymentAttemptRepo, private val payuWebhookLogRepo: PayuWebhookLogRepo ) { @@ -63,26 +66,58 @@ class PayuWebhookCapture( 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 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) ?: return - val signedAmount = if (isRefund) -amount else amount + 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) - 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) - data["txnid"]?.let { append(" txnid=").append(it) } - data["bank_ref_num"]?.let { append(" bank_ref=").append(it) } + gatewayTxnId?.let { append(" txnid=").append(it) } + bankRef?.let { append(" bank_ref=").append(it) } } paymentRepo.save( @@ -92,7 +127,15 @@ class PayuWebhookCapture( amount = signedAmount, currency = booking.property.currency, method = PaymentMethod.ONLINE, - reference = reference, + 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 ) diff --git a/src/main/kotlin/com/android/trisolarisserver/models/booking/Payment.kt b/src/main/kotlin/com/android/trisolarisserver/models/booking/Payment.kt index 85c47d2..c05fedd 100644 --- a/src/main/kotlin/com/android/trisolarisserver/models/booking/Payment.kt +++ b/src/main/kotlin/com/android/trisolarisserver/models/booking/Payment.kt @@ -32,6 +32,30 @@ class Payment( @Column(name = "method", nullable = false) var method: PaymentMethod, + @Column(name = "gateway_payment_id") + var gatewayPaymentId: String? = null, + + @Column(name = "gateway_txn_id") + var gatewayTxnId: String? = null, + + @Column(name = "bank_ref_num") + var bankRefNum: String? = null, + + @Column(name = "mode") + var mode: String? = null, + + @Column(name = "pg_type") + var pgType: String? = null, + + @Column(name = "payer_vpa") + var payerVpa: String? = null, + + @Column(name = "payer_name") + var payerName: String? = null, + + @Column(name = "payment_source") + var paymentSource: String? = null, + @Column(name = "reference") var reference: String? = null, diff --git a/src/main/kotlin/com/android/trisolarisserver/models/payment/PayuPaymentAttempt.kt b/src/main/kotlin/com/android/trisolarisserver/models/payment/PayuPaymentAttempt.kt new file mode 100644 index 0000000..eb9fc99 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/models/payment/PayuPaymentAttempt.kt @@ -0,0 +1,79 @@ +package com.android.trisolarisserver.models.payment + +import com.android.trisolarisserver.models.booking.Booking +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_payment_attempt") +class PayuPaymentAttempt( + @Id + @GeneratedValue + @Column(columnDefinition = "uuid") + val id: UUID? = null, + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "property_id", nullable = false) + var property: Property, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "booking_id") + var booking: Booking? = null, + + @Column(name = "status") + var status: String? = null, + + @Column(name = "unmapped_status") + var unmappedStatus: String? = null, + + @Column(name = "amount") + var amount: Long? = null, + + @Column(name = "currency") + var currency: String? = null, + + @Column(name = "gateway_payment_id") + var gatewayPaymentId: String? = null, + + @Column(name = "gateway_txn_id") + var gatewayTxnId: String? = null, + + @Column(name = "bank_ref_num") + var bankRefNum: String? = null, + + @Column(name = "mode") + var mode: String? = null, + + @Column(name = "pg_type") + var pgType: String? = null, + + @Column(name = "payer_vpa") + var payerVpa: String? = null, + + @Column(name = "payer_name") + var payerName: String? = null, + + @Column(name = "payment_source") + var paymentSource: String? = null, + + @Column(name = "error_code") + var errorCode: String? = null, + + @Column(name = "error_message") + var errorMessage: String? = null, + + @Column(name = "payload", columnDefinition = "text") + var payload: String? = null, + + @Column(name = "received_at", columnDefinition = "timestamptz") + var receivedAt: OffsetDateTime = OffsetDateTime.now() +) diff --git a/src/main/kotlin/com/android/trisolarisserver/repo/PaymentRepo.kt b/src/main/kotlin/com/android/trisolarisserver/repo/PaymentRepo.kt index 1f36ed3..ab99710 100644 --- a/src/main/kotlin/com/android/trisolarisserver/repo/PaymentRepo.kt +++ b/src/main/kotlin/com/android/trisolarisserver/repo/PaymentRepo.kt @@ -9,6 +9,8 @@ import java.util.UUID interface PaymentRepo : JpaRepository { fun findByBookingIdOrderByReceivedAtDesc(bookingId: UUID): List fun findByReference(reference: String): Payment? + fun findByGatewayPaymentId(gatewayPaymentId: String): Payment? + fun findByGatewayTxnId(gatewayTxnId: String): Payment? @Query( """ diff --git a/src/main/kotlin/com/android/trisolarisserver/repo/PayuPaymentAttemptRepo.kt b/src/main/kotlin/com/android/trisolarisserver/repo/PayuPaymentAttemptRepo.kt new file mode 100644 index 0000000..135f798 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/repo/PayuPaymentAttemptRepo.kt @@ -0,0 +1,7 @@ +package com.android.trisolarisserver.repo + +import com.android.trisolarisserver.models.payment.PayuPaymentAttempt +import org.springframework.data.jpa.repository.JpaRepository +import java.util.UUID + +interface PayuPaymentAttemptRepo : JpaRepository