Store Razorpay test keys alongside live keys
All checks were successful
build-and-deploy / build-deploy (push) Successful in 34s
All checks were successful
build-and-deploy / build-deploy (push) Successful in 34s
This commit is contained in:
@@ -27,6 +27,9 @@ class RazorpaySettingsSchemaFix(
|
|||||||
key_id varchar not null,
|
key_id varchar not null,
|
||||||
key_secret varchar not null,
|
key_secret varchar not null,
|
||||||
webhook_secret varchar,
|
webhook_secret varchar,
|
||||||
|
key_id_test varchar,
|
||||||
|
key_secret_test varchar,
|
||||||
|
webhook_secret_test varchar,
|
||||||
is_test boolean not null default false,
|
is_test boolean not null default false,
|
||||||
updated_at timestamptz not null
|
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_id type text")
|
||||||
jdbcTemplate.execute("alter table razorpay_settings alter column key_secret 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 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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,9 +146,10 @@ class RazorpayPaymentLinksController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun postJson(url: String, settings: com.android.trisolarisserver.models.payment.RazorpaySettings, json: String): ResponseEntity<String> {
|
private fun postJson(url: String, settings: com.android.trisolarisserver.models.payment.RazorpaySettings, json: String): ResponseEntity<String> {
|
||||||
|
val (keyId, keySecret) = requireKeys(settings)
|
||||||
val headers = HttpHeaders()
|
val headers = HttpHeaders()
|
||||||
headers.contentType = MediaType.APPLICATION_JSON
|
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)
|
return restTemplate.exchange(url, HttpMethod.POST, HttpEntity(json, headers), String::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +167,14 @@ class RazorpayPaymentLinksController(
|
|||||||
return "Basic $encoded"
|
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? {
|
private fun parseExpiryEpoch(value: String?): Long? {
|
||||||
if (value.isNullOrBlank()) return null
|
if (value.isNullOrBlank()) return null
|
||||||
return value.trim().toLongOrNull()
|
return value.trim().toLongOrNull()
|
||||||
|
|||||||
@@ -158,9 +158,10 @@ class RazorpayPaymentRequestsController(
|
|||||||
settings: com.android.trisolarisserver.models.payment.RazorpaySettings,
|
settings: com.android.trisolarisserver.models.payment.RazorpaySettings,
|
||||||
json: String
|
json: String
|
||||||
): org.springframework.http.ResponseEntity<String> {
|
): org.springframework.http.ResponseEntity<String> {
|
||||||
|
val (keyId, keySecret) = requireKeys(settings)
|
||||||
val headers = HttpHeaders()
|
val headers = HttpHeaders()
|
||||||
headers.contentType = MediaType.APPLICATION_JSON
|
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)
|
return restTemplate.exchange(url, HttpMethod.POST, HttpEntity(json, headers), String::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,6 +179,14 @@ class RazorpayPaymentRequestsController(
|
|||||||
return "Basic $encoded"
|
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 {
|
private fun isQrClosed(status: String?): Boolean {
|
||||||
return when (status?.lowercase()) {
|
return when (status?.lowercase()) {
|
||||||
"closed", "expired", "credited" -> true
|
"closed", "expired", "credited" -> true
|
||||||
|
|||||||
@@ -310,9 +310,10 @@ class RazorpayQrPayments(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun postJson(url: String, settings: com.android.trisolarisserver.models.payment.RazorpaySettings, json: String): ResponseEntity<String> {
|
private fun postJson(url: String, settings: com.android.trisolarisserver.models.payment.RazorpaySettings, json: String): ResponseEntity<String> {
|
||||||
|
val (keyId, keySecret) = requireKeys(settings)
|
||||||
val headers = HttpHeaders()
|
val headers = HttpHeaders()
|
||||||
headers.contentType = MediaType.APPLICATION_JSON
|
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)
|
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())
|
val encoded = Base64.getEncoder().encodeToString(raw.toByteArray())
|
||||||
return "Basic $encoded"
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,10 @@ class RazorpaySettingsController(
|
|||||||
isTest = false,
|
isTest = false,
|
||||||
hasKeyId = false,
|
hasKeyId = false,
|
||||||
hasKeySecret = false,
|
hasKeySecret = false,
|
||||||
hasWebhookSecret = false
|
hasWebhookSecret = false,
|
||||||
|
hasKeyIdTest = false,
|
||||||
|
hasKeySecretTest = false,
|
||||||
|
hasWebhookSecretTest = false
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
settings.toResponse()
|
settings.toResponse()
|
||||||
@@ -68,21 +71,39 @@ class RazorpaySettingsController(
|
|||||||
candidate?.trim()?.ifBlank { null } ?: request.salt256?.trim()?.ifBlank { null } ?: request.salt32?.trim()?.ifBlank { null }
|
candidate?.trim()?.ifBlank { null } ?: request.salt256?.trim()?.ifBlank { null } ?: request.salt32?.trim()?.ifBlank { null }
|
||||||
}
|
}
|
||||||
val webhookSecret = request.webhookSecret?.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 isTest = request.isTest
|
||||||
val hasKeyId = keyId != null
|
val hasKeyId = keyId != null
|
||||||
val hasKeySecret = keySecret != null
|
val hasKeySecret = keySecret != null
|
||||||
if (hasKeyId != hasKeySecret) {
|
if (hasKeyId != hasKeySecret) {
|
||||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "keyId and keySecret must be provided together")
|
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)) {
|
if (existing == null && (keyId == null || keySecret == null)) {
|
||||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "keyId/keySecret required")
|
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) {
|
val updated = if (existing == null) {
|
||||||
RazorpaySettings(
|
RazorpaySettings(
|
||||||
property = property,
|
property = property,
|
||||||
keyId = keyId!!,
|
keyId = keyId!!,
|
||||||
keySecret = keySecret!!,
|
keySecret = keySecret!!,
|
||||||
webhookSecret = webhookSecret,
|
webhookSecret = webhookSecret,
|
||||||
|
keyIdTest = keyIdTest,
|
||||||
|
keySecretTest = keySecretTest,
|
||||||
|
webhookSecretTest = webhookSecretTest,
|
||||||
isTest = isTest ?: false,
|
isTest = isTest ?: false,
|
||||||
updatedAt = OffsetDateTime.now()
|
updatedAt = OffsetDateTime.now()
|
||||||
)
|
)
|
||||||
@@ -90,6 +111,9 @@ class RazorpaySettingsController(
|
|||||||
if (keyId != null) existing.keyId = keyId
|
if (keyId != null) existing.keyId = keyId
|
||||||
if (keySecret != null) existing.keySecret = keySecret
|
if (keySecret != null) existing.keySecret = keySecret
|
||||||
if (webhookSecret != null) existing.webhookSecret = webhookSecret
|
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 }
|
isTest?.let { existing.isTest = it }
|
||||||
existing.updatedAt = OffsetDateTime.now()
|
existing.updatedAt = OffsetDateTime.now()
|
||||||
existing
|
existing
|
||||||
@@ -105,6 +129,9 @@ private fun RazorpaySettings.toResponse(): RazorpaySettingsResponse {
|
|||||||
isTest = isTest,
|
isTest = isTest,
|
||||||
hasKeyId = keyId.isNotBlank(),
|
hasKeyId = keyId.isNotBlank(),
|
||||||
hasKeySecret = keySecret.isNotBlank(),
|
hasKeySecret = keySecret.isNotBlank(),
|
||||||
hasWebhookSecret = !webhookSecret.isNullOrBlank()
|
hasWebhookSecret = !webhookSecret.isNullOrBlank(),
|
||||||
|
hasKeyIdTest = !keyIdTest.isNullOrBlank(),
|
||||||
|
hasKeySecretTest = !keySecretTest.isNullOrBlank(),
|
||||||
|
hasWebhookSecretTest = !webhookSecretTest.isNullOrBlank()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ class RazorpayWebhookCapture(
|
|||||||
if (body.isNullOrBlank()) return
|
if (body.isNullOrBlank()) return
|
||||||
val signature = request.getHeader("X-Razorpay-Signature")
|
val signature = request.getHeader("X-Razorpay-Signature")
|
||||||
?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing signature")
|
?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing signature")
|
||||||
val secret = settings.webhookSecret
|
val secret = settings.resolveWebhookSecret()
|
||||||
?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Webhook secret not configured")
|
?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Webhook secret not configured")
|
||||||
if (!verifySignature(body, secret, signature)) {
|
if (!verifySignature(body, secret, signature)) {
|
||||||
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid signature")
|
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid signature")
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ data class RazorpaySettingsUpsertRequest(
|
|||||||
val keyId: String? = null,
|
val keyId: String? = null,
|
||||||
val keySecret: String? = null,
|
val keySecret: String? = null,
|
||||||
val webhookSecret: String? = null,
|
val webhookSecret: String? = null,
|
||||||
|
val keyIdTest: String? = null,
|
||||||
|
val keySecretTest: String? = null,
|
||||||
|
val webhookSecretTest: String? = null,
|
||||||
val isTest: Boolean? = null,
|
val isTest: Boolean? = null,
|
||||||
// Backward-compatible aliases (older clients sending PayU-shaped payloads)
|
// Backward-compatible aliases (older clients sending PayU-shaped payloads)
|
||||||
val merchantKey: String? = null,
|
val merchantKey: String? = null,
|
||||||
@@ -20,7 +23,10 @@ data class RazorpaySettingsResponse(
|
|||||||
val isTest: Boolean,
|
val isTest: Boolean,
|
||||||
val hasKeyId: Boolean,
|
val hasKeyId: Boolean,
|
||||||
val hasKeySecret: Boolean,
|
val hasKeySecret: Boolean,
|
||||||
val hasWebhookSecret: Boolean
|
val hasWebhookSecret: Boolean,
|
||||||
|
val hasKeyIdTest: Boolean,
|
||||||
|
val hasKeySecretTest: Boolean,
|
||||||
|
val hasWebhookSecretTest: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
data class RazorpayQrGenerateRequest(
|
data class RazorpayQrGenerateRequest(
|
||||||
|
|||||||
@@ -26,9 +26,30 @@ class RazorpaySettings(
|
|||||||
@Column(name = "webhook_secret", columnDefinition = "text")
|
@Column(name = "webhook_secret", columnDefinition = "text")
|
||||||
var webhookSecret: String? = null,
|
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)
|
@Column(name = "is_test", nullable = false)
|
||||||
var isTest: Boolean = false,
|
var isTest: Boolean = false,
|
||||||
|
|
||||||
@Column(name = "updated_at", nullable = false, columnDefinition = "timestamptz")
|
@Column(name = "updated_at", nullable = false, columnDefinition = "timestamptz")
|
||||||
var updatedAt: OffsetDateTime = OffsetDateTime.now()
|
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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user