197 KiB
تحلیل امکانات قابل ارائه به مشتری (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
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:
{
"sub": "123", // UserId
"email": "user@example.com",
"phone": "09123456789",
"IsSignMainContract": "True", // قرارداد امضا شده؟
"exp": 1234567890
}
استخراج UserId در Handler:
public class GetMyCommissionPayoutsQueryHandler : IRequestHandler<...>
{
private readonly ICurrentUserService _currentUser;
public async Task<Response> Handle(Query request, CancellationToken ct)
{
var userId = _currentUser.UserId; // از JWT
// Call CMS with userId
var result = await _cmsClient.GetUserCommissionPayoutsAsync(userId);
return result;
}
}
📦 الگوی DTO Mapping
CMS DTO (داده خام):
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 (مشتریمحور):
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، فیلترها و اضافه کردن جریان برداشت از موجودی شبکه. برداشت فعلاً تنها اکشن عملی روی موجودی شبکه است.
- اقدام ریز:
- BFF: تکمیل
GetUserWalletبا DiscountBalance، افزودن فیلتر بهGetAllUserWalletChangeLog، پیادهسازیWithdrawBalanceبا CMS RequestWithdrawal + ولیدیشن MinWithdrawalAmount/IBAN. - Front: بهروزرسانی کارت کیفپول با سه کیف و توضیح کاربرد، لینک به برداشت برای NetworkBalance، فیلتر/مرتبسازی لاگ، نمایش مبلغ تغییر و Reference/Type.
- تجربه کاربری برداشت: پیام خطاهای Withdrawal (کمتر از حداقل مبلغ، درخواست در صف) و نمایش وضعیتهای Pending/Approved/Rejected در UI.
- BFF: تکمیل
کشفیات جدید (ویژگیهای مشتری در CMS که باید به BFF/فرانت برسد)
- پروفایل/OTP/ثبتنام: جریان OTP و ثبتنام، ذخیره کد ملی/نام/موبایل (
User,OtpToken) و ClaimIsSignMainContractدر 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 هفته):
-
CommissionCQ - کمیسیون و برداشت
- BFF: 2 Commands + 5 Queries + gRPC Client
- UI: CommissionDashboard + WithdrawalRequest + WeeklyBalanceChart
- ⏱️ تخمین: 5 روز کاری
-
ClubMembershipCQ - عضویت باشگاه
- BFF: 3 Commands + 3 Queries + gRPC Client
- UI: ClubMembershipPage + Profile widgets
- ⏱️ تخمین: 4 روز کاری
-
NetworkMembershipCQ - شبکه باینری
- BFF: 3 Commands + 3 Queries + gRPC Client
- UI: OrganizationChart update + Position page
- ⏱️ تخمین: 5 روز کاری
🟡 فاز 2 (Important - 1 هفته):
-
UserWalletCQ Enhancement - کیف تخفیف
- BFF: Add DiscountBalance to DTO
- UI: Display in WalletCard
- ⏱️ تخمین: 1 روز کاری
-
UserWalletChangeLogCQ Enhancement - فیلتر تراکنشها
- BFF: Add filter params (Type, DateRange, ReferenceId)
- UI: Filter controls in WalletDetailsPage
- ⏱️ تخمین: 2 روز کاری
-
DayaLoanCQ - وام دایا
- BFF: 2 Commands + Mock API Client
- UI: DayaLoanStatusPage
- ⏱️ تخمین: 2 روز کاری
🟢 فاز 3 (Nice to Have - 3 روز):
-
OtpTokenCQ Enhancement - RateLimit
- BFF: Add middleware (5 req/10min per IP)
- ⏱️ تخمین: 1 روز کاری
-
TransactionsCQ Enhancement - Refund & VAT
- BFF: RefundTransaction Command + VAT fields
- UI: Refund button + VAT display
- ⏱️ تخمین: 2 روز کاری
📋 چکلیست کامل (Copy-Paste Ready)
FrontOffice.BFF:
// ماژولهای جدید (از صفر)
[ ] Create /Application/ClubMembershipCQ/
[ ] Commands/ActivateClubMembership.cs + Handler
[ ] Queries/GetClubMembershipStatus.cs + Handler
[ ] DTOs/ClubMembershipDto.cs
[ ] Create /Application/NetworkMembershipCQ/
[ ] Commands/JoinNetwork.cs + Handler
[ ] Queries/GetNetworkTree.cs + Handler (MaxDepth: 1-10)
[ ] Queries/GetUserNetworkPosition.cs + Handler
[ ] DTOs/NetworkTreeDto.cs, NetworkPositionDto.cs
[ ] Create /Application/CommissionCQ/
[ ] Commands/RequestWithdrawal.cs (تکمیل Handler خالی موجود)
[ ] Queries/GetUserCommissionPayouts.cs + Handler
[ ] Queries/GetUserWeeklyBalances.cs + Handler
[ ] Queries/GetWeeklyCommissionPool.cs + Handler
[ ] DTOs/CommissionPayoutDto.cs, WeeklyBalanceDto.cs
[ ] Create /Application/DayaLoanCQ/
[ ] Commands/CheckDayaLoanStatus.cs + Handler
[ ] Services/MockDayaApiClient.cs
[ ] DTOs/DayaLoanInfoDto.cs
// بهروزرسانی ماژولهای موجود
[ ] Update /Application/UserWalletCQ/
[ ] DTOs/UserWalletDto.cs → Add: public decimal DiscountBalance { get; set; }
[ ] Handlers/GetUserWalletQueryHandler.cs → Map DiscountBalance from CMS
[ ] Update /Application/UserWalletCQ/ (ChangeLog)
[ ] Queries/GetAllUserWalletChangeLog.cs → Add params:
- WalletChangeType? Type
- DateTime? DateFrom, DateTime? DateTo
- string? ReferenceId
[ ] Handler → Apply filters in CMS gRPC call
// gRPC Registration
[ ] Update /Infrastructure/ConfigureGrpcServices.cs
builder.Services.AddGrpcClient<ClubMembershipServiceClient>(...)
builder.Services.AddGrpcClient<NetworkMembershipServiceClient>(...)
builder.Services.AddGrpcClient<CommissionServiceClient>(...)
// Security
[ ] Create /Infrastructure/Middleware/RateLimitMiddleware.cs
- Apply to: /api/user/otp endpoints
- Limit: 5 requests per 10 minutes per IP
FrontOffice (UI):
// صفحات جدید
[ ] Create /Pages/ClubMembership/Index.razor
- نمایش وضعیت عضویت (Active/Inactive)
- دکمه فعالسازی (هدایت به درگاه پرداخت 56M)
- لیست فیچرهای فعال (Badge system)
[ ] Create /Pages/Commission/Dashboard.razor
- نمایش استخر هفته (TotalPool, ValuePerPoint)
- نمودار تعادل (Left vs Right Volume)
- تاریخچه پرداختها با Badge وضعیت
[ ] Create /Pages/Commission/Withdrawal.razor
- فرم برداشت (IBAN, Amount, Method: Cash/Diamond)
- ولیدیشن MinWithdrawalAmount (100,000 تومان)
- نمایش پیام خطا (کمتر از حداقل، صف تایید)
[ ] Create /Pages/Network/Tree.razor
- بهروزرسانی OrganizationChart.razor
- Slider MaxDepth (1-10)
- نمایش امتیاز در هر Node
- رنگبندی بر اساس تعادل (سبز=متعادل، قرمز=نامتعادل)
- Tooltip: Parent, Children count, Carryover
[ ] Create /Pages/DayaLoan/Status.razor
- نمایش ContractNumber
- تاریخ دریافت اعتبار
- مبالغ شارژ شده (3×56M)
// کامپوننتهای جدید
[ ] Update /Components/Wallet/WalletCard.razor
<MudCard>
<MudText>موجودی کیف پول: {Balance:N0} ریال</MudText>
<MudText>موجودی شبکه: {NetworkBalance:N0} ریال</MudText>
<MudText Color="Color.Success">موجودی تخفیف: {DiscountBalance:N0} ریال</MudText>
<MudButton Href="/wallet/withdraw" Disabled="@(NetworkBalance < 100000)">
درخواست برداشت
</MudButton>
</MudCard>
[ ] Create /Components/Commission/WeeklyBalanceChart.razor
- نمودار میلهای Left/Right Volume
- نمایش WeakerLeg (کمترین حجم)
- نمایش Carryover و سقف 300
// بهروزرسانی موجودی
[ ] Update /Pages/Wallet/DetailsPage.razor
[ ] Add filter controls:
- نوع تراکنش (Deposit, Withdraw, Purchase, Refund)
- بازه زمانی (DatePicker: From/To)
- جستجوی ReferenceId (TextBox)
[ ] نمایش ChangeValue به جای CurrentBalance
[ ] پیجینیشن
[ ] Update /Components/Layout/NavMenu.razor
<MudNavLink Href="/clubmembership" Icon="@Icons.Material.Filled.CardMembership">
باشگاه مشتریان
</MudNavLink>
<MudNavLink Href="/commission/dashboard" Icon="@Icons.Material.Filled.Payments">
کمیسیون و برداشت
</MudNavLink>
<MudNavLink Href="/network/tree" Icon="@Icons.Material.Filled.AccountTree">
شبکه من
</MudNavLink>
🚨 نکات حیاتی (Critical Notes)
⚠️ امنیت:
1. WithdrawBalance:
- MinAmount: 100,000 ریال (CMS config)
- IBAN: IR + 24 digits validation
- Daily limit per user: Check CMS setting
2. JoinNetwork:
- IsDescendant recursive check (prevent circular ref)
- Position validation (Left/Right must be empty)
- SponsorId must be active club member
3. OTP RateLimit:
- 5 requests / 10 min per IP
- Redis/InMemory cache
💡 UI/UX:
1. WalletCard: 3 کیفپول با رنگ متفاوت
- Balance: آبی (خرید عمومی)
- NetworkBalance: سبز (برداشت Cash/Diamond)
- DiscountBalance: زرد (فروشگاه باشگاه)
2. CommissionDashboard:
- Badge colors: Pending=زرد, Calculated=آبی, Paid=سبز, Withdrawn=خاکستری
- Carryover info tooltip
- سقف 300 Balance در هفته
3. NetworkTree:
- MaxDepth default: 3
- Load on demand برای عمق بیشتر
- Tooltip با Shift+Click
❌ خطاهای رایج:
1. BFF: Handler خالی
❌ FrontOffice.BFF/Application/UserWalletCQ/Commands/WithdrawBalanceCommandHandler.cs
✅ Fix: Call CMS.RequestWithdrawal via gRPC
2. UI: Mock data
❌ FrontOffice/Pages/Network/OrganizationChart.razor (hardcoded nodes)
✅ Fix: @inject NetworkService → await GetTreeAsync()
3. DTO: Missing field
❌ UserWalletDto missing DiscountBalance
✅ Fix: Add property + map in Handler
📊 تحلیل کامل شکافهای پیادهسازی (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- وضعیت WorkerGetWorkerExecutionLogs- لاگ اجرای 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 غایب:
WithdrawBalanceHandler - خالی است و کار نمیکند
⚠️ مشکلات موجود:
GetUserWalletResponse فقط 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 ماژول موجود (کیفپول، سبد، قرارداد) ناقص
کارهایی که توسعهدهنده قبلی انجام نداد:
- ❌ هیچ Handler برای باشگاه (ClubMembership)
- ❌ هیچ Handler برای شبکه (NetworkMembership)
- ❌ هیچ Handler برای کمیسیون (Commission) به جز یک Handler خالی
- ❌ هیچ Handler برای وام دایا (DayaLoan)
- ⚠️ Handler کیفپول (UserWallet) ناقص - DiscountBalance غایب
- ⚠️ Handler سبد (ShoppingCart) ناقص - ClearCart, Merge غایب
- ⚠️ UI درخت شبکه (OrganizationChart) با داده Mock
تسکهای واقعی که باید از CMS به FrontOffice.BFF منتقل شوند:
- ✅ 26 Command موجود در CMS که در BFF نیستند
- ✅ 24 Query موجود در CMS که در BFF نیستند
- ✅ 15+ صفحه UI که باید در FrontOffice ساخته شوند
اولویتبندی توصیه شده:
- Week 1-2: ClubMembership - چون بدون این، کاربر نمیتواند عضو شود
- Week 3-5: Commission + Withdrawal - چون کاربر نمیتواند پول خود را ببیند/برداشت کند
- Week 6-7: NetworkMembership - چون درخت شبکه Mock است
- Week 8: UserWallet completion - اضافه کردن DiscountBalance و فیلترها
- Week 9: DayaLoan + ShoppingCart completion
این تحلیل نشان میدهد که حداقل 50 روز کاری (2 ماه) برای تکمیل نیاز است.
🌳 مرحله 3: راهنمای گامبهگام - NetworkMembership (شبکه باینری)
📊 خلاصه ماژول
هدف کسبوکار: مشتری باید بتواند درخت شبکه باینری خود را ببیند (پدر، فرزند چپ، فرزند راست)، موقعیت خود را بررسی کند، و تاریخچه جابجاییها را مشاهده نماید.
اجزای موجود در CMS:
- ✅
NetworkMembershipEntity با 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
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
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: ساخت فولدرها
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
using MediatR;
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
/// <summary>
/// Query برای دریافت درخت شبکه کاربر جاری
/// </summary>
public record GetMyNetworkTreeQuery : IRequest<MyNetworkTreeResponseDto>
{
/// <summary>
/// عمق درخت (چند لایه زیرمجموعه نمایش داده شود)
/// پیشفرض: 3 لایه
/// </summary>
public int Depth { get; init; } = 3;
}
فایل 2: MyNetworkTreeResponseDto.cs
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
/// <summary>
/// DTO مشتریمحور برای نمایش درخت شبکه
/// </summary>
public class MyNetworkTreeResponseDto
{
public NetworkNodeDto CurrentUser { get; set; }
public int TotalNetworkSize { get; set; } // تعداد کل افراد در شبکه
public int DirectChildrenCount { get; set; } // تعداد فرزندان مستقیم
public string LastUpdatePersian { get; set; } // آخرین بهروزرسانی
}
/// <summary>
/// نود درخت (Recursive)
/// </summary>
public class NetworkNodeDto
{
public long UserId { get; set; }
public string FullName { get; set; }
public string Mobile { get; set; }
public string Position { get; set; } // "Root" / "Left" / "Right"
public string JoinDatePersian { get; set; }
public bool HasLeftChild { get; set; }
public bool HasRightChild { get; set; }
// Recursive children
public NetworkNodeDto LeftChild { get; set; }
public NetworkNodeDto RightChild { get; set; }
// UI Helper fields
public string StatusBadge { get; set; } // "فعال" / "غیرفعال"
public string StatusColor { get; set; } // "success" / "error"
}
فایل 3: GetMyNetworkTreeQueryHandler.cs
using MediatR;
using FrontOffice.BFF.Application.Common.Interfaces;
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
public class GetMyNetworkTreeQueryHandler
: IRequestHandler<GetMyNetworkTreeQuery, MyNetworkTreeResponseDto>
{
private readonly ICurrentUserService _currentUser;
// TODO: private readonly NetworkMembershipServiceClient _cmsClient;
public GetMyNetworkTreeQueryHandler(ICurrentUserService currentUser)
{
_currentUser = currentUser;
}
public async Task<MyNetworkTreeResponseDto> Handle(
GetMyNetworkTreeQuery request,
CancellationToken cancellationToken)
{
var userId = _currentUser.UserId;
// TODO: فراخوانی CMS
// var cmsResult = await _cmsClient.GetNetworkTreeAsync(
// new GetNetworkTreeRequest { UserId = userId, Depth = request.Depth });
// Mock Data برای تست UI
return new MyNetworkTreeResponseDto
{
CurrentUser = new NetworkNodeDto
{
UserId = userId,
FullName = "علی احمدی",
Mobile = "09121234567",
Position = "Root",
JoinDatePersian = "1 آذر 1403",
HasLeftChild = true,
HasRightChild = true,
StatusBadge = "فعال",
StatusColor = "success",
LeftChild = new NetworkNodeDto
{
UserId = 101,
FullName = "رضا محمدی",
Mobile = "09129876543",
Position = "Left",
JoinDatePersian = "5 آذر 1403",
HasLeftChild = false,
HasRightChild = false,
StatusBadge = "فعال",
StatusColor = "success"
},
RightChild = new NetworkNodeDto
{
UserId = 102,
FullName = "سارا کریمی",
Mobile = "09131111111",
Position = "Right",
JoinDatePersian = "10 آذر 1403",
HasLeftChild = false,
HasRightChild = false,
StatusBadge = "فعال",
StatusColor = "success"
}
},
TotalNetworkSize = 3,
DirectChildrenCount = 2,
LastUpdatePersian = "15 آذر 1403"
};
}
}
Checkpoint Task 2.2:
[ ] 3 فایل ایجاد شدند
[ ] Recursive DTO به درستی تعریف شد
[ ] Mock tree data با 2 فرزند برمیگردد
Task 2.3: Query #2 - GetMyNetworkPosition (موقعیت من)
فایل 1: GetMyNetworkPositionQuery.cs
using MediatR;
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkPosition;
public record GetMyNetworkPositionQuery : IRequest<MyNetworkPositionResponseDto>
{
}
فایل 2: MyNetworkPositionResponseDto.cs
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)
using MediatR;
using FrontOffice.BFF.Application.Common.Interfaces;
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkPosition;
public class GetMyNetworkPositionQueryHandler
: IRequestHandler<GetMyNetworkPositionQuery, MyNetworkPositionResponseDto>
{
private readonly ICurrentUserService _currentUser;
public GetMyNetworkPositionQueryHandler(ICurrentUserService currentUser)
{
_currentUser = currentUser;
}
public async Task<MyNetworkPositionResponseDto> Handle(
GetMyNetworkPositionQuery request,
CancellationToken cancellationToken)
{
// TODO: Call CMS
return new MyNetworkPositionResponseDto
{
HasParent = true,
ParentFullName = "حسن رضایی",
ParentMobile = "09123456789",
MyPosition = "چپ",
MyPositionIcon = "arrow_back",
NetworkLevel = 2,
TotalDownlineCount = 5,
JoinDatePersian = "1 آذر 1403"
};
}
}
📝 STEP 3: اضافه کردن Controller
فایل: NetworkMembershipController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MediatR;
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkPosition;
namespace FrontOffice.BFF.WebApi.Controllers;
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class NetworkMembershipController : ControllerBase
{
private readonly IMediator _mediator;
public NetworkMembershipController(IMediator mediator)
{
_mediator = mediator;
}
/// <summary>
/// دریافت درخت شبکه من
/// </summary>
[HttpGet("my-tree")]
[ProducesResponseType(typeof(MyNetworkTreeResponseDto), 200)]
public async Task<IActionResult> GetMyTree([FromQuery] int depth = 3)
{
var query = new GetMyNetworkTreeQuery { Depth = depth };
var result = await _mediator.Send(query);
return Ok(result);
}
/// <summary>
/// دریافت موقعیت من در شبکه
/// </summary>
[HttpGet("my-position")]
[ProducesResponseType(typeof(MyNetworkPositionResponseDto), 200)]
public async Task<IActionResult> GetMyPosition()
{
var query = new GetMyNetworkPositionQuery();
var result = await _mediator.Send(query);
return Ok(result);
}
}
Test Endpoints:
# 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
cd /home/masoud/Apps/project/FourSat/FrontOffice/src/FrontOffice.Main/Services/
nano NetworkMembershipService.cs
using System.Net.Http.Json;
using FrontOffice.Main.Models;
namespace FrontOffice.Main.Services;
public class NetworkMembershipService
{
private readonly HttpClient _httpClient;
public NetworkMembershipService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<MyNetworkTreeDto> GetMyTreeAsync(int depth = 3)
{
var response = await _httpClient.GetAsync($"/api/networkmembership/my-tree?depth={depth}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<MyNetworkTreeDto>();
}
public async Task<MyNetworkPositionDto> GetMyPositionAsync()
{
var response = await _httpClient.GetAsync("/api/networkmembership/my-position");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<MyNetworkPositionDto>();
}
}
ثبت در Program.cs:
builder.Services.AddScoped<NetworkMembershipService>();
Task 4.2: Models
// 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)
cd /home/masoud/Apps/project/FourSat/FrontOffice/src/FrontOffice.Main/Components/Network/
mkdir -p Network
nano NetworkTreeNode.razor
@* Component برای نمایش یک نود درخت (Recursive) *@
<MudCard Class="ma-2" Style="min-width: 250px;">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.body1"><strong>@Node.FullName</strong></MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">@Node.Mobile</MudText>
</CardHeaderContent>
<CardHeaderActions>
<MudChip Size="Size.Small"
Color="@(Node.StatusColor == "success" ? Color.Success : Color.Error)">
@Node.StatusBadge
</MudChip>
</CardHeaderActions>
</MudCardHeader>
<MudCardContent>
<MudText Typo="Typo.caption">موقعیت: @Node.Position</MudText>
<MudText Typo="Typo.caption">تاریخ: @Node.JoinDatePersian</MudText>
</MudCardContent>
</MudCard>
@if (Node.LeftChild != null || Node.RightChild != null)
{
<MudGrid Class="mt-2" Justify="Justify.Center">
@if (Node.LeftChild != null)
{
<MudItem xs="6">
<div style="border-right: 2px solid #ccc; padding-right: 10px;">
<MudText Typo="Typo.caption" Color="Color.Primary">← چپ</MudText>
<NetworkTreeNode Node="@Node.LeftChild" />
</div>
</MudItem>
}
@if (Node.RightChild != null)
{
<MudItem xs="6">
<div style="border-left: 2px solid #ccc; padding-left: 10px;">
<MudText Typo="Typo.caption" Color="Color.Secondary">راست →</MudText>
<NetworkTreeNode Node="@Node.RightChild" />
</div>
</MudItem>
}
</MudGrid>
}
@code {
[Parameter]
public NetworkNodeDto Node { get; set; }
}
Task 4.4: Page - NetworkTreePage
nano /home/masoud/Apps/project/FourSat/FrontOffice/src/FrontOffice.Main/Pages/Network/NetworkTreePage.razor
@page "/network/tree"
@inject NetworkMembershipService NetworkService
@inject ISnackbar Snackbar
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
<MudText Typo="Typo.h4" Class="mb-4">شبکه باینری من</MudText>
@if (_loading)
{
<MudProgressLinear Indeterminate="true" Color="Color.Primary" />
}
else if (_tree != null)
{
<MudGrid>
<MudItem xs="12" md="4">
<MudCard>
<MudCardContent>
<MudStack Spacing="2">
<MudText Typo="Typo.h6">آمار کلی</MudText>
<MudText>
<strong>تعداد کل اعضا:</strong> @_tree.TotalNetworkSize نفر
</MudText>
<MudText>
<strong>فرزندان مستقیم:</strong> @_tree.DirectChildrenCount نفر
</MudText>
<MudText>
<strong>آخرین بهروزرسانی:</strong> @_tree.LastUpdatePersian
</MudText>
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" md="8">
<MudText Typo="Typo.h6" Class="mb-2">درخت شبکه</MudText>
<div style="overflow-x: auto;">
<NetworkTreeNode Node="@_tree.CurrentUser" />
</div>
</MudItem>
</MudGrid>
}
</MudContainer>
@code {
private MyNetworkTreeDto? _tree;
private bool _loading = true;
protected override async Task OnInitializedAsync()
{
await LoadTree();
}
private async Task LoadTree()
{
try
{
_loading = true;
_tree = await NetworkService.GetMyTreeAsync(depth: 3);
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
}
}
}
Task 4.5: Page - NetworkPositionPage
nano /home/masoud/Apps/project/FourSat/FrontOffice/src/FrontOffice.Main/Pages/Network/NetworkPositionPage.razor
@page "/network/position"
@inject NetworkMembershipService NetworkService
@inject ISnackbar Snackbar
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
<MudText Typo="Typo.h4" Class="mb-4">موقعیت من در شبکه</MudText>
@if (_loading)
{
<MudProgressLinear Indeterminate="true" />
}
else if (_position != null)
{
<MudCard>
<MudCardContent>
<MudGrid>
@if (_position.HasParent)
{
<MudItem xs="12" md="6">
<MudPaper Class="pa-4" Elevation="2">
<MudText Typo="Typo.h6" Class="mb-2">معرف من</MudText>
<MudText><strong>نام:</strong> @_position.ParentFullName</MudText>
<MudText><strong>موبایل:</strong> @_position.ParentMobile</MudText>
</MudPaper>
</MudItem>
}
<MudItem xs="12" md="6">
<MudPaper Class="pa-4" Elevation="2">
<MudText Typo="Typo.h6" Class="mb-2">موقعیت من</MudText>
<MudText>
<MudIcon Icon="@_position.MyPositionIcon" />
<strong>@_position.MyPosition</strong>
</MudText>
<MudText><strong>سطح:</strong> @_position.NetworkLevel</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12">
<MudPaper Class="pa-4" Elevation="2">
<MudText Typo="Typo.h6" Class="mb-2">آمار زیرمجموعه</MudText>
<MudText>
<strong>تعداد کل افراد زیر مجموعه:</strong> @_position.TotalDownlineCount نفر
</MudText>
<MudText>
<strong>تاریخ پیوستن:</strong> @_position.JoinDatePersian
</MudText>
</MudPaper>
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
}
</MudContainer>
@code {
private MyNetworkPositionDto? _position;
private bool _loading = true;
protected override async Task OnInitializedAsync()
{
await LoadPosition();
}
private async Task LoadPosition()
{
try
{
_loading = true;
_position = await NetworkService.GetMyPositionAsync();
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
}
}
}
Task 4.6: اضافه کردن به NavMenu
<MudNavGroup Title="شبکه من" Icon="@Icons.Material.Filled.AccountTree">
<MudNavLink Href="/network/tree" Icon="@Icons.Material.Filled.Schema">
درخت شبکه
</MudNavLink>
<MudNavLink Href="/network/position" Icon="@Icons.Material.Filled.MyLocation">
موقعیت من
</MudNavLink>
</MudNavGroup>
✅ Checkpoint نهایی STEP 4
# 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
- Recursive Component:
NetworkTreeNodeبه صورت Recursive خودش را صدا میزند - مراقب Performance باش - Depth Control: هرگز
depth > 5نگذار (درخت خیلی بزرگ میشود) - UI Overflow: از
overflow-x: autoبرای درختهای بزرگ استفاده شد - CMS Integration: بعد از اتصال به CMS، حتماً Handle کن که LeftChild/RightChild ممکنه
nullباشند
💰 مرحله 4: راهنمای گامبهگام - Commission + Withdrawal (کمیسیون و برداشت)
📊 خلاصه ماژول
هدف کسبوکار: مشتری باید بتواند کمیسیونهای خود را مشاهده کند، درخواست برداشت بدهد، وضعیت برداشتها را پیگیری کند، و موجودی قابل برداشت خود را ببیند.
اجزای موجود در CMS:
- ✅
CommissionPayoutEntity (مبلغ، هفته، وضعیت، تاریخ) - ✅
WithdrawalRequestEntity (مبلغ، وضعیت: 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
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
# Query 1: GetUserCommissionPayouts
cat CMSMicroservice.Application/CommissionCQ/Queries/GetUserCommissionPayouts/GetUserCommissionPayoutsQueryHandler.cs
# Input: UserId, FromDate, ToDate, PageNumber, PageSize
# Output: List<CommissionPayoutDto> + TotalCount
# Query 2: GetUserBalance
cat CMSMicroservice.Application/CommissionCQ/Queries/GetUserBalance/GetUserBalanceQueryHandler.cs
# Input: UserId
# Output: CommissionBalance, WithdrawableBalance, PendingWithdrawal, TotalEarned
# Query 3: GetWithdrawalHistory
cat CMSMicroservice.Application/CommissionCQ/Queries/GetWithdrawalHistory/GetWithdrawalHistoryQueryHandler.cs
# Input: UserId, FromDate, ToDate, Status (optional)
# Output: List<WithdrawalRequestDto>
# Query 4: GetAvailableBalance
cat CMSMicroservice.Application/CommissionCQ/Queries/GetAvailableBalance/GetAvailableBalanceQueryHandler.cs
# Input: UserId
# Output: AvailableAmount, MinWithdrawalAmount, MaxWithdrawalAmount
Task 1.3: بررسی Command RequestWithdrawal
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: ساخت فولدرها
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
using MediatR;
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
public record GetMyCommissionPayoutsQuery : IRequest<MyCommissionPayoutsResponseDto>
{
/// <summary>
/// تعداد آیتم در هر صفحه (پیشفرض: 10)
/// </summary>
public int PageSize { get; init; } = 10;
/// <summary>
/// شماره صفحه (پیشفرض: 1)
/// </summary>
public int PageNumber { get; init; } = 1;
/// <summary>
/// فیلتر بر اساس نوع کمیسیون (اختیاری)
/// </summary>
public string PayoutType { get; init; }
}
فایل 2: MyCommissionPayoutsResponseDto.cs
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
public class MyCommissionPayoutsResponseDto
{
public List<CommissionPayoutItemDto> Payouts { get; set; }
public int TotalCount { get; set; }
public int CurrentPage { get; set; }
public int TotalPages { get; set; }
public decimal TotalAmount { get; set; } // مجموع کل کمیسیونها
}
public class CommissionPayoutItemDto
{
public long Id { get; set; }
public string WeekDisplay { get; set; } // "هفته 48 - سال 1403"
public decimal Amount { get; set; }
public string AmountFormatted { get; set; } // "1,250,000 تومان"
public string PayoutType { get; set; } // "مستقیم" / "باینری" / "پول" / "باشگاه"
public string PayoutTypeIcon { get; set; } // Icon name for UI
public string Status { get; set; } // "در انتظار" / "محاسبه شده" / "پرداخت شده"
public string StatusColor { get; set; } // "warning" / "info" / "success"
public string PayoutDatePersian { get; set; }
}
فایل 3: GetMyCommissionPayoutsQueryHandler.cs
using MediatR;
using FrontOffice.BFF.Application.Common.Interfaces;
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
public class GetMyCommissionPayoutsQueryHandler
: IRequestHandler<GetMyCommissionPayoutsQuery, MyCommissionPayoutsResponseDto>
{
private readonly ICurrentUserService _currentUser;
// TODO: private readonly CommissionServiceClient _cmsClient;
public GetMyCommissionPayoutsQueryHandler(ICurrentUserService currentUser)
{
_currentUser = currentUser;
}
public async Task<MyCommissionPayoutsResponseDto> Handle(
GetMyCommissionPayoutsQuery request,
CancellationToken cancellationToken)
{
var userId = _currentUser.UserId;
// TODO: فراخوانی CMS
// var cmsResult = await _cmsClient.GetUserCommissionPayoutsAsync(
// new GetUserCommissionPayoutsRequest {
// UserId = userId,
// PageNumber = request.PageNumber,
// PageSize = request.PageSize
// });
// Mock Data
return new MyCommissionPayoutsResponseDto
{
Payouts = new List<CommissionPayoutItemDto>
{
new() {
Id = 1,
WeekDisplay = "هفته 48 - سال 1403",
Amount = 1250000,
AmountFormatted = "1,250,000 تومان",
PayoutType = "مستقیم",
PayoutTypeIcon = "trending_up",
Status = "پرداخت شده",
StatusColor = "success",
PayoutDatePersian = "20 آذر 1403"
},
new() {
Id = 2,
WeekDisplay = "هفته 47 - سال 1403",
Amount = 850000,
AmountFormatted = "850,000 تومان",
PayoutType = "باینری",
PayoutTypeIcon = "account_tree",
Status = "پرداخت شده",
StatusColor = "success",
PayoutDatePersian = "13 آذر 1403"
}
},
TotalCount = 2,
CurrentPage = 1,
TotalPages = 1,
TotalAmount = 2100000
};
}
}
Task 2.3: Query #2 - GetMyBalance (موجودی)
فایل 1: GetMyBalanceQuery.cs
using MediatR;
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyBalance;
public record GetMyBalanceQuery : IRequest<MyBalanceResponseDto>
{
}
فایل 2: MyBalanceResponseDto.cs
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
using MediatR;
using FrontOffice.BFF.Application.Common.Interfaces;
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyBalance;
public class GetMyBalanceQueryHandler
: IRequestHandler<GetMyBalanceQuery, MyBalanceResponseDto>
{
private readonly ICurrentUserService _currentUser;
public GetMyBalanceQueryHandler(ICurrentUserService currentUser)
{
_currentUser = currentUser;
}
public async Task<MyBalanceResponseDto> Handle(
GetMyBalanceQuery request,
CancellationToken cancellationToken)
{
var userId = _currentUser.UserId;
// TODO: Call CMS
// Mock Data
return new MyBalanceResponseDto
{
TotalEarned = 15750000,
TotalEarnedFormatted = "15,750,000 تومان",
CurrentBalance = 8500000,
CurrentBalanceFormatted = "8,500,000 تومان",
WithdrawableBalance = 7000000,
WithdrawableBalanceFormatted = "7,000,000 تومان",
PendingWithdrawal = 1500000,
PendingWithdrawalFormatted = "1,500,000 تومان",
CanRequestWithdrawal = true,
MinWithdrawalAmount = "100,000 تومان",
MaxWithdrawalAmount = "7,000,000 تومان"
};
}
}
Task 2.4: Query #3 - GetMyWithdrawalHistory
فایل 1: GetMyWithdrawalHistoryQuery.cs
using MediatR;
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyWithdrawalHistory;
public record GetMyWithdrawalHistoryQuery : IRequest<MyWithdrawalHistoryResponseDto>
{
public int PageSize { get; init; } = 10;
public int PageNumber { get; init; } = 1;
}
فایل 2: MyWithdrawalHistoryResponseDto.cs
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyWithdrawalHistory;
public class MyWithdrawalHistoryResponseDto
{
public List<WithdrawalItemDto> Withdrawals { get; set; }
public int TotalCount { get; set; }
}
public class WithdrawalItemDto
{
public long Id { get; set; }
public decimal Amount { get; set; }
public string AmountFormatted { get; set; }
public string Status { get; set; } // "در انتظار" / "تایید" / "رد" / "پرداخت شده"
public string StatusColor { get; set; } // "warning" / "success" / "error" / "info"
public string RequestDatePersian { get; set; }
public string ProcessDatePersian { get; set; }
public string BankAccount { get; set; } // "6037-****-****-1234"
public string RejectReason { get; set; } // دلیل رد (اگر رد شده)
}
فایل 3: GetMyWithdrawalHistoryQueryHandler.cs
using MediatR;
using FrontOffice.BFF.Application.Common.Interfaces;
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyWithdrawalHistory;
public class GetMyWithdrawalHistoryQueryHandler
: IRequestHandler<GetMyWithdrawalHistoryQuery, MyWithdrawalHistoryResponseDto>
{
private readonly ICurrentUserService _currentUser;
public GetMyWithdrawalHistoryQueryHandler(ICurrentUserService currentUser)
{
_currentUser = currentUser;
}
public async Task<MyWithdrawalHistoryResponseDto> Handle(
GetMyWithdrawalHistoryQuery request,
CancellationToken cancellationToken)
{
// TODO: Call CMS
return new MyWithdrawalHistoryResponseDto
{
Withdrawals = new List<WithdrawalItemDto>
{
new() {
Id = 1,
Amount = 1500000,
AmountFormatted = "1,500,000 تومان",
Status = "در انتظار",
StatusColor = "warning",
RequestDatePersian = "25 آذر 1403",
ProcessDatePersian = "-",
BankAccount = "6037-****-****-1234"
},
new() {
Id = 2,
Amount = 2000000,
AmountFormatted = "2,000,000 تومان",
Status = "پرداخت شده",
StatusColor = "success",
RequestDatePersian = "15 آذر 1403",
ProcessDatePersian = "18 آذر 1403",
BankAccount = "6037-****-****-1234"
}
},
TotalCount = 2
};
}
}
Task 2.5: Command - RequestMyWithdrawal
فایل 1: RequestMyWithdrawalCommand.cs
using MediatR;
namespace FrontOffice.BFF.Application.CommissionCQ.Commands.RequestMyWithdrawal;
public record RequestMyWithdrawalCommand : IRequest<RequestMyWithdrawalResponseDto>
{
public decimal Amount { get; init; }
public string BankAccountNumber { get; init; } // شماره کارت یا شبا
}
فایل 2: RequestMyWithdrawalResponseDto.cs
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
using MediatR;
using FrontOffice.BFF.Application.Common.Interfaces;
namespace FrontOffice.BFF.Application.CommissionCQ.Commands.RequestMyWithdrawal;
public class RequestMyWithdrawalCommandHandler
: IRequestHandler<RequestMyWithdrawalCommand, RequestMyWithdrawalResponseDto>
{
private readonly ICurrentUserService _currentUser;
// TODO: private readonly CommissionServiceClient _cmsClient;
public RequestMyWithdrawalCommandHandler(ICurrentUserService currentUser)
{
_currentUser = currentUser;
}
public async Task<RequestMyWithdrawalResponseDto> Handle(
RequestMyWithdrawalCommand request,
CancellationToken cancellationToken)
{
var userId = _currentUser.UserId;
// TODO: فراخوانی CMS
// var cmsResult = await _cmsClient.RequestWithdrawalAsync(
// new RequestWithdrawalRequest {
// UserId = userId,
// Amount = request.Amount,
// BankAccountNumber = request.BankAccountNumber
// });
// Mock Response
return new RequestMyWithdrawalResponseDto
{
Success = true,
WithdrawalRequestId = 123,
Message = "درخواست برداشت شما با موفقیت ثبت شد و در انتظار تایید است.",
NewWithdrawableBalance = "5,500,000 تومان"
};
}
}
فایل 4: RequestMyWithdrawalCommandValidator.cs
using FluentValidation;
namespace FrontOffice.BFF.Application.CommissionCQ.Commands.RequestMyWithdrawal;
public class RequestMyWithdrawalCommandValidator : AbstractValidator<RequestMyWithdrawalCommand>
{
public RequestMyWithdrawalCommandValidator()
{
RuleFor(x => x.Amount)
.GreaterThan(0).WithMessage("مبلغ باید بیشتر از صفر باشد")
.LessThanOrEqualTo(50000000).WithMessage("حداکثر مبلغ برداشت 50 میلیون تومان است");
RuleFor(x => x.BankAccountNumber)
.NotEmpty().WithMessage("شماره کارت الزامی است")
.Length(16, 24).WithMessage("شماره کارت یا شبا نامعتبر است");
}
}
📝 STEP 3: اضافه کردن Controller
فایل: CommissionController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MediatR;
using FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
using FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyBalance;
using FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyWithdrawalHistory;
using FrontOffice.BFF.Application.CommissionCQ.Commands.RequestMyWithdrawal;
namespace FrontOffice.BFF.WebApi.Controllers;
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class CommissionController : ControllerBase
{
private readonly IMediator _mediator;
public CommissionController(IMediator mediator)
{
_mediator = mediator;
}
/// <summary>
/// دریافت لیست کمیسیونهای من
/// </summary>
[HttpGet("my-payouts")]
[ProducesResponseType(typeof(MyCommissionPayoutsResponseDto), 200)]
public async Task<IActionResult> GetMyPayouts(
[FromQuery] int pageNumber = 1,
[FromQuery] int pageSize = 10)
{
var query = new GetMyCommissionPayoutsQuery
{
PageNumber = pageNumber,
PageSize = pageSize
};
var result = await _mediator.Send(query);
return Ok(result);
}
/// <summary>
/// دریافت موجودی من
/// </summary>
[HttpGet("my-balance")]
[ProducesResponseType(typeof(MyBalanceResponseDto), 200)]
public async Task<IActionResult> GetMyBalance()
{
var query = new GetMyBalanceQuery();
var result = await _mediator.Send(query);
return Ok(result);
}
/// <summary>
/// دریافت تاریخچه برداشتهای من
/// </summary>
[HttpGet("my-withdrawal-history")]
[ProducesResponseType(typeof(MyWithdrawalHistoryResponseDto), 200)]
public async Task<IActionResult> GetMyWithdrawalHistory(
[FromQuery] int pageNumber = 1,
[FromQuery] int pageSize = 10)
{
var query = new GetMyWithdrawalHistoryQuery
{
PageNumber = pageNumber,
PageSize = pageSize
};
var result = await _mediator.Send(query);
return Ok(result);
}
/// <summary>
/// درخواست برداشت
/// </summary>
[HttpPost("request-withdrawal")]
[ProducesResponseType(typeof(RequestMyWithdrawalResponseDto), 200)]
public async Task<IActionResult> RequestWithdrawal(
[FromBody] RequestMyWithdrawalCommand command)
{
var result = await _mediator.Send(command);
return Ok(result);
}
}
Test Endpoints:
# 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
cd /home/masoud/Apps/project/FourSat/FrontOffice/src/FrontOffice.Main/Services/
nano CommissionService.cs
using System.Net.Http.Json;
using FrontOffice.Main.Models;
namespace FrontOffice.Main.Services;
public class CommissionService
{
private readonly HttpClient _httpClient;
public CommissionService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<MyCommissionPayoutsDto> GetMyPayoutsAsync(int pageNumber = 1, int pageSize = 10)
{
var response = await _httpClient.GetAsync(
$"/api/commission/my-payouts?pageNumber={pageNumber}&pageSize={pageSize}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<MyCommissionPayoutsDto>();
}
public async Task<MyBalanceDto> GetMyBalanceAsync()
{
var response = await _httpClient.GetAsync("/api/commission/my-balance");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<MyBalanceDto>();
}
public async Task<MyWithdrawalHistoryDto> GetMyWithdrawalHistoryAsync(int pageNumber = 1)
{
var response = await _httpClient.GetAsync(
$"/api/commission/my-withdrawal-history?pageNumber={pageNumber}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<MyWithdrawalHistoryDto>();
}
public async Task<RequestWithdrawalResultDto> RequestWithdrawalAsync(decimal amount, string bankAccount)
{
var request = new { Amount = amount, BankAccountNumber = bankAccount };
var response = await _httpClient.PostAsJsonAsync("/api/commission/request-withdrawal", request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<RequestWithdrawalResultDto>();
}
}
ثبت در Program.cs:
builder.Services.AddScoped<CommissionService>();
Task 4.2: Models (در فولدر Models/)
// کپی DTOها از BFF به FrontOffice.Main/Models/
// MyCommissionPayoutsDto.cs
// MyBalanceDto.cs
// MyWithdrawalHistoryDto.cs
// RequestWithdrawalResultDto.cs
Task 4.3: Page - CommissionPayoutsPage (صفحه کمیسیونها)
@page "/commission/payouts"
@inject CommissionService CommissionService
@inject ISnackbar Snackbar
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
<MudText Typo="Typo.h4" Class="mb-4">کمیسیونهای من</MudText>
@if (_loading)
{
<MudProgressLinear Indeterminate="true" />
}
else if (_payouts != null)
{
<MudCard Class="mb-4">
<MudCardContent>
<MudText Typo="Typo.h6">
مجموع کل: <strong style="color: green;">@_payouts.TotalAmount.ToString("N0") تومان</strong>
</MudText>
</MudCardContent>
</MudCard>
<MudTable Items="@_payouts.Payouts" Hover="true" Striped="true">
<HeaderContent>
<MudTh>هفته</MudTh>
<MudTh>نوع</MudTh>
<MudTh>مبلغ</MudTh>
<MudTh>وضعیت</MudTh>
<MudTh>تاریخ پرداخت</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.WeekDisplay</MudTd>
<MudTd>
<MudIcon Icon="@context.PayoutTypeIcon" Size="Size.Small" />
@context.PayoutType
</MudTd>
<MudTd><strong>@context.AmountFormatted</strong></MudTd>
<MudTd>
<MudChip Size="Size.Small" Color="@GetStatusColor(context.StatusColor)">
@context.Status
</MudChip>
</MudTd>
<MudTd>@context.PayoutDatePersian</MudTd>
</RowTemplate>
</MudTable>
<MudPagination Class="mt-4"
Count="_payouts.TotalPages"
Selected="_currentPage"
SelectedChanged="OnPageChanged" />
}
</MudContainer>
@code {
private MyCommissionPayoutsDto? _payouts;
private bool _loading = true;
private int _currentPage = 1;
protected override async Task OnInitializedAsync()
{
await LoadPayouts();
}
private async Task LoadPayouts()
{
try
{
_loading = true;
_payouts = await CommissionService.GetMyPayoutsAsync(_currentPage, 10);
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
}
}
private async Task OnPageChanged(int page)
{
_currentPage = page;
await LoadPayouts();
}
private Color GetStatusColor(string color)
{
return color switch
{
"success" => Color.Success,
"warning" => Color.Warning,
"error" => Color.Error,
"info" => Color.Info,
_ => Color.Default
};
}
}
Task 4.4: Page - WithdrawalPage (صفحه برداشت)
@page "/commission/withdrawal"
@inject CommissionService CommissionService
@inject ISnackbar Snackbar
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
<MudText Typo="Typo.h4" Class="mb-4">برداشت وجه</MudText>
@if (_loadingBalance)
{
<MudProgressLinear Indeterminate="true" />
}
else if (_balance != null)
{
<MudGrid>
<MudItem xs="12" md="6">
<MudCard Class="mb-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">موجودی من</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudStack Spacing="2">
<MudText>
<strong>کل درآمد:</strong>
<span style="color: blue;">@_balance.TotalEarnedFormatted</span>
</MudText>
<MudText>
<strong>موجودی فعلی:</strong>
<span style="color: green;">@_balance.CurrentBalanceFormatted</span>
</MudText>
<MudText>
<strong>قابل برداشت:</strong>
<span style="color: orange; font-size: 1.2em;">@_balance.WithdrawableBalanceFormatted</span>
</MudText>
<MudText>
<strong>در انتظار برداشت:</strong> @_balance.PendingWithdrawalFormatted
</MudText>
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" md="6">
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">درخواست برداشت جدید</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudForm @ref="_form">
<MudNumericField @bind-Value="_withdrawalAmount"
Label="مبلغ (تومان)"
Variant="Variant.Outlined"
Required="true"
Min="100000"
Max="_balance.WithdrawableBalance" />
<MudTextField @bind-Value="_bankAccount"
Label="شماره کارت یا شبا"
Variant="Variant.Outlined"
Required="true"
MaxLength="24"
Class="mt-3" />
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="mt-2">
حداقل: @_balance.MinWithdrawalAmount | حداکثر: @_balance.MaxWithdrawalAmount
</MudText>
</MudForm>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Disabled="@(!_balance.CanRequestWithdrawal || _submitting)"
OnClick="SubmitWithdrawal">
@if (_submitting)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" />
<span class="ms-2">در حال ارسال...</span>
}
else
{
<span>ثبت درخواست</span>
}
</MudButton>
</MudCardActions>
</MudCard>
</MudItem>
</MudGrid>
<MudText Typo="Typo.h5" Class="mt-6 mb-3">تاریخچه برداشتها</MudText>
@if (_loadingHistory)
{
<MudProgressLinear Indeterminate="true" />
}
else if (_history != null)
{
<MudTable Items="@_history.Withdrawals" Hover="true">
<HeaderContent>
<MudTh>مبلغ</MudTh>
<MudTh>وضعیت</MudTh>
<MudTh>تاریخ درخواست</MudTh>
<MudTh>تاریخ پردازش</MudTh>
<MudTh>شماره کارت</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd><strong>@context.AmountFormatted</strong></MudTd>
<MudTd>
<MudChip Size="Size.Small" Color="@GetStatusColor(context.StatusColor)">
@context.Status
</MudChip>
</MudTd>
<MudTd>@context.RequestDatePersian</MudTd>
<MudTd>@context.ProcessDatePersian</MudTd>
<MudTd>@context.BankAccount</MudTd>
</RowTemplate>
</MudTable>
}
}
</MudContainer>
@code {
private MyBalanceDto? _balance;
private MyWithdrawalHistoryDto? _history;
private bool _loadingBalance = true;
private bool _loadingHistory = true;
private bool _submitting = false;
private MudForm _form;
private decimal _withdrawalAmount;
private string _bankAccount = "";
protected override async Task OnInitializedAsync()
{
await Task.WhenAll(LoadBalance(), LoadHistory());
}
private async Task LoadBalance()
{
try
{
_loadingBalance = true;
_balance = await CommissionService.GetMyBalanceAsync();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری موجودی: {ex.Message}", Severity.Error);
}
finally
{
_loadingBalance = false;
}
}
private async Task LoadHistory()
{
try
{
_loadingHistory = true;
_history = await CommissionService.GetMyWithdrawalHistoryAsync();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری تاریخچه: {ex.Message}", Severity.Error);
}
finally
{
_loadingHistory = false;
}
}
private async Task SubmitWithdrawal()
{
await _form.Validate();
if (!_form.IsValid) return;
try
{
_submitting = true;
var result = await CommissionService.RequestWithdrawalAsync(_withdrawalAmount, _bankAccount);
if (result.Success)
{
Snackbar.Add(result.Message, Severity.Success);
_withdrawalAmount = 0;
_bankAccount = "";
await Task.WhenAll(LoadBalance(), LoadHistory());
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
finally
{
_submitting = false;
}
}
private Color GetStatusColor(string color)
{
return color switch
{
"success" => Color.Success,
"warning" => Color.Warning,
"error" => Color.Error,
_ => Color.Default
};
}
}
Task 4.5: اضافه کردن به NavMenu
<MudNavGroup Title="کمیسیون و برداشت" Icon="@Icons.Material.Filled.AccountBalanceWallet">
<MudNavLink Href="/commission/payouts" Icon="@Icons.Material.Filled.Payments">
کمیسیونهای من
</MudNavLink>
<MudNavLink Href="/commission/withdrawal" Icon="@Icons.Material.Filled.LocalAtm">
برداشت وجه
</MudNavLink>
</MudNavGroup>
✅ Checkpoint نهایی
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
- Validation: از FluentValidation استفاده شد - حتماً Validator را در DI ثبت کن
- Amount Formatting: همه مبالغ با Format "N0" نمایش داده میشوند (1,250,000)
- Bank Account Masking: شماره کارت را Mask کن: "6037---1234"
- Minimum Withdrawal: در CMS حداقل مبلغ برداشت را Check کن (معمولاً 100,000 تومان)
- Concurrent Requests: کاربر نباید بتواند همزمان چند درخواست برداشت بزند
- 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
cd /home/masoud/Apps/project/FourSat/CMS/src/
cat CMSMicroservice.Domain/Entities/UserWallet.cs
# باید ببینی:
# - MainBalance: موجودی اصلی ✅
# - DiscountBalance: موجودی تخفیف ❌ (این قسمت غایب است)
# - RewardBalance: موجودی پاداش ✅
Task 1.2: بررسی Queries موجود
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 وجود ندارد، اضافه کن:
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:
// در GetMyWalletQueryHandler.cs
public async Task<MyWalletResponseDto> Handle(...)
{
var userId = _currentUser.UserId;
// TODO: Call CMS
// var wallet = await _cmsClient.GetUserWalletAsync(new { UserId = userId });
// Mock Data با DiscountBalance
var mainBalance = 5000000m;
var rewardBalance = 1200000m;
var discountBalance = 800000m; // اضافه شد
var total = mainBalance + rewardBalance + discountBalance;
return new MyWalletResponseDto
{
MainBalance = mainBalance,
MainBalanceFormatted = mainBalance.ToString("N0") + " تومان",
RewardBalance = rewardBalance,
RewardBalanceFormatted = rewardBalance.ToString("N0") + " تومان",
DiscountBalance = discountBalance,
DiscountBalanceFormatted = discountBalance.ToString("N0") + " تومان",
TotalBalance = total,
TotalBalanceFormatted = total.ToString("N0") + " تومان",
HasDiscount = discountBalance > 0,
DiscountPercentage = "15%"
};
}
Task 2.2: ایجاد Query جدید - GetMyDiscountTransactions
فایل 1: GetMyDiscountTransactionsQuery.cs
mkdir -p FrontOffice.BFF.Application/UserWalletCQ/Queries/GetMyDiscountTransactions/
nano GetMyDiscountTransactionsQuery.cs
using MediatR;
namespace FrontOffice.BFF.Application.UserWalletCQ.Queries.GetMyDiscountTransactions;
public record GetMyDiscountTransactionsQuery : IRequest<MyDiscountTransactionsResponseDto>
{
public int PageNumber { get; init; } = 1;
public int PageSize { get; init; } = 10;
}
فایل 2: MyDiscountTransactionsResponseDto.cs
namespace FrontOffice.BFF.Application.UserWalletCQ.Queries.GetMyDiscountTransactions;
public class MyDiscountTransactionsResponseDto
{
public List<DiscountTransactionItemDto> Transactions { get; set; }
public int TotalCount { get; set; }
}
public class DiscountTransactionItemDto
{
public long Id { get; set; }
public string Type { get; set; } // "دریافت" / "استفاده"
public string TypeIcon { get; set; } // "add_circle" / "remove_circle"
public string TypeColor { get; set; } // "success" / "error"
public decimal Amount { get; set; }
public string AmountFormatted { get; set; }
public string Description { get; set; } // "تخفیف خرید محصول X"
public string DatePersian { get; set; }
}
فایل 3: GetMyDiscountTransactionsQueryHandler.cs
using MediatR;
using FrontOffice.BFF.Application.Common.Interfaces;
namespace FrontOffice.BFF.Application.UserWalletCQ.Queries.GetMyDiscountTransactions;
public class GetMyDiscountTransactionsQueryHandler
: IRequestHandler<GetMyDiscountTransactionsQuery, MyDiscountTransactionsResponseDto>
{
private readonly ICurrentUserService _currentUser;
public GetMyDiscountTransactionsQueryHandler(ICurrentUserService currentUser)
{
_currentUser = currentUser;
}
public async Task<MyDiscountTransactionsResponseDto> Handle(
GetMyDiscountTransactionsQuery request,
CancellationToken cancellationToken)
{
var userId = _currentUser.UserId;
// TODO: Call CMS
// Mock Data
return new MyDiscountTransactionsResponseDto
{
Transactions = new List<DiscountTransactionItemDto>
{
new() {
Id = 1,
Type = "دریافت",
TypeIcon = "add_circle",
TypeColor = "success",
Amount = 500000,
AmountFormatted = "500,000 تومان",
Description = "تخفیف خرید بسته طلایی",
DatePersian = "20 آذر 1403"
},
new() {
Id = 2,
Type = "استفاده",
TypeIcon = "remove_circle",
TypeColor = "error",
Amount = -200000,
AmountFormatted = "200,000 تومان",
Description = "استفاده در خرید محصول A",
DatePersian = "22 آذر 1403"
}
},
TotalCount = 2
};
}
}
Task 2.3: آپدیت Controller
فایل موجود: UserWalletController.cs
// اضافه کردن endpoint جدید
using FrontOffice.BFF.Application.UserWalletCQ.Queries.GetMyDiscountTransactions;
[HttpGet("my-discount-transactions")]
[ProducesResponseType(typeof(MyDiscountTransactionsResponseDto), 200)]
public async Task<IActionResult> GetMyDiscountTransactions(
[FromQuery] int pageNumber = 1,
[FromQuery] int pageSize = 10)
{
var query = new GetMyDiscountTransactionsQuery
{
PageNumber = pageNumber,
PageSize = pageSize
};
var result = await _mediator.Send(query);
return Ok(result);
}
📝 STEP 3: تکمیل Withdrawal Handler
Task 3.1: پیدا کردن WithdrawalFromWallet Handler
find FrontOffice.BFF.Application/UserWalletCQ/ -name "*Withdrawal*"
# باید پیدا کنی: Commands/WithdrawalFromWallet/WithdrawalFromWalletCommandHandler.cs
Task 3.2: تکمیل Handler خالی
// فایل موجود: WithdrawalFromWalletCommandHandler.cs
using MediatR;
using FrontOffice.BFF.Application.Common.Interfaces;
namespace FrontOffice.BFF.Application.UserWalletCQ.Commands.WithdrawalFromWallet;
public class WithdrawalFromWalletCommandHandler
: IRequestHandler<WithdrawalFromWalletCommand, WithdrawalFromWalletResponseDto>
{
private readonly ICurrentUserService _currentUser;
// TODO: private readonly UserWalletServiceClient _cmsClient;
public WithdrawalFromWalletCommandHandler(ICurrentUserService currentUser)
{
_currentUser = currentUser;
}
public async Task<WithdrawalFromWalletResponseDto> Handle(
WithdrawalFromWalletCommand request,
CancellationToken cancellationToken)
{
var userId = _currentUser.UserId;
// TODO: فراخوانی CMS
// var result = await _cmsClient.WithdrawalFromWalletAsync(new {
// UserId = userId,
// Amount = request.Amount,
// WalletType = request.WalletType // Main / Reward / Discount
// });
// Mock Response
return new WithdrawalFromWalletResponseDto
{
Success = true,
TransactionId = 456,
Message = "برداشت با موفقیت انجام شد",
NewBalance = "4,500,000 تومان"
};
}
}
Task 3.3: اضافه کردن Validator
// فایل جدید: WithdrawalFromWalletCommandValidator.cs
using FluentValidation;
namespace FrontOffice.BFF.Application.UserWalletCQ.Commands.WithdrawalFromWallet;
public class WithdrawalFromWalletCommandValidator : AbstractValidator<WithdrawalFromWalletCommand>
{
public WithdrawalFromWalletCommandValidator()
{
RuleFor(x => x.Amount)
.GreaterThan(0).WithMessage("مبلغ باید بیشتر از صفر باشد")
.LessThanOrEqualTo(10000000).WithMessage("حداکثر مبلغ برداشت 10 میلیون تومان است");
RuleFor(x => x.WalletType)
.NotEmpty().WithMessage("نوع کیف پول الزامی است")
.Must(x => new[] { "Main", "Reward", "Discount" }.Contains(x))
.WithMessage("نوع کیف پول نامعتبر است");
}
}
📝 STEP 4: آپدیت UI - WalletPage
Task 4.1: اضافه کردن DiscountBalance به Service
// فایل موجود: FrontOffice.Main/Services/UserWalletService.cs
public async Task<MyDiscountTransactionsDto> GetMyDiscountTransactionsAsync(int pageNumber = 1)
{
var response = await _httpClient.GetAsync(
$"/api/userwallet/my-discount-transactions?pageNumber={pageNumber}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<MyDiscountTransactionsDto>();
}
Task 4.2: آپدیت WalletPage.razor
اضافه کردن Card برای DiscountBalance:
@page "/wallet"
@inject UserWalletService WalletService
@inject ISnackbar Snackbar
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
<MudText Typo="Typo.h4" Class="mb-4">کیف پول من</MudText>
@if (_loading)
{
<MudProgressLinear Indeterminate="true" />
}
else if (_wallet != null)
{
<MudGrid>
<!-- موجودی اصلی -->
<MudItem xs="12" md="4">
<MudCard Style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
<MudCardContent>
<MudText Typo="Typo.h6">موجودی اصلی</MudText>
<MudText Typo="Typo.h4" Class="mt-2">
<strong>@_wallet.MainBalanceFormatted</strong>
</MudText>
</MudCardContent>
</MudCard>
</MudItem>
<!-- موجودی پاداش -->
<MudItem xs="12" md="4">
<MudCard Style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: white;">
<MudCardContent>
<MudText Typo="Typo.h6">موجودی پاداش</MudText>
<MudText Typo="Typo.h4" Class="mt-2">
<strong>@_wallet.RewardBalanceFormatted</strong>
</MudText>
</MudCardContent>
</MudCard>
</MudItem>
<!-- موجودی تخفیف (جدید) -->
<MudItem xs="12" md="4">
<MudCard Style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); color: white;">
<MudCardContent>
<MudText Typo="Typo.h6">موجودی تخفیف</MudText>
<MudText Typo="Typo.h4" Class="mt-2">
<strong>@_wallet.DiscountBalanceFormatted</strong>
</MudText>
@if (_wallet.HasDiscount)
{
<MudChip Size="Size.Small" Color="Color.Success" Class="mt-2">
@_wallet.DiscountPercentage تخفیف
</MudChip>
}
</MudCardContent>
</MudCard>
</MudItem>
<!-- مجموع کل -->
<MudItem xs="12">
<MudCard Elevation="5">
<MudCardContent>
<MudText Typo="Typo.h5" Align="Align.Center">
مجموع کل: <strong style="color: green;">@_wallet.TotalBalanceFormatted</strong>
</MudText>
</MudCardContent>
</MudCard>
</MudItem>
</MudGrid>
<!-- Tabs برای تراکنشها -->
<MudTabs Class="mt-6" ApplyEffectsToContainer="true" PanelClass="pa-4">
<MudTabPanel Text="تراکنشهای اصلی" Icon="@Icons.Material.Filled.Receipt">
<!-- کد موجود برای Main Transactions -->
</MudTabPanel>
<MudTabPanel Text="تراکنشهای پاداش" Icon="@Icons.Material.Filled.CardGiftcard">
<!-- کد موجود برای Reward Transactions -->
</MudTabPanel>
<!-- Tab جدید برای DiscountTransactions -->
<MudTabPanel Text="تراکنشهای تخفیف" Icon="@Icons.Material.Filled.Discount">
@if (_loadingDiscountTxs)
{
<MudProgressLinear Indeterminate="true" />
}
else if (_discountTransactions != null)
{
<MudTable Items="@_discountTransactions.Transactions" Hover="true">
<HeaderContent>
<MudTh>نوع</MudTh>
<MudTh>مبلغ</MudTh>
<MudTh>شرح</MudTh>
<MudTh>تاریخ</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<MudIcon Icon="@context.TypeIcon"
Color="@(context.TypeColor == "success" ? Color.Success : Color.Error)" />
@context.Type
</MudTd>
<MudTd>
<strong style="color: @(context.Amount > 0 ? "green" : "red")">
@context.AmountFormatted
</strong>
</MudTd>
<MudTd>@context.Description</MudTd>
<MudTd>@context.DatePersian</MudTd>
</RowTemplate>
</MudTable>
}
</MudTabPanel>
</MudTabs>
}
</MudContainer>
@code {
private MyWalletDto? _wallet;
private MyDiscountTransactionsDto? _discountTransactions;
private bool _loading = true;
private bool _loadingDiscountTxs = true;
protected override async Task OnInitializedAsync()
{
await LoadWallet();
await LoadDiscountTransactions();
}
private async Task LoadWallet()
{
try
{
_loading = true;
_wallet = await WalletService.GetMyWalletAsync();
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
}
}
private async Task LoadDiscountTransactions()
{
try
{
_loadingDiscountTxs = true;
_discountTransactions = await WalletService.GetMyDiscountTransactionsAsync();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری تراکنشهای تخفیف: {ex.Message}", Severity.Error);
}
finally
{
_loadingDiscountTxs = false;
}
}
}
✅ Checkpoint نهایی
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 ساعت
💡 نکات مهم
- DiscountBalance vs RewardBalance: تخفیف فقط در خرید استفاده میشود، پاداش قابل برداشت است
- Gradient Colors: از Linear Gradient برای Cards استفاده شد - زیباتر است
- Tabs Performance: از
MudTabsاستفاده کن - بهتر از Separate Pages - Amount Sign: در DiscountTransactions مبلغهای منفی را با رنگ قرمز نشان بده
- 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 موجود
cd /home/masoud/Apps/project/FourSat/CMS/src/
ls CMSMicroservice.Application/ShoppingCartCQ/Commands/
# باید ببینی:
# - AddToCart/ ✅
# - UpdateCartItemQuantity/ ✅
# - DeleteCartItem/ (چک کن وجود دارد؟)
# - ClearCart/ (چک کن وجود دارد؟)
# - MergeGuestCart/ (چک کن وجود دارد؟)
# - ApplyDiscountCode/ (چک کن وجود دارد؟)
Task 1.2: بررسی DeleteCartItem در CMS
# اگر وجود داشت:
cat CMSMicroservice.Application/ShoppingCartCQ/Commands/DeleteCartItem/DeleteCartItemCommandHandler.cs
# Input:
# - UserId
# - CartItemId
# Logic:
# - پیدا کردن CartItem
# - حذف از دیتابیس
# - بهروزرسانی TotalPrice سبد
Task 1.3: بررسی ClearCart در CMS
# اگر وجود داشت:
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
mkdir -p FrontOffice.BFF.Application/ShoppingCartCQ/Commands/DeleteMyCartItem/
nano DeleteMyCartItemCommand.cs
using MediatR;
namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.DeleteMyCartItem;
/// <summary>
/// حذف یک آیتم از سبد خرید من
/// </summary>
public record DeleteMyCartItemCommand : IRequest<DeleteMyCartItemResponseDto>
{
public long CartItemId { get; init; }
}
فایل 2: DeleteMyCartItemResponseDto.cs
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
using MediatR;
using FrontOffice.BFF.Application.Common.Interfaces;
namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.DeleteMyCartItem;
public class DeleteMyCartItemCommandHandler
: IRequestHandler<DeleteMyCartItemCommand, DeleteMyCartItemResponseDto>
{
private readonly ICurrentUserService _currentUser;
// TODO: private readonly ShoppingCartServiceClient _cmsClient;
public DeleteMyCartItemCommandHandler(ICurrentUserService currentUser)
{
_currentUser = currentUser;
}
public async Task<DeleteMyCartItemResponseDto> Handle(
DeleteMyCartItemCommand request,
CancellationToken cancellationToken)
{
var userId = _currentUser.UserId;
// TODO: Call CMS
// var result = await _cmsClient.DeleteCartItemAsync(new {
// UserId = userId,
// CartItemId = request.CartItemId
// });
// Mock Response
return new DeleteMyCartItemResponseDto
{
Success = true,
Message = "محصول از سبد خرید حذف شد",
NewTotalPrice = 4500000,
NewTotalPriceFormatted = "4,500,000 تومان",
RemainingItemsCount = 2
};
}
}
فایل 4: DeleteMyCartItemCommandValidator.cs
using FluentValidation;
namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.DeleteMyCartItem;
public class DeleteMyCartItemCommandValidator : AbstractValidator<DeleteMyCartItemCommand>
{
public DeleteMyCartItemCommandValidator()
{
RuleFor(x => x.CartItemId)
.GreaterThan(0).WithMessage("شناسه آیتم نامعتبر است");
}
}
Task 2.2: Command - ClearMyCart
فایل 1: ClearMyCartCommand.cs
mkdir -p FrontOffice.BFF.Application/ShoppingCartCQ/Commands/ClearMyCart/
nano ClearMyCartCommand.cs
using MediatR;
namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.ClearMyCart;
/// <summary>
/// پاک کردن کل سبد خرید من
/// </summary>
public record ClearMyCartCommand : IRequest<ClearMyCartResponseDto>
{
// هیچ ورودی ندارد - UserId از Token میآید
}
فایل 2: ClearMyCartResponseDto.cs
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
using MediatR;
using FrontOffice.BFF.Application.Common.Interfaces;
namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.ClearMyCart;
public class ClearMyCartCommandHandler
: IRequestHandler<ClearMyCartCommand, ClearMyCartResponseDto>
{
private readonly ICurrentUserService _currentUser;
public ClearMyCartCommandHandler(ICurrentUserService currentUser)
{
_currentUser = currentUser;
}
public async Task<ClearMyCartResponseDto> Handle(
ClearMyCartCommand request,
CancellationToken cancellationToken)
{
var userId = _currentUser.UserId;
// TODO: Call CMS
// var result = await _cmsClient.ClearCartAsync(new { UserId = userId });
return new ClearMyCartResponseDto
{
Success = true,
Message = "سبد خرید شما با موفقیت خالی شد",
DeletedItemsCount = 3
};
}
}
Task 2.3: Command - MergeGuestCart (اختیاری - پیچیدهتر)
فایل 1: MergeGuestCartCommand.cs
mkdir -p FrontOffice.BFF.Application/ShoppingCartCQ/Commands/MergeGuestCart/
nano MergeGuestCartCommand.cs
using MediatR;
namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.MergeGuestCart;
/// <summary>
/// ادغام سبد مهمان با سبد کاربر لاگین شده
/// زمانی استفاده میشود که کاربر بدون لاگین خرید میکند و بعد لاگین میکند
/// </summary>
public record MergeGuestCartCommand : IRequest<MergeGuestCartResponseDto>
{
public string GuestCartId { get; init; } // GUID سبد مهمان (از LocalStorage)
}
فایل 2: MergeGuestCartResponseDto.cs
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
using MediatR;
using FrontOffice.BFF.Application.Common.Interfaces;
namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.MergeGuestCart;
public class MergeGuestCartCommandHandler
: IRequestHandler<MergeGuestCartCommand, MergeGuestCartResponseDto>
{
private readonly ICurrentUserService _currentUser;
public MergeGuestCartCommandHandler(ICurrentUserService currentUser)
{
_currentUser = currentUser;
}
public async Task<MergeGuestCartResponseDto> Handle(
MergeGuestCartCommand request,
CancellationToken cancellationToken)
{
var userId = _currentUser.UserId;
// TODO: Call CMS
// Logic:
// 1. دریافت سبد مهمان از GuestCartId
// 2. دریافت سبد کاربر فعلی
// 3. ادغام آیتمها (اگر محصول تکراری بود، Quantity جمع شود)
// 4. حذف سبد مهمان
return new MergeGuestCartResponseDto
{
Success = true,
Message = "سبد خرید شما با موفقیت ادغام شد",
MergedItemsCount = 2,
NewTotalPrice = 6500000,
NewTotalPriceFormatted = "6,500,000 تومان"
};
}
}
فایل 4: MergeGuestCartCommandValidator.cs
using FluentValidation;
namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.MergeGuestCart;
public class MergeGuestCartCommandValidator : AbstractValidator<MergeGuestCartCommand>
{
public MergeGuestCartCommandValidator()
{
RuleFor(x => x.GuestCartId)
.NotEmpty().WithMessage("شناسه سبد مهمان الزامی است")
.Must(BeValidGuid).WithMessage("شناسه سبد نامعتبر است");
}
private bool BeValidGuid(string guestCartId)
{
return Guid.TryParse(guestCartId, out _);
}
}
📝 STEP 3: آپدیت Controller
فایل موجود: ShoppingCartController.cs
اضافه کردن 3 endpoint جدید:
using FrontOffice.BFF.Application.ShoppingCartCQ.Commands.DeleteMyCartItem;
using FrontOffice.BFF.Application.ShoppingCartCQ.Commands.ClearMyCart;
using FrontOffice.BFF.Application.ShoppingCartCQ.Commands.MergeGuestCart;
/// <summary>
/// حذف یک آیتم از سبد خرید
/// </summary>
[HttpDelete("items/{cartItemId}")]
[ProducesResponseType(typeof(DeleteMyCartItemResponseDto), 200)]
public async Task<IActionResult> DeleteCartItem(long cartItemId)
{
var command = new DeleteMyCartItemCommand { CartItemId = cartItemId };
var result = await _mediator.Send(command);
return Ok(result);
}
/// <summary>
/// پاک کردن کل سبد خرید
/// </summary>
[HttpDelete("clear")]
[ProducesResponseType(typeof(ClearMyCartResponseDto), 200)]
public async Task<IActionResult> ClearCart()
{
var command = new ClearMyCartCommand();
var result = await _mediator.Send(command);
return Ok(result);
}
/// <summary>
/// ادغام سبد مهمان
/// </summary>
[HttpPost("merge-guest")]
[ProducesResponseType(typeof(MergeGuestCartResponseDto), 200)]
public async Task<IActionResult> MergeGuestCart([FromBody] MergeGuestCartCommand command)
{
var result = await _mediator.Send(command);
return Ok(result);
}
Test Endpoints:
# 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
// فایل موجود: FrontOffice.Main/Services/ShoppingCartService.cs
public async Task<DeleteCartItemResultDto> DeleteCartItemAsync(long cartItemId)
{
var response = await _httpClient.DeleteAsync($"/api/shoppingcart/items/{cartItemId}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<DeleteCartItemResultDto>();
}
public async Task<ClearCartResultDto> ClearCartAsync()
{
var response = await _httpClient.DeleteAsync("/api/shoppingcart/clear");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ClearCartResultDto>();
}
public async Task<MergeGuestCartResultDto> MergeGuestCartAsync(string guestCartId)
{
var request = new { GuestCartId = guestCartId };
var response = await _httpClient.PostAsJsonAsync("/api/shoppingcart/merge-guest", request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<MergeGuestCartResultDto>();
}
Task 4.2: آپدیت CartPage.razor
اضافه کردن دکمههای حذف:
@page "/cart"
@inject ShoppingCartService CartService
@inject ISnackbar Snackbar
@inject IDialogService DialogService
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
<MudGrid>
<MudItem xs="12">
<MudText Typo="Typo.h4" Class="mb-4">سبد خرید من</MudText>
</MudItem>
@if (_loading)
{
<MudProgressLinear Indeterminate="true" />
}
else if (_cart != null && _cart.Items.Any())
{
<MudItem xs="12" md="8">
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">
محصولات (@_cart.TotalItemsCount مورد)
</MudText>
</CardHeaderContent>
<CardHeaderActions>
<MudButton StartIcon="@Icons.Material.Filled.DeleteSweep"
Color="Color.Error"
Variant="Variant.Text"
OnClick="ClearCartWithConfirm">
پاک کردن سبد
</MudButton>
</CardHeaderActions>
</MudCardHeader>
<MudCardContent>
@foreach (var item in _cart.Items)
{
<MudPaper Class="pa-3 mb-3" Elevation="2">
<MudGrid>
<MudItem xs="12" md="6">
<MudText Typo="Typo.h6">@item.ProductName</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">
@item.ProductDescription
</MudText>
</MudItem>
<MudItem xs="6" md="2">
<MudNumericField @bind-Value="item.Quantity"
Label="تعداد"
Min="1"
Max="10"
Variant="Variant.Outlined"
OnChange="() => UpdateQuantity(item.CartItemId, item.Quantity)" />
</MudItem>
<MudItem xs="6" md="2">
<MudText Typo="Typo.body1">
<strong>@item.TotalPriceFormatted</strong>
</MudText>
</MudItem>
<MudItem xs="12" md="2">
<MudButton StartIcon="@Icons.Material.Filled.Delete"
Color="Color.Error"
Variant="Variant.Text"
FullWidth="true"
OnClick="() => DeleteItem(item.CartItemId)">
حذف
</MudButton>
</MudItem>
</MudGrid>
</MudPaper>
}
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" md="4">
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">خلاصه سبد خرید</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudStack Spacing="2">
<MudText>
<strong>تعداد کل:</strong> @_cart.TotalItemsCount مورد
</MudText>
<MudText>
<strong>قیمت کل:</strong>
<span style="color: green; font-size: 1.2em;">
@_cart.TotalPriceFormatted
</span>
</MudText>
<MudDivider />
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
FullWidth="true"
StartIcon="@Icons.Material.Filled.ShoppingCart">
تکمیل خرید
</MudButton>
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
}
else
{
<MudItem xs="12">
<MudAlert Severity="Severity.Info">
سبد خرید شما خالی است
</MudAlert>
</MudItem>
}
</MudGrid>
</MudContainer>
@code {
private MyCartDto? _cart;
private bool _loading = true;
protected override async Task OnInitializedAsync()
{
await LoadCart();
}
private async Task LoadCart()
{
try
{
_loading = true;
_cart = await CartService.GetMyCartAsync();
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
}
}
private async Task UpdateQuantity(long cartItemId, int newQuantity)
{
try
{
await CartService.UpdateCartItemQuantityAsync(cartItemId, newQuantity);
Snackbar.Add("تعداد بهروزرسانی شد", Severity.Success);
await LoadCart();
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
}
private async Task DeleteItem(long cartItemId)
{
bool? confirm = await DialogService.ShowMessageBox(
"تایید حذف",
"آیا از حذف این محصول اطمینان دارید؟",
yesText: "بله", cancelText: "خیر");
if (confirm == true)
{
try
{
var result = await CartService.DeleteCartItemAsync(cartItemId);
Snackbar.Add(result.Message, Severity.Success);
await LoadCart();
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
}
}
private async Task ClearCartWithConfirm()
{
bool? confirm = await DialogService.ShowMessageBox(
"پاک کردن سبد",
"آیا از پاک کردن کل سبد خرید اطمینان دارید؟",
yesText: "بله، پاک کن", cancelText: "خیر");
if (confirm == true)
{
try
{
var result = await CartService.ClearCartAsync();
Snackbar.Add(result.Message, Severity.Success);
await LoadCart();
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
}
}
}
✅ Checkpoint نهایی
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 ساعت
💡 نکات بسیار مهم
- Confirmation Dialog: همیشه قبل از حذف از کاربر تایید بگیر (UX بهتر)
- DeleteCartItem vs ClearCart: Delete یک آیتم حذف میکند، Clear همه را پاک میکند
- MergeGuestCart: این قابلیت برای زمانی است که کاربر بدون لاگین خرید کرده و بعد لاگین کند
- LocalStorage: سبد مهمان در LocalStorage ذخیره شود (GuestCartId = Guid)
- Quantity Update: بلافاصله بعد از تغییر Quantity، Cart را reload کن
- HTTP Methods: Delete →
DeleteAsync, Clear →DeleteAsync, Merge →PostAsync - Icon Usage: از
DeleteSweepبرای Clear وDeleteبرای DeleteItem استفاده کن
💳 مرحله 7: DayaLoan UI + Contract Completion
📊 خلاصه این مرحله
دو کار اصلی:
- DayaLoan UI: نمایش وضعیت وام دایا برای مشتری (Backend در CMS آماده است)
- Contract Completion: تکمیل Queries غایب در Contract Module
بخش اول: DayaLoan - نمایش وضعیت وام
📝 STEP 1: بررسی DayaLoan در CMS
Task 1.1: بررسی موجودیها
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: ساخت فولدرها
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
using MediatR;
namespace FrontOffice.BFF.Application.DayaLoanCQ.Queries.GetMyDayaLoanStatus;
/// <summary>
/// دریافت وضعیت وام دایا برای کاربر جاری
/// </summary>
public record GetMyDayaLoanStatusQuery : IRequest<MyDayaLoanStatusResponseDto>
{
}
فایل 2: MyDayaLoanStatusResponseDto.cs
namespace FrontOffice.BFF.Application.DayaLoanCQ.Queries.GetMyDayaLoanStatus;
public class MyDayaLoanStatusResponseDto
{
public bool HasActiveLoan { get; set; } // آیا وام فعال دارد؟
public string Status { get; set; } // "در انتظار دریافت" / "تایید شده" / "رد شده"
public string StatusColor { get; set; } // "warning" / "success" / "error"
public string StatusIcon { get; set; } // Icon name
public string NationalCode { get; set; }
public decimal RequestAmount { get; set; } // 56,000,000
public string RequestAmountFormatted { get; set; }
public string ContractNumber { get; set; } // شماره قرارداد (اگر تایید شده)
public string LastCheckDatePersian { get; set; } // آخرین استعلام
public string ApprovalDatePersian { get; set; } // تاریخ تایید (اگر تایید شده)
public bool CanRequestCheck { get; set; } // آیا میتواند درخواست استعلام دهد؟
public string NextCheckAvailable { get; set; } // "امکان استعلام بعد از 1 ساعت"
// برای نمایش جزئیات شارژ کیف پول
public List<WalletChargeDto> WalletCharges { get; set; }
}
public class WalletChargeDto
{
public string WalletType { get; set; } // "Main" / "Reward" / "Discount"
public string WalletTypePersian { get; set; } // "کیف پول اصلی"
public decimal Amount { get; set; } // 56,000,000
public string AmountFormatted { get; set; }
public bool IsCharged { get; set; } // آیا شارژ شده؟
public string ChargedDatePersian { get; set; }
}
فایل 3: GetMyDayaLoanStatusQueryHandler.cs
using MediatR;
using FrontOffice.BFF.Application.Common.Interfaces;
namespace FrontOffice.BFF.Application.DayaLoanCQ.Queries.GetMyDayaLoanStatus;
public class GetMyDayaLoanStatusQueryHandler
: IRequestHandler<GetMyDayaLoanStatusQuery, MyDayaLoanStatusResponseDto>
{
private readonly ICurrentUserService _currentUser;
// TODO: private readonly DayaLoanServiceClient _cmsClient;
public GetMyDayaLoanStatusQueryHandler(ICurrentUserService currentUser)
{
_currentUser = currentUser;
}
public async Task<MyDayaLoanStatusResponseDto> Handle(
GetMyDayaLoanStatusQuery request,
CancellationToken cancellationToken)
{
var userId = _currentUser.UserId;
// TODO: Call CMS
// var result = await _cmsClient.GetDayaLoanStatusAsync(new { UserId = userId });
// Mock Data - وضعیت "تایید شده"
return new MyDayaLoanStatusResponseDto
{
HasActiveLoan = true,
Status = "تایید شده",
StatusColor = "success",
StatusIcon = "check_circle",
NationalCode = "1234567890",
RequestAmount = 56000000,
RequestAmountFormatted = "56,000,000 تومان",
ContractNumber = "DL-1403-001234",
LastCheckDatePersian = "25 آذر 1403",
ApprovalDatePersian = "25 آذر 1403",
CanRequestCheck = false,
NextCheckAvailable = "وام شما قبلاً تایید شده است",
WalletCharges = new List<WalletChargeDto>
{
new() {
WalletType = "Main",
WalletTypePersian = "کیف پول اصلی",
Amount = 56000000,
AmountFormatted = "56,000,000 تومان",
IsCharged = true,
ChargedDatePersian = "25 آذر 1403"
},
new() {
WalletType = "Reward",
WalletTypePersian = "کیف پول پاداش",
Amount = 56000000,
AmountFormatted = "56,000,000 تومان",
IsCharged = true,
ChargedDatePersian = "25 آذر 1403"
},
new() {
WalletType = "Discount",
WalletTypePersian = "کیف پول تخفیف",
Amount = 56000000,
AmountFormatted = "56,000,000 تومان",
IsCharged = true,
ChargedDatePersian = "25 آذر 1403"
}
}
};
}
}
Task 2.3: Command - RequestDayaLoanCheck (اختیاری)
فایل 1: RequestDayaLoanCheckCommand.cs
using MediatR;
namespace FrontOffice.BFF.Application.DayaLoanCQ.Commands.RequestDayaLoanCheck;
/// <summary>
/// درخواست استعلام فوری وضعیت وام دایا
/// معمولاً Worker این کار را انجام میدهد، اما کاربر میتواند استعلام فوری بزند
/// </summary>
public record RequestDayaLoanCheckCommand : IRequest<RequestDayaLoanCheckResponseDto>
{
}
فایل 2: RequestDayaLoanCheckResponseDto.cs
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
using MediatR;
using FrontOffice.BFF.Application.Common.Interfaces;
namespace FrontOffice.BFF.Application.DayaLoanCQ.Commands.RequestDayaLoanCheck;
public class RequestDayaLoanCheckCommandHandler
: IRequestHandler<RequestDayaLoanCheckCommand, RequestDayaLoanCheckResponseDto>
{
private readonly ICurrentUserService _currentUser;
public RequestDayaLoanCheckCommandHandler(ICurrentUserService currentUser)
{
_currentUser = currentUser;
}
public async Task<RequestDayaLoanCheckResponseDto> Handle(
RequestDayaLoanCheckCommand request,
CancellationToken cancellationToken)
{
var userId = _currentUser.UserId;
// TODO: Call CMS CheckDayaLoanStatus Command
return new RequestDayaLoanCheckResponseDto
{
Success = true,
Message = "استعلام وضعیت وام با موفقیت انجام شد. نتیجه در صفحه نمایش داده میشود.",
NewStatus = "در انتظار دریافت"
};
}
}
📝 STEP 3: Controller - DayaLoanController
فایل جدید: DayaLoanController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MediatR;
using FrontOffice.BFF.Application.DayaLoanCQ.Queries.GetMyDayaLoanStatus;
using FrontOffice.BFF.Application.DayaLoanCQ.Commands.RequestDayaLoanCheck;
namespace FrontOffice.BFF.WebApi.Controllers;
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class DayaLoanController : ControllerBase
{
private readonly IMediator _mediator;
public DayaLoanController(IMediator mediator)
{
_mediator = mediator;
}
/// <summary>
/// دریافت وضعیت وام دایا من
/// </summary>
[HttpGet("my-status")]
[ProducesResponseType(typeof(MyDayaLoanStatusResponseDto), 200)]
public async Task<IActionResult> GetMyStatus()
{
var query = new GetMyDayaLoanStatusQuery();
var result = await _mediator.Send(query);
return Ok(result);
}
/// <summary>
/// درخواست استعلام فوری
/// </summary>
[HttpPost("request-check")]
[ProducesResponseType(typeof(RequestDayaLoanCheckResponseDto), 200)]
public async Task<IActionResult> RequestCheck()
{
var command = new RequestDayaLoanCheckCommand();
var result = await _mediator.Send(command);
return Ok(result);
}
}
📝 STEP 4: UI - DayaLoanPage
Task 4.1: Service
// فایل جدید: FrontOffice.Main/Services/DayaLoanService.cs
using System.Net.Http.Json;
using FrontOffice.Main.Models;
namespace FrontOffice.Main.Services;
public class DayaLoanService
{
private readonly HttpClient _httpClient;
public DayaLoanService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<MyDayaLoanStatusDto> GetMyStatusAsync()
{
var response = await _httpClient.GetAsync("/api/dayaloan/my-status");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<MyDayaLoanStatusDto>();
}
public async Task<RequestCheckResultDto> RequestCheckAsync()
{
var response = await _httpClient.PostAsync("/api/dayaloan/request-check", null);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<RequestCheckResultDto>();
}
}
ثبت در Program.cs:
builder.Services.AddScoped<DayaLoanService>();
Task 4.2: Page - DayaLoanStatusPage.razor
mkdir -p FrontOffice/src/FrontOffice.Main/Pages/Loan/
nano DayaLoanStatusPage.razor
@page "/loan/daya-status"
@inject DayaLoanService LoanService
@inject ISnackbar Snackbar
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
<MudText Typo="Typo.h4" Class="mb-4">وضعیت وام دایا</MudText>
@if (_loading)
{
<MudProgressLinear Indeterminate="true" />
}
else if (_status != null)
{
<MudGrid>
<!-- کارت وضعیت اصلی -->
<MudItem xs="12" md="6">
<MudCard Elevation="5">
<MudCardHeader Style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
<CardHeaderContent>
<MudText Typo="Typo.h5">وضعیت درخواست</MudText>
</CardHeaderContent>
<CardHeaderActions>
<MudIcon Icon="@_status.StatusIcon" Size="Size.Large" />
</CardHeaderActions>
</MudCardHeader>
<MudCardContent>
<MudStack Spacing="3">
<MudAlert Severity="@GetSeverity(_status.StatusColor)" Dense="true">
<strong>@_status.Status</strong>
</MudAlert>
<MudText>
<strong>کد ملی:</strong> @_status.NationalCode
</MudText>
<MudText>
<strong>مبلغ درخواستی (هر کیف پول):</strong>
<span style="color: green; font-size: 1.1em;">
@_status.RequestAmountFormatted
</span>
</MudText>
@if (!string.IsNullOrEmpty(_status.ContractNumber))
{
<MudText>
<strong>شماره قرارداد:</strong>
<MudChip Size="Size.Small" Color="Color.Info">
@_status.ContractNumber
</MudChip>
</MudText>
}
<MudText>
<strong>آخرین استعلام:</strong> @_status.LastCheckDatePersian
</MudText>
@if (!string.IsNullOrEmpty(_status.ApprovalDatePersian))
{
<MudText>
<strong>تاریخ تایید:</strong> @_status.ApprovalDatePersian
</MudText>
}
</MudStack>
</MudCardContent>
@if (_status.CanRequestCheck)
{
<MudCardActions>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Refresh"
Disabled="_checking"
OnClick="RequestCheck">
@if (_checking)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" />
<span class="ms-2">در حال استعلام...</span>
}
else
{
<span>استعلام فوری</span>
}
</MudButton>
</MudCardActions>
}
else
{
<MudCardActions>
<MudText Typo="Typo.caption" Color="Color.Secondary">
@_status.NextCheckAvailable
</MudText>
</MudCardActions>
}
</MudCard>
</MudItem>
<!-- کارت جزئیات شارژ کیف پولها -->
<MudItem xs="12" md="6">
<MudCard Elevation="5">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">جزئیات شارژ کیف پولها</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
@if (_status.WalletCharges != null && _status.WalletCharges.Any())
{
<MudStack Spacing="3">
@foreach (var wallet in _status.WalletCharges)
{
<MudPaper Class="pa-3" Elevation="2">
<MudGrid>
<MudItem xs="8">
<MudText Typo="Typo.body1">
<strong>@wallet.WalletTypePersian</strong>
</MudText>
<MudText Typo="Typo.body2" Color="Color.Success">
@wallet.AmountFormatted
</MudText>
</MudItem>
<MudItem xs="4" Class="d-flex align-center justify-end">
@if (wallet.IsCharged)
{
<MudIcon Icon="@Icons.Material.Filled.CheckCircle"
Color="Color.Success"
Size="Size.Large" />
}
else
{
<MudIcon Icon="@Icons.Material.Filled.HourglassEmpty"
Color="Color.Warning"
Size="Size.Large" />
}
</MudItem>
</MudGrid>
@if (wallet.IsCharged)
{
<MudText Typo="Typo.caption" Color="Color.Secondary">
شارژ شده در: @wallet.ChargedDatePersian
</MudText>
}
</MudPaper>
}
<MudDivider />
<MudText Typo="Typo.h6" Align="Align.Center">
مجموع کل شارژ:
<strong style="color: green;">
@((56000000m * 3).ToString("N0")) تومان
</strong>
</MudText>
</MudStack>
}
else
{
<MudAlert Severity="Severity.Info">
هنوز کیف پولی شارژ نشده است
</MudAlert>
}
</MudCardContent>
</MudCard>
</MudItem>
<!-- راهنما -->
<MudItem xs="12">
<MudCard>
<MudCardContent>
<MudText Typo="Typo.h6" Class="mb-2">
<MudIcon Icon="@Icons.Material.Filled.Info" /> راهنما
</MudText>
<MudList Dense="true">
<MudListItem Icon="@Icons.Material.Filled.Circle" IconSize="Size.Small">
وام دایا برای هر کیف پول (اصلی، پاداش، تخفیف) به مبلغ 56 میلیون تومان است
</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.Circle" IconSize="Size.Small">
سیستم هر 15 دقیقه یکبار وضعیت وام شما را بررسی میکند
</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.Circle" IconSize="Size.Small">
در صورت تایید، کیف پولهای شما به صورت خودکار شارژ خواهند شد
</MudListItem>
</MudList>
</MudCardContent>
</MudCard>
</MudItem>
</MudGrid>
}
</MudContainer>
@code {
private MyDayaLoanStatusDto? _status;
private bool _loading = true;
private bool _checking = false;
protected override async Task OnInitializedAsync()
{
await LoadStatus();
}
private async Task LoadStatus()
{
try
{
_loading = true;
_status = await LoanService.GetMyStatusAsync();
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
}
}
private async Task RequestCheck()
{
try
{
_checking = true;
var result = await LoanService.RequestCheckAsync();
Snackbar.Add(result.Message, Severity.Success);
// Reload status after 2 seconds
await Task.Delay(2000);
await LoadStatus();
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
finally
{
_checking = false;
}
}
private Severity GetSeverity(string color)
{
return color switch
{
"success" => Severity.Success,
"warning" => Severity.Warning,
"error" => Severity.Error,
"info" => Severity.Info,
_ => Severity.Normal
};
}
}
Task 4.3: اضافه کردن به NavMenu
<MudNavLink Href="/loan/daya-status" Icon="@Icons.Material.Filled.AccountBalance">
وام دایا
</MudNavLink>
بخش دوم: Contract Completion
📝 STEP 5: تکمیل Contract Module
وضعیت فعلی (80%):
- ✅ CreateContract Command
- ✅ UpdateContract Command
- ❌ GetContract Query (غایب)
- ❌ GetAllMyContracts Query (غایب)
Task 5.1: Query - GetMyContract
فایل 1: GetMyContractQuery.cs
mkdir -p FrontOffice.BFF.Application/ContractCQ/Queries/GetMyContract/
nano GetMyContractQuery.cs
using MediatR;
namespace FrontOffice.BFF.Application.ContractCQ.Queries.GetMyContract;
public record GetMyContractQuery : IRequest<MyContractResponseDto>
{
public long ContractId { get; init; }
}
فایل 2: MyContractResponseDto.cs
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
using MediatR;
using FrontOffice.BFF.Application.Common.Interfaces;
namespace FrontOffice.BFF.Application.ContractCQ.Queries.GetMyContract;
public class GetMyContractQueryHandler
: IRequestHandler<GetMyContractQuery, MyContractResponseDto>
{
private readonly ICurrentUserService _currentUser;
public GetMyContractQueryHandler(ICurrentUserService currentUser)
{
_currentUser = currentUser;
}
public async Task<MyContractResponseDto> Handle(
GetMyContractQuery request,
CancellationToken cancellationToken)
{
var userId = _currentUser.UserId;
// TODO: Call CMS
return new MyContractResponseDto
{
Id = request.ContractId,
ContractNumber = "CNT-1403-001234",
Type = "خرید محصول",
Status = "فعال",
StatusColor = "success",
TotalAmount = 5000000,
TotalAmountFormatted = "5,000,000 تومان",
StartDatePersian = "1 آذر 1403",
EndDatePersian = "1 آذر 1404",
Description = "قرارداد خرید بسته طلایی",
Terms = "شرایط و قوانین قرارداد..."
};
}
}
Task 5.2: Query - GetMyContracts
فایل 1: GetMyContractsQuery.cs
using MediatR;
namespace FrontOffice.BFF.Application.ContractCQ.Queries.GetMyContracts;
public record GetMyContractsQuery : IRequest<MyContractsResponseDto>
{
public int PageNumber { get; init; } = 1;
public int PageSize { get; init; } = 10;
}
فایل 2: MyContractsResponseDto.cs
namespace FrontOffice.BFF.Application.ContractCQ.Queries.GetMyContracts;
public class MyContractsResponseDto
{
public List<ContractItemDto> Contracts { get; set; }
public int TotalCount { get; set; }
}
public class ContractItemDto
{
public long Id { get; set; }
public string ContractNumber { get; set; }
public string Type { get; set; }
public string Status { get; set; }
public string StatusColor { get; set; }
public string TotalAmountFormatted { get; set; }
public string StartDatePersian { get; set; }
}
Task 5.3: آپدیت ContractController
using FrontOffice.BFF.Application.ContractCQ.Queries.GetMyContract;
using FrontOffice.BFF.Application.ContractCQ.Queries.GetMyContracts;
[HttpGet("{contractId}")]
[ProducesResponseType(typeof(MyContractResponseDto), 200)]
public async Task<IActionResult> GetContract(long contractId)
{
var query = new GetMyContractQuery { ContractId = contractId };
var result = await _mediator.Send(query);
return Ok(result);
}
[HttpGet("my-contracts")]
[ProducesResponseType(typeof(MyContractsResponseDto), 200)]
public async Task<IActionResult> GetMyContracts(
[FromQuery] int pageNumber = 1,
[FromQuery] int pageSize = 10)
{
var query = new GetMyContractsQuery { PageNumber = pageNumber, PageSize = pageSize };
var result = await _mediator.Send(query);
return Ok(result);
}
✅ Checkpoint نهایی
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 ساعت
💡 نکات مهم
- Worker: Worker در CMS هر 15 دقیقه استعلام میکند - کاربر نباید بیش از حد استعلام فوری بزند
- 168M Total: 56M × 3 کیف پول = 168 میلیون تومان کل شارژ
- Status Icons: از Icons.Material.Filled استفاده کن برای نمایش بهتر
- Gradient Background: برای کارت وضعیت از Gradient استفاده شد
- Contract Module: فقط 2 Query اضافه شد تا 100% شود
🔍 مرحله 8: بررسی نهایی - فقط کارهای ناتمام قبلی (بدون فیچرهای جدید)
⚠️ تذکر مهم
این مرحله فقط روی قابلیتهایی تمرکز دارد که:
- ✅ در CMS از قبل موجود است
- ❌ در FrontOffice.BFF یا FrontOffice پیادهسازی نشده
- 🎯 مختص مشتری است (نه Admin)
حذف شده از لیست:
- ❌ DayaLoan (فیچر جدید - هنوز در CMS کامل نیست)
- ❌ Manual Payment (فیچر Admin)
- ❌ ClubMembership Admin Commands (مثل Deactivate, AssignFeature)
📝 STEP 1: بررسی دقیق CMS vs BFF
Task 1.1: مقایسه Commands/Queries موجود
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 فقط
# ماژولهایی که حتماً 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:
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:
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:
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:
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:
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:
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 (حیاتی - باید حتماً باشد):
- Commission + Withdrawal: مشتری باید بتواند پولش را ببیند و برداشت کند
- ClubMembership: مشتری باید بتواند عضو باشگاه شود
- NetworkMembership: مشتری باید درخت شبکه خود را ببیند
اولویت 2 (مهم):
- UserWallet Completion: تکمیل برداشت + تخفیف
- ShoppingCart Completion: حذف آیتم + پاک کردن سبد + کد تخفیف
اولویت 3 (نرمال):
- 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- آمار شبکه → آمار تیم