Store Razorpay test keys alongside live keys
All checks were successful
build-and-deploy / build-deploy (push) Successful in 34s

This commit is contained in:
androidlover5842
2026-02-01 15:19:26 +05:30
parent 06ffbd86f5
commit 2421ba5edf
8 changed files with 95 additions and 8 deletions

View File

@@ -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")
}
}

View File

@@ -146,9 +146,10 @@ class RazorpayPaymentLinksController(
}
private fun postJson(url: String, settings: com.android.trisolarisserver.models.payment.RazorpaySettings, json: String): ResponseEntity<String> {
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<String, String> {
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()

View File

@@ -158,9 +158,10 @@ class RazorpayPaymentRequestsController(
settings: com.android.trisolarisserver.models.payment.RazorpaySettings,
json: String
): org.springframework.http.ResponseEntity<String> {
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<String, String> {
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

View File

@@ -310,9 +310,10 @@ class RazorpayQrPayments(
}
private fun postJson(url: String, settings: com.android.trisolarisserver.models.payment.RazorpaySettings, json: String): ResponseEntity<String> {
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<String, String> {
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
}
}

View File

@@ -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()
)
}

View File

@@ -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")

View File

@@ -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(

View File

@@ -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 }
}
}