using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using GoodBooks.PAY.PAYBLL.Abstractions;
using Microsoft.Extensions.Logging;
namespace GoodBooks.PAY.PAYBLL.Providers
{
///
/// Razorpay gateway implementation using raw HttpClient (no SDK).
/// BaseAddress for the named "Razorpay" HttpClient: https://api.razorpay.com/v1/
/// Authentication: HTTP Basic — apiKey:apiSecret (Base64 encoded).
/// Webhook signature: HMAC-SHA256(webhookSecret, rawPayload) compared to X-Razorpay-Signature header.
/// Amounts: Razorpay uses paise (integer) — multiply/divide by 100 when converting to/from rupees.
///
public sealed class RazorpayGateway : IPaymentGateway
{
private readonly HttpClient _http;
private readonly ILogger _logger;
public string GatewayCode => "RAZORPAY";
public RazorpayGateway(HttpClient http, ILogger logger)
{
_http = http;
_logger = logger;
}
// ── Create Order ──────────────────────────────────────────────────────
public async Task CreateOrderAsync(
CreateGatewayOrderRequest req, CancellationToken ct)
{
try
{
_http.DefaultRequestHeaders.Authorization = BuildBasicAuth(req.ApiKey, req.ApiSecret);
var body = new
{
amount = (long)(req.Amount * 100), // paise
currency = req.Currency,
receipt = req.PayOrderNo,
notes = new { pay_order_id = req.PayOrderId.ToString() }
};
var content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
var response = await _http.PostAsync("orders", content, ct).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var errBody = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
_logger.LogError("Razorpay CreateOrder HTTP {StatusCode}: {Body}", (int)response.StatusCode, errBody);
return new GatewayOrderResult(false, string.Empty, null,
$"Razorpay error {(int)response.StatusCode}: {errBody}");
}
var json = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
using var doc = JsonDocument.Parse(json);
var orderId = doc.RootElement.GetProperty("id").GetString() ?? string.Empty;
return new GatewayOrderResult(true, orderId, null, null);
}
catch (Exception ex)
{
_logger.LogError(ex, "RazorpayGateway.CreateOrderAsync failed for PayOrderNo {PayOrderNo}", req.PayOrderNo);
return new GatewayOrderResult(false, string.Empty, null, ex.Message);
}
}
// ── Verify Payment ────────────────────────────────────────────────────
public async Task VerifyPaymentAsync(
VerifyPaymentRequest req, CancellationToken ct)
{
try
{
_http.DefaultRequestHeaders.Authorization = BuildBasicAuth(req.ApiKey, req.ApiSecret);
var response = await _http.GetAsync(
$"payments/{req.GatewayTransactionId}", ct).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var errBody = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
return new GatewayVerifyResult(false, "error", req.GatewayTransactionId, 0m,
$"Razorpay error {(int)response.StatusCode}: {errBody}");
}
var json = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
var status = root.GetProperty("status").GetString() ?? "unknown";
var amtPaise = root.TryGetProperty("amount", out var amtEl) ? amtEl.GetInt64() : 0L;
var amtRupees = amtPaise / 100m;
return new GatewayVerifyResult(status == "captured", status, req.GatewayTransactionId, amtRupees, null);
}
catch (Exception ex)
{
_logger.LogError(ex, "RazorpayGateway.VerifyPaymentAsync failed for TxnId {TxnId}", req.GatewayTransactionId);
return new GatewayVerifyResult(false, "error", req.GatewayTransactionId, 0m, ex.Message);
}
}
// ── Payout (Phase 2) ──────────────────────────────────────────────────
public Task InitiatePayoutAsync(PayoutRequest req, CancellationToken ct)
=> throw new NotImplementedException(
"Vendor payout — Phase 2. Use RazorpayX Payouts API (requires Fund Account creation).");
// ── Parse & Verify Webhook ────────────────────────────────────────────
public Task ParseAndVerifyWebhookAsync(
string rawPayload, string sigHeader, string webhookSecret, CancellationToken ct)
{
try
{
// 1. Verify HMAC-SHA256 signature
var computedSig = ComputeHmacSha256Hex(webhookSecret, rawPayload);
if (!string.Equals(computedSig, sigHeader, StringComparison.OrdinalIgnoreCase))
{
_logger.LogWarning("Razorpay webhook signature mismatch. Expected {Expected}, got {Got}",
computedSig, sigHeader);
return Task.FromResult(null);
}
// 2. Parse event
using var doc = JsonDocument.Parse(rawPayload);
var root = doc.RootElement;
var eventType = root.TryGetProperty("event", out var evEl) ? evEl.GetString() ?? "" : "";
// payment.captured / payment.failed are the primary events
string orderId = string.Empty;
string txnId = string.Empty;
decimal amount = 0m;
if (root.TryGetProperty("payload", out var payload) &&
payload.TryGetProperty("payment", out var payment) &&
payment.TryGetProperty("entity", out var entity))
{
orderId = entity.TryGetProperty("order_id", out var oEl) ? oEl.GetString() ?? "" : "";
txnId = entity.TryGetProperty("id", out var tEl) ? tEl.GetString() ?? "" : "";
amount = entity.TryGetProperty("amount", out var aEl) ? aEl.GetInt64() / 100m : 0m;
}
// Use txnId as idempotency key — unique per Razorpay payment
var webhookEvent = new GatewayWebhookEvent(
IdempotencyKey: txnId,
EventType: eventType,
GatewayOrderId: orderId,
GatewayTxnId: txnId,
Status: eventType == "payment.captured" ? "captured" : "failed",
Amount: amount);
return Task.FromResult(webhookEvent);
}
catch (Exception ex)
{
_logger.LogError(ex, "RazorpayGateway.ParseAndVerifyWebhookAsync failed");
return Task.FromResult(null);
}
}
// ── Transaction Status ────────────────────────────────────────────────
public async Task GetTransactionStatusAsync(string gatewayTxnId, CancellationToken ct)
{
// Note: called without credentials in this signature — pass dummy values or extend interface if needed
var response = await _http.GetAsync($"payments/{gatewayTxnId}", ct).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
return new GatewayTransactionStatus("unknown", 0m, null, null);
var json = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
var status = root.TryGetProperty("status", out var sEl) ? sEl.GetString() ?? "unknown" : "unknown";
var amount = root.TryGetProperty("amount", out var aEl) ? aEl.GetInt64() / 100m : 0m;
var method = root.TryGetProperty("method", out var mEl) ? mEl.GetString() : null;
return new GatewayTransactionStatus(status, amount, method, null);
}
// ── Helpers ───────────────────────────────────────────────────────────
private static AuthenticationHeaderValue BuildBasicAuth(string key, string secret)
{
var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{key}:{secret}"));
return new AuthenticationHeaderValue("Basic", credentials);
}
private static string ComputeHmacSha256Hex(string secret, string data)
{
var keyBytes = Encoding.UTF8.GetBytes(secret);
var dataBytes = Encoding.UTF8.GetBytes(data);
using var hmac = new HMACSHA256(keyBytes);
var hash = hmac.ComputeHash(dataBytes);
return Convert.ToHexString(hash).ToLowerInvariant();
}
}
}