e3850f9dd8
- CHANGELOG: Phase 11 (11a-11f) — ZarinPal verify fix, تومان/ریال مدل, صفحه موفقیت, حذف ×۱۰ دوبار, callback URL امنیت - BUSINESS-02: تصحیح مدل ارزی (DB=تومان نه ریال), ZarinPal verify fix, جدول callback URL امنیت - TECH-01: اضافه CmsBaseUrl/FrontOfficeBaseUrl به appsettings, توضیح امنیت Open Redirect - TECH-02: اضافه PaymentCallback.razor, وضعیتهای جدید - ROADMAP: بروزرسانی Payment 97→99%, اضافه فاز ۱۱ به DONE list
285 lines
13 KiB
Markdown
285 lines
13 KiB
Markdown
# 💰 سیستم مالی، پرداخت و درگاهها
|
||
|
||
> **منابع ادغامشده:** `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) اعمال شد.
|