Files
docs/roadmap/MAGIC-WALLET-SPEC.md
T
masoodafar-web b1dd69b31f docs: fix Magic Mode exit condition — BOTH Balance=0 AND cap reached
- Exit requires BOTH simultaneously: Balance==0 AND TotalDeposited>=100M
- If Balance=0 but cap not reached → still Magic (can charge more)
- If cap reached but Balance>0 → still Magic (can spend more)
- Added examples: partial deposit (50M/100M) stays in Magic
- Fixed handler 8.1 pseudo-code
- Updated test scenarios in PLAN (7 scenarios covering edge cases)
2026-02-19 02:32:04 +03:30

467 lines
18 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 از فروشگاه عادی، وارد **حالت جادویی** می‌شه. در این حالت می‌تونه کیف‌پولش رو از درگاه شارژ کنه و **۲.۵ برابر** اعتبار بگیره — بدون هیچ کمیسیون یا پورسانتی.
> ⚠️ **سقف ۱۰۰M ورودی / ۲۵۰M خروجی per-cycle هست** — هر بار که کاربر دوباره پکیج ۵۶M بخره و وارد Magic بشه، سقف ریست میشه. این چرخه تا بی‌نهایت تکرار میشه.
```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
### ۳.۱ ضریب و سقف (per-cycle)
| پارامتر | مقدار | ثابت پیشنهادی | اسکوپ |
|----------|-------|---------------|--------|
| ضریب شارژ | **×2.5** | `MagicWalletMultiplier = 2.5m` | — |
| سقف ورودی | **100M تومان** (1B ریال) | `MagicWalletMaxDeposit = 1_000_000_000` | **هر دور** |
| سقف خروجی | **250M تومان** (2.5B ریال) | `MagicWalletMaxCredit = 2_500_000_000` | **هر دور** |
| سود کاربر | **150%** | — | — |
> 🔄 **سقف per-cycle هست نه lifetime.** هر بار که کاربر از Magic خارج بشه و دوباره پکیج ۵۶M بخره،
> `MagicTotalDeposited` و `MagicTotalCredited` به **صفر ریست** میشن و یه دور جدید شروع میشه.
### ۳.۲ مثال عددی (یک شارژ)
```
واریز: 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
```
### ۳.۳ مثال چند دوری (چرخه تکرار)
```
══════════════════════════════════════════════════════════════
دور ۱ (اولین بار)
══════════════════════════════════════════════════════════════
① خرید پکیج 56M (IPG یا دایا) → Balance=56M, Discount=112M
② فعال‌سازی باشگاه → کمیسیون ✅
③ خرید از فروشگاه عادی → Balance کم میشه...
④ Balance = 0 → 🪄 Magic Mode فعال!
MagicTotalDeposited = 0 (ریست)
⑤ شارژ جادویی: مجموعاً 100M واریز → 250M اعتبار
⑥ خرید از فروشگاه عادی → Balance کم میشه...
⑦ Balance = 0 → خروج از Magic → Normal Mode
══════════════════════════════════════════════════════════════
دور ۲ (خرید مجدد پکیج — فقط IPG، دایا ❌)
══════════════════════════════════════════════════════════════
① خرید پکیج 56M (فقط IPG) → Balance=56M, Discount=112M
② کمیسیون دوباره فعال ✅
③ خرید از فروشگاه عادی → Balance کم میشه...
④ Balance = 0 → 🪄 Magic Mode فعال!
MagicTotalDeposited = 0 (ریست)
─────────
⑤ شارژ جادویی: مجموعاً 100M واریز → 250M اعتبار
⑥ خرید → Balance = 0 → خروج از Magic
══════════════════════════════════════════════════════════════
دور ۳, ۴, ۵, ... (تا بی‌نهایت — همین چرخه تکرار)
══════════════════════════════════════════════════════════════
```
### ۳.۴ چه چیزهایی غیرفعال میشه
| قابلیت | 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
```
شرط‌ها (هر دو باید همزمان true باشن):
├── wallet.Balance == 0 (همه رو خرج کرده)
└── wallet.MagicTotalDeposited >= MagicWalletMaxDeposit (سقف 100M پر شده)
نتیجه:
├── wallet.WalletMode = Normal
├── wallet.MagicCompletedAt = DateTime.UtcNow
└── user.PurchaseCycleCount++
⚠️ توضیح مهم:
اگه Balance=0 بشه ولی هنوز سقف شارژ پر نشده → هنوز Magic هست!
کاربر میتونه دوباره شارژ کنه (تا سقف 100M).
مثال:
TotalDeposited = 50M, Balance = 0
→ هنوز Magic → میتونه 50M دیگه شارژ کنه (125M اعتبار بگیره)
TotalDeposited = 100M, Balance = 30M
→ هنوز Magic → نمیتونه شارژ کنه ولی هنوز بالانس داره
TotalDeposited = 100M, Balance = 0
→ ✅ خروج از Magic → Normal Mode
```
### ۴.۳ ریست سقف در دور بعدی
```
وقتی کاربر دوباره پکیج ۵۶M بخره و Balance=0 بشه → Magic Mode:
├── MagicTotalDeposited = 0 ← ریست!
├── MagicTotalCredited = 0 ← ریست!
├── MagicActivatedAt = now ← زمان جدید
└── MagicCompletedAt = null ← پاک میشه
⚠️ سقف per-cycle هست:
├── هر دور: حداکثر 100M واریز → 250M اعتبار
├── تعداد دور: بی‌نهایت (تا وقتی پکیج بخره)
└── 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 >= SystemConstants.MagicWalletMaxDeposit)
{
→ خروج از Magic Mode
// سقف 100M پر شده + همه رو خرج کرده
}
// ⚠️ اگه Balance=0 ولی سقف پر نشده → هنوز Magic!
// کاربر میتونه دوباره شارژ کنه
```
### ۸.۲ 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;
}
```