feat(payment): add GetUnverifiedAuthoritiesAsync method to IPaymentGatewayService and ZarinPalPaymentService for payment reconciliation
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 8m7s

- Introduced GetUnverifiedAuthoritiesAsync method to retrieve authorities with unverified payments.
- Implemented ZarinPal-specific logic for fetching unverified authorities from the new endpoint.
- Added background job for Zarinpal payment reconciliation every 30 minutes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
masoodafar-web
2026-05-21 18:26:49 +03:30
parent aac0d4271d
commit bc52240a41
4 changed files with 481 additions and 0 deletions
@@ -30,6 +30,7 @@ public class ZarinPalPaymentService : IPaymentGatewayService
// مسیرهای API (مشترک)
private const string RequestEndpoint = "/pg/v4/payment/request.json";
private const string VerifyEndpoint = "/pg/v4/payment/verify.json";
private const string UnverifiedEndpoint = "/pg/v4/payment/unVerified.json";
private const string StartPayPath = "/pg/StartPay/";
private static readonly JsonSerializerOptions JsonOptions = new()
@@ -271,6 +272,49 @@ public class ZarinPalPaymentService : IPaymentGatewayService
}
}
/// <summary>
/// فهرست Authority‌هایی که کاربر پرداخت کرده ولی Verify نشده‌اند
/// زرین‌پال endpoint: POST /pg/v4/payment/unVerified.json
/// </summary>
public async Task<IReadOnlyList<string>> GetUnverifiedAuthoritiesAsync(
CancellationToken cancellationToken = default)
{
try
{
var requestBody = new ZarinPalUnverifiedRequest { MerchantId = _merchantId };
var json = JsonSerializer.Serialize(requestBody, JsonOptions);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(UnverifiedEndpoint, content, cancellationToken);
var body = await response.Content.ReadAsStringAsync(cancellationToken);
_logger.LogDebug("ZarinPal unVerified response: {StatusCode} - {Body}", response.StatusCode, body);
var result = JsonSerializer.Deserialize<ZarinPalUnverifiedResponse>(body, JsonOptions);
if (result?.Data?.Code == 100 && result.Data.Authorities?.Count > 0)
{
var authorities = result.Data.Authorities
.Where(a => !string.IsNullOrEmpty(a.Authority))
.Select(a => a.Authority!)
.ToList();
_logger.LogInformation(
"ZarinPal unVerified: {Count} unverified payment(s) found", authorities.Count);
return authorities;
}
_logger.LogDebug("ZarinPal unVerified: no pending payments. Code={Code}", result?.Data?.Code);
return Array.Empty<string>();
}
catch (Exception ex)
{
_logger.LogError(ex, "ZarinPal GetUnverifiedAuthorities failed — will fallback to DB scan");
return Array.Empty<string>();
}
}
/// <summary>
/// زرین‌پال Payout مستقیم ندارد — این متد NotSupported برمی‌گرداند
/// برای Payout باید از سرویس دیگری (مثل دایا) استفاده شود
@@ -339,6 +383,31 @@ public class ZarinPalPaymentService : IPaymentGatewayService
public string? Message { get; set; }
}
private class ZarinPalUnverifiedRequest
{
public string MerchantId { get; set; } = string.Empty;
}
private class ZarinPalUnverifiedResponse
{
public ZarinPalUnverifiedData? Data { get; set; }
}
private class ZarinPalUnverifiedData
{
public int? Code { get; set; }
public string? Message { get; set; }
public List<ZarinPalUnverifiedAuthority>? Authorities { get; set; }
}
private class ZarinPalUnverifiedAuthority
{
public string? Authority { get; set; }
public long? Amount { get; set; }
public string? Channel { get; set; }
public string? Date { get; set; }
}
/// <summary>
/// ZarinPal returns errors as [] (empty array) when no error, or as {...} object when there's an error.
/// This converter handles both cases.