Files
docs/roadmap/MAGIC-WALLET-SPEC.md
T
masoodafar-web 2f0d43aa81 docs: clarify Magic Wallet cap is per-cycle, not lifetime
- Cap resets every time user buys a new 56M package and re-enters Magic
- MagicTotalDeposited & MagicTotalCredited reset to 0 on each new cycle
- Added multi-cycle example (cycle 1, 2, 3... ∞)
- Added section 4.3: reset behavior on re-entry
- Added test scenarios for cycle reset
- PurchaseCycleCount tracks cycles but has no limit
2026-02-19 02:17:10 +03:30

18 KiB
Raw Blame History

🪄 کیف‌پول جادویی (Magic Wallet)

وضعیت: طراحی — آماده پیاده‌سازی
تاریخ: اسفند ۱۴۰۴
وابستگی: خرید پکیج پایه، فروشگاه عادی، سیستم کمیسیون


۱. خلاصه بیزینسی

کاربر بعد از خرید پکیج پایه (۵۶M) و خرج کردن کامل Balance از فروشگاه عادی، وارد حالت جادویی می‌شه. در این حالت می‌تونه کیف‌پولش رو از درگاه شارژ کنه و ۲.۵ برابر اعتبار بگیره — بدون هیچ کمیسیون یا پورسانتی.

⚠️ سقف ۱۰۰M ورودی / ۲۵۰M خروجی per-cycle هست — هر بار که کاربر دوباره پکیج ۵۶M بخره و وارد Magic بشه، سقف ریست میشه. این چرخه تا بی‌نهایت تکرار میشه.

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)

۶.۱ فلوی کامل

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 — فیلدهای جدید

// اضافه به 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 (جدید)

public enum WalletMode
{
    Normal = 0,    // حالت عادی — کمیسیون فعال
    Magic = 1      // حالت جادویی — شارژ ×2.5، بدون کمیسیون
}

۷.۳ TransactionType — مقادیر جدید

// اضافه به TransactionType enum:
MagicWalletDeposit = 14,    // واریز از درگاه (مبلغ واقعی)
MagicWalletBonus = 15       // بونوس داخلی (مبلغ × 1.5)

۷.۴ SystemConstants — ثابت‌های جدید

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 اضافات

// 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;
}