using System.Net.Http.Json; using System.Text.Json.Serialization; using Meezi.Core.Interfaces; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace Meezi.Infrastructure.ExternalServices; public class ZarinPalGateway : IZarinPalGateway { public async Task IsEnabledAsync(CancellationToken cancellationToken = default) { var enabled = await _platform.GetAsync("payment.zarinpal.enabled", cancellationToken); return enabled is not "false"; } private readonly HttpClient _httpClient; private readonly IConfiguration _configuration; private readonly IPlatformRuntimeConfig _platform; private readonly IHostEnvironment _environment; private readonly ILogger _logger; public ZarinPalGateway( HttpClient httpClient, IConfiguration configuration, IPlatformRuntimeConfig platform, IHostEnvironment environment, ILogger logger) { _httpClient = httpClient; _configuration = configuration; _platform = platform; _environment = environment; _logger = logger; } public async Task RequestPaymentAsync( long amountRials, string description, string callbackUrl, CancellationToken cancellationToken = default) { var merchantId = await GetMerchantIdAsync(cancellationToken); if (string.IsNullOrWhiteSpace(merchantId)) { // Mock checkout exists ONLY for local development. In production a // missing merchant id must fail loudly — a fake-success here would // activate paid plans without any real payment. if (!_environment.IsDevelopment()) { _logger.LogError("ZarinPal merchant id missing — refusing payment init in {Env}", _environment.EnvironmentName); return new ZarinPalRequestResult(false, null, null, "Payment gateway is not configured."); } var mockAuthority = Guid.NewGuid().ToString("N")[..16]; var mockUrl = $"{callbackUrl}?Authority={mockAuthority}&Status=OK"; _logger.LogInformation("ZarinPal mock payment {Authority} amount {Amount} Rials", mockAuthority, amountRials); return new ZarinPalRequestResult(true, mockAuthority, mockUrl, null); } var sandbox = await IsSandboxAsync(cancellationToken); var baseUrl = sandbox ? "https://sandbox.zarinpal.com/pg/v4/payment" : "https://api.zarinpal.com/pg/v4/payment"; var payload = new { merchant_id = merchantId, amount = amountRials, description, callback_url = callbackUrl }; var response = await _httpClient.PostAsJsonAsync($"{baseUrl}/request.json", payload, cancellationToken); var body = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); if (body?.Data?.Code is 100 && !string.IsNullOrEmpty(body.Data.Authority)) { var startUrl = sandbox ? $"https://sandbox.zarinpal.com/pg/StartPay/{body.Data.Authority}" : $"https://www.zarinpal.com/pg/StartPay/{body.Data.Authority}"; return new ZarinPalRequestResult(true, body.Data.Authority, startUrl, null); } return new ZarinPalRequestResult(false, null, null, body?.Errors?.FirstOrDefault()?.Message ?? "ZarinPal request failed."); } public async Task VerifyPaymentAsync( string authority, long amountRials, CancellationToken cancellationToken = default) { var merchantId = await GetMerchantIdAsync(cancellationToken); if (string.IsNullOrWhiteSpace(merchantId)) { if (!_environment.IsDevelopment()) { _logger.LogError("ZarinPal merchant id missing — refusing payment verify in {Env}", _environment.EnvironmentName); return new ZarinPalVerifyResult(false, null, "Payment gateway is not configured."); } _logger.LogInformation("ZarinPal mock verify authority {Authority}", authority); return new ZarinPalVerifyResult(true, "MOCK-" + authority[..8], null); } var sandbox = await IsSandboxAsync(cancellationToken); var baseUrl = sandbox ? "https://sandbox.zarinpal.com/pg/v4/payment" : "https://api.zarinpal.com/pg/v4/payment"; var payload = new { merchant_id = merchantId, amount = amountRials, authority }; var response = await _httpClient.PostAsJsonAsync($"{baseUrl}/verify.json", payload, cancellationToken); var body = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); if (body?.Data?.Code is 100 or 101) return new ZarinPalVerifyResult(true, body.Data.RefId?.ToString(), null); return new ZarinPalVerifyResult(false, null, body?.Errors?.FirstOrDefault()?.Message ?? "ZarinPal verify failed."); } private sealed class ZarinPalDataResponse { [JsonPropertyName("data")] public ZarinPalData? Data { get; set; } [JsonPropertyName("errors")] public List? Errors { get; set; } } private sealed class ZarinPalData { [JsonPropertyName("code")] public int Code { get; set; } [JsonPropertyName("authority")] public string? Authority { get; set; } [JsonPropertyName("ref_id")] public long? RefId { get; set; } } private sealed class ZarinPalError { [JsonPropertyName("message")] public string? Message { get; set; } } private async Task GetMerchantIdAsync(CancellationToken cancellationToken) { var fromDb = await _platform.GetAsync("payment.zarinpal.merchantId", cancellationToken); if (!string.IsNullOrWhiteSpace(fromDb)) return fromDb; return _configuration["ZarinPal:MerchantId"]; } private async Task IsSandboxAsync(CancellationToken cancellationToken) { var fromDb = await _platform.GetAsync("payment.zarinpal.sandbox", cancellationToken); if (!string.IsNullOrWhiteSpace(fromDb)) return fromDb is not "false"; return _configuration.GetValue("ZarinPal:Sandbox", true); } }