# 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 ```csharp public interface IPaymentGatewayService { // پرداخت (خرید بسته) Task InitiatePaymentAsync( PaymentRequest request, CancellationToken cancellationToken = default); // تایید پرداخت (Callback) Task VerifyPaymentAsync( string refId, string verificationToken, CancellationToken cancellationToken = default); // برداشت/پرداخت به کاربر (Withdrawal) Task ProcessPayoutAsync( PayoutRequest request, CancellationToken cancellationToken = default); } ``` ### DTO Models #### PaymentRequest ```csharp 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 ```csharp public class PaymentInitiateResult { public bool IsSuccess { get; set; } public string? RefId { get; set; } public string? GatewayUrl { get; set; } public string? ErrorMessage { get; set; } } ``` #### PaymentVerificationResult ```csharp 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 ```csharp public class PayoutRequest { public long UserId { get; set; } public string Iban { get; set; } public decimal Amount { get; set; } public string? Description { get; set; } } ``` #### PayoutResult ```csharp 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**: ```json { "UseRealPaymentGateway": false } ``` **Example**: ```csharp 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**: ```json { "UseRealPaymentGateway": true, "PaymentProvider": "Daya", "DayaPayment": { "BaseUrl": "https://api.daya.ir", "ApiKey": "YOUR_DAYA_API_KEY" } } ``` **API Endpoints**: #### Initiate Payment ```http 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 ```http 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 ```http 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**: ```csharp 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(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**: ```json { "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) ```xml {TERMINAL_ID} {USERNAME} {PASSWORD} {ORDER_ID} {AMOUNT_IN_RIALS} {yyyyMMdd} {HHmmss} {DESCRIPTION} {CALLBACK_URL} 0 ``` **Response**: ```xml {REF_ID} ``` #### bpVerifyRequest (Verify Payment) ```xml {TERMINAL_ID} {USERNAME} {PASSWORD} {ORDER_ID} {ORDER_ID} {REF_ID} ``` #### bpSettleRequest (Settle Payment) ```xml {TERMINAL_ID} {USERNAME} {PASSWORD} {ORDER_ID} {ORDER_ID} {REF_ID} ``` **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) ```csharp // Payment Gateway Service - برای Development از Mock استفاده می‌شود var useRealPaymentGateway = configuration.GetValue("UseRealPaymentGateway", false); if (useRealPaymentGateway) { var paymentProvider = configuration.GetValue("PaymentProvider", "BankMellat"); if (paymentProvider == "Daya") { services.AddHttpClient() .SetHandlerLifetime(TimeSpan.FromMinutes(5)); } else if (paymentProvider == "BankMellat") { services.AddHttpClient() .SetHandlerLifetime(TimeSpan.FromMinutes(5)); } else { throw new InvalidOperationException($"Invalid PaymentProvider: {paymentProvider}"); } } else { // Mock برای Development و Testing services.AddScoped(); } ``` --- ## 📝 Usage Examples ### Purchase Package (InitiatePaymentAsync) ```csharp // In Command Handler public class PurchaseGoldenPackageCommandHandler : IRequestHandler { private readonly IPaymentGatewayService _paymentGateway; public async Task 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) ```csharp public class VerifyGoldenPackagePurchaseCommandHandler : IRequestHandler { 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) ```csharp public class ProcessWithdrawalCommandHandler : IRequestHandler { 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 ```csharp [Fact] public async Task InitiatePayment_Should_Return_Success_With_Valid_Data() { // Arrange var mockLogger = new Mock>(); 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>(); 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 ```csharp public class PaymentGatewayIntegrationTests : IClassFixture> { private readonly HttpClient _client; public PaymentGatewayIntegrationTests(WebApplicationFactory 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(); 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 ### Recommended Logs ```csharp // 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 ```csharp 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 - [Daya API Documentation](https://api.daya.ir/docs) (placeholder) - [Bank Mellat IPG Guide](https://bpm.shaparak.ir/) (official) - [Shaparak Paya Documentation](https://www.shaparak.ir/) - [ISO 8601 Week Numbering](https://en.wikipedia.org/wiki/ISO_8601) --- --- ## 🆕 فاز ۲ — ZarinPal + PaymentTransaction (بهمن ۱۴۰۴) ### ۴. ZarinPalPaymentService (فعال) **Purpose**: درگاه پرداخت مستقیم زرین‌پال — بدون PYMS واسط **Configuration**: ```json { "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 (بروز‌شده)**: ```csharp 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 (بروز‌شده)**: ```csharp var paymentProvider = configuration.GetValue("PaymentProvider", "zarinpal"); if (paymentProvider?.ToLower() == "zarinpal") { services.AddHttpClient(); } ``` --- ### ۵. جدول PaymentTransaction (جداگانه از Transaction) **Purpose**: ذخیره جزئیات سطح درگاه — جدا از Transaction entity اصلی **Entity**: `Domain/Entities/Payment/PaymentTransaction.cs` ```csharp 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 با مپینگ دستی: ```csharp PaymentCompleted = result.PaymentStatus == DomainEnums.PaymentStatus.Success ``` ### ۷. فیکس DeliveryStatus بعد از پرداخت **مشکل**: فروشگاه تخفیفی بعد از پرداخت موفق، `DeliveryStatus = InTransit` (ارسال شده) ست می‌کرد. ولی فروشگاه عادی `Pending` نگه می‌داشت. **رفع**: هر دو handler (`CompleteOrderPaymentCommandHandler` و `PlaceOrderCommandHandler`) به `DeliveryStatus.Pending` تغییر کردند — ادمین باید وضعیت پستی رو مشخص کنه. --- ### ۸. فیکس ZarinPal Callback URL (اسفند ۱۴۰۴) **مشکل**: `PurchasePackageCommandHandler` از `yourdomain.com` به صورت hardcode استفاده می‌کرد. **رفع**: خواندن از `IConfiguration`: ```csharp var cmsBaseUrl = _configuration["CmsBaseUrl"]; var frontOfficeBaseUrl = _configuration["FrontOfficeBaseUrl"]; ``` **appsettings.json (Production)**: ```json { "CmsBaseUrl": "https://cms.kbs1.ir", "FrontOfficeBaseUrl": "https://foursat.kbs1.ir" } ``` **appsettings.json (Staging)**: ```json { "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.cs` — `BackgroundService` | تنظیم | مقدار | |-------|-------| | بررسی | هر ۵ دقیقه | | انقضا | بعد از ۳۰ دقیقه `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