Files
docs/roadmap/MAGIC-WALLET-SPEC.md
T
masoodafar-web 0e61513b0e docs: add ClubMembershipCycle table to preserve activation date
Problem: ActivateClubMembership overwrites ActivatedAt on re-purchase,
losing the original club activation date. Commission check uses
ActivatedAt to determine 'new member this week'.

Solution: New ClubMembershipCycle table
- Each package purchase creates a new cycle record
- ClubMembership.ActivatedAt = first-time only (never overwritten)
- Commission uses Cycle.PackagePurchasedAt instead of ActivatedAt
- IsCurrentCycle flag tracks active cycle
- Full history preserved for all purchase cycles

Also updated:
- SPEC: section 7.5 (entity), 7.6 (migration), 8.2 (SP change), 8.3 (handler)
- PLAN: phase 1 (entity), phase 2 (activate handler), phase 4 (date query)
- Checklist: added 4 new items
2026-02-19 02:58:14 +03:30

601 lines
24 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 ریال
```
### ۷.۵ ClubMembershipCycle — جدول جدید (حل مشکل تاریخ کمیسیون)
#### مشکل فعلی
```
⚠️ الان محاسبه کمیسیون هفتگی از ClubMembership.ActivatedAt استفاده میکنه:
WHERE cm.ActivatedAt >= @StartDate AND cm.ActivatedAt <= @EndDate
وقتی کاربر دور دوم پکیج بخره، ActivateClubMembership این تاریخ رو overwrite میکنه:
entity.ActivatedAt = DateTime.Now; // ← تاریخ اصلی از بین میره!
مشکل: تاریخ اولین فعال‌سازی باشگاه از دست میره.
```
#### راه‌حل: جدول `ClubMembershipCycle`
به‌جای آپدیت کردن `ActivatedAt`، هر بار که پکیج خریده میشه یک رکورد جدید در جدول `ClubMembershipCycle` ایجاد میشه. محاسبه کمیسیون از این جدول استفاده میکنه.
```csharp
// Entity جدید:
public class ClubMembershipCycle : BaseAuditableEntity
{
public long UserId { get; set; }
public User User { get; set; }
public long ClubMembershipId { get; set; }
public ClubMembership ClubMembership { get; set; }
public int CycleNumber { get; set; } // شماره دور (1, 2, 3...)
public DateTime PackagePurchasedAt { get; set; } // تاریخ خرید پکیج
public DateTime? MagicStartedAt { get; set; } // شروع Magic (Balance=0)
public DateTime? MagicCompletedAt { get; set; } // پایان Magic
public PackagePurchaseMethod PurchaseMethod { get; set; } // IPG یا Daya
public long PackageAmount { get; set; } // 56M
public bool IsCurrentCycle { get; set; } // فقط یکی true
}
```
#### تغییرات در منطق کمیسیون
```sql
-- قبل (غلط — ActivatedAt از بین میره):
WHERE cm.ActivatedAt >= @StartDate AND cm.ActivatedAt <= @EndDate
-- بعد (درست — از جدول Cycle):
WHERE cc.PackagePurchasedAt >= @StartDate
AND cc.PackagePurchasedAt <= @EndDate
AND cc.IsCurrentCycle = 1
```
#### ClubMembership — بدون تغییر ساختاری
```
ClubMembership:
├── ActivatedAt → تاریخ اولین فعال‌سازی (هرگز overwrite نمیشه ✅)
├── IsActive → وضعیت فعلی باشگاه
└── + Cycles (nav prop) → لیست دورها
```
#### مثال عملی
```
ClubMembership #42:
UserId = 100
ActivatedAt = 1403/10/15 ← اولین بار (حفظ میشه ✅)
IsActive = true
ClubMembershipCycles:
┌────┬──────┬───────────────────┬──────────────┬─────────────┐
│ Id │ Cycle│ PackagePurchasedAt│ PurchaseMethod│IsCurrentCycle│
├────┼──────┼───────────────────┼──────────────┼─────────────┤
│ 1 │ 1 │ 1403/10/15 │ DayaLoan │ false │
│ 2 │ 2 │ 1404/01/20 │ DirectIPG │ false │
│ 3 │ 3 │ 1404/04/05 │ DirectIPG │ true ✅ │
└────┴──────┴───────────────────┴──────────────┴─────────────┘
→ کمیسیون هفته 1404/04/05 تا 1404/04/11:
PackagePurchasedAt (دور ۳) = 1404/04/05 → ✅ در بازه هست → امتیاز میگیره
→ تاریخ اولین فعال‌سازی: 1403/10/15 → حفظ شده ✅
```
### ۷.۶ 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
Migration: AddClubMembershipCycle
├── CREATE TABLE ClubMembershipCycles (
│ Id bigint IDENTITY PRIMARY KEY,
│ UserId bigint NOT NULL FK → Users,
│ ClubMembershipId bigint NOT NULL FK → ClubMemberships,
│ CycleNumber int NOT NULL,
│ PackagePurchasedAt datetime2 NOT NULL,
│ MagicStartedAt datetime2 NULL,
│ MagicCompletedAt datetime2 NULL,
│ PurchaseMethod int NOT NULL,
│ PackageAmount bigint NOT NULL,
│ IsCurrentCycle bit NOT NULL DEFAULT 0,
│ + BaseAuditableEntity fields
│ )
└── Data Migration: INSERT یک رکورد Cycle=1 برای هر ClubMembership موجود
(PackagePurchasedAt = ClubMembership.ActivatedAt)
```
---
## ۸. تغییرات 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 (تغییر)
```
تغییر ۱ — فیلتر Magic:
فقط کاربرهایی که wallet.WalletMode == Normal
(کاربرهای Magic از محاسبه کمیسیون خارج میشن)
تغییر ۲ — تاریخ از Cycle (به‌جای ActivatedAt):
قبل:
WHERE cm.ActivatedAt >= @StartDate AND cm.ActivatedAt <= @EndDate
بعد:
WHERE cc.PackagePurchasedAt >= @StartDate
AND cc.PackagePurchasedAt <= @EndDate
AND cc.IsCurrentCycle = 1
(هم در C# handler و هم در SP باید تغییر کنه)
```
### ۸.۳ ActivateClubMembershipCommandHandler (تغییر مهم)
```
قبل (غلط — تاریخ overwrite میشه):
entity.ActivatedAt = DateTime.Now;
بعد (درست):
// ActivatedAt فقط بار اول ست میشه:
if (entity.ActivatedAt == default)
entity.ActivatedAt = DateTime.Now;
// هر بار یه Cycle جدید:
var previousCycle = entity.Cycles.FirstOrDefault(c => c.IsCurrentCycle);
if (previousCycle != null)
previousCycle.IsCurrentCycle = false;
entity.Cycles.Add(new ClubMembershipCycle
{
CycleNumber = (previousCycle?.CycleNumber ?? 0) + 1,
PackagePurchasedAt = DateTime.Now,
PurchaseMethod = user.PackagePurchaseMethod,
PackageAmount = SystemConstants.BasePackageAmount,
IsCurrentCycle = true
});
```
### ۸.۴ 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;
}
```