Files
docs/cms/payment-gateway.md
T
masoodafar-web 4ef4bfbeef docs: بروزرسانی کامل مستندات — باگ‌ها، فیکس‌ها، دیپلوی Production، CI/CD cross-deploy
- payment-gateway.md: سکشن ۸-۱۳ (ZarinPal callback, تخفیف ۱۰۰٪, VAT, ExpirePendingOrders, DeliveryStatus mapping, Production deploy)
- CICD-PIPELINE-GUIDE.md: باگ cross-deploy, قالب workflow Production, جدول مقایسه دو محیط
- INFRASTRUCTURE-GUIDE.md: سرور Production (45.149.79.127), DB KBS, Proto v0.0.179
- DISCOUNT-STORE-STATUS.md: وضعیت Production Deploy, فلوی پرداخت جدید
- SERVER-MIRRORS-CONFIG.md: registries.yaml سرور Production
- INDEX.md: تاریخ, توضیحات بروز, لینک‌های سریع جدید
2026-02-17 01:44:05 +03:30

31 KiB
Raw Blame History

Payment Gateway Integration Guide

📋 Overview

🔄 جریان پرداخت در سیستم

1️⃣ دریافت پول از کاربر (Payment IN)

کاربر → Gateway/PYMS → بانک → پرداخت موفق
                              ↓
                      Callback به CMS
                              ↓
          CMS: VerifyTransaction + فعال‌سازی عضویت

توضیح:

  • درگاه اینترنتی در Gateway/PYMS است (نه CMS)
  • CMS فقط نتیجه پرداخت را دریافت می‌کند (از طریق Callback)
  • سپس عملیات بعدی (فعال‌سازی، اضافه PV، Wallet) را انجام می‌دهد
  • Transaction System در CMS برای این کار طراحی شده

2️⃣ پرداخت به کاربر (Payout)

ادمین تایید برداشت → CMS → DayaPaymentService → واریز به حساب کاربر

توضیح:

  • این سند فقط برای Payout است
  • سیستم از دو پیاده‌سازی پشتیبانی می‌کند:
  1. MockPaymentGatewayService - برای Development و Testing
  2. DayaPaymentService - API واقعی Daya (برای واریز به حساب کاربران)

🏗️ Architecture

Interface Design

public interface IPaymentGatewayService
{
    // پرداخت (خرید بسته)
    Task<PaymentInitiateResult> InitiatePaymentAsync(
        PaymentRequest request, 
        CancellationToken cancellationToken = default);

    // تایید پرداخت (Callback)
    Task<PaymentVerificationResult> VerifyPaymentAsync(
        string refId, 
        string verificationToken, 
        CancellationToken cancellationToken = default);

    // برداشت/پرداخت به کاربر (Withdrawal)
    Task<PayoutResult> ProcessPayoutAsync(
        PayoutRequest request, 
        CancellationToken cancellationToken = default);
}

DTO Models

PaymentRequest

public class PaymentRequest
{
    public long UserId { get; set; }
    public string Mobile { get; set; }
    public decimal Amount { get; set; }
    public string Description { get; set; }
    public string CallbackUrl { get; set; }
}

PaymentInitiateResult

public class PaymentInitiateResult
{
    public bool IsSuccess { get; set; }
    public string? RefId { get; set; }
    public string? GatewayUrl { get; set; }
    public string? ErrorMessage { get; set; }
}

PaymentVerificationResult

public class PaymentVerificationResult
{
    public bool IsSuccess { get; set; }
    public string RefId { get; set; }
    public string? TrackingCode { get; set; }
    public decimal Amount { get; set; }
    public string? Message { get; set; }
}

PayoutRequest

public class PayoutRequest
{
    public long UserId { get; set; }
    public string Iban { get; set; }
    public decimal Amount { get; set; }
    public string? Description { get; set; }
}

PayoutResult

public class PayoutResult
{
    public bool IsSuccess { get; set; }
    public string? TransactionId { get; set; }
    public string Message { get; set; }
    public DateTime ProcessedAt { get; set; }
}

🔧 Implementation Details

1. MockPaymentGatewayService

Purpose: Development و Testing بدون نیاز به API واقعی

Features:

  • IBAN validation (IR prefix, 26 characters)
  • Amount validation (min 10,000 Toman)
  • Mock RefId generation (MockRef_{timestamp})
  • Simulated network delay (500ms)
  • Comprehensive logging
  • Gateway URL generation (mock://payment)

Usage:

{
  "UseRealPaymentGateway": false
}

Example:

var result = await _paymentGateway.InitiatePaymentAsync(new PaymentRequest
{
    UserId = 123,
    Mobile = "09123456789",
    Amount = 100000,
    Description = "خرید بسته طلایی",
    CallbackUrl = "https://yoursite.com/payment/callback"
});

// result.IsSuccess = true
// result.RefId = "MockRef_1701619200"
// result.GatewayUrl = "mock://payment/MockRef_1701619200"

2. DayaPaymentService

Purpose: یکپارچه‌سازی با API واقعی Daya برای پرداخت و برداشت

Configuration:

{
  "UseRealPaymentGateway": true,
  "PaymentProvider": "Daya",
  "DayaPayment": {
    "BaseUrl": "https://api.daya.ir",
    "ApiKey": "YOUR_DAYA_API_KEY"
  }
}

API Endpoints:

Initiate Payment

POST {BaseUrl}/api/v1/payment/initiate
Content-Type: application/json
X-API-Key: {ApiKey}

{
  "userId": 123,
  "mobile": "09123456789",
  "amount": 100000,
  "description": "خرید بسته طلایی",
  "callbackUrl": "https://yoursite.com/payment/callback"
}

Response:
{
  "success": true,
  "refId": "DAYA123456789",
  "gatewayUrl": "https://gateway.daya.ir/pay/DAYA123456789",
  "errorMessage": null
}

Verify Payment

POST {BaseUrl}/api/v1/payment/verify
Content-Type: application/json
X-API-Key: {ApiKey}

{
  "refId": "DAYA123456789",
  "token": "DAYA123456789"
}

Response:
{
  "success": true,
  "refId": "DAYA123456789",
  "trackingCode": "TRACK987654321",
  "amount": 100000,
  "message": "تراکنش موفق"
}

Process Payout

POST {BaseUrl}/api/v1/payout/process
Content-Type: application/json
X-API-Key: {ApiKey}

{
  "userId": 123,
  "iban": "IR123456789012345678901234",
  "amount": 50000,
  "description": "برداشت کمیسیون"
}

Response:
{
  "success": true,
  "transactionId": "TXN_123456789",
  "message": "پرداخت با موفقیت انجام شد",
  "processedAt": "2024-12-02T10:30:00Z"
}

Error Handling:

try
{
    var response = await _httpClient.PostAsJsonAsync(url, request, cancellationToken);
    
    if (!response.IsSuccessStatusCode)
    {
        _logger.LogError("Daya API error: StatusCode={StatusCode}", response.StatusCode);
        return new PaymentInitiateResult
        {
            IsSuccess = false,
            ErrorMessage = $"خطا در ارتباط با سرویس پرداخت: {response.StatusCode}"
        };
    }
    
    var result = await response.Content.ReadFromJsonAsync<DayaInitiateResponse>(cancellationToken);
    // Process result...
}
catch (Exception ex)
{
    _logger.LogError(ex, "Error in InitiatePaymentAsync");
    return new PaymentInitiateResult
    {
        IsSuccess = false,
        ErrorMessage = "خطای غیرمنتظره در برقراری ارتباط با سرویس پرداخت"
    };
}

3. BankMellatPaymentService

Purpose: یکپارچه‌سازی با IPG بانک ملت (SOAP Web Service)

Configuration:

{
  "UseRealPaymentGateway": true,
  "PaymentProvider": "BankMellat",
  "BankMellat": {
    "ServiceUrl": "https://bpm.shaparak.ir/pgwchannel/services/pgw",
    "TerminalId": "YOUR_TERMINAL_ID",
    "Username": "YOUR_USERNAME",
    "Password": "YOUR_PASSWORD"
  }
}

SOAP Operations:

bpPayRequest (Initiate Payment)

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" 
               xmlns:ns="http://interfaces.core.sw.bps.com/">
    <soap:Body>
        <ns:bpPayRequest>
            <terminalId>{TERMINAL_ID}</terminalId>
            <userName>{USERNAME}</userName>
            <userPassword>{PASSWORD}</userPassword>
            <orderId>{ORDER_ID}</orderId>
            <amount>{AMOUNT_IN_RIALS}</amount>
            <localDate>{yyyyMMdd}</localDate>
            <localTime>{HHmmss}</localTime>
            <additionalData>{DESCRIPTION}</additionalData>
            <callBackUrl>{CALLBACK_URL}</callBackUrl>
            <payerId>0</payerId>
        </ns:bpPayRequest>
    </soap:Body>
</soap:Envelope>

Response:

<soap:Envelope>
    <soap:Body>
        <ns:bpPayRequestResponse>
            <return>{REF_ID}</return>  <!-- Success: positive number, Error: negative number -->
        </ns:bpPayRequestResponse>
    </soap:Body>
</soap:Envelope>

bpVerifyRequest (Verify Payment)

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" 
               xmlns:ns="http://interfaces.core.sw.bps.com/">
    <soap:Body>
        <ns:bpVerifyRequest>
            <terminalId>{TERMINAL_ID}</terminalId>
            <userName>{USERNAME}</userName>
            <userPassword>{PASSWORD}</userPassword>
            <orderId>{ORDER_ID}</orderId>
            <saleOrderId>{ORDER_ID}</saleOrderId>
            <saleReferenceId>{REF_ID}</saleReferenceId>
        </ns:bpVerifyRequest>
    </soap:Body>
</soap:Envelope>

bpSettleRequest (Settle Payment)

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" 
               xmlns:ns="http://interfaces.core.sw.bps.com/">
    <soap:Body>
        <ns:bpSettleRequest>
            <terminalId>{TERMINAL_ID}</terminalId>
            <userName>{USERNAME}</userName>
            <userPassword>{PASSWORD}</userPassword>
            <orderId>{ORDER_ID}</orderId>
            <saleOrderId>{ORDER_ID}</saleOrderId>
            <saleReferenceId>{REF_ID}</saleReferenceId>
        </ns:bpSettleRequest>
    </soap:Body>
</soap:Envelope>

Error Codes:

Code Description (Persian)
0 تراکنش موفق
11 شماره کارت نامعتبر است
12 موجودی کافی نیست
13 رمز نادرست است
14 تعداد دفعات وارد کردن رمز بیش از حد مجاز است
15 کارت نامعتبر است
17 کاربر از انجام تراکنش منصرف شده است
18 تاریخ انقضای کارت گذشته است
21 پذیرنده نامعتبر است
23 خطای امنیتی رخ داده است
24 اطلاعات کاربری پذیرنده نامعتبر است
25 مبلغ نامعتبر است
41 شماره درخواست تکراری است
43 قبلا درخواست Verify داده شده است
51 تراکنش تکراری است

Limitations:

  • ⚠️ Direct payout (ProcessPayoutAsync) not supported by Bank Mellat IPG
  • For withdrawals, use Shaparak Paya or third-party services like Fanapay, IPG.ir

⚙️ Service Registration (ConfigureServices.cs)

// Payment Gateway Service - برای Development از Mock استفاده می‌شود
var useRealPaymentGateway = configuration.GetValue<bool>("UseRealPaymentGateway", false);

if (useRealPaymentGateway)
{
    var paymentProvider = configuration.GetValue<string>("PaymentProvider", "BankMellat");
    
    if (paymentProvider == "Daya")
    {
        services.AddHttpClient<IPaymentGatewayService, DayaPaymentService>()
            .SetHandlerLifetime(TimeSpan.FromMinutes(5));
    }
    else if (paymentProvider == "BankMellat")
    {
        services.AddHttpClient<IPaymentGatewayService, BankMellatPaymentService>()
            .SetHandlerLifetime(TimeSpan.FromMinutes(5));
    }
    else
    {
        throw new InvalidOperationException($"Invalid PaymentProvider: {paymentProvider}");
    }
}
else
{
    // Mock برای Development و Testing
    services.AddScoped<IPaymentGatewayService, MockPaymentGatewayService>();
}

📝 Usage Examples

Purchase Package (InitiatePaymentAsync)

// In Command Handler
public class PurchaseGoldenPackageCommandHandler : IRequestHandler<PurchaseGoldenPackageCommand, long>
{
    private readonly IPaymentGatewayService _paymentGateway;

    public async Task<long> Handle(PurchaseGoldenPackageCommand request, CancellationToken ct)
    {
        // Initiate payment
        var paymentResult = await _paymentGateway.InitiatePaymentAsync(new PaymentRequest
        {
            UserId = request.UserId,
            Mobile = user.Mobile,
            Amount = packagePrice,
            Description = "خرید بسته طلایی",
            CallbackUrl = "https://yoursite.com/payment/callback"
        }, ct);

        if (!paymentResult.IsSuccess)
        {
            throw new InvalidOperationException(paymentResult.ErrorMessage);
        }

        // Create transaction record
        var transaction = new Transaction
        {
            UserId = request.UserId,
            Type = TransactionType.PackagePurchase,
            Amount = packagePrice,
            Status = TransactionStatus.Pending,
            RefId = paymentResult.RefId,
            Description = "خرید بسته طلایی"
        };

        await _context.Transactions.AddAsync(transaction, ct);
        await _context.SaveChangesAsync(ct);

        // Redirect user to gateway
        return transaction.Id; // Return transaction ID for frontend to track
    }
}

Verify Payment (Callback)

public class VerifyGoldenPackagePurchaseCommandHandler : IRequestHandler<VerifyGoldenPackagePurchaseCommand>
{
    private readonly IPaymentGatewayService _paymentGateway;

    public async Task Handle(VerifyGoldenPackagePurchaseCommand request, CancellationToken ct)
    {
        // Verify payment
        var verifyResult = await _paymentGateway.VerifyPaymentAsync(
            request.Authority, 
            request.Authority, 
            ct);

        if (!verifyResult.IsSuccess)
        {
            transaction.Status = TransactionStatus.Failed;
            transaction.ErrorMessage = verifyResult.Message;
            throw new InvalidOperationException(verifyResult.Message);
        }

        // Update transaction
        transaction.Status = TransactionStatus.Completed;
        transaction.CompletedAt = DateTime.UtcNow;

        // Activate club membership
        var clubMembership = new ClubMembership
        {
            UserId = transaction.UserId,
            Status = ClubMembershipStatus.Active,
            StartDate = DateTime.UtcNow,
            EndDate = DateTime.UtcNow.AddMonths(1),
            PurchaseMethod = PackagePurchaseMethod.DirectPurchase
        };

        await _context.ClubMemberships.AddAsync(clubMembership, ct);
        await _context.SaveChangesAsync(ct);
    }
}

Process Withdrawal (ProcessPayoutAsync)

public class ProcessWithdrawalCommandHandler : IRequestHandler<ProcessWithdrawalCommand>
{
    private readonly IPaymentGatewayService _paymentGateway;

    public async Task Handle(ProcessWithdrawalCommand request, CancellationToken ct)
    {
        if (request.IsApproved)
        {
            if (payout.WithdrawalMethod == WithdrawalMethod.Diamond)
            {
                // Credit user wallet
                userWallet.DiscountBalance += payout.TotalAmount;
            }
            else if (payout.WithdrawalMethod == WithdrawalMethod.Cash)
            {
                // Process bank transfer
                var payoutResult = await _paymentGateway.ProcessPayoutAsync(new PayoutRequest
                {
                    UserId = payout.UserId,
                    Iban = payout.Iban,
                    Amount = payout.TotalAmount,
                    Description = $"برداشت کمیسیون هفته {payout.WeekNumber}"
                }, ct);

                if (payoutResult.IsSuccess)
                {
                    payout.Status = CommissionStatus.Withdrawn;
                    payout.CompletedAt = DateTime.UtcNow;
                    payout.TransactionId = payoutResult.TransactionId;
                }
                else
                {
                    payout.Status = CommissionStatus.PaymentFailed;
                    payout.ErrorMessage = payoutResult.Message;
                }
            }

            // Record history
            await _context.CommissionPayoutHistories.AddAsync(new CommissionPayoutHistory
            {
                PayoutId = payout.Id,
                TransactionType = payout.Status == CommissionStatus.Withdrawn 
                    ? TransactionType.Withdrawn 
                    : TransactionType.PaymentFailed,
                Amount = payout.TotalAmount,
                ProcessedBy = _currentUserService.UserId,
                ProcessedAt = DateTime.UtcNow
            }, ct);

            await _context.SaveChangesAsync(ct);
        }
    }
}

🧪 Testing Guide

Unit Testing with Mock

[Fact]
public async Task InitiatePayment_Should_Return_Success_With_Valid_Data()
{
    // Arrange
    var mockLogger = new Mock<ILogger<MockPaymentGatewayService>>();
    var service = new MockPaymentGatewayService(mockLogger.Object);
    
    var request = new PaymentRequest
    {
        UserId = 123,
        Mobile = "09123456789",
        Amount = 100000,
        Description = "Test payment",
        CallbackUrl = "https://test.com/callback"
    };

    // Act
    var result = await service.InitiatePaymentAsync(request);

    // Assert
    Assert.True(result.IsSuccess);
    Assert.NotNull(result.RefId);
    Assert.StartsWith("MockRef_", result.RefId);
    Assert.NotNull(result.GatewayUrl);
}

[Fact]
public async Task ProcessPayout_Should_Fail_With_Invalid_IBAN()
{
    // Arrange
    var mockLogger = new Mock<ILogger<MockPaymentGatewayService>>();
    var service = new MockPaymentGatewayService(mockLogger.Object);
    
    var request = new PayoutRequest
    {
        UserId = 123,
        Iban = "INVALID_IBAN",
        Amount = 50000,
        Description = "Test payout"
    };

    // Act
    var result = await service.ProcessPayoutAsync(request);

    // Assert
    Assert.False(result.IsSuccess);
    Assert.Contains("فرمت شماره شبا نامعتبر", result.Message);
}

Integration Testing

public class PaymentGatewayIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public PaymentGatewayIntegrationTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task PurchaseGoldenPackage_Should_Initiate_Payment()
    {
        // Arrange
        var command = new PurchaseGoldenPackageCommand
        {
            UserId = 123,
            PaymentMethod = PackagePurchaseMethod.DirectPurchase
        };

        // Act
        var response = await _client.PostAsJsonAsync("/api/package/purchase", command);

        // Assert
        response.EnsureSuccessStatusCode();
        var transactionId = await response.Content.ReadFromJsonAsync<long>();
        Assert.True(transactionId > 0);
    }
}

🔒 Security Best Practices

  1. Configuration Security:

    • Store API keys in appsettings.json (excluded from git)
    • Use Azure Key Vault or AWS Secrets Manager in production
    • Never hardcode credentials in code
  2. HTTPS Only:

    • Enforce HTTPS for all payment callbacks
    • Validate SSL certificates
  3. Amount Validation:

    • Validate min/max amounts before API call
    • Verify amounts match on callback
  4. IBAN Validation:

    • Format: IR + 24 digits = 26 characters
    • Validate before payout processing
  5. Idempotency:

    • Use unique OrderId for each payment
    • Store RefId to prevent duplicate processing
  6. Error Handling:

    • Never expose internal errors to users
    • Log detailed errors for debugging
    • Return user-friendly error messages

📊 Monitoring & Logging

// Success
_logger.LogInformation(
    "Payment initiated successfully: UserId={UserId}, Amount={Amount}, RefId={RefId}",
    request.UserId, request.Amount, result.RefId);

// Failure
_logger.LogError(
    "Payment initiation failed: UserId={UserId}, Amount={Amount}, Error={Error}",
    request.UserId, request.Amount, result.ErrorMessage);

// API Error
_logger.LogError(
    "Payment gateway API error: StatusCode={StatusCode}, Response={Response}",
    response.StatusCode, responseContent);

Sentry Integration

try
{
    var result = await _paymentGateway.InitiatePaymentAsync(request, ct);
}
catch (Exception ex)
{
    SentrySdk.CaptureException(ex, scope =>
    {
        scope.SetTag("payment_provider", "Daya");
        scope.SetExtra("user_id", request.UserId);
        scope.SetExtra("amount", request.Amount);
    });
    throw;
}

🚀 Production Deployment Checklist

  • Obtain Daya API credentials (BaseUrl + ApiKey)
  • Obtain Bank Mellat credentials (TerminalId, Username, Password)
  • Test in sandbox environment
  • Update appsettings.Production.json with credentials
  • Set UseRealPaymentGateway = true
  • Configure HTTPS callback URLs
  • Set up monitoring (Sentry/Application Insights)
  • Configure retry policies (Polly)
  • Test full payment flow (Initiate → Callback → Verify)
  • Test withdrawal flow (Request → Approve → Payout)
  • Document production URLs and credentials (secure location)

📞 Support & Troubleshooting

Common Issues

Issue: "Payment gateway API error: 401 Unauthorized"

  • Solution: Check API key in appsettings.json, verify credentials

Issue: "IBAN validation failed"

  • Solution: Ensure IBAN starts with "IR" and is exactly 26 characters

Issue: "Bank Mellat returns negative RefId"

  • Solution: Check error code mapping, verify TerminalId/Username/Password

Issue: "HttpClient timeout"

  • Solution: Increase timeout in ConfigureServices.cs, check network connectivity

📚 References



🆕 فاز ۲ — ZarinPal + PaymentTransaction (بهمن ۱۴۰۴)

۴. ZarinPalPaymentService (فعال)

Purpose: درگاه پرداخت مستقیم زرین‌پال — بدون PYMS واسط

Configuration:

{
  "UseRealPaymentGateway": true,
  "PaymentProvider": "zarinpal",
  "ZarinPal": {
    "MerchantId": "6b098fc8-f490-47a1-aac3-1de1a1b84404",
    "UseSandbox": true
  }
}

API Endpoints:

InitiatePayment (درخواست پرداخت)

POST https://sandbox.zarinpal.com/pg/v4/payment/request.json
{
  "merchant_id": "...",
  "amount": 100000,
  "description": "خرید پکیج طلایی",
  "callback_url": "https://cms.se.kbs1.ir/api/payment/callback",
  "metadata": { "mobile": "09123456789" }
}

Response:
{
  "data": {
    "authority": "A00000000000000000000000000123456",
    "code": 100
  }
}

VerifyPayment (تأیید پرداخت)

POST https://sandbox.zarinpal.com/pg/v4/payment/verify.json
{
  "merchant_id": "...",
  "authority": "A00000000000000000000000000123456",
  "amount": 100000
}

Response:
{
  "data": {
    "code": 100,
    "ref_id": 123456789,
    "card_pan": "6037****1234",
    "card_hash": "...",
    "fee_type": "Merchant",
    "fee": 0
  }
}

Sandbox URL: https://sandbox.zarinpal.com/pg/StartPay/{Authority}
Production URL: https://zarinpal.com/pg/StartPay/{Authority}

PaymentVerificationResult (بروز‌شده):

public class PaymentVerificationResult
{
    public bool IsSuccess { get; set; }
    public string RefId { get; set; }
    public string? TrackingCode { get; set; }
    public decimal Amount { get; set; }
    public string? Message { get; set; }
    public string? CardPan { get; set; }    // 🆕 شماره کارت ماسک‌شده
    public string? CardHash { get; set; }   // 🆕 هش کارت
    public int? VerificationCode { get; set; } // 🆕 کد تأیید زرین‌پال
}

Service Registration (بروز‌شده):

var paymentProvider = configuration.GetValue<string>("PaymentProvider", "zarinpal");

if (paymentProvider?.ToLower() == "zarinpal")
{
    services.AddHttpClient<IPaymentGatewayService, ZarinPalPaymentService>();
}

۵. جدول PaymentTransaction (جداگانه از Transaction)

Purpose: ذخیره جزئیات سطح درگاه — جدا از Transaction entity اصلی

Entity: Domain/Entities/Payment/PaymentTransaction.cs

public class PaymentTransaction : BaseEntity
{
    public string GatewayProvider { get; set; }     // "zarinpal"
    public string MerchantId { get; set; }
    public long Amount { get; set; }
    public string? CallbackUrl { get; set; }
    public string? Description { get; set; }
    public string? Mobile { get; set; }
    public long? UserId { get; set; }
    
    // Request
    public int? RequestStatusCode { get; set; }     // 100 = success
    public string? RequestStatusMessage { get; set; }
    public string? Authority { get; set; }          // ZarinPal authority
    
    // Verification
    public bool PaymentStatus { get; set; }
    public int? VerificationStatusCode { get; set; }
    public string? VerificationStatusMessage { get; set; }
    public string? CardHash { get; set; }
    public string? CardPan { get; set; }            // ماسک‌شده: 6037****1234
    public long? RefId { get; set; }
    
    // Relations
    public long? TransactionId { get; set; }
    public long? OrderId { get; set; }
}

Indexes: Authority, GatewayProvider, UserId, TransactionId, RefId
Migration: AddPaymentTransactionTable

جریان کامل پرداخت:

1. PlaceOrderCommandHandler → InitiatePayment → PaymentTransaction ایجاد (PaymentStatus=false)
2. کاربر → ریدایرکت به ZarinPal
3. ZarinPal → Callback به /api/payment/callback
4. PaymentCallbackController → VerifyPayment → PaymentTransaction بروز (PaymentStatus=true, CardPan, RefId)
5. CompleteOrderPaymentCommandHandler → Transaction + Order + Wallet بروز

مصرف‌کننده‌ها:

سرویس عملیات
PlaceOrderCommandHandler ایجاد PaymentTransaction بعد از InitiatePayment
PaymentCallbackController بروزرسانی بعد از VerifyPayment
TransactionsService ایجاد/بروزرسانی در CustomerPaymentRequest/Verification
PackageService ایجاد/بروزرسانی در CustomerPurchasePackage/Verify

۶. فیکس نمایش وضعیت پرداخت سفارشات تخفیفی

مشکل: DiscountOrderService.GetOrderById/GetUserOrders از Mapster.Adapt<>() استفاده می‌کرد. نام‌ها متفاوت بودند:

  • Domain: PaymentStatus (enum: Success=0, Reject=1, Pending=2)
  • Proto: payment_completed (bool)

Mapster نمی‌تونست enum رو به bool مپ کنه → همیشه false (در انتظار پرداخت).

رفع: جایگزینی Mapster با مپینگ دستی:

PaymentCompleted = result.PaymentStatus == DomainEnums.PaymentStatus.Success

۷. فیکس DeliveryStatus بعد از پرداخت

مشکل: فروشگاه تخفیفی بعد از پرداخت موفق، DeliveryStatus = InTransit (ارسال شده) ست می‌کرد. ولی فروشگاه عادی Pending نگه می‌داشت.

رفع: هر دو handler (CompleteOrderPaymentCommandHandler و PlaceOrderCommandHandler) به DeliveryStatus.Pending تغییر کردند — ادمین باید وضعیت پستی رو مشخص کنه.


۸. فیکس ZarinPal Callback URL (اسفند ۱۴۰۴)

مشکل: PurchasePackageCommandHandler از yourdomain.com به صورت hardcode استفاده می‌کرد.

رفع: خواندن از IConfiguration:

var cmsBaseUrl = _configuration["CmsBaseUrl"];
var frontOfficeBaseUrl = _configuration["FrontOfficeBaseUrl"];

appsettings.json (Production):

{
  "CmsBaseUrl": "https://cms.kbs1.ir",
  "FrontOfficeBaseUrl": "https://foursat.kbs1.ir"
}

appsettings.json (Staging):

{
  "CmsBaseUrl": "https://cms.se.kbs1.ir",
  "FrontOfficeBaseUrl": "https://foursat.se.kbs1.ir"
}

۹. اجبار تخفیف ۱۰۰٪ (اسفند ۱۴۰۴)

تغییر بیزینسی: کاربر دیگه نمی‌تونه درصد تخفیف رو انتخاب کنه — همیشه حداکثر تخفیف (MaxDiscountPercent) اعمال می‌شه.

تغییرات CMS (بکند):

  • PlaceOrderCommandHandler: همیشه MaxDiscountPercent محصول استفاده می‌شه
  • فیلد requested_discount_percent از request نادیده گرفته می‌شه

تغییرات FrontOffice:

  • حذف MudSlider و MudNumericField از Checkout.razor
  • حذف کامل بخش نمایش موجودی تخفیفی
  • Badge محصولات: نمایش درصد واقعی (مثلاً "۳۰٪ تخفیف") بجای "۱۰۰٪ تخفیفی"

۱۰. نمایش مالیات (VAT) در Checkout (اسفند ۱۴۰۴)

جدول خلاصه مالی کامل اضافه شد:

فیلد توضیح
جمع کل قبل از تخفیف
تخفیف مجموع DiscountAmount
مبلغ پس از تخفیف بعد از کسر تخفیف
مالیات ۹٪ VatCalculator روی مبلغ درگاه
مبلغ قابل پرداخت مبلغ درگاه + مالیات

۱۱. سرویس Expire سفارشات معلق (اسفند ۱۴۰۴)

فایل: ExpirePendingOrdersService.csBackgroundService

تنظیم مقدار
بررسی هر ۵ دقیقه
انقضا بعد از ۳۰ دقیقه PaymentStatus=Pending
عملیات PaymentStatus=Reject, DeliveryStatus=Cancelled, آزادسازی رزرو انبار
ساعت DateTime.Now (نه UtcNow — DB از ساعت محلی استفاده می‌کنه)

۱۲. فیکس DeliveryStatus مپینگ (اسفند ۱۴۰۴)

مشکل: Domain DeliveryStatus.Pending(1) مستقیم cast به Proto PROCESSING(1) می‌شد → سفارشات failed نشون می‌دادن "در حال پردازش".

راه‌حل: MapDeliveryStatus() و MapPaymentStatus() اضافه شدن:

Domain Proto
PaymentStatus.Success(0) COMPLETED(1)
PaymentStatus.Reject(1) FAILED(2)
PaymentStatus.Pending(2) PENDING(0)
DeliveryStatus.None(0) PENDING(0)
DeliveryStatus.Pending(1) PROCESSING(1)
DeliveryStatus.InTransit(2) SHIPPED(2)
DeliveryStatus.Delivered(3) DELIVERED(3)
DeliveryStatus.Returned/Cancelled(4,5) CANCELLED(4)
  • وقتی پرداخت ناموفقه: order.DeliveryStatus = DeliveryStatus.Cancelled

۱۳. استقرار Production (اسفند ۱۴۰۴)

Merge از kub-stage به production — هر ۳ ریپو:

  • CMS: ۳ conflict حل شد (workflow, Dockerfile, appsettings)
  • FrontOffice: ۱ conflict (workflow)
  • BackOffice: ۲ conflict (workflow, Dockerfile)

Migration دیتابیس Production: ۷ migration اعمال شد:

  1. AddDiscountProductImages
  2. AddInventorySystem
  3. u19 + u20
  4. AddBlogAndContentEntities
  5. RemoveImagePathMaxLength
  6. AddPaymentTransactionTable

ZarinPal در Production: UseSandbox: true → "درگاه فعال نمیباشد" (عمدی)


Last Updated: February 17, 2026 Version: 3.0 Proto Version: 0.0.179 Status: ZarinPal Active (Sandbox) + PaymentTransaction + ExpireOrders + Production Deployed