# 🪄 کیف‌پول جادویی (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 ``` شرط‌ها (هرکدام کافیه): ├── wallet.Balance == 0 && MagicTotalDeposited > 0 (همه رو خرج کرد) └── MagicTotalDeposited >= MagicWalletMaxDeposit (سقف پر شد) نتیجه: ├── wallet.WalletMode = Normal ├── wallet.MagicCompletedAt = DateTime.UtcNow └── user.PurchaseCycleCount++ ``` ### ۴.۳ ریست سقف در دور بعدی ``` وقتی کاربر دوباره پکیج ۵۶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:
WalletMode == Magic
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
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; } ```