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,
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user