# 💰 سیستم مالی، پرداخت و درگاه‌ها > **منابع ادغام‌شده:** `payment-gateway.md`, `payment-architecture-pyms.md`, `daya-loan-integration.md`, `manual-payment-system.md`, `discount-shop-business.md` > **آخرین بروزرسانی:** اسفند ۱۴۰۴ (بروزرسانی: تصحیح مدل تومان/ریال + فیکس ZarinPal Verify + امنیت Callback URL) --- ## ۱. معماری کلی مالی ```mermaid flowchart TD subgraph GATEWAYS["درگاه‌ها"] ZP["ZarinPal\nIPG"] DL["Daya Loan\nAPI"] MP["Manual Pay\nCard2Card"] DW["Discount Wallet\nInternal"] end ZP --> PYMS["PYMS — Payment Service\ngRPC ↔ CMS ↔ FrontOffice/BackOffice"] DL --> PYMS MP --> PYMS DW --> PYMS PYMS --> WALLETS subgraph WALLETS["3 Wallet System"] W1["💰 Balance\nکیف پول اصلی"] W2["🌟 NetworkBalance\nپاداش تیمی"] W3["🏷️ DiscountBalance\nکیف پول اعتباری"] end ``` --- ## ۲. درگاه ZarinPal (IPG) > **✅ محل استفاده:** ZarinPal در چهار جا فعال است: > 1. **فروشگاه اعتباری** — باقیمانده بعد از کسر DiscountBalance (اگر > 0) > 2. **شارژ کیف‌پول اعتباری** — واریز مستقیم از پروفایل کاربر > 3. **خرید پکیج** — پرداخت مستقیم با کارت بانکی (هر دو شاخه فعال) > 4. **شارژ کیف‌پول جادویی** — واریز با ضریب ×2.5 (فقط در حالت Magic) > > ❌ **فروشگاه عادی (Regular Store) از ZarinPal استفاده نمی‌کند** — فقط کسر از Balance کیف‌پول ### ۲.۱ فلوی پرداخت ```mermaid flowchart TD A["کاربر → انتخاب محصول\nدرخواست پرداخت"] --> B["CMS → CreatePaymentRequest\ngRPC to PYMS"] B --> C["PYMS → ZarinPal API\nمبلغ ×۱۰ (تومان→ریال)\nدریافت Authority"] C --> D["Redirect کاربر\nصفحه پرداخت ZarinPal"] D --> E["بازگشت با Authority\nCMS VerifyPayment (مبلغ ×۱۰)"] E -->|موفق| F["✅ ثبت سفارش\n+ شارژ کیف‌پول"] E -->|ناموفق| G["❌ نمایش پیام خطا"] ``` > **✅ فیکس ZarinPal Verify (اسفند ۱۴۰۴ — `721661a`):** > - **باگ:** `VerifyPaymentAsync(authority)` با ۲ آرگومان → amount=0 → ZarinPal Code=-1 > - **فیکس:** lookup `PaymentTransaction.Amount` از DB + استفاده از overload ۳ آرگومانه `VerifyPaymentAsync(authority, orderId, amount)` > - `IPaymentGatewayService` — default impl ۳ آرگومانه اضافه شد > - ۷ فایل تغییر: PackageService, TransactionsService, VerifyDiscountWalletChargeCommandHandler, VerifyPackagePurchaseCommandHandler, IPaymentGatewayService, MockPaymentGatewayService, DayaPaymentService ### ۲.۲ تنظیمات ZarinPal | پارامتر | مقدار | |----------|-------| | `MerchantId` | `4225d555-5fa9-4df0-9b61-1ce152cbbba8` | | `CallbackUrl` | از `appsettings.json` خوانده می‌شود (نه از ورودی کاربر) | | `Sandbox` | `true` (staging) / `false` (production) | | `Currency` | DB: تومان — ZarinPal: ریال (×۱۰ هنگام ارسال) | > **✅ مدل ارزی (تصحیح اسفند ۱۴۰۴):** > - **DB:** `Package.Price` و همه مبالغ مالی به **تومان** ذخیره می‌شوند > - **CMS → ZarinPal:** `ZarinPalPaymentService` مبلغ را ×۱۰ می‌کند (`amountInRials = amount * 10`) > - **FrontOffice UI:** مبالغ مستقیم به تومان نمایش داده می‌شوند (بدون تبدیل) > - **FrontOffice → CMS:** مبالغ به تومان ارسال می‌شوند (FO هیچ تبدیلی انجام نمی‌دهد) > - **باگ قبلی ۱:** FO مبلغ تومان را ×۱۰ تبدیل می‌کرد + CMS/ZarinPal دوباره ×۱۰ → مبلغ ۱۰۰ برابر (فیکس: `2f9ef15`) > - **باگ قبلی ۲:** `FormattedPrice = Price / 10` اشتباه بود — Price از قبل تومان است (فیکس: `3c1a8ff` اصلاح شد) > **✅ امنیت Callback URL (اسفند ۱۴۰۴):** > - هیچ callback URL از ورودی کاربر خوانده نمی‌شود — همه از `appsettings.json` خوانده می‌شوند > - `PackageService` و `TransactionsService`: از `FrontOfficeBaseUrl` config > - `MagicWallet` و `DiscountWallet`: از `CmsBaseUrl` config > - جلوگیری از حمله Open Redirect > **تنظیمات محیطی:** > - `appsettings.json` + `appsettings.Staging.json`: `UseSandbox: true` (تست) > - `appsettings.Production.json`: `UseSandbox: false` (واقعی) > - Production URL: `cms.kbs1.ir` | FrontOffice GwUrl: `cms.kbs2.ir` --- ## ۳. سیستم وام دایا (DayaLoan) ### ۳.۱ معماری ```mermaid flowchart TD A["Hangfire Recurring Job\nهر ۲۰ دقیقه — */20 * * * *"] --> B["DayaLoanProcessorJob.Execute"] B --> C["بررسی LoanRequests\nStatus = Pending"] C --> D["برای هر درخواست:"] D --> E["ارسال به DayaLoan API\nAutomaticRetry Attempts=3"] E -->|تأیید| F["✅ Balance += 56M\nDiscountBalance += 112M\n+ ثبت Transaction + Log"] E -->|رد| G["❌ Status = Rejected\n+ ارسال SMS"] ``` ### ۳.۲ Mock Mode ```csharp // appsettings.json "DayaLoan": { "UseMock": true, // staging "BaseUrl": "https://api.dayaloan.ir", "ApiKey": "***", "AutoApproveInMock": true } ``` ### ۳.۳ مقادیر | آیتم | مقدار | |------|-------| | مبلغ وام (DayaLoanAmount) | ۵۶,۰۰۰,۰۰۰ ریال | | شارژ Balance | ۵۶,۰۰۰,۰۰۰ ریال | | شارژ DiscountBalance | ۱۱۲,۰۰۰,۰۰۰ ریال (دو برابر — DayaLoanAmount × 2) | | مجموع شارژ | ۱۶۸,۰۰۰,۰۰۰ ریال | | NetworkBalance | شارژ نمی‌شود | | بازپرداخت | طبق شرایط دایا | --- ## ۴. پرداخت دستی (کارت‌به‌کارت) > ⚠️ **وضعیت: طراحی‌شده — پیاده‌سازی نشده** ```mermaid flowchart TD A["کاربر → انتخاب کارت‌به‌کارت"] --> B["نمایش شماره‌کارت مقصد\n+ مبلغ"] B --> C["کاربر → واریز\n+ آپلود تصویر رسید"] C --> D["ادمین BackOffice\nمشاهده لیست درخواست‌ها"] D --> E{"تأیید / رد؟"} E -->|تأیید| F["✅ شارژ خودکار کیف‌پول"] E -->|رد| G["❌ اطلاع‌رسانی به کاربر"] ``` **موجودیت‌های مورد نیاز:** - `ManualPaymentRequest` (UserId, Amount, ReceiptImage, Status, AdminNote) - `ManualPaymentStatus` enum: Pending, Approved, Rejected --- ## ۵. پرداخت ترکیبی فروشگاه اعتباری (Hybrid Payment) ### ۵.۱ فرمول ``` قیمت محصول = 1,000,000 ریال MaxDiscountPercent محصول = 40% (هر محصول درصد تخفیف مخصوص خود را دارد) سهم تخفیف = قیمت × MaxDiscountPercent% = 400,000 ⚠️ اگر DiscountBalance < سهم تخفیف → خطا: «موجودی کیف پول اعتباری کافی نیست» (دیگر MIN استفاده نمی‌شود — کاربر باید موجودی کافی داشته باشد) باقیمانده → ZarinPal IPG = 1,000,000 - 400,000 = 600,000 ───────── مجموع = 1,000,000 ℹ️ تخفیف ثابت ۳۰% نیست — فیلد Product.MaxDiscountPercent (0-100) تعیین‌کننده است. ``` ### ۵.۲ فلوی خرید فروشگاه اعتباری ```mermaid flowchart TD A["کاربر عضو باشگاه\nمشاهده محصول"] --> B["قیمت تخفیف‌خورده نمایش داده می‌شود"] B --> C["افزودن به سبد\nمحاسبه MaxDiscount% هر محصول"] C --> D["سهم تخفیف = قیمت × MaxDiscount%"] D --> V{"DiscountBalance >= سهم تخفیف?"} V -->|خیر| X["❌ خطا: موجودی کیف پول اعتباری کافی نیست"] V -->|بله| E["باقیمانده = مجموع - سهم تخفیف"] E --> F{"باقیمانده > 0?"} F -->|بله| G["کسر DiscountBalance\n+ Redirect → ZarinPal IPG\nباقیمانده + 10% VAT"] F -->|خیر| H["فقط کسر از DiscountBalance\nبدون درگاه → ثبت مستقیم"] G --> LOG["ثبت UserWalletChangeLog"] H --> LOG ``` ### ۵.۳ دسترسی فروشگاه اعتباری | شرط | نتیجه | |------|--------| | `IsClubMember = true` | دسترسی به Discount Store | | `IsClubMember = false` | فقط Regular Store | | `DiscountBalance >= سهم تخفیف` | خرید مجاز | | `DiscountBalance < سهم تخفیف` | ❌ خطا: موجودی کیف پول اعتباری کافی نیست | ### ۵.۴ UserWalletChangeLog (اسفند ۱۴۰۴ — فیکس) > **باگ:** هنگام خرید از فروشگاه اعتباری، `DiscountBalance` در دیتابیس کم می‌شد ولی هیچ > `UserWalletChangeLog` ثبت نمی‌شد → کاربر در تاریخچه کیف‌پول چیزی نمی‌دید. فیکس در ۳ هندلر: | هندلر | سناریو | فیکس | |--------|---------|------| | `PlaceOrderCommandHandler` | پرداخت کامل با DiscountBalance (بدون درگاه) | ✅ ثبت log با `ChangeDiscountValue = -amount` | | `CompleteOrderPaymentCommandHandler` | پرداخت ترکیبی (درگاه + DiscountBalance) | ✅ ثبت log بعد از verify موفق درگاه | | `VerifyDiscountWalletChargeCommandHandler` | شارژ کیف‌پول اعتباری | ✅ ثبت log با `ChangeDiscountValue = +amount` | --- ## ۶. PYMS — سرویس پرداخت مرکزی ### ۶.۱ gRPC Services ```protobuf service PaymentService { rpc CreatePayment (CreatePaymentRequest) returns (CreatePaymentResponse); rpc VerifyPayment (VerifyPaymentRequest) returns (VerifyPaymentResponse); rpc GetPaymentStatus (GetPaymentStatusRequest) returns (PaymentStatusResponse); rpc RefundPayment (RefundPaymentRequest) returns (RefundPaymentResponse); } ``` ### ۶.۲ Transaction Types | نوع | کد | توضیح | |-----|-----|--------| | PackagePurchase | 1 | خرید پکیج طلایی | | StorePurchase | 2 | خرید از فروشگاه | | DiscountStorePurchase | 3 | خرید از فروشگاه اعتباری | | CommissionPayout | 4 | واریز کمیسیون هفتگی | | WalletCharge | 5 | شارژ مستقیم کیف‌پول | | ActivationFee | 6 | هزینه فعالسازی | | DayaLoanCharge | 7 | شارژ از وام دایا | | MagicWalletDeposit | 14 | واریز به کیف‌پول جادویی | | MagicWalletBonus | 15 | بونوس ضریب ×2.5 کیف‌پول جادویی | --- ## ۷. مالیات و VAT ``` هر دو فروشگاه از نرخ 10% استفاده می‌کنند: Regular Store → const vatRate = 0.10m (hardcoded در SubmitShopBuyOrderCommandHandler) Discount Store → VatCalculator.VAT_RATE = 0.10m SystemConstants.ShopVAT = 0.1 (10%) قیمت نمایشی = قیمت پایه × (1 + 0.10) در صورتحساب: قیمت پایه + مالیات جداگانه نمایش داده می‌شود ``` --- ## ۸. خلاصه وضعیت پیاده‌سازی | ماژول | وضعیت | یادداشت | |-------|--------|---------| | ZarinPal IPG | ✅ کامل | **Production فعال** — MerchantId: `4225d555...` | | ZarinPal Verify | ✅ فیکس شده | رفع amount=0 با overload ۳ آرگومانه (`721661a`) | | Callback URL امنیت | ✅ فیکس شده | همه از config خوانده می‌شوند — جلوگیری از Open Redirect | | وام دایا | ✅ کامل | Mock mode فعال در staging | | پرداخت ترکیبی | ✅ کامل | Discount + IPG | | Pool هفتگی | ✅ کامل | SP + Hangfire | | WalletChangeLog | ✅ فیکس شده | لاگ تغییرات کیف‌پول در ۳ هندلر اضافه شد | | Validation کیف‌پول اعتباری | ✅ فیکس شده | ارور اگر موجودی کافی نباشد | | Toman/Rial مدل | ✅ تصحیح شده | DB=تومان، فقط ZarinPal ریال (×۱۰) — FO بدون تبدیل | | صفحه موفقیت پرداخت | ✅ بهبود | TransactionId + موجودی واقعی + دکمه بازگشت | | PackagePurchaseDialog | ✅ کامل | دیالوگ داینامیک کاشی‌ای با انتخاب روش پرداخت | | پرداخت دستی | ⬜ طراحی | نیاز به تصمیم مدیریت | | Refund | ⬜ طراحی | فقط در PYMS تعریف‌شده | | کیف‌پول جادویی (Magic) | ✅ کامل | فاز 1-6 پیاده‌سازی شده — Production فعال | ### ۸.۱ نام‌گذاری استاندارد کیف‌پول‌ها (اسفند ۱۴۰۴) | فیلد دیتابیس | نام قدیم (UI) | نام جدید (UI) | |-------------|--------------|---------------| | `Balance` | عادی / اعتباری / نقدی | **کیف پول اصلی** | | `DiscountBalance` | تخفیفی / تخفیف | **کیف پول اعتباری** | | `NetworkBalance` | شبکه / طلایی / پورسانت | **پاداش تیمی** | > تغییرات UI در ۱۲ فایل (FrontOffice: 5, BackOffice: 7) اعمال شد.