diff --git a/src/main/kotlin/com/android/trisolarisserver/config/PayuQrRequestSchemaFix.kt b/src/main/kotlin/com/android/trisolarisserver/config/PayuQrRequestSchemaFix.kt index b60a793..f8b5705 100644 --- a/src/main/kotlin/com/android/trisolarisserver/config/PayuQrRequestSchemaFix.kt +++ b/src/main/kotlin/com/android/trisolarisserver/config/PayuQrRequestSchemaFix.kt @@ -31,10 +31,30 @@ class PayuQrRequestSchemaFix( status varchar not null, request_payload text, response_payload text, + expiry_at timestamptz, created_at timestamptz not null ) """.trimIndent() ) + } else { + val hasExpiryAt = jdbcTemplate.queryForObject( + """ + select count(*) + from information_schema.columns + where table_name = 'payu_qr_request' + and column_name = 'expiry_at' + """.trimIndent(), + Int::class.java + ) ?: 0 + if (hasExpiryAt == 0) { + logger.info("Adding expiry_at to payu_qr_request table") + jdbcTemplate.execute( + """ + alter table payu_qr_request + add column expiry_at timestamptz + """.trimIndent() + ) + } } } } diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/Payments.kt b/src/main/kotlin/com/android/trisolarisserver/controller/Payments.kt index c7928d5..5f12d81 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/Payments.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/Payments.kt @@ -4,6 +4,7 @@ import com.android.trisolarisserver.component.PropertyAccess import com.android.trisolarisserver.controller.dto.PaymentCreateRequest import com.android.trisolarisserver.controller.dto.PaymentResponse import com.android.trisolarisserver.db.repo.BookingRepo +import com.android.trisolarisserver.models.booking.BookingStatus import com.android.trisolarisserver.models.booking.Payment import com.android.trisolarisserver.models.booking.PaymentMethod import com.android.trisolarisserver.models.property.Role @@ -105,6 +106,12 @@ class Payments( if (booking.property.id != propertyId) { throw ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found for property") } + if (booking.status != BookingStatus.OPEN && booking.status != BookingStatus.CHECKED_IN) { + throw ResponseStatusException( + HttpStatus.BAD_REQUEST, + "Cash payments can only be deleted for OPEN or CHECKED_IN bookings" + ) + } val payment = paymentRepo.findById(paymentId).orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Payment not found") } diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/PayuQrPayments.kt b/src/main/kotlin/com/android/trisolarisserver/controller/PayuQrPayments.kt index 828b4cd..a016db4 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/PayuQrPayments.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/PayuQrPayments.kt @@ -41,6 +41,7 @@ class PayuQrPayments( private val payuQrRequestRepo: PayuQrRequestRepo, private val restTemplate: RestTemplate ) { + private val defaultExpirySeconds = 30 * 60 @PostMapping("/qr") @Transactional @@ -79,6 +80,31 @@ class PayuQrPayments( throw ResponseStatusException(HttpStatus.CONFLICT, "Amount exceeds pending") } val amountLong = requestedAmount ?: pending + val expirySeconds = request.expirySeconds + ?: request.expiryMinutes?.let { it * 60 } + ?: defaultExpirySeconds + + val existing = payuQrRequestRepo.findTopByBookingIdAndAmountAndCurrencyAndStatusOrderByCreatedAtDesc( + bookingId, + amountLong, + booking.property.currency, + PayuQrStatus.SENT + ) + if (existing != null) { + val expiryAt = existing.expiryAt + val responsePayload = existing.responsePayload + if (expiryAt != null && !responsePayload.isNullOrBlank()) { + val now = OffsetDateTime.now() + if (now.isBefore(expiryAt)) { + return PayuQrGenerateResponse( + txnid = existing.txnid, + amount = amountLong, + currency = booking.property.currency, + payuResponse = responsePayload + ) + } + } + } val txnid = "QR${bookingId.toString().substring(0, 8)}${System.currentTimeMillis()}" val productInfo = "Booking $bookingId" @@ -132,9 +158,9 @@ class PayuQrPayments( add("hash", hash) add("udf1", udf1) add("udf2", udf2) - if (udf3.isNotBlank()) add("udf3", udf3) - if (udf4.isNotBlank()) add("udf4", udf4) - if (udf5.isNotBlank()) add("udf5", udf5) + add("udf3", udf3) // always + add("udf4", udf4) // always + add("udf5", udf5) // always add("txn_s2s_flow", "4") val clientIp = request.clientIp?.trim()?.ifBlank { null } ?: extractClientIp(httpRequest) @@ -143,9 +169,7 @@ class PayuQrPayments( ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "deviceInfo required") add("s2s_client_ip", clientIp) add("s2s_device_info", deviceInfo) - val expirySeconds = request.expirySeconds - ?: request.expiryMinutes?.let { it * 60 } - expirySeconds?.let { add("expiry_time", it.toString()) } + add("expiry_time", expirySeconds.toString()) request.address1?.trim()?.ifBlank { null }?.let { add("address1", it) } request.address2?.trim()?.ifBlank { null }?.let { add("address2", it) } request.city?.trim()?.ifBlank { null }?.let { add("city", it) } @@ -158,6 +182,8 @@ class PayuQrPayments( entry.value.joinToString("&") { value -> "${entry.key}=$value" } } + val createdAt = OffsetDateTime.now() + val expiryAt = createdAt.plusSeconds(expirySeconds.toLong()) val record = payuQrRequestRepo.save( PayuQrRequest( property = booking.property, @@ -167,7 +193,8 @@ class PayuQrPayments( currency = booking.property.currency, status = PayuQrStatus.CREATED, requestPayload = requestPayload, - createdAt = OffsetDateTime.now() + expiryAt = expiryAt, + createdAt = createdAt ) ) @@ -214,7 +241,7 @@ class PayuQrPayments( } private fun sha512(input: String): String { - val bytes = MessageDigest.getInstance("SHA-512").digest(input.toByteArray()) + val bytes = MessageDigest.getInstance("SHA-512").digest(input.toByteArray(Charsets.UTF_8)) return bytes.joinToString("") { "%02x".format(it) } } diff --git a/src/main/kotlin/com/android/trisolarisserver/models/payment/PayuQrRequest.kt b/src/main/kotlin/com/android/trisolarisserver/models/payment/PayuQrRequest.kt index f1cccc4..cf5cafd 100644 --- a/src/main/kotlin/com/android/trisolarisserver/models/payment/PayuQrRequest.kt +++ b/src/main/kotlin/com/android/trisolarisserver/models/payment/PayuQrRequest.kt @@ -50,6 +50,9 @@ class PayuQrRequest( @Column(name = "response_payload", columnDefinition = "text") var responsePayload: String? = null, + @Column(name = "expiry_at", columnDefinition = "timestamptz") + var expiryAt: OffsetDateTime? = null, + @Column(name = "created_at", nullable = false, columnDefinition = "timestamptz") val createdAt: OffsetDateTime = OffsetDateTime.now() ) diff --git a/src/main/kotlin/com/android/trisolarisserver/repo/PayuQrRequestRepo.kt b/src/main/kotlin/com/android/trisolarisserver/repo/PayuQrRequestRepo.kt index e59235e..34138f6 100644 --- a/src/main/kotlin/com/android/trisolarisserver/repo/PayuQrRequestRepo.kt +++ b/src/main/kotlin/com/android/trisolarisserver/repo/PayuQrRequestRepo.kt @@ -6,4 +6,10 @@ import java.util.UUID interface PayuQrRequestRepo : JpaRepository { fun findByBookingId(bookingId: UUID): List + fun findTopByBookingIdAndAmountAndCurrencyAndStatusOrderByCreatedAtDesc( + bookingId: UUID, + amount: Long, + currency: String, + status: com.android.trisolarisserver.models.payment.PayuQrStatus + ): PayuQrRequest? }