# تحلیل امکانات قابل ارائه به مشتری (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 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/فرانت فعال و توکن جدید پس از امضا صادر می‌شود.
--- ## 📊 تحلیل جامع: شکاف‌های پیاده‌سازی در 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(...) builder.Services.AddGrpcClient(...) builder.Services.AddGrpcClient(...) // 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 موجودی کیف پول: {Balance:N0} ریال موجودی شبکه: {NetworkBalance:N0} ریال موجودی تخفیف: {DiscountBalance:N0} ریال درخواست برداشت [ ] 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 باشگاه مشتریان کمیسیون و برداشت شبکه من ``` --- ### 🚨 نکات حیاتی (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 ``` --- ## 📊 تحلیل کامل شکاف‌های پیاده‌سازی (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 ماه) برای تکمیل نیاز است. --- ## 🌳 مرحله 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; /// /// Query برای دریافت درخت شبکه کاربر جاری /// public record GetMyNetworkTreeQuery : IRequest { /// /// عمق درخت (چند لایه زیرمجموعه نمایش داده شود) /// پیش‌فرض: 3 لایه /// public int Depth { get; init; } = 3; } ``` **فایل 2: MyNetworkTreeResponseDto.cs** ```csharp namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree; /// /// DTO مشتری‌محور برای نمایش درخت شبکه /// public class MyNetworkTreeResponseDto { public NetworkNodeDto CurrentUser { get; set; } public int TotalNetworkSize { get; set; } // تعداد کل افراد در شبکه public int DirectChildrenCount { get; set; } // تعداد فرزندان مستقیم public string LastUpdatePersian { get; set; } // آخرین به‌روزرسانی } /// /// نود درخت (Recursive) /// 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 { private readonly ICurrentUserService _currentUser; // TODO: private readonly NetworkMembershipServiceClient _cmsClient; public GetMyNetworkTreeQueryHandler(ICurrentUserService currentUser) { _currentUser = currentUser; } public async Task 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 { } ``` **فایل 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 { private readonly ICurrentUserService _currentUser; public GetMyNetworkPositionQueryHandler(ICurrentUserService currentUser) { _currentUser = currentUser; } public async Task 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; } /// /// دریافت درخت شبکه من /// [HttpGet("my-tree")] [ProducesResponseType(typeof(MyNetworkTreeResponseDto), 200)] public async Task GetMyTree([FromQuery] int depth = 3) { var query = new GetMyNetworkTreeQuery { Depth = depth }; var result = await _mediator.Send(query); return Ok(result); } /// /// دریافت موقعیت من در شبکه /// [HttpGet("my-position")] [ProducesResponseType(typeof(MyNetworkPositionResponseDto), 200)] public async Task 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 GetMyTreeAsync(int depth = 3) { var response = await _httpClient.GetAsync($"/api/networkmembership/my-tree?depth={depth}"); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync(); } public async Task GetMyPositionAsync() { var response = await _httpClient.GetAsync("/api/networkmembership/my-position"); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync(); } } ``` **ثبت در Program.cs:** ```csharp builder.Services.AddScoped(); ``` #### 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) *@ @Node.FullName @Node.Mobile @Node.StatusBadge موقعیت: @Node.Position تاریخ: @Node.JoinDatePersian @if (Node.LeftChild != null || Node.RightChild != null) { @if (Node.LeftChild != null) {
← چپ
} @if (Node.RightChild != null) {
راست →
}
} @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 شبکه باینری من @if (_loading) { } else if (_tree != null) { آمار کلی تعداد کل اعضا: @_tree.TotalNetworkSize نفر فرزندان مستقیم: @_tree.DirectChildrenCount نفر آخرین به‌روزرسانی: @_tree.LastUpdatePersian درخت شبکه
}
@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 موقعیت من در شبکه @if (_loading) { } else if (_position != null) { @if (_position.HasParent) { معرف من نام: @_position.ParentFullName موبایل: @_position.ParentMobile } موقعیت من @_position.MyPosition سطح: @_position.NetworkLevel آمار زیرمجموعه تعداد کل افراد زیر مجموعه: @_position.TotalDownlineCount نفر تاریخ پیوستن: @_position.JoinDatePersian } @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 درخت شبکه موقعیت من ``` --- ### ✅ 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 + 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 # 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 { /// /// تعداد آیتم در هر صفحه (پیش‌فرض: 10) /// public int PageSize { get; init; } = 10; /// /// شماره صفحه (پیش‌فرض: 1) /// public int PageNumber { get; init; } = 1; /// /// فیلتر بر اساس نوع کمیسیون (اختیاری) /// public string PayoutType { get; init; } } ``` **فایل 2: MyCommissionPayoutsResponseDto.cs** ```csharp namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyCommissionPayouts; public class MyCommissionPayoutsResponseDto { public List 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 { private readonly ICurrentUserService _currentUser; // TODO: private readonly CommissionServiceClient _cmsClient; public GetMyCommissionPayoutsQueryHandler(ICurrentUserService currentUser) { _currentUser = currentUser; } public async Task 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 { 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 { } ``` **فایل 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 { private readonly ICurrentUserService _currentUser; public GetMyBalanceQueryHandler(ICurrentUserService currentUser) { _currentUser = currentUser; } public async Task 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 { 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 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 { private readonly ICurrentUserService _currentUser; public GetMyWithdrawalHistoryQueryHandler(ICurrentUserService currentUser) { _currentUser = currentUser; } public async Task Handle( GetMyWithdrawalHistoryQuery request, CancellationToken cancellationToken) { // TODO: Call CMS return new MyWithdrawalHistoryResponseDto { Withdrawals = new List { 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 { 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 { private readonly ICurrentUserService _currentUser; // TODO: private readonly CommissionServiceClient _cmsClient; public RequestMyWithdrawalCommandHandler(ICurrentUserService currentUser) { _currentUser = currentUser; } public async Task 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 { 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; } /// /// دریافت لیست کمیسیون‌های من /// [HttpGet("my-payouts")] [ProducesResponseType(typeof(MyCommissionPayoutsResponseDto), 200)] public async Task 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); } /// /// دریافت موجودی من /// [HttpGet("my-balance")] [ProducesResponseType(typeof(MyBalanceResponseDto), 200)] public async Task GetMyBalance() { var query = new GetMyBalanceQuery(); var result = await _mediator.Send(query); return Ok(result); } /// /// دریافت تاریخچه برداشت‌های من /// [HttpGet("my-withdrawal-history")] [ProducesResponseType(typeof(MyWithdrawalHistoryResponseDto), 200)] public async Task 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); } /// /// درخواست برداشت /// [HttpPost("request-withdrawal")] [ProducesResponseType(typeof(RequestMyWithdrawalResponseDto), 200)] public async Task 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 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(); } public async Task GetMyBalanceAsync() { var response = await _httpClient.GetAsync("/api/commission/my-balance"); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync(); } public async Task GetMyWithdrawalHistoryAsync(int pageNumber = 1) { var response = await _httpClient.GetAsync( $"/api/commission/my-withdrawal-history?pageNumber={pageNumber}"); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync(); } public async Task 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(); } } ``` **ثبت در Program.cs:** ```csharp builder.Services.AddScoped(); ``` #### 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 کمیسیون‌های من @if (_loading) { } else if (_payouts != null) { مجموع کل: @_payouts.TotalAmount.ToString("N0") تومان هفته نوع مبلغ وضعیت تاریخ پرداخت @context.WeekDisplay @context.PayoutType @context.AmountFormatted @context.Status @context.PayoutDatePersian } @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 برداشت وجه @if (_loadingBalance) { } else if (_balance != null) { موجودی من کل درآمد: @_balance.TotalEarnedFormatted موجودی فعلی: @_balance.CurrentBalanceFormatted قابل برداشت: @_balance.WithdrawableBalanceFormatted در انتظار برداشت: @_balance.PendingWithdrawalFormatted درخواست برداشت جدید حداقل: @_balance.MinWithdrawalAmount | حداکثر: @_balance.MaxWithdrawalAmount @if (_submitting) { در حال ارسال... } else { ثبت درخواست } تاریخچه برداشت‌ها @if (_loadingHistory) { } else if (_history != null) { مبلغ وضعیت تاریخ درخواست تاریخ پردازش شماره کارت @context.AmountFormatted @context.Status @context.RequestDatePersian @context.ProcessDatePersian @context.BankAccount } } @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 کمیسیون‌های من برداشت وجه ``` --- ### ✅ 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 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 { 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 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 { private readonly ICurrentUserService _currentUser; public GetMyDiscountTransactionsQueryHandler(ICurrentUserService currentUser) { _currentUser = currentUser; } public async Task Handle( GetMyDiscountTransactionsQuery request, CancellationToken cancellationToken) { var userId = _currentUser.UserId; // TODO: Call CMS // Mock Data return new MyDiscountTransactionsResponseDto { Transactions = new List { 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 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 { private readonly ICurrentUserService _currentUser; // TODO: private readonly UserWalletServiceClient _cmsClient; public WithdrawalFromWalletCommandHandler(ICurrentUserService currentUser) { _currentUser = currentUser; } public async Task 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 { 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 GetMyDiscountTransactionsAsync(int pageNumber = 1) { var response = await _httpClient.GetAsync( $"/api/userwallet/my-discount-transactions?pageNumber={pageNumber}"); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync(); } ``` #### Task 4.2: آپدیت WalletPage.razor **اضافه کردن Card برای DiscountBalance:** ```razor @page "/wallet" @inject UserWalletService WalletService @inject ISnackbar Snackbar کیف پول من @if (_loading) { } else if (_wallet != null) { موجودی اصلی @_wallet.MainBalanceFormatted موجودی پاداش @_wallet.RewardBalanceFormatted موجودی تخفیف @_wallet.DiscountBalanceFormatted @if (_wallet.HasDiscount) { @_wallet.DiscountPercentage تخفیف } مجموع کل: @_wallet.TotalBalanceFormatted @if (_loadingDiscountTxs) { } else if (_discountTransactions != null) { نوع مبلغ شرح تاریخ @context.Type @context.AmountFormatted @context.Description @context.DatePersian } } @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; /// /// حذف یک آیتم از سبد خرید من /// public record DeleteMyCartItemCommand : IRequest { 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 { private readonly ICurrentUserService _currentUser; // TODO: private readonly ShoppingCartServiceClient _cmsClient; public DeleteMyCartItemCommandHandler(ICurrentUserService currentUser) { _currentUser = currentUser; } public async Task 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 { 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; /// /// پاک کردن کل سبد خرید من /// public record ClearMyCartCommand : IRequest { // هیچ ورودی ندارد - 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 { private readonly ICurrentUserService _currentUser; public ClearMyCartCommandHandler(ICurrentUserService currentUser) { _currentUser = currentUser; } public async Task 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; /// /// ادغام سبد مهمان با سبد کاربر لاگین شده /// زمانی استفاده می‌شود که کاربر بدون لاگین خرید می‌کند و بعد لاگین می‌کند /// public record MergeGuestCartCommand : IRequest { 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 { private readonly ICurrentUserService _currentUser; public MergeGuestCartCommandHandler(ICurrentUserService currentUser) { _currentUser = currentUser; } public async Task 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 { 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; /// /// حذف یک آیتم از سبد خرید /// [HttpDelete("items/{cartItemId}")] [ProducesResponseType(typeof(DeleteMyCartItemResponseDto), 200)] public async Task DeleteCartItem(long cartItemId) { var command = new DeleteMyCartItemCommand { CartItemId = cartItemId }; var result = await _mediator.Send(command); return Ok(result); } /// /// پاک کردن کل سبد خرید /// [HttpDelete("clear")] [ProducesResponseType(typeof(ClearMyCartResponseDto), 200)] public async Task ClearCart() { var command = new ClearMyCartCommand(); var result = await _mediator.Send(command); return Ok(result); } /// /// ادغام سبد مهمان /// [HttpPost("merge-guest")] [ProducesResponseType(typeof(MergeGuestCartResponseDto), 200)] public async Task 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 DeleteCartItemAsync(long cartItemId) { var response = await _httpClient.DeleteAsync($"/api/shoppingcart/items/{cartItemId}"); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync(); } public async Task ClearCartAsync() { var response = await _httpClient.DeleteAsync("/api/shoppingcart/clear"); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync(); } public async Task 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(); } ``` #### Task 4.2: آپدیت CartPage.razor **اضافه کردن دکمه‌های حذف:** ```razor @page "/cart" @inject ShoppingCartService CartService @inject ISnackbar Snackbar @inject IDialogService DialogService سبد خرید من @if (_loading) { } else if (_cart != null && _cart.Items.Any()) { محصولات (@_cart.TotalItemsCount مورد) پاک کردن سبد @foreach (var item in _cart.Items) { @item.ProductName @item.ProductDescription @item.TotalPriceFormatted حذف } خلاصه سبد خرید تعداد کل: @_cart.TotalItemsCount مورد قیمت کل: @_cart.TotalPriceFormatted تکمیل خرید } else { سبد خرید شما خالی است } @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; /// /// دریافت وضعیت وام دایا برای کاربر جاری /// public record GetMyDayaLoanStatusQuery : IRequest { } ``` **فایل 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 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 { private readonly ICurrentUserService _currentUser; // TODO: private readonly DayaLoanServiceClient _cmsClient; public GetMyDayaLoanStatusQueryHandler(ICurrentUserService currentUser) { _currentUser = currentUser; } public async Task 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 { 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; /// /// درخواست استعلام فوری وضعیت وام دایا /// معمولاً Worker این کار را انجام می‌دهد، اما کاربر می‌تواند استعلام فوری بزند /// public record RequestDayaLoanCheckCommand : IRequest { } ``` **فایل 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 { private readonly ICurrentUserService _currentUser; public RequestDayaLoanCheckCommandHandler(ICurrentUserService currentUser) { _currentUser = currentUser; } public async Task 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; } /// /// دریافت وضعیت وام دایا من /// [HttpGet("my-status")] [ProducesResponseType(typeof(MyDayaLoanStatusResponseDto), 200)] public async Task GetMyStatus() { var query = new GetMyDayaLoanStatusQuery(); var result = await _mediator.Send(query); return Ok(result); } /// /// درخواست استعلام فوری /// [HttpPost("request-check")] [ProducesResponseType(typeof(RequestDayaLoanCheckResponseDto), 200)] public async Task 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 GetMyStatusAsync() { var response = await _httpClient.GetAsync("/api/dayaloan/my-status"); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync(); } public async Task RequestCheckAsync() { var response = await _httpClient.PostAsync("/api/dayaloan/request-check", null); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync(); } } ``` **ثبت در Program.cs:** ```csharp builder.Services.AddScoped(); ``` #### 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 وضعیت وام دایا @if (_loading) { } else if (_status != null) { وضعیت درخواست @_status.Status کد ملی: @_status.NationalCode مبلغ درخواستی (هر کیف پول): @_status.RequestAmountFormatted @if (!string.IsNullOrEmpty(_status.ContractNumber)) { شماره قرارداد: @_status.ContractNumber } آخرین استعلام: @_status.LastCheckDatePersian @if (!string.IsNullOrEmpty(_status.ApprovalDatePersian)) { تاریخ تایید: @_status.ApprovalDatePersian } @if (_status.CanRequestCheck) { @if (_checking) { در حال استعلام... } else { استعلام فوری } } else { @_status.NextCheckAvailable } جزئیات شارژ کیف پول‌ها @if (_status.WalletCharges != null && _status.WalletCharges.Any()) { @foreach (var wallet in _status.WalletCharges) { @wallet.WalletTypePersian @wallet.AmountFormatted @if (wallet.IsCharged) { } else { } @if (wallet.IsCharged) { شارژ شده در: @wallet.ChargedDatePersian } } مجموع کل شارژ: @((56000000m * 3).ToString("N0")) تومان } else { هنوز کیف پولی شارژ نشده است } راهنما وام دایا برای هر کیف پول (اصلی، پاداش، تخفیف) به مبلغ 56 میلیون تومان است سیستم هر 15 دقیقه یکبار وضعیت وام شما را بررسی می‌کند در صورت تایید، کیف پول‌های شما به صورت خودکار شارژ خواهند شد } @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 وام دایا ``` --- ## بخش دوم: 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 { 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 { private readonly ICurrentUserService _currentUser; public GetMyContractQueryHandler(ICurrentUserService currentUser) { _currentUser = currentUser; } public async Task 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 { 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 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 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 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` - آمار شبکه → آمار تیم