Auto-fetch PayU payment link tokens
All checks were successful
build-and-deploy / build-deploy (push) Successful in 36s
All checks were successful
build-and-deploy / build-deploy (push) Successful in 36s
This commit is contained in:
@@ -25,12 +25,35 @@ class PayuPaymentLinkSettingsSchemaFix(
|
|||||||
id uuid primary key,
|
id uuid primary key,
|
||||||
property_id uuid not null unique references property(id) on delete cascade,
|
property_id uuid not null unique references property(id) on delete cascade,
|
||||||
merchant_id text not null,
|
merchant_id text not null,
|
||||||
access_token text not null,
|
client_id text,
|
||||||
|
client_secret text,
|
||||||
|
access_token text,
|
||||||
|
token_expires_at timestamptz,
|
||||||
is_test boolean not null default false,
|
is_test boolean not null default false,
|
||||||
updated_at timestamptz not null
|
updated_at timestamptz not null
|
||||||
)
|
)
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
ensureColumn("payu_payment_link_settings", "client_id", "text")
|
||||||
|
ensureColumn("payu_payment_link_settings", "client_secret", "text")
|
||||||
|
ensureColumn("payu_payment_link_settings", "access_token", "text")
|
||||||
|
ensureColumn("payu_payment_link_settings", "token_expires_at", "timestamptz")
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ class PayuPaymentLinkSettingsController(
|
|||||||
configured = false,
|
configured = false,
|
||||||
merchantId = null,
|
merchantId = null,
|
||||||
isTest = false,
|
isTest = false,
|
||||||
|
hasClientId = false,
|
||||||
|
hasClientSecret = false,
|
||||||
hasAccessToken = false
|
hasAccessToken = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -60,22 +62,23 @@ class PayuPaymentLinkSettingsController(
|
|||||||
val merchantId = request.merchantId.trim().ifBlank {
|
val merchantId = request.merchantId.trim().ifBlank {
|
||||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "merchantId required")
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "merchantId required")
|
||||||
}
|
}
|
||||||
val accessToken = request.accessToken.trim().ifBlank {
|
|
||||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "accessToken required")
|
|
||||||
}
|
|
||||||
val isTest = request.isTest ?: false
|
val isTest = request.isTest ?: false
|
||||||
val existing = settingsRepo.findByPropertyId(propertyId)
|
val existing = settingsRepo.findByPropertyId(propertyId)
|
||||||
val updated = if (existing == null) {
|
val updated = if (existing == null) {
|
||||||
PayuPaymentLinkSettings(
|
PayuPaymentLinkSettings(
|
||||||
property = property,
|
property = property,
|
||||||
merchantId = merchantId,
|
merchantId = merchantId,
|
||||||
accessToken = accessToken,
|
clientId = request.clientId?.trim()?.ifBlank { null },
|
||||||
|
clientSecret = request.clientSecret?.trim()?.ifBlank { null },
|
||||||
|
accessToken = request.accessToken?.trim()?.ifBlank { null },
|
||||||
isTest = isTest,
|
isTest = isTest,
|
||||||
updatedAt = OffsetDateTime.now()
|
updatedAt = OffsetDateTime.now()
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
existing.merchantId = merchantId
|
existing.merchantId = merchantId
|
||||||
existing.accessToken = accessToken
|
if (request.clientId != null) existing.clientId = request.clientId.trim().ifBlank { null }
|
||||||
|
if (request.clientSecret != null) existing.clientSecret = request.clientSecret.trim().ifBlank { null }
|
||||||
|
if (request.accessToken != null) existing.accessToken = request.accessToken.trim().ifBlank { null }
|
||||||
existing.isTest = isTest
|
existing.isTest = isTest
|
||||||
existing.updatedAt = OffsetDateTime.now()
|
existing.updatedAt = OffsetDateTime.now()
|
||||||
existing
|
existing
|
||||||
@@ -91,6 +94,8 @@ private fun PayuPaymentLinkSettings.toResponse(): PayuPaymentLinkSettingsRespons
|
|||||||
configured = true,
|
configured = true,
|
||||||
merchantId = merchantId,
|
merchantId = merchantId,
|
||||||
isTest = isTest,
|
isTest = isTest,
|
||||||
hasAccessToken = accessToken.isNotBlank()
|
hasClientId = !clientId.isNullOrBlank(),
|
||||||
|
hasClientSecret = !clientSecret.isNullOrBlank(),
|
||||||
|
hasAccessToken = !accessToken.isNullOrBlank()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import org.springframework.web.bind.annotation.RequestMapping
|
|||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
import org.springframework.web.client.RestTemplate
|
import org.springframework.web.client.RestTemplate
|
||||||
import org.springframework.web.server.ResponseStatusException
|
import org.springframework.web.server.ResponseStatusException
|
||||||
|
import java.time.OffsetDateTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -116,9 +117,11 @@ class PayuPaymentLinksController(
|
|||||||
}
|
}
|
||||||
request.expiryDate?.trim()?.ifBlank { null }?.let { body["expiryDate"] = it }
|
request.expiryDate?.trim()?.ifBlank { null }?.let { body["expiryDate"] = it }
|
||||||
|
|
||||||
|
val accessToken = resolveAccessToken(settings)
|
||||||
|
settingsRepo.save(settings)
|
||||||
val headers = HttpHeaders().apply {
|
val headers = HttpHeaders().apply {
|
||||||
contentType = MediaType.APPLICATION_JSON
|
contentType = MediaType.APPLICATION_JSON
|
||||||
set("Authorization", "Bearer ${settings.accessToken}")
|
set("Authorization", "Bearer $accessToken")
|
||||||
set("merchantId", settings.merchantId)
|
set("merchantId", settings.merchantId)
|
||||||
}
|
}
|
||||||
val entity = HttpEntity(body, headers)
|
val entity = HttpEntity(body, headers)
|
||||||
@@ -145,6 +148,60 @@ class PayuPaymentLinksController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun resolveAccessToken(settings: com.android.trisolarisserver.models.payment.PayuPaymentLinkSettings): String {
|
||||||
|
val now = OffsetDateTime.now()
|
||||||
|
val existing = settings.accessToken?.trim()?.ifBlank { null }
|
||||||
|
val expiresAt = settings.tokenExpiresAt
|
||||||
|
if (existing != null && expiresAt != null && expiresAt.isAfter(now.plusSeconds(60))) {
|
||||||
|
return existing
|
||||||
|
}
|
||||||
|
val clientId = settings.clientId?.trim()?.ifBlank { null }
|
||||||
|
val clientSecret = settings.clientSecret?.trim()?.ifBlank { null }
|
||||||
|
if (clientId == null || clientSecret == null) {
|
||||||
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Payment link client credentials missing")
|
||||||
|
}
|
||||||
|
val tokenResponse = fetchAccessToken(settings.isTest, clientId, clientSecret)
|
||||||
|
settings.accessToken = tokenResponse.accessToken
|
||||||
|
settings.tokenExpiresAt = now.plusSeconds(tokenResponse.expiresIn.toLong())
|
||||||
|
return tokenResponse.accessToken
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class TokenResponse(val accessToken: String, val expiresIn: Int)
|
||||||
|
|
||||||
|
private fun fetchAccessToken(isTest: Boolean, clientId: String, clientSecret: String): TokenResponse {
|
||||||
|
val url = if (isTest) {
|
||||||
|
"https://uat-accounts.payu.in/oauth/token"
|
||||||
|
} else {
|
||||||
|
"https://accounts.payu.in/oauth/token"
|
||||||
|
}
|
||||||
|
val form = org.springframework.util.LinkedMultiValueMap<String, String>().apply {
|
||||||
|
add("client_id", clientId)
|
||||||
|
add("client_secret", clientSecret)
|
||||||
|
add("grant_type", "client_credentials")
|
||||||
|
add("scope", "create_payment_links")
|
||||||
|
}
|
||||||
|
val headers = HttpHeaders().apply {
|
||||||
|
contentType = MediaType.APPLICATION_FORM_URLENCODED
|
||||||
|
}
|
||||||
|
val entity = HttpEntity(form, headers)
|
||||||
|
val response = restTemplate.postForEntity(url, entity, String::class.java)
|
||||||
|
val body = response.body ?: ""
|
||||||
|
if (!response.statusCode.is2xxSuccessful) {
|
||||||
|
throw ResponseStatusException(HttpStatus.BAD_GATEWAY, "PayU token request failed")
|
||||||
|
}
|
||||||
|
return try {
|
||||||
|
val node = objectMapper.readTree(body)
|
||||||
|
val token = node.path("access_token").asText(null)
|
||||||
|
val expiresIn = node.path("expires_in").asInt(0)
|
||||||
|
if (token.isNullOrBlank() || expiresIn <= 0) {
|
||||||
|
throw IllegalStateException("Token missing")
|
||||||
|
}
|
||||||
|
TokenResponse(token, expiresIn)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
throw ResponseStatusException(HttpStatus.BAD_GATEWAY, "PayU token parse failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun extractPaymentLink(body: String): String? {
|
private fun extractPaymentLink(body: String): String? {
|
||||||
if (body.isBlank()) return null
|
if (body.isBlank()) return null
|
||||||
return try {
|
return try {
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ data class PayuQrGenerateResponse(
|
|||||||
|
|
||||||
data class PayuPaymentLinkSettingsUpsertRequest(
|
data class PayuPaymentLinkSettingsUpsertRequest(
|
||||||
val merchantId: String,
|
val merchantId: String,
|
||||||
val accessToken: String,
|
val clientId: String? = null,
|
||||||
|
val clientSecret: String? = null,
|
||||||
|
val accessToken: String? = null,
|
||||||
val isTest: Boolean? = null
|
val isTest: Boolean? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -58,6 +60,8 @@ data class PayuPaymentLinkSettingsResponse(
|
|||||||
val configured: Boolean,
|
val configured: Boolean,
|
||||||
val merchantId: String?,
|
val merchantId: String?,
|
||||||
val isTest: Boolean,
|
val isTest: Boolean,
|
||||||
|
val hasClientId: Boolean,
|
||||||
|
val hasClientSecret: Boolean,
|
||||||
val hasAccessToken: Boolean
|
val hasAccessToken: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,17 @@ class PayuPaymentLinkSettings(
|
|||||||
@Column(name = "merchant_id", nullable = false, columnDefinition = "text")
|
@Column(name = "merchant_id", nullable = false, columnDefinition = "text")
|
||||||
var merchantId: String,
|
var merchantId: String,
|
||||||
|
|
||||||
@Column(name = "access_token", nullable = false, columnDefinition = "text")
|
@Column(name = "client_id", columnDefinition = "text")
|
||||||
var accessToken: String,
|
var clientId: String? = null,
|
||||||
|
|
||||||
|
@Column(name = "client_secret", columnDefinition = "text")
|
||||||
|
var clientSecret: String? = null,
|
||||||
|
|
||||||
|
@Column(name = "access_token", columnDefinition = "text")
|
||||||
|
var accessToken: String? = null,
|
||||||
|
|
||||||
|
@Column(name = "token_expires_at", columnDefinition = "timestamptz")
|
||||||
|
var tokenExpiresAt: OffsetDateTime? = null,
|
||||||
|
|
||||||
@Column(name = "is_test", nullable = false)
|
@Column(name = "is_test", nullable = false)
|
||||||
var isTest: Boolean = false,
|
var isTest: Boolean = false,
|
||||||
|
|||||||
Reference in New Issue
Block a user