Files
docs/roadmap/MAGIC-WALLET-SPEC.md
T
masoodafar-web a94d0dbe95 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
2026-02-19 02:08:28 +03:30

398 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🪄 کیف‌پول جادویی (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;
}
```