Replace PayU integration with Razorpay
All checks were successful
build-and-deploy / build-deploy (push) Successful in 33s
All checks were successful
build-and-deploy / build-deploy (push) Successful in 33s
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
package com.android.trisolarisserver.controller
|
||||
|
||||
import com.android.trisolarisserver.component.PropertyAccess
|
||||
import com.android.trisolarisserver.controller.dto.RazorpayQrGenerateRequest
|
||||
import com.android.trisolarisserver.controller.dto.RazorpayQrGenerateResponse
|
||||
import com.android.trisolarisserver.models.booking.BookingStatus
|
||||
import com.android.trisolarisserver.models.payment.RazorpayQrRequest
|
||||
import com.android.trisolarisserver.models.property.Role
|
||||
import com.android.trisolarisserver.db.repo.BookingRepo
|
||||
import com.android.trisolarisserver.repo.RazorpayQrRequestRepo
|
||||
import com.android.trisolarisserver.repo.RazorpaySettingsRepo
|
||||
import com.android.trisolarisserver.security.MyPrincipal
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import org.springframework.http.HttpEntity
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.HttpMethod
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
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.RestController
|
||||
import org.springframework.web.client.RestTemplate
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import org.springframework.http.HttpStatus
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.Base64
|
||||
import java.util.UUID
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/properties/{propertyId}/bookings/{bookingId}/payments/razorpay")
|
||||
class RazorpayQrPayments(
|
||||
private val propertyAccess: PropertyAccess,
|
||||
private val bookingRepo: BookingRepo,
|
||||
private val settingsRepo: RazorpaySettingsRepo,
|
||||
private val qrRequestRepo: RazorpayQrRequestRepo,
|
||||
private val restTemplate: RestTemplate,
|
||||
private val objectMapper: ObjectMapper
|
||||
) {
|
||||
|
||||
@PostMapping("/qr")
|
||||
fun createQr(
|
||||
@PathVariable propertyId: UUID,
|
||||
@PathVariable bookingId: UUID,
|
||||
@RequestBody request: RazorpayQrGenerateRequest,
|
||||
principal: MyPrincipal?
|
||||
): RazorpayQrGenerateResponse {
|
||||
requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER, Role.STAFF)
|
||||
val booking = bookingRepo.findById(bookingId).orElseThrow {
|
||||
ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found")
|
||||
}
|
||||
if (booking.property.id != propertyId) {
|
||||
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found for property")
|
||||
}
|
||||
if (booking.status == BookingStatus.CANCELLED || booking.status == BookingStatus.NO_SHOW) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Booking is not active")
|
||||
}
|
||||
|
||||
val settings = settingsRepo.findByPropertyId(propertyId)
|
||||
?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Razorpay settings not configured")
|
||||
|
||||
val amount = request.amount ?: 0L
|
||||
if (amount <= 0) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "amount must be > 0")
|
||||
}
|
||||
val currency = booking.property.currency
|
||||
|
||||
val existing = qrRequestRepo.findTopByBookingIdAndAmountAndCurrencyAndStatusOrderByCreatedAtDesc(
|
||||
bookingId,
|
||||
amount,
|
||||
currency,
|
||||
"active"
|
||||
)
|
||||
if (existing != null && existing.qrId != null) {
|
||||
return RazorpayQrGenerateResponse(
|
||||
qrId = existing.qrId,
|
||||
amount = existing.amount,
|
||||
currency = existing.currency,
|
||||
imageUrl = existing.imageUrl,
|
||||
razorpayResponse = existing.responsePayload ?: "{}"
|
||||
)
|
||||
}
|
||||
|
||||
val expirySeconds = request.expirySeconds ?: request.expiryMinutes?.let { it * 60 }
|
||||
val expiresAt = expirySeconds?.let { OffsetDateTime.now().plusSeconds(it.toLong()) }
|
||||
val notes = mapOf(
|
||||
"bookingId" to bookingId.toString(),
|
||||
"propertyId" to propertyId.toString()
|
||||
)
|
||||
|
||||
val payload = linkedMapOf<String, Any>(
|
||||
"type" to "upi_qr",
|
||||
"name" to "Booking $bookingId",
|
||||
"usage" to "single_use",
|
||||
"fixed_amount" to true,
|
||||
"payment_amount" to amount * 100,
|
||||
"notes" to notes
|
||||
)
|
||||
if (expirySeconds != null) {
|
||||
payload["close_by"] = OffsetDateTime.now().plusSeconds(expirySeconds.toLong()).toEpochSecond()
|
||||
}
|
||||
|
||||
val requestPayload = objectMapper.writeValueAsString(payload)
|
||||
val response = postJson(resolveBaseUrl(settings.isTest) + "/qr_codes", settings, requestPayload)
|
||||
val body = response.body ?: "{}"
|
||||
val node = objectMapper.readTree(body)
|
||||
val qrId = node.path("id").asText(null)
|
||||
val status = node.path("status").asText("unknown")
|
||||
val imageUrl = node.path("image_url").asText(null)
|
||||
|
||||
val record = qrRequestRepo.save(
|
||||
RazorpayQrRequest(
|
||||
property = booking.property,
|
||||
booking = booking,
|
||||
qrId = qrId,
|
||||
amount = amount,
|
||||
currency = currency,
|
||||
status = status,
|
||||
imageUrl = imageUrl,
|
||||
requestPayload = requestPayload,
|
||||
responsePayload = body,
|
||||
expiryAt = expiresAt
|
||||
)
|
||||
)
|
||||
|
||||
if (!response.statusCode.is2xxSuccessful) {
|
||||
record.status = "failed"
|
||||
qrRequestRepo.save(record)
|
||||
throw ResponseStatusException(HttpStatus.BAD_GATEWAY, "Razorpay request failed")
|
||||
}
|
||||
|
||||
return RazorpayQrGenerateResponse(
|
||||
qrId = qrId,
|
||||
amount = amount,
|
||||
currency = currency,
|
||||
imageUrl = imageUrl,
|
||||
razorpayResponse = body
|
||||
)
|
||||
}
|
||||
|
||||
private fun postJson(url: String, settings: com.android.trisolarisserver.models.payment.RazorpaySettings, json: String): ResponseEntity<String> {
|
||||
val headers = HttpHeaders()
|
||||
headers.contentType = MediaType.APPLICATION_JSON
|
||||
headers.set(HttpHeaders.AUTHORIZATION, basicAuth(settings.keyId, settings.keySecret))
|
||||
return restTemplate.exchange(url, HttpMethod.POST, HttpEntity(json, headers), String::class.java)
|
||||
}
|
||||
|
||||
private fun resolveBaseUrl(isTest: Boolean): String {
|
||||
return if (isTest) {
|
||||
"https://api.razorpay.com/v1"
|
||||
} else {
|
||||
"https://api.razorpay.com/v1"
|
||||
}
|
||||
}
|
||||
|
||||
private fun basicAuth(keyId: String, keySecret: String): String {
|
||||
val raw = "$keyId:$keySecret"
|
||||
val encoded = Base64.getEncoder().encodeToString(raw.toByteArray())
|
||||
return "Basic $encoded"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user