Auto-fetch PayU payment link tokens
All checks were successful
build-and-deploy / build-deploy (push) Successful in 36s

This commit is contained in:
androidlover5842
2026-01-30 08:06:32 +05:30
parent 92b023cb5b
commit a3257e4827
5 changed files with 109 additions and 11 deletions

View File

@@ -25,12 +25,35 @@ class PayuPaymentLinkSettingsSchemaFix(
id uuid primary key,
property_id uuid not null unique references property(id) on delete cascade,
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,
updated_at timestamptz not null
)
""".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")
}
}
}

View File

@@ -41,6 +41,8 @@ class PayuPaymentLinkSettingsController(
configured = false,
merchantId = null,
isTest = false,
hasClientId = false,
hasClientSecret = false,
hasAccessToken = false
)
}
@@ -60,22 +62,23 @@ class PayuPaymentLinkSettingsController(
val merchantId = request.merchantId.trim().ifBlank {
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 existing = settingsRepo.findByPropertyId(propertyId)
val updated = if (existing == null) {
PayuPaymentLinkSettings(
property = property,
merchantId = merchantId,
accessToken = accessToken,
clientId = request.clientId?.trim()?.ifBlank { null },
clientSecret = request.clientSecret?.trim()?.ifBlank { null },
accessToken = request.accessToken?.trim()?.ifBlank { null },
isTest = isTest,
updatedAt = OffsetDateTime.now()
)
} else {
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.updatedAt = OffsetDateTime.now()
existing
@@ -91,6 +94,8 @@ private fun PayuPaymentLinkSettings.toResponse(): PayuPaymentLinkSettingsRespons
configured = true,
merchantId = merchantId,
isTest = isTest,
hasAccessToken = accessToken.isNotBlank()
hasClientId = !clientId.isNullOrBlank(),
hasClientSecret = !clientSecret.isNullOrBlank(),
hasAccessToken = !accessToken.isNullOrBlank()
)
}

View File

@@ -23,6 +23,7 @@ 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 java.time.OffsetDateTime
import java.util.UUID
@RestController
@@ -116,9 +117,11 @@ class PayuPaymentLinksController(
}
request.expiryDate?.trim()?.ifBlank { null }?.let { body["expiryDate"] = it }
val accessToken = resolveAccessToken(settings)
settingsRepo.save(settings)
val headers = HttpHeaders().apply {
contentType = MediaType.APPLICATION_JSON
set("Authorization", "Bearer ${settings.accessToken}")
set("Authorization", "Bearer $accessToken")
set("merchantId", settings.merchantId)
}
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? {
if (body.isBlank()) return null
return try {

View File

@@ -49,7 +49,9 @@ data class PayuQrGenerateResponse(
data class PayuPaymentLinkSettingsUpsertRequest(
val merchantId: String,
val accessToken: String,
val clientId: String? = null,
val clientSecret: String? = null,
val accessToken: String? = null,
val isTest: Boolean? = null
)
@@ -58,6 +60,8 @@ data class PayuPaymentLinkSettingsResponse(
val configured: Boolean,
val merchantId: String?,
val isTest: Boolean,
val hasClientId: Boolean,
val hasClientSecret: Boolean,
val hasAccessToken: Boolean
)

View File

@@ -31,8 +31,17 @@ class PayuPaymentLinkSettings(
@Column(name = "merchant_id", nullable = false, columnDefinition = "text")
var merchantId: String,
@Column(name = "access_token", nullable = false, columnDefinition = "text")
var accessToken: String,
@Column(name = "client_id", columnDefinition = "text")
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)
var isTest: Boolean = false,