Files
docs/migration/customer-facing-capabilities-codex.md
T

5300 lines
197 KiB
Markdown

<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` - آمار شبکه → آمار تیم