From a3257e482763c7146156591e793a9234ea3731f3 Mon Sep 17 00:00:00 2001 From: androidlover5842 Date: Fri, 30 Jan 2026 08:06:32 +0530 Subject: [PATCH] Auto-fetch PayU payment link tokens --- .../PayuPaymentLinkSettingsSchemaFix.kt | 25 +++++++- .../PayuPaymentLinkSettingsController.kt | 17 ++++-- .../controller/PayuPaymentLinksController.kt | 59 ++++++++++++++++++- .../controller/dto/PayuDtos.kt | 6 +- .../models/payment/PayuPaymentLinkSettings.kt | 13 +++- 5 files changed, 109 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/com/android/trisolarisserver/config/PayuPaymentLinkSettingsSchemaFix.kt b/src/main/kotlin/com/android/trisolarisserver/config/PayuPaymentLinkSettingsSchemaFix.kt index a15608e..c5d79d1 100644 --- a/src/main/kotlin/com/android/trisolarisserver/config/PayuPaymentLinkSettingsSchemaFix.kt +++ b/src/main/kotlin/com/android/trisolarisserver/config/PayuPaymentLinkSettingsSchemaFix.kt @@ -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") + } } } diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/PayuPaymentLinkSettingsController.kt b/src/main/kotlin/com/android/trisolarisserver/controller/PayuPaymentLinkSettingsController.kt index bc05777..8e931bd 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/PayuPaymentLinkSettingsController.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/PayuPaymentLinkSettingsController.kt @@ -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() ) } diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/PayuPaymentLinksController.kt b/src/main/kotlin/com/android/trisolarisserver/controller/PayuPaymentLinksController.kt index 21769c5..5b4b526 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/PayuPaymentLinksController.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/PayuPaymentLinksController.kt @@ -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().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 { diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/dto/PayuDtos.kt b/src/main/kotlin/com/android/trisolarisserver/controller/dto/PayuDtos.kt index f57faa1..b2ee6c6 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/dto/PayuDtos.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/dto/PayuDtos.kt @@ -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 ) diff --git a/src/main/kotlin/com/android/trisolarisserver/models/payment/PayuPaymentLinkSettings.kt b/src/main/kotlin/com/android/trisolarisserver/models/payment/PayuPaymentLinkSettings.kt index 47c7a5e..7ff94ce 100644 --- a/src/main/kotlin/com/android/trisolarisserver/models/payment/PayuPaymentLinkSettings.kt +++ b/src/main/kotlin/com/android/trisolarisserver/models/payment/PayuPaymentLinkSettings.kt @@ -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,