<div dir="rtl" align="right">

# تحلیل امکانات قابل ارائه به مشتری (Codex)
تحلیل مختصر بر اساس: `CMS/cms-data-and-business.md`, `CMS/network-club-commission-system-v1.1.md`, `CMS/balance-calculation-carryover-logic.md`, `CMS/email-sms-configuration-guide.md`, `REMAINING-TASKS.md`.


---

## 🏗️ راهنمای معماری: جریان توسعه از CMS تا FrontOffice

### 📐 ساختار کلی پروژه

```
┌─────────────────────────────────────────────────────────────┐
│                     USER (Customer)                         │
│                    مشتری / کاربر نهایی                      │
└──────────────────────────┬──────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│              FrontOffice (Blazor WebAssembly)               │
│                       فرانت سمت مشتری                       │
│  Location: FrontOffice/src/FrontOffice.Main/                │
│  Technology: Blazor WASM + MudBlazor                        │
│  Files: Pages/*.razor, Components/*.razor                   │
└──────────────────────────┬──────────────────────────────────┘
                           │ HTTP/REST
                           ▼
┌─────────────────────────────────────────────────────────────┐
│          FrontOffice.BFF (Backend For Frontend)             │
│                    گیت‌وی سمت مشتری                         │
│  Location: FrontOffice.BFF/src/                             │
│  Technology: ASP.NET Core REST API                          │
│  Structure:                                                 │
│    ├── Application/                                         │
│    │   ├── [ModuleName]CQ/                                  │
│    │   │   ├── Commands/                                    │
│    │   │   └── Queries/                                     │
│    │   └── DTOs/                                            │
│    └── WebApi/                                              │
│        └── Controllers/                                     │
└──────────────────────────┬──────────────────────────────────┘
                           │ gRPC (CMS Protobuf)
                           ▼
┌─────────────────────────────────────────────────────────────┐
│                    CMS (Microservice)                        │
│                  سرویس اصلی / دیتابیس                       │
│  Location: CMS/src/CMSMicroservice.*/                       │
│  Technology: ASP.NET Core + gRPC + SQL Server               │
│  Structure (Clean Architecture):                            │
│    ├── Domain/         (Entities, Enums, Events)            │
│    ├── Application/    (Commands, Queries, Handlers)        │
│    ├── Infrastructure/ (Database, Services)                 │
│    ├── Protobuf/       (gRPC Proto definitions)             │
│    └── WebApi/         (gRPC Services, Hangfire)            │
└─────────────────────────────────────────────────────────────┘
```

### 🔄 جریان توسعه یک قابلیت (Feature Flow)

#### مثال: پیاده‌سازی "نمایش کمیسیون هفتگی"

```
Step 1: CMS (Already Done ✅)
├── Domain/Entities/UserCommissionPayout.cs
├── Application/CommissionCQ/Queries/GetUserCommissionPayouts/
│   ├── GetUserCommissionPayoutsQuery.cs
│   ├── GetUserCommissionPayoutsQueryHandler.cs
│   └── CommissionPayoutDto.cs
└── Protobuf/Protos/Commission.proto (gRPC definition)

Step 2: FrontOffice.BFF (TODO ❌)
├── Application/CommissionCQ/Queries/GetMyCommissionPayouts/
│   ├── GetMyCommissionPayoutsQuery.cs
│   ├── GetMyCommissionPayoutsQueryHandler.cs
│   │   └── Calls CMS via gRPC: CommissionService.GetUserCommissionPayouts
│   └── CommissionPayoutResponseDto.cs (Customer-friendly DTO)
└── WebApi/Controllers/CommissionController.cs
    └── GET /api/commission/my-payouts

Step 3: FrontOffice UI (TODO ❌)
└── Pages/Commission/PayoutsPage.razor
    ├── @inject CommissionService _commissionService
    ├── await _commissionService.GetMyPayoutsAsync()
    └── Display: MudTable with Columns (Week, Amount, Status, Date)
```

### 🎨 تفاوت‌های کلیدی CMS vs BFF

| جنبه | CMS (Microservice) | FrontOffice.BFF | FrontOffice UI |
|------|-------------------|-----------------|----------------|
| **مخاطب** | Admin + System | Customer فقط | Customer |
| **داده** | همه کاربران | کاربر جاری (`UserId` از JWT) | کاربر جاری |
| **Response** | DTO کامل + Metadata | DTO ساده + فقط فیلدهای لازم | UI-friendly JSON |
| **Authorization** | Role-based (Admin/User) | User-only (No Admin access) | Login required |
| **مثال Query** | `GetAllCommissionPayouts` | `GetMyCommissionPayouts` | نمایش جدول |
| **Input** | `UserId` required | `UserId` از Token | هیچ ورودی (خودکار) |

### 📁 ساختار استاندارد BFF Module

```csharp
FrontOffice.BFF/src/FrontOffice.BFF.Application/
└── [ModuleName]CQ/
    ├── Commands/
    │   └── [ActionName]/
    │       ├── [ActionName]Command.cs          // Input
    │       ├── [ActionName]CommandHandler.cs   // Logic
    │       ├── [ActionName]CommandValidator.cs // Validation
    │       └── [ActionName]ResponseDto.cs      // Output
    └── Queries/
        └── [QueryName]/
            ├── [QueryName]Query.cs
            ├── [QueryName]QueryHandler.cs
            └── [QueryName]ResponseDto.cs
```

### 🔐 احراز هویت و دسترسی

**JWT Token Structure:**
```json
{
  "sub": "123",                    // UserId
  "email": "user@example.com",
  "phone": "09123456789",
  "IsSignMainContract": "True",    // قرارداد امضا شده؟
  "exp": 1234567890
}
```

**استخراج UserId در Handler:**
```csharp
public class GetMyCommissionPayoutsQueryHandler : IRequestHandler<...>
{
    private readonly ICurrentUserService _currentUser;
    
    public async Task<Response> Handle(Query request, CancellationToken ct)
    {
        var userId = _currentUser.UserId; // از JWT
        
        // Call CMS with userId
        var result = await _cmsClient.GetUserCommissionPayoutsAsync(userId);
        return result;
    }
}
```

### 📦 الگوی DTO Mapping

**CMS DTO (داده خام):**
```csharp
public class CommissionPayoutDto
{
    public long Id { get; set; }
    public long UserId { get; set; }
    public int WeekNumber { get; set; }
    public long TotalAmount { get; set; }
    public CommissionPayoutStatus Status { get; set; }
    public DateTime CalculatedDate { get; set; }
    // ... 10 فیلد دیگر
}
```

**BFF Response DTO (مشتری‌محور):**
```csharp
public class MyCommissionPayoutDto
{
    public long Id { get; set; }
    public string WeekLabel { get; set; }        // "هفته 45 - آذر 1403"
    public string AmountFormatted { get; set; }  // "1,250,000 تومان"
    public string StatusText { get; set; }       // "پرداخت شده"
    public string StatusBadgeColor { get; set; } // "success" / "warning"
    public string DatePersian { get; set; }      // "25 آذر 1403"
}
```

### 🎯 چک‌لیست شروع توسعه

قبل از شروع کار روی هر ماژول، این موارد را چک کنید:

```
[ ] CMS Commands/Queries مربوطه را شناسایی کردم
[ ] Proto definitions مربوطه را یافتم (Protobuf/*.proto)
[ ] نمونه Handler موجود در BFF را بررسی کردم
[ ] JWT Token و CurrentUserService را فهمیدم
[ ] ساختار DTO مشتری‌محور را طراحی کردم
[ ] Mock data برای UI آماده کردم (قبل از اتصال به API)
```

## ترمینولوژی
- «گت‌وی سمت مشتری» = `FrontOffice.BFF`
- «فرانت» = پروژه `FrontOffice` (UI مشتری)
- **الویت کار**: تغییر روی CMS فقط پس از تأیید؛ تمرکز اصلی روی `FrontOffice.BFF` و `FrontOffice`. هر نیازمندی جدید سمت مشتری قبل از دست‌کاری CMS باید تأیید شود.

## امکانات موجود در CMS که باید در FrontOffice دیده شود
- **عضویت باشگاه و کیف‌پول‌های سه‌گانه**: جریان پرداخت/فعال‌سازی (۵۶M) → افزایش همزمان `Balance` و `DiscountBalance` و واریز ۲۵M به استخر؛ نیاز به UI «عضویت در باشگاه»، نمایش موجودی هر سه کیف‌پول و تراکنش‌های مرتبط.
- **فروشگاه باشگاه با تخفیف**: خرید از فروشگاه ویژه با `DiscountBalance`؛ تفکیک لیست محصولات باشگاه و عمومی + نمایش سقف/درصد تخفیف و موجودی تخفیف در کارت محصول/Checkout.
- **شبکه باینری و تعادل هفتگی**: نمایش درخت دوبخشی، اعضای جدید هر پا، تعادل هفته، Carryover و سقف هفتگی (`MaxWeeklyBalances`=۳۰۰)؛ UI گزارش هفتگی و نمودار رشد برای شفاف‌سازی محاسبه کمیسیون.
- **کمیسیون هفتگی و پرداخت‌ها**: نمایش مقدار استخر هفته، ارزش هر Balance، امتیازهای کاربر، مبلغ قابل برداشت، تاریخچه `UserCommissionPayout` با وضعیت (Pending/Calculated/Paid/Withdrawn) و امکان انتخاب روش برداشت (IBAN).
- **ویژگی‌های باشگاه (ClubFeature)**: لیست فیچرهای فعال/قابل دریافت، امتیاز موردنیاز و تاریخ فعال‌سازی (`UserClubFeature`); ارائه به‌صورت Badge/Progress Bar در پروفایل.
- **تجربه خرید استاندارد**: کاتالوگ دسته/تگ، سبد (`UserCarts`)، Checkout، پرداخت ترکیبی (کیف‌پول + درگاه)، فاکتور (`FactorDetails`)، وضعیت ارسال/کد رهگیری؛ تاریخچه سفارش در پروفایل.
- **کیف‌پول و لاگ مالی**: تاریخچه `UserWalletChangeLog` (واریز، خرید، بازپرداخت، برداشت) با فیلتر نوع/بازه زمانی؛ واریز از درگاه، برداشت با صف تأیید دستی؛ نمایش `NetworkBalance` جداگانه.
- **آدرس‌ها و قراردادها**: مدیریت آدرس پیش‌فرض برای سفارش؛ اجباری‌کردن قبول آخرین نسخه قرارداد/Terms و نگه‌داری PDF امضا شده؛ هدایت اجباری به صفحه امضا در اولین ورود بعد از تغییر نسخه.
- **اعلان‌ها**: ایمیل/SMS برای فعال‌سازی باشگاه، پرداخت کمیسیون، خطاها و وضعیت ارسال؛ در پروفایل دکمه Opt-in/Opt-out اعلان‌ها (موبایل/ایمیل) نیاز است.

## قابلیت‌های جدید/در حال تکمیل که باید در Gateway و UI برنامه‌ریزی شود
- **سیستم تراکنش درگاه (۰٪)**: جریان Create/Verify/Refund تراکنش؛ در FrontOffice صفحات وضعیت تراکنش، Retry/Verify، نمایش `ReferenceId` و همگام‌سازی وضعیت سفارش/کیف‌پول.
- **سبد خرید پیشرفته (۰٪)**: پشتیبانی Add/Update/Delete/Clear/Merge (مهمان→ورود) روی `UserCarts`; UI ادغام سبد مهمان و کاربر، و بازگردانی سبد در شکست پرداخت.
- **تکمیل Products & Orders (۷۰٪)**: اعمال Tag/Category فیلترها، نمایش موجودی/تخفیف/گالری، کنترل تغییر قیمت روی اقلام فاکتور، قابلیت لغو سفارش و Refund به کیف‌پول.
- **Withdrawal/Settlement (۴۰٪)**: فرم درخواست برداشت از `NetworkBalance`/Balance با IBAN، پیگیری وضعیت صف تأیید، تاریخچه برداشت و سقف‌های روزانه.
- **VAT روی سفارشات (جدید)**: نمایش `VatPercentage` و خط مجزا در فاکتور/Checkout («شامل ۱۰٪ مالیات بر ارزش افزوده»)؛ نگه‌داری مقدار در سفارش و UI.

## پیشنهاد اقدام برای FrontOffice/BFF (ترتیب توصیه‌شده)
۱) صفحه «عضویت باشگاه» + داشبورد کیف‌پول/کمیسیون/فیچرها (یکپارچه با نمودار تعادل هفتگی و تاریخچه پرداخت کمیسیون).  
۲) راه‌اندازی پرداخت تراکنش و خطایابی: مسیر پرداخت، صفحه نتیجه، Retry/Verify، بازپرداخت به کیف‌پول.  
۳) تکمیل سبد/Checkout: Merge سبد مهمان، پرداخت ترکیبی، نمایش VAT و تفکیک فروشگاه باشگاه.  
۴) تاریخچه مالی و برداشت: لیست ChangeLog، درخواست/پیگیری برداشت، قوانین سقف/صف تأیید.  
۵) اعلان‌ها و قراردادها: تنظیمات Opt-in اعلان، اجبار امضای نسخه جدید قرارداد پیش از دسترسی به بخش‌های مالی.  

## وضعیت فعلی FrontOffice.BFF (گت‌وی سمت مشتری)
- مستندات موجود: فقط `FrontOffice.BFF/README.md` (خالی) و `docs/CMS.sql`/`model.ndm2` (ساختار دیتابیس CMS). هیچ API یا هندلر مستند نشده است.
- نتیجه: پوشش قابلیت‌ها در BFF نامشخص؛ فرض پیش‌فرض «پیاده‌سازی نشده/نیاز به بررسی» برای موارد زیر: عضویت باشگاه، کیف‌پول سه‌گانه و لاگ مالی، کمیسیون هفتگی و پرداخت/Withdraw، فروشگاه باشگاه و تخفیف، تراکنش درگاه (Create/Verify/Refund)، Merge سبد مهمان→ورود، VAT در Checkout، اعلان‌های Email/SMS/Push.
- **استثنا (موجود و پیاده‌سازی‌شده)**: جریان قرارداد در گت‌وی سمت مشتری و فرانت پیاده شده است؛ ثبت‌نام بدون امضای قرارداد متوقف می‌شود و پس از امضا Claim/Roll مربوط در توکن ست می‌شود.
- اقدام فوری: فهرست APIهای فعلی BFF را استخراج و مقابل نیازهای بالا چک کنیم؛ تا زمان تأیید، تغییری در CMS داده نمی‌شود و تمرکز بر طراحی/افزودن هندلرهای BFF و UI فرانت است.

## جدول پیشرفت قابلیت‌های مشتری (FrontOffice.BFF ↔ FrontOffice)
> درصدها براساس شواهد فعلی؛ در صورت کشف پیاده‌سازی بیشتر، مقدار به‌روزرسانی شود.

| قابلیت | وضعیت فعلی | درصد پیشرفت | اقدام بعدی (BFF) | اقدام بعدی (FrontOffice) |
| --- | --- | --- | --- | --- |
| قرارداد و امضا | پیاده‌سازی شده (امضا اجباری، Claim در توکن) | ۱۰۰٪ | بررسی صحت Claim در JWT و روتینگ پس از امضا | نمایش وضعیت امضا، ریدایرکت به امضا در اولین ورود بعد از تغییر نسخه |
| عضویت باشگاه | پیاده‌سازی نشده | ۰٪ | API شروع عضویت و فعال‌سازی باشگاه | صفحه عضویت و پرداخت ورود به باشگاه |
| خلاصه کیف‌پول‌ها (Balance/Discount/Network) | BFF/فرانت سه موجودی را نمایش می‌دهند؛ DiscountBalance هنوز از CMS برنمی‌گردد (در UI پیام «در انتظار اتصال CMS» نشان داده می‌شود، fallback صفر شد). | ۷۵٪ | **Blocked:** اضافه‌شدن DiscountBalance به سرویس CMS و مپ در BFF | نمایش مقدار واقعی پس از اتصال |
| جزئیات تراکنش کیف‌پول | BFF: `GetAllUserWalletChangeLog` پارامتر ReferenceId/IsIncrease دارد؛ فرانت فیلتر ارجاع/نوع تراکنش دارد. | ۷۵٪ | افزودن فیلتر تاریخ/Channel (در صورت نیاز) | بهبود نمایش برچسب نوع و Channel |
| فروشگاه باشگاه (خرید با DiscountBalance) | نامشخص/احتمالاً صفر | ۰٪ | API فهرست محصولات باشگاه + اعتبارسنجی موجودی تخفیف | تفکیک کاتالوگ باشگاه/عمومی، نمایش موجودی تخفیف در کارت و Checkout |
| شبکه باینری، تعادل و کمیسیون هفتگی | نامشخص/احتمالاً صفر | ۰٪ | API گزارش تعادل هفته، استخر، پرداخت کمیسیون و Withdraw | داشبورد شبکه/کمیسیون، نمودار تعادل، درخواست برداشت |
| تراکنش درگاه (Create/Verify/Refund) | PaymentRequest/PaymentVerification در BFF و Checkout فرانت پیاده شده؛ Refund دیده نشد. | ۶۰٪ | افزودن Refund و همگام‌سازی وضعیت سفارش/کیف‌پول | نمایش وضعیت پرداخت و مسیر Retry/Verify در UI |
| VAT در سفارش | نامشخص/احتمالاً صفر | ۰٪ | افزودن فیلد VAT به DTO/پاسخ سفارش | نمایش خط VAT در Checkout و فاکتور |
| برداشت/Settlement از کیف‌پول شبکه | BFF: `WithdrawBalance` به `RequestWithdrawal` و `GetWithdrawalSettings` (MinWithdrawalAmount از CMS) متصل؛ `GetUserWithdrawals` فعال. فرانت: فرم برداشت با حداقل مبلغ دینامیک، مپ وضعیت/روش، فیلتر وضعیت، نمایش پیام خطای CMS و لیست درخواست‌ها. | ۹۵٪ | همگام‌سازی ترجمه وضعیت/روش در همه صفحات | — |
| اعلان‌ها (Email/SMS/Push) | نامشخص/احتمالاً صفر | ۰٪ | API Opt-in/Opt-out و تریگر اعلان‌های کلیدی | تنظیمات اعلان در پروفایل، نمایش وضعیت ارسال |
| آدرس‌ها | CRUD آدرس در BFF و فرانت موجود است. | ۸۰٪ | بررسی ولیدیشن/کشورها و پیش‌فرض | بهبود UX انتخاب آدرس پیش‌فرض و پیام خطا |
| ثبت‌نام/OTP/دعوت | OTP و Verify در BFF و فرانت موجود؛ ReferralCode در پروفایل نمایش داده می‌شود. | ۸۰٪ | سناریوهای خطا و RateLimit OTP | بهبود متن راهنما و تجربه اشتراک‌گذاری کد دعوت |
| سفارش و تاریخچه | Create/Submit/Update/Delete و فیلتر در BFF موجود؛ فرانت سفارش و Checkout دارد، Refund دیده نشد. | ۷۰٪ | افزودن Refund/Cancellation و فیلد VAT | نمایش تاریخچه سفارش با وضعیت ارسال و کد رهگیری |
| درخت شبکه (نمایش اعضا) | کامپوننت OrganizationChart در فرانت با داده‌ی User/GetAllUserByFilter؛ بدون تعادل/امتیاز. | ۳۰٪ | API داده شبکه/تعادل از CMS (درخت باینری) | نمایش درخت با امتیاز، تعداد تعادل و Carryover |

### نکات مربوط به کیف‌پول و برداشت
- CMS: ماژول کیف‌پول و Withdrawal پیاده‌سازی شده (Commands: `RequestWithdrawal`, `ProcessWithdrawal`, History، MinWithdrawalAmount، حالت Cash/Diamond). می‌توانیم مستقیماً از gRPC/HTTP آن در BFF استفاده کنیم.
- FrontOffice: کارت کیف‌پول و صفحه جزئیات لاگ موجود است؛ نیاز به نمایش کیف تخفیف، بهبود UI، فیلترها و اضافه کردن جریان برداشت از موجودی شبکه. برداشت فعلاً تنها اکشن عملی روی موجودی شبکه است.
- اقدام ریز:
  1) BFF: تکمیل `GetUserWallet` با DiscountBalance، افزودن فیلتر به `GetAllUserWalletChangeLog`، پیاده‌سازی `WithdrawBalance` با CMS RequestWithdrawal + ولیدیشن MinWithdrawalAmount/IBAN.
  2) Front: به‌روزرسانی کارت کیف‌پول با سه کیف و توضیح کاربرد، لینک به برداشت برای NetworkBalance، فیلتر/مرتب‌سازی لاگ، نمایش مبلغ تغییر و Reference/Type.
  3) تجربه کاربری برداشت: پیام خطاهای Withdrawal (کمتر از حداقل مبلغ، درخواست در صف) و نمایش وضعیت‌های Pending/Approved/Rejected در UI.

### کشفیات جدید (ویژگی‌های مشتری در CMS که باید به BFF/فرانت برسد)
- **پروفایل/OTP/ثبت‌نام**: جریان OTP و ثبت‌نام، ذخیره کد ملی/نام/موبایل (`User`, `OtpToken`) و Claim `IsSignMainContract` در JWT پس از امضا.
- **آدرس‌ها**: `UserAddress` با پیش‌فرض برای سفارش‌ها؛ در فرانت پیاده است، نیاز به بهبود UX.
- **سبد/سفارش/پرداخت**: `UserCarts`, `UserOrder`, `Transactions` و PaymentRequest/Verification در BFF/فرانت موجود؛ Refund و VAT پوشش داده نشده.
- **شبکه و کمیسیون**: Network/Commission/WeeklyPool در CMS (باینری، Carryover، سقف ۳۰۰)؛ فرانت فقط درخت ساده بدون تعادل/امتیاز دارد.
- **باشگاه و کیف تخفیف**: ClubMembership, ClubFeature, DiscountBalance تعریف شده؛ هنوز Endpoint/UI ندارد.
- **برداشت کمیسیون/کیف شبکه**: `RequestWithdrawal/ProcessWithdrawal` در CMS؛ در BFF وصل شد ولی UI و استعلام وضعیت هنوز نداریم.
- **اعلان‌ها (Email/SMS)**: پیکربندی و ارسال در CMS آماده؛ Opt-in/Opt-out و نمایش وضعیت ارسال در فرانت پیاده نشده.
- **قرارداد**: AcceptContract در BFF/فرانت فعال و توکن جدید پس از امضا صادر می‌شود.

</div>

---

## 📊 تحلیل جامع: شکاف‌های پیاده‌سازی در FrontOffice/FrontOffice.BFF

> **تاریخ تحلیل**: 2024-12-01  
> **روش تحلیل**: بررسی عمیق ساختار دایرکتوری‌های CMS/Application در مقابل FrontOffice.BFF/Application  
> **یافته کلیدی**: از 27 ماژول CMS، تنها 10 ماژول در BFF پیاده‌سازی شده. **4 ماژول کلیدی مشتری‌محور کاملاً غایب هستند.**

### 📌 خلاصه اجرایی
- **CMS Modules**: 27 ماژول (13 ماژول مرتبط با مشتری)
- **FrontOffice.BFF Modules**: 10 ماژول (فقط 70% از نیازهای مشتری)
- **Missing Modules**: 4 ماژول حیاتی (ClubMembership, NetworkMembership, Commission, DayaLoan)
- **Partial Modules**: 3 ماژول با پیاده‌سازی ناقص (UserWallet, UserWalletChangeLog, Contract)

---

### 🔴 ماژول‌های کاملاً غایب (Critical Gap)

#### 1️⃣ ClubMembershipCQ - باشگاه مشتریان
**📍 مسیر**: `CMS/src/CMSMicroservice.Application/ClubMembershipCQ/`  
**❌ وضعیت**: هیچ معادلی در BFF وجود ندارد

**Commands در CMS:**
- `ActivateClubMembershipCommand` - فعال‌سازی عضویت (پرداخت 56M + شارژ کیف‌پول‌ها)
- `DeactivateClubMembershipCommand` - غیرفعال کردن عضویت
- `UpdateClubMembershipCommand` - به‌روزرسانی جزئیات

**Queries در CMS:**
- `GetClubMembershipStatusQuery` - وضعیت و فیچرهای فعال
- `GetAllClubMembershipsQuery` - لیست عضویت‌ها (Admin)
- `GetClubMembershipHistoryQuery` - تاریخچه تغییرات

**💥 تأثیر بر کاربر:**
- ❌ عدم امکان عضویت در باشگاه
- ❌ عدم دسترسی به فروشگاه تخفیفی
- ❌ عدم نمایش فیچرها و امتیازات باشگاه

**📋 اقدام مورد نیاز:**
```
BFF: ایجاد ClubMembershipCQ + 6 Handler + gRPC Client
UI:  ClubMembershipPage.razor + نمایش وضعیت در داشبورد
```

---

#### 2️⃣ NetworkMembershipCQ - شبکه باینری
**📍 مسیر**: `CMS/src/CMSMicroservice.Application/NetworkMembershipCQ/`  
**❌ وضعیت**: هیچ معادلی در BFF وجود ندارد (UI درخت دارد اما بدون داده واقعی)

**Commands در CMS:**
- `JoinNetworkCommand` - ثبت در شبکه باینری (SponsorId, ParentId, Position)
- `MoveInNetworkCommand` - جابجایی در درخت (Admin)
- `RemoveFromNetworkCommand` - حذف از شبکه (Admin)

**Queries در CMS:**
- `GetNetworkTreeQuery` - درخت باینری با MaxDepth (1-10)
- `GetUserNetworkPositionQuery` - موقعیت + آمار (Parent, Children, Total)
- `GetNetworkMembershipHistoryQuery` - تاریخچه تغییرات

**💥 تأثیر بر کاربر:**
- ⚠️ UI درخت موجود اما با Mock data
- ❌ عدم نمایش امتیازات و تعادل پاها
- ❌ عدم امکان دعوت افراد به شبکه

**📋 اقدام مورد نیاز:**
```
BFF: ایجاد NetworkMembershipCQ + 6 Handler + gRPC Client
UI:  به‌روزرسانی OrganizationChart.razor با داده واقعی + نمایش امتیاز
```

---

#### 3️⃣ CommissionCQ - کمیسیون هفتگی و برداشت
**📍 مسیر**: `CMS/src/CMSMicroservice.Application/CommissionCQ/`  
**❌ وضعیت**: BFF دارای `WithdrawBalance` اما **Handler خالی است**

**Commands در CMS:**
- `RequestWithdrawalCommand` - درخواست برداشت (Cash/Diamond)
- `ProcessWithdrawalCommand` - تایید/رد توسط ادمین

**Queries در CMS:**
- `GetWeeklyCommissionPoolQuery` - اطلاعات استخر (TotalPool, ValuePerPoint)
- `GetUserCommissionPayoutsQuery` - تاریخچه پرداخت‌ها (Pending→Paid→Withdrawn)
- `GetUserWeeklyBalancesQuery` - تعادل هفتگی (Left/Right Volume, Carryover, سقف 300)
- `GetAllWeeklyPoolsQuery` - تاریخچه استخرها (Admin)
- `GetWithdrawalRequestsQuery` - لیست درخواست‌های برداشت (Admin)

**💥 تأثیر بر کاربر:**
- ❌ عدم نمایش کمیسیون هفتگی
- ❌ عدم امکان درخواست برداشت (Handler خالی)
- ❌ عدم پیگیری وضعیت برداشت‌ها

**📋 اقدام مورد نیاز:**
```
BFF: ایجاد CommissionCQ + تکمیل WithdrawBalance + 5 Query + gRPC Client
UI:  CommissionDashboardPage.razor + WithdrawalRequestPage.razor + WeeklyBalanceChart.razor
```

---

#### 4️⃣ DayaLoanCQ - وام دایا (Phase 11 - جدید)
**📍 مسیر**: `CMS/src/CMSMicroservice.Application/DayaLoanCQ/`  
**❌ وضعیت**: هیچ معادلی در BFF وجود ندارد (تازه در CMS پیاده شده)

**Commands در CMS:**
- `ProcessDayaLoanApprovalCommand` - شارژ 3 کیف‌پول (168M تومان)
- `CheckDayaLoanStatusCommand` - استعلام از API دایا

**💥 تأثیر بر کاربر:**
- ❌ عدم نمایش وضعیت وام
- ❌ عدم امکان پیگیری اعتبار دریافتی

**📋 اقدام مورد نیاز:**
```
BFF: ایجاد DayaLoanCQ + 2 Handler + Mock API Client
UI:  DayaLoanStatusPage.razor + نمایش ContractNumber و تاریخ دریافت
```

---

### ⚠️ ماژول‌های پیاده‌سازی ناقص

#### 5️⃣ UserWalletChangeLogCQ - تاریخچه مالی
**وضعیت**: BFF دارد `GetAllUserWalletChangeLog` اما **بدون فیلتر**

**گپ:**
- ❌ فیلتر نوع تراکنش (Deposit, Withdraw, Purchase, Refund)
- ❌ فیلتر بازه زمانی
- ❌ جستجوی ReferenceId
- ❌ Query برای جزئیات تراکنش خاص (`GetUserWalletChangeLogQuery`)

**📋 اقدام:**
```
BFF: افزودن پارامترهای فیلتر به Handler موجود
UI:  افزودن فیلتر/جستجو به WalletDetailsPage.razor
```

---

#### 6️⃣ UserWalletCQ - کیف‌پول‌ها
**وضعیت**: BFF دارد `GetUserWallet` اما **بدون DiscountBalance در DTO**

**گپ:**
- ⚠️ Response فقط Balance + NetworkBalance برمی‌گرداند
- ❌ DiscountBalance نمایش داده نمی‌شود

**📋 اقدام:**
```
BFF: افزودن DiscountBalance به GetUserWallet Response DTO
UI:  نمایش کیف تخفیف در WalletCard.razor
```

---

### 📊 آمار نهایی شکاف

| ماژول CMS | Commands | Queries | BFF Status | UI Status | Gap % |
|-----------|----------|---------|------------|-----------|-------|
| ClubMembershipCQ | 3 | 3 | ❌ None | ❌ None | **100%** |
| NetworkMembershipCQ | 3 | 3 | ❌ None | ⚠️ Mock | **90%** |
| CommissionCQ | 2 | 5 | ⚠️ Empty Handler | ❌ None | **100%** |
| DayaLoanCQ | 2 | 0 | ❌ None | ❌ None | **100%** |
| UserWalletChangeLogCQ | 0 | 2 | ⚠️ No Filter | ⚠️ No Filter | **40%** |
| UserWalletCQ | 1 | 2 | ⚠️ Missing Field | ⚠️ Missing | **30%** |
| **TOTAL** | **11** | **15** | **10/27 Modules** | - | **63% Missing** |

**نتیجه‌گیری**: از 26 قابلیت (Commands/Queries) مورد نیاز مشتری، **16 قابلیت (62%) کاملاً غایب** و **4 قابلیت (15%) ناقص** هستند.

---

### 🎯 اولویت‌بندی توسعه (برای Developer بعدی)

#### 🔴 فاز 1 (Critical - 2 هفته):
1. **CommissionCQ** - کمیسیون و برداشت
   - [ ] BFF: 2 Commands + 5 Queries + gRPC Client
   - [ ] UI: CommissionDashboard + WithdrawalRequest + WeeklyBalanceChart
   - ⏱️ تخمین: 5 روز کاری

2. **ClubMembershipCQ** - عضویت باشگاه
   - [ ] BFF: 3 Commands + 3 Queries + gRPC Client
   - [ ] UI: ClubMembershipPage + Profile widgets
   - ⏱️ تخمین: 4 روز کاری

3. **NetworkMembershipCQ** - شبکه باینری
   - [ ] BFF: 3 Commands + 3 Queries + gRPC Client
   - [ ] UI: OrganizationChart update + Position page
   - ⏱️ تخمین: 5 روز کاری

---

#### 🟡 فاز 2 (Important - 1 هفته):
4. **UserWalletCQ Enhancement** - کیف تخفیف
   - [ ] BFF: Add DiscountBalance to DTO
   - [ ] UI: Display in WalletCard
   - ⏱️ تخمین: 1 روز کاری

5. **UserWalletChangeLogCQ Enhancement** - فیلتر تراکنش‌ها
   - [ ] BFF: Add filter params (Type, DateRange, ReferenceId)
   - [ ] UI: Filter controls in WalletDetailsPage
   - ⏱️ تخمین: 2 روز کاری

6. **DayaLoanCQ** - وام دایا
   - [ ] BFF: 2 Commands + Mock API Client
   - [ ] UI: DayaLoanStatusPage
   - ⏱️ تخمین: 2 روز کاری

---

#### 🟢 فاز 3 (Nice to Have - 3 روز):
7. **OtpTokenCQ Enhancement** - RateLimit
   - [ ] BFF: Add middleware (5 req/10min per IP)
   - ⏱️ تخمین: 1 روز کاری

8. **TransactionsCQ Enhancement** - Refund & VAT
   - [ ] BFF: RefundTransaction Command + VAT fields
   - [ ] UI: Refund button + VAT display
   - ⏱️ تخمین: 2 روز کاری

---

### 📋 چک‌لیست کامل (Copy-Paste Ready)

#### FrontOffice.BFF:
```csharp
// ماژول‌های جدید (از صفر)
[ ] Create /Application/ClubMembershipCQ/
    [ ] Commands/ActivateClubMembership.cs + Handler
    [ ] Queries/GetClubMembershipStatus.cs + Handler
    [ ] DTOs/ClubMembershipDto.cs

[ ] Create /Application/NetworkMembershipCQ/
    [ ] Commands/JoinNetwork.cs + Handler
    [ ] Queries/GetNetworkTree.cs + Handler (MaxDepth: 1-10)
    [ ] Queries/GetUserNetworkPosition.cs + Handler
    [ ] DTOs/NetworkTreeDto.cs, NetworkPositionDto.cs

[ ] Create /Application/CommissionCQ/
    [ ] Commands/RequestWithdrawal.cs (تکمیل Handler خالی موجود)
    [ ] Queries/GetUserCommissionPayouts.cs + Handler
    [ ] Queries/GetUserWeeklyBalances.cs + Handler
    [ ] Queries/GetWeeklyCommissionPool.cs + Handler
    [ ] DTOs/CommissionPayoutDto.cs, WeeklyBalanceDto.cs

[ ] Create /Application/DayaLoanCQ/
    [ ] Commands/CheckDayaLoanStatus.cs + Handler
    [ ] Services/MockDayaApiClient.cs
    [ ] DTOs/DayaLoanInfoDto.cs

// به‌روزرسانی ماژول‌های موجود
[ ] Update /Application/UserWalletCQ/
    [ ] DTOs/UserWalletDto.cs → Add: public decimal DiscountBalance { get; set; }
    [ ] Handlers/GetUserWalletQueryHandler.cs → Map DiscountBalance from CMS

[ ] Update /Application/UserWalletCQ/ (ChangeLog)
    [ ] Queries/GetAllUserWalletChangeLog.cs → Add params:
        - WalletChangeType? Type
        - DateTime? DateFrom, DateTime? DateTo
        - string? ReferenceId
    [ ] Handler → Apply filters in CMS gRPC call

// gRPC Registration
[ ] Update /Infrastructure/ConfigureGrpcServices.cs
    builder.Services.AddGrpcClient<ClubMembershipServiceClient>(...)
    builder.Services.AddGrpcClient<NetworkMembershipServiceClient>(...)
    builder.Services.AddGrpcClient<CommissionServiceClient>(...)

// Security
[ ] Create /Infrastructure/Middleware/RateLimitMiddleware.cs
    - Apply to: /api/user/otp endpoints
    - Limit: 5 requests per 10 minutes per IP
```

#### FrontOffice (UI):
```razor
// صفحات جدید
[ ] Create /Pages/ClubMembership/Index.razor
    - نمایش وضعیت عضویت (Active/Inactive)
    - دکمه فعال‌سازی (هدایت به درگاه پرداخت 56M)
    - لیست فیچرهای فعال (Badge system)

[ ] Create /Pages/Commission/Dashboard.razor
    - نمایش استخر هفته (TotalPool, ValuePerPoint)
    - نمودار تعادل (Left vs Right Volume)
    - تاریخچه پرداخت‌ها با Badge وضعیت

[ ] Create /Pages/Commission/Withdrawal.razor
    - فرم برداشت (IBAN, Amount, Method: Cash/Diamond)
    - ولیدیشن MinWithdrawalAmount (100,000 تومان)
    - نمایش پیام خطا (کمتر از حداقل، صف تایید)

[ ] Create /Pages/Network/Tree.razor
    - به‌روزرسانی OrganizationChart.razor
    - Slider MaxDepth (1-10)
    - نمایش امتیاز در هر Node
    - رنگ‌بندی بر اساس تعادل (سبز=متعادل، قرمز=نامتعادل)
    - Tooltip: Parent, Children count, Carryover

[ ] Create /Pages/DayaLoan/Status.razor
    - نمایش ContractNumber
    - تاریخ دریافت اعتبار
    - مبالغ شارژ شده (3×56M)

// کامپوننت‌های جدید
[ ] Update /Components/Wallet/WalletCard.razor
    <MudCard>
      <MudText>موجودی کیف پول: {Balance:N0} ریال</MudText>
      <MudText>موجودی شبکه: {NetworkBalance:N0} ریال</MudText>
      <MudText Color="Color.Success">موجودی تخفیف: {DiscountBalance:N0} ریال</MudText>
      <MudButton Href="/wallet/withdraw" Disabled="@(NetworkBalance < 100000)">
        درخواست برداشت
      </MudButton>
    </MudCard>

[ ] Create /Components/Commission/WeeklyBalanceChart.razor
    - نمودار میله‌ای Left/Right Volume
    - نمایش WeakerLeg (کمترین حجم)
    - نمایش Carryover و سقف 300

// به‌روزرسانی موجودی
[ ] Update /Pages/Wallet/DetailsPage.razor
    [ ] Add filter controls:
        - نوع تراکنش (Deposit, Withdraw, Purchase, Refund)
        - بازه زمانی (DatePicker: From/To)
        - جستجوی ReferenceId (TextBox)
    [ ] نمایش ChangeValue به جای CurrentBalance
    [ ] پیجینیشن

[ ] Update /Components/Layout/NavMenu.razor
    <MudNavLink Href="/clubmembership" Icon="@Icons.Material.Filled.CardMembership">
      باشگاه مشتریان
    </MudNavLink>
    <MudNavLink Href="/commission/dashboard" Icon="@Icons.Material.Filled.Payments">
      کمیسیون و برداشت
    </MudNavLink>
    <MudNavLink Href="/network/tree" Icon="@Icons.Material.Filled.AccountTree">
      شبکه من
    </MudNavLink>
```

---

### 🚨 نکات حیاتی (Critical Notes)

#### ⚠️ امنیت:
```
1. WithdrawBalance:
   - MinAmount: 100,000 ریال (CMS config)
   - IBAN: IR + 24 digits validation
   - Daily limit per user: Check CMS setting

2. JoinNetwork:
   - IsDescendant recursive check (prevent circular ref)
   - Position validation (Left/Right must be empty)
   - SponsorId must be active club member

3. OTP RateLimit:
   - 5 requests / 10 min per IP
   - Redis/InMemory cache
```

#### 💡 UI/UX:
```
1. WalletCard: 3 کیف‌پول با رنگ متفاوت
   - Balance: آبی (خرید عمومی)
   - NetworkBalance: سبز (برداشت Cash/Diamond)
   - DiscountBalance: زرد (فروشگاه باشگاه)

2. CommissionDashboard:
   - Badge colors: Pending=زرد, Calculated=آبی, Paid=سبز, Withdrawn=خاکستری
   - Carryover info tooltip
   - سقف 300 Balance در هفته

3. NetworkTree:
   - MaxDepth default: 3
   - Load on demand برای عمق بیشتر
   - Tooltip با Shift+Click
```

#### ❌ خطاهای رایج:
```
1. BFF: Handler خالی
   ❌ FrontOffice.BFF/Application/UserWalletCQ/Commands/WithdrawBalanceCommandHandler.cs
   ✅ Fix: Call CMS.RequestWithdrawal via gRPC

2. UI: Mock data
   ❌ FrontOffice/Pages/Network/OrganizationChart.razor (hardcoded nodes)
   ✅ Fix: @inject NetworkService → await GetTreeAsync()

3. DTO: Missing field
   ❌ UserWalletDto missing DiscountBalance
   ✅ Fix: Add property + map in Handler
```

</div>


---

## 📊 تحلیل کامل شکاف‌های پیاده‌سازی (Gap Analysis Report)
> **تاریخ تحلیل**: 2024-12-01  
> **روش**: مقایسه ماژول به ماژول CMS vs FrontOffice.BFF vs FrontOffice

### 📈 آمار کلی

| مجموع | CMS Modules | BFF Modules | شکاف (Missing) | نرخ پوشش |
|-------|-------------|-------------|----------------|----------|
| **کل ماژول‌ها** | 26 ماژول | 9 ماژول | 17 ماژول | 35% |
| **ماژول‌های مشتری‌محور** | 15 ماژول | 7 ماژول | 8 ماژول | 47% |
| **ماژول‌های حیاتی غایب** | - | - | 4 ماژول | 0% |

---

### 🔴 CRITICAL: ماژول‌های کاملاً غایب (0% پیاده‌سازی)

#### 1️⃣ ClubMembershipCQ - باشگاه مشتریان
**Commands در CMS (موجود):**
- `ActivateClubMembership` - فعال‌سازی عضویت (پرداخت 56M)
- `DeactivateClubMembership` - غیرفعال کردن
- `AssignClubFeature` - اختصاص فیچر (Trial/VIP)

**Queries در CMS (موجود):**
- `GetClubMembership` - دریافت وضعیت عضویت کاربر
- `GetAllClubMemberships` - لیست کل اعضا (Admin)
- `GetClubMembershipHistory` - تاریخچه تغییرات
- `GetClubStatistics` - آمار کلی باشگاه

**❌ در FrontOffice.BFF**: هیچ چیز موجود نیست  
**❌ در FrontOffice UI**: هیچ صفحه‌ای برای باشگاه وجود ندارد

**📋 Task های مورد نیاز:**
```
[ ] FrontOffice.BFF:
    [ ] Create ClubMembershipCQ/Commands/ActivateClubMembership/
    [ ] Create ClubMembershipCQ/Queries/GetMyClubMembership/
    [ ] Create ClubMembershipCQ/Queries/GetClubFeatures/
    
[ ] FrontOffice UI:
    [ ] Create /Pages/Club/MembershipPage.razor
        - نمایش وضعیت عضویت (Active/Inactive/Trial)
        - دکمه فعال‌سازی (پرداخت 56M)
        - لیست فیچرهای باشگاه
    [ ] Create /Pages/Club/FeaturesPage.razor
        - لیست فیچرهای Trial vs VIP
        - Badge امتیاز برای هر فیچر
    [ ] Create /Components/Club/ActivationButton.razor
        - فرم پرداخت
        - اتصال به درگاه
```

**💰 بیزینس اثر:**
- کاربر نمی‌تواند عضو باشگاه شود
- 56M تومان در Balance/Discount شارژ نمی‌شود
- دسترسی به فروشگاه تخفیفی ندارد

---

#### 2️⃣ NetworkMembershipCQ - شبکه باینری
**Commands در CMS (موجود):**
- `JoinNetwork` - عضویت در شبکه (Parent/Position)
- `MoveInNetwork` - جابجایی موقعیت
- `RemoveFromNetwork` - حذف از شبکه

**Queries در CMS (موجود):**
- `GetNetworkTree` - دریافت درخت باینری (MaxDepth: 1-10)
- `GetUserNetworkPosition` - موقعیت کاربر در درخت
- `GetNetworkMembershipHistory` - تاریخچه جابجایی‌ها
- `GetNetworkStatistics` - آمار شبکه (تعداد چپ/راست/کل)

**❌ در FrontOffice.BFF**: هیچ چیز موجود نیست  
**⚠️ در FrontOffice UI**: فقط OrganizationChart با داده Mock

**📋 Task های مورد نیاز:**
```
[ ] FrontOffice.BFF:
    [ ] Create NetworkMembershipCQ/Commands/JoinNetwork/
    [ ] Create NetworkMembershipCQ/Queries/GetNetworkTree/
    [ ] Create NetworkMembershipCQ/Queries/GetMyNetworkPosition/
    [ ] Create NetworkMembershipCQ/Queries/GetNetworkStatistics/
    
[ ] FrontOffice UI:
    [ ] Update /Pages/Network/OrganizationChart.razor
        - حذف Mock data
        - فراخوانی GetNetworkTree از BFF
        - نمایش MaxDepth selector (1-10)
        - Lazy loading برای سطوح پایین‌تر
    [ ] Create /Pages/Network/JoinPage.razor
        - فرم انتخاب Parent
        - انتخاب Position (Left/Right)
        - نمایش پیش‌نمایش موقعیت
    [ ] Create /Pages/Network/StatsPage.razor
        - تعداد اعضای چپ/راست
        - عمق درخت
        - آخرین عضو جدید
```

**💰 بیزینس اثر:**
- کاربر نمی‌تواند زیرمجموعه بگیرد
- درخت شبکه واقعی نمایش داده نمی‌شود
- محاسبه کمیسیون باینری کار نمی‌کند

---

#### 3️⃣ CommissionCQ - کمیسیون هفتگی و برداشت
**Commands در CMS (موجود):**
- `RequestWithdrawal` - درخواست برداشت (Cash/Diamond/IBAN)
- `ApproveWithdrawal` - تایید برداشت (Admin)
- `RejectWithdrawal` - رد برداشت (Admin)
- `ProcessWithdrawal` - پردازش برداشت
- `CalculateWeeklyBalances` - محاسبه تعادل هفتگی
- `CalculateWeeklyCommissionPool` - محاسبه استخر
- `ProcessUserPayouts` - توزیع کمیسیون
- `TriggerWeeklyCalculation` - اجرای دستی Worker

**Queries در CMS (موجود):**
- `GetUserCommissionPayouts` - لیست پرداخت‌های کمیسیون کاربر
- `GetCommissionPayoutHistory` - تاریخچه تغییرات
- `GetUserWeeklyBalances` - تعادل هفتگی (Left/Right/Weaker)
- `GetWeeklyCommissionPool` - اطلاعات استخر هفته
- `GetAllWeeklyPools` - تمام استخرها (Admin)
- `GetWithdrawalRequests` - درخواست‌های برداشت
- `GetWorkerStatus` - وضعیت Worker
- `GetWorkerExecutionLogs` - لاگ اجرای Worker

**⚠️ در FrontOffice.BFF**: فقط یک Handler خالی `WithdrawBalance`  
**❌ در FrontOffice UI**: هیچ چیز موجود نیست

**📋 Task های مورد نیاز:**
```
[ ] FrontOffice.BFF:
    [ ] Complete UserWalletCQ/Commands/WithdrawBalance/
        - فراخوانی CMS.RequestWithdrawal
        - ولیدیشن MinWithdrawalAmount
        - چک IBAN format
    [ ] Create CommissionCQ/Queries/GetMyCommissionPayouts/
    [ ] Create CommissionCQ/Queries/GetMyWeeklyBalances/
    [ ] Create CommissionCQ/Queries/GetWithdrawalHistory/
    
[ ] FrontOffice UI:
    [ ] Create /Pages/Commission/DashboardPage.razor
        - کارت استخر هفته (TotalPool, BalanceValue)
        - کارت امتیازات من (LesserLegPoints)
        - پیش‌بینی کمیسیون این هفته
    [ ] Create /Pages/Commission/HistoryPage.razor
        - جدول پرداخت‌های گذشته
        - فیلتر Status (Pending/Paid/Withdrawn)
        - نمودار روند کمیسیون
    [ ] Create /Pages/Commission/WithdrawPage.razor
        - فرم برداشت (Amount, Method, IBAN)
        - نمایش MinWithdrawalAmount
        - نمایش موجودی قابل برداشت
        - تاریخچه برداشت‌ها
    [ ] Create /Pages/Commission/WeeklyBalancePage.razor
        - تعادل چپ/راست
        - Carryover از هفته قبل
        - سقف 300 Balance
        - نمودار خطی رشد هفتگی
```

**💰 بیزینس اثر:**
- کاربر نمی‌تواند کمیسیون خود را ببیند
- برداشت از NetworkBalance کار نمی‌کند
- تعادل هفتگی و Carryover نامشخص است

---

#### 4️⃣ DayaLoanCQ - وام دایا
**Commands در CMS (موجود - جدید):**
- `CheckDayaLoanStatus` - استعلام وضعیت وام
- `ProcessDayaLoanApproval` - پردازش تایید وام (شارژ 168M)

**❌ در FrontOffice.BFF**: هیچ چیز موجود نیست  
**❌ در FrontOffice UI**: هیچ چیز موجود نیست

**📋 Task های مورد نیاز:**
```
[ ] FrontOffice.BFF:
    [ ] Create DayaLoanCQ/Queries/GetMyDayaLoanStatus/
    [ ] Create DayaLoanCQ/Commands/RequestDayaLoanCheck/ (optional)
    
[ ] FrontOffice UI:
    [ ] Create /Pages/DayaLoan/StatusPage.razor
        - نمایش وضعیت وام (PendingReceive/Received/Rejected)
        - شماره قرارداد (ContractNumber)
        - تاریخ آخرین بررسی
    [ ] Create /Components/DayaLoan/StatusBadge.razor
        - Badge رنگی برای Status
```

**💰 بیزینس اثر:**
- کاربر نمی‌تواند وضعیت وام دایا خود را ببیند
- 168M شارژ کیف‌پول (56M×3) نامشخص است
- فقط Worker پس‌زمینه فعال است (بدون UI)

---

### 🟡 PARTIAL: ماژول‌های نیمه‌پیاده (50-80% تکمیل)

#### 5️⃣ UserWalletCQ - کیف‌پول
**✅ در BFF موجود:**
- `GetUserWallet` - دریافت موجودی
- `GetAllUserWalletChangeLog` - تاریخچه تراکنش‌ها

**❌ در BFF غایب:**
- `WithdrawBalance` Handler - خالی است و کار نمی‌کند

**⚠️ مشکلات موجود:**
- `GetUserWallet` Response فقط Balance و NetworkBalance دارد
- **DiscountBalance موجود نیست** (باید اضافه شود)
- `GetAllUserWalletChangeLog` فیلتر ندارد (نوع/بازه زمانی)

**📋 Task های مورد نیاز:**
```
[ ] FrontOffice.BFF:
    [ ] Update GetUserWallet Response DTO
        ✅ Balance (موجود)
        ✅ NetworkBalance (موجود)
        ❌ DiscountBalance (باید اضافه شود)
    [ ] Update GetAllUserWalletChangeLog
        - فیلتر نوع تراکنش (Deposit/Withdraw/Purchase)
        - فیلتر بازه زمانی (From/To)
        - فیلتر ReferenceId
    [ ] Fix WithdrawBalance Handler
        - Call CMS.RequestWithdrawal
        - Validation: MinAmount, IBAN
        
[ ] FrontOffice UI:
    [ ] Update /Pages/Wallet/WalletCard.razor
        ✅ Balance (موجود)
        ✅ NetworkBalance (موجود)
        ❌ DiscountBalance (باید اضافه شود - با رنگ زرد)
        - حذف داده Mock
    [ ] Update /Pages/Wallet/DetailsPage.razor
        - فیلترها (نوع/تاریخ/جستجو)
        - نمایش ChangeValue به جای CurrentBalance
        - Pagination
```

---

#### 6️⃣ UserCartsCQ / ShoppingCartCQ - سبد خرید
**✅ در BFF موجود (نام: ShopingCartCQ):**
- `AddNewUserCart` - افزودن به سبد
- `UpdateUserCart` - به‌روزرسانی تعداد

**❌ در BFF غایب:**
- `ClearCart` - پاک کردن کل سبد
- `DeleteUserCarts` - حذف یک آیتم
- `MergeGuestCart` - ادغام سبد مهمان→ورود

**📋 Task های مورد نیاز:**
```
[ ] FrontOffice.BFF:
    [ ] Create ShopingCartCQ/Commands/ClearCart/
    [ ] Create ShopingCartCQ/Commands/DeleteCartItem/
    [ ] Create ShopingCartCQ/Commands/MergeGuestCart/
        - Input: SessionId مهمان + UserId ورود
        - Logic: Merge duplicate products (sum quantities)
        
[ ] FrontOffice UI:
    [ ] Update /Pages/Cart/CartPage.razor
        - دکمه "پاک کردن سبد"
        - دکمه حذف آیتم (هر سطر)
    [ ] Implement Guest→Login merge
        - ذخیره SessionId در LocalStorage
        - POST به MergeGuestCart بعد از Login
        - نمایش پیام "x محصول از سبد قبلی شما اضافه شد"
```

---

#### 7️⃣ ContractCQ - قرارداد
**✅ در BFF موجود:**
- `AcceptContract` در UserCQ/Commands/

**❌ در BFF غایب:**
- `GetContract` - دریافت متن قرارداد
- `GetAllContracts` - لیست نسخه‌های قرارداد

**📋 Task های مورد نیاز:**
```
[ ] FrontOffice.BFF:
    [ ] Create ContractCQ/Queries/GetLatestContract/
    [ ] Create ContractCQ/Queries/GetMyContractHistory/
    
[ ] FrontOffice UI:
    [ ] Update /Pages/Auth/ContractPage.razor
        - دریافت متن قرارداد از API (حذف hardcode)
        - نمایش تاریخ آخرین نسخه
    [ ] Create /Pages/Profile/ContractHistoryPage.razor
        - لیست قراردادهای امضا شده
        - دانلود PDF
```

---

### ✅ COMPLETE: ماژول‌های کامل (80-100% تکمیل)

#### 8️⃣ UserCQ - پروفایل و احراز هویت
**✅ پیاده‌سازی کامل:**
- Login, Register, UpdateProfile
- ChangePassword, ForgotPassword
- GetUserByFilter
- AcceptContract (امضای قرارداد)

#### 9️⃣ UserAddressCQ - آدرس‌ها
**✅ پیاده‌سازی کامل:**
- CRUD آدرس
- SetDefault
- UI: AddressPage و AddressCard

#### 🔟 UserOrderCQ - سفارشات
**✅ پیاده‌سازی 70%:**
- Create, Update, Delete, GetById, GetByFilter
- ❌ غایب: Cancel, Refund, TrackingCode

---

### 📊 جدول خلاصه اولویت‌بندی

| اولویت | ماژول | درصد فعلی | Tasks باقی‌مانده | تخمین زمان |
|--------|-------|-----------|-------------------|-------------|
| 🔴 P0 | ClubMembershipCQ | 0% | 8 Handlers + 4 Pages | 2 هفته |
| 🔴 P0 | CommissionCQ | 10% | 12 Handlers + 6 Pages | 3 هفته |
| 🔴 P0 | NetworkMembershipCQ | 5% | 7 Handlers + 4 Pages | 2 هفته |
| 🟡 P1 | UserWalletCQ | 60% | 3 Handlers + 2 Pages | 1 هفته |
| 🟡 P1 | ShoppingCartCQ | 50% | 3 Handlers + UI updates | 1 هفته |
| 🟢 P2 | DayaLoanCQ | 0% | 2 Handlers + 1 Page | 3 روز |
| 🟢 P2 | ContractCQ | 80% | 2 Handlers + 1 Page | 2 روز |

**مجموع تخمین:** 9 هفته = 2 ماه (1 نفر Full-time)

---

### 🎯 خلاصه اجرایی برای توسعه‌دهنده

**وضعیت فعلی:**
- از 15 ماژول مشتری‌محور CMS، تنها 7 ماژول در BFF دارید
- 4 ماژول حیاتی (باشگاه، شبکه، کمیسیون، وام) کاملاً غایب
- 3 ماژول موجود (کیف‌پول، سبد، قرارداد) ناقص

**کارهایی که توسعه‌دهنده قبلی انجام نداد:**
1. ❌ هیچ Handler برای باشگاه (ClubMembership)
2. ❌ هیچ Handler برای شبکه (NetworkMembership)
3. ❌ هیچ Handler برای کمیسیون (Commission) به جز یک Handler خالی
4. ❌ هیچ Handler برای وام دایا (DayaLoan)
5. ⚠️ Handler کیف‌پول (UserWallet) ناقص - DiscountBalance غایب
6. ⚠️ Handler سبد (ShoppingCart) ناقص - ClearCart, Merge غایب
7. ⚠️ UI درخت شبکه (OrganizationChart) با داده Mock

**تسک‌های واقعی که باید از CMS به FrontOffice.BFF منتقل شوند:**
- ✅ 26 Command موجود در CMS که در BFF نیستند
- ✅ 24 Query موجود در CMS که در BFF نیستند
- ✅ 15+ صفحه UI که باید در FrontOffice ساخته شوند

**اولویت‌بندی توصیه شده:**
1. **Week 1-2**: ClubMembership - چون بدون این، کاربر نمی‌تواند عضو شود
2. **Week 3-5**: Commission + Withdrawal - چون کاربر نمی‌تواند پول خود را ببیند/برداشت کند
3. **Week 6-7**: NetworkMembership - چون درخت شبکه Mock است
4. **Week 8**: UserWallet completion - اضافه کردن DiscountBalance و فیلترها
5. **Week 9**: DayaLoan + ShoppingCart completion

این تحلیل نشان می‌دهد که **حداقل 50 روز کاری** (2 ماه) برای تکمیل نیاز است.

</div>

---

## 🌳 مرحله 3: راهنمای گام‌به‌گام - NetworkMembership (شبکه باینری)

### 📊 خلاصه ماژول

**هدف کسب‌وکار**: مشتری باید بتواند درخت شبکه باینری خود را ببیند (پدر، فرزند چپ، فرزند راست)، موقعیت خود را بررسی کند، و تاریخچه جابجایی‌ها را مشاهده نماید.

**اجزای موجود در CMS:**
- ✅ `NetworkMembership` Entity با BinaryTree structure (ParentId, LeftChildId, RightChildId)
- ✅ 3 Commands: JoinNetwork, MoveInNetwork, RemoveFromNetwork
- ✅ 4 Queries: GetNetworkTree, GetUserPosition, GetNetworkHistory, GetNetworkStatistics

**چیزهای غایب:**
- ❌ هیچ Handler در FrontOffice.BFF
- ❌ هیچ صفحه نمایش درخت در FrontOffice UI
- ❌ Component نمایش درخت باینری (Tree Visualization)

---

### 📝 STEP 1: بررسی CMS NetworkMembership

#### Task 1.1: بررسی Entity و Logic
```bash
cd /home/masoud/Apps/project/FourSat/CMS/src/

# 1. بررسی Entity
cat CMSMicroservice.Domain/Entities/NetworkMembership.cs
# چیزهایی که باید بفهمی:
#   - UserId: کاربر اصلی
#   - ParentId: کاربر بالایی در شبکه
#   - LeftChildId: فرزند چپ (nullable)
#   - RightChildId: فرزند راست (nullable)
#   - Position: Left/Right (موقعیت در شبکه پدر)
#   - JoinDate: تاریخ پیوستن

# 2. بررسی Query GetNetworkTree
cat CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQueryHandler.cs
# توجه کن به:
#   - Input: UserId (برای نمایش درخت از این کاربر به بعد)
#   - Depth: عمق درخت (چند لایه)
#   - Output: Recursive DTO (Parent + Left + Right با فیلدهای کامل)

# 3. بررسی DTO
cat CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/NetworkTreeNodeDto.cs
# Structure:
#   - UserId, UserFullName, UserMobile
#   - Position (Left/Right)
#   - JoinDate
#   - LeftChild (recursive NetworkTreeNodeDto?)
#   - RightChild (recursive NetworkTreeNodeDto?)
```

**Output Task 1.1:**
```
[ ] Entity NetworkMembership را خواندم
[ ] ساختار Recursive Tree را فهمیدم
[ ] GetNetworkTreeQueryHandler را بررسی کردم
```

#### Task 1.2: بررسی GetUserPosition Query
```bash
cat CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetUserPosition/GetUserPositionQueryHandler.cs
# این Query چه می‌دهد:
#   - Parent info: نام و موبایل پدر
#   - User Position: Left یا Right
#   - Left Child info (if exists)
#   - Right Child info (if exists)
#   - Total Depth: عمق کل درخت از این کاربر
```

---

### 📝 STEP 2: ایجاد BFF Module - NetworkMembershipCQ

#### Task 2.1: ساخت فولدرها
```bash
cd /home/masoud/Apps/project/FourSat/FrontOffice.BFF/src/FrontOffice.BFF.Application/

mkdir -p NetworkMembershipCQ/Queries/GetMyNetworkTree
mkdir -p NetworkMembershipCQ/Queries/GetMyNetworkPosition
mkdir -p NetworkMembershipCQ/Queries/GetMyNetworkHistory

tree NetworkMembershipCQ/
```

**Expected Output:**
```
NetworkMembershipCQ/
└── Queries/
    ├── GetMyNetworkTree/
    ├── GetMyNetworkPosition/
    └── GetMyNetworkHistory/
```

#### Task 2.2: Query #1 - GetMyNetworkTree (نمایش درخت)

**فایل 1: GetMyNetworkTreeQuery.cs**
```csharp
using MediatR;

namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;

/// <summary>
/// Query برای دریافت درخت شبکه کاربر جاری
/// </summary>
public record GetMyNetworkTreeQuery : IRequest<MyNetworkTreeResponseDto>
{
    /// <summary>
    /// عمق درخت (چند لایه زیرمجموعه نمایش داده شود)
    /// پیش‌فرض: 3 لایه
    /// </summary>
    public int Depth { get; init; } = 3;
}
```

**فایل 2: MyNetworkTreeResponseDto.cs**
```csharp
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;

/// <summary>
/// DTO مشتری‌محور برای نمایش درخت شبکه
/// </summary>
public class MyNetworkTreeResponseDto
{
    public NetworkNodeDto CurrentUser { get; set; }
    public int TotalNetworkSize { get; set; }      // تعداد کل افراد در شبکه
    public int DirectChildrenCount { get; set; }   // تعداد فرزندان مستقیم
    public string LastUpdatePersian { get; set; }  // آخرین به‌روزرسانی
}

/// <summary>
/// نود درخت (Recursive)
/// </summary>
public class NetworkNodeDto
{
    public long UserId { get; set; }
    public string FullName { get; set; }
    public string Mobile { get; set; }
    public string Position { get; set; }           // "Root" / "Left" / "Right"
    public string JoinDatePersian { get; set; }
    public bool HasLeftChild { get; set; }
    public bool HasRightChild { get; set; }
    
    // Recursive children
    public NetworkNodeDto LeftChild { get; set; }
    public NetworkNodeDto RightChild { get; set; }
    
    // UI Helper fields
    public string StatusBadge { get; set; }        // "فعال" / "غیرفعال"
    public string StatusColor { get; set; }        // "success" / "error"
}
```

**فایل 3: GetMyNetworkTreeQueryHandler.cs**
```csharp
using MediatR;
using FrontOffice.BFF.Application.Common.Interfaces;

namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;

public class GetMyNetworkTreeQueryHandler 
    : IRequestHandler<GetMyNetworkTreeQuery, MyNetworkTreeResponseDto>
{
    private readonly ICurrentUserService _currentUser;
    // TODO: private readonly NetworkMembershipServiceClient _cmsClient;
    
    public GetMyNetworkTreeQueryHandler(ICurrentUserService currentUser)
    {
        _currentUser = currentUser;
    }
    
    public async Task<MyNetworkTreeResponseDto> Handle(
        GetMyNetworkTreeQuery request, 
        CancellationToken cancellationToken)
    {
        var userId = _currentUser.UserId;
        
        // TODO: فراخوانی CMS
        // var cmsResult = await _cmsClient.GetNetworkTreeAsync(
        //     new GetNetworkTreeRequest { UserId = userId, Depth = request.Depth });
        
        // Mock Data برای تست UI
        return new MyNetworkTreeResponseDto
        {
            CurrentUser = new NetworkNodeDto
            {
                UserId = userId,
                FullName = "علی احمدی",
                Mobile = "09121234567",
                Position = "Root",
                JoinDatePersian = "1 آذر 1403",
                HasLeftChild = true,
                HasRightChild = true,
                StatusBadge = "فعال",
                StatusColor = "success",
                LeftChild = new NetworkNodeDto
                {
                    UserId = 101,
                    FullName = "رضا محمدی",
                    Mobile = "09129876543",
                    Position = "Left",
                    JoinDatePersian = "5 آذر 1403",
                    HasLeftChild = false,
                    HasRightChild = false,
                    StatusBadge = "فعال",
                    StatusColor = "success"
                },
                RightChild = new NetworkNodeDto
                {
                    UserId = 102,
                    FullName = "سارا کریمی",
                    Mobile = "09131111111",
                    Position = "Right",
                    JoinDatePersian = "10 آذر 1403",
                    HasLeftChild = false,
                    HasRightChild = false,
                    StatusBadge = "فعال",
                    StatusColor = "success"
                }
            },
            TotalNetworkSize = 3,
            DirectChildrenCount = 2,
            LastUpdatePersian = "15 آذر 1403"
        };
    }
}
```

**Checkpoint Task 2.2:**
```
[ ] 3 فایل ایجاد شدند
[ ] Recursive DTO به درستی تعریف شد
[ ] Mock tree data با 2 فرزند برمی‌گردد
```

#### Task 2.3: Query #2 - GetMyNetworkPosition (موقعیت من)

**فایل 1: GetMyNetworkPositionQuery.cs**
```csharp
using MediatR;

namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkPosition;

public record GetMyNetworkPositionQuery : IRequest<MyNetworkPositionResponseDto>
{
}
```

**فایل 2: MyNetworkPositionResponseDto.cs**
```csharp
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkPosition;

public class MyNetworkPositionResponseDto
{
    public bool HasParent { get; set; }
    public string ParentFullName { get; set; }
    public string ParentMobile { get; set; }
    public string MyPosition { get; set; }          // "چپ" / "راست" / "ریشه"
    public string MyPositionIcon { get; set; }      // "arrow_back" / "arrow_forward"
    
    public int NetworkLevel { get; set; }           // سطح در شبکه (1=ریشه, 2=فرزند, ...)
    public int TotalDownlineCount { get; set; }     // تعداد کل زیرمجموعه‌ها
    public string JoinDatePersian { get; set; }
}
```

**فایل 3: GetMyNetworkPositionQueryHandler.cs** (Mock Data)
```csharp
using MediatR;
using FrontOffice.BFF.Application.Common.Interfaces;

namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkPosition;

public class GetMyNetworkPositionQueryHandler 
    : IRequestHandler<GetMyNetworkPositionQuery, MyNetworkPositionResponseDto>
{
    private readonly ICurrentUserService _currentUser;
    
    public GetMyNetworkPositionQueryHandler(ICurrentUserService currentUser)
    {
        _currentUser = currentUser;
    }
    
    public async Task<MyNetworkPositionResponseDto> Handle(
        GetMyNetworkPositionQuery request, 
        CancellationToken cancellationToken)
    {
        // TODO: Call CMS
        return new MyNetworkPositionResponseDto
        {
            HasParent = true,
            ParentFullName = "حسن رضایی",
            ParentMobile = "09123456789",
            MyPosition = "چپ",
            MyPositionIcon = "arrow_back",
            NetworkLevel = 2,
            TotalDownlineCount = 5,
            JoinDatePersian = "1 آذر 1403"
        };
    }
}
```

---

### 📝 STEP 3: اضافه کردن Controller

**فایل: NetworkMembershipController.cs**
```csharp
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MediatR;
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkPosition;

namespace FrontOffice.BFF.WebApi.Controllers;

[Authorize]
[ApiController]
[Route("api/[controller]")]
public class NetworkMembershipController : ControllerBase
{
    private readonly IMediator _mediator;
    
    public NetworkMembershipController(IMediator mediator)
    {
        _mediator = mediator;
    }
    
    /// <summary>
    /// دریافت درخت شبکه من
    /// </summary>
    [HttpGet("my-tree")]
    [ProducesResponseType(typeof(MyNetworkTreeResponseDto), 200)]
    public async Task<IActionResult> GetMyTree([FromQuery] int depth = 3)
    {
        var query = new GetMyNetworkTreeQuery { Depth = depth };
        var result = await _mediator.Send(query);
        return Ok(result);
    }
    
    /// <summary>
    /// دریافت موقعیت من در شبکه
    /// </summary>
    [HttpGet("my-position")]
    [ProducesResponseType(typeof(MyNetworkPositionResponseDto), 200)]
    public async Task<IActionResult> GetMyPosition()
    {
        var query = new GetMyNetworkPositionQuery();
        var result = await _mediator.Send(query);
        return Ok(result);
    }
}
```

**Test Endpoints:**
```bash
# Test 1: Get Tree
curl -H "Authorization: Bearer TOKEN" \
     "http://localhost:5002/api/networkmembership/my-tree?depth=3"

# Test 2: Get Position
curl -H "Authorization: Bearer TOKEN" \
     http://localhost:5002/api/networkmembership/my-position
```

---

### 📝 STEP 4: ایجاد UI - Network Pages

#### Task 4.1: Service Layer
```bash
cd /home/masoud/Apps/project/FourSat/FrontOffice/src/FrontOffice.Main/Services/
nano NetworkMembershipService.cs
```

```csharp
using System.Net.Http.Json;
using FrontOffice.Main.Models;

namespace FrontOffice.Main.Services;

public class NetworkMembershipService
{
    private readonly HttpClient _httpClient;
    
    public NetworkMembershipService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }
    
    public async Task<MyNetworkTreeDto> GetMyTreeAsync(int depth = 3)
    {
        var response = await _httpClient.GetAsync($"/api/networkmembership/my-tree?depth={depth}");
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<MyNetworkTreeDto>();
    }
    
    public async Task<MyNetworkPositionDto> GetMyPositionAsync()
    {
        var response = await _httpClient.GetAsync("/api/networkmembership/my-position");
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<MyNetworkPositionDto>();
    }
}
```

**ثبت در Program.cs:**
```csharp
builder.Services.AddScoped<NetworkMembershipService>();
```

#### Task 4.2: Models
```csharp
// Models/MyNetworkTreeDto.cs
namespace FrontOffice.Main.Models;

public class MyNetworkTreeDto
{
    public NetworkNodeDto CurrentUser { get; set; }
    public int TotalNetworkSize { get; set; }
    public int DirectChildrenCount { get; set; }
    public string LastUpdatePersian { get; set; }
}

public class NetworkNodeDto
{
    public long UserId { get; set; }
    public string FullName { get; set; }
    public string Mobile { get; set; }
    public string Position { get; set; }
    public string JoinDatePersian { get; set; }
    public bool HasLeftChild { get; set; }
    public bool HasRightChild { get; set; }
    public NetworkNodeDto LeftChild { get; set; }
    public NetworkNodeDto RightChild { get; set; }
    public string StatusBadge { get; set; }
    public string StatusColor { get; set; }
}

// Models/MyNetworkPositionDto.cs
public class MyNetworkPositionDto
{
    public bool HasParent { get; set; }
    public string ParentFullName { get; set; }
    public string ParentMobile { get; set; }
    public string MyPosition { get; set; }
    public string MyPositionIcon { get; set; }
    public int NetworkLevel { get; set; }
    public int TotalDownlineCount { get; set; }
    public string JoinDatePersian { get; set; }
}
```

#### Task 4.3: Component - NetworkTreeNode (Recursive Component)
```bash
cd /home/masoud/Apps/project/FourSat/FrontOffice/src/FrontOffice.Main/Components/Network/
mkdir -p Network
nano NetworkTreeNode.razor
```

```razor
@* Component برای نمایش یک نود درخت (Recursive) *@

<MudCard Class="ma-2" Style="min-width: 250px;">
    <MudCardHeader>
        <CardHeaderContent>
            <MudText Typo="Typo.body1"><strong>@Node.FullName</strong></MudText>
            <MudText Typo="Typo.body2" Color="Color.Secondary">@Node.Mobile</MudText>
        </CardHeaderContent>
        <CardHeaderActions>
            <MudChip Size="Size.Small" 
                     Color="@(Node.StatusColor == "success" ? Color.Success : Color.Error)">
                @Node.StatusBadge
            </MudChip>
        </CardHeaderActions>
    </MudCardHeader>
    <MudCardContent>
        <MudText Typo="Typo.caption">موقعیت: @Node.Position</MudText>
        <MudText Typo="Typo.caption">تاریخ: @Node.JoinDatePersian</MudText>
    </MudCardContent>
</MudCard>

@if (Node.LeftChild != null || Node.RightChild != null)
{
    <MudGrid Class="mt-2" Justify="Justify.Center">
        @if (Node.LeftChild != null)
        {
            <MudItem xs="6">
                <div style="border-right: 2px solid #ccc; padding-right: 10px;">
                    <MudText Typo="Typo.caption" Color="Color.Primary">← چپ</MudText>
                    <NetworkTreeNode Node="@Node.LeftChild" />
                </div>
            </MudItem>
        }
        
        @if (Node.RightChild != null)
        {
            <MudItem xs="6">
                <div style="border-left: 2px solid #ccc; padding-left: 10px;">
                    <MudText Typo="Typo.caption" Color="Color.Secondary">راست →</MudText>
                    <NetworkTreeNode Node="@Node.RightChild" />
                </div>
            </MudItem>
        }
    </MudGrid>
}

@code {
    [Parameter]
    public NetworkNodeDto Node { get; set; }
}
```

#### Task 4.4: Page - NetworkTreePage
```bash
nano /home/masoud/Apps/project/FourSat/FrontOffice/src/FrontOffice.Main/Pages/Network/NetworkTreePage.razor
```

```razor
@page "/network/tree"
@inject NetworkMembershipService NetworkService
@inject ISnackbar Snackbar

<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
    <MudText Typo="Typo.h4" Class="mb-4">شبکه باینری من</MudText>
    
    @if (_loading)
    {
        <MudProgressLinear Indeterminate="true" Color="Color.Primary" />
    }
    else if (_tree != null)
    {
        <MudGrid>
            <MudItem xs="12" md="4">
                <MudCard>
                    <MudCardContent>
                        <MudStack Spacing="2">
                            <MudText Typo="Typo.h6">آمار کلی</MudText>
                            <MudText>
                                <strong>تعداد کل اعضا:</strong> @_tree.TotalNetworkSize نفر
                            </MudText>
                            <MudText>
                                <strong>فرزندان مستقیم:</strong> @_tree.DirectChildrenCount نفر
                            </MudText>
                            <MudText>
                                <strong>آخرین به‌روزرسانی:</strong> @_tree.LastUpdatePersian
                            </MudText>
                        </MudStack>
                    </MudCardContent>
                </MudCard>
            </MudItem>
            
            <MudItem xs="12" md="8">
                <MudText Typo="Typo.h6" Class="mb-2">درخت شبکه</MudText>
                <div style="overflow-x: auto;">
                    <NetworkTreeNode Node="@_tree.CurrentUser" />
                </div>
            </MudItem>
        </MudGrid>
    }
</MudContainer>

@code {
    private MyNetworkTreeDto? _tree;
    private bool _loading = true;
    
    protected override async Task OnInitializedAsync()
    {
        await LoadTree();
    }
    
    private async Task LoadTree()
    {
        try
        {
            _loading = true;
            _tree = await NetworkService.GetMyTreeAsync(depth: 3);
        }
        catch (Exception ex)
        {
            Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
        }
        finally
        {
            _loading = false;
        }
    }
}
```

#### Task 4.5: Page - NetworkPositionPage
```bash
nano /home/masoud/Apps/project/FourSat/FrontOffice/src/FrontOffice.Main/Pages/Network/NetworkPositionPage.razor
```

```razor
@page "/network/position"
@inject NetworkMembershipService NetworkService
@inject ISnackbar Snackbar

<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
    <MudText Typo="Typo.h4" Class="mb-4">موقعیت من در شبکه</MudText>
    
    @if (_loading)
    {
        <MudProgressLinear Indeterminate="true" />
    }
    else if (_position != null)
    {
        <MudCard>
            <MudCardContent>
                <MudGrid>
                    @if (_position.HasParent)
                    {
                        <MudItem xs="12" md="6">
                            <MudPaper Class="pa-4" Elevation="2">
                                <MudText Typo="Typo.h6" Class="mb-2">معرف من</MudText>
                                <MudText><strong>نام:</strong> @_position.ParentFullName</MudText>
                                <MudText><strong>موبایل:</strong> @_position.ParentMobile</MudText>
                            </MudPaper>
                        </MudItem>
                    }
                    
                    <MudItem xs="12" md="6">
                        <MudPaper Class="pa-4" Elevation="2">
                            <MudText Typo="Typo.h6" Class="mb-2">موقعیت من</MudText>
                            <MudText>
                                <MudIcon Icon="@_position.MyPositionIcon" />
                                <strong>@_position.MyPosition</strong>
                            </MudText>
                            <MudText><strong>سطح:</strong> @_position.NetworkLevel</MudText>
                        </MudPaper>
                    </MudItem>
                    
                    <MudItem xs="12">
                        <MudPaper Class="pa-4" Elevation="2">
                            <MudText Typo="Typo.h6" Class="mb-2">آمار زیرمجموعه</MudText>
                            <MudText>
                                <strong>تعداد کل افراد زیر مجموعه:</strong> @_position.TotalDownlineCount نفر
                            </MudText>
                            <MudText>
                                <strong>تاریخ پیوستن:</strong> @_position.JoinDatePersian
                            </MudText>
                        </MudPaper>
                    </MudItem>
                </MudGrid>
            </MudCardContent>
        </MudCard>
    }
</MudContainer>

@code {
    private MyNetworkPositionDto? _position;
    private bool _loading = true;
    
    protected override async Task OnInitializedAsync()
    {
        await LoadPosition();
    }
    
    private async Task LoadPosition()
    {
        try
        {
            _loading = true;
            _position = await NetworkService.GetMyPositionAsync();
        }
        catch (Exception ex)
        {
            Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
        }
        finally
        {
            _loading = false;
        }
    }
}
```

#### Task 4.6: اضافه کردن به NavMenu
```razor
<MudNavGroup Title="شبکه من" Icon="@Icons.Material.Filled.AccountTree">
    <MudNavLink Href="/network/tree" Icon="@Icons.Material.Filled.Schema">
        درخت شبکه
    </MudNavLink>
    <MudNavLink Href="/network/position" Icon="@Icons.Material.Filled.MyLocation">
        موقعیت من
    </MudNavLink>
</MudNavGroup>
```

---

### ✅ Checkpoint نهایی STEP 4

```bash
# Build & Test
cd /home/masoud/Apps/project/FourSat/FrontOffice.BFF/src/
dotnet build

cd /home/masoud/Apps/project/FourSat/FrontOffice/src/
dotnet build
```

**چیزهایی که باید کار کنند:**
```
[ ] BFF Build می‌شود (2 Query, 2 Controller endpoints)
[ ] FrontOffice Build می‌شود
[ ] صفحه /network/tree درخت نمایش می‌دهد
[ ] صفحه /network/position موقعیت نمایش می‌دهد
[ ] Recursive Component به درستی کار می‌کند
[ ] Mock data با 2 فرزند نمایش داده می‌شود
```

---

### 📊 آماری از کارهای انجام شده

| مورد | تعداد | وضعیت |
|------|-------|-------|
| Queries پیاده شده | 2 از 4 | 50% |
| Commands پیاده شده | 0 از 3 | 0% |
| Handlers | 2 | Mock Data |
| Controllers | 1 | 2 Endpoints |
| UI Pages | 2 | ✅ |
| UI Components | 1 | Recursive Tree ✅ |
| Services | 1 | ✅ |

**زمان تخمینی تا اینجا:** 5 ساعت  
**کارهای باقی‌مانده:** GetNetworkHistory Query + اتصال واقعی به CMS

---

### 💡 نکات مهم برای Developer

1. **Recursive Component**: `NetworkTreeNode` به صورت Recursive خودش را صدا می‌زند - مراقب Performance باش
2. **Depth Control**: هرگز `depth > 5` نگذار (درخت خیلی بزرگ می‌شود)
3. **UI Overflow**: از `overflow-x: auto` برای درخت‌های بزرگ استفاده شد
4. **CMS Integration**: بعد از اتصال به CMS، حتماً Handle کن که LeftChild/RightChild ممکنه `null` باشند


---

## 💰 مرحله 4: راهنمای گام‌به‌گام - Commission + Withdrawal (کمیسیون و برداشت)

### 📊 خلاصه ماژول

**هدف کسب‌وکار**: مشتری باید بتواند کمیسیون‌های خود را مشاهده کند، درخواست برداشت بدهد، وضعیت برداشت‌ها را پیگیری کند، و موجودی قابل برداشت خود را ببیند.

**اجزای موجود در CMS:**
- ✅ `CommissionPayout` Entity (مبلغ، هفته، وضعیت، تاریخ)
- ✅ `WithdrawalRequest` Entity (مبلغ، وضعیت: Pending/Approved/Rejected/Paid)
- ✅ 8 Commands: RequestWithdrawal, ApproveWithdrawal, RejectWithdrawal, PayWithdrawal, CancelWithdrawal, RecalculateCommission, AdjustBalance, TransferCommission
- ✅ 8 Queries: GetUserCommissionPayouts, GetUserBalance, GetWithdrawalHistory, GetWeeklyReport, GetPoolShare, GetDownlineCommissions, GetCommissionStatistics, GetAvailableBalance

**چیزهای غایب در BFF:**
- ❌ فقط 10% پیاده شده (GetUserCommissionPayouts Query)
- ❌ هیچ Command برای RequestWithdrawal
- ❌ هیچ Query برای موجودی و برداشت‌ها

**UI غایب:**
- ❌ صفحه نمایش کمیسیون‌ها
- ❌ صفحه درخواست برداشت
- ❌ صفحه تاریخچه برداشت‌ها

---

### 📝 STEP 1: بررسی CMS Commission Module

#### Task 1.1: بررسی Entities
```bash
cd /home/masoud/Apps/project/FourSat/CMS/src/

# 1. بررسی CommissionPayout Entity
cat CMSMicroservice.Domain/Entities/CommissionPayout.cs
# فیلدهای کلیدی:
#   - UserId: کاربر دریافت‌کننده
#   - Amount: مبلغ کمیسیون (decimal)
#   - WeekNumber: شماره هفته
#   - PayoutDate: تاریخ پرداخت
#   - Status: Pending/Calculated/Paid
#   - PayoutType: Direct/Binary/Pool/Club

# 2. بررسی WithdrawalRequest Entity
cat CMSMicroservice.Domain/Entities/WithdrawalRequest.cs
# فیلدهای کلیدی:
#   - UserId: کاربر درخواست‌دهنده
#   - Amount: مبلغ درخواستی
#   - Status: Pending/Approved/Rejected/Paid/Cancelled
#   - RequestDate: تاریخ درخواست
#   - ProcessDate: تاریخ پردازش
#   - BankAccountInfo: اطلاعات حساب (شماره کارت/شبا)
#   - RejectReason: دلیل رد (اگر رد شده)

# 3. بررسی UserBalance (موجودی)
cat CMSMicroservice.Domain/Entities/UserBalance.cs
# فیلدها:
#   - UserId
#   - CommissionBalance: موجودی کمیسیون
#   - WithdrawableBalance: قابل برداشت
#   - PendingWithdrawal: در انتظار برداشت
#   - TotalEarned: کل درآمد
```

**Output Task 1.1:**
```
[ ] CommissionPayout Entity را خواندم
[ ] WithdrawalRequest Entity را خواندم
[ ] UserBalance Entity را خواندم
[ ] Status enums را یادداشت کردم
```

#### Task 1.2: بررسی Queries موجود در CMS
```bash
# Query 1: GetUserCommissionPayouts
cat CMSMicroservice.Application/CommissionCQ/Queries/GetUserCommissionPayouts/GetUserCommissionPayoutsQueryHandler.cs
# Input: UserId, FromDate, ToDate, PageNumber, PageSize
# Output: List<CommissionPayoutDto> + TotalCount

# Query 2: GetUserBalance
cat CMSMicroservice.Application/CommissionCQ/Queries/GetUserBalance/GetUserBalanceQueryHandler.cs
# Input: UserId
# Output: CommissionBalance, WithdrawableBalance, PendingWithdrawal, TotalEarned

# Query 3: GetWithdrawalHistory
cat CMSMicroservice.Application/CommissionCQ/Queries/GetWithdrawalHistory/GetWithdrawalHistoryQueryHandler.cs
# Input: UserId, FromDate, ToDate, Status (optional)
# Output: List<WithdrawalRequestDto>

# Query 4: GetAvailableBalance
cat CMSMicroservice.Application/CommissionCQ/Queries/GetAvailableBalance/GetAvailableBalanceQueryHandler.cs
# Input: UserId
# Output: AvailableAmount, MinWithdrawalAmount, MaxWithdrawalAmount
```

#### Task 1.3: بررسی Command RequestWithdrawal
```bash
cat CMSMicroservice.Application/CommissionCQ/Commands/RequestWithdrawal/RequestWithdrawalCommandHandler.cs
# Input:
#   - UserId
#   - Amount
#   - BankAccountNumber (شماره کارت/شبا)
# Logic:
#   1. بررسی موجودی کافی
#   2. بررسی حداقل/حداکثر مبلغ
#   3. ایجاد WithdrawalRequest
#   4. کسر از WithdrawableBalance
#   5. اضافه به PendingWithdrawal
# Output: WithdrawalRequestId
```

---

### 📝 STEP 2: ایجاد BFF Module - CommissionCQ

#### Task 2.1: ساخت فولدرها
```bash
cd /home/masoud/Apps/project/FourSat/FrontOffice.BFF/src/FrontOffice.BFF.Application/

mkdir -p CommissionCQ/Queries/GetMyCommissionPayouts
mkdir -p CommissionCQ/Queries/GetMyBalance
mkdir -p CommissionCQ/Queries/GetMyWithdrawalHistory
mkdir -p CommissionCQ/Commands/RequestMyWithdrawal

tree CommissionCQ/
```

**Expected Output:**
```
CommissionCQ/
├── Commands/
│   └── RequestMyWithdrawal/
└── Queries/
    ├── GetMyCommissionPayouts/
    ├── GetMyBalance/
    └── GetMyWithdrawalHistory/
```

#### Task 2.2: Query #1 - GetMyCommissionPayouts

**فایل 1: GetMyCommissionPayoutsQuery.cs**
```csharp
using MediatR;

namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyCommissionPayouts;

public record GetMyCommissionPayoutsQuery : IRequest<MyCommissionPayoutsResponseDto>
{
    /// <summary>
    /// تعداد آیتم در هر صفحه (پیش‌فرض: 10)
    /// </summary>
    public int PageSize { get; init; } = 10;
    
    /// <summary>
    /// شماره صفحه (پیش‌فرض: 1)
    /// </summary>
    public int PageNumber { get; init; } = 1;
    
    /// <summary>
    /// فیلتر بر اساس نوع کمیسیون (اختیاری)
    /// </summary>
    public string PayoutType { get; init; }
}
```

**فایل 2: MyCommissionPayoutsResponseDto.cs**
```csharp
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyCommissionPayouts;

public class MyCommissionPayoutsResponseDto
{
    public List<CommissionPayoutItemDto> Payouts { get; set; }
    public int TotalCount { get; set; }
    public int CurrentPage { get; set; }
    public int TotalPages { get; set; }
    public decimal TotalAmount { get; set; }       // مجموع کل کمیسیون‌ها
}

public class CommissionPayoutItemDto
{
    public long Id { get; set; }
    public string WeekDisplay { get; set; }        // "هفته 48 - سال 1403"
    public decimal Amount { get; set; }
    public string AmountFormatted { get; set; }    // "1,250,000 تومان"
    public string PayoutType { get; set; }         // "مستقیم" / "باینری" / "پول" / "باشگاه"
    public string PayoutTypeIcon { get; set; }     // Icon name for UI
    public string Status { get; set; }             // "در انتظار" / "محاسبه شده" / "پرداخت شده"
    public string StatusColor { get; set; }        // "warning" / "info" / "success"
    public string PayoutDatePersian { get; set; }
}
```

**فایل 3: GetMyCommissionPayoutsQueryHandler.cs**
```csharp
using MediatR;
using FrontOffice.BFF.Application.Common.Interfaces;

namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyCommissionPayouts;

public class GetMyCommissionPayoutsQueryHandler 
    : IRequestHandler<GetMyCommissionPayoutsQuery, MyCommissionPayoutsResponseDto>
{
    private readonly ICurrentUserService _currentUser;
    // TODO: private readonly CommissionServiceClient _cmsClient;
    
    public GetMyCommissionPayoutsQueryHandler(ICurrentUserService currentUser)
    {
        _currentUser = currentUser;
    }
    
    public async Task<MyCommissionPayoutsResponseDto> Handle(
        GetMyCommissionPayoutsQuery request,
        CancellationToken cancellationToken)
    {
        var userId = _currentUser.UserId;
        
        // TODO: فراخوانی CMS
        // var cmsResult = await _cmsClient.GetUserCommissionPayoutsAsync(
        //     new GetUserCommissionPayoutsRequest { 
        //         UserId = userId, 
        //         PageNumber = request.PageNumber,
        //         PageSize = request.PageSize 
        //     });
        
        // Mock Data
        return new MyCommissionPayoutsResponseDto
        {
            Payouts = new List<CommissionPayoutItemDto>
            {
                new() {
                    Id = 1,
                    WeekDisplay = "هفته 48 - سال 1403",
                    Amount = 1250000,
                    AmountFormatted = "1,250,000 تومان",
                    PayoutType = "مستقیم",
                    PayoutTypeIcon = "trending_up",
                    Status = "پرداخت شده",
                    StatusColor = "success",
                    PayoutDatePersian = "20 آذر 1403"
                },
                new() {
                    Id = 2,
                    WeekDisplay = "هفته 47 - سال 1403",
                    Amount = 850000,
                    AmountFormatted = "850,000 تومان",
                    PayoutType = "باینری",
                    PayoutTypeIcon = "account_tree",
                    Status = "پرداخت شده",
                    StatusColor = "success",
                    PayoutDatePersian = "13 آذر 1403"
                }
            },
            TotalCount = 2,
            CurrentPage = 1,
            TotalPages = 1,
            TotalAmount = 2100000
        };
    }
}
```

#### Task 2.3: Query #2 - GetMyBalance (موجودی)

**فایل 1: GetMyBalanceQuery.cs**
```csharp
using MediatR;

namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyBalance;

public record GetMyBalanceQuery : IRequest<MyBalanceResponseDto>
{
}
```

**فایل 2: MyBalanceResponseDto.cs**
```csharp
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyBalance;

public class MyBalanceResponseDto
{
    public decimal TotalEarned { get; set; }           // کل درآمد تاکنون
    public string TotalEarnedFormatted { get; set; }
    
    public decimal CurrentBalance { get; set; }        // موجودی فعلی
    public string CurrentBalanceFormatted { get; set; }
    
    public decimal WithdrawableBalance { get; set; }   // قابل برداشت
    public string WithdrawableBalanceFormatted { get; set; }
    
    public decimal PendingWithdrawal { get; set; }     // در انتظار برداشت
    public string PendingWithdrawalFormatted { get; set; }
    
    public bool CanRequestWithdrawal { get; set; }     // آیا می‌تواند برداشت کند؟
    public string MinWithdrawalAmount { get; set; }    // حداقل مبلغ برداشت
    public string MaxWithdrawalAmount { get; set; }    // حداکثر مبلغ برداشت
}
```

**فایل 3: GetMyBalanceQueryHandler.cs**
```csharp
using MediatR;
using FrontOffice.BFF.Application.Common.Interfaces;

namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyBalance;

public class GetMyBalanceQueryHandler 
    : IRequestHandler<GetMyBalanceQuery, MyBalanceResponseDto>
{
    private readonly ICurrentUserService _currentUser;
    
    public GetMyBalanceQueryHandler(ICurrentUserService currentUser)
    {
        _currentUser = currentUser;
    }
    
    public async Task<MyBalanceResponseDto> Handle(
        GetMyBalanceQuery request,
        CancellationToken cancellationToken)
    {
        var userId = _currentUser.UserId;
        
        // TODO: Call CMS
        
        // Mock Data
        return new MyBalanceResponseDto
        {
            TotalEarned = 15750000,
            TotalEarnedFormatted = "15,750,000 تومان",
            CurrentBalance = 8500000,
            CurrentBalanceFormatted = "8,500,000 تومان",
            WithdrawableBalance = 7000000,
            WithdrawableBalanceFormatted = "7,000,000 تومان",
            PendingWithdrawal = 1500000,
            PendingWithdrawalFormatted = "1,500,000 تومان",
            CanRequestWithdrawal = true,
            MinWithdrawalAmount = "100,000 تومان",
            MaxWithdrawalAmount = "7,000,000 تومان"
        };
    }
}
```

#### Task 2.4: Query #3 - GetMyWithdrawalHistory

**فایل 1: GetMyWithdrawalHistoryQuery.cs**
```csharp
using MediatR;

namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyWithdrawalHistory;

public record GetMyWithdrawalHistoryQuery : IRequest<MyWithdrawalHistoryResponseDto>
{
    public int PageSize { get; init; } = 10;
    public int PageNumber { get; init; } = 1;
}
```

**فایل 2: MyWithdrawalHistoryResponseDto.cs**
```csharp
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyWithdrawalHistory;

public class MyWithdrawalHistoryResponseDto
{
    public List<WithdrawalItemDto> Withdrawals { get; set; }
    public int TotalCount { get; set; }
}

public class WithdrawalItemDto
{
    public long Id { get; set; }
    public decimal Amount { get; set; }
    public string AmountFormatted { get; set; }
    public string Status { get; set; }              // "در انتظار" / "تایید" / "رد" / "پرداخت شده"
    public string StatusColor { get; set; }         // "warning" / "success" / "error" / "info"
    public string RequestDatePersian { get; set; }
    public string ProcessDatePersian { get; set; }
    public string BankAccount { get; set; }         // "6037-****-****-1234"
    public string RejectReason { get; set; }        // دلیل رد (اگر رد شده)
}
```

**فایل 3: GetMyWithdrawalHistoryQueryHandler.cs**
```csharp
using MediatR;
using FrontOffice.BFF.Application.Common.Interfaces;

namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyWithdrawalHistory;

public class GetMyWithdrawalHistoryQueryHandler 
    : IRequestHandler<GetMyWithdrawalHistoryQuery, MyWithdrawalHistoryResponseDto>
{
    private readonly ICurrentUserService _currentUser;
    
    public GetMyWithdrawalHistoryQueryHandler(ICurrentUserService currentUser)
    {
        _currentUser = currentUser;
    }
    
    public async Task<MyWithdrawalHistoryResponseDto> Handle(
        GetMyWithdrawalHistoryQuery request,
        CancellationToken cancellationToken)
    {
        // TODO: Call CMS
        
        return new MyWithdrawalHistoryResponseDto
        {
            Withdrawals = new List<WithdrawalItemDto>
            {
                new() {
                    Id = 1,
                    Amount = 1500000,
                    AmountFormatted = "1,500,000 تومان",
                    Status = "در انتظار",
                    StatusColor = "warning",
                    RequestDatePersian = "25 آذر 1403",
                    ProcessDatePersian = "-",
                    BankAccount = "6037-****-****-1234"
                },
                new() {
                    Id = 2,
                    Amount = 2000000,
                    AmountFormatted = "2,000,000 تومان",
                    Status = "پرداخت شده",
                    StatusColor = "success",
                    RequestDatePersian = "15 آذر 1403",
                    ProcessDatePersian = "18 آذر 1403",
                    BankAccount = "6037-****-****-1234"
                }
            },
            TotalCount = 2
        };
    }
}
```

#### Task 2.5: Command - RequestMyWithdrawal

**فایل 1: RequestMyWithdrawalCommand.cs**
```csharp
using MediatR;

namespace FrontOffice.BFF.Application.CommissionCQ.Commands.RequestMyWithdrawal;

public record RequestMyWithdrawalCommand : IRequest<RequestMyWithdrawalResponseDto>
{
    public decimal Amount { get; init; }
    public string BankAccountNumber { get; init; }    // شماره کارت یا شبا
}
```

**فایل 2: RequestMyWithdrawalResponseDto.cs**
```csharp
namespace FrontOffice.BFF.Application.CommissionCQ.Commands.RequestMyWithdrawal;

public class RequestMyWithdrawalResponseDto
{
    public bool Success { get; set; }
    public long WithdrawalRequestId { get; set; }
    public string Message { get; set; }               // "درخواست شما با موفقیت ثبت شد"
    public string NewWithdrawableBalance { get; set; } // موجودی جدید قابل برداشت
}
```

**فایل 3: RequestMyWithdrawalCommandHandler.cs**
```csharp
using MediatR;
using FrontOffice.BFF.Application.Common.Interfaces;

namespace FrontOffice.BFF.Application.CommissionCQ.Commands.RequestMyWithdrawal;

public class RequestMyWithdrawalCommandHandler 
    : IRequestHandler<RequestMyWithdrawalCommand, RequestMyWithdrawalResponseDto>
{
    private readonly ICurrentUserService _currentUser;
    // TODO: private readonly CommissionServiceClient _cmsClient;
    
    public RequestMyWithdrawalCommandHandler(ICurrentUserService currentUser)
    {
        _currentUser = currentUser;
    }
    
    public async Task<RequestMyWithdrawalResponseDto> Handle(
        RequestMyWithdrawalCommand request,
        CancellationToken cancellationToken)
    {
        var userId = _currentUser.UserId;
        
        // TODO: فراخوانی CMS
        // var cmsResult = await _cmsClient.RequestWithdrawalAsync(
        //     new RequestWithdrawalRequest {
        //         UserId = userId,
        //         Amount = request.Amount,
        //         BankAccountNumber = request.BankAccountNumber
        //     });
        
        // Mock Response
        return new RequestMyWithdrawalResponseDto
        {
            Success = true,
            WithdrawalRequestId = 123,
            Message = "درخواست برداشت شما با موفقیت ثبت شد و در انتظار تایید است.",
            NewWithdrawableBalance = "5,500,000 تومان"
        };
    }
}
```

**فایل 4: RequestMyWithdrawalCommandValidator.cs**
```csharp
using FluentValidation;

namespace FrontOffice.BFF.Application.CommissionCQ.Commands.RequestMyWithdrawal;

public class RequestMyWithdrawalCommandValidator : AbstractValidator<RequestMyWithdrawalCommand>
{
    public RequestMyWithdrawalCommandValidator()
    {
        RuleFor(x => x.Amount)
            .GreaterThan(0).WithMessage("مبلغ باید بیشتر از صفر باشد")
            .LessThanOrEqualTo(50000000).WithMessage("حداکثر مبلغ برداشت 50 میلیون تومان است");
        
        RuleFor(x => x.BankAccountNumber)
            .NotEmpty().WithMessage("شماره کارت الزامی است")
            .Length(16, 24).WithMessage("شماره کارت یا شبا نامعتبر است");
    }
}
```

---

### 📝 STEP 3: اضافه کردن Controller

**فایل: CommissionController.cs**
```csharp
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MediatR;
using FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
using FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyBalance;
using FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyWithdrawalHistory;
using FrontOffice.BFF.Application.CommissionCQ.Commands.RequestMyWithdrawal;

namespace FrontOffice.BFF.WebApi.Controllers;

[Authorize]
[ApiController]
[Route("api/[controller]")]
public class CommissionController : ControllerBase
{
    private readonly IMediator _mediator;
    
    public CommissionController(IMediator mediator)
    {
        _mediator = mediator;
    }
    
    /// <summary>
    /// دریافت لیست کمیسیون‌های من
    /// </summary>
    [HttpGet("my-payouts")]
    [ProducesResponseType(typeof(MyCommissionPayoutsResponseDto), 200)]
    public async Task<IActionResult> GetMyPayouts(
        [FromQuery] int pageNumber = 1,
        [FromQuery] int pageSize = 10)
    {
        var query = new GetMyCommissionPayoutsQuery 
        { 
            PageNumber = pageNumber, 
            PageSize = pageSize 
        };
        var result = await _mediator.Send(query);
        return Ok(result);
    }
    
    /// <summary>
    /// دریافت موجودی من
    /// </summary>
    [HttpGet("my-balance")]
    [ProducesResponseType(typeof(MyBalanceResponseDto), 200)]
    public async Task<IActionResult> GetMyBalance()
    {
        var query = new GetMyBalanceQuery();
        var result = await _mediator.Send(query);
        return Ok(result);
    }
    
    /// <summary>
    /// دریافت تاریخچه برداشت‌های من
    /// </summary>
    [HttpGet("my-withdrawal-history")]
    [ProducesResponseType(typeof(MyWithdrawalHistoryResponseDto), 200)]
    public async Task<IActionResult> GetMyWithdrawalHistory(
        [FromQuery] int pageNumber = 1,
        [FromQuery] int pageSize = 10)
    {
        var query = new GetMyWithdrawalHistoryQuery 
        { 
            PageNumber = pageNumber, 
            PageSize = pageSize 
        };
        var result = await _mediator.Send(query);
        return Ok(result);
    }
    
    /// <summary>
    /// درخواست برداشت
    /// </summary>
    [HttpPost("request-withdrawal")]
    [ProducesResponseType(typeof(RequestMyWithdrawalResponseDto), 200)]
    public async Task<IActionResult> RequestWithdrawal(
        [FromBody] RequestMyWithdrawalCommand command)
    {
        var result = await _mediator.Send(command);
        return Ok(result);
    }
}
```

**Test Endpoints:**
```bash
# Test 1: Get Payouts
curl -H "Authorization: Bearer TOKEN" \
     "http://localhost:5002/api/commission/my-payouts?pageNumber=1&pageSize=10"

# Test 2: Get Balance
curl -H "Authorization: Bearer TOKEN" \
     http://localhost:5002/api/commission/my-balance

# Test 3: Get Withdrawal History
curl -H "Authorization: Bearer TOKEN" \
     http://localhost:5002/api/commission/my-withdrawal-history

# Test 4: Request Withdrawal
curl -X POST \
     -H "Authorization: Bearer TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"amount": 1500000, "bankAccountNumber": "6037997012345678"}' \
     http://localhost:5002/api/commission/request-withdrawal
```

---

### 📝 STEP 4: ایجاد UI - Commission Pages

#### Task 4.1: Service Layer
```bash
cd /home/masoud/Apps/project/FourSat/FrontOffice/src/FrontOffice.Main/Services/
nano CommissionService.cs
```

```csharp
using System.Net.Http.Json;
using FrontOffice.Main.Models;

namespace FrontOffice.Main.Services;

public class CommissionService
{
    private readonly HttpClient _httpClient;
    
    public CommissionService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }
    
    public async Task<MyCommissionPayoutsDto> GetMyPayoutsAsync(int pageNumber = 1, int pageSize = 10)
    {
        var response = await _httpClient.GetAsync(
            $"/api/commission/my-payouts?pageNumber={pageNumber}&pageSize={pageSize}");
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<MyCommissionPayoutsDto>();
    }
    
    public async Task<MyBalanceDto> GetMyBalanceAsync()
    {
        var response = await _httpClient.GetAsync("/api/commission/my-balance");
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<MyBalanceDto>();
    }
    
    public async Task<MyWithdrawalHistoryDto> GetMyWithdrawalHistoryAsync(int pageNumber = 1)
    {
        var response = await _httpClient.GetAsync(
            $"/api/commission/my-withdrawal-history?pageNumber={pageNumber}");
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<MyWithdrawalHistoryDto>();
    }
    
    public async Task<RequestWithdrawalResultDto> RequestWithdrawalAsync(decimal amount, string bankAccount)
    {
        var request = new { Amount = amount, BankAccountNumber = bankAccount };
        var response = await _httpClient.PostAsJsonAsync("/api/commission/request-withdrawal", request);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<RequestWithdrawalResultDto>();
    }
}
```

**ثبت در Program.cs:**
```csharp
builder.Services.AddScoped<CommissionService>();
```

#### Task 4.2: Models (در فولدر Models/)
```csharp
// کپی DTOها از BFF به FrontOffice.Main/Models/
// MyCommissionPayoutsDto.cs
// MyBalanceDto.cs
// MyWithdrawalHistoryDto.cs
// RequestWithdrawalResultDto.cs
```

#### Task 4.3: Page - CommissionPayoutsPage (صفحه کمیسیون‌ها)
```razor
@page "/commission/payouts"
@inject CommissionService CommissionService
@inject ISnackbar Snackbar

<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
    <MudText Typo="Typo.h4" Class="mb-4">کمیسیون‌های من</MudText>
    
    @if (_loading)
    {
        <MudProgressLinear Indeterminate="true" />
    }
    else if (_payouts != null)
    {
        <MudCard Class="mb-4">
            <MudCardContent>
                <MudText Typo="Typo.h6">
                    مجموع کل: <strong style="color: green;">@_payouts.TotalAmount.ToString("N0") تومان</strong>
                </MudText>
            </MudCardContent>
        </MudCard>
        
        <MudTable Items="@_payouts.Payouts" Hover="true" Striped="true">
            <HeaderContent>
                <MudTh>هفته</MudTh>
                <MudTh>نوع</MudTh>
                <MudTh>مبلغ</MudTh>
                <MudTh>وضعیت</MudTh>
                <MudTh>تاریخ پرداخت</MudTh>
            </HeaderContent>
            <RowTemplate>
                <MudTd>@context.WeekDisplay</MudTd>
                <MudTd>
                    <MudIcon Icon="@context.PayoutTypeIcon" Size="Size.Small" />
                    @context.PayoutType
                </MudTd>
                <MudTd><strong>@context.AmountFormatted</strong></MudTd>
                <MudTd>
                    <MudChip Size="Size.Small" Color="@GetStatusColor(context.StatusColor)">
                        @context.Status
                    </MudChip>
                </MudTd>
                <MudTd>@context.PayoutDatePersian</MudTd>
            </RowTemplate>
        </MudTable>
        
        <MudPagination Class="mt-4" 
                       Count="_payouts.TotalPages" 
                       Selected="_currentPage"
                       SelectedChanged="OnPageChanged" />
    }
</MudContainer>

@code {
    private MyCommissionPayoutsDto? _payouts;
    private bool _loading = true;
    private int _currentPage = 1;
    
    protected override async Task OnInitializedAsync()
    {
        await LoadPayouts();
    }
    
    private async Task LoadPayouts()
    {
        try
        {
            _loading = true;
            _payouts = await CommissionService.GetMyPayoutsAsync(_currentPage, 10);
        }
        catch (Exception ex)
        {
            Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
        }
        finally
        {
            _loading = false;
        }
    }
    
    private async Task OnPageChanged(int page)
    {
        _currentPage = page;
        await LoadPayouts();
    }
    
    private Color GetStatusColor(string color)
    {
        return color switch
        {
            "success" => Color.Success,
            "warning" => Color.Warning,
            "error" => Color.Error,
            "info" => Color.Info,
            _ => Color.Default
        };
    }
}
```

#### Task 4.4: Page - WithdrawalPage (صفحه برداشت)
```razor
@page "/commission/withdrawal"
@inject CommissionService CommissionService
@inject ISnackbar Snackbar

<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
    <MudText Typo="Typo.h4" Class="mb-4">برداشت وجه</MudText>
    
    @if (_loadingBalance)
    {
        <MudProgressLinear Indeterminate="true" />
    }
    else if (_balance != null)
    {
        <MudGrid>
            <MudItem xs="12" md="6">
                <MudCard Class="mb-4">
                    <MudCardHeader>
                        <CardHeaderContent>
                            <MudText Typo="Typo.h6">موجودی من</MudText>
                        </CardHeaderContent>
                    </MudCardHeader>
                    <MudCardContent>
                        <MudStack Spacing="2">
                            <MudText>
                                <strong>کل درآمد:</strong> 
                                <span style="color: blue;">@_balance.TotalEarnedFormatted</span>
                            </MudText>
                            <MudText>
                                <strong>موجودی فعلی:</strong> 
                                <span style="color: green;">@_balance.CurrentBalanceFormatted</span>
                            </MudText>
                            <MudText>
                                <strong>قابل برداشت:</strong> 
                                <span style="color: orange; font-size: 1.2em;">@_balance.WithdrawableBalanceFormatted</span>
                            </MudText>
                            <MudText>
                                <strong>در انتظار برداشت:</strong> @_balance.PendingWithdrawalFormatted
                            </MudText>
                        </MudStack>
                    </MudCardContent>
                </MudCard>
            </MudItem>
            
            <MudItem xs="12" md="6">
                <MudCard>
                    <MudCardHeader>
                        <CardHeaderContent>
                            <MudText Typo="Typo.h6">درخواست برداشت جدید</MudText>
                        </CardHeaderContent>
                    </MudCardHeader>
                    <MudCardContent>
                        <MudForm @ref="_form">
                            <MudNumericField @bind-Value="_withdrawalAmount"
                                           Label="مبلغ (تومان)"
                                           Variant="Variant.Outlined"
                                           Required="true"
                                           Min="100000"
                                           Max="_balance.WithdrawableBalance" />
                            
                            <MudTextField @bind-Value="_bankAccount"
                                        Label="شماره کارت یا شبا"
                                        Variant="Variant.Outlined"
                                        Required="true"
                                        MaxLength="24"
                                        Class="mt-3" />
                            
                            <MudText Typo="Typo.caption" Color="Color.Secondary" Class="mt-2">
                                حداقل: @_balance.MinWithdrawalAmount | حداکثر: @_balance.MaxWithdrawalAmount
                            </MudText>
                        </MudForm>
                    </MudCardContent>
                    <MudCardActions>
                        <MudButton Variant="Variant.Filled"
                                 Color="Color.Primary"
                                 Disabled="@(!_balance.CanRequestWithdrawal || _submitting)"
                                 OnClick="SubmitWithdrawal">
                            @if (_submitting)
                            {
                                <MudProgressCircular Size="Size.Small" Indeterminate="true" />
                                <span class="ms-2">در حال ارسال...</span>
                            }
                            else
                            {
                                <span>ثبت درخواست</span>
                            }
                        </MudButton>
                    </MudCardActions>
                </MudCard>
            </MudItem>
        </MudGrid>
        
        <MudText Typo="Typo.h5" Class="mt-6 mb-3">تاریخچه برداشت‌ها</MudText>
        
        @if (_loadingHistory)
        {
            <MudProgressLinear Indeterminate="true" />
        }
        else if (_history != null)
        {
            <MudTable Items="@_history.Withdrawals" Hover="true">
                <HeaderContent>
                    <MudTh>مبلغ</MudTh>
                    <MudTh>وضعیت</MudTh>
                    <MudTh>تاریخ درخواست</MudTh>
                    <MudTh>تاریخ پردازش</MudTh>
                    <MudTh>شماره کارت</MudTh>
                </HeaderContent>
                <RowTemplate>
                    <MudTd><strong>@context.AmountFormatted</strong></MudTd>
                    <MudTd>
                        <MudChip Size="Size.Small" Color="@GetStatusColor(context.StatusColor)">
                            @context.Status
                        </MudChip>
                    </MudTd>
                    <MudTd>@context.RequestDatePersian</MudTd>
                    <MudTd>@context.ProcessDatePersian</MudTd>
                    <MudTd>@context.BankAccount</MudTd>
                </RowTemplate>
            </MudTable>
        }
    }
</MudContainer>

@code {
    private MyBalanceDto? _balance;
    private MyWithdrawalHistoryDto? _history;
    private bool _loadingBalance = true;
    private bool _loadingHistory = true;
    private bool _submitting = false;
    
    private MudForm _form;
    private decimal _withdrawalAmount;
    private string _bankAccount = "";
    
    protected override async Task OnInitializedAsync()
    {
        await Task.WhenAll(LoadBalance(), LoadHistory());
    }
    
    private async Task LoadBalance()
    {
        try
        {
            _loadingBalance = true;
            _balance = await CommissionService.GetMyBalanceAsync();
        }
        catch (Exception ex)
        {
            Snackbar.Add($"خطا در بارگذاری موجودی: {ex.Message}", Severity.Error);
        }
        finally
        {
            _loadingBalance = false;
        }
    }
    
    private async Task LoadHistory()
    {
        try
        {
            _loadingHistory = true;
            _history = await CommissionService.GetMyWithdrawalHistoryAsync();
        }
        catch (Exception ex)
        {
            Snackbar.Add($"خطا در بارگذاری تاریخچه: {ex.Message}", Severity.Error);
        }
        finally
        {
            _loadingHistory = false;
        }
    }
    
    private async Task SubmitWithdrawal()
    {
        await _form.Validate();
        if (!_form.IsValid) return;
        
        try
        {
            _submitting = true;
            var result = await CommissionService.RequestWithdrawalAsync(_withdrawalAmount, _bankAccount);
            
            if (result.Success)
            {
                Snackbar.Add(result.Message, Severity.Success);
                _withdrawalAmount = 0;
                _bankAccount = "";
                await Task.WhenAll(LoadBalance(), LoadHistory());
            }
        }
        catch (Exception ex)
        {
            Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
        }
        finally
        {
            _submitting = false;
        }
    }
    
    private Color GetStatusColor(string color)
    {
        return color switch
        {
            "success" => Color.Success,
            "warning" => Color.Warning,
            "error" => Color.Error,
            _ => Color.Default
        };
    }
}
```

#### Task 4.5: اضافه کردن به NavMenu
```razor
<MudNavGroup Title="کمیسیون و برداشت" Icon="@Icons.Material.Filled.AccountBalanceWallet">
    <MudNavLink Href="/commission/payouts" Icon="@Icons.Material.Filled.Payments">
        کمیسیون‌های من
    </MudNavLink>
    <MudNavLink Href="/commission/withdrawal" Icon="@Icons.Material.Filled.LocalAtm">
        برداشت وجه
    </MudNavLink>
</MudNavGroup>
```

---

### ✅ Checkpoint نهایی

```bash
cd /home/masoud/Apps/project/FourSat/FrontOffice.BFF/src/
dotnet build

cd /home/masoud/Apps/project/FourSat/FrontOffice/src/
dotnet build
```

**چیزهایی که باید کار کنند:**
```
[ ] BFF Build شود (3 Queries + 1 Command + Validator)
[ ] FrontOffice Build شود
[ ] صفحه /commission/payouts نمایش داده شود
[ ] صفحه /commission/withdrawal کار کند
[ ] فرم درخواست برداشت Validate شود
[ ] Mock data نمایش داده شود
```

---

### 📊 آماری از کارهای انجام شده

| مورد | تعداد | وضعیت |
|------|-------|-------|
| Queries پیاده شده | 3 از 8 | 37.5% |
| Commands پیاده شده | 1 از 8 | 12.5% |
| Handlers | 4 | Mock Data |
| Validators | 1 | FluentValidation ✅ |
| Controllers | 1 | 4 Endpoints |
| UI Pages | 2 | ✅ |
| Services | 1 | ✅ |

**زمان تخمینی تا اینجا:** 6 ساعت  
**کارهای باقی‌مانده:** 
- 5 Query دیگر (Weekly Report, Pool Share, Downline, Statistics, Available)
- 7 Command دیگر (Approve, Reject, Pay, Cancel, Recalculate, Adjust, Transfer)
- اتصال واقعی به CMS

---

### 💡 نکات بسیار مهم برای Developer

1. **Validation**: از FluentValidation استفاده شد - حتماً Validator را در DI ثبت کن
2. **Amount Formatting**: همه مبالغ با Format "N0" نمایش داده می‌شوند (1,250,000)
3. **Bank Account Masking**: شماره کارت را Mask کن: "6037-****-****-1234"
4. **Minimum Withdrawal**: در CMS حداقل مبلغ برداشت را Check کن (معمولاً 100,000 تومان)
5. **Concurrent Requests**: کاربر نباید بتواند همزمان چند درخواست برداشت بزند
6. **Status Colors**: از Color mapping استفاده کن برای نمایش بهتر وضعیت‌ها


---

## 🎒 مرحله 5: تکمیل UserWallet (کیف پول)

### 📊 وضعیت فعلی

**موجود در BFF (60%):**
- ✅ GetUserWallet Query
- ✅ GetWalletTransactions Query
- ✅ ChargeWallet Command (ولی ناقص)
- ⚠️ Withdrawal Handler خالی است (TODO)

**غایب (40%):**
- ❌ DiscountBalance (موجودی تخفیف) - هیچ Query و UI ندارد
- ❌ GetDiscountTransactions Query
- ❌ UseDiscount Command (استفاده از تخفیف در خرید)
- ❌ صفحه نمایش موجودی تخفیف در UI

---

### 📝 STEP 1: بررسی DiscountBalance در CMS

#### Task 1.1: بررسی UserWallet Entity
```bash
cd /home/masoud/Apps/project/FourSat/CMS/src/

cat CMSMicroservice.Domain/Entities/UserWallet.cs
# باید ببینی:
#   - MainBalance: موجودی اصلی ✅
#   - DiscountBalance: موجودی تخفیف ❌ (این قسمت غایب است)
#   - RewardBalance: موجودی پاداش ✅
```

#### Task 1.2: بررسی Queries موجود
```bash
ls CMSMicroservice.Application/UserWalletCQ/Queries/
# باید ببینی:
#   - GetUserWallet/ ✅
#   - GetWalletTransactions/ ✅
#   - GetDiscountTransactions/ (ممکن است وجود نداشته باشد)

# اگر GetDiscountTransactions وجود داشت:
cat CMSMicroservice.Application/UserWalletCQ/Queries/GetDiscountTransactions/GetDiscountTransactionsQueryHandler.cs
```

**Output Task 1.2:**
```
[ ] GetUserWallet Query را بررسی کردم
[ ] چک کردم DiscountBalance در DTO موجود است یا خیر
[ ] GetDiscountTransactions را پیدا کردم (یا متوجه شدم که وجود ندارد)
```

---

### 📝 STEP 2: تکمیل BFF - DiscountBalance

#### Task 2.1: اضافه کردن DiscountBalance به GetMyWallet

**فایل موجود: FrontOffice.BFF.Application/UserWalletCQ/Queries/GetMyWallet/MyWalletResponseDto.cs**

اگر DiscountBalance وجود ندارد، اضافه کن:

```csharp
namespace FrontOffice.BFF.Application.UserWalletCQ.Queries.GetMyWallet;

public class MyWalletResponseDto
{
    // موجود:
    public decimal MainBalance { get; set; }
    public string MainBalanceFormatted { get; set; }
    
    public decimal RewardBalance { get; set; }
    public string RewardBalanceFormatted { get; set; }
    
    // اضافه کن:
    public decimal DiscountBalance { get; set; }
    public string DiscountBalanceFormatted { get; set; }
    
    // مجموع کل
    public decimal TotalBalance { get; set; }
    public string TotalBalanceFormatted { get; set; }
    
    // UI Helpers
    public bool HasDiscount { get; set; }           // آیا تخفیف دارد؟
    public string DiscountPercentage { get; set; }  // "15%" (اگر applicable)
}
```

**آپدیت Handler:**
```csharp
// در GetMyWalletQueryHandler.cs
public async Task<MyWalletResponseDto> Handle(...)
{
    var userId = _currentUser.UserId;
    
    // TODO: Call CMS
    // var wallet = await _cmsClient.GetUserWalletAsync(new { UserId = userId });
    
    // Mock Data با DiscountBalance
    var mainBalance = 5000000m;
    var rewardBalance = 1200000m;
    var discountBalance = 800000m;  // اضافه شد
    var total = mainBalance + rewardBalance + discountBalance;
    
    return new MyWalletResponseDto
    {
        MainBalance = mainBalance,
        MainBalanceFormatted = mainBalance.ToString("N0") + " تومان",
        
        RewardBalance = rewardBalance,
        RewardBalanceFormatted = rewardBalance.ToString("N0") + " تومان",
        
        DiscountBalance = discountBalance,
        DiscountBalanceFormatted = discountBalance.ToString("N0") + " تومان",
        
        TotalBalance = total,
        TotalBalanceFormatted = total.ToString("N0") + " تومان",
        
        HasDiscount = discountBalance > 0,
        DiscountPercentage = "15%"
    };
}
```

#### Task 2.2: ایجاد Query جدید - GetMyDiscountTransactions

**فایل 1: GetMyDiscountTransactionsQuery.cs**
```bash
mkdir -p FrontOffice.BFF.Application/UserWalletCQ/Queries/GetMyDiscountTransactions/
nano GetMyDiscountTransactionsQuery.cs
```

```csharp
using MediatR;

namespace FrontOffice.BFF.Application.UserWalletCQ.Queries.GetMyDiscountTransactions;

public record GetMyDiscountTransactionsQuery : IRequest<MyDiscountTransactionsResponseDto>
{
    public int PageNumber { get; init; } = 1;
    public int PageSize { get; init; } = 10;
}
```

**فایل 2: MyDiscountTransactionsResponseDto.cs**
```csharp
namespace FrontOffice.BFF.Application.UserWalletCQ.Queries.GetMyDiscountTransactions;

public class MyDiscountTransactionsResponseDto
{
    public List<DiscountTransactionItemDto> Transactions { get; set; }
    public int TotalCount { get; set; }
}

public class DiscountTransactionItemDto
{
    public long Id { get; set; }
    public string Type { get; set; }            // "دریافت" / "استفاده"
    public string TypeIcon { get; set; }        // "add_circle" / "remove_circle"
    public string TypeColor { get; set; }       // "success" / "error"
    public decimal Amount { get; set; }
    public string AmountFormatted { get; set; }
    public string Description { get; set; }     // "تخفیف خرید محصول X"
    public string DatePersian { get; set; }
}
```

**فایل 3: GetMyDiscountTransactionsQueryHandler.cs**
```csharp
using MediatR;
using FrontOffice.BFF.Application.Common.Interfaces;

namespace FrontOffice.BFF.Application.UserWalletCQ.Queries.GetMyDiscountTransactions;

public class GetMyDiscountTransactionsQueryHandler 
    : IRequestHandler<GetMyDiscountTransactionsQuery, MyDiscountTransactionsResponseDto>
{
    private readonly ICurrentUserService _currentUser;
    
    public GetMyDiscountTransactionsQueryHandler(ICurrentUserService currentUser)
    {
        _currentUser = currentUser;
    }
    
    public async Task<MyDiscountTransactionsResponseDto> Handle(
        GetMyDiscountTransactionsQuery request,
        CancellationToken cancellationToken)
    {
        var userId = _currentUser.UserId;
        
        // TODO: Call CMS
        
        // Mock Data
        return new MyDiscountTransactionsResponseDto
        {
            Transactions = new List<DiscountTransactionItemDto>
            {
                new() {
                    Id = 1,
                    Type = "دریافت",
                    TypeIcon = "add_circle",
                    TypeColor = "success",
                    Amount = 500000,
                    AmountFormatted = "500,000 تومان",
                    Description = "تخفیف خرید بسته طلایی",
                    DatePersian = "20 آذر 1403"
                },
                new() {
                    Id = 2,
                    Type = "استفاده",
                    TypeIcon = "remove_circle",
                    TypeColor = "error",
                    Amount = -200000,
                    AmountFormatted = "200,000 تومان",
                    Description = "استفاده در خرید محصول A",
                    DatePersian = "22 آذر 1403"
                }
            },
            TotalCount = 2
        };
    }
}
```

#### Task 2.3: آپدیت Controller

**فایل موجود: UserWalletController.cs**
```csharp
// اضافه کردن endpoint جدید
using FrontOffice.BFF.Application.UserWalletCQ.Queries.GetMyDiscountTransactions;

[HttpGet("my-discount-transactions")]
[ProducesResponseType(typeof(MyDiscountTransactionsResponseDto), 200)]
public async Task<IActionResult> GetMyDiscountTransactions(
    [FromQuery] int pageNumber = 1,
    [FromQuery] int pageSize = 10)
{
    var query = new GetMyDiscountTransactionsQuery 
    { 
        PageNumber = pageNumber, 
        PageSize = pageSize 
    };
    var result = await _mediator.Send(query);
    return Ok(result);
}
```

---

### 📝 STEP 3: تکمیل Withdrawal Handler

**Task 3.1: پیدا کردن WithdrawalFromWallet Handler**
```bash
find FrontOffice.BFF.Application/UserWalletCQ/ -name "*Withdrawal*"
# باید پیدا کنی: Commands/WithdrawalFromWallet/WithdrawalFromWalletCommandHandler.cs
```

**Task 3.2: تکمیل Handler خالی**
```csharp
// فایل موجود: WithdrawalFromWalletCommandHandler.cs
using MediatR;
using FrontOffice.BFF.Application.Common.Interfaces;

namespace FrontOffice.BFF.Application.UserWalletCQ.Commands.WithdrawalFromWallet;

public class WithdrawalFromWalletCommandHandler 
    : IRequestHandler<WithdrawalFromWalletCommand, WithdrawalFromWalletResponseDto>
{
    private readonly ICurrentUserService _currentUser;
    // TODO: private readonly UserWalletServiceClient _cmsClient;
    
    public WithdrawalFromWalletCommandHandler(ICurrentUserService currentUser)
    {
        _currentUser = currentUser;
    }
    
    public async Task<WithdrawalFromWalletResponseDto> Handle(
        WithdrawalFromWalletCommand request,
        CancellationToken cancellationToken)
    {
        var userId = _currentUser.UserId;
        
        // TODO: فراخوانی CMS
        // var result = await _cmsClient.WithdrawalFromWalletAsync(new {
        //     UserId = userId,
        //     Amount = request.Amount,
        //     WalletType = request.WalletType  // Main / Reward / Discount
        // });
        
        // Mock Response
        return new WithdrawalFromWalletResponseDto
        {
            Success = true,
            TransactionId = 456,
            Message = "برداشت با موفقیت انجام شد",
            NewBalance = "4,500,000 تومان"
        };
    }
}
```

**Task 3.3: اضافه کردن Validator**
```csharp
// فایل جدید: WithdrawalFromWalletCommandValidator.cs
using FluentValidation;

namespace FrontOffice.BFF.Application.UserWalletCQ.Commands.WithdrawalFromWallet;

public class WithdrawalFromWalletCommandValidator : AbstractValidator<WithdrawalFromWalletCommand>
{
    public WithdrawalFromWalletCommandValidator()
    {
        RuleFor(x => x.Amount)
            .GreaterThan(0).WithMessage("مبلغ باید بیشتر از صفر باشد")
            .LessThanOrEqualTo(10000000).WithMessage("حداکثر مبلغ برداشت 10 میلیون تومان است");
        
        RuleFor(x => x.WalletType)
            .NotEmpty().WithMessage("نوع کیف پول الزامی است")
            .Must(x => new[] { "Main", "Reward", "Discount" }.Contains(x))
            .WithMessage("نوع کیف پول نامعتبر است");
    }
}
```

---

### 📝 STEP 4: آپدیت UI - WalletPage

#### Task 4.1: اضافه کردن DiscountBalance به Service
```csharp
// فایل موجود: FrontOffice.Main/Services/UserWalletService.cs
public async Task<MyDiscountTransactionsDto> GetMyDiscountTransactionsAsync(int pageNumber = 1)
{
    var response = await _httpClient.GetAsync(
        $"/api/userwallet/my-discount-transactions?pageNumber={pageNumber}");
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadFromJsonAsync<MyDiscountTransactionsDto>();
}
```

#### Task 4.2: آپدیت WalletPage.razor

**اضافه کردن Card برای DiscountBalance:**
```razor
@page "/wallet"
@inject UserWalletService WalletService
@inject ISnackbar Snackbar

<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
    <MudText Typo="Typo.h4" Class="mb-4">کیف پول من</MudText>
    
    @if (_loading)
    {
        <MudProgressLinear Indeterminate="true" />
    }
    else if (_wallet != null)
    {
        <MudGrid>
            <!-- موجودی اصلی -->
            <MudItem xs="12" md="4">
                <MudCard Style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
                    <MudCardContent>
                        <MudText Typo="Typo.h6">موجودی اصلی</MudText>
                        <MudText Typo="Typo.h4" Class="mt-2">
                            <strong>@_wallet.MainBalanceFormatted</strong>
                        </MudText>
                    </MudCardContent>
                </MudCard>
            </MudItem>
            
            <!-- موجودی پاداش -->
            <MudItem xs="12" md="4">
                <MudCard Style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: white;">
                    <MudCardContent>
                        <MudText Typo="Typo.h6">موجودی پاداش</MudText>
                        <MudText Typo="Typo.h4" Class="mt-2">
                            <strong>@_wallet.RewardBalanceFormatted</strong>
                        </MudText>
                    </MudCardContent>
                </MudCard>
            </MudItem>
            
            <!-- موجودی تخفیف (جدید) -->
            <MudItem xs="12" md="4">
                <MudCard Style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); color: white;">
                    <MudCardContent>
                        <MudText Typo="Typo.h6">موجودی تخفیف</MudText>
                        <MudText Typo="Typo.h4" Class="mt-2">
                            <strong>@_wallet.DiscountBalanceFormatted</strong>
                        </MudText>
                        @if (_wallet.HasDiscount)
                        {
                            <MudChip Size="Size.Small" Color="Color.Success" Class="mt-2">
                                @_wallet.DiscountPercentage تخفیف
                            </MudChip>
                        }
                    </MudCardContent>
                </MudCard>
            </MudItem>
            
            <!-- مجموع کل -->
            <MudItem xs="12">
                <MudCard Elevation="5">
                    <MudCardContent>
                        <MudText Typo="Typo.h5" Align="Align.Center">
                            مجموع کل: <strong style="color: green;">@_wallet.TotalBalanceFormatted</strong>
                        </MudText>
                    </MudCardContent>
                </MudCard>
            </MudItem>
        </MudGrid>
        
        <!-- Tabs برای تراکنش‌ها -->
        <MudTabs Class="mt-6" ApplyEffectsToContainer="true" PanelClass="pa-4">
            <MudTabPanel Text="تراکنش‌های اصلی" Icon="@Icons.Material.Filled.Receipt">
                <!-- کد موجود برای Main Transactions -->
            </MudTabPanel>
            
            <MudTabPanel Text="تراکنش‌های پاداش" Icon="@Icons.Material.Filled.CardGiftcard">
                <!-- کد موجود برای Reward Transactions -->
            </MudTabPanel>
            
            <!-- Tab جدید برای DiscountTransactions -->
            <MudTabPanel Text="تراکنش‌های تخفیف" Icon="@Icons.Material.Filled.Discount">
                @if (_loadingDiscountTxs)
                {
                    <MudProgressLinear Indeterminate="true" />
                }
                else if (_discountTransactions != null)
                {
                    <MudTable Items="@_discountTransactions.Transactions" Hover="true">
                        <HeaderContent>
                            <MudTh>نوع</MudTh>
                            <MudTh>مبلغ</MudTh>
                            <MudTh>شرح</MudTh>
                            <MudTh>تاریخ</MudTh>
                        </HeaderContent>
                        <RowTemplate>
                            <MudTd>
                                <MudIcon Icon="@context.TypeIcon" 
                                        Color="@(context.TypeColor == "success" ? Color.Success : Color.Error)" />
                                @context.Type
                            </MudTd>
                            <MudTd>
                                <strong style="color: @(context.Amount > 0 ? "green" : "red")">
                                    @context.AmountFormatted
                                </strong>
                            </MudTd>
                            <MudTd>@context.Description</MudTd>
                            <MudTd>@context.DatePersian</MudTd>
                        </RowTemplate>
                    </MudTable>
                }
            </MudTabPanel>
        </MudTabs>
    }
</MudContainer>

@code {
    private MyWalletDto? _wallet;
    private MyDiscountTransactionsDto? _discountTransactions;
    private bool _loading = true;
    private bool _loadingDiscountTxs = true;
    
    protected override async Task OnInitializedAsync()
    {
        await LoadWallet();
        await LoadDiscountTransactions();
    }
    
    private async Task LoadWallet()
    {
        try
        {
            _loading = true;
            _wallet = await WalletService.GetMyWalletAsync();
        }
        catch (Exception ex)
        {
            Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
        }
        finally
        {
            _loading = false;
        }
    }
    
    private async Task LoadDiscountTransactions()
    {
        try
        {
            _loadingDiscountTxs = true;
            _discountTransactions = await WalletService.GetMyDiscountTransactionsAsync();
        }
        catch (Exception ex)
        {
            Snackbar.Add($"خطا در بارگذاری تراکنش‌های تخفیف: {ex.Message}", Severity.Error);
        }
        finally
        {
            _loadingDiscountTxs = false;
        }
    }
}
```

---

### ✅ Checkpoint نهایی

```bash
cd /home/masoud/Apps/project/FourSat/FrontOffice.BFF/src/
dotnet build

cd /home/masoud/Apps/project/FourSat/FrontOffice/src/
dotnet build
```

**چیزهایی که باید کار کنند:**
```
[ ] GetMyWallet شامل DiscountBalance است
[ ] GetMyDiscountTransactions Query کار می‌کند
[ ] WithdrawalFromWallet Handler تکمیل شده
[ ] Validator برای Withdrawal اضافه شده
[ ] UI سه کارت موجودی نمایش می‌دهد
[ ] Tab جدید "تراکنش‌های تخفیف" کار می‌کند
```

---

### 📊 آماری از تکمیل UserWallet

| مورد | قبل | بعد | وضعیت |
|------|-----|-----|-------|
| Queries | 2 | 3 | ✅ +1 |
| Commands | 2 | 2 | ✅ Handler تکمیل شد |
| Validators | 1 | 2 | ✅ +1 |
| UI Cards | 2 | 3 | ✅ +1 |
| UI Tabs | 2 | 3 | ✅ +1 |
| درصد تکمیل | 60% | 100% | 🎉 |

**زمان تخمینی:** 2 ساعت

---

### 💡 نکات مهم

1. **DiscountBalance vs RewardBalance**: تخفیف فقط در خرید استفاده می‌شود، پاداش قابل برداشت است
2. **Gradient Colors**: از Linear Gradient برای Cards استفاده شد - زیباتر است
3. **Tabs Performance**: از `MudTabs` استفاده کن - بهتر از Separate Pages
4. **Amount Sign**: در DiscountTransactions مبلغ‌های منفی را با رنگ قرمز نشان بده
5. **Validator Registration**: فراموش نکن Validator را در DI ثبت کنی


---

## 🛒 مرحله 6: تکمیل ShoppingCart (سبد خرید)

### 📊 وضعیت فعلی

**موجود در BFF (50%):**
- ✅ GetMyCart Query
- ✅ AddToCart Command
- ✅ UpdateCartItemQuantity Command

**غایب (50%):**
- ❌ ClearCart Command (پاک کردن کل سبد)
- ❌ DeleteCartItem Command (حذف یک آیتم)
- ❌ MergeGuestCart Command (ادغام سبد مهمان با سبد کاربر لاگین شده)
- ❌ ApplyDiscount Command (اعمال کد تخفیف)

**UI غایب:**
- ❌ دکمه "پاک کردن سبد"
- ❌ دکمه "حذف" برای هر آیتم
- ❌ فرم اعمال کد تخفیف

---

### 📝 STEP 1: بررسی CMS ShoppingCart

#### Task 1.1: بررسی Commands موجود
```bash
cd /home/masoud/Apps/project/FourSat/CMS/src/

ls CMSMicroservice.Application/ShoppingCartCQ/Commands/
# باید ببینی:
#   - AddToCart/ ✅
#   - UpdateCartItemQuantity/ ✅
#   - DeleteCartItem/ (چک کن وجود دارد؟)
#   - ClearCart/ (چک کن وجود دارد؟)
#   - MergeGuestCart/ (چک کن وجود دارد؟)
#   - ApplyDiscountCode/ (چک کن وجود دارد؟)
```

#### Task 1.2: بررسی DeleteCartItem در CMS
```bash
# اگر وجود داشت:
cat CMSMicroservice.Application/ShoppingCartCQ/Commands/DeleteCartItem/DeleteCartItemCommandHandler.cs
# Input:
#   - UserId
#   - CartItemId
# Logic:
#   - پیدا کردن CartItem
#   - حذف از دیتابیس
#   - به‌روزرسانی TotalPrice سبد
```

#### Task 1.3: بررسی ClearCart در CMS
```bash
# اگر وجود داشت:
cat CMSMicroservice.Application/ShoppingCartCQ/Commands/ClearCart/ClearCartCommandHandler.cs
# Input:
#   - UserId
# Logic:
#   - حذف همه CartItems کاربر
#   - TotalPrice = 0
```

---

### 📝 STEP 2: پیاده‌سازی Commands غایب در BFF

#### Task 2.1: Command - DeleteMyCartItem

**فایل 1: DeleteMyCartItemCommand.cs**
```bash
mkdir -p FrontOffice.BFF.Application/ShoppingCartCQ/Commands/DeleteMyCartItem/
nano DeleteMyCartItemCommand.cs
```

```csharp
using MediatR;

namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.DeleteMyCartItem;

/// <summary>
/// حذف یک آیتم از سبد خرید من
/// </summary>
public record DeleteMyCartItemCommand : IRequest<DeleteMyCartItemResponseDto>
{
    public long CartItemId { get; init; }
}
```

**فایل 2: DeleteMyCartItemResponseDto.cs**
```csharp
namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.DeleteMyCartItem;

public class DeleteMyCartItemResponseDto
{
    public bool Success { get; set; }
    public string Message { get; set; }             // "آیتم با موفقیت حذف شد"
    public decimal NewTotalPrice { get; set; }
    public string NewTotalPriceFormatted { get; set; }
    public int RemainingItemsCount { get; set; }    // تعداد آیتم‌های باقی‌مانده
}
```

**فایل 3: DeleteMyCartItemCommandHandler.cs**
```csharp
using MediatR;
using FrontOffice.BFF.Application.Common.Interfaces;

namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.DeleteMyCartItem;

public class DeleteMyCartItemCommandHandler 
    : IRequestHandler<DeleteMyCartItemCommand, DeleteMyCartItemResponseDto>
{
    private readonly ICurrentUserService _currentUser;
    // TODO: private readonly ShoppingCartServiceClient _cmsClient;
    
    public DeleteMyCartItemCommandHandler(ICurrentUserService currentUser)
    {
        _currentUser = currentUser;
    }
    
    public async Task<DeleteMyCartItemResponseDto> Handle(
        DeleteMyCartItemCommand request,
        CancellationToken cancellationToken)
    {
        var userId = _currentUser.UserId;
        
        // TODO: Call CMS
        // var result = await _cmsClient.DeleteCartItemAsync(new {
        //     UserId = userId,
        //     CartItemId = request.CartItemId
        // });
        
        // Mock Response
        return new DeleteMyCartItemResponseDto
        {
            Success = true,
            Message = "محصول از سبد خرید حذف شد",
            NewTotalPrice = 4500000,
            NewTotalPriceFormatted = "4,500,000 تومان",
            RemainingItemsCount = 2
        };
    }
}
```

**فایل 4: DeleteMyCartItemCommandValidator.cs**
```csharp
using FluentValidation;

namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.DeleteMyCartItem;

public class DeleteMyCartItemCommandValidator : AbstractValidator<DeleteMyCartItemCommand>
{
    public DeleteMyCartItemCommandValidator()
    {
        RuleFor(x => x.CartItemId)
            .GreaterThan(0).WithMessage("شناسه آیتم نامعتبر است");
    }
}
```

#### Task 2.2: Command - ClearMyCart

**فایل 1: ClearMyCartCommand.cs**
```bash
mkdir -p FrontOffice.BFF.Application/ShoppingCartCQ/Commands/ClearMyCart/
nano ClearMyCartCommand.cs
```

```csharp
using MediatR;

namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.ClearMyCart;

/// <summary>
/// پاک کردن کل سبد خرید من
/// </summary>
public record ClearMyCartCommand : IRequest<ClearMyCartResponseDto>
{
    // هیچ ورودی ندارد - UserId از Token می‌آید
}
```

**فایل 2: ClearMyCartResponseDto.cs**
```csharp
namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.ClearMyCart;

public class ClearMyCartResponseDto
{
    public bool Success { get; set; }
    public string Message { get; set; }  // "سبد خرید شما خالی شد"
    public int DeletedItemsCount { get; set; }
}
```

**فایل 3: ClearMyCartCommandHandler.cs**
```csharp
using MediatR;
using FrontOffice.BFF.Application.Common.Interfaces;

namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.ClearMyCart;

public class ClearMyCartCommandHandler 
    : IRequestHandler<ClearMyCartCommand, ClearMyCartResponseDto>
{
    private readonly ICurrentUserService _currentUser;
    
    public ClearMyCartCommandHandler(ICurrentUserService currentUser)
    {
        _currentUser = currentUser;
    }
    
    public async Task<ClearMyCartResponseDto> Handle(
        ClearMyCartCommand request,
        CancellationToken cancellationToken)
    {
        var userId = _currentUser.UserId;
        
        // TODO: Call CMS
        // var result = await _cmsClient.ClearCartAsync(new { UserId = userId });
        
        return new ClearMyCartResponseDto
        {
            Success = true,
            Message = "سبد خرید شما با موفقیت خالی شد",
            DeletedItemsCount = 3
        };
    }
}
```

#### Task 2.3: Command - MergeGuestCart (اختیاری - پیچیده‌تر)

**فایل 1: MergeGuestCartCommand.cs**
```bash
mkdir -p FrontOffice.BFF.Application/ShoppingCartCQ/Commands/MergeGuestCart/
nano MergeGuestCartCommand.cs
```

```csharp
using MediatR;

namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.MergeGuestCart;

/// <summary>
/// ادغام سبد مهمان با سبد کاربر لاگین شده
/// زمانی استفاده می‌شود که کاربر بدون لاگین خرید می‌کند و بعد لاگین می‌کند
/// </summary>
public record MergeGuestCartCommand : IRequest<MergeGuestCartResponseDto>
{
    public string GuestCartId { get; init; }  // GUID سبد مهمان (از LocalStorage)
}
```

**فایل 2: MergeGuestCartResponseDto.cs**
```csharp
namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.MergeGuestCart;

public class MergeGuestCartResponseDto
{
    public bool Success { get; set; }
    public string Message { get; set; }
    public int MergedItemsCount { get; set; }      // تعداد آیتم‌های ادغام شده
    public decimal NewTotalPrice { get; set; }
    public string NewTotalPriceFormatted { get; set; }
}
```

**فایل 3: MergeGuestCartCommandHandler.cs**
```csharp
using MediatR;
using FrontOffice.BFF.Application.Common.Interfaces;

namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.MergeGuestCart;

public class MergeGuestCartCommandHandler 
    : IRequestHandler<MergeGuestCartCommand, MergeGuestCartResponseDto>
{
    private readonly ICurrentUserService _currentUser;
    
    public MergeGuestCartCommandHandler(ICurrentUserService currentUser)
    {
        _currentUser = currentUser;
    }
    
    public async Task<MergeGuestCartResponseDto> Handle(
        MergeGuestCartCommand request,
        CancellationToken cancellationToken)
    {
        var userId = _currentUser.UserId;
        
        // TODO: Call CMS
        // Logic:
        //   1. دریافت سبد مهمان از GuestCartId
        //   2. دریافت سبد کاربر فعلی
        //   3. ادغام آیتم‌ها (اگر محصول تکراری بود، Quantity جمع شود)
        //   4. حذف سبد مهمان
        
        return new MergeGuestCartResponseDto
        {
            Success = true,
            Message = "سبد خرید شما با موفقیت ادغام شد",
            MergedItemsCount = 2,
            NewTotalPrice = 6500000,
            NewTotalPriceFormatted = "6,500,000 تومان"
        };
    }
}
```

**فایل 4: MergeGuestCartCommandValidator.cs**
```csharp
using FluentValidation;

namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.MergeGuestCart;

public class MergeGuestCartCommandValidator : AbstractValidator<MergeGuestCartCommand>
{
    public MergeGuestCartCommandValidator()
    {
        RuleFor(x => x.GuestCartId)
            .NotEmpty().WithMessage("شناسه سبد مهمان الزامی است")
            .Must(BeValidGuid).WithMessage("شناسه سبد نامعتبر است");
    }
    
    private bool BeValidGuid(string guestCartId)
    {
        return Guid.TryParse(guestCartId, out _);
    }
}
```

---

### 📝 STEP 3: آپدیت Controller

**فایل موجود: ShoppingCartController.cs**

اضافه کردن 3 endpoint جدید:

```csharp
using FrontOffice.BFF.Application.ShoppingCartCQ.Commands.DeleteMyCartItem;
using FrontOffice.BFF.Application.ShoppingCartCQ.Commands.ClearMyCart;
using FrontOffice.BFF.Application.ShoppingCartCQ.Commands.MergeGuestCart;

/// <summary>
/// حذف یک آیتم از سبد خرید
/// </summary>
[HttpDelete("items/{cartItemId}")]
[ProducesResponseType(typeof(DeleteMyCartItemResponseDto), 200)]
public async Task<IActionResult> DeleteCartItem(long cartItemId)
{
    var command = new DeleteMyCartItemCommand { CartItemId = cartItemId };
    var result = await _mediator.Send(command);
    return Ok(result);
}

/// <summary>
/// پاک کردن کل سبد خرید
/// </summary>
[HttpDelete("clear")]
[ProducesResponseType(typeof(ClearMyCartResponseDto), 200)]
public async Task<IActionResult> ClearCart()
{
    var command = new ClearMyCartCommand();
    var result = await _mediator.Send(command);
    return Ok(result);
}

/// <summary>
/// ادغام سبد مهمان
/// </summary>
[HttpPost("merge-guest")]
[ProducesResponseType(typeof(MergeGuestCartResponseDto), 200)]
public async Task<IActionResult> MergeGuestCart([FromBody] MergeGuestCartCommand command)
{
    var result = await _mediator.Send(command);
    return Ok(result);
}
```

**Test Endpoints:**
```bash
# Test 1: Delete Item
curl -X DELETE \
     -H "Authorization: Bearer TOKEN" \
     http://localhost:5002/api/shoppingcart/items/123

# Test 2: Clear Cart
curl -X DELETE \
     -H "Authorization: Bearer TOKEN" \
     http://localhost:5002/api/shoppingcart/clear

# Test 3: Merge Guest Cart
curl -X POST \
     -H "Authorization: Bearer TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"guestCartId": "550e8400-e29b-41d4-a716-446655440000"}' \
     http://localhost:5002/api/shoppingcart/merge-guest
```

---

### 📝 STEP 4: آپدیت UI - CartPage

#### Task 4.1: اضافه کردن متدها به Service
```csharp
// فایل موجود: FrontOffice.Main/Services/ShoppingCartService.cs

public async Task<DeleteCartItemResultDto> DeleteCartItemAsync(long cartItemId)
{
    var response = await _httpClient.DeleteAsync($"/api/shoppingcart/items/{cartItemId}");
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadFromJsonAsync<DeleteCartItemResultDto>();
}

public async Task<ClearCartResultDto> ClearCartAsync()
{
    var response = await _httpClient.DeleteAsync("/api/shoppingcart/clear");
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadFromJsonAsync<ClearCartResultDto>();
}

public async Task<MergeGuestCartResultDto> MergeGuestCartAsync(string guestCartId)
{
    var request = new { GuestCartId = guestCartId };
    var response = await _httpClient.PostAsJsonAsync("/api/shoppingcart/merge-guest", request);
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadFromJsonAsync<MergeGuestCartResultDto>();
}
```

#### Task 4.2: آپدیت CartPage.razor

**اضافه کردن دکمه‌های حذف:**

```razor
@page "/cart"
@inject ShoppingCartService CartService
@inject ISnackbar Snackbar
@inject IDialogService DialogService

<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
    <MudGrid>
        <MudItem xs="12">
            <MudText Typo="Typo.h4" Class="mb-4">سبد خرید من</MudText>
        </MudItem>
        
        @if (_loading)
        {
            <MudProgressLinear Indeterminate="true" />
        }
        else if (_cart != null && _cart.Items.Any())
        {
            <MudItem xs="12" md="8">
                <MudCard>
                    <MudCardHeader>
                        <CardHeaderContent>
                            <MudText Typo="Typo.h6">
                                محصولات (@_cart.TotalItemsCount مورد)
                            </MudText>
                        </CardHeaderContent>
                        <CardHeaderActions>
                            <MudButton StartIcon="@Icons.Material.Filled.DeleteSweep"
                                     Color="Color.Error"
                                     Variant="Variant.Text"
                                     OnClick="ClearCartWithConfirm">
                                پاک کردن سبد
                            </MudButton>
                        </CardHeaderActions>
                    </MudCardHeader>
                    <MudCardContent>
                        @foreach (var item in _cart.Items)
                        {
                            <MudPaper Class="pa-3 mb-3" Elevation="2">
                                <MudGrid>
                                    <MudItem xs="12" md="6">
                                        <MudText Typo="Typo.h6">@item.ProductName</MudText>
                                        <MudText Typo="Typo.body2" Color="Color.Secondary">
                                            @item.ProductDescription
                                        </MudText>
                                    </MudItem>
                                    <MudItem xs="6" md="2">
                                        <MudNumericField @bind-Value="item.Quantity"
                                                       Label="تعداد"
                                                       Min="1"
                                                       Max="10"
                                                       Variant="Variant.Outlined"
                                                       OnChange="() => UpdateQuantity(item.CartItemId, item.Quantity)" />
                                    </MudItem>
                                    <MudItem xs="6" md="2">
                                        <MudText Typo="Typo.body1">
                                            <strong>@item.TotalPriceFormatted</strong>
                                        </MudText>
                                    </MudItem>
                                    <MudItem xs="12" md="2">
                                        <MudButton StartIcon="@Icons.Material.Filled.Delete"
                                                 Color="Color.Error"
                                                 Variant="Variant.Text"
                                                 FullWidth="true"
                                                 OnClick="() => DeleteItem(item.CartItemId)">
                                            حذف
                                        </MudButton>
                                    </MudItem>
                                </MudGrid>
                            </MudPaper>
                        }
                    </MudCardContent>
                </MudCard>
            </MudItem>
            
            <MudItem xs="12" md="4">
                <MudCard>
                    <MudCardHeader>
                        <CardHeaderContent>
                            <MudText Typo="Typo.h6">خلاصه سبد خرید</MudText>
                        </CardHeaderContent>
                    </MudCardHeader>
                    <MudCardContent>
                        <MudStack Spacing="2">
                            <MudText>
                                <strong>تعداد کل:</strong> @_cart.TotalItemsCount مورد
                            </MudText>
                            <MudText>
                                <strong>قیمت کل:</strong> 
                                <span style="color: green; font-size: 1.2em;">
                                    @_cart.TotalPriceFormatted
                                </span>
                            </MudText>
                            <MudDivider />
                            <MudButton Variant="Variant.Filled"
                                     Color="Color.Primary"
                                     FullWidth="true"
                                     StartIcon="@Icons.Material.Filled.ShoppingCart">
                                تکمیل خرید
                            </MudButton>
                        </MudStack>
                    </MudCardContent>
                </MudCard>
            </MudItem>
        }
        else
        {
            <MudItem xs="12">
                <MudAlert Severity="Severity.Info">
                    سبد خرید شما خالی است
                </MudAlert>
            </MudItem>
        }
    </MudGrid>
</MudContainer>

@code {
    private MyCartDto? _cart;
    private bool _loading = true;
    
    protected override async Task OnInitializedAsync()
    {
        await LoadCart();
    }
    
    private async Task LoadCart()
    {
        try
        {
            _loading = true;
            _cart = await CartService.GetMyCartAsync();
        }
        catch (Exception ex)
        {
            Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
        }
        finally
        {
            _loading = false;
        }
    }
    
    private async Task UpdateQuantity(long cartItemId, int newQuantity)
    {
        try
        {
            await CartService.UpdateCartItemQuantityAsync(cartItemId, newQuantity);
            Snackbar.Add("تعداد به‌روزرسانی شد", Severity.Success);
            await LoadCart();
        }
        catch (Exception ex)
        {
            Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
        }
    }
    
    private async Task DeleteItem(long cartItemId)
    {
        bool? confirm = await DialogService.ShowMessageBox(
            "تایید حذف",
            "آیا از حذف این محصول اطمینان دارید؟",
            yesText: "بله", cancelText: "خیر");
        
        if (confirm == true)
        {
            try
            {
                var result = await CartService.DeleteCartItemAsync(cartItemId);
                Snackbar.Add(result.Message, Severity.Success);
                await LoadCart();
            }
            catch (Exception ex)
            {
                Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
            }
        }
    }
    
    private async Task ClearCartWithConfirm()
    {
        bool? confirm = await DialogService.ShowMessageBox(
            "پاک کردن سبد",
            "آیا از پاک کردن کل سبد خرید اطمینان دارید؟",
            yesText: "بله، پاک کن", cancelText: "خیر");
        
        if (confirm == true)
        {
            try
            {
                var result = await CartService.ClearCartAsync();
                Snackbar.Add(result.Message, Severity.Success);
                await LoadCart();
            }
            catch (Exception ex)
            {
                Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
            }
        }
    }
}
```

---

### ✅ Checkpoint نهایی

```bash
cd /home/masoud/Apps/project/FourSat/FrontOffice.BFF/src/
dotnet build

cd /home/masoud/Apps/project/FourSat/FrontOffice/src/
dotnet build
```

**چیزهایی که باید کار کنند:**
```
[ ] DeleteMyCartItem Command کار می‌کند
[ ] ClearMyCart Command کار می‌کند
[ ] MergeGuestCart Command کار می‌کند
[ ] 3 Validator اضافه شده
[ ] 3 endpoint جدید در Controller
[ ] UI دکمه "حذف" برای هر آیتم دارد
[ ] UI دکمه "پاک کردن سبد" دارد
[ ] Confirmation Dialog نمایش داده می‌شود
```

---

### 📊 آماری از تکمیل ShoppingCart

| مورد | قبل | بعد | وضعیت |
|------|-----|-----|-------|
| Commands | 3 | 6 | ✅ +3 |
| Validators | 1 | 4 | ✅ +3 |
| Controller Endpoints | 3 | 6 | ✅ +3 |
| UI Delete Button | ❌ | ✅ | ✅ |
| UI Clear Button | ❌ | ✅ | ✅ |
| Confirmation Dialogs | ❌ | ✅ | ✅ |
| درصد تکمیل | 50% | 100% | 🎉 |

**زمان تخمینی:** 3 ساعت

---

### 💡 نکات بسیار مهم

1. **Confirmation Dialog**: همیشه قبل از حذف از کاربر تایید بگیر (UX بهتر)
2. **DeleteCartItem vs ClearCart**: Delete یک آیتم حذف می‌کند، Clear همه را پاک می‌کند
3. **MergeGuestCart**: این قابلیت برای زمانی است که کاربر بدون لاگین خرید کرده و بعد لاگین کند
4. **LocalStorage**: سبد مهمان در LocalStorage ذخیره شود (GuestCartId = Guid)
5. **Quantity Update**: بلافاصله بعد از تغییر Quantity، Cart را reload کن
6. **HTTP Methods**: Delete → `DeleteAsync`, Clear → `DeleteAsync`, Merge → `PostAsync`
7. **Icon Usage**: از `DeleteSweep` برای Clear و `Delete` برای DeleteItem استفاده کن


---

## 💳 مرحله 7: DayaLoan UI + Contract Completion

### 📊 خلاصه این مرحله

**دو کار اصلی:**
1. **DayaLoan UI**: نمایش وضعیت وام دایا برای مشتری (Backend در CMS آماده است)
2. **Contract Completion**: تکمیل Queries غایب در Contract Module

---

## بخش اول: DayaLoan - نمایش وضعیت وام

### 📝 STEP 1: بررسی DayaLoan در CMS

#### Task 1.1: بررسی موجودی‌ها
```bash
cd /home/masoud/Apps/project/FourSat/CMS/src/

# بررسی Entity
cat CMSMicroservice.Domain/Entities/DayaLoanContract.cs
# باید ببینی:
#   - NationalCode: کد ملی
#   - LoanStatus: PendingReceive / Approved / Rejected
#   - ContractNumber: شماره قرارداد (بعد از تایید)
#   - RequestAmount: 56,000,000 (برای هر کیف پول)
#   - LastCheckDate: آخرین بار استعلام
#   - ApprovalDate: تاریخ تایید

# بررسی Commands
ls CMSMicroservice.Application/DayaLoanCQ/Commands/
# باید ببینی:
#   - CheckDayaLoanStatus/ ✅
#   - ProcessDayaLoanApproval/ ✅

# بررسی Worker
find . -name "*DayaLoanWorker*"
# Worker که هر 15 دقیقه استعلام می‌کند
```

**Output Task 1.1:**
```
[ ] DayaLoanContract Entity را بررسی کردم
[ ] CheckDayaLoanStatus Command را دیدم
[ ] ProcessDayaLoanApproval Command را دیدم
[ ] Worker را پیدا کردم
```

---

### 📝 STEP 2: ایجاد BFF Module - DayaLoanCQ

#### Task 2.1: ساخت فولدرها
```bash
cd /home/masoud/Apps/project/FourSat/FrontOffice.BFF/src/FrontOffice.BFF.Application/

mkdir -p DayaLoanCQ/Queries/GetMyDayaLoanStatus
mkdir -p DayaLoanCQ/Commands/RequestDayaLoanCheck

tree DayaLoanCQ/
```

**Expected Output:**
```
DayaLoanCQ/
├── Commands/
│   └── RequestDayaLoanCheck/
└── Queries/
    └── GetMyDayaLoanStatus/
```

#### Task 2.2: Query - GetMyDayaLoanStatus

**فایل 1: GetMyDayaLoanStatusQuery.cs**
```csharp
using MediatR;

namespace FrontOffice.BFF.Application.DayaLoanCQ.Queries.GetMyDayaLoanStatus;

/// <summary>
/// دریافت وضعیت وام دایا برای کاربر جاری
/// </summary>
public record GetMyDayaLoanStatusQuery : IRequest<MyDayaLoanStatusResponseDto>
{
}
```

**فایل 2: MyDayaLoanStatusResponseDto.cs**
```csharp
namespace FrontOffice.BFF.Application.DayaLoanCQ.Queries.GetMyDayaLoanStatus;

public class MyDayaLoanStatusResponseDto
{
    public bool HasActiveLoan { get; set; }           // آیا وام فعال دارد؟
    public string Status { get; set; }                // "در انتظار دریافت" / "تایید شده" / "رد شده"
    public string StatusColor { get; set; }           // "warning" / "success" / "error"
    public string StatusIcon { get; set; }            // Icon name
    
    public string NationalCode { get; set; }
    public decimal RequestAmount { get; set; }        // 56,000,000
    public string RequestAmountFormatted { get; set; }
    
    public string ContractNumber { get; set; }        // شماره قرارداد (اگر تایید شده)
    public string LastCheckDatePersian { get; set; }  // آخرین استعلام
    public string ApprovalDatePersian { get; set; }   // تاریخ تایید (اگر تایید شده)
    
    public bool CanRequestCheck { get; set; }         // آیا می‌تواند درخواست استعلام دهد؟
    public string NextCheckAvailable { get; set; }    // "امکان استعلام بعد از 1 ساعت"
    
    // برای نمایش جزئیات شارژ کیف پول
    public List<WalletChargeDto> WalletCharges { get; set; }
}

public class WalletChargeDto
{
    public string WalletType { get; set; }            // "Main" / "Reward" / "Discount"
    public string WalletTypePersian { get; set; }     // "کیف پول اصلی"
    public decimal Amount { get; set; }               // 56,000,000
    public string AmountFormatted { get; set; }
    public bool IsCharged { get; set; }               // آیا شارژ شده؟
    public string ChargedDatePersian { get; set; }
}
```

**فایل 3: GetMyDayaLoanStatusQueryHandler.cs**
```csharp
using MediatR;
using FrontOffice.BFF.Application.Common.Interfaces;

namespace FrontOffice.BFF.Application.DayaLoanCQ.Queries.GetMyDayaLoanStatus;

public class GetMyDayaLoanStatusQueryHandler 
    : IRequestHandler<GetMyDayaLoanStatusQuery, MyDayaLoanStatusResponseDto>
{
    private readonly ICurrentUserService _currentUser;
    // TODO: private readonly DayaLoanServiceClient _cmsClient;
    
    public GetMyDayaLoanStatusQueryHandler(ICurrentUserService currentUser)
    {
        _currentUser = currentUser;
    }
    
    public async Task<MyDayaLoanStatusResponseDto> Handle(
        GetMyDayaLoanStatusQuery request,
        CancellationToken cancellationToken)
    {
        var userId = _currentUser.UserId;
        
        // TODO: Call CMS
        // var result = await _cmsClient.GetDayaLoanStatusAsync(new { UserId = userId });
        
        // Mock Data - وضعیت "تایید شده"
        return new MyDayaLoanStatusResponseDto
        {
            HasActiveLoan = true,
            Status = "تایید شده",
            StatusColor = "success",
            StatusIcon = "check_circle",
            
            NationalCode = "1234567890",
            RequestAmount = 56000000,
            RequestAmountFormatted = "56,000,000 تومان",
            
            ContractNumber = "DL-1403-001234",
            LastCheckDatePersian = "25 آذر 1403",
            ApprovalDatePersian = "25 آذر 1403",
            
            CanRequestCheck = false,
            NextCheckAvailable = "وام شما قبلاً تایید شده است",
            
            WalletCharges = new List<WalletChargeDto>
            {
                new() {
                    WalletType = "Main",
                    WalletTypePersian = "کیف پول اصلی",
                    Amount = 56000000,
                    AmountFormatted = "56,000,000 تومان",
                    IsCharged = true,
                    ChargedDatePersian = "25 آذر 1403"
                },
                new() {
                    WalletType = "Reward",
                    WalletTypePersian = "کیف پول پاداش",
                    Amount = 56000000,
                    AmountFormatted = "56,000,000 تومان",
                    IsCharged = true,
                    ChargedDatePersian = "25 آذر 1403"
                },
                new() {
                    WalletType = "Discount",
                    WalletTypePersian = "کیف پول تخفیف",
                    Amount = 56000000,
                    AmountFormatted = "56,000,000 تومان",
                    IsCharged = true,
                    ChargedDatePersian = "25 آذر 1403"
                }
            }
        };
    }
}
```

#### Task 2.3: Command - RequestDayaLoanCheck (اختیاری)

**فایل 1: RequestDayaLoanCheckCommand.cs**
```csharp
using MediatR;

namespace FrontOffice.BFF.Application.DayaLoanCQ.Commands.RequestDayaLoanCheck;

/// <summary>
/// درخواست استعلام فوری وضعیت وام دایا
/// معمولاً Worker این کار را انجام می‌دهد، اما کاربر می‌تواند استعلام فوری بزند
/// </summary>
public record RequestDayaLoanCheckCommand : IRequest<RequestDayaLoanCheckResponseDto>
{
}
```

**فایل 2: RequestDayaLoanCheckResponseDto.cs**
```csharp
namespace FrontOffice.BFF.Application.DayaLoanCQ.Commands.RequestDayaLoanCheck;

public class RequestDayaLoanCheckResponseDto
{
    public bool Success { get; set; }
    public string Message { get; set; }    // "استعلام با موفقیت انجام شد"
    public string NewStatus { get; set; }  // وضعیت جدید
}
```

**فایل 3: RequestDayaLoanCheckCommandHandler.cs**
```csharp
using MediatR;
using FrontOffice.BFF.Application.Common.Interfaces;

namespace FrontOffice.BFF.Application.DayaLoanCQ.Commands.RequestDayaLoanCheck;

public class RequestDayaLoanCheckCommandHandler 
    : IRequestHandler<RequestDayaLoanCheckCommand, RequestDayaLoanCheckResponseDto>
{
    private readonly ICurrentUserService _currentUser;
    
    public RequestDayaLoanCheckCommandHandler(ICurrentUserService currentUser)
    {
        _currentUser = currentUser;
    }
    
    public async Task<RequestDayaLoanCheckResponseDto> Handle(
        RequestDayaLoanCheckCommand request,
        CancellationToken cancellationToken)
    {
        var userId = _currentUser.UserId;
        
        // TODO: Call CMS CheckDayaLoanStatus Command
        
        return new RequestDayaLoanCheckResponseDto
        {
            Success = true,
            Message = "استعلام وضعیت وام با موفقیت انجام شد. نتیجه در صفحه نمایش داده می‌شود.",
            NewStatus = "در انتظار دریافت"
        };
    }
}
```

---

### 📝 STEP 3: Controller - DayaLoanController

**فایل جدید: DayaLoanController.cs**
```csharp
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MediatR;
using FrontOffice.BFF.Application.DayaLoanCQ.Queries.GetMyDayaLoanStatus;
using FrontOffice.BFF.Application.DayaLoanCQ.Commands.RequestDayaLoanCheck;

namespace FrontOffice.BFF.WebApi.Controllers;

[Authorize]
[ApiController]
[Route("api/[controller]")]
public class DayaLoanController : ControllerBase
{
    private readonly IMediator _mediator;
    
    public DayaLoanController(IMediator mediator)
    {
        _mediator = mediator;
    }
    
    /// <summary>
    /// دریافت وضعیت وام دایا من
    /// </summary>
    [HttpGet("my-status")]
    [ProducesResponseType(typeof(MyDayaLoanStatusResponseDto), 200)]
    public async Task<IActionResult> GetMyStatus()
    {
        var query = new GetMyDayaLoanStatusQuery();
        var result = await _mediator.Send(query);
        return Ok(result);
    }
    
    /// <summary>
    /// درخواست استعلام فوری
    /// </summary>
    [HttpPost("request-check")]
    [ProducesResponseType(typeof(RequestDayaLoanCheckResponseDto), 200)]
    public async Task<IActionResult> RequestCheck()
    {
        var command = new RequestDayaLoanCheckCommand();
        var result = await _mediator.Send(command);
        return Ok(result);
    }
}
```

---

### 📝 STEP 4: UI - DayaLoanPage

#### Task 4.1: Service
```csharp
// فایل جدید: FrontOffice.Main/Services/DayaLoanService.cs
using System.Net.Http.Json;
using FrontOffice.Main.Models;

namespace FrontOffice.Main.Services;

public class DayaLoanService
{
    private readonly HttpClient _httpClient;
    
    public DayaLoanService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }
    
    public async Task<MyDayaLoanStatusDto> GetMyStatusAsync()
    {
        var response = await _httpClient.GetAsync("/api/dayaloan/my-status");
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<MyDayaLoanStatusDto>();
    }
    
    public async Task<RequestCheckResultDto> RequestCheckAsync()
    {
        var response = await _httpClient.PostAsync("/api/dayaloan/request-check", null);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<RequestCheckResultDto>();
    }
}
```

**ثبت در Program.cs:**
```csharp
builder.Services.AddScoped<DayaLoanService>();
```

#### Task 4.2: Page - DayaLoanStatusPage.razor
```bash
mkdir -p FrontOffice/src/FrontOffice.Main/Pages/Loan/
nano DayaLoanStatusPage.razor
```

```razor
@page "/loan/daya-status"
@inject DayaLoanService LoanService
@inject ISnackbar Snackbar

<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
    <MudText Typo="Typo.h4" Class="mb-4">وضعیت وام دایا</MudText>
    
    @if (_loading)
    {
        <MudProgressLinear Indeterminate="true" />
    }
    else if (_status != null)
    {
        <MudGrid>
            <!-- کارت وضعیت اصلی -->
            <MudItem xs="12" md="6">
                <MudCard Elevation="5">
                    <MudCardHeader Style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
                        <CardHeaderContent>
                            <MudText Typo="Typo.h5">وضعیت درخواست</MudText>
                        </CardHeaderContent>
                        <CardHeaderActions>
                            <MudIcon Icon="@_status.StatusIcon" Size="Size.Large" />
                        </CardHeaderActions>
                    </MudCardHeader>
                    <MudCardContent>
                        <MudStack Spacing="3">
                            <MudAlert Severity="@GetSeverity(_status.StatusColor)" Dense="true">
                                <strong>@_status.Status</strong>
                            </MudAlert>
                            
                            <MudText>
                                <strong>کد ملی:</strong> @_status.NationalCode
                            </MudText>
                            
                            <MudText>
                                <strong>مبلغ درخواستی (هر کیف پول):</strong> 
                                <span style="color: green; font-size: 1.1em;">
                                    @_status.RequestAmountFormatted
                                </span>
                            </MudText>
                            
                            @if (!string.IsNullOrEmpty(_status.ContractNumber))
                            {
                                <MudText>
                                    <strong>شماره قرارداد:</strong> 
                                    <MudChip Size="Size.Small" Color="Color.Info">
                                        @_status.ContractNumber
                                    </MudChip>
                                </MudText>
                            }
                            
                            <MudText>
                                <strong>آخرین استعلام:</strong> @_status.LastCheckDatePersian
                            </MudText>
                            
                            @if (!string.IsNullOrEmpty(_status.ApprovalDatePersian))
                            {
                                <MudText>
                                    <strong>تاریخ تایید:</strong> @_status.ApprovalDatePersian
                                </MudText>
                            }
                        </MudStack>
                    </MudCardContent>
                    
                    @if (_status.CanRequestCheck)
                    {
                        <MudCardActions>
                            <MudButton Variant="Variant.Filled"
                                     Color="Color.Primary"
                                     StartIcon="@Icons.Material.Filled.Refresh"
                                     Disabled="_checking"
                                     OnClick="RequestCheck">
                                @if (_checking)
                                {
                                    <MudProgressCircular Size="Size.Small" Indeterminate="true" />
                                    <span class="ms-2">در حال استعلام...</span>
                                }
                                else
                                {
                                    <span>استعلام فوری</span>
                                }
                            </MudButton>
                        </MudCardActions>
                    }
                    else
                    {
                        <MudCardActions>
                            <MudText Typo="Typo.caption" Color="Color.Secondary">
                                @_status.NextCheckAvailable
                            </MudText>
                        </MudCardActions>
                    }
                </MudCard>
            </MudItem>
            
            <!-- کارت جزئیات شارژ کیف پول‌ها -->
            <MudItem xs="12" md="6">
                <MudCard Elevation="5">
                    <MudCardHeader>
                        <CardHeaderContent>
                            <MudText Typo="Typo.h6">جزئیات شارژ کیف پول‌ها</MudText>
                        </CardHeaderContent>
                    </MudCardHeader>
                    <MudCardContent>
                        @if (_status.WalletCharges != null && _status.WalletCharges.Any())
                        {
                            <MudStack Spacing="3">
                                @foreach (var wallet in _status.WalletCharges)
                                {
                                    <MudPaper Class="pa-3" Elevation="2">
                                        <MudGrid>
                                            <MudItem xs="8">
                                                <MudText Typo="Typo.body1">
                                                    <strong>@wallet.WalletTypePersian</strong>
                                                </MudText>
                                                <MudText Typo="Typo.body2" Color="Color.Success">
                                                    @wallet.AmountFormatted
                                                </MudText>
                                            </MudItem>
                                            <MudItem xs="4" Class="d-flex align-center justify-end">
                                                @if (wallet.IsCharged)
                                                {
                                                    <MudIcon Icon="@Icons.Material.Filled.CheckCircle" 
                                                           Color="Color.Success" 
                                                           Size="Size.Large" />
                                                }
                                                else
                                                {
                                                    <MudIcon Icon="@Icons.Material.Filled.HourglassEmpty" 
                                                           Color="Color.Warning" 
                                                           Size="Size.Large" />
                                                }
                                            </MudItem>
                                        </MudGrid>
                                        @if (wallet.IsCharged)
                                        {
                                            <MudText Typo="Typo.caption" Color="Color.Secondary">
                                                شارژ شده در: @wallet.ChargedDatePersian
                                            </MudText>
                                        }
                                    </MudPaper>
                                }
                                
                                <MudDivider />
                                
                                <MudText Typo="Typo.h6" Align="Align.Center">
                                    مجموع کل شارژ: 
                                    <strong style="color: green;">
                                        @((56000000m * 3).ToString("N0")) تومان
                                    </strong>
                                </MudText>
                            </MudStack>
                        }
                        else
                        {
                            <MudAlert Severity="Severity.Info">
                                هنوز کیف پولی شارژ نشده است
                            </MudAlert>
                        }
                    </MudCardContent>
                </MudCard>
            </MudItem>
            
            <!-- راهنما -->
            <MudItem xs="12">
                <MudCard>
                    <MudCardContent>
                        <MudText Typo="Typo.h6" Class="mb-2">
                            <MudIcon Icon="@Icons.Material.Filled.Info" /> راهنما
                        </MudText>
                        <MudList Dense="true">
                            <MudListItem Icon="@Icons.Material.Filled.Circle" IconSize="Size.Small">
                                وام دایا برای هر کیف پول (اصلی، پاداش، تخفیف) به مبلغ 56 میلیون تومان است
                            </MudListItem>
                            <MudListItem Icon="@Icons.Material.Filled.Circle" IconSize="Size.Small">
                                سیستم هر 15 دقیقه یکبار وضعیت وام شما را بررسی می‌کند
                            </MudListItem>
                            <MudListItem Icon="@Icons.Material.Filled.Circle" IconSize="Size.Small">
                                در صورت تایید، کیف پول‌های شما به صورت خودکار شارژ خواهند شد
                            </MudListItem>
                        </MudList>
                    </MudCardContent>
                </MudCard>
            </MudItem>
        </MudGrid>
    }
</MudContainer>

@code {
    private MyDayaLoanStatusDto? _status;
    private bool _loading = true;
    private bool _checking = false;
    
    protected override async Task OnInitializedAsync()
    {
        await LoadStatus();
    }
    
    private async Task LoadStatus()
    {
        try
        {
            _loading = true;
            _status = await LoanService.GetMyStatusAsync();
        }
        catch (Exception ex)
        {
            Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
        }
        finally
        {
            _loading = false;
        }
    }
    
    private async Task RequestCheck()
    {
        try
        {
            _checking = true;
            var result = await LoanService.RequestCheckAsync();
            Snackbar.Add(result.Message, Severity.Success);
            
            // Reload status after 2 seconds
            await Task.Delay(2000);
            await LoadStatus();
        }
        catch (Exception ex)
        {
            Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
        }
        finally
        {
            _checking = false;
        }
    }
    
    private Severity GetSeverity(string color)
    {
        return color switch
        {
            "success" => Severity.Success,
            "warning" => Severity.Warning,
            "error" => Severity.Error,
            "info" => Severity.Info,
            _ => Severity.Normal
        };
    }
}
```

#### Task 4.3: اضافه کردن به NavMenu
```razor
<MudNavLink Href="/loan/daya-status" Icon="@Icons.Material.Filled.AccountBalance">
    وام دایا
</MudNavLink>
```

---

## بخش دوم: Contract Completion

### 📝 STEP 5: تکمیل Contract Module

**وضعیت فعلی (80%):**
- ✅ CreateContract Command
- ✅ UpdateContract Command
- ❌ GetContract Query (غایب)
- ❌ GetAllMyContracts Query (غایب)

#### Task 5.1: Query - GetMyContract

**فایل 1: GetMyContractQuery.cs**
```bash
mkdir -p FrontOffice.BFF.Application/ContractCQ/Queries/GetMyContract/
nano GetMyContractQuery.cs
```

```csharp
using MediatR;

namespace FrontOffice.BFF.Application.ContractCQ.Queries.GetMyContract;

public record GetMyContractQuery : IRequest<MyContractResponseDto>
{
    public long ContractId { get; init; }
}
```

**فایل 2: MyContractResponseDto.cs**
```csharp
namespace FrontOffice.BFF.Application.ContractCQ.Queries.GetMyContract;

public class MyContractResponseDto
{
    public long Id { get; set; }
    public string ContractNumber { get; set; }
    public string Type { get; set; }                 // "خرید" / "عضویت" / "وام"
    public string Status { get; set; }               // "فعال" / "غیرفعال" / "منقضی"
    public string StatusColor { get; set; }
    
    public decimal TotalAmount { get; set; }
    public string TotalAmountFormatted { get; set; }
    
    public string StartDatePersian { get; set; }
    public string EndDatePersian { get; set; }
    
    public string Description { get; set; }
    public string Terms { get; set; }                // شرایط قرارداد
}
```

**فایل 3: GetMyContractQueryHandler.cs**
```csharp
using MediatR;
using FrontOffice.BFF.Application.Common.Interfaces;

namespace FrontOffice.BFF.Application.ContractCQ.Queries.GetMyContract;

public class GetMyContractQueryHandler 
    : IRequestHandler<GetMyContractQuery, MyContractResponseDto>
{
    private readonly ICurrentUserService _currentUser;
    
    public GetMyContractQueryHandler(ICurrentUserService currentUser)
    {
        _currentUser = currentUser;
    }
    
    public async Task<MyContractResponseDto> Handle(
        GetMyContractQuery request,
        CancellationToken cancellationToken)
    {
        var userId = _currentUser.UserId;
        
        // TODO: Call CMS
        
        return new MyContractResponseDto
        {
            Id = request.ContractId,
            ContractNumber = "CNT-1403-001234",
            Type = "خرید محصول",
            Status = "فعال",
            StatusColor = "success",
            TotalAmount = 5000000,
            TotalAmountFormatted = "5,000,000 تومان",
            StartDatePersian = "1 آذر 1403",
            EndDatePersian = "1 آذر 1404",
            Description = "قرارداد خرید بسته طلایی",
            Terms = "شرایط و قوانین قرارداد..."
        };
    }
}
```

#### Task 5.2: Query - GetMyContracts

**فایل 1: GetMyContractsQuery.cs**
```csharp
using MediatR;

namespace FrontOffice.BFF.Application.ContractCQ.Queries.GetMyContracts;

public record GetMyContractsQuery : IRequest<MyContractsResponseDto>
{
    public int PageNumber { get; init; } = 1;
    public int PageSize { get; init; } = 10;
}
```

**فایل 2: MyContractsResponseDto.cs**
```csharp
namespace FrontOffice.BFF.Application.ContractCQ.Queries.GetMyContracts;

public class MyContractsResponseDto
{
    public List<ContractItemDto> Contracts { get; set; }
    public int TotalCount { get; set; }
}

public class ContractItemDto
{
    public long Id { get; set; }
    public string ContractNumber { get; set; }
    public string Type { get; set; }
    public string Status { get; set; }
    public string StatusColor { get; set; }
    public string TotalAmountFormatted { get; set; }
    public string StartDatePersian { get; set; }
}
```

#### Task 5.3: آپدیت ContractController

```csharp
using FrontOffice.BFF.Application.ContractCQ.Queries.GetMyContract;
using FrontOffice.BFF.Application.ContractCQ.Queries.GetMyContracts;

[HttpGet("{contractId}")]
[ProducesResponseType(typeof(MyContractResponseDto), 200)]
public async Task<IActionResult> GetContract(long contractId)
{
    var query = new GetMyContractQuery { ContractId = contractId };
    var result = await _mediator.Send(query);
    return Ok(result);
}

[HttpGet("my-contracts")]
[ProducesResponseType(typeof(MyContractsResponseDto), 200)]
public async Task<IActionResult> GetMyContracts(
    [FromQuery] int pageNumber = 1,
    [FromQuery] int pageSize = 10)
{
    var query = new GetMyContractsQuery { PageNumber = pageNumber, PageSize = pageSize };
    var result = await _mediator.Send(query);
    return Ok(result);
}
```

---

### ✅ Checkpoint نهایی

```bash
cd /home/masoud/Apps/project/FourSat/FrontOffice.BFF/src/
dotnet build

cd /home/masoud/Apps/project/FourSat/FrontOffice/src/
dotnet build
```

**چیزهایی که باید کار کنند:**
```
[ ] DayaLoan: GetMyDayaLoanStatus Query کار می‌کند
[ ] DayaLoan: RequestDayaLoanCheck Command کار می‌کند
[ ] DayaLoan: Controller با 2 endpoint
[ ] DayaLoan: UI صفحه کامل با نمایش 3 کیف پول
[ ] Contract: GetMyContract Query کار می‌کند
[ ] Contract: GetMyContracts Query کار می‌کند
[ ] Contract: Controller آپدیت شد
```

---

### 📊 آماری از مرحله 7

| ماژول | Queries قبل | Queries بعد | Commands قبل | Commands بعد | وضعیت |
|-------|------------|------------|-------------|-------------|-------|
| DayaLoan | 0 | 1 | 0 | 1 | ✅ 100% |
| Contract | 0 | 2 | 2 | 2 | ✅ 100% |

**زمان تخمینی:** 4 ساعت

---

### 💡 نکات مهم

1. **Worker**: Worker در CMS هر 15 دقیقه استعلام می‌کند - کاربر نباید بیش از حد استعلام فوری بزند
2. **168M Total**: 56M × 3 کیف پول = 168 میلیون تومان کل شارژ
3. **Status Icons**: از Icons.Material.Filled استفاده کن برای نمایش بهتر
4. **Gradient Background**: برای کارت وضعیت از Gradient استفاده شد
5. **Contract Module**: فقط 2 Query اضافه شد تا 100% شود


---

## 🔍 مرحله 8: بررسی نهایی - فقط کارهای ناتمام قبلی (بدون فیچرهای جدید)

### ⚠️ تذکر مهم

این مرحله **فقط** روی قابلیت‌هایی تمرکز دارد که:
1. ✅ در CMS **از قبل موجود** است
2. ❌ در FrontOffice.BFF یا FrontOffice **پیاده‌سازی نشده**
3. 🎯 **مختص مشتری** است (نه Admin)

**حذف شده از لیست:**
- ❌ DayaLoan (فیچر جدید - هنوز در CMS کامل نیست)
- ❌ Manual Payment (فیچر Admin)
- ❌ ClubMembership Admin Commands (مثل Deactivate, AssignFeature)

---

### 📝 STEP 1: بررسی دقیق CMS vs BFF

#### Task 1.1: مقایسه Commands/Queries موجود
```bash
cd /home/masoud/Apps/project/FourSat

# بررسی CMS Modules
echo "=== CMS Modules ===" > /tmp/cms_modules.txt
find CMS/src/CMSMicroservice.Application -type d -name "*CQ" | grep -v "bin\|obj" | sort >> /tmp/cms_modules.txt

# بررسی BFF Modules
echo "=== BFF Modules ===" > /tmp/bff_modules.txt
find FrontOffice.BFF/src/FrontOffice.BFF.Application -type d -name "*CQ" | grep -v "bin\|obj" | sort >> /tmp/bff_modules.txt

# مقایسه
echo "=== Comparison ===" > /tmp/comparison.txt
comm -3 <(find CMS/src/CMSMicroservice.Application -type d -name "*CQ" | xargs -I {} basename {} | sort -u) \
        <(find FrontOffice.BFF/src/FrontOffice.BFF.Application -type d -name "*CQ" | xargs -I {} basename {} | sort -u) \
        >> /tmp/comparison.txt

cat /tmp/comparison.txt
```

#### Task 1.2: فیلتر کردن Customer-Facing فقط
```bash
# ماژول‌هایی که حتماً Customer-Facing هستند:
echo "Customer-Facing Modules که در BFF غایب هستند:" > /tmp/customer_missing.txt
echo "1. ClubMembershipCQ - نیاز به UI برای مشتری" >> /tmp/customer_missing.txt
echo "2. NetworkMembershipCQ - نیاز به UI درخت" >> /tmp/customer_missing.txt
echo "3. CommissionCQ - بخش‌های ناقص (Pool, Downline)" >> /tmp/customer_missing.txt

cat /tmp/customer_missing.txt
```

---

### 📊 ماژول‌های ناقص واقعی (بدون فیچرهای جدید)

#### 1. ClubMembership (Priority 0 - حیاتی)

**موجود در CMS:**
```bash
ls CMS/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/
# ActivateClubMembership/      ← مشتری می‌خواهد عضو شود
# DeactivateClubMembership/    ← Admin only
# AssignClubFeature/           ← Admin only

ls CMS/src/CMSMicroservice.Application/ClubMembershipCQ/Queries/
# GetClubMembership/           ← مشتری می‌خواهد ببیند
# GetAllClubMemberships/       ← Admin only
# GetClubMembershipHistory/    ← مشتری می‌خواهد تاریخچه ببیند
# GetClubStatistics/           ← Admin + مشتری
```

**غایب در BFF:**
- ❌ Query: GetMyClubMembership (نمایش عضویت من)
- ❌ Query: GetMyClubHistory (تاریخچه عضویت من)
- ❌ Query: GetClubFeatures (لیست امکانات باشگاه برای انتخاب)
- ❌ Command: ActivateMyClubMembership (فعال‌سازی عضویت - پرداخت 56M)

**UI غایب:**
- ❌ صفحه نمایش وضعیت عضویت
- ❌ صفحه لیست امکانات باشگاه
- ❌ دکمه فعال‌سازی عضویت

---

#### 2. NetworkMembership (Priority 0 - حیاتی)

**موجود در CMS:**
```bash
ls CMS/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/
# GetNetworkTree/              ← مشتری می‌خواهد درخت ببیند
# GetUserPosition/             ← مشتری می‌خواهد موقعیت خود را ببیند
# GetNetworkHistory/           ← مشتری می‌خواهد تاریخچه ببیند
# GetNetworkStatistics/        ← مشتری می‌خواهد آمار ببیند

ls CMS/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/
# JoinNetwork/                 ← Admin (وقت ثبت‌نام)
# MoveInNetwork/               ← Admin only
# RemoveFromNetwork/           ← Admin only
```

**غایب در BFF:**
- ❌ Query: GetMyNetworkTree (درخت شبکه من)
- ❌ Query: GetMyNetworkPosition (موقعیت من)
- ❌ Query: GetMyNetworkHistory (تاریخچه جابجایی‌ها)
- ❌ Query: GetMyNetworkStatistics (آمار شبکه من: تعداد افراد، عمق، ...)

**UI غایب:**
- ❌ صفحه نمایش درخت باینری
- ❌ Component نمایش Recursive Tree
- ❌ صفحه آمار شبکه

---

#### 3. Commission (Priority 0 - حیاتی)

**موجود در CMS:**
```bash
ls CMS/src/CMSMicroservice.Application/CommissionCQ/Queries/
# GetUserCommissionPayouts/    ← مشتری می‌خواهد کمیسیون‌ها را ببیند
# GetUserBalance/              ← مشتری می‌خواهد موجودی ببیند
# GetWithdrawalHistory/        ← مشتری می‌خواهد تاریخچه برداشت ببیند
# GetWeeklyReport/             ← مشتری می‌خواهد گزارش هفتگی ببیند
# GetPoolShare/                ← مشتری می‌خواهد سهم پول ببیند
# GetDownlineCommissions/      ← مشتری می‌خواهد کمیسیون زیرمجموعه ببیند
# GetCommissionStatistics/     ← مشتری می‌خواهد آمار ببیند
# GetAvailableBalance/         ← مشتری می‌خواهد مبلغ قابل برداشت ببیند

ls CMS/src/CMSMicroservice.Application/CommissionCQ/Commands/
# RequestWithdrawal/           ← مشتری می‌خواهد برداشت کند
# ApproveWithdrawal/           ← Admin only
# RejectWithdrawal/            ← Admin only
# PayWithdrawal/               ← Admin only
# CancelWithdrawal/            ← مشتری می‌تواند لغو کند
# RecalculateCommission/       ← Admin only
# AdjustBalance/               ← Admin only
# TransferCommission/          ← Admin only
```

**موجود در BFF (10%):**
- ✅ Query: GetUserCommissionPayouts (ولی ناقص)

**غایب در BFF (90%):**
- ❌ Query: GetMyBalance (موجودی کامل)
- ❌ Query: GetMyWithdrawalHistory (تاریخچه برداشت‌ها)
- ❌ Query: GetMyWeeklyReport (گزارش هفتگی)
- ❌ Query: GetMyPoolShare (سهم من از پول)
- ❌ Query: GetMyDownlineCommissions (کمیسیون زیرمجموعه‌های من)
- ❌ Query: GetMyCommissionStatistics (آمار کمیسیون‌های من)
- ❌ Command: RequestMyWithdrawal (درخواست برداشت)
- ❌ Command: CancelMyWithdrawal (لغو درخواست برداشت)

**UI غایب:**
- ❌ صفحه نمایش موجودی کامل
- ❌ صفحه درخواست برداشت
- ❌ صفحه تاریخچه برداشت‌ها
- ❌ صفحه گزارش هفتگی
- ❌ صفحه سهم پول
- ❌ صفحه کمیسیون زیرمجموعه‌ها

---

#### 4. UserWallet (Priority 1)

**موجود در CMS:**
```bash
ls CMS/src/CMSMicroservice.Application/UserWalletCQ/Queries/
# GetUserWallet/               ← مشتری می‌خواهد موجودی ببیند
# GetWalletTransactions/       ← مشتری می‌خواهد تراکنش‌ها را ببیند
# GetDiscountTransactions/     ← مشتری می‌خواهد تراکنش‌های تخفیف ببیند (اگر وجود دارد)

ls CMS/src/CMSMicroservice.Application/UserWalletCQ/Commands/
# ChargeWallet/                ← Admin یا Gateway
# WithdrawFromWallet/          ← مشتری می‌تواند برداشت کند
# TransferBetweenWallets/      ← مشتری می‌تواند انتقال دهد (اگر مجاز باشد)
```

**موجود در BFF (60%):**
- ✅ Query: GetUserWallet
- ✅ Query: GetWalletTransactions
- ✅ Command: ChargeWallet (ناقص)
- ⚠️ Command: WithdrawFromWallet (Handler خالی است)

**غایب در BFF (40%):**
- ❌ Query: GetDiscountTransactions (اگر در CMS هست)
- ❌ تکمیل WithdrawFromWallet Handler
- ❌ Command: TransferBetweenWallets (اگر مجاز باشد)

**UI غایب:**
- ❌ تب تراکنش‌های تخفیف (اگر DiscountBalance موجود است)
- ❌ دکمه/فرم برداشت از کیف پول
- ❌ فرم انتقال بین کیف پول‌ها

---

#### 5. ShoppingCart (Priority 1)

**موجود در CMS:**
```bash
ls CMS/src/CMSMicroservice.Application/ShoppingCartCQ/Commands/
# AddToCart/                   ← مشتری اضافه می‌کند
# UpdateCartItemQuantity/      ← مشتری تغییر می‌دهد
# DeleteCartItem/              ← مشتری حذف می‌کند
# ClearCart/                   ← مشتری پاک می‌کند
# MergeGuestCart/              ← سیستم ادغام می‌کند (بعد از Login)
# ApplyDiscountCode/           ← مشتری کد تخفیف وارد می‌کند
```

**موجود در BFF (50%):**
- ✅ Query: GetMyCart
- ✅ Command: AddToCart
- ✅ Command: UpdateCartItemQuantity

**غایب در BFF (50%):**
- ❌ Command: DeleteCartItem
- ❌ Command: ClearCart
- ❌ Command: MergeGuestCart
- ❌ Command: ApplyDiscountCode

**UI غایب:**
- ❌ دکمه حذف آیتم
- ❌ دکمه پاک کردن سبد
- ❌ فرم کد تخفیف

---

#### 6. Contract (Priority 2)

**موجود در CMS:**
```bash
ls CMS/src/CMSMicroservice.Application/ContractCQ/Queries/
# GetContract/                 ← مشتری می‌خواهد قرارداد ببیند
# GetAllContracts/             ← مشتری می‌خواهد لیست قراردادها را ببیند
# GetContractDetails/          ← مشتری می‌خواهد جزئیات ببیند

ls CMS/src/CMSMicroservice.Application/ContractCQ/Commands/
# CreateContract/              ← سیستم ایجاد می‌کند
# UpdateContract/              ← Admin
# SignContract/                ← مشتری امضا می‌کند (اگر نیاز باشد)
```

**موجود در BFF (20%):**
- ✅ Command: CreateContract (ولی مشتری استفاده نمی‌کند - سیستم استفاده می‌کند)

**غایب در BFF (80%):**
- ❌ Query: GetMyContract
- ❌ Query: GetMyContracts
- ❌ Query: GetMyContractDetails
- ❌ Command: SignMyContract (اگر نیاز باشد)

**UI غایب:**
- ❌ صفحه لیست قراردادهای من
- ❌ صفحه جزئیات قرارداد
- ❌ دکمه امضای قرارداد

---

### 📋 خلاصه کارهای باقی‌مانده (فقط Customer-Facing)

| ماژول | Queries غایب | Commands غایب | UI Pages غایب | اولویت |
|-------|-------------|--------------|---------------|--------|
| **ClubMembership** | 3 | 1 | 2 | P0 🔥 |
| **NetworkMembership** | 4 | 0 | 3 | P0 🔥 |
| **Commission** | 7 | 2 | 6 | P0 🔥 |
| **UserWallet** | 1 | 1 (تکمیل) | 2 | P1 |
| **ShoppingCart** | 0 | 4 | 1 | P1 |
| **Contract** | 3 | 1 | 2 | P2 |

**جمع کل:**
- Queries: 18
- Commands: 9
- UI Pages: 16

---

### 🎯 توصیه نهایی برای Developer

#### اولویت 1 (حیاتی - باید حتماً باشد):
1. **Commission + Withdrawal**: مشتری باید بتواند پولش را ببیند و برداشت کند
2. **ClubMembership**: مشتری باید بتواند عضو باشگاه شود
3. **NetworkMembership**: مشتری باید درخت شبکه خود را ببیند

#### اولویت 2 (مهم):
4. **UserWallet Completion**: تکمیل برداشت + تخفیف
5. **ShoppingCart Completion**: حذف آیتم + پاک کردن سبد + کد تخفیف

#### اولویت 3 (نرمال):
6. **Contract**: نمایش قراردادها

---

### 💡 نکته بسیار مهم

**چیزهایی که حذف شدند (چون جدید هستند یا Admin هستند):**
- ❌ DayaLoan (فیچر جدید - هنوز در CMS کامل نیست)
- ❌ Manual Payment (فیچر جدید)
- ❌ Admin Commands در همه ماژول‌ها (Approve, Reject, Recalculate, Adjust, ...)
- ❌ Admin Queries (GetAll, GetStatistics با دسترسی Admin)

**فقط روی اینها تمرکز کن:**
- ✅ Queries که مشتری می‌خواهد ببیند (GetMy...)
- ✅ Commands که مشتری می‌خواهد اجرا کند (ActivateMy..., RequestMy..., DeleteMy...)
- ✅ UI Pages که مشتری می‌خواهد استفاده کند

---

### 📊 تخمین زمان واقعی (بدون فیچرهای جدید)

| کار | زمان تخمینی |
|-----|-------------|
| Commission (7 Query + 2 Command + 6 Page) | 12 ساعت |
| ClubMembership (3 Query + 1 Command + 2 Page) | 6 ساعت |
| NetworkMembership (4 Query + 3 Page) | 8 ساعت |
| UserWallet Completion (1 Query + 1 تکمیل + 2 Page) | 3 ساعت |
| ShoppingCart Completion (4 Command + 1 Page) | 4 ساعت |
| Contract (3 Query + 1 Command + 2 Page) | 4 ساعت |
| **جمع کل** | **37 ساعت (تقریباً 5 روز کاری)** |

این زمان واقع‌بینانه‌تر است چون فیچرهای جدید (DayaLoan, Manual Payment) حذف شدند.

---

## 📝 اصطلاحات جایگزین (MLM-Sensitive Terminology)

> **آخرین بروزرسانی**: ۹ دی ۱۴۰۴ (29 دسامبر 2025)

برای جلوگیری از حساسیت مشتریان به کلمات مرتبط با MLM، از اصطلاحات جایگزین زیر در UI مشتری استفاده شود:

| کلمه حساس (فارسی) | جایگزین پیشنهادی | توضیح |
|-------------------|------------------|-------|
| کمیسیون | **پاداش** | Commission → Reward |
| شبکه‌سازی | **تیم‌سازی** | Network Building → Team Building |
| شبکه | **تیم** | Network → Team (در context MLM) |
| شاخه چپ/راست | **تیم اول/دوم** | Left/Right Leg → Team 1/2 |
| زیرمجموعه | **اعضای تیم** | Downline → Team Members |
| تعادل | **امتیاز/جفت** | Balance → Points/Pairs |
| درخت شبکه | **نمودار سازمانی** | Network Tree → Org Chart |
| سقف | **حداکثر** | Cap → Maximum |
| Binary | **دوبخشی** | Binary → Two-part |

### ⚠️ موارد استثنا (نباید تغییر کنند):
- **شبکه‌های اجتماعی** - Social Networks (مرتبط با MLM نیست)
- **درخت دسته‌بندی** - Category Tree (مرتبط با محصولات)
- **پنل ادمین (BackOffice)** - نیاز به صراحت اصطلاحات دارد

### ✅ فایل‌های تغییر یافته (۹ دی):
- `WeeklyBalancePage.razor` - کمیسیون → پاداش
- `CommissionDashboardPage.razor` - کمیسیون → پاداش
- `MyPackages.razor` - مشاهده شبکه → مشاهده تیم
- `Index.razor` - شبکه‌سازی → تیم‌سازی
- `About.razor` - شبکه‌های فروش → تیم‌های فروش
- `Footer.razor` - شبکه‌های فروش → تیم‌های فروش
- `NetworkStatisticsPage.razor` - آمار شبکه → آمار تیم
