docs: add Magic Wallet spec & implementation plan
- New folder: roadmap/ for upcoming features - MAGIC-WALLET-SPEC: full business rules, state machine, x2.5 multiplier, 100M deposit cap, API flow, data model changes, transaction logging - MAGIC-WALLET-PLAN: 6-phase implementation, checklist, time estimates - Note: ChargeDiscountWallet completion is independent (not Magic Wallet) - Note: WalletChangeLog is mandatory (not optional) - Updated master index with roadmap section
This commit is contained in:
@@ -0,0 +1,397 @@
|
||||
# 🪄 کیفپول جادویی (Magic Wallet)
|
||||
|
||||
> **وضعیت:** طراحی — آماده پیادهسازی
|
||||
> **تاریخ:** اسفند ۱۴۰۴
|
||||
> **وابستگی:** خرید پکیج پایه، فروشگاه عادی، سیستم کمیسیون
|
||||
|
||||
---
|
||||
|
||||
## ۱. خلاصه بیزینسی
|
||||
|
||||
کاربر بعد از خرید پکیج پایه (۵۶M) و خرج کردن کامل Balance از فروشگاه عادی، وارد **حالت جادویی** میشه. در این حالت میتونه کیفپولش رو از درگاه شارژ کنه و **۲.۵ برابر** اعتبار بگیره — بدون هیچ کمیسیون یا پورسانتی.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> NewUser: ثبتنام
|
||||
NewUser --> Normal: خرید پکیج 56M\n(IPG یا دایا)
|
||||
Normal --> Magic: Balance = 0\n(همه رو خرج کرد)
|
||||
Magic --> PostMagic: Balance = 0\n(250M رو خرج کرد)
|
||||
PostMagic --> Normal: خرید مجدد پکیج 56M\n(فقط IPG — دایا ❌)
|
||||
Normal --> Magic: Balance = 0\n(دوباره خرج کرد)
|
||||
|
||||
state Normal {
|
||||
[*] --> خرید_عادی
|
||||
خرید_عادی: Balance -= مبلغ خرید
|
||||
خرید_عادی --> کمیسیون_فعال
|
||||
کمیسیون_فعال: ✅ پورسانت + 25.2M Pool
|
||||
}
|
||||
|
||||
state Magic {
|
||||
[*] --> شارژ_جادویی
|
||||
شارژ_جادویی: واریز × 2.5 = اعتبار
|
||||
شارژ_جادویی --> خرید_با_اعتبار
|
||||
خرید_با_اعتبار: Balance -= مبلغ خرید
|
||||
خرید_با_اعتبار --> بدون_کمیسیون
|
||||
بدون_کمیسیون: ❌ هیچ پورسانتی آزاد نمیشه
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ۲. چرخه کامل کیفپول
|
||||
|
||||
### فاز ۱ — خرید پکیج (Normal Mode)
|
||||
|
||||
| مرحله | عملیات | نتیجه |
|
||||
|--------|---------|--------|
|
||||
| ۱ | کاربر پکیج ۵۶M میخره (IPG یا دایا) | `Balance += 56M`, `DiscountBalance += 112M` |
|
||||
| ۲ | فعالسازی باشگاه | `25.2M → Pool`, فیچرها باز میشه |
|
||||
| ۳ | کمیسیون هفتگی | `NetworkBalance += سهم` ✅ |
|
||||
| ۴ | خرید از فروشگاه عادی | `Balance -= مبلغ` |
|
||||
| ۵ | Balance = 0 | **→ ورود به Magic Mode** |
|
||||
|
||||
### فاز ۲ — کیفپول جادویی (Magic Mode)
|
||||
|
||||
| مرحله | عملیات | نتیجه |
|
||||
|--------|---------|--------|
|
||||
| ۱ | کاربر از صفحه شارژ جادویی مبلغ واریز میکنه | درگاه ZarinPal |
|
||||
| ۲ | تراکنش واریز ثبت میشه (مبلغ اصلی) | `Transaction(MagicDeposit, 10M)` |
|
||||
| ۳ | اعتبار ×2.5 به Balance اضافه میشه | `Balance += 25M` |
|
||||
| ۴ | تراکنش بونوس ثبت میشه | `Transaction(MagicBonus, 15M)` |
|
||||
| ۵ | لاگ کیفپول ثبت میشه | `WalletChangeLog` ✅ (اجباری) |
|
||||
| ۶ | MagicTotalDeposited += مبلغ واریزی | ترک سقف |
|
||||
| ۷ | خرید از فروشگاه عادی | `Balance -= مبلغ` |
|
||||
| ۸ | Balance = 0 و سقف پر شده | **→ خروج از Magic Mode** |
|
||||
|
||||
### فاز ۳ — بازگشت (Post-Magic)
|
||||
|
||||
| مرحله | عملیات | نتیجه |
|
||||
|--------|---------|--------|
|
||||
| ۱ | کیفپول جادویی تمام شد | `WalletMode = Normal` |
|
||||
| ۲ | برای ادامه باید دوباره پکیج ۵۶M بخره | **فقط IPG** (دایا ❌) |
|
||||
| ۳ | خرید مجدد پکیج | `Balance += 56M`, `DiscountBalance += 112M` |
|
||||
| ۴ | همه آپشنها دوباره فعال | کمیسیون ✅, Pool ✅ |
|
||||
| ۵ | دوباره Balance = 0 بشه | **→ Magic Mode مجدد** |
|
||||
|
||||
---
|
||||
|
||||
## ۳. قوانین Magic Mode
|
||||
|
||||
### ۳.۱ ضریب و سقف
|
||||
|
||||
| پارامتر | مقدار | ثابت پیشنهادی |
|
||||
|----------|-------|---------------|
|
||||
| ضریب شارژ | **×2.5** | `MagicWalletMultiplier = 2.5m` |
|
||||
| سقف ورودی | **100M تومان** (1B ریال) | `MagicWalletMaxDeposit = 1_000_000_000` |
|
||||
| سقف خروجی | **250M تومان** (2.5B ریال) | `MagicWalletMaxCredit = 2_500_000_000` |
|
||||
| سود کاربر | **150%** | — |
|
||||
|
||||
### ۳.۲ مثال عددی
|
||||
|
||||
```
|
||||
واریز: 10,000,000 تومان (100M ریال)
|
||||
├── تراکنش واریز: 10,000,000 تومان (Transaction: MagicDeposit)
|
||||
├── بونوس داخلی: 15,000,000 تومان (Transaction: MagicBonus)
|
||||
├── اعتبار نهایی: 25,000,000 تومان (Balance += 250M ریال)
|
||||
└── WalletChangeLog: BalanceChange = +250,000,000 ریال ✅
|
||||
|
||||
سقف:
|
||||
├── مجموع واریزی: MagicTotalDeposited += 100,000,000 ریال
|
||||
├── مجموع اعتبار: MagicTotalCredited += 250,000,000 ریال
|
||||
└── باقیمانده سقف: MaxDeposit - TotalDeposited
|
||||
```
|
||||
|
||||
### ۳.۳ چه چیزهایی غیرفعال میشه
|
||||
|
||||
| قابلیت | Normal Mode | Magic Mode |
|
||||
|--------|-------------|------------|
|
||||
| خرید از فروشگاه عادی | ✅ | ✅ |
|
||||
| خرید از فروشگاه تخفیفی | ✅ | ✅ (DiscountBalance قبلی) |
|
||||
| کمیسیون هفتگی | ✅ | ❌ |
|
||||
| پورسانت ۲۵.۲M | ✅ | ❌ |
|
||||
| شارژ جادویی ×2.5 | ❌ | ✅ |
|
||||
| خرید مجدد پکیج | ✅ | ❌ |
|
||||
|
||||
---
|
||||
|
||||
## ۴. شرایط ورود و خروج
|
||||
|
||||
### ۴.۱ ورود به Magic Mode
|
||||
|
||||
```
|
||||
شرطها (همه باید true باشن):
|
||||
├── wallet.Balance == 0 (کیفپول خالی شد)
|
||||
├── user.PackagePurchaseMethod != None (قبلاً پکیج خریده)
|
||||
├── wallet.WalletMode == Normal (الان عادیه)
|
||||
└── user.ClubMembership.IsActive == true (باشگاه فعاله)
|
||||
|
||||
نتیجه:
|
||||
├── wallet.WalletMode = Magic
|
||||
├── wallet.MagicActivatedAt = DateTime.UtcNow
|
||||
├── wallet.MagicTotalDeposited = 0
|
||||
└── wallet.MagicTotalCredited = 0
|
||||
```
|
||||
|
||||
### ۴.۲ خروج از Magic Mode
|
||||
|
||||
```
|
||||
شرطها (هرکدام کافیه):
|
||||
├── wallet.Balance == 0 && MagicTotalDeposited > 0 (همه رو خرج کرد)
|
||||
└── MagicTotalDeposited >= MagicWalletMaxDeposit (سقف پر شد)
|
||||
|
||||
نتیجه:
|
||||
├── wallet.WalletMode = Normal
|
||||
├── wallet.MagicCompletedAt = DateTime.UtcNow
|
||||
└── user.PurchaseCycleCount++
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ۵. تراکنشها و لاگ
|
||||
|
||||
### ۵.۱ انواع تراکنش جدید
|
||||
|
||||
| TransactionType | کد | توضیح |
|
||||
|-----------------|-----|--------|
|
||||
| `MagicWalletDeposit` | 14 | واریز اصلی از درگاه (مبلغ واقعی) |
|
||||
| `MagicWalletBonus` | 15 | بونوس داخلی (مبلغ × 1.5) |
|
||||
|
||||
### ۵.۲ لاگ کیفپول (اجباری)
|
||||
|
||||
هر شارژ جادویی **باید** یک رکورد `UserWalletChangeLog` ایجاد کنه:
|
||||
|
||||
```
|
||||
UserWalletChangeLog:
|
||||
├── UserWalletId = wallet.Id
|
||||
├── CurrentBalance = wallet.Balance (بعد از تغییر)
|
||||
├── BalanceChange = creditAmount (مبلغ × 2.5)
|
||||
├── IsIncrement = true
|
||||
├── ReferenceId = transaction.Id
|
||||
├── CurrentDiscountBalance = wallet.DiscountBalance (بدون تغییر)
|
||||
├── DiscountBalanceChange = 0
|
||||
├── CurrentNetworkBalance = wallet.NetworkBalance (بدون تغییر)
|
||||
└── NetworkBalanceChange = 0
|
||||
```
|
||||
|
||||
> ⚠️ **لاگ کیفپول دلخواه نیست — اجباریه.** هر تغییر Balance باید لاگ بخوره.
|
||||
|
||||
---
|
||||
|
||||
## ۶. مسیر شارژ جادویی (API)
|
||||
|
||||
### ۶.۱ فلوی کامل
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as کاربر
|
||||
participant FO as FrontOffice
|
||||
participant CMS as CMS (gRPC)
|
||||
participant PYMS as PYMS
|
||||
participant ZP as ZarinPal
|
||||
|
||||
U->>FO: مبلغ واریز (مثلاً 10M)
|
||||
FO->>CMS: InitiateMagicCharge(userId, amount)
|
||||
|
||||
Note over CMS: Validations:<br/>WalletMode == Magic<br/>TotalDeposited + amount <= 100M
|
||||
|
||||
CMS->>PYMS: CreatePaymentRequest(amount)
|
||||
PYMS->>ZP: Request Authority
|
||||
ZP-->>PYMS: Authority
|
||||
PYMS-->>CMS: PaymentUrl
|
||||
CMS-->>FO: PaymentUrl
|
||||
FO->>U: Redirect to ZarinPal
|
||||
|
||||
U->>ZP: پرداخت
|
||||
ZP->>CMS: Callback /api/wallet/verify-magic-charge
|
||||
|
||||
Note over CMS: creditAmount = amount × 2.5<br/>bonusAmount = amount × 1.5
|
||||
|
||||
CMS->>CMS: Balance += creditAmount
|
||||
CMS->>CMS: Transaction #1 (MagicDeposit, amount)
|
||||
CMS->>CMS: Transaction #2 (MagicBonus, bonusAmount)
|
||||
CMS->>CMS: WalletChangeLog ✅
|
||||
CMS->>CMS: MagicTotalDeposited += amount
|
||||
|
||||
CMS-->>FO: Redirect to callback page
|
||||
FO->>U: نتیجه + بالانس جدید
|
||||
```
|
||||
|
||||
### ۶.۲ تفاوت با ChargeDiscountWallet
|
||||
|
||||
| ویژگی | ChargeDiscountWallet | MagicCharge |
|
||||
|--------|---------------------|-------------|
|
||||
| **هدف** | شارژ DiscountBalance | شارژ Balance (جادویی) |
|
||||
| **ضریب** | ×1 (مبلغ واقعی) | ×2.5 |
|
||||
| **سقف** | ندارد | 100M تومان ورودی |
|
||||
| **شرط** | همیشه فعال | فقط WalletMode == Magic |
|
||||
| **کمیسیون** | — | ❌ غیرفعال |
|
||||
| **Callback** | `/api/wallet/verify-discount-charge` | `/api/wallet/verify-magic-charge` |
|
||||
| **وضعیت** | ⚠️ نیمهکاره (controller ندارد) | 🆕 باید ساخته بشه |
|
||||
|
||||
> ⚠️ **ChargeDiscountWallet ناقصه و باید مستقل کامل بشه — ربطی به کیفپول جادویی نداره.**
|
||||
|
||||
---
|
||||
|
||||
## ۷. تغییرات مدل داده
|
||||
|
||||
### ۷.۱ UserWallet — فیلدهای جدید
|
||||
|
||||
```csharp
|
||||
// اضافه به UserWallet entity:
|
||||
public WalletMode WalletMode { get; set; } = WalletMode.Normal;
|
||||
public long MagicTotalDeposited { get; set; } // مجموع واریزی واقعی (ریال)
|
||||
public long MagicTotalCredited { get; set; } // مجموع اعتبار دادهشده (ریال)
|
||||
public DateTime? MagicActivatedAt { get; set; }
|
||||
public DateTime? MagicCompletedAt { get; set; }
|
||||
```
|
||||
|
||||
### ۷.۲ WalletMode enum (جدید)
|
||||
|
||||
```csharp
|
||||
public enum WalletMode
|
||||
{
|
||||
Normal = 0, // حالت عادی — کمیسیون فعال
|
||||
Magic = 1 // حالت جادویی — شارژ ×2.5، بدون کمیسیون
|
||||
}
|
||||
```
|
||||
|
||||
### ۷.۳ TransactionType — مقادیر جدید
|
||||
|
||||
```csharp
|
||||
// اضافه به TransactionType enum:
|
||||
MagicWalletDeposit = 14, // واریز از درگاه (مبلغ واقعی)
|
||||
MagicWalletBonus = 15 // بونوس داخلی (مبلغ × 1.5)
|
||||
```
|
||||
|
||||
### ۷.۴ SystemConstants — ثابتهای جدید
|
||||
|
||||
```csharp
|
||||
public const decimal MagicWalletMultiplier = 2.5m;
|
||||
public const long MagicWalletMaxDeposit = 1_000_000_000; // 100M تومان = 1B ریال
|
||||
public const long MagicWalletMaxCredit = 2_500_000_000; // 250M تومان = 2.5B ریال
|
||||
```
|
||||
|
||||
### ۷.۵ EF Migration
|
||||
|
||||
```
|
||||
Migration: AddMagicWalletFields
|
||||
├── ALTER TABLE UserWallets ADD WalletMode int NOT NULL DEFAULT 0
|
||||
├── ALTER TABLE UserWallets ADD MagicTotalDeposited bigint NOT NULL DEFAULT 0
|
||||
├── ALTER TABLE UserWallets ADD MagicTotalCredited bigint NOT NULL DEFAULT 0
|
||||
├── ALTER TABLE UserWallets ADD MagicActivatedAt datetime2 NULL
|
||||
└── ALTER TABLE UserWallets ADD MagicCompletedAt datetime2 NULL
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ۸. تغییرات Handlerها
|
||||
|
||||
### ۸.۱ SubmitShopBuyOrderCommandHandler (تغییر)
|
||||
|
||||
```
|
||||
بعد از کسر Balance:
|
||||
if (wallet.Balance == 0
|
||||
&& user.PackagePurchaseMethod != None
|
||||
&& wallet.WalletMode == Normal
|
||||
&& user.ClubMembership?.IsActive == true)
|
||||
{
|
||||
→ ورود به Magic Mode
|
||||
}
|
||||
|
||||
if (wallet.WalletMode == Magic
|
||||
&& wallet.Balance == 0
|
||||
&& wallet.MagicTotalDeposited > 0)
|
||||
{
|
||||
→ خروج از Magic Mode
|
||||
}
|
||||
```
|
||||
|
||||
### ۸.۲ CalculateWeeklyBalancesCommandHandler (تغییر)
|
||||
|
||||
```
|
||||
فیلتر اضافه:
|
||||
فقط کاربرهایی که wallet.WalletMode == Normal
|
||||
(کاربرهای Magic از محاسبه کمیسیون خارج میشن)
|
||||
```
|
||||
|
||||
### ۸.۳ CheckAndProcessDayaLoansCommandHandler (تغییر)
|
||||
|
||||
```
|
||||
Validation اضافه:
|
||||
if (user.PurchaseCycleCount > 0)
|
||||
→ reject: "وام دایا فقط برای خرید اولین پکیج"
|
||||
```
|
||||
|
||||
### ۸.۴ InitiateMagicChargeCommandHandler (جدید)
|
||||
|
||||
```
|
||||
Input: UserId, Amount
|
||||
Validations:
|
||||
├── wallet.WalletMode == Magic
|
||||
├── Amount > 0
|
||||
└── MagicTotalDeposited + Amount <= MagicWalletMaxDeposit
|
||||
Action:
|
||||
├── PaymentTransaction → PYMS → ZarinPal
|
||||
└── CallbackUrl = "/api/wallet/verify-magic-charge"
|
||||
Return: PaymentUrl
|
||||
```
|
||||
|
||||
### ۸.۵ VerifyMagicChargeCommandHandler (جدید)
|
||||
|
||||
```
|
||||
Input: Authority, Status
|
||||
On Success:
|
||||
├── creditAmount = amount × 2.5
|
||||
├── bonusAmount = creditAmount - amount
|
||||
├── wallet.Balance += creditAmount
|
||||
├── wallet.MagicTotalDeposited += amount
|
||||
├── wallet.MagicTotalCredited += creditAmount
|
||||
├── Transaction #1 (MagicWalletDeposit, amount)
|
||||
├── Transaction #2 (MagicWalletBonus, bonusAmount)
|
||||
└── WalletChangeLog ✅ (اجباری)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ۹. تغییرات UI (FrontOffice)
|
||||
|
||||
### ۹.۱ صفحات جدید
|
||||
|
||||
| صفحه | Route | توضیح |
|
||||
|-------|-------|--------|
|
||||
| MagicWallet.razor | `/profile/magic-wallet` | فرم شارژ + پروگرسبار سقف |
|
||||
| MagicPaymentCallback.razor | `/profile/magic-payment-callback` | نتیجه پرداخت شارژ جادویی |
|
||||
|
||||
### ۹.۲ تغییر صفحات موجود
|
||||
|
||||
| صفحه | تغییر |
|
||||
|-------|--------|
|
||||
| Profile/Index.razor | بنر "🪄 کیفپول جادویی فعال" + لینک شارژ |
|
||||
| Profile/Wallet.razor | نمایش وضعیت Magic + پروگرس (deposited/100M) |
|
||||
| Club membership page | اگه Magic → پیام "بعد از اتمام جادویی میتونید پکیج بخرید" |
|
||||
|
||||
### ۹.۳ gRPC Proto اضافات
|
||||
|
||||
```protobuf
|
||||
// userwallet.proto — RPCهای جدید:
|
||||
rpc InitiateMagicCharge (MagicChargeRequest) returns (MagicChargeResponse);
|
||||
rpc GetMagicWalletStatus (MagicWalletStatusRequest) returns (MagicWalletStatusResponse);
|
||||
|
||||
message MagicChargeRequest {
|
||||
int64 user_id = 1;
|
||||
int64 amount = 2;
|
||||
}
|
||||
|
||||
message MagicChargeResponse {
|
||||
string payment_url = 1;
|
||||
int64 remaining_deposit_cap = 2;
|
||||
}
|
||||
|
||||
message MagicWalletStatusResponse {
|
||||
bool is_magic_mode = 1;
|
||||
int64 total_deposited = 2;
|
||||
int64 total_credited = 3;
|
||||
int64 remaining_cap = 4;
|
||||
string activated_at = 5;
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user