From 2421ba5edf6a8fb04978ec5b9835cb14a22139ed Mon Sep 17 00:00:00 2001 From: androidlover5842 Date: Sun, 1 Feb 2026 15:19:26 +0530 Subject: [PATCH] Store Razorpay test keys alongside live keys --- .../config/RazorpaySettingsSchemaFix.kt | 6 ++++ .../RazorpayPaymentLinksController.kt | 11 ++++++- .../RazorpayPaymentRequestsController.kt | 11 ++++++- .../controller/RazorpayQrPayments.kt | 11 ++++++- .../controller/RazorpaySettingsController.kt | 31 +++++++++++++++++-- .../controller/RazorpayWebhookCapture.kt | 2 +- .../controller/dto/RazorpayDtos.kt | 8 ++++- .../models/payment/RazorpaySettings.kt | 23 +++++++++++++- 8 files changed, 95 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/com/android/trisolarisserver/config/RazorpaySettingsSchemaFix.kt b/src/main/kotlin/com/android/trisolarisserver/config/RazorpaySettingsSchemaFix.kt index 647c268..cde536f 100644 --- a/src/main/kotlin/com/android/trisolarisserver/config/RazorpaySettingsSchemaFix.kt +++ b/src/main/kotlin/com/android/trisolarisserver/config/RazorpaySettingsSchemaFix.kt @@ -27,6 +27,9 @@ class RazorpaySettingsSchemaFix( key_id varchar not null, key_secret varchar not null, webhook_secret varchar, + key_id_test varchar, + key_secret_test varchar, + webhook_secret_test varchar, is_test boolean not null default false, updated_at timestamptz not null ) @@ -37,5 +40,8 @@ class RazorpaySettingsSchemaFix( jdbcTemplate.execute("alter table razorpay_settings alter column key_id type text") jdbcTemplate.execute("alter table razorpay_settings alter column key_secret type text") jdbcTemplate.execute("alter table razorpay_settings alter column webhook_secret type text") + jdbcTemplate.execute("alter table razorpay_settings add column if not exists key_id_test text") + jdbcTemplate.execute("alter table razorpay_settings add column if not exists key_secret_test text") + jdbcTemplate.execute("alter table razorpay_settings add column if not exists webhook_secret_test text") } } diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayPaymentLinksController.kt b/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayPaymentLinksController.kt index 9880bc1..338d918 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayPaymentLinksController.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayPaymentLinksController.kt @@ -146,9 +146,10 @@ class RazorpayPaymentLinksController( } private fun postJson(url: String, settings: com.android.trisolarisserver.models.payment.RazorpaySettings, json: String): ResponseEntity { + val (keyId, keySecret) = requireKeys(settings) val headers = HttpHeaders() headers.contentType = MediaType.APPLICATION_JSON - headers.set(HttpHeaders.AUTHORIZATION, basicAuth(settings.keyId, settings.keySecret)) + headers.set(HttpHeaders.AUTHORIZATION, basicAuth(keyId, keySecret)) return restTemplate.exchange(url, HttpMethod.POST, HttpEntity(json, headers), String::class.java) } @@ -166,6 +167,14 @@ class RazorpayPaymentLinksController( return "Basic $encoded" } + private fun requireKeys(settings: com.android.trisolarisserver.models.payment.RazorpaySettings): Pair { + val keyId = settings.resolveKeyId() + ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Razorpay test keys not configured") + val keySecret = settings.resolveKeySecret() + ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Razorpay test keys not configured") + return keyId to keySecret + } + private fun parseExpiryEpoch(value: String?): Long? { if (value.isNullOrBlank()) return null return value.trim().toLongOrNull() diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayPaymentRequestsController.kt b/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayPaymentRequestsController.kt index af9e4d9..dc9fd7d 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayPaymentRequestsController.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayPaymentRequestsController.kt @@ -158,9 +158,10 @@ class RazorpayPaymentRequestsController( settings: com.android.trisolarisserver.models.payment.RazorpaySettings, json: String ): org.springframework.http.ResponseEntity { + val (keyId, keySecret) = requireKeys(settings) val headers = HttpHeaders() headers.contentType = MediaType.APPLICATION_JSON - headers.set(HttpHeaders.AUTHORIZATION, basicAuth(settings.keyId, settings.keySecret)) + headers.set(HttpHeaders.AUTHORIZATION, basicAuth(keyId, keySecret)) return restTemplate.exchange(url, HttpMethod.POST, HttpEntity(json, headers), String::class.java) } @@ -178,6 +179,14 @@ class RazorpayPaymentRequestsController( return "Basic $encoded" } + private fun requireKeys(settings: com.android.trisolarisserver.models.payment.RazorpaySettings): Pair { + val keyId = settings.resolveKeyId() + ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Razorpay test keys not configured") + val keySecret = settings.resolveKeySecret() + ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Razorpay test keys not configured") + return keyId to keySecret + } + private fun isQrClosed(status: String?): Boolean { return when (status?.lowercase()) { "closed", "expired", "credited" -> true diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayQrPayments.kt b/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayQrPayments.kt index afcaad9..e0d9291 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayQrPayments.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayQrPayments.kt @@ -310,9 +310,10 @@ class RazorpayQrPayments( } private fun postJson(url: String, settings: com.android.trisolarisserver.models.payment.RazorpaySettings, json: String): ResponseEntity { + val (keyId, keySecret) = requireKeys(settings) val headers = HttpHeaders() headers.contentType = MediaType.APPLICATION_JSON - headers.set(HttpHeaders.AUTHORIZATION, basicAuth(settings.keyId, settings.keySecret)) + headers.set(HttpHeaders.AUTHORIZATION, basicAuth(keyId, keySecret)) return restTemplate.exchange(url, HttpMethod.POST, HttpEntity(json, headers), String::class.java) } @@ -329,4 +330,12 @@ class RazorpayQrPayments( val encoded = Base64.getEncoder().encodeToString(raw.toByteArray()) return "Basic $encoded" } + + private fun requireKeys(settings: com.android.trisolarisserver.models.payment.RazorpaySettings): Pair { + val keyId = settings.resolveKeyId() + ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Razorpay test keys not configured") + val keySecret = settings.resolveKeySecret() + ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Razorpay test keys not configured") + return keyId to keySecret + } } diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/RazorpaySettingsController.kt b/src/main/kotlin/com/android/trisolarisserver/controller/RazorpaySettingsController.kt index ac25f25..c93bb5f 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/RazorpaySettingsController.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/RazorpaySettingsController.kt @@ -42,7 +42,10 @@ class RazorpaySettingsController( isTest = false, hasKeyId = false, hasKeySecret = false, - hasWebhookSecret = false + hasWebhookSecret = false, + hasKeyIdTest = false, + hasKeySecretTest = false, + hasWebhookSecretTest = false ) } else { settings.toResponse() @@ -68,21 +71,39 @@ class RazorpaySettingsController( candidate?.trim()?.ifBlank { null } ?: request.salt256?.trim()?.ifBlank { null } ?: request.salt32?.trim()?.ifBlank { null } } val webhookSecret = request.webhookSecret?.trim()?.ifBlank { null } + val keyIdTest = request.keyIdTest?.trim()?.ifBlank { null } + val keySecretTest = request.keySecretTest?.trim()?.ifBlank { null } + val webhookSecretTest = request.webhookSecretTest?.trim()?.ifBlank { null } val isTest = request.isTest val hasKeyId = keyId != null val hasKeySecret = keySecret != null if (hasKeyId != hasKeySecret) { throw ResponseStatusException(HttpStatus.BAD_REQUEST, "keyId and keySecret must be provided together") } + val hasKeyIdTest = keyIdTest != null + val hasKeySecretTest = keySecretTest != null + if (hasKeyIdTest != hasKeySecretTest) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "keyIdTest and keySecretTest must be provided together") + } if (existing == null && (keyId == null || keySecret == null)) { throw ResponseStatusException(HttpStatus.BAD_REQUEST, "keyId/keySecret required") } + if (isTest == true || existing?.isTest == true) { + val effectiveKeyIdTest = keyIdTest ?: existing?.keyIdTest + val effectiveKeySecretTest = keySecretTest ?: existing?.keySecretTest + if (effectiveKeyIdTest.isNullOrBlank() || effectiveKeySecretTest.isNullOrBlank()) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "keyIdTest/keySecretTest required when isTest=true") + } + } val updated = if (existing == null) { RazorpaySettings( property = property, keyId = keyId!!, keySecret = keySecret!!, webhookSecret = webhookSecret, + keyIdTest = keyIdTest, + keySecretTest = keySecretTest, + webhookSecretTest = webhookSecretTest, isTest = isTest ?: false, updatedAt = OffsetDateTime.now() ) @@ -90,6 +111,9 @@ class RazorpaySettingsController( if (keyId != null) existing.keyId = keyId if (keySecret != null) existing.keySecret = keySecret if (webhookSecret != null) existing.webhookSecret = webhookSecret + if (keyIdTest != null) existing.keyIdTest = keyIdTest + if (keySecretTest != null) existing.keySecretTest = keySecretTest + if (webhookSecretTest != null) existing.webhookSecretTest = webhookSecretTest isTest?.let { existing.isTest = it } existing.updatedAt = OffsetDateTime.now() existing @@ -105,6 +129,9 @@ private fun RazorpaySettings.toResponse(): RazorpaySettingsResponse { isTest = isTest, hasKeyId = keyId.isNotBlank(), hasKeySecret = keySecret.isNotBlank(), - hasWebhookSecret = !webhookSecret.isNullOrBlank() + hasWebhookSecret = !webhookSecret.isNullOrBlank(), + hasKeyIdTest = !keyIdTest.isNullOrBlank(), + hasKeySecretTest = !keySecretTest.isNullOrBlank(), + hasWebhookSecretTest = !webhookSecretTest.isNullOrBlank() ) } diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayWebhookCapture.kt b/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayWebhookCapture.kt index be10cf0..45d046c 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayWebhookCapture.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/RazorpayWebhookCapture.kt @@ -74,7 +74,7 @@ class RazorpayWebhookCapture( if (body.isNullOrBlank()) return val signature = request.getHeader("X-Razorpay-Signature") ?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing signature") - val secret = settings.webhookSecret + val secret = settings.resolveWebhookSecret() ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Webhook secret not configured") if (!verifySignature(body, secret, signature)) { throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid signature") diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/dto/RazorpayDtos.kt b/src/main/kotlin/com/android/trisolarisserver/controller/dto/RazorpayDtos.kt index 08707d9..3bd81b8 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/dto/RazorpayDtos.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/dto/RazorpayDtos.kt @@ -6,6 +6,9 @@ data class RazorpaySettingsUpsertRequest( val keyId: String? = null, val keySecret: String? = null, val webhookSecret: String? = null, + val keyIdTest: String? = null, + val keySecretTest: String? = null, + val webhookSecretTest: String? = null, val isTest: Boolean? = null, // Backward-compatible aliases (older clients sending PayU-shaped payloads) val merchantKey: String? = null, @@ -20,7 +23,10 @@ data class RazorpaySettingsResponse( val isTest: Boolean, val hasKeyId: Boolean, val hasKeySecret: Boolean, - val hasWebhookSecret: Boolean + val hasWebhookSecret: Boolean, + val hasKeyIdTest: Boolean, + val hasKeySecretTest: Boolean, + val hasWebhookSecretTest: Boolean ) data class RazorpayQrGenerateRequest( diff --git a/src/main/kotlin/com/android/trisolarisserver/models/payment/RazorpaySettings.kt b/src/main/kotlin/com/android/trisolarisserver/models/payment/RazorpaySettings.kt index 466366e..b8d596f 100644 --- a/src/main/kotlin/com/android/trisolarisserver/models/payment/RazorpaySettings.kt +++ b/src/main/kotlin/com/android/trisolarisserver/models/payment/RazorpaySettings.kt @@ -26,9 +26,30 @@ class RazorpaySettings( @Column(name = "webhook_secret", columnDefinition = "text") var webhookSecret: String? = null, + @Column(name = "key_id_test", columnDefinition = "text") + var keyIdTest: String? = null, + + @Column(name = "key_secret_test", columnDefinition = "text") + var keySecretTest: String? = null, + + @Column(name = "webhook_secret_test", columnDefinition = "text") + var webhookSecretTest: String? = null, + @Column(name = "is_test", nullable = false) var isTest: Boolean = false, @Column(name = "updated_at", nullable = false, columnDefinition = "timestamptz") var updatedAt: OffsetDateTime = OffsetDateTime.now() -) +) { + fun resolveKeyId(): String? { + return if (isTest) keyIdTest?.ifBlank { null } else keyId.ifBlank { null } + } + + fun resolveKeySecret(): String? { + return if (isTest) keySecretTest?.ifBlank { null } else keySecret.ifBlank { null } + } + + fun resolveWebhookSecret(): String? { + return if (isTest) webhookSecretTest?.ifBlank { null } else webhookSecret?.ifBlank { null } + } +}