# راهنمای فنی پیاده‌سازی درگاه پرداخت زرین‌پال (ZarinPal)

> **نسخه:** 1.0  
> **تاریخ:** اسفند ۱۴۰۴  
> **تکنولوژی:** ASP.NET Core (.NET 9) — Clean Architecture — Strategy Pattern  
> **API زرین‌پال:** v4

---

## فهرست مطالب

1. [معماری کلی](#1-معماری-کلی)
2. [ساختار فایل‌ها و لایه‌ها](#2-ساختار-فایلها-و-لایهها)
3. [فلوی پرداخت (Payment Flow)](#3-فلوی-پرداخت)
4. [پیاده‌سازی گام‌به‌گام](#4-پیادهسازی-گامبهگام)
   - [4.1 تنظیمات (Configuration)](#41-تنظیمات-configuration)
   - [4.2 Interface — لایه Application](#42-interface--لایه-application)
   - [4.3 DTOs — مدل‌های Request/Response](#43-dtos--مدلهای-requestresponse)
   - [4.4 Domain Entity — PaymentTransaction](#44-domain-entity--paymenttransaction)
   - [4.5 سرویس زرین‌پال — لایه Infrastructure](#45-سرویس-زرینپال--لایه-infrastructure)
   - [4.6 ثبت سرویس (DI Registration)](#46-ثبت-سرویس-di-registration)
   - [4.7 Mock Service (برای تست)](#47-mock-service-برای-تست)
5. [فلوی Initiate Payment (ایجاد پرداخت)](#5-فلوی-initiate-payment)
6. [فلوی Verify Payment (تأیید پرداخت)](#6-فلوی-verify-payment)
7. [نکات مهم تومان/ریال](#7-نکات-مهم-تومانریال)
8. [مدیریت خطای API زرین‌پال](#8-مدیریت-خطای-api-زرینپال)
9. [Sandbox vs Production](#9-sandbox-vs-production)
10. [الگوی استفاده در Handler/Controller](#10-الگوی-استفاده-در-handlercontroller)
11. [Migration — جدول PaymentTransaction](#11-migration--جدول-paymenttransaction)
12. [چک‌لیست پیاده‌سازی در پروژه جدید](#12-چکلیست-پیادهسازی-در-پروژه-جدید)
13. [کدهای خطای زرین‌پال](#13-کدهای-خطای-زرینپال)
14. [Sequence Diagram](#14-sequence-diagram)

---

## 1. معماری کلی

از الگوی **Strategy Pattern** استفاده شده. یک `IPaymentGatewayService` اینترفیس وجود داره که چندین پیاده‌سازی (ZarinPal, Mock, و ...) داره. سوئیچ بین provider‌ها فقط با تغییر یک مقدار در `appsettings.json` انجام میشه:

```
┌─────────────────────────────────────────────────────┐
│                    Application Layer                 │
│                                                     │
│  IPaymentGatewayService (Interface)                 │
│  ├── InitiatePaymentAsync(PaymentRequest)           │
│  ├── VerifyPaymentAsync(refId, status, amount)      │
│  └── ProcessPayoutAsync(PayoutRequest)              │
└────────────────────┬────────────────────────────────┘
                     │
         ┌───────────┼───────────┐
         │           │           │
         ▼           ▼           ▼
┌──────────────┐┌──────────┐┌──────────────┐
│ ZarinPal     ││  Mock    ││   (Other)    │
│ PaymentSvc   ││  Svc     ││   Provider   │
└──────────────┘└──────────┘└──────────────┘
   Infrastructure Layer
```

**مزایا:**
- Caller (Handler/Controller) هیچ وابستگی مستقیمی به زرین‌پال نداره
- سوئیچ بین درگاه‌ها بدون تغییر کد — فقط config
- تست آسان با Mock
- هر Provider خودش DTO و Error Handling مخصوص خودش رو داره

---

## 2. ساختار فایل‌ها و لایه‌ها

```
YourProject/
├── src/
│   ├── Application/                          ← لایه Application (Interface + DTOs)
│   │   └── Common/
│   │       └── Interfaces/
│   │           └── IPaymentGatewayService.cs ← اینترفیس + تمام DTOs
│   │
│   ├── Domain/                               ← لایه Domain (Entity)
│   │   └── Entities/
│   │       └── Payment/
│   │           └── PaymentTransaction.cs     ← Entity ذخیره تراکنش درگاه
│   │
│   ├── Infrastructure/                       ← لایه Infrastructure (پیاده‌سازی)
│   │   ├── ConfigureServices.cs              ← DI Registration (Strategy Switch)
│   │   └── Services/
│   │       └── Payment/
│   │           ├── ZarinPalPaymentService.cs ← پیاده‌سازی زرین‌پال
│   │           └── MockPaymentGatewayService.cs ← Mock برای تست
│   │
│   └── WebApi/                               ← لایه Presentation
│       └── appsettings.json                  ← تنظیمات MerchantId و UseSandbox
```

---

## 3. فلوی پرداخت

```
┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
│  Client  │    │  Your    │    │ ZarinPal │    │ ZarinPal │
│ (Browser)│    │  Server  │    │   API    │    │ Gateway  │
└────┬─────┘    └────┬─────┘    └────┬─────┘    └────┬─────┘
     │               │               │               │
     │ 1. درخواست    │               │               │
     │   پرداخت      │               │               │
     │──────────────>│               │               │
     │               │               │               │
     │               │ 2. POST       │               │
     │               │  /request.json│               │
     │               │──────────────>│               │
     │               │               │               │
     │               │ 3. Authority  │               │
     │               │<──────────────│               │
     │               │               │               │
     │ 4. Redirect   │               │               │
     │   to Gateway  │               │               │
     │<──────────────│               │               │
     │               │               │               │
     │ 5. پرداخت     │               │               │
     │──────────────────────────────────────────────>│
     │               │               │               │
     │ 6. Callback   │               │               │
     │   (Authority  │               │               │
     │    + Status)  │               │               │
     │──────────────>│               │               │
     │               │               │               │
     │               │ 7. POST       │               │
     │               │  /verify.json │               │
     │               │──────────────>│               │
     │               │               │               │
     │               │ 8. نتیجه      │               │
     │               │   (RefId,     │               │
     │               │    CardPan)   │               │
     │               │<──────────────│               │
     │               │               │               │
     │ 9. نتیجه      │               │               │
     │   نهایی       │               │               │
     │<──────────────│               │               │
```

---

## 4. پیاده‌سازی گام‌به‌گام

### 4.1 تنظیمات (Configuration)

فایل `appsettings.json`:

```json
{
  "PaymentProvider": "zarinpal",
  "ZarinPal": {
    "MerchantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "UseSandbox": true
  }
}
```

| کلید | توضیح |
|------|-------|
| `PaymentProvider` | انتخاب درگاه فعال: `"zarinpal"`, `"mock"` |
| `ZarinPal:MerchantId` | مرچنت آیدی از پنل زرین‌پال (GUID) |
| `ZarinPal:UseSandbox` | `true` = محیط تست / `false` = محیط واقعی |

**Production** (`appsettings.Production.json` یا K8s Secret):
```json
{
  "PaymentProvider": "zarinpal",
  "ZarinPal": {
    "MerchantId": "your-real-merchant-id",
    "UseSandbox": false
  }
}
```

---

### 4.2 Interface — لایه Application

```csharp
namespace YourProject.Application.Common.Interfaces;

/// <summary>
/// Interface یکپارچه برای درگاه‌های پرداخت — Strategy Pattern
/// </summary>
public interface IPaymentGatewayService
{
    /// <summary>
    /// مرحله ۱: ارسال درخواست پرداخت و دریافت لینک درگاه
    /// </summary>
    Task<PaymentInitiateResult> InitiatePaymentAsync(
        PaymentRequest request,
        CancellationToken cancellationToken = default);

    /// <summary>
    /// مرحله ۲: تأیید پرداخت بعد از بازگشت کاربر از درگاه (بدون مبلغ)
    /// </summary>
    Task<PaymentVerificationResult> VerifyPaymentAsync(
        string refId,
        string verificationToken,
        CancellationToken cancellationToken = default);

    /// <summary>
    /// مرحله ۲ (نسخه با مبلغ): تأیید پرداخت — زرین‌پال مبلغ رو در Verify نیاز داره
    /// ⚠ این overload رو برای زرین‌پال حتماً استفاده کنید
    /// </summary>
    Task<PaymentVerificationResult> VerifyPaymentAsync(
        string refId,
        string verificationToken,
        decimal amountInToman,
        CancellationToken cancellationToken = default)
    {
        // Default implementation — throws اگه provider پیاده‌سازی نکنه
        throw new NotImplementedException(
            "درگاه پرداخت باید متد VerifyPaymentAsync با مبلغ را پیاده‌سازی کند");
    }

    /// <summary>
    /// واریز (Payout) — زرین‌پال ساپورت نمیکنه
    /// </summary>
    Task<PayoutResult> ProcessPayoutAsync(
        PayoutRequest request,
        CancellationToken cancellationToken = default);
}
```

> ⚠ **نکته مهم:** زرین‌پال در API Verify مبلغ (Amount) رو هم نیاز داره. اگه مبلغ ارسال نشه (0 باشه) خطای `Code=-1` برمیگردونه. حتماً از overload سه‌آرگومانه (`refId`, `status`, `amountInToman`) استفاده کنید.

---

### 4.3 DTOs — مدل‌های Request/Response

```csharp
// ── درخواست ایجاد پرداخت ──
public class PaymentRequest
{
    /// <summary>مبلغ به تومان (سرویس خودش ×10 میکنه برای ریال)</summary>
    public decimal Amount { get; set; }
    public long UserId { get; set; }
    public string Mobile { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    /// <summary>آدرسی که زرین‌پال بعد از پرداخت کاربر رو Redirect میکنه</summary>
    public string CallbackUrl { get; set; } = string.Empty;
}

// ── نتیجه ایجاد پرداخت ──
public class PaymentInitiateResult
{
    public bool IsSuccess { get; set; }
    /// <summary>Authority — شناسه یکتای تراکنش در زرین‌پال</summary>
    public string? RefId { get; set; }
    /// <summary>URL درگاه برای Redirect کاربر</summary>
    public string? GatewayUrl { get; set; }
    public string? ErrorMessage { get; set; }
}

// ── نتیجه تأیید پرداخت ──
public class PaymentVerificationResult
{
    public bool IsSuccess { get; set; }
    /// <summary>Authority</summary>
    public string RefId { get; set; } = string.Empty;
    /// <summary>شماره پیگیری بانکی (RefId عددی از زرین‌پال)</summary>
    public string? TrackingCode { get; set; }
    /// <summary>مبلغ تأیید شده (به تومان)</summary>
    public decimal Amount { get; set; }
    public string? Message { get; set; }
    /// <summary>شماره کارت ماسک‌شده (مثال: 6037-99**-****-1234)</summary>
    public string? CardPan { get; set; }
    /// <summary>هش کارت پرداخت‌کننده</summary>
    public string? CardHash { get; set; }
    /// <summary>کد وضعیت (100=موفق, 101=قبلاً تأیید شده)</summary>
    public int? VerificationCode { get; set; }
}

// ── درخواست واریز (Payout) — زرین‌پال ندارد ──
public class PayoutRequest
{
    public decimal Amount { get; set; }
    public long UserId { get; set; }
    public string Iban { get; set; } = string.Empty;
    public string AccountHolderName { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public string InternalRefId { get; set; } = string.Empty;
}

// ── نتیجه واریز ──
public class PayoutResult
{
    public bool IsSuccess { get; set; }
    public string? BankRefId { get; set; }
    public string? TrackingCode { get; set; }
    public string? Message { get; set; }
    public DateTime ProcessedAt { get; set; }
}
```

---

### 4.4 Domain Entity — PaymentTransaction

این Entity برای لاگ کردن تمام تراکنش‌های درگاه پرداخت در دیتابیسه:

```csharp
namespace YourProject.Domain.Entities.Payment;

/// <summary>
/// ذخیره تراکنش‌های درگاه پرداخت — یک رکورد برای هر درخواست پرداخت
/// </summary>
public class PaymentTransaction : BaseAuditableEntity
{
    // ── اطلاعات درخواست (مرحله Request) ──
    
    /// <summary>نام درگاه (zarinpal, mock, ...)</summary>
    public string GatewayProvider { get; set; } = string.Empty;
    
    /// <summary>مرچنت آیدی استفاده‌شده</summary>
    public string MerchantId { get; set; } = string.Empty;
    
    /// <summary>مبلغ تراکنش (به تومان)</summary>
    public long Amount { get; set; }
    
    /// <summary>Callback URL ارسال‌شده به درگاه</summary>
    public string CallbackUrl { get; set; } = string.Empty;
    
    /// <summary>توضیح تراکنش</summary>
    public string Description { get; set; } = string.Empty;
    
    /// <summary>شماره موبایل کاربر</summary>
    public string? Mobile { get; set; }
    
    /// <summary>شناسه کاربر</summary>
    public long? UserId { get; set; }

    // ── پاسخ Request API ──
    
    /// <summary>کد وضعیت از Request API (100=موفق)</summary>
    public int? RequestStatusCode { get; set; }
    public string? RequestStatusMessage { get; set; }
    
    /// <summary>Authority — شناسه یکتای تراکنش در زرین‌پال</summary>
    public string? Authority { get; set; }

    // ── وضعیت نهایی ──
    
    /// <summary>آیا پرداخت موفق بود؟</summary>
    public bool PaymentStatus { get; set; }

    // ── نتیجه Verify API ──
    
    /// <summary>کد وضعیت از Verify API (100=موفق, 101=تکراری)</summary>
    public int? VerificationStatusCode { get; set; }
    public string? VerificationStatusMessage { get; set; }
    
    /// <summary>هش کارت بانکی</summary>
    public string? CardHash { get; set; }
    
    /// <summary>شماره کارت ماسک‌شده</summary>
    public string? CardPan { get; set; }
    
    /// <summary>شماره پیگیری بانکی</summary>
    public string? RefId { get; set; }

    // ── ارتباط با سیستم داخلی ──
    
    /// <summary>شناسه تراکنش داخلی (اختیاری)</summary>
    public long? TransactionId { get; set; }
    
    /// <summary>شناسه سفارش (اختیاری)</summary>
    public string? OrderId { get; set; }
}
```

---

### 4.5 سرویس زرین‌پال — لایه Infrastructure

```csharp
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using YourProject.Application.Common.Interfaces;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace YourProject.Infrastructure.Services.Payment;

public class ZarinPalPaymentService : IPaymentGatewayService
{
    private readonly HttpClient _httpClient;
    private readonly ILogger<ZarinPalPaymentService> _logger;
    private readonly string _merchantId;
    private readonly bool _useSandbox;

    // ── آدرس‌های API ──
    private const string ProductionApiBase   = "https://api.zarinpal.com";
    private const string ProductionStartPay  = "https://www.zarinpal.com";
    private const string SandboxApiBase      = "https://sandbox.zarinpal.com";
    private const string SandboxStartPay     = "https://sandbox.zarinpal.com";
    
    // ── Endpoints (مشترک بین Sandbox و Production) ──
    private const string RequestEndpoint = "/pg/v4/payment/request.json";
    private const string VerifyEndpoint  = "/pg/v4/payment/verify.json";
    private const string StartPayPath    = "/pg/StartPay/";

    // ── JSON Config ──
    // ⚠ زرین‌پال snake_case میخواد: merchant_id, callback_url, ...
    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
    };

    public ZarinPalPaymentService(
        HttpClient httpClient,
        IConfiguration configuration,
        ILogger<ZarinPalPaymentService> logger)
    {
        _httpClient = httpClient;
        _logger = logger;

        _merchantId = configuration["ZarinPal:MerchantId"]
            ?? throw new InvalidOperationException("ZarinPal:MerchantId is not configured.");
        
        _useSandbox = configuration.GetValue<bool>("ZarinPal:UseSandbox", true);

        // Base Address بر اساس محیط
        var apiBase = _useSandbox ? SandboxApiBase : ProductionApiBase;
        _httpClient.BaseAddress = new Uri(apiBase);

        _logger.LogInformation("ZarinPal initialized. Mode: {Mode}",
            _useSandbox ? "Sandbox" : "Production");
    }

    // ═══════════════════════════════════════════════════════
    //  مرحله ۱: InitiatePayment — ایجاد تراکنش و دریافت Authority
    // ═══════════════════════════════════════════════════════
    public async Task<PaymentInitiateResult> InitiatePaymentAsync(
        PaymentRequest request,
        CancellationToken cancellationToken = default)
    {
        try
        {
            // ⚠ مبلغ از caller به تومان می‌رسد — تبدیل به ریال (×10)
            var amountInToman = (long)request.Amount;
            var amountInRials = amountInToman * 10;

            var zarinPalRequest = new ZarinPalPaymentRequest
            {
                MerchantId = _merchantId,
                Amount = amountInRials,       // باید ریال باشد
                Description = request.Description,
                CallbackUrl = request.CallbackUrl,
                Metadata = new ZarinPalMetadata
                {
                    Mobile = string.IsNullOrWhiteSpace(request.Mobile) 
                        ? null : request.Mobile
                }
            };

            var jsonContent = JsonSerializer.Serialize(zarinPalRequest, JsonOptions);
            var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");

            _logger.LogInformation(
                "ZarinPal request: Amount={Toman}T ({Rial}R), User={User}",
                amountInToman, amountInRials, request.UserId);

            var response = await _httpClient.PostAsync(
                RequestEndpoint, content, cancellationToken);
            var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);

            // HTTP Error
            if (!response.IsSuccessStatusCode)
            {
                _logger.LogError("ZarinPal HTTP {Code}: {Body}",
                    (int)response.StatusCode, responseBody);

                return new PaymentInitiateResult
                {
                    IsSuccess = false,
                    ErrorMessage = $"خطای ارتباط با زرین‌پال (HTTP {(int)response.StatusCode})"
                };
            }

            var result = JsonSerializer.Deserialize<ZarinPalResponse>(
                responseBody, JsonOptions);

            // ✅ موفق — Code 100 + Authority
            if (result?.Data?.Code == 100 
                && !string.IsNullOrEmpty(result.Data.Authority))
            {
                var startPayBase = _useSandbox 
                    ? SandboxStartPay : ProductionStartPay;
                var gatewayUrl = 
                    $"{startPayBase}{StartPayPath}{result.Data.Authority}";

                _logger.LogInformation(
                    "ZarinPal OK: Authority={Auth}, URL={Url}",
                    result.Data.Authority, gatewayUrl);

                return new PaymentInitiateResult
                {
                    IsSuccess = true,
                    RefId = result.Data.Authority,  // ← Authority
                    GatewayUrl = gatewayUrl          // ← لینک Redirect
                };
            }

            // ❌ خطای زرین‌پال
            var errorCode = result?.Errors?.Code 
                ?? result?.Data?.Code ?? -1;
            var errorMessage = result?.Errors?.Message 
                ?? "خطای ناشناخته از زرین‌پال";

            _logger.LogError("ZarinPal error: Code={Code}, Msg={Msg}",
                errorCode, errorMessage);

            return new PaymentInitiateResult
            {
                IsSuccess = false,
                ErrorMessage = 
                    $"خطای درگاه زرین‌پال (کد {errorCode}): {errorMessage}"
            };
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "ZarinPal InitiatePayment exception");
            return new PaymentInitiateResult
            {
                IsSuccess = false,
                ErrorMessage = $"خطا در ارتباط با درگاه: {ex.Message}"
            };
        }
    }

    // ═══════════════════════════════════════════════════════
    //  مرحله ۲: VerifyPayment — تأیید پرداخت بعد از Callback
    // ═══════════════════════════════════════════════════════
    
    /// <summary>Verify بدون مبلغ — سازگاری با interface (ممکنه fail بشه!)</summary>
    public Task<PaymentVerificationResult> VerifyPaymentAsync(
        string refId, string verificationToken,
        CancellationToken cancellationToken = default)
    {
        _logger.LogWarning("VerifyPayment called without amount — may fail!");
        return VerifyPaymentWithAmountAsync(
            refId, verificationToken, 0, cancellationToken);
    }

    /// <summary>
    /// Verify با مبلغ — نسخه اصلی
    /// refId = Authority, verificationToken = "OK"/"NOK", amount = تومان
    /// </summary>
    public Task<PaymentVerificationResult> VerifyPaymentAsync(
        string refId, string verificationToken, decimal amount,
        CancellationToken cancellationToken = default)
    {
        return VerifyPaymentWithAmountAsync(
            refId, verificationToken, amount, cancellationToken);
    }

    private async Task<PaymentVerificationResult> VerifyPaymentWithAmountAsync(
        string refId, string verificationToken,
        decimal amountInToman, CancellationToken cancellationToken)
    {
        try
        {
            // 1. چک Status — "OK" یعنی کاربر پرداخت کرده
            if (!string.Equals(verificationToken, "OK", 
                StringComparison.OrdinalIgnoreCase))
            {
                _logger.LogWarning(
                    "Payment cancelled by user: Authority={Auth}", refId);
                return new PaymentVerificationResult
                {
                    IsSuccess = false,
                    RefId = refId,
                    Message = "پرداخت توسط کاربر لغو شد"
                };
            }

            // 2. تبدیل تومان → ریال
            var amountInRials = (long)(amountInToman * 10);

            // 3. ارسال Verify Request
            var verifyRequest = new ZarinPalVerifyRequest
            {
                MerchantId = _merchantId,
                Authority = refId,
                Amount = amountInRials  // ⚠ باید ریال باشد
            };

            var jsonContent = JsonSerializer.Serialize(
                verifyRequest, JsonOptions);
            var content = new StringContent(
                jsonContent, Encoding.UTF8, "application/json");

            _logger.LogInformation(
                "ZarinPal verify: Auth={Auth}, Amount={T}T ({R}R)",
                refId, (long)amountInToman, amountInRials);

            var response = await _httpClient.PostAsync(
                VerifyEndpoint, content, cancellationToken);
            var responseBody = await response.Content
                .ReadAsStringAsync(cancellationToken);

            var result = JsonSerializer.Deserialize<ZarinPalResponse>(
                responseBody, JsonOptions);

            // 4. بررسی نتیجه
            // Code 100 = موفق  |  Code 101 = قبلاً verify شده (تکراری)
            if (result?.Data?.Code is 100 or 101)
            {
                _logger.LogInformation(
                    "ZarinPal verified: Auth={Auth}, RefId={Ref}, Card={Card}",
                    refId, result.Data.RefId, result.Data.CardPan);

                return new PaymentVerificationResult
                {
                    IsSuccess = true,
                    RefId = refId,
                    TrackingCode = result.Data.RefId?.ToString(),
                    Amount = (result.Data.Amount ?? 0) / 10,  // ریال → تومان
                    CardPan = result.Data.CardPan,
                    CardHash = result.Data.CardHash,
                    VerificationCode = result.Data.Code,
                    Message = result.Data.Code == 101
                        ? "تراکنش قبلاً تأیید شده"
                        : "تراکنش موفق"
                };
            }

            // ❌ Verify ناموفق
            var errorCode = result?.Errors?.Code 
                ?? result?.Data?.Code ?? -1;
            var errorMessage = result?.Errors?.Message 
                ?? "تأیید تراکنش ناموفق";

            _logger.LogError(
                "ZarinPal verify failed: Auth={Auth}, Code={Code}",
                refId, errorCode);

            return new PaymentVerificationResult
            {
                IsSuccess = false,
                RefId = refId,
                Message = $"تأیید پرداخت ناموفق (کد {errorCode}): {errorMessage}"
            };
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, 
                "ZarinPal Verify exception: Auth={Auth}", refId);
            return new PaymentVerificationResult
            {
                IsSuccess = false,
                RefId = refId,
                Message = $"خطا در تأیید تراکنش: {ex.Message}"
            };
        }
    }

    /// <summary>زرین‌پال Payout ندارد</summary>
    public Task<PayoutResult> ProcessPayoutAsync(
        PayoutRequest request,
        CancellationToken cancellationToken = default)
    {
        _logger.LogWarning("ZarinPal does not support payout.");
        return Task.FromResult(new PayoutResult
        {
            IsSuccess = false,
            Message = "زرین‌پال از واریز مستقیم پشتیبانی نمی‌کند",
            ProcessedAt = DateTime.UtcNow
        });
    }

    // ═══════════════════════════════════════════════════════
    //  Private DTOs — ساختار API زرین‌پال v4
    // ═══════════════════════════════════════════════════════

    // ── Request ──
    private class ZarinPalPaymentRequest
    {
        public string MerchantId { get; set; } = string.Empty;
        public long Amount { get; set; }         // ریال
        public string Description { get; set; } = string.Empty;
        public string CallbackUrl { get; set; } = string.Empty;
        public ZarinPalMetadata? Metadata { get; set; }
    }

    private class ZarinPalMetadata
    {
        public string? Mobile { get; set; }
        public string? Email { get; set; }
    }

    // ── Verify ──
    private class ZarinPalVerifyRequest
    {
        public string MerchantId { get; set; } = string.Empty;
        public long Amount { get; set; }         // ریال
        public string Authority { get; set; } = string.Empty;
    }

    // ── Response ──
    private class ZarinPalResponse
    {
        public ZarinPalResponseData? Data { get; set; }

        [JsonConverter(typeof(ZarinPalErrorsConverter))]
        public ZarinPalResponseErrors? Errors { get; set; }
    }

    private class ZarinPalResponseData
    {
        public int? Code { get; set; }         // 100=موفق, 101=تکراری
        public string? Message { get; set; }
        public string? Authority { get; set; }  // فقط در Request
        public long? RefId { get; set; }        // فقط در Verify — شماره پیگیری
        public long? Amount { get; set; }       // ریال
        public string? CardPan { get; set; }    // شماره کارت ماسک‌شده
        public string? CardHash { get; set; }   // هش کارت
        public string? FeeType { get; set; }
        public long? Fee { get; set; }
    }

    private class ZarinPalResponseErrors
    {
        public int? Code { get; set; }
        public string? Message { get; set; }
    }

    // ═══════════════════════════════════════════════════════
    //  Custom JSON Converter (مهم!)
    // ═══════════════════════════════════════════════════════
    //
    //  زرین‌پال وقتی خطا نداره، فیلد errors رو به‌صورت
    //  آرایه خالی [] برمیگردونه — نه object! 
    //  ولی وقتی خطا هست، {...} object برمیگردونه.
    //  بدون این Converter دسیریالایز crash میکنه.
    //
    private class ZarinPalErrorsConverter : JsonConverter<ZarinPalResponseErrors?>
    {
        public override ZarinPalResponseErrors? Read(
            ref Utf8JsonReader reader, Type typeToConvert,
            JsonSerializerOptions options)
        {
            // حالت ۱: [] آرایه خالی — هیچ خطایی نیست
            if (reader.TokenType == JsonTokenType.StartArray)
            {
                while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) { }
                return null;
            }

            // حالت ۲: {...} object — خطا وجود داره
            if (reader.TokenType == JsonTokenType.StartObject)
            {
                return JsonSerializer
                    .Deserialize<ZarinPalResponseErrors>(ref reader);
            }

            // حالت ۳: null
            if (reader.TokenType == JsonTokenType.Null)
            {
                return null;
            }

            reader.Skip();
            return null;
        }

        public override void Write(
            Utf8JsonWriter writer, ZarinPalResponseErrors? value,
            JsonSerializerOptions options)
        {
            JsonSerializer.Serialize(writer, value, options);
        }
    }
}
```

---

### 4.6 ثبت سرویس (DI Registration)

در `ConfigureServices.cs` لایه Infrastructure:

```csharp
public static IServiceCollection AddInfrastructureServices(
    this IServiceCollection services, IConfiguration configuration)
{
    // ... سرویس‌های دیگر ...

    // ── Payment Gateway — Strategy Pattern ──
    var paymentProvider = configuration
        .GetValue<string>("PaymentProvider", "Mock")
        ?.ToLowerInvariant();

    switch (paymentProvider)
    {
        case "zarinpal":
            services.AddHttpClient<IPaymentGatewayService, ZarinPalPaymentService>()
                .SetHandlerLifetime(TimeSpan.FromMinutes(5));
            break;
        
        case "mock":
        default:
            services.AddScoped<IPaymentGatewayService, MockPaymentGatewayService>();
            break;
    }

    return services;
}
```

**نکات:**
- از `AddHttpClient<>` استفاده شده (نه `AddScoped`) — `HttpClientFactory` مدیریت connection pooling رو بر عهده میگیره
- `SetHandlerLifetime(5min)` — جلوگیری از DNS stale issues
- `MockPaymentGatewayService` با `AddScoped` ثبت میشه چون `HttpClient` نیاز نداره

---

### 4.7 Mock Service (برای تست)

```csharp
public class MockPaymentGatewayService : IPaymentGatewayService
{
    private readonly ILogger<MockPaymentGatewayService> _logger;

    public MockPaymentGatewayService(ILogger<MockPaymentGatewayService> logger)
    {
        _logger = logger;
    }

    public async Task<PaymentInitiateResult> InitiatePaymentAsync(
        PaymentRequest request, CancellationToken ct = default)
    {
        _logger.LogWarning("⚠️ Using MOCK Payment Gateway");
        await Task.Delay(200, ct);
        var refId = $"MOCK-PAY-{DateTime.Now.Ticks}";
        return new PaymentInitiateResult
        {
            IsSuccess = true,
            RefId = refId,
            GatewayUrl = $"https://mock-gateway.local/pay?ref={refId}"
        };
    }

    public async Task<PaymentVerificationResult> VerifyPaymentAsync(
        string refId, string token, CancellationToken ct = default)
    {
        await Task.Delay(150, ct);
        return new PaymentVerificationResult
        {
            IsSuccess = true,
            RefId = refId,
            TrackingCode = $"TRK-{DateTime.Now.Ticks}",
            Amount = 0,
            Message = "تراکنش موفق (Mock)"
        };
    }

    public Task<PaymentVerificationResult> VerifyPaymentAsync(
        string refId, string token, decimal amount, CancellationToken ct = default)
        => VerifyPaymentAsync(refId, token, ct);

    public async Task<PayoutResult> ProcessPayoutAsync(
        PayoutRequest request, CancellationToken ct = default)
    {
        await Task.Delay(300, ct);
        return new PayoutResult
        {
            IsSuccess = true,
            BankRefId = $"BANK-{DateTime.Now.Ticks}",
            TrackingCode = $"TRK-PAYOUT-{DateTime.Now.Ticks}",
            Message = $"واریز {request.Amount:N0} تومان (Mock)",
            ProcessedAt = DateTime.Now
        };
    }
}
```

---

## 5. فلوی Initiate Payment

الگوی استفاده در Handler/Controller:

```csharp
// 1. ساخت PaymentTransaction برای لاگ
var paymentTx = new PaymentTransaction
{
    GatewayProvider = _configuration["PaymentProvider"] ?? "zarinpal",
    MerchantId = _configuration["ZarinPal:MerchantId"] ?? "",
    Amount = amountInToman,    // تومان ذخیره میشه
    CallbackUrl = callbackUrl,
    Description = "توضیح تراکنش",
    Mobile = user.PhoneNumber,
    UserId = userId,
};
_context.PaymentTransactions.Add(paymentTx);
await _context.SaveChangesAsync(ct);

// 2. ارسال به درگاه
var paymentResult = await _paymentGateway.InitiatePaymentAsync(new PaymentRequest
{
    Amount = amountInToman,        // به تومان — سرویس ×10 میکنه
    UserId = userId,
    Mobile = user.PhoneNumber,
    Description = "توضیح تراکنش",
    CallbackUrl = callbackUrl      // URL بازگشت کاربر
}, ct);

// 3. آپدیت PaymentTransaction
if (paymentResult.IsSuccess)
{
    paymentTx.Authority = paymentResult.RefId;       // Authority
    paymentTx.RequestStatusCode = 100;
    paymentTx.RequestStatusMessage = "Success";
}
else
{
    paymentTx.RequestStatusCode = -1;
    paymentTx.RequestStatusMessage = paymentResult.ErrorMessage;
}
await _context.SaveChangesAsync(ct);

// 4. برگرداندن لینک درگاه به کلاینت
if (paymentResult.IsSuccess)
{
    // کلاینت باید کاربر رو Redirect کنه به paymentResult.GatewayUrl
    return new { GatewayUrl = paymentResult.GatewayUrl };
}
```

---

## 6. فلوی Verify Payment

بعد از پرداخت، زرین‌پال کاربر رو به `CallbackUrl` ریدایرکت میکنه با این Query Parameters:

```
https://your-site.com/payment/callback?Authority=A00000000000000000000000000123456&Status=OK
```

| Parameter | توضیح |
|-----------|-------|
| `Authority` | شناسه تراکنش (همون RefId که از Request گرفتید) |
| `Status` | `"OK"` = پرداخت موفق / `"NOK"` = لغو یا ناموفق |

**⚠ مهم:** فقط Status=OK یعنی کاربر پول داده — باید Verify کنید تا مطمئن بشید.

```csharp
// 1. دریافت Authority و Status از Callback
var authority = request.Authority;  // از Query String
var status = request.Status;        // "OK" یا "NOK"

// 2. پیدا کردن PaymentTransaction و مبلغ از دیتابیس
var paymentTx = await _context.PaymentTransactions
    .FirstOrDefaultAsync(pt => pt.Authority == authority, ct);

if (paymentTx == null) throw new NotFoundException("تراکنش یافت نشد");

var amountInToman = paymentTx.Amount;   // مبلغ از دیتابیس

// 3. Verify با درگاه
// ⚠ حتماً مبلغ رو هم بفرستید — زرین‌پال نیاز داره
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
    authority,              // refId = Authority
    status,                 // verificationToken = "OK" یا "NOK"
    amountInToman,          // مبلغ به تومان — سرویس ×10 میکنه
    ct
);

// 4. آپدیت PaymentTransaction
paymentTx.PaymentStatus = verifyResult.IsSuccess;
paymentTx.VerificationStatusCode = verifyResult.VerificationCode;
paymentTx.VerificationStatusMessage = verifyResult.Message;
paymentTx.CardPan = verifyResult.CardPan;
paymentTx.CardHash = verifyResult.CardHash;
paymentTx.RefId = verifyResult.TrackingCode;
await _context.SaveChangesAsync(ct);

// 5. اگه موفق بود → انجام عملیات بیزینسی (شارژ کیف پول، ثبت سفارش، ...)
if (verifyResult.IsSuccess)
{
    // ✅ تراکنش موفق — بیزینس لاجیک اینجا
}
```

---

## 7. نکات مهم تومان/ریال

> **قانون کلی:** همه جا به **تومان** کار کنید. فقط `ZarinPalPaymentService` خودش ×10 (تبدیل به ریال) میکنه.

```
┌──────────────────────────────────────────────────────────────┐
│  دیتابیس        →  تومان ذخیره میشه                         │
│  PaymentRequest  →  Amount به تومان ارسال میشه               │
│  ZarinPalService →  خودش ×10 میکنه و به ریال به API میفرسته │
│  Verify Response →  Amount ریال برمیگرده → سرویس ÷10 میکنه  │
│  VerifyResult    →  Amount به تومان برمیگرده                 │
└──────────────────────────────────────────────────────────────┘
```

**باگ رایج:** اگه caller هم ×10 کنه و سرویس هم ×10 کنه = مبلغ ×100 میشه! 

---

## 8. مدیریت خطای API زرین‌پال

زرین‌پال یه ساختار خاص برای Response داره:

**وقتی موفقه:**
```json
{
  "data": {
    "code": 100,
    "message": "Success",
    "authority": "A00000000000000000000000000123456",
    "fee_type": "Merchant",
    "fee": 0
  },
  "errors": []          ← آرایه خالی! (نه object)
}
```

**وقتی خطا داره:**
```json
{
  "data": [],
  "errors": {            ← object! (نه آرایه)
    "code": -9,
    "message": "The input params invalid, ..."
  }
}
```

**⚠ مشکل:** فیلد `errors` گاهی `[]` (آرایه) و گاهی `{...}` (object) برمیگردونه. بدون Custom Converter، `System.Text.Json` خطا میده.

**راه‌حل:** `ZarinPalErrorsConverter` — یک `JsonConverter` سفارشی که هر دو حالت رو هندل میکنه. (کد کامل در بخش 4.5)

---

## 9. Sandbox vs Production

| | Sandbox (تست) | Production (واقعی) |
|---|---|---|
| **Config** | `"UseSandbox": true` | `"UseSandbox": false` |
| **API Base** | `https://sandbox.zarinpal.com` | `https://api.zarinpal.com` |
| **StartPay** | `https://sandbox.zarinpal.com/pg/StartPay/{Authority}` | `https://www.zarinpal.com/pg/StartPay/{Authority}` |
| **پول واقعی** | ❌ نه | ✅ بله |
| **MerchantId** | هر GUID — معتبر نیازی نیست | از پنل زرین‌پال |

**Sandbox نکات:**
- هر MerchantId (حتی fake) کار میکنه
- پرداخت simulate میشه — پول واقعی کسر نمیشه
- Authority و RefId واقعی برمیگردونه

---

## 10. الگوی استفاده در Handler/Controller

### الگوی ۱: REST Controller (Callback Endpoint)

```csharp
[ApiController]
[Route("api/payment")]
public class PaymentController : ControllerBase
{
    private readonly IPaymentGatewayService _paymentGateway;
    private readonly ApplicationDbContext _context;

    // POST: api/payment/initiate
    [HttpPost("initiate")]
    public async Task<IActionResult> Initiate([FromBody] InitiateDto dto)
    {
        // ... ساخت PaymentTransaction + ارسال به درگاه ...
        var result = await _paymentGateway.InitiatePaymentAsync(
            new PaymentRequest { ... });
        
        if (result.IsSuccess)
            return Ok(new { result.GatewayUrl });
        
        return BadRequest(new { result.ErrorMessage });
    }

    // GET: api/payment/callback?Authority=xxx&Status=OK
    [HttpGet("callback")]
    public async Task<IActionResult> Callback(
        [FromQuery] string Authority,
        [FromQuery] string Status)
    {
        var paymentTx = await _context.PaymentTransactions
            .FirstOrDefaultAsync(p => p.Authority == Authority);
        
        if (paymentTx == null) return NotFound();

        var result = await _paymentGateway.VerifyPaymentAsync(
            Authority, Status, paymentTx.Amount);

        // آپدیت PaymentTransaction ...
        
        if (result.IsSuccess)
            return Redirect("/payment/success");
        
        return Redirect("/payment/failed");
    }
}
```

### الگوی ۲: CQRS Command Handler

```csharp
public class InitiatePaymentCommandHandler 
    : IRequestHandler<InitiatePaymentCommand, PaymentResult>
{
    private readonly IPaymentGatewayService _paymentGateway;
    private readonly IApplicationDbContext _context;
    private readonly IConfiguration _configuration;

    public async Task<PaymentResult> Handle(
        InitiatePaymentCommand cmd, CancellationToken ct)
    {
        // 1. ساخت PaymentTransaction
        var tx = new PaymentTransaction
        {
            GatewayProvider = _configuration["PaymentProvider"] ?? "zarinpal",
            MerchantId = _configuration["ZarinPal:MerchantId"] ?? "",
            Amount = cmd.Amount,
            CallbackUrl = cmd.CallbackUrl,
            Description = cmd.Description,
            UserId = cmd.UserId,
        };
        _context.PaymentTransactions.Add(tx);
        await _context.SaveChangesAsync(ct);

        // 2. ارسال به درگاه
        var result = await _paymentGateway.InitiatePaymentAsync(
            new PaymentRequest
            {
                Amount = cmd.Amount,
                UserId = cmd.UserId,
                CallbackUrl = cmd.CallbackUrl,
                Description = cmd.Description,
            }, ct);

        // 3. آپدیت
        tx.Authority = result.RefId;
        tx.RequestStatusCode = result.IsSuccess ? 100 : -1;
        await _context.SaveChangesAsync(ct);

        return new PaymentResult { ... };
    }
}
```

---

## 11. Migration — جدول PaymentTransaction

```sql
CREATE TABLE PaymentTransactions (
    Id              BIGINT PRIMARY KEY IDENTITY,
    
    -- Request Info
    GatewayProvider     NVARCHAR(50)    NOT NULL DEFAULT 'zarinpal',
    MerchantId          NVARCHAR(100)   NOT NULL,
    Amount              BIGINT          NOT NULL,  -- تومان
    CallbackUrl         NVARCHAR(500)   NOT NULL,
    Description         NVARCHAR(500)   NULL,
    Mobile              NVARCHAR(20)    NULL,
    UserId              BIGINT          NULL,
    
    -- Request Response
    RequestStatusCode   INT             NULL,
    RequestStatusMessage NVARCHAR(500)  NULL,
    Authority           NVARCHAR(100)   NULL,  -- INDEX ← سرچ Verify روی این
    
    -- Payment Status
    PaymentStatus       BIT             NOT NULL DEFAULT 0,
    
    -- Verify Response
    VerificationStatusCode    INT           NULL,
    VerificationStatusMessage NVARCHAR(500) NULL,
    CardHash                  NVARCHAR(200) NULL,
    CardPan                   NVARCHAR(50)  NULL,
    RefId                     NVARCHAR(100) NULL,  -- شماره پیگیری بانکی
    
    -- Internal Links
    TransactionId       BIGINT          NULL,
    OrderId             NVARCHAR(100)   NULL,
    
    -- Audit
    Created             DATETIME2       NOT NULL DEFAULT GETUTCDATE(),
    LastModified        DATETIME2       NULL,
    CreatedBy           NVARCHAR(100)   NULL,
    LastModifiedBy      NVARCHAR(100)   NULL
);

-- ایندکس روی Authority — برای Verify Lookup
CREATE INDEX IX_PaymentTransactions_Authority 
    ON PaymentTransactions(Authority);
```

**EF Core Fluent API:**

```csharp
builder.Entity<PaymentTransaction>(entity =>
{
    entity.HasIndex(e => e.Authority);
    entity.Property(e => e.GatewayProvider).HasMaxLength(50);
    entity.Property(e => e.MerchantId).HasMaxLength(100);
    entity.Property(e => e.Authority).HasMaxLength(100);
    entity.Property(e => e.CardPan).HasMaxLength(50);
    entity.Property(e => e.RefId).HasMaxLength(100);
});
```

---

## 12. چک‌لیست پیاده‌سازی در پروژه جدید

### مرحله ۱: آماده‌سازی زیرساخت
- [ ] ثبت‌نام در [zarinpal.com](https://www.zarinpal.com) و دریافت MerchantId
- [ ] IP سرور رو در پنل زرین‌پال ثبت کنید (برای Production)
- [ ] Callback URL رو در پنل تنظیم کنید

### مرحله ۲: کد
- [ ] `IPaymentGatewayService` و DTOs رو در لایه Application ایجاد کنید
- [ ] `PaymentTransaction` Entity رو در لایه Domain ایجاد کنید
- [ ] `ZarinPalPaymentService` رو در لایه Infrastructure ایجاد کنید
- [ ] `MockPaymentGatewayService` رو ایجاد کنید
- [ ] DI Registration در `ConfigureServices.cs` (switch بر اساس `PaymentProvider`)
- [ ] Migration برای جدول `PaymentTransactions` اجرا کنید
- [ ] Callback Endpoint (REST یا gRPC) ایجاد کنید

### مرحله ۳: Configuration
- [ ] `appsettings.json` — تنظیمات Development (Sandbox)
- [ ] `appsettings.Production.json` — تنظیمات Production
- [ ] Environment variable یا K8s Secret برای MerchantId واقعی

### مرحله ۴: تست
- [ ] تست با Mock (بدون درگاه واقعی)
- [ ] تست با Sandbox زرین‌پال
- [ ] تست خطای شبکه (timeout, HTTP 5xx)
- [ ] تست Cancel — کاربر برمیگرده با `Status=NOK`
- [ ] تست Verify تکراری (Code 101)
- [ ] تست مبلغ صفر در Verify (باید خطا بده)

### مرحله ۵: Production
- [ ] `UseSandbox: false` تنظیم شه
- [ ] MerchantId واقعی تنظیم شه
- [ ] لاگ‌ها بررسی بشن (Authority, Amount, HTTP Status)
- [ ] مانیتورینگ تراکنش‌های ناموفق

---

## 13. کدهای خطای زرین‌پال

### کدهای Request API

| Code | توضیح |
|------|-------|
| `100` | ✅ موفق — Authority ایجاد شد |
| `-9` | پارامترهای ورودی نامعتبر |
| `-10` | مرچنت آیدی نامعتبر |
| `-11` | مرچنت غیرفعال |
| `-12` | تلاش بیش از حد — بعداً تلاش کنید |
| `-15` | مبلغ کمتر از حد مجاز (حداقل ۱۰۰۰ ریال) |
| `-16` | سطح دسترسی مرچنت پایین‌تر از حد نقره‌ای |
| `-30` | مرچنت دسترسی به Settlement ندارد |
| `-31` | حساب بانکی متصل نیست |
| `-32` | مشکل اتصال حساب بانکی |
| `-33` | مبلغ بالاتر از حد مجاز |
| `-34` | محدودیت تعداد تراکنش |
| `-40` | دسترسی غیرمجاز به method |
| `-54` | درخواست آرشیو شده |

### کدهای Verify API

| Code | توضیح |
|------|-------|
| `100` | ✅ تأیید موفق |
| `101` | ✅ قبلاً تأیید شده (تکراری — حساب نشده) |
| `-1` | اطلاعات ناقص (معمولاً Amount=0) |
| `-50` | مبلغ ارسالی با مبلغ تراکنش مطابقت ندارد |
| `-51` | پرداخت ناموفق |
| `-52` | خطای غیرمنتظره |
| `-53` | Authority نامعتبر |
| `-54` | تراکنش آرشیو شده |

---

## 14. Sequence Diagram

```mermaid
sequenceDiagram
    participant C as Client (Browser)
    participant S as Your Server
    participant DB as Database
    participant ZP as ZarinPal API
    participant GW as ZarinPal Gateway

    Note over C,GW: ─── مرحله ۱: ایجاد پرداخت ───

    C->>S: POST /api/payment/initiate<br/>{amount: 50000, ...}
    S->>DB: INSERT PaymentTransaction<br/>(amount=50000, provider=zarinpal)
    
    S->>ZP: POST /pg/v4/payment/request.json<br/>{merchant_id, amount: 500000, callback_url}
    Note right of S: ×10 تبدیل تومان به ریال
    
    ZP-->>S: {code: 100, authority: "A0000...123"}
    S->>DB: UPDATE PaymentTransaction<br/>SET Authority = "A0000...123"
    S-->>C: {gatewayUrl: "https://zarinpal.com/pg/StartPay/A0000...123"}

    Note over C,GW: ─── مرحله ۲: پرداخت در درگاه ───

    C->>GW: Redirect → درگاه بانکی
    GW->>GW: کاربر اطلاعات کارت وارد میکنه
    GW-->>C: Redirect → callback?Authority=A0000...123&Status=OK

    Note over C,GW: ─── مرحله ۳: تأیید پرداخت ───

    C->>S: GET /api/payment/callback<br/>?Authority=A0000...123&Status=OK
    
    S->>DB: SELECT * FROM PaymentTransactions<br/>WHERE Authority = "A0000...123"
    DB-->>S: {amount: 50000, ...}
    
    S->>ZP: POST /pg/v4/payment/verify.json<br/>{merchant_id, authority, amount: 500000}
    Note right of S: ×10 تبدیل تومان به ریال
    
    ZP-->>S: {code: 100, ref_id: 45678, card_pan: "6037-99**-****-1234"}
    
    S->>DB: UPDATE PaymentTransaction<br/>SET PaymentStatus=true, RefId="45678",<br/>CardPan="6037-99**-****-1234"
    
    S->>S: ✅ بیزینس لاجیک (شارژ کیف، ثبت سفارش، ...)
    S-->>C: Redirect → /payment/success
```

---

## NuGet Packages مورد نیاز

```xml
<!-- فقط همینا نیاز هست — بدون پکیج اضافه! -->
<PackageReference Include="Microsoft.Extensions.Http" Version="9.*" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.*" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.*" />
```

`System.Text.Json` و `System.Net.Http` جزو فریمورک هستن و پکیج جداگانه نمیخوان.

---

## خلاصه سریع

| چی | کجا |
|----|-----|
| Interface + DTOs | `Application/Common/Interfaces/IPaymentGatewayService.cs` |
| Entity | `Domain/Entities/Payment/PaymentTransaction.cs` |
| ZarinPal Service | `Infrastructure/Services/Payment/ZarinPalPaymentService.cs` |
| Mock Service | `Infrastructure/Services/Payment/MockPaymentGatewayService.cs` |
| DI Switch | `Infrastructure/ConfigureServices.cs` |
| Config | `appsettings.json` → `PaymentProvider` + `ZarinPal:*` |
| واحد پول | **تومان** — سرویس خودش ×10 میکنه |
| API Version | v4 (`/pg/v4/payment/...`) |
| JSON Naming | `snake_case` (`PropertyNamingPolicy.SnakeCaseLower`) |
| خطای رایج | Amount=0 در Verify → Code=-1 |
| خطای رایج | errors=[] vs errors={} → نیاز به Custom Converter |
