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