5300 lines
197 KiB
Markdown
5300 lines
197 KiB
Markdown
<div dir="rtl" align="right">
|
|
|
|
# تحلیل امکانات قابل ارائه به مشتری (Codex)
|
|
تحلیل مختصر بر اساس: `CMS/cms-data-and-business.md`, `CMS/network-club-commission-system-v1.1.md`, `CMS/balance-calculation-carryover-logic.md`, `CMS/email-sms-configuration-guide.md`, `REMAINING-TASKS.md`.
|
|
|
|
|
|
---
|
|
|
|
## 🏗️ راهنمای معماری: جریان توسعه از CMS تا FrontOffice
|
|
|
|
### 📐 ساختار کلی پروژه
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ USER (Customer) │
|
|
│ مشتری / کاربر نهایی │
|
|
└──────────────────────────┬──────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ FrontOffice (Blazor WebAssembly) │
|
|
│ فرانت سمت مشتری │
|
|
│ Location: FrontOffice/src/FrontOffice.Main/ │
|
|
│ Technology: Blazor WASM + MudBlazor │
|
|
│ Files: Pages/*.razor, Components/*.razor │
|
|
└──────────────────────────┬──────────────────────────────────┘
|
|
│ HTTP/REST
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ FrontOffice.BFF (Backend For Frontend) │
|
|
│ گیتوی سمت مشتری │
|
|
│ Location: FrontOffice.BFF/src/ │
|
|
│ Technology: ASP.NET Core REST API │
|
|
│ Structure: │
|
|
│ ├── Application/ │
|
|
│ │ ├── [ModuleName]CQ/ │
|
|
│ │ │ ├── Commands/ │
|
|
│ │ │ └── Queries/ │
|
|
│ │ └── DTOs/ │
|
|
│ └── WebApi/ │
|
|
│ └── Controllers/ │
|
|
└──────────────────────────┬──────────────────────────────────┘
|
|
│ gRPC (CMS Protobuf)
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ CMS (Microservice) │
|
|
│ سرویس اصلی / دیتابیس │
|
|
│ Location: CMS/src/CMSMicroservice.*/ │
|
|
│ Technology: ASP.NET Core + gRPC + SQL Server │
|
|
│ Structure (Clean Architecture): │
|
|
│ ├── Domain/ (Entities, Enums, Events) │
|
|
│ ├── Application/ (Commands, Queries, Handlers) │
|
|
│ ├── Infrastructure/ (Database, Services) │
|
|
│ ├── Protobuf/ (gRPC Proto definitions) │
|
|
│ └── WebApi/ (gRPC Services, Hangfire) │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### 🔄 جریان توسعه یک قابلیت (Feature Flow)
|
|
|
|
#### مثال: پیادهسازی "نمایش کمیسیون هفتگی"
|
|
|
|
```
|
|
Step 1: CMS (Already Done ✅)
|
|
├── Domain/Entities/UserCommissionPayout.cs
|
|
├── Application/CommissionCQ/Queries/GetUserCommissionPayouts/
|
|
│ ├── GetUserCommissionPayoutsQuery.cs
|
|
│ ├── GetUserCommissionPayoutsQueryHandler.cs
|
|
│ └── CommissionPayoutDto.cs
|
|
└── Protobuf/Protos/Commission.proto (gRPC definition)
|
|
|
|
Step 2: FrontOffice.BFF (TODO ❌)
|
|
├── Application/CommissionCQ/Queries/GetMyCommissionPayouts/
|
|
│ ├── GetMyCommissionPayoutsQuery.cs
|
|
│ ├── GetMyCommissionPayoutsQueryHandler.cs
|
|
│ │ └── Calls CMS via gRPC: CommissionService.GetUserCommissionPayouts
|
|
│ └── CommissionPayoutResponseDto.cs (Customer-friendly DTO)
|
|
└── WebApi/Controllers/CommissionController.cs
|
|
└── GET /api/commission/my-payouts
|
|
|
|
Step 3: FrontOffice UI (TODO ❌)
|
|
└── Pages/Commission/PayoutsPage.razor
|
|
├── @inject CommissionService _commissionService
|
|
├── await _commissionService.GetMyPayoutsAsync()
|
|
└── Display: MudTable with Columns (Week, Amount, Status, Date)
|
|
```
|
|
|
|
### 🎨 تفاوتهای کلیدی CMS vs BFF
|
|
|
|
| جنبه | CMS (Microservice) | FrontOffice.BFF | FrontOffice UI |
|
|
|------|-------------------|-----------------|----------------|
|
|
| **مخاطب** | Admin + System | Customer فقط | Customer |
|
|
| **داده** | همه کاربران | کاربر جاری (`UserId` از JWT) | کاربر جاری |
|
|
| **Response** | DTO کامل + Metadata | DTO ساده + فقط فیلدهای لازم | UI-friendly JSON |
|
|
| **Authorization** | Role-based (Admin/User) | User-only (No Admin access) | Login required |
|
|
| **مثال Query** | `GetAllCommissionPayouts` | `GetMyCommissionPayouts` | نمایش جدول |
|
|
| **Input** | `UserId` required | `UserId` از Token | هیچ ورودی (خودکار) |
|
|
|
|
### 📁 ساختار استاندارد BFF Module
|
|
|
|
```csharp
|
|
FrontOffice.BFF/src/FrontOffice.BFF.Application/
|
|
└── [ModuleName]CQ/
|
|
├── Commands/
|
|
│ └── [ActionName]/
|
|
│ ├── [ActionName]Command.cs // Input
|
|
│ ├── [ActionName]CommandHandler.cs // Logic
|
|
│ ├── [ActionName]CommandValidator.cs // Validation
|
|
│ └── [ActionName]ResponseDto.cs // Output
|
|
└── Queries/
|
|
└── [QueryName]/
|
|
├── [QueryName]Query.cs
|
|
├── [QueryName]QueryHandler.cs
|
|
└── [QueryName]ResponseDto.cs
|
|
```
|
|
|
|
### 🔐 احراز هویت و دسترسی
|
|
|
|
**JWT Token Structure:**
|
|
```json
|
|
{
|
|
"sub": "123", // UserId
|
|
"email": "user@example.com",
|
|
"phone": "09123456789",
|
|
"IsSignMainContract": "True", // قرارداد امضا شده؟
|
|
"exp": 1234567890
|
|
}
|
|
```
|
|
|
|
**استخراج UserId در Handler:**
|
|
```csharp
|
|
public class GetMyCommissionPayoutsQueryHandler : IRequestHandler<...>
|
|
{
|
|
private readonly ICurrentUserService _currentUser;
|
|
|
|
public async Task<Response> Handle(Query request, CancellationToken ct)
|
|
{
|
|
var userId = _currentUser.UserId; // از JWT
|
|
|
|
// Call CMS with userId
|
|
var result = await _cmsClient.GetUserCommissionPayoutsAsync(userId);
|
|
return result;
|
|
}
|
|
}
|
|
```
|
|
|
|
### 📦 الگوی DTO Mapping
|
|
|
|
**CMS DTO (داده خام):**
|
|
```csharp
|
|
public class CommissionPayoutDto
|
|
{
|
|
public long Id { get; set; }
|
|
public long UserId { get; set; }
|
|
public int WeekNumber { get; set; }
|
|
public long TotalAmount { get; set; }
|
|
public CommissionPayoutStatus Status { get; set; }
|
|
public DateTime CalculatedDate { get; set; }
|
|
// ... 10 فیلد دیگر
|
|
}
|
|
```
|
|
|
|
**BFF Response DTO (مشتریمحور):**
|
|
```csharp
|
|
public class MyCommissionPayoutDto
|
|
{
|
|
public long Id { get; set; }
|
|
public string WeekLabel { get; set; } // "هفته 45 - آذر 1403"
|
|
public string AmountFormatted { get; set; } // "1,250,000 تومان"
|
|
public string StatusText { get; set; } // "پرداخت شده"
|
|
public string StatusBadgeColor { get; set; } // "success" / "warning"
|
|
public string DatePersian { get; set; } // "25 آذر 1403"
|
|
}
|
|
```
|
|
|
|
### 🎯 چکلیست شروع توسعه
|
|
|
|
قبل از شروع کار روی هر ماژول، این موارد را چک کنید:
|
|
|
|
```
|
|
[ ] CMS Commands/Queries مربوطه را شناسایی کردم
|
|
[ ] Proto definitions مربوطه را یافتم (Protobuf/*.proto)
|
|
[ ] نمونه Handler موجود در BFF را بررسی کردم
|
|
[ ] JWT Token و CurrentUserService را فهمیدم
|
|
[ ] ساختار DTO مشتریمحور را طراحی کردم
|
|
[ ] Mock data برای UI آماده کردم (قبل از اتصال به API)
|
|
```
|
|
|
|
## ترمینولوژی
|
|
- «گتوی سمت مشتری» = `FrontOffice.BFF`
|
|
- «فرانت» = پروژه `FrontOffice` (UI مشتری)
|
|
- **الویت کار**: تغییر روی CMS فقط پس از تأیید؛ تمرکز اصلی روی `FrontOffice.BFF` و `FrontOffice`. هر نیازمندی جدید سمت مشتری قبل از دستکاری CMS باید تأیید شود.
|
|
|
|
## امکانات موجود در CMS که باید در FrontOffice دیده شود
|
|
- **عضویت باشگاه و کیفپولهای سهگانه**: جریان پرداخت/فعالسازی (۵۶M) → افزایش همزمان `Balance` و `DiscountBalance` و واریز ۲۵M به استخر؛ نیاز به UI «عضویت در باشگاه»، نمایش موجودی هر سه کیفپول و تراکنشهای مرتبط.
|
|
- **فروشگاه باشگاه با تخفیف**: خرید از فروشگاه ویژه با `DiscountBalance`؛ تفکیک لیست محصولات باشگاه و عمومی + نمایش سقف/درصد تخفیف و موجودی تخفیف در کارت محصول/Checkout.
|
|
- **شبکه باینری و تعادل هفتگی**: نمایش درخت دوبخشی، اعضای جدید هر پا، تعادل هفته، Carryover و سقف هفتگی (`MaxWeeklyBalances`=۳۰۰)؛ UI گزارش هفتگی و نمودار رشد برای شفافسازی محاسبه کمیسیون.
|
|
- **کمیسیون هفتگی و پرداختها**: نمایش مقدار استخر هفته، ارزش هر Balance، امتیازهای کاربر، مبلغ قابل برداشت، تاریخچه `UserCommissionPayout` با وضعیت (Pending/Calculated/Paid/Withdrawn) و امکان انتخاب روش برداشت (IBAN).
|
|
- **ویژگیهای باشگاه (ClubFeature)**: لیست فیچرهای فعال/قابل دریافت، امتیاز موردنیاز و تاریخ فعالسازی (`UserClubFeature`); ارائه بهصورت Badge/Progress Bar در پروفایل.
|
|
- **تجربه خرید استاندارد**: کاتالوگ دسته/تگ، سبد (`UserCarts`)، Checkout، پرداخت ترکیبی (کیفپول + درگاه)، فاکتور (`FactorDetails`)، وضعیت ارسال/کد رهگیری؛ تاریخچه سفارش در پروفایل.
|
|
- **کیفپول و لاگ مالی**: تاریخچه `UserWalletChangeLog` (واریز، خرید، بازپرداخت، برداشت) با فیلتر نوع/بازه زمانی؛ واریز از درگاه، برداشت با صف تأیید دستی؛ نمایش `NetworkBalance` جداگانه.
|
|
- **آدرسها و قراردادها**: مدیریت آدرس پیشفرض برای سفارش؛ اجباریکردن قبول آخرین نسخه قرارداد/Terms و نگهداری PDF امضا شده؛ هدایت اجباری به صفحه امضا در اولین ورود بعد از تغییر نسخه.
|
|
- **اعلانها**: ایمیل/SMS برای فعالسازی باشگاه، پرداخت کمیسیون، خطاها و وضعیت ارسال؛ در پروفایل دکمه Opt-in/Opt-out اعلانها (موبایل/ایمیل) نیاز است.
|
|
|
|
## قابلیتهای جدید/در حال تکمیل که باید در Gateway و UI برنامهریزی شود
|
|
- **سیستم تراکنش درگاه (۰٪)**: جریان Create/Verify/Refund تراکنش؛ در FrontOffice صفحات وضعیت تراکنش، Retry/Verify، نمایش `ReferenceId` و همگامسازی وضعیت سفارش/کیفپول.
|
|
- **سبد خرید پیشرفته (۰٪)**: پشتیبانی Add/Update/Delete/Clear/Merge (مهمان→ورود) روی `UserCarts`; UI ادغام سبد مهمان و کاربر، و بازگردانی سبد در شکست پرداخت.
|
|
- **تکمیل Products & Orders (۷۰٪)**: اعمال Tag/Category فیلترها، نمایش موجودی/تخفیف/گالری، کنترل تغییر قیمت روی اقلام فاکتور، قابلیت لغو سفارش و Refund به کیفپول.
|
|
- **Withdrawal/Settlement (۴۰٪)**: فرم درخواست برداشت از `NetworkBalance`/Balance با IBAN، پیگیری وضعیت صف تأیید، تاریخچه برداشت و سقفهای روزانه.
|
|
- **VAT روی سفارشات (جدید)**: نمایش `VatPercentage` و خط مجزا در فاکتور/Checkout («شامل ۱۰٪ مالیات بر ارزش افزوده»)؛ نگهداری مقدار در سفارش و UI.
|
|
|
|
## پیشنهاد اقدام برای FrontOffice/BFF (ترتیب توصیهشده)
|
|
۱) صفحه «عضویت باشگاه» + داشبورد کیفپول/کمیسیون/فیچرها (یکپارچه با نمودار تعادل هفتگی و تاریخچه پرداخت کمیسیون).
|
|
۲) راهاندازی پرداخت تراکنش و خطایابی: مسیر پرداخت، صفحه نتیجه، Retry/Verify، بازپرداخت به کیفپول.
|
|
۳) تکمیل سبد/Checkout: Merge سبد مهمان، پرداخت ترکیبی، نمایش VAT و تفکیک فروشگاه باشگاه.
|
|
۴) تاریخچه مالی و برداشت: لیست ChangeLog، درخواست/پیگیری برداشت، قوانین سقف/صف تأیید.
|
|
۵) اعلانها و قراردادها: تنظیمات Opt-in اعلان، اجبار امضای نسخه جدید قرارداد پیش از دسترسی به بخشهای مالی.
|
|
|
|
## وضعیت فعلی FrontOffice.BFF (گتوی سمت مشتری)
|
|
- مستندات موجود: فقط `FrontOffice.BFF/README.md` (خالی) و `docs/CMS.sql`/`model.ndm2` (ساختار دیتابیس CMS). هیچ API یا هندلر مستند نشده است.
|
|
- نتیجه: پوشش قابلیتها در BFF نامشخص؛ فرض پیشفرض «پیادهسازی نشده/نیاز به بررسی» برای موارد زیر: عضویت باشگاه، کیفپول سهگانه و لاگ مالی، کمیسیون هفتگی و پرداخت/Withdraw، فروشگاه باشگاه و تخفیف، تراکنش درگاه (Create/Verify/Refund)، Merge سبد مهمان→ورود، VAT در Checkout، اعلانهای Email/SMS/Push.
|
|
- **استثنا (موجود و پیادهسازیشده)**: جریان قرارداد در گتوی سمت مشتری و فرانت پیاده شده است؛ ثبتنام بدون امضای قرارداد متوقف میشود و پس از امضا Claim/Roll مربوط در توکن ست میشود.
|
|
- اقدام فوری: فهرست APIهای فعلی BFF را استخراج و مقابل نیازهای بالا چک کنیم؛ تا زمان تأیید، تغییری در CMS داده نمیشود و تمرکز بر طراحی/افزودن هندلرهای BFF و UI فرانت است.
|
|
|
|
## جدول پیشرفت قابلیتهای مشتری (FrontOffice.BFF ↔ FrontOffice)
|
|
> درصدها براساس شواهد فعلی؛ در صورت کشف پیادهسازی بیشتر، مقدار بهروزرسانی شود.
|
|
|
|
| قابلیت | وضعیت فعلی | درصد پیشرفت | اقدام بعدی (BFF) | اقدام بعدی (FrontOffice) |
|
|
| --- | --- | --- | --- | --- |
|
|
| قرارداد و امضا | پیادهسازی شده (امضا اجباری، Claim در توکن) | ۱۰۰٪ | بررسی صحت Claim در JWT و روتینگ پس از امضا | نمایش وضعیت امضا، ریدایرکت به امضا در اولین ورود بعد از تغییر نسخه |
|
|
| عضویت باشگاه | پیادهسازی نشده | ۰٪ | API شروع عضویت و فعالسازی باشگاه | صفحه عضویت و پرداخت ورود به باشگاه |
|
|
| خلاصه کیفپولها (Balance/Discount/Network) | BFF/فرانت سه موجودی را نمایش میدهند؛ DiscountBalance هنوز از CMS برنمیگردد (در UI پیام «در انتظار اتصال CMS» نشان داده میشود، fallback صفر شد). | ۷۵٪ | **Blocked:** اضافهشدن DiscountBalance به سرویس CMS و مپ در BFF | نمایش مقدار واقعی پس از اتصال |
|
|
| جزئیات تراکنش کیفپول | BFF: `GetAllUserWalletChangeLog` پارامتر ReferenceId/IsIncrease دارد؛ فرانت فیلتر ارجاع/نوع تراکنش دارد. | ۷۵٪ | افزودن فیلتر تاریخ/Channel (در صورت نیاز) | بهبود نمایش برچسب نوع و Channel |
|
|
| فروشگاه باشگاه (خرید با DiscountBalance) | نامشخص/احتمالاً صفر | ۰٪ | API فهرست محصولات باشگاه + اعتبارسنجی موجودی تخفیف | تفکیک کاتالوگ باشگاه/عمومی، نمایش موجودی تخفیف در کارت و Checkout |
|
|
| شبکه باینری، تعادل و کمیسیون هفتگی | نامشخص/احتمالاً صفر | ۰٪ | API گزارش تعادل هفته، استخر، پرداخت کمیسیون و Withdraw | داشبورد شبکه/کمیسیون، نمودار تعادل، درخواست برداشت |
|
|
| تراکنش درگاه (Create/Verify/Refund) | PaymentRequest/PaymentVerification در BFF و Checkout فرانت پیاده شده؛ Refund دیده نشد. | ۶۰٪ | افزودن Refund و همگامسازی وضعیت سفارش/کیفپول | نمایش وضعیت پرداخت و مسیر Retry/Verify در UI |
|
|
| VAT در سفارش | نامشخص/احتمالاً صفر | ۰٪ | افزودن فیلد VAT به DTO/پاسخ سفارش | نمایش خط VAT در Checkout و فاکتور |
|
|
| برداشت/Settlement از کیفپول شبکه | BFF: `WithdrawBalance` به `RequestWithdrawal` و `GetWithdrawalSettings` (MinWithdrawalAmount از CMS) متصل؛ `GetUserWithdrawals` فعال. فرانت: فرم برداشت با حداقل مبلغ دینامیک، مپ وضعیت/روش، فیلتر وضعیت، نمایش پیام خطای CMS و لیست درخواستها. | ۹۵٪ | همگامسازی ترجمه وضعیت/روش در همه صفحات | — |
|
|
| اعلانها (Email/SMS/Push) | نامشخص/احتمالاً صفر | ۰٪ | API Opt-in/Opt-out و تریگر اعلانهای کلیدی | تنظیمات اعلان در پروفایل، نمایش وضعیت ارسال |
|
|
| آدرسها | CRUD آدرس در BFF و فرانت موجود است. | ۸۰٪ | بررسی ولیدیشن/کشورها و پیشفرض | بهبود UX انتخاب آدرس پیشفرض و پیام خطا |
|
|
| ثبتنام/OTP/دعوت | OTP و Verify در BFF و فرانت موجود؛ ReferralCode در پروفایل نمایش داده میشود. | ۸۰٪ | سناریوهای خطا و RateLimit OTP | بهبود متن راهنما و تجربه اشتراکگذاری کد دعوت |
|
|
| سفارش و تاریخچه | Create/Submit/Update/Delete و فیلتر در BFF موجود؛ فرانت سفارش و Checkout دارد، Refund دیده نشد. | ۷۰٪ | افزودن Refund/Cancellation و فیلد VAT | نمایش تاریخچه سفارش با وضعیت ارسال و کد رهگیری |
|
|
| درخت شبکه (نمایش اعضا) | کامپوننت OrganizationChart در فرانت با دادهی User/GetAllUserByFilter؛ بدون تعادل/امتیاز. | ۳۰٪ | API داده شبکه/تعادل از CMS (درخت باینری) | نمایش درخت با امتیاز، تعداد تعادل و Carryover |
|
|
|
|
### نکات مربوط به کیفپول و برداشت
|
|
- CMS: ماژول کیفپول و Withdrawal پیادهسازی شده (Commands: `RequestWithdrawal`, `ProcessWithdrawal`, History، MinWithdrawalAmount، حالت Cash/Diamond). میتوانیم مستقیماً از gRPC/HTTP آن در BFF استفاده کنیم.
|
|
- FrontOffice: کارت کیفپول و صفحه جزئیات لاگ موجود است؛ نیاز به نمایش کیف تخفیف، بهبود UI، فیلترها و اضافه کردن جریان برداشت از موجودی شبکه. برداشت فعلاً تنها اکشن عملی روی موجودی شبکه است.
|
|
- اقدام ریز:
|
|
1) BFF: تکمیل `GetUserWallet` با DiscountBalance، افزودن فیلتر به `GetAllUserWalletChangeLog`، پیادهسازی `WithdrawBalance` با CMS RequestWithdrawal + ولیدیشن MinWithdrawalAmount/IBAN.
|
|
2) Front: بهروزرسانی کارت کیفپول با سه کیف و توضیح کاربرد، لینک به برداشت برای NetworkBalance، فیلتر/مرتبسازی لاگ، نمایش مبلغ تغییر و Reference/Type.
|
|
3) تجربه کاربری برداشت: پیام خطاهای Withdrawal (کمتر از حداقل مبلغ، درخواست در صف) و نمایش وضعیتهای Pending/Approved/Rejected در UI.
|
|
|
|
### کشفیات جدید (ویژگیهای مشتری در CMS که باید به BFF/فرانت برسد)
|
|
- **پروفایل/OTP/ثبتنام**: جریان OTP و ثبتنام، ذخیره کد ملی/نام/موبایل (`User`, `OtpToken`) و Claim `IsSignMainContract` در JWT پس از امضا.
|
|
- **آدرسها**: `UserAddress` با پیشفرض برای سفارشها؛ در فرانت پیاده است، نیاز به بهبود UX.
|
|
- **سبد/سفارش/پرداخت**: `UserCarts`, `UserOrder`, `Transactions` و PaymentRequest/Verification در BFF/فرانت موجود؛ Refund و VAT پوشش داده نشده.
|
|
- **شبکه و کمیسیون**: Network/Commission/WeeklyPool در CMS (باینری، Carryover، سقف ۳۰۰)؛ فرانت فقط درخت ساده بدون تعادل/امتیاز دارد.
|
|
- **باشگاه و کیف تخفیف**: ClubMembership, ClubFeature, DiscountBalance تعریف شده؛ هنوز Endpoint/UI ندارد.
|
|
- **برداشت کمیسیون/کیف شبکه**: `RequestWithdrawal/ProcessWithdrawal` در CMS؛ در BFF وصل شد ولی UI و استعلام وضعیت هنوز نداریم.
|
|
- **اعلانها (Email/SMS)**: پیکربندی و ارسال در CMS آماده؛ Opt-in/Opt-out و نمایش وضعیت ارسال در فرانت پیاده نشده.
|
|
- **قرارداد**: AcceptContract در BFF/فرانت فعال و توکن جدید پس از امضا صادر میشود.
|
|
|
|
</div>
|
|
|
|
---
|
|
|
|
## 📊 تحلیل جامع: شکافهای پیادهسازی در FrontOffice/FrontOffice.BFF
|
|
|
|
> **تاریخ تحلیل**: 2024-12-01
|
|
> **روش تحلیل**: بررسی عمیق ساختار دایرکتوریهای CMS/Application در مقابل FrontOffice.BFF/Application
|
|
> **یافته کلیدی**: از 27 ماژول CMS، تنها 10 ماژول در BFF پیادهسازی شده. **4 ماژول کلیدی مشتریمحور کاملاً غایب هستند.**
|
|
|
|
### 📌 خلاصه اجرایی
|
|
- **CMS Modules**: 27 ماژول (13 ماژول مرتبط با مشتری)
|
|
- **FrontOffice.BFF Modules**: 10 ماژول (فقط 70% از نیازهای مشتری)
|
|
- **Missing Modules**: 4 ماژول حیاتی (ClubMembership, NetworkMembership, Commission, DayaLoan)
|
|
- **Partial Modules**: 3 ماژول با پیادهسازی ناقص (UserWallet, UserWalletChangeLog, Contract)
|
|
|
|
---
|
|
|
|
### 🔴 ماژولهای کاملاً غایب (Critical Gap)
|
|
|
|
#### 1️⃣ ClubMembershipCQ - باشگاه مشتریان
|
|
**📍 مسیر**: `CMS/src/CMSMicroservice.Application/ClubMembershipCQ/`
|
|
**❌ وضعیت**: هیچ معادلی در BFF وجود ندارد
|
|
|
|
**Commands در CMS:**
|
|
- `ActivateClubMembershipCommand` - فعالسازی عضویت (پرداخت 56M + شارژ کیفپولها)
|
|
- `DeactivateClubMembershipCommand` - غیرفعال کردن عضویت
|
|
- `UpdateClubMembershipCommand` - بهروزرسانی جزئیات
|
|
|
|
**Queries در CMS:**
|
|
- `GetClubMembershipStatusQuery` - وضعیت و فیچرهای فعال
|
|
- `GetAllClubMembershipsQuery` - لیست عضویتها (Admin)
|
|
- `GetClubMembershipHistoryQuery` - تاریخچه تغییرات
|
|
|
|
**💥 تأثیر بر کاربر:**
|
|
- ❌ عدم امکان عضویت در باشگاه
|
|
- ❌ عدم دسترسی به فروشگاه تخفیفی
|
|
- ❌ عدم نمایش فیچرها و امتیازات باشگاه
|
|
|
|
**📋 اقدام مورد نیاز:**
|
|
```
|
|
BFF: ایجاد ClubMembershipCQ + 6 Handler + gRPC Client
|
|
UI: ClubMembershipPage.razor + نمایش وضعیت در داشبورد
|
|
```
|
|
|
|
---
|
|
|
|
#### 2️⃣ NetworkMembershipCQ - شبکه باینری
|
|
**📍 مسیر**: `CMS/src/CMSMicroservice.Application/NetworkMembershipCQ/`
|
|
**❌ وضعیت**: هیچ معادلی در BFF وجود ندارد (UI درخت دارد اما بدون داده واقعی)
|
|
|
|
**Commands در CMS:**
|
|
- `JoinNetworkCommand` - ثبت در شبکه باینری (SponsorId, ParentId, Position)
|
|
- `MoveInNetworkCommand` - جابجایی در درخت (Admin)
|
|
- `RemoveFromNetworkCommand` - حذف از شبکه (Admin)
|
|
|
|
**Queries در CMS:**
|
|
- `GetNetworkTreeQuery` - درخت باینری با MaxDepth (1-10)
|
|
- `GetUserNetworkPositionQuery` - موقعیت + آمار (Parent, Children, Total)
|
|
- `GetNetworkMembershipHistoryQuery` - تاریخچه تغییرات
|
|
|
|
**💥 تأثیر بر کاربر:**
|
|
- ⚠️ UI درخت موجود اما با Mock data
|
|
- ❌ عدم نمایش امتیازات و تعادل پاها
|
|
- ❌ عدم امکان دعوت افراد به شبکه
|
|
|
|
**📋 اقدام مورد نیاز:**
|
|
```
|
|
BFF: ایجاد NetworkMembershipCQ + 6 Handler + gRPC Client
|
|
UI: بهروزرسانی OrganizationChart.razor با داده واقعی + نمایش امتیاز
|
|
```
|
|
|
|
---
|
|
|
|
#### 3️⃣ CommissionCQ - کمیسیون هفتگی و برداشت
|
|
**📍 مسیر**: `CMS/src/CMSMicroservice.Application/CommissionCQ/`
|
|
**❌ وضعیت**: BFF دارای `WithdrawBalance` اما **Handler خالی است**
|
|
|
|
**Commands در CMS:**
|
|
- `RequestWithdrawalCommand` - درخواست برداشت (Cash/Diamond)
|
|
- `ProcessWithdrawalCommand` - تایید/رد توسط ادمین
|
|
|
|
**Queries در CMS:**
|
|
- `GetWeeklyCommissionPoolQuery` - اطلاعات استخر (TotalPool, ValuePerPoint)
|
|
- `GetUserCommissionPayoutsQuery` - تاریخچه پرداختها (Pending→Paid→Withdrawn)
|
|
- `GetUserWeeklyBalancesQuery` - تعادل هفتگی (Left/Right Volume, Carryover, سقف 300)
|
|
- `GetAllWeeklyPoolsQuery` - تاریخچه استخرها (Admin)
|
|
- `GetWithdrawalRequestsQuery` - لیست درخواستهای برداشت (Admin)
|
|
|
|
**💥 تأثیر بر کاربر:**
|
|
- ❌ عدم نمایش کمیسیون هفتگی
|
|
- ❌ عدم امکان درخواست برداشت (Handler خالی)
|
|
- ❌ عدم پیگیری وضعیت برداشتها
|
|
|
|
**📋 اقدام مورد نیاز:**
|
|
```
|
|
BFF: ایجاد CommissionCQ + تکمیل WithdrawBalance + 5 Query + gRPC Client
|
|
UI: CommissionDashboardPage.razor + WithdrawalRequestPage.razor + WeeklyBalanceChart.razor
|
|
```
|
|
|
|
---
|
|
|
|
#### 4️⃣ DayaLoanCQ - وام دایا (Phase 11 - جدید)
|
|
**📍 مسیر**: `CMS/src/CMSMicroservice.Application/DayaLoanCQ/`
|
|
**❌ وضعیت**: هیچ معادلی در BFF وجود ندارد (تازه در CMS پیاده شده)
|
|
|
|
**Commands در CMS:**
|
|
- `ProcessDayaLoanApprovalCommand` - شارژ 3 کیفپول (168M تومان)
|
|
- `CheckDayaLoanStatusCommand` - استعلام از API دایا
|
|
|
|
**💥 تأثیر بر کاربر:**
|
|
- ❌ عدم نمایش وضعیت وام
|
|
- ❌ عدم امکان پیگیری اعتبار دریافتی
|
|
|
|
**📋 اقدام مورد نیاز:**
|
|
```
|
|
BFF: ایجاد DayaLoanCQ + 2 Handler + Mock API Client
|
|
UI: DayaLoanStatusPage.razor + نمایش ContractNumber و تاریخ دریافت
|
|
```
|
|
|
|
---
|
|
|
|
### ⚠️ ماژولهای پیادهسازی ناقص
|
|
|
|
#### 5️⃣ UserWalletChangeLogCQ - تاریخچه مالی
|
|
**وضعیت**: BFF دارد `GetAllUserWalletChangeLog` اما **بدون فیلتر**
|
|
|
|
**گپ:**
|
|
- ❌ فیلتر نوع تراکنش (Deposit, Withdraw, Purchase, Refund)
|
|
- ❌ فیلتر بازه زمانی
|
|
- ❌ جستجوی ReferenceId
|
|
- ❌ Query برای جزئیات تراکنش خاص (`GetUserWalletChangeLogQuery`)
|
|
|
|
**📋 اقدام:**
|
|
```
|
|
BFF: افزودن پارامترهای فیلتر به Handler موجود
|
|
UI: افزودن فیلتر/جستجو به WalletDetailsPage.razor
|
|
```
|
|
|
|
---
|
|
|
|
#### 6️⃣ UserWalletCQ - کیفپولها
|
|
**وضعیت**: BFF دارد `GetUserWallet` اما **بدون DiscountBalance در DTO**
|
|
|
|
**گپ:**
|
|
- ⚠️ Response فقط Balance + NetworkBalance برمیگرداند
|
|
- ❌ DiscountBalance نمایش داده نمیشود
|
|
|
|
**📋 اقدام:**
|
|
```
|
|
BFF: افزودن DiscountBalance به GetUserWallet Response DTO
|
|
UI: نمایش کیف تخفیف در WalletCard.razor
|
|
```
|
|
|
|
---
|
|
|
|
### 📊 آمار نهایی شکاف
|
|
|
|
| ماژول CMS | Commands | Queries | BFF Status | UI Status | Gap % |
|
|
|-----------|----------|---------|------------|-----------|-------|
|
|
| ClubMembershipCQ | 3 | 3 | ❌ None | ❌ None | **100%** |
|
|
| NetworkMembershipCQ | 3 | 3 | ❌ None | ⚠️ Mock | **90%** |
|
|
| CommissionCQ | 2 | 5 | ⚠️ Empty Handler | ❌ None | **100%** |
|
|
| DayaLoanCQ | 2 | 0 | ❌ None | ❌ None | **100%** |
|
|
| UserWalletChangeLogCQ | 0 | 2 | ⚠️ No Filter | ⚠️ No Filter | **40%** |
|
|
| UserWalletCQ | 1 | 2 | ⚠️ Missing Field | ⚠️ Missing | **30%** |
|
|
| **TOTAL** | **11** | **15** | **10/27 Modules** | - | **63% Missing** |
|
|
|
|
**نتیجهگیری**: از 26 قابلیت (Commands/Queries) مورد نیاز مشتری، **16 قابلیت (62%) کاملاً غایب** و **4 قابلیت (15%) ناقص** هستند.
|
|
|
|
---
|
|
|
|
### 🎯 اولویتبندی توسعه (برای Developer بعدی)
|
|
|
|
#### 🔴 فاز 1 (Critical - 2 هفته):
|
|
1. **CommissionCQ** - کمیسیون و برداشت
|
|
- [ ] BFF: 2 Commands + 5 Queries + gRPC Client
|
|
- [ ] UI: CommissionDashboard + WithdrawalRequest + WeeklyBalanceChart
|
|
- ⏱️ تخمین: 5 روز کاری
|
|
|
|
2. **ClubMembershipCQ** - عضویت باشگاه
|
|
- [ ] BFF: 3 Commands + 3 Queries + gRPC Client
|
|
- [ ] UI: ClubMembershipPage + Profile widgets
|
|
- ⏱️ تخمین: 4 روز کاری
|
|
|
|
3. **NetworkMembershipCQ** - شبکه باینری
|
|
- [ ] BFF: 3 Commands + 3 Queries + gRPC Client
|
|
- [ ] UI: OrganizationChart update + Position page
|
|
- ⏱️ تخمین: 5 روز کاری
|
|
|
|
---
|
|
|
|
#### 🟡 فاز 2 (Important - 1 هفته):
|
|
4. **UserWalletCQ Enhancement** - کیف تخفیف
|
|
- [ ] BFF: Add DiscountBalance to DTO
|
|
- [ ] UI: Display in WalletCard
|
|
- ⏱️ تخمین: 1 روز کاری
|
|
|
|
5. **UserWalletChangeLogCQ Enhancement** - فیلتر تراکنشها
|
|
- [ ] BFF: Add filter params (Type, DateRange, ReferenceId)
|
|
- [ ] UI: Filter controls in WalletDetailsPage
|
|
- ⏱️ تخمین: 2 روز کاری
|
|
|
|
6. **DayaLoanCQ** - وام دایا
|
|
- [ ] BFF: 2 Commands + Mock API Client
|
|
- [ ] UI: DayaLoanStatusPage
|
|
- ⏱️ تخمین: 2 روز کاری
|
|
|
|
---
|
|
|
|
#### 🟢 فاز 3 (Nice to Have - 3 روز):
|
|
7. **OtpTokenCQ Enhancement** - RateLimit
|
|
- [ ] BFF: Add middleware (5 req/10min per IP)
|
|
- ⏱️ تخمین: 1 روز کاری
|
|
|
|
8. **TransactionsCQ Enhancement** - Refund & VAT
|
|
- [ ] BFF: RefundTransaction Command + VAT fields
|
|
- [ ] UI: Refund button + VAT display
|
|
- ⏱️ تخمین: 2 روز کاری
|
|
|
|
---
|
|
|
|
### 📋 چکلیست کامل (Copy-Paste Ready)
|
|
|
|
#### FrontOffice.BFF:
|
|
```csharp
|
|
// ماژولهای جدید (از صفر)
|
|
[ ] Create /Application/ClubMembershipCQ/
|
|
[ ] Commands/ActivateClubMembership.cs + Handler
|
|
[ ] Queries/GetClubMembershipStatus.cs + Handler
|
|
[ ] DTOs/ClubMembershipDto.cs
|
|
|
|
[ ] Create /Application/NetworkMembershipCQ/
|
|
[ ] Commands/JoinNetwork.cs + Handler
|
|
[ ] Queries/GetNetworkTree.cs + Handler (MaxDepth: 1-10)
|
|
[ ] Queries/GetUserNetworkPosition.cs + Handler
|
|
[ ] DTOs/NetworkTreeDto.cs, NetworkPositionDto.cs
|
|
|
|
[ ] Create /Application/CommissionCQ/
|
|
[ ] Commands/RequestWithdrawal.cs (تکمیل Handler خالی موجود)
|
|
[ ] Queries/GetUserCommissionPayouts.cs + Handler
|
|
[ ] Queries/GetUserWeeklyBalances.cs + Handler
|
|
[ ] Queries/GetWeeklyCommissionPool.cs + Handler
|
|
[ ] DTOs/CommissionPayoutDto.cs, WeeklyBalanceDto.cs
|
|
|
|
[ ] Create /Application/DayaLoanCQ/
|
|
[ ] Commands/CheckDayaLoanStatus.cs + Handler
|
|
[ ] Services/MockDayaApiClient.cs
|
|
[ ] DTOs/DayaLoanInfoDto.cs
|
|
|
|
// بهروزرسانی ماژولهای موجود
|
|
[ ] Update /Application/UserWalletCQ/
|
|
[ ] DTOs/UserWalletDto.cs → Add: public decimal DiscountBalance { get; set; }
|
|
[ ] Handlers/GetUserWalletQueryHandler.cs → Map DiscountBalance from CMS
|
|
|
|
[ ] Update /Application/UserWalletCQ/ (ChangeLog)
|
|
[ ] Queries/GetAllUserWalletChangeLog.cs → Add params:
|
|
- WalletChangeType? Type
|
|
- DateTime? DateFrom, DateTime? DateTo
|
|
- string? ReferenceId
|
|
[ ] Handler → Apply filters in CMS gRPC call
|
|
|
|
// gRPC Registration
|
|
[ ] Update /Infrastructure/ConfigureGrpcServices.cs
|
|
builder.Services.AddGrpcClient<ClubMembershipServiceClient>(...)
|
|
builder.Services.AddGrpcClient<NetworkMembershipServiceClient>(...)
|
|
builder.Services.AddGrpcClient<CommissionServiceClient>(...)
|
|
|
|
// Security
|
|
[ ] Create /Infrastructure/Middleware/RateLimitMiddleware.cs
|
|
- Apply to: /api/user/otp endpoints
|
|
- Limit: 5 requests per 10 minutes per IP
|
|
```
|
|
|
|
#### FrontOffice (UI):
|
|
```razor
|
|
// صفحات جدید
|
|
[ ] Create /Pages/ClubMembership/Index.razor
|
|
- نمایش وضعیت عضویت (Active/Inactive)
|
|
- دکمه فعالسازی (هدایت به درگاه پرداخت 56M)
|
|
- لیست فیچرهای فعال (Badge system)
|
|
|
|
[ ] Create /Pages/Commission/Dashboard.razor
|
|
- نمایش استخر هفته (TotalPool, ValuePerPoint)
|
|
- نمودار تعادل (Left vs Right Volume)
|
|
- تاریخچه پرداختها با Badge وضعیت
|
|
|
|
[ ] Create /Pages/Commission/Withdrawal.razor
|
|
- فرم برداشت (IBAN, Amount, Method: Cash/Diamond)
|
|
- ولیدیشن MinWithdrawalAmount (100,000 تومان)
|
|
- نمایش پیام خطا (کمتر از حداقل، صف تایید)
|
|
|
|
[ ] Create /Pages/Network/Tree.razor
|
|
- بهروزرسانی OrganizationChart.razor
|
|
- Slider MaxDepth (1-10)
|
|
- نمایش امتیاز در هر Node
|
|
- رنگبندی بر اساس تعادل (سبز=متعادل، قرمز=نامتعادل)
|
|
- Tooltip: Parent, Children count, Carryover
|
|
|
|
[ ] Create /Pages/DayaLoan/Status.razor
|
|
- نمایش ContractNumber
|
|
- تاریخ دریافت اعتبار
|
|
- مبالغ شارژ شده (3×56M)
|
|
|
|
// کامپوننتهای جدید
|
|
[ ] Update /Components/Wallet/WalletCard.razor
|
|
<MudCard>
|
|
<MudText>موجودی کیف پول: {Balance:N0} ریال</MudText>
|
|
<MudText>موجودی شبکه: {NetworkBalance:N0} ریال</MudText>
|
|
<MudText Color="Color.Success">موجودی تخفیف: {DiscountBalance:N0} ریال</MudText>
|
|
<MudButton Href="/wallet/withdraw" Disabled="@(NetworkBalance < 100000)">
|
|
درخواست برداشت
|
|
</MudButton>
|
|
</MudCard>
|
|
|
|
[ ] Create /Components/Commission/WeeklyBalanceChart.razor
|
|
- نمودار میلهای Left/Right Volume
|
|
- نمایش WeakerLeg (کمترین حجم)
|
|
- نمایش Carryover و سقف 300
|
|
|
|
// بهروزرسانی موجودی
|
|
[ ] Update /Pages/Wallet/DetailsPage.razor
|
|
[ ] Add filter controls:
|
|
- نوع تراکنش (Deposit, Withdraw, Purchase, Refund)
|
|
- بازه زمانی (DatePicker: From/To)
|
|
- جستجوی ReferenceId (TextBox)
|
|
[ ] نمایش ChangeValue به جای CurrentBalance
|
|
[ ] پیجینیشن
|
|
|
|
[ ] Update /Components/Layout/NavMenu.razor
|
|
<MudNavLink Href="/clubmembership" Icon="@Icons.Material.Filled.CardMembership">
|
|
باشگاه مشتریان
|
|
</MudNavLink>
|
|
<MudNavLink Href="/commission/dashboard" Icon="@Icons.Material.Filled.Payments">
|
|
کمیسیون و برداشت
|
|
</MudNavLink>
|
|
<MudNavLink Href="/network/tree" Icon="@Icons.Material.Filled.AccountTree">
|
|
شبکه من
|
|
</MudNavLink>
|
|
```
|
|
|
|
---
|
|
|
|
### 🚨 نکات حیاتی (Critical Notes)
|
|
|
|
#### ⚠️ امنیت:
|
|
```
|
|
1. WithdrawBalance:
|
|
- MinAmount: 100,000 ریال (CMS config)
|
|
- IBAN: IR + 24 digits validation
|
|
- Daily limit per user: Check CMS setting
|
|
|
|
2. JoinNetwork:
|
|
- IsDescendant recursive check (prevent circular ref)
|
|
- Position validation (Left/Right must be empty)
|
|
- SponsorId must be active club member
|
|
|
|
3. OTP RateLimit:
|
|
- 5 requests / 10 min per IP
|
|
- Redis/InMemory cache
|
|
```
|
|
|
|
#### 💡 UI/UX:
|
|
```
|
|
1. WalletCard: 3 کیفپول با رنگ متفاوت
|
|
- Balance: آبی (خرید عمومی)
|
|
- NetworkBalance: سبز (برداشت Cash/Diamond)
|
|
- DiscountBalance: زرد (فروشگاه باشگاه)
|
|
|
|
2. CommissionDashboard:
|
|
- Badge colors: Pending=زرد, Calculated=آبی, Paid=سبز, Withdrawn=خاکستری
|
|
- Carryover info tooltip
|
|
- سقف 300 Balance در هفته
|
|
|
|
3. NetworkTree:
|
|
- MaxDepth default: 3
|
|
- Load on demand برای عمق بیشتر
|
|
- Tooltip با Shift+Click
|
|
```
|
|
|
|
#### ❌ خطاهای رایج:
|
|
```
|
|
1. BFF: Handler خالی
|
|
❌ FrontOffice.BFF/Application/UserWalletCQ/Commands/WithdrawBalanceCommandHandler.cs
|
|
✅ Fix: Call CMS.RequestWithdrawal via gRPC
|
|
|
|
2. UI: Mock data
|
|
❌ FrontOffice/Pages/Network/OrganizationChart.razor (hardcoded nodes)
|
|
✅ Fix: @inject NetworkService → await GetTreeAsync()
|
|
|
|
3. DTO: Missing field
|
|
❌ UserWalletDto missing DiscountBalance
|
|
✅ Fix: Add property + map in Handler
|
|
```
|
|
|
|
</div>
|
|
|
|
|
|
---
|
|
|
|
## 📊 تحلیل کامل شکافهای پیادهسازی (Gap Analysis Report)
|
|
> **تاریخ تحلیل**: 2024-12-01
|
|
> **روش**: مقایسه ماژول به ماژول CMS vs FrontOffice.BFF vs FrontOffice
|
|
|
|
### 📈 آمار کلی
|
|
|
|
| مجموع | CMS Modules | BFF Modules | شکاف (Missing) | نرخ پوشش |
|
|
|-------|-------------|-------------|----------------|----------|
|
|
| **کل ماژولها** | 26 ماژول | 9 ماژول | 17 ماژول | 35% |
|
|
| **ماژولهای مشتریمحور** | 15 ماژول | 7 ماژول | 8 ماژول | 47% |
|
|
| **ماژولهای حیاتی غایب** | - | - | 4 ماژول | 0% |
|
|
|
|
---
|
|
|
|
### 🔴 CRITICAL: ماژولهای کاملاً غایب (0% پیادهسازی)
|
|
|
|
#### 1️⃣ ClubMembershipCQ - باشگاه مشتریان
|
|
**Commands در CMS (موجود):**
|
|
- `ActivateClubMembership` - فعالسازی عضویت (پرداخت 56M)
|
|
- `DeactivateClubMembership` - غیرفعال کردن
|
|
- `AssignClubFeature` - اختصاص فیچر (Trial/VIP)
|
|
|
|
**Queries در CMS (موجود):**
|
|
- `GetClubMembership` - دریافت وضعیت عضویت کاربر
|
|
- `GetAllClubMemberships` - لیست کل اعضا (Admin)
|
|
- `GetClubMembershipHistory` - تاریخچه تغییرات
|
|
- `GetClubStatistics` - آمار کلی باشگاه
|
|
|
|
**❌ در FrontOffice.BFF**: هیچ چیز موجود نیست
|
|
**❌ در FrontOffice UI**: هیچ صفحهای برای باشگاه وجود ندارد
|
|
|
|
**📋 Task های مورد نیاز:**
|
|
```
|
|
[ ] FrontOffice.BFF:
|
|
[ ] Create ClubMembershipCQ/Commands/ActivateClubMembership/
|
|
[ ] Create ClubMembershipCQ/Queries/GetMyClubMembership/
|
|
[ ] Create ClubMembershipCQ/Queries/GetClubFeatures/
|
|
|
|
[ ] FrontOffice UI:
|
|
[ ] Create /Pages/Club/MembershipPage.razor
|
|
- نمایش وضعیت عضویت (Active/Inactive/Trial)
|
|
- دکمه فعالسازی (پرداخت 56M)
|
|
- لیست فیچرهای باشگاه
|
|
[ ] Create /Pages/Club/FeaturesPage.razor
|
|
- لیست فیچرهای Trial vs VIP
|
|
- Badge امتیاز برای هر فیچر
|
|
[ ] Create /Components/Club/ActivationButton.razor
|
|
- فرم پرداخت
|
|
- اتصال به درگاه
|
|
```
|
|
|
|
**💰 بیزینس اثر:**
|
|
- کاربر نمیتواند عضو باشگاه شود
|
|
- 56M تومان در Balance/Discount شارژ نمیشود
|
|
- دسترسی به فروشگاه تخفیفی ندارد
|
|
|
|
---
|
|
|
|
#### 2️⃣ NetworkMembershipCQ - شبکه باینری
|
|
**Commands در CMS (موجود):**
|
|
- `JoinNetwork` - عضویت در شبکه (Parent/Position)
|
|
- `MoveInNetwork` - جابجایی موقعیت
|
|
- `RemoveFromNetwork` - حذف از شبکه
|
|
|
|
**Queries در CMS (موجود):**
|
|
- `GetNetworkTree` - دریافت درخت باینری (MaxDepth: 1-10)
|
|
- `GetUserNetworkPosition` - موقعیت کاربر در درخت
|
|
- `GetNetworkMembershipHistory` - تاریخچه جابجاییها
|
|
- `GetNetworkStatistics` - آمار شبکه (تعداد چپ/راست/کل)
|
|
|
|
**❌ در FrontOffice.BFF**: هیچ چیز موجود نیست
|
|
**⚠️ در FrontOffice UI**: فقط OrganizationChart با داده Mock
|
|
|
|
**📋 Task های مورد نیاز:**
|
|
```
|
|
[ ] FrontOffice.BFF:
|
|
[ ] Create NetworkMembershipCQ/Commands/JoinNetwork/
|
|
[ ] Create NetworkMembershipCQ/Queries/GetNetworkTree/
|
|
[ ] Create NetworkMembershipCQ/Queries/GetMyNetworkPosition/
|
|
[ ] Create NetworkMembershipCQ/Queries/GetNetworkStatistics/
|
|
|
|
[ ] FrontOffice UI:
|
|
[ ] Update /Pages/Network/OrganizationChart.razor
|
|
- حذف Mock data
|
|
- فراخوانی GetNetworkTree از BFF
|
|
- نمایش MaxDepth selector (1-10)
|
|
- Lazy loading برای سطوح پایینتر
|
|
[ ] Create /Pages/Network/JoinPage.razor
|
|
- فرم انتخاب Parent
|
|
- انتخاب Position (Left/Right)
|
|
- نمایش پیشنمایش موقعیت
|
|
[ ] Create /Pages/Network/StatsPage.razor
|
|
- تعداد اعضای چپ/راست
|
|
- عمق درخت
|
|
- آخرین عضو جدید
|
|
```
|
|
|
|
**💰 بیزینس اثر:**
|
|
- کاربر نمیتواند زیرمجموعه بگیرد
|
|
- درخت شبکه واقعی نمایش داده نمیشود
|
|
- محاسبه کمیسیون باینری کار نمیکند
|
|
|
|
---
|
|
|
|
#### 3️⃣ CommissionCQ - کمیسیون هفتگی و برداشت
|
|
**Commands در CMS (موجود):**
|
|
- `RequestWithdrawal` - درخواست برداشت (Cash/Diamond/IBAN)
|
|
- `ApproveWithdrawal` - تایید برداشت (Admin)
|
|
- `RejectWithdrawal` - رد برداشت (Admin)
|
|
- `ProcessWithdrawal` - پردازش برداشت
|
|
- `CalculateWeeklyBalances` - محاسبه تعادل هفتگی
|
|
- `CalculateWeeklyCommissionPool` - محاسبه استخر
|
|
- `ProcessUserPayouts` - توزیع کمیسیون
|
|
- `TriggerWeeklyCalculation` - اجرای دستی Worker
|
|
|
|
**Queries در CMS (موجود):**
|
|
- `GetUserCommissionPayouts` - لیست پرداختهای کمیسیون کاربر
|
|
- `GetCommissionPayoutHistory` - تاریخچه تغییرات
|
|
- `GetUserWeeklyBalances` - تعادل هفتگی (Left/Right/Weaker)
|
|
- `GetWeeklyCommissionPool` - اطلاعات استخر هفته
|
|
- `GetAllWeeklyPools` - تمام استخرها (Admin)
|
|
- `GetWithdrawalRequests` - درخواستهای برداشت
|
|
- `GetWorkerStatus` - وضعیت Worker
|
|
- `GetWorkerExecutionLogs` - لاگ اجرای Worker
|
|
|
|
**⚠️ در FrontOffice.BFF**: فقط یک Handler خالی `WithdrawBalance`
|
|
**❌ در FrontOffice UI**: هیچ چیز موجود نیست
|
|
|
|
**📋 Task های مورد نیاز:**
|
|
```
|
|
[ ] FrontOffice.BFF:
|
|
[ ] Complete UserWalletCQ/Commands/WithdrawBalance/
|
|
- فراخوانی CMS.RequestWithdrawal
|
|
- ولیدیشن MinWithdrawalAmount
|
|
- چک IBAN format
|
|
[ ] Create CommissionCQ/Queries/GetMyCommissionPayouts/
|
|
[ ] Create CommissionCQ/Queries/GetMyWeeklyBalances/
|
|
[ ] Create CommissionCQ/Queries/GetWithdrawalHistory/
|
|
|
|
[ ] FrontOffice UI:
|
|
[ ] Create /Pages/Commission/DashboardPage.razor
|
|
- کارت استخر هفته (TotalPool, BalanceValue)
|
|
- کارت امتیازات من (LesserLegPoints)
|
|
- پیشبینی کمیسیون این هفته
|
|
[ ] Create /Pages/Commission/HistoryPage.razor
|
|
- جدول پرداختهای گذشته
|
|
- فیلتر Status (Pending/Paid/Withdrawn)
|
|
- نمودار روند کمیسیون
|
|
[ ] Create /Pages/Commission/WithdrawPage.razor
|
|
- فرم برداشت (Amount, Method, IBAN)
|
|
- نمایش MinWithdrawalAmount
|
|
- نمایش موجودی قابل برداشت
|
|
- تاریخچه برداشتها
|
|
[ ] Create /Pages/Commission/WeeklyBalancePage.razor
|
|
- تعادل چپ/راست
|
|
- Carryover از هفته قبل
|
|
- سقف 300 Balance
|
|
- نمودار خطی رشد هفتگی
|
|
```
|
|
|
|
**💰 بیزینس اثر:**
|
|
- کاربر نمیتواند کمیسیون خود را ببیند
|
|
- برداشت از NetworkBalance کار نمیکند
|
|
- تعادل هفتگی و Carryover نامشخص است
|
|
|
|
---
|
|
|
|
#### 4️⃣ DayaLoanCQ - وام دایا
|
|
**Commands در CMS (موجود - جدید):**
|
|
- `CheckDayaLoanStatus` - استعلام وضعیت وام
|
|
- `ProcessDayaLoanApproval` - پردازش تایید وام (شارژ 168M)
|
|
|
|
**❌ در FrontOffice.BFF**: هیچ چیز موجود نیست
|
|
**❌ در FrontOffice UI**: هیچ چیز موجود نیست
|
|
|
|
**📋 Task های مورد نیاز:**
|
|
```
|
|
[ ] FrontOffice.BFF:
|
|
[ ] Create DayaLoanCQ/Queries/GetMyDayaLoanStatus/
|
|
[ ] Create DayaLoanCQ/Commands/RequestDayaLoanCheck/ (optional)
|
|
|
|
[ ] FrontOffice UI:
|
|
[ ] Create /Pages/DayaLoan/StatusPage.razor
|
|
- نمایش وضعیت وام (PendingReceive/Received/Rejected)
|
|
- شماره قرارداد (ContractNumber)
|
|
- تاریخ آخرین بررسی
|
|
[ ] Create /Components/DayaLoan/StatusBadge.razor
|
|
- Badge رنگی برای Status
|
|
```
|
|
|
|
**💰 بیزینس اثر:**
|
|
- کاربر نمیتواند وضعیت وام دایا خود را ببیند
|
|
- 168M شارژ کیفپول (56M×3) نامشخص است
|
|
- فقط Worker پسزمینه فعال است (بدون UI)
|
|
|
|
---
|
|
|
|
### 🟡 PARTIAL: ماژولهای نیمهپیاده (50-80% تکمیل)
|
|
|
|
#### 5️⃣ UserWalletCQ - کیفپول
|
|
**✅ در BFF موجود:**
|
|
- `GetUserWallet` - دریافت موجودی
|
|
- `GetAllUserWalletChangeLog` - تاریخچه تراکنشها
|
|
|
|
**❌ در BFF غایب:**
|
|
- `WithdrawBalance` Handler - خالی است و کار نمیکند
|
|
|
|
**⚠️ مشکلات موجود:**
|
|
- `GetUserWallet` Response فقط Balance و NetworkBalance دارد
|
|
- **DiscountBalance موجود نیست** (باید اضافه شود)
|
|
- `GetAllUserWalletChangeLog` فیلتر ندارد (نوع/بازه زمانی)
|
|
|
|
**📋 Task های مورد نیاز:**
|
|
```
|
|
[ ] FrontOffice.BFF:
|
|
[ ] Update GetUserWallet Response DTO
|
|
✅ Balance (موجود)
|
|
✅ NetworkBalance (موجود)
|
|
❌ DiscountBalance (باید اضافه شود)
|
|
[ ] Update GetAllUserWalletChangeLog
|
|
- فیلتر نوع تراکنش (Deposit/Withdraw/Purchase)
|
|
- فیلتر بازه زمانی (From/To)
|
|
- فیلتر ReferenceId
|
|
[ ] Fix WithdrawBalance Handler
|
|
- Call CMS.RequestWithdrawal
|
|
- Validation: MinAmount, IBAN
|
|
|
|
[ ] FrontOffice UI:
|
|
[ ] Update /Pages/Wallet/WalletCard.razor
|
|
✅ Balance (موجود)
|
|
✅ NetworkBalance (موجود)
|
|
❌ DiscountBalance (باید اضافه شود - با رنگ زرد)
|
|
- حذف داده Mock
|
|
[ ] Update /Pages/Wallet/DetailsPage.razor
|
|
- فیلترها (نوع/تاریخ/جستجو)
|
|
- نمایش ChangeValue به جای CurrentBalance
|
|
- Pagination
|
|
```
|
|
|
|
---
|
|
|
|
#### 6️⃣ UserCartsCQ / ShoppingCartCQ - سبد خرید
|
|
**✅ در BFF موجود (نام: ShopingCartCQ):**
|
|
- `AddNewUserCart` - افزودن به سبد
|
|
- `UpdateUserCart` - بهروزرسانی تعداد
|
|
|
|
**❌ در BFF غایب:**
|
|
- `ClearCart` - پاک کردن کل سبد
|
|
- `DeleteUserCarts` - حذف یک آیتم
|
|
- `MergeGuestCart` - ادغام سبد مهمان→ورود
|
|
|
|
**📋 Task های مورد نیاز:**
|
|
```
|
|
[ ] FrontOffice.BFF:
|
|
[ ] Create ShopingCartCQ/Commands/ClearCart/
|
|
[ ] Create ShopingCartCQ/Commands/DeleteCartItem/
|
|
[ ] Create ShopingCartCQ/Commands/MergeGuestCart/
|
|
- Input: SessionId مهمان + UserId ورود
|
|
- Logic: Merge duplicate products (sum quantities)
|
|
|
|
[ ] FrontOffice UI:
|
|
[ ] Update /Pages/Cart/CartPage.razor
|
|
- دکمه "پاک کردن سبد"
|
|
- دکمه حذف آیتم (هر سطر)
|
|
[ ] Implement Guest→Login merge
|
|
- ذخیره SessionId در LocalStorage
|
|
- POST به MergeGuestCart بعد از Login
|
|
- نمایش پیام "x محصول از سبد قبلی شما اضافه شد"
|
|
```
|
|
|
|
---
|
|
|
|
#### 7️⃣ ContractCQ - قرارداد
|
|
**✅ در BFF موجود:**
|
|
- `AcceptContract` در UserCQ/Commands/
|
|
|
|
**❌ در BFF غایب:**
|
|
- `GetContract` - دریافت متن قرارداد
|
|
- `GetAllContracts` - لیست نسخههای قرارداد
|
|
|
|
**📋 Task های مورد نیاز:**
|
|
```
|
|
[ ] FrontOffice.BFF:
|
|
[ ] Create ContractCQ/Queries/GetLatestContract/
|
|
[ ] Create ContractCQ/Queries/GetMyContractHistory/
|
|
|
|
[ ] FrontOffice UI:
|
|
[ ] Update /Pages/Auth/ContractPage.razor
|
|
- دریافت متن قرارداد از API (حذف hardcode)
|
|
- نمایش تاریخ آخرین نسخه
|
|
[ ] Create /Pages/Profile/ContractHistoryPage.razor
|
|
- لیست قراردادهای امضا شده
|
|
- دانلود PDF
|
|
```
|
|
|
|
---
|
|
|
|
### ✅ COMPLETE: ماژولهای کامل (80-100% تکمیل)
|
|
|
|
#### 8️⃣ UserCQ - پروفایل و احراز هویت
|
|
**✅ پیادهسازی کامل:**
|
|
- Login, Register, UpdateProfile
|
|
- ChangePassword, ForgotPassword
|
|
- GetUserByFilter
|
|
- AcceptContract (امضای قرارداد)
|
|
|
|
#### 9️⃣ UserAddressCQ - آدرسها
|
|
**✅ پیادهسازی کامل:**
|
|
- CRUD آدرس
|
|
- SetDefault
|
|
- UI: AddressPage و AddressCard
|
|
|
|
#### 🔟 UserOrderCQ - سفارشات
|
|
**✅ پیادهسازی 70%:**
|
|
- Create, Update, Delete, GetById, GetByFilter
|
|
- ❌ غایب: Cancel, Refund, TrackingCode
|
|
|
|
---
|
|
|
|
### 📊 جدول خلاصه اولویتبندی
|
|
|
|
| اولویت | ماژول | درصد فعلی | Tasks باقیمانده | تخمین زمان |
|
|
|--------|-------|-----------|-------------------|-------------|
|
|
| 🔴 P0 | ClubMembershipCQ | 0% | 8 Handlers + 4 Pages | 2 هفته |
|
|
| 🔴 P0 | CommissionCQ | 10% | 12 Handlers + 6 Pages | 3 هفته |
|
|
| 🔴 P0 | NetworkMembershipCQ | 5% | 7 Handlers + 4 Pages | 2 هفته |
|
|
| 🟡 P1 | UserWalletCQ | 60% | 3 Handlers + 2 Pages | 1 هفته |
|
|
| 🟡 P1 | ShoppingCartCQ | 50% | 3 Handlers + UI updates | 1 هفته |
|
|
| 🟢 P2 | DayaLoanCQ | 0% | 2 Handlers + 1 Page | 3 روز |
|
|
| 🟢 P2 | ContractCQ | 80% | 2 Handlers + 1 Page | 2 روز |
|
|
|
|
**مجموع تخمین:** 9 هفته = 2 ماه (1 نفر Full-time)
|
|
|
|
---
|
|
|
|
### 🎯 خلاصه اجرایی برای توسعهدهنده
|
|
|
|
**وضعیت فعلی:**
|
|
- از 15 ماژول مشتریمحور CMS، تنها 7 ماژول در BFF دارید
|
|
- 4 ماژول حیاتی (باشگاه، شبکه، کمیسیون، وام) کاملاً غایب
|
|
- 3 ماژول موجود (کیفپول، سبد، قرارداد) ناقص
|
|
|
|
**کارهایی که توسعهدهنده قبلی انجام نداد:**
|
|
1. ❌ هیچ Handler برای باشگاه (ClubMembership)
|
|
2. ❌ هیچ Handler برای شبکه (NetworkMembership)
|
|
3. ❌ هیچ Handler برای کمیسیون (Commission) به جز یک Handler خالی
|
|
4. ❌ هیچ Handler برای وام دایا (DayaLoan)
|
|
5. ⚠️ Handler کیفپول (UserWallet) ناقص - DiscountBalance غایب
|
|
6. ⚠️ Handler سبد (ShoppingCart) ناقص - ClearCart, Merge غایب
|
|
7. ⚠️ UI درخت شبکه (OrganizationChart) با داده Mock
|
|
|
|
**تسکهای واقعی که باید از CMS به FrontOffice.BFF منتقل شوند:**
|
|
- ✅ 26 Command موجود در CMS که در BFF نیستند
|
|
- ✅ 24 Query موجود در CMS که در BFF نیستند
|
|
- ✅ 15+ صفحه UI که باید در FrontOffice ساخته شوند
|
|
|
|
**اولویتبندی توصیه شده:**
|
|
1. **Week 1-2**: ClubMembership - چون بدون این، کاربر نمیتواند عضو شود
|
|
2. **Week 3-5**: Commission + Withdrawal - چون کاربر نمیتواند پول خود را ببیند/برداشت کند
|
|
3. **Week 6-7**: NetworkMembership - چون درخت شبکه Mock است
|
|
4. **Week 8**: UserWallet completion - اضافه کردن DiscountBalance و فیلترها
|
|
5. **Week 9**: DayaLoan + ShoppingCart completion
|
|
|
|
این تحلیل نشان میدهد که **حداقل 50 روز کاری** (2 ماه) برای تکمیل نیاز است.
|
|
|
|
</div>
|
|
|
|
---
|
|
|
|
## 🌳 مرحله 3: راهنمای گامبهگام - NetworkMembership (شبکه باینری)
|
|
|
|
### 📊 خلاصه ماژول
|
|
|
|
**هدف کسبوکار**: مشتری باید بتواند درخت شبکه باینری خود را ببیند (پدر، فرزند چپ، فرزند راست)، موقعیت خود را بررسی کند، و تاریخچه جابجاییها را مشاهده نماید.
|
|
|
|
**اجزای موجود در CMS:**
|
|
- ✅ `NetworkMembership` Entity با BinaryTree structure (ParentId, LeftChildId, RightChildId)
|
|
- ✅ 3 Commands: JoinNetwork, MoveInNetwork, RemoveFromNetwork
|
|
- ✅ 4 Queries: GetNetworkTree, GetUserPosition, GetNetworkHistory, GetNetworkStatistics
|
|
|
|
**چیزهای غایب:**
|
|
- ❌ هیچ Handler در FrontOffice.BFF
|
|
- ❌ هیچ صفحه نمایش درخت در FrontOffice UI
|
|
- ❌ Component نمایش درخت باینری (Tree Visualization)
|
|
|
|
---
|
|
|
|
### 📝 STEP 1: بررسی CMS NetworkMembership
|
|
|
|
#### Task 1.1: بررسی Entity و Logic
|
|
```bash
|
|
cd /home/masoud/Apps/project/FourSat/CMS/src/
|
|
|
|
# 1. بررسی Entity
|
|
cat CMSMicroservice.Domain/Entities/NetworkMembership.cs
|
|
# چیزهایی که باید بفهمی:
|
|
# - UserId: کاربر اصلی
|
|
# - ParentId: کاربر بالایی در شبکه
|
|
# - LeftChildId: فرزند چپ (nullable)
|
|
# - RightChildId: فرزند راست (nullable)
|
|
# - Position: Left/Right (موقعیت در شبکه پدر)
|
|
# - JoinDate: تاریخ پیوستن
|
|
|
|
# 2. بررسی Query GetNetworkTree
|
|
cat CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQueryHandler.cs
|
|
# توجه کن به:
|
|
# - Input: UserId (برای نمایش درخت از این کاربر به بعد)
|
|
# - Depth: عمق درخت (چند لایه)
|
|
# - Output: Recursive DTO (Parent + Left + Right با فیلدهای کامل)
|
|
|
|
# 3. بررسی DTO
|
|
cat CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/NetworkTreeNodeDto.cs
|
|
# Structure:
|
|
# - UserId, UserFullName, UserMobile
|
|
# - Position (Left/Right)
|
|
# - JoinDate
|
|
# - LeftChild (recursive NetworkTreeNodeDto?)
|
|
# - RightChild (recursive NetworkTreeNodeDto?)
|
|
```
|
|
|
|
**Output Task 1.1:**
|
|
```
|
|
[ ] Entity NetworkMembership را خواندم
|
|
[ ] ساختار Recursive Tree را فهمیدم
|
|
[ ] GetNetworkTreeQueryHandler را بررسی کردم
|
|
```
|
|
|
|
#### Task 1.2: بررسی GetUserPosition Query
|
|
```bash
|
|
cat CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetUserPosition/GetUserPositionQueryHandler.cs
|
|
# این Query چه میدهد:
|
|
# - Parent info: نام و موبایل پدر
|
|
# - User Position: Left یا Right
|
|
# - Left Child info (if exists)
|
|
# - Right Child info (if exists)
|
|
# - Total Depth: عمق کل درخت از این کاربر
|
|
```
|
|
|
|
---
|
|
|
|
### 📝 STEP 2: ایجاد BFF Module - NetworkMembershipCQ
|
|
|
|
#### Task 2.1: ساخت فولدرها
|
|
```bash
|
|
cd /home/masoud/Apps/project/FourSat/FrontOffice.BFF/src/FrontOffice.BFF.Application/
|
|
|
|
mkdir -p NetworkMembershipCQ/Queries/GetMyNetworkTree
|
|
mkdir -p NetworkMembershipCQ/Queries/GetMyNetworkPosition
|
|
mkdir -p NetworkMembershipCQ/Queries/GetMyNetworkHistory
|
|
|
|
tree NetworkMembershipCQ/
|
|
```
|
|
|
|
**Expected Output:**
|
|
```
|
|
NetworkMembershipCQ/
|
|
└── Queries/
|
|
├── GetMyNetworkTree/
|
|
├── GetMyNetworkPosition/
|
|
└── GetMyNetworkHistory/
|
|
```
|
|
|
|
#### Task 2.2: Query #1 - GetMyNetworkTree (نمایش درخت)
|
|
|
|
**فایل 1: GetMyNetworkTreeQuery.cs**
|
|
```csharp
|
|
using MediatR;
|
|
|
|
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
|
|
|
|
/// <summary>
|
|
/// Query برای دریافت درخت شبکه کاربر جاری
|
|
/// </summary>
|
|
public record GetMyNetworkTreeQuery : IRequest<MyNetworkTreeResponseDto>
|
|
{
|
|
/// <summary>
|
|
/// عمق درخت (چند لایه زیرمجموعه نمایش داده شود)
|
|
/// پیشفرض: 3 لایه
|
|
/// </summary>
|
|
public int Depth { get; init; } = 3;
|
|
}
|
|
```
|
|
|
|
**فایل 2: MyNetworkTreeResponseDto.cs**
|
|
```csharp
|
|
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
|
|
|
|
/// <summary>
|
|
/// DTO مشتریمحور برای نمایش درخت شبکه
|
|
/// </summary>
|
|
public class MyNetworkTreeResponseDto
|
|
{
|
|
public NetworkNodeDto CurrentUser { get; set; }
|
|
public int TotalNetworkSize { get; set; } // تعداد کل افراد در شبکه
|
|
public int DirectChildrenCount { get; set; } // تعداد فرزندان مستقیم
|
|
public string LastUpdatePersian { get; set; } // آخرین بهروزرسانی
|
|
}
|
|
|
|
/// <summary>
|
|
/// نود درخت (Recursive)
|
|
/// </summary>
|
|
public class NetworkNodeDto
|
|
{
|
|
public long UserId { get; set; }
|
|
public string FullName { get; set; }
|
|
public string Mobile { get; set; }
|
|
public string Position { get; set; } // "Root" / "Left" / "Right"
|
|
public string JoinDatePersian { get; set; }
|
|
public bool HasLeftChild { get; set; }
|
|
public bool HasRightChild { get; set; }
|
|
|
|
// Recursive children
|
|
public NetworkNodeDto LeftChild { get; set; }
|
|
public NetworkNodeDto RightChild { get; set; }
|
|
|
|
// UI Helper fields
|
|
public string StatusBadge { get; set; } // "فعال" / "غیرفعال"
|
|
public string StatusColor { get; set; } // "success" / "error"
|
|
}
|
|
```
|
|
|
|
**فایل 3: GetMyNetworkTreeQueryHandler.cs**
|
|
```csharp
|
|
using MediatR;
|
|
using FrontOffice.BFF.Application.Common.Interfaces;
|
|
|
|
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
|
|
|
|
public class GetMyNetworkTreeQueryHandler
|
|
: IRequestHandler<GetMyNetworkTreeQuery, MyNetworkTreeResponseDto>
|
|
{
|
|
private readonly ICurrentUserService _currentUser;
|
|
// TODO: private readonly NetworkMembershipServiceClient _cmsClient;
|
|
|
|
public GetMyNetworkTreeQueryHandler(ICurrentUserService currentUser)
|
|
{
|
|
_currentUser = currentUser;
|
|
}
|
|
|
|
public async Task<MyNetworkTreeResponseDto> Handle(
|
|
GetMyNetworkTreeQuery request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var userId = _currentUser.UserId;
|
|
|
|
// TODO: فراخوانی CMS
|
|
// var cmsResult = await _cmsClient.GetNetworkTreeAsync(
|
|
// new GetNetworkTreeRequest { UserId = userId, Depth = request.Depth });
|
|
|
|
// Mock Data برای تست UI
|
|
return new MyNetworkTreeResponseDto
|
|
{
|
|
CurrentUser = new NetworkNodeDto
|
|
{
|
|
UserId = userId,
|
|
FullName = "علی احمدی",
|
|
Mobile = "09121234567",
|
|
Position = "Root",
|
|
JoinDatePersian = "1 آذر 1403",
|
|
HasLeftChild = true,
|
|
HasRightChild = true,
|
|
StatusBadge = "فعال",
|
|
StatusColor = "success",
|
|
LeftChild = new NetworkNodeDto
|
|
{
|
|
UserId = 101,
|
|
FullName = "رضا محمدی",
|
|
Mobile = "09129876543",
|
|
Position = "Left",
|
|
JoinDatePersian = "5 آذر 1403",
|
|
HasLeftChild = false,
|
|
HasRightChild = false,
|
|
StatusBadge = "فعال",
|
|
StatusColor = "success"
|
|
},
|
|
RightChild = new NetworkNodeDto
|
|
{
|
|
UserId = 102,
|
|
FullName = "سارا کریمی",
|
|
Mobile = "09131111111",
|
|
Position = "Right",
|
|
JoinDatePersian = "10 آذر 1403",
|
|
HasLeftChild = false,
|
|
HasRightChild = false,
|
|
StatusBadge = "فعال",
|
|
StatusColor = "success"
|
|
}
|
|
},
|
|
TotalNetworkSize = 3,
|
|
DirectChildrenCount = 2,
|
|
LastUpdatePersian = "15 آذر 1403"
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
**Checkpoint Task 2.2:**
|
|
```
|
|
[ ] 3 فایل ایجاد شدند
|
|
[ ] Recursive DTO به درستی تعریف شد
|
|
[ ] Mock tree data با 2 فرزند برمیگردد
|
|
```
|
|
|
|
#### Task 2.3: Query #2 - GetMyNetworkPosition (موقعیت من)
|
|
|
|
**فایل 1: GetMyNetworkPositionQuery.cs**
|
|
```csharp
|
|
using MediatR;
|
|
|
|
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkPosition;
|
|
|
|
public record GetMyNetworkPositionQuery : IRequest<MyNetworkPositionResponseDto>
|
|
{
|
|
}
|
|
```
|
|
|
|
**فایل 2: MyNetworkPositionResponseDto.cs**
|
|
```csharp
|
|
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkPosition;
|
|
|
|
public class MyNetworkPositionResponseDto
|
|
{
|
|
public bool HasParent { get; set; }
|
|
public string ParentFullName { get; set; }
|
|
public string ParentMobile { get; set; }
|
|
public string MyPosition { get; set; } // "چپ" / "راست" / "ریشه"
|
|
public string MyPositionIcon { get; set; } // "arrow_back" / "arrow_forward"
|
|
|
|
public int NetworkLevel { get; set; } // سطح در شبکه (1=ریشه, 2=فرزند, ...)
|
|
public int TotalDownlineCount { get; set; } // تعداد کل زیرمجموعهها
|
|
public string JoinDatePersian { get; set; }
|
|
}
|
|
```
|
|
|
|
**فایل 3: GetMyNetworkPositionQueryHandler.cs** (Mock Data)
|
|
```csharp
|
|
using MediatR;
|
|
using FrontOffice.BFF.Application.Common.Interfaces;
|
|
|
|
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkPosition;
|
|
|
|
public class GetMyNetworkPositionQueryHandler
|
|
: IRequestHandler<GetMyNetworkPositionQuery, MyNetworkPositionResponseDto>
|
|
{
|
|
private readonly ICurrentUserService _currentUser;
|
|
|
|
public GetMyNetworkPositionQueryHandler(ICurrentUserService currentUser)
|
|
{
|
|
_currentUser = currentUser;
|
|
}
|
|
|
|
public async Task<MyNetworkPositionResponseDto> Handle(
|
|
GetMyNetworkPositionQuery request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
// TODO: Call CMS
|
|
return new MyNetworkPositionResponseDto
|
|
{
|
|
HasParent = true,
|
|
ParentFullName = "حسن رضایی",
|
|
ParentMobile = "09123456789",
|
|
MyPosition = "چپ",
|
|
MyPositionIcon = "arrow_back",
|
|
NetworkLevel = 2,
|
|
TotalDownlineCount = 5,
|
|
JoinDatePersian = "1 آذر 1403"
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 📝 STEP 3: اضافه کردن Controller
|
|
|
|
**فایل: NetworkMembershipController.cs**
|
|
```csharp
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using MediatR;
|
|
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
|
|
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkPosition;
|
|
|
|
namespace FrontOffice.BFF.WebApi.Controllers;
|
|
|
|
[Authorize]
|
|
[ApiController]
|
|
[Route("api/[controller]")]
|
|
public class NetworkMembershipController : ControllerBase
|
|
{
|
|
private readonly IMediator _mediator;
|
|
|
|
public NetworkMembershipController(IMediator mediator)
|
|
{
|
|
_mediator = mediator;
|
|
}
|
|
|
|
/// <summary>
|
|
/// دریافت درخت شبکه من
|
|
/// </summary>
|
|
[HttpGet("my-tree")]
|
|
[ProducesResponseType(typeof(MyNetworkTreeResponseDto), 200)]
|
|
public async Task<IActionResult> GetMyTree([FromQuery] int depth = 3)
|
|
{
|
|
var query = new GetMyNetworkTreeQuery { Depth = depth };
|
|
var result = await _mediator.Send(query);
|
|
return Ok(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// دریافت موقعیت من در شبکه
|
|
/// </summary>
|
|
[HttpGet("my-position")]
|
|
[ProducesResponseType(typeof(MyNetworkPositionResponseDto), 200)]
|
|
public async Task<IActionResult> GetMyPosition()
|
|
{
|
|
var query = new GetMyNetworkPositionQuery();
|
|
var result = await _mediator.Send(query);
|
|
return Ok(result);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Test Endpoints:**
|
|
```bash
|
|
# Test 1: Get Tree
|
|
curl -H "Authorization: Bearer TOKEN" \
|
|
"http://localhost:5002/api/networkmembership/my-tree?depth=3"
|
|
|
|
# Test 2: Get Position
|
|
curl -H "Authorization: Bearer TOKEN" \
|
|
http://localhost:5002/api/networkmembership/my-position
|
|
```
|
|
|
|
---
|
|
|
|
### 📝 STEP 4: ایجاد UI - Network Pages
|
|
|
|
#### Task 4.1: Service Layer
|
|
```bash
|
|
cd /home/masoud/Apps/project/FourSat/FrontOffice/src/FrontOffice.Main/Services/
|
|
nano NetworkMembershipService.cs
|
|
```
|
|
|
|
```csharp
|
|
using System.Net.Http.Json;
|
|
using FrontOffice.Main.Models;
|
|
|
|
namespace FrontOffice.Main.Services;
|
|
|
|
public class NetworkMembershipService
|
|
{
|
|
private readonly HttpClient _httpClient;
|
|
|
|
public NetworkMembershipService(HttpClient httpClient)
|
|
{
|
|
_httpClient = httpClient;
|
|
}
|
|
|
|
public async Task<MyNetworkTreeDto> GetMyTreeAsync(int depth = 3)
|
|
{
|
|
var response = await _httpClient.GetAsync($"/api/networkmembership/my-tree?depth={depth}");
|
|
response.EnsureSuccessStatusCode();
|
|
return await response.Content.ReadFromJsonAsync<MyNetworkTreeDto>();
|
|
}
|
|
|
|
public async Task<MyNetworkPositionDto> GetMyPositionAsync()
|
|
{
|
|
var response = await _httpClient.GetAsync("/api/networkmembership/my-position");
|
|
response.EnsureSuccessStatusCode();
|
|
return await response.Content.ReadFromJsonAsync<MyNetworkPositionDto>();
|
|
}
|
|
}
|
|
```
|
|
|
|
**ثبت در Program.cs:**
|
|
```csharp
|
|
builder.Services.AddScoped<NetworkMembershipService>();
|
|
```
|
|
|
|
#### Task 4.2: Models
|
|
```csharp
|
|
// Models/MyNetworkTreeDto.cs
|
|
namespace FrontOffice.Main.Models;
|
|
|
|
public class MyNetworkTreeDto
|
|
{
|
|
public NetworkNodeDto CurrentUser { get; set; }
|
|
public int TotalNetworkSize { get; set; }
|
|
public int DirectChildrenCount { get; set; }
|
|
public string LastUpdatePersian { get; set; }
|
|
}
|
|
|
|
public class NetworkNodeDto
|
|
{
|
|
public long UserId { get; set; }
|
|
public string FullName { get; set; }
|
|
public string Mobile { get; set; }
|
|
public string Position { get; set; }
|
|
public string JoinDatePersian { get; set; }
|
|
public bool HasLeftChild { get; set; }
|
|
public bool HasRightChild { get; set; }
|
|
public NetworkNodeDto LeftChild { get; set; }
|
|
public NetworkNodeDto RightChild { get; set; }
|
|
public string StatusBadge { get; set; }
|
|
public string StatusColor { get; set; }
|
|
}
|
|
|
|
// Models/MyNetworkPositionDto.cs
|
|
public class MyNetworkPositionDto
|
|
{
|
|
public bool HasParent { get; set; }
|
|
public string ParentFullName { get; set; }
|
|
public string ParentMobile { get; set; }
|
|
public string MyPosition { get; set; }
|
|
public string MyPositionIcon { get; set; }
|
|
public int NetworkLevel { get; set; }
|
|
public int TotalDownlineCount { get; set; }
|
|
public string JoinDatePersian { get; set; }
|
|
}
|
|
```
|
|
|
|
#### Task 4.3: Component - NetworkTreeNode (Recursive Component)
|
|
```bash
|
|
cd /home/masoud/Apps/project/FourSat/FrontOffice/src/FrontOffice.Main/Components/Network/
|
|
mkdir -p Network
|
|
nano NetworkTreeNode.razor
|
|
```
|
|
|
|
```razor
|
|
@* Component برای نمایش یک نود درخت (Recursive) *@
|
|
|
|
<MudCard Class="ma-2" Style="min-width: 250px;">
|
|
<MudCardHeader>
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.body1"><strong>@Node.FullName</strong></MudText>
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary">@Node.Mobile</MudText>
|
|
</CardHeaderContent>
|
|
<CardHeaderActions>
|
|
<MudChip Size="Size.Small"
|
|
Color="@(Node.StatusColor == "success" ? Color.Success : Color.Error)">
|
|
@Node.StatusBadge
|
|
</MudChip>
|
|
</CardHeaderActions>
|
|
</MudCardHeader>
|
|
<MudCardContent>
|
|
<MudText Typo="Typo.caption">موقعیت: @Node.Position</MudText>
|
|
<MudText Typo="Typo.caption">تاریخ: @Node.JoinDatePersian</MudText>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
|
|
@if (Node.LeftChild != null || Node.RightChild != null)
|
|
{
|
|
<MudGrid Class="mt-2" Justify="Justify.Center">
|
|
@if (Node.LeftChild != null)
|
|
{
|
|
<MudItem xs="6">
|
|
<div style="border-right: 2px solid #ccc; padding-right: 10px;">
|
|
<MudText Typo="Typo.caption" Color="Color.Primary">← چپ</MudText>
|
|
<NetworkTreeNode Node="@Node.LeftChild" />
|
|
</div>
|
|
</MudItem>
|
|
}
|
|
|
|
@if (Node.RightChild != null)
|
|
{
|
|
<MudItem xs="6">
|
|
<div style="border-left: 2px solid #ccc; padding-left: 10px;">
|
|
<MudText Typo="Typo.caption" Color="Color.Secondary">راست →</MudText>
|
|
<NetworkTreeNode Node="@Node.RightChild" />
|
|
</div>
|
|
</MudItem>
|
|
}
|
|
</MudGrid>
|
|
}
|
|
|
|
@code {
|
|
[Parameter]
|
|
public NetworkNodeDto Node { get; set; }
|
|
}
|
|
```
|
|
|
|
#### Task 4.4: Page - NetworkTreePage
|
|
```bash
|
|
nano /home/masoud/Apps/project/FourSat/FrontOffice/src/FrontOffice.Main/Pages/Network/NetworkTreePage.razor
|
|
```
|
|
|
|
```razor
|
|
@page "/network/tree"
|
|
@inject NetworkMembershipService NetworkService
|
|
@inject ISnackbar Snackbar
|
|
|
|
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
|
<MudText Typo="Typo.h4" Class="mb-4">شبکه باینری من</MudText>
|
|
|
|
@if (_loading)
|
|
{
|
|
<MudProgressLinear Indeterminate="true" Color="Color.Primary" />
|
|
}
|
|
else if (_tree != null)
|
|
{
|
|
<MudGrid>
|
|
<MudItem xs="12" md="4">
|
|
<MudCard>
|
|
<MudCardContent>
|
|
<MudStack Spacing="2">
|
|
<MudText Typo="Typo.h6">آمار کلی</MudText>
|
|
<MudText>
|
|
<strong>تعداد کل اعضا:</strong> @_tree.TotalNetworkSize نفر
|
|
</MudText>
|
|
<MudText>
|
|
<strong>فرزندان مستقیم:</strong> @_tree.DirectChildrenCount نفر
|
|
</MudText>
|
|
<MudText>
|
|
<strong>آخرین بهروزرسانی:</strong> @_tree.LastUpdatePersian
|
|
</MudText>
|
|
</MudStack>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
<MudItem xs="12" md="8">
|
|
<MudText Typo="Typo.h6" Class="mb-2">درخت شبکه</MudText>
|
|
<div style="overflow-x: auto;">
|
|
<NetworkTreeNode Node="@_tree.CurrentUser" />
|
|
</div>
|
|
</MudItem>
|
|
</MudGrid>
|
|
}
|
|
</MudContainer>
|
|
|
|
@code {
|
|
private MyNetworkTreeDto? _tree;
|
|
private bool _loading = true;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await LoadTree();
|
|
}
|
|
|
|
private async Task LoadTree()
|
|
{
|
|
try
|
|
{
|
|
_loading = true;
|
|
_tree = await NetworkService.GetMyTreeAsync(depth: 3);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
_loading = false;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Task 4.5: Page - NetworkPositionPage
|
|
```bash
|
|
nano /home/masoud/Apps/project/FourSat/FrontOffice/src/FrontOffice.Main/Pages/Network/NetworkPositionPage.razor
|
|
```
|
|
|
|
```razor
|
|
@page "/network/position"
|
|
@inject NetworkMembershipService NetworkService
|
|
@inject ISnackbar Snackbar
|
|
|
|
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
|
|
<MudText Typo="Typo.h4" Class="mb-4">موقعیت من در شبکه</MudText>
|
|
|
|
@if (_loading)
|
|
{
|
|
<MudProgressLinear Indeterminate="true" />
|
|
}
|
|
else if (_position != null)
|
|
{
|
|
<MudCard>
|
|
<MudCardContent>
|
|
<MudGrid>
|
|
@if (_position.HasParent)
|
|
{
|
|
<MudItem xs="12" md="6">
|
|
<MudPaper Class="pa-4" Elevation="2">
|
|
<MudText Typo="Typo.h6" Class="mb-2">معرف من</MudText>
|
|
<MudText><strong>نام:</strong> @_position.ParentFullName</MudText>
|
|
<MudText><strong>موبایل:</strong> @_position.ParentMobile</MudText>
|
|
</MudPaper>
|
|
</MudItem>
|
|
}
|
|
|
|
<MudItem xs="12" md="6">
|
|
<MudPaper Class="pa-4" Elevation="2">
|
|
<MudText Typo="Typo.h6" Class="mb-2">موقعیت من</MudText>
|
|
<MudText>
|
|
<MudIcon Icon="@_position.MyPositionIcon" />
|
|
<strong>@_position.MyPosition</strong>
|
|
</MudText>
|
|
<MudText><strong>سطح:</strong> @_position.NetworkLevel</MudText>
|
|
</MudPaper>
|
|
</MudItem>
|
|
|
|
<MudItem xs="12">
|
|
<MudPaper Class="pa-4" Elevation="2">
|
|
<MudText Typo="Typo.h6" Class="mb-2">آمار زیرمجموعه</MudText>
|
|
<MudText>
|
|
<strong>تعداد کل افراد زیر مجموعه:</strong> @_position.TotalDownlineCount نفر
|
|
</MudText>
|
|
<MudText>
|
|
<strong>تاریخ پیوستن:</strong> @_position.JoinDatePersian
|
|
</MudText>
|
|
</MudPaper>
|
|
</MudItem>
|
|
</MudGrid>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
}
|
|
</MudContainer>
|
|
|
|
@code {
|
|
private MyNetworkPositionDto? _position;
|
|
private bool _loading = true;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await LoadPosition();
|
|
}
|
|
|
|
private async Task LoadPosition()
|
|
{
|
|
try
|
|
{
|
|
_loading = true;
|
|
_position = await NetworkService.GetMyPositionAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
_loading = false;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Task 4.6: اضافه کردن به NavMenu
|
|
```razor
|
|
<MudNavGroup Title="شبکه من" Icon="@Icons.Material.Filled.AccountTree">
|
|
<MudNavLink Href="/network/tree" Icon="@Icons.Material.Filled.Schema">
|
|
درخت شبکه
|
|
</MudNavLink>
|
|
<MudNavLink Href="/network/position" Icon="@Icons.Material.Filled.MyLocation">
|
|
موقعیت من
|
|
</MudNavLink>
|
|
</MudNavGroup>
|
|
```
|
|
|
|
---
|
|
|
|
### ✅ Checkpoint نهایی STEP 4
|
|
|
|
```bash
|
|
# Build & Test
|
|
cd /home/masoud/Apps/project/FourSat/FrontOffice.BFF/src/
|
|
dotnet build
|
|
|
|
cd /home/masoud/Apps/project/FourSat/FrontOffice/src/
|
|
dotnet build
|
|
```
|
|
|
|
**چیزهایی که باید کار کنند:**
|
|
```
|
|
[ ] BFF Build میشود (2 Query, 2 Controller endpoints)
|
|
[ ] FrontOffice Build میشود
|
|
[ ] صفحه /network/tree درخت نمایش میدهد
|
|
[ ] صفحه /network/position موقعیت نمایش میدهد
|
|
[ ] Recursive Component به درستی کار میکند
|
|
[ ] Mock data با 2 فرزند نمایش داده میشود
|
|
```
|
|
|
|
---
|
|
|
|
### 📊 آماری از کارهای انجام شده
|
|
|
|
| مورد | تعداد | وضعیت |
|
|
|------|-------|-------|
|
|
| Queries پیاده شده | 2 از 4 | 50% |
|
|
| Commands پیاده شده | 0 از 3 | 0% |
|
|
| Handlers | 2 | Mock Data |
|
|
| Controllers | 1 | 2 Endpoints |
|
|
| UI Pages | 2 | ✅ |
|
|
| UI Components | 1 | Recursive Tree ✅ |
|
|
| Services | 1 | ✅ |
|
|
|
|
**زمان تخمینی تا اینجا:** 5 ساعت
|
|
**کارهای باقیمانده:** GetNetworkHistory Query + اتصال واقعی به CMS
|
|
|
|
---
|
|
|
|
### 💡 نکات مهم برای Developer
|
|
|
|
1. **Recursive Component**: `NetworkTreeNode` به صورت Recursive خودش را صدا میزند - مراقب Performance باش
|
|
2. **Depth Control**: هرگز `depth > 5` نگذار (درخت خیلی بزرگ میشود)
|
|
3. **UI Overflow**: از `overflow-x: auto` برای درختهای بزرگ استفاده شد
|
|
4. **CMS Integration**: بعد از اتصال به CMS، حتماً Handle کن که LeftChild/RightChild ممکنه `null` باشند
|
|
|
|
|
|
---
|
|
|
|
## 💰 مرحله 4: راهنمای گامبهگام - Commission + Withdrawal (کمیسیون و برداشت)
|
|
|
|
### 📊 خلاصه ماژول
|
|
|
|
**هدف کسبوکار**: مشتری باید بتواند کمیسیونهای خود را مشاهده کند، درخواست برداشت بدهد، وضعیت برداشتها را پیگیری کند، و موجودی قابل برداشت خود را ببیند.
|
|
|
|
**اجزای موجود در CMS:**
|
|
- ✅ `CommissionPayout` Entity (مبلغ، هفته، وضعیت، تاریخ)
|
|
- ✅ `WithdrawalRequest` Entity (مبلغ، وضعیت: Pending/Approved/Rejected/Paid)
|
|
- ✅ 8 Commands: RequestWithdrawal, ApproveWithdrawal, RejectWithdrawal, PayWithdrawal, CancelWithdrawal, RecalculateCommission, AdjustBalance, TransferCommission
|
|
- ✅ 8 Queries: GetUserCommissionPayouts, GetUserBalance, GetWithdrawalHistory, GetWeeklyReport, GetPoolShare, GetDownlineCommissions, GetCommissionStatistics, GetAvailableBalance
|
|
|
|
**چیزهای غایب در BFF:**
|
|
- ❌ فقط 10% پیاده شده (GetUserCommissionPayouts Query)
|
|
- ❌ هیچ Command برای RequestWithdrawal
|
|
- ❌ هیچ Query برای موجودی و برداشتها
|
|
|
|
**UI غایب:**
|
|
- ❌ صفحه نمایش کمیسیونها
|
|
- ❌ صفحه درخواست برداشت
|
|
- ❌ صفحه تاریخچه برداشتها
|
|
|
|
---
|
|
|
|
### 📝 STEP 1: بررسی CMS Commission Module
|
|
|
|
#### Task 1.1: بررسی Entities
|
|
```bash
|
|
cd /home/masoud/Apps/project/FourSat/CMS/src/
|
|
|
|
# 1. بررسی CommissionPayout Entity
|
|
cat CMSMicroservice.Domain/Entities/CommissionPayout.cs
|
|
# فیلدهای کلیدی:
|
|
# - UserId: کاربر دریافتکننده
|
|
# - Amount: مبلغ کمیسیون (decimal)
|
|
# - WeekNumber: شماره هفته
|
|
# - PayoutDate: تاریخ پرداخت
|
|
# - Status: Pending/Calculated/Paid
|
|
# - PayoutType: Direct/Binary/Pool/Club
|
|
|
|
# 2. بررسی WithdrawalRequest Entity
|
|
cat CMSMicroservice.Domain/Entities/WithdrawalRequest.cs
|
|
# فیلدهای کلیدی:
|
|
# - UserId: کاربر درخواستدهنده
|
|
# - Amount: مبلغ درخواستی
|
|
# - Status: Pending/Approved/Rejected/Paid/Cancelled
|
|
# - RequestDate: تاریخ درخواست
|
|
# - ProcessDate: تاریخ پردازش
|
|
# - BankAccountInfo: اطلاعات حساب (شماره کارت/شبا)
|
|
# - RejectReason: دلیل رد (اگر رد شده)
|
|
|
|
# 3. بررسی UserBalance (موجودی)
|
|
cat CMSMicroservice.Domain/Entities/UserBalance.cs
|
|
# فیلدها:
|
|
# - UserId
|
|
# - CommissionBalance: موجودی کمیسیون
|
|
# - WithdrawableBalance: قابل برداشت
|
|
# - PendingWithdrawal: در انتظار برداشت
|
|
# - TotalEarned: کل درآمد
|
|
```
|
|
|
|
**Output Task 1.1:**
|
|
```
|
|
[ ] CommissionPayout Entity را خواندم
|
|
[ ] WithdrawalRequest Entity را خواندم
|
|
[ ] UserBalance Entity را خواندم
|
|
[ ] Status enums را یادداشت کردم
|
|
```
|
|
|
|
#### Task 1.2: بررسی Queries موجود در CMS
|
|
```bash
|
|
# Query 1: GetUserCommissionPayouts
|
|
cat CMSMicroservice.Application/CommissionCQ/Queries/GetUserCommissionPayouts/GetUserCommissionPayoutsQueryHandler.cs
|
|
# Input: UserId, FromDate, ToDate, PageNumber, PageSize
|
|
# Output: List<CommissionPayoutDto> + TotalCount
|
|
|
|
# Query 2: GetUserBalance
|
|
cat CMSMicroservice.Application/CommissionCQ/Queries/GetUserBalance/GetUserBalanceQueryHandler.cs
|
|
# Input: UserId
|
|
# Output: CommissionBalance, WithdrawableBalance, PendingWithdrawal, TotalEarned
|
|
|
|
# Query 3: GetWithdrawalHistory
|
|
cat CMSMicroservice.Application/CommissionCQ/Queries/GetWithdrawalHistory/GetWithdrawalHistoryQueryHandler.cs
|
|
# Input: UserId, FromDate, ToDate, Status (optional)
|
|
# Output: List<WithdrawalRequestDto>
|
|
|
|
# Query 4: GetAvailableBalance
|
|
cat CMSMicroservice.Application/CommissionCQ/Queries/GetAvailableBalance/GetAvailableBalanceQueryHandler.cs
|
|
# Input: UserId
|
|
# Output: AvailableAmount, MinWithdrawalAmount, MaxWithdrawalAmount
|
|
```
|
|
|
|
#### Task 1.3: بررسی Command RequestWithdrawal
|
|
```bash
|
|
cat CMSMicroservice.Application/CommissionCQ/Commands/RequestWithdrawal/RequestWithdrawalCommandHandler.cs
|
|
# Input:
|
|
# - UserId
|
|
# - Amount
|
|
# - BankAccountNumber (شماره کارت/شبا)
|
|
# Logic:
|
|
# 1. بررسی موجودی کافی
|
|
# 2. بررسی حداقل/حداکثر مبلغ
|
|
# 3. ایجاد WithdrawalRequest
|
|
# 4. کسر از WithdrawableBalance
|
|
# 5. اضافه به PendingWithdrawal
|
|
# Output: WithdrawalRequestId
|
|
```
|
|
|
|
---
|
|
|
|
### 📝 STEP 2: ایجاد BFF Module - CommissionCQ
|
|
|
|
#### Task 2.1: ساخت فولدرها
|
|
```bash
|
|
cd /home/masoud/Apps/project/FourSat/FrontOffice.BFF/src/FrontOffice.BFF.Application/
|
|
|
|
mkdir -p CommissionCQ/Queries/GetMyCommissionPayouts
|
|
mkdir -p CommissionCQ/Queries/GetMyBalance
|
|
mkdir -p CommissionCQ/Queries/GetMyWithdrawalHistory
|
|
mkdir -p CommissionCQ/Commands/RequestMyWithdrawal
|
|
|
|
tree CommissionCQ/
|
|
```
|
|
|
|
**Expected Output:**
|
|
```
|
|
CommissionCQ/
|
|
├── Commands/
|
|
│ └── RequestMyWithdrawal/
|
|
└── Queries/
|
|
├── GetMyCommissionPayouts/
|
|
├── GetMyBalance/
|
|
└── GetMyWithdrawalHistory/
|
|
```
|
|
|
|
#### Task 2.2: Query #1 - GetMyCommissionPayouts
|
|
|
|
**فایل 1: GetMyCommissionPayoutsQuery.cs**
|
|
```csharp
|
|
using MediatR;
|
|
|
|
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
|
|
|
|
public record GetMyCommissionPayoutsQuery : IRequest<MyCommissionPayoutsResponseDto>
|
|
{
|
|
/// <summary>
|
|
/// تعداد آیتم در هر صفحه (پیشفرض: 10)
|
|
/// </summary>
|
|
public int PageSize { get; init; } = 10;
|
|
|
|
/// <summary>
|
|
/// شماره صفحه (پیشفرض: 1)
|
|
/// </summary>
|
|
public int PageNumber { get; init; } = 1;
|
|
|
|
/// <summary>
|
|
/// فیلتر بر اساس نوع کمیسیون (اختیاری)
|
|
/// </summary>
|
|
public string PayoutType { get; init; }
|
|
}
|
|
```
|
|
|
|
**فایل 2: MyCommissionPayoutsResponseDto.cs**
|
|
```csharp
|
|
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
|
|
|
|
public class MyCommissionPayoutsResponseDto
|
|
{
|
|
public List<CommissionPayoutItemDto> Payouts { get; set; }
|
|
public int TotalCount { get; set; }
|
|
public int CurrentPage { get; set; }
|
|
public int TotalPages { get; set; }
|
|
public decimal TotalAmount { get; set; } // مجموع کل کمیسیونها
|
|
}
|
|
|
|
public class CommissionPayoutItemDto
|
|
{
|
|
public long Id { get; set; }
|
|
public string WeekDisplay { get; set; } // "هفته 48 - سال 1403"
|
|
public decimal Amount { get; set; }
|
|
public string AmountFormatted { get; set; } // "1,250,000 تومان"
|
|
public string PayoutType { get; set; } // "مستقیم" / "باینری" / "پول" / "باشگاه"
|
|
public string PayoutTypeIcon { get; set; } // Icon name for UI
|
|
public string Status { get; set; } // "در انتظار" / "محاسبه شده" / "پرداخت شده"
|
|
public string StatusColor { get; set; } // "warning" / "info" / "success"
|
|
public string PayoutDatePersian { get; set; }
|
|
}
|
|
```
|
|
|
|
**فایل 3: GetMyCommissionPayoutsQueryHandler.cs**
|
|
```csharp
|
|
using MediatR;
|
|
using FrontOffice.BFF.Application.Common.Interfaces;
|
|
|
|
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
|
|
|
|
public class GetMyCommissionPayoutsQueryHandler
|
|
: IRequestHandler<GetMyCommissionPayoutsQuery, MyCommissionPayoutsResponseDto>
|
|
{
|
|
private readonly ICurrentUserService _currentUser;
|
|
// TODO: private readonly CommissionServiceClient _cmsClient;
|
|
|
|
public GetMyCommissionPayoutsQueryHandler(ICurrentUserService currentUser)
|
|
{
|
|
_currentUser = currentUser;
|
|
}
|
|
|
|
public async Task<MyCommissionPayoutsResponseDto> Handle(
|
|
GetMyCommissionPayoutsQuery request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var userId = _currentUser.UserId;
|
|
|
|
// TODO: فراخوانی CMS
|
|
// var cmsResult = await _cmsClient.GetUserCommissionPayoutsAsync(
|
|
// new GetUserCommissionPayoutsRequest {
|
|
// UserId = userId,
|
|
// PageNumber = request.PageNumber,
|
|
// PageSize = request.PageSize
|
|
// });
|
|
|
|
// Mock Data
|
|
return new MyCommissionPayoutsResponseDto
|
|
{
|
|
Payouts = new List<CommissionPayoutItemDto>
|
|
{
|
|
new() {
|
|
Id = 1,
|
|
WeekDisplay = "هفته 48 - سال 1403",
|
|
Amount = 1250000,
|
|
AmountFormatted = "1,250,000 تومان",
|
|
PayoutType = "مستقیم",
|
|
PayoutTypeIcon = "trending_up",
|
|
Status = "پرداخت شده",
|
|
StatusColor = "success",
|
|
PayoutDatePersian = "20 آذر 1403"
|
|
},
|
|
new() {
|
|
Id = 2,
|
|
WeekDisplay = "هفته 47 - سال 1403",
|
|
Amount = 850000,
|
|
AmountFormatted = "850,000 تومان",
|
|
PayoutType = "باینری",
|
|
PayoutTypeIcon = "account_tree",
|
|
Status = "پرداخت شده",
|
|
StatusColor = "success",
|
|
PayoutDatePersian = "13 آذر 1403"
|
|
}
|
|
},
|
|
TotalCount = 2,
|
|
CurrentPage = 1,
|
|
TotalPages = 1,
|
|
TotalAmount = 2100000
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Task 2.3: Query #2 - GetMyBalance (موجودی)
|
|
|
|
**فایل 1: GetMyBalanceQuery.cs**
|
|
```csharp
|
|
using MediatR;
|
|
|
|
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyBalance;
|
|
|
|
public record GetMyBalanceQuery : IRequest<MyBalanceResponseDto>
|
|
{
|
|
}
|
|
```
|
|
|
|
**فایل 2: MyBalanceResponseDto.cs**
|
|
```csharp
|
|
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyBalance;
|
|
|
|
public class MyBalanceResponseDto
|
|
{
|
|
public decimal TotalEarned { get; set; } // کل درآمد تاکنون
|
|
public string TotalEarnedFormatted { get; set; }
|
|
|
|
public decimal CurrentBalance { get; set; } // موجودی فعلی
|
|
public string CurrentBalanceFormatted { get; set; }
|
|
|
|
public decimal WithdrawableBalance { get; set; } // قابل برداشت
|
|
public string WithdrawableBalanceFormatted { get; set; }
|
|
|
|
public decimal PendingWithdrawal { get; set; } // در انتظار برداشت
|
|
public string PendingWithdrawalFormatted { get; set; }
|
|
|
|
public bool CanRequestWithdrawal { get; set; } // آیا میتواند برداشت کند؟
|
|
public string MinWithdrawalAmount { get; set; } // حداقل مبلغ برداشت
|
|
public string MaxWithdrawalAmount { get; set; } // حداکثر مبلغ برداشت
|
|
}
|
|
```
|
|
|
|
**فایل 3: GetMyBalanceQueryHandler.cs**
|
|
```csharp
|
|
using MediatR;
|
|
using FrontOffice.BFF.Application.Common.Interfaces;
|
|
|
|
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyBalance;
|
|
|
|
public class GetMyBalanceQueryHandler
|
|
: IRequestHandler<GetMyBalanceQuery, MyBalanceResponseDto>
|
|
{
|
|
private readonly ICurrentUserService _currentUser;
|
|
|
|
public GetMyBalanceQueryHandler(ICurrentUserService currentUser)
|
|
{
|
|
_currentUser = currentUser;
|
|
}
|
|
|
|
public async Task<MyBalanceResponseDto> Handle(
|
|
GetMyBalanceQuery request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var userId = _currentUser.UserId;
|
|
|
|
// TODO: Call CMS
|
|
|
|
// Mock Data
|
|
return new MyBalanceResponseDto
|
|
{
|
|
TotalEarned = 15750000,
|
|
TotalEarnedFormatted = "15,750,000 تومان",
|
|
CurrentBalance = 8500000,
|
|
CurrentBalanceFormatted = "8,500,000 تومان",
|
|
WithdrawableBalance = 7000000,
|
|
WithdrawableBalanceFormatted = "7,000,000 تومان",
|
|
PendingWithdrawal = 1500000,
|
|
PendingWithdrawalFormatted = "1,500,000 تومان",
|
|
CanRequestWithdrawal = true,
|
|
MinWithdrawalAmount = "100,000 تومان",
|
|
MaxWithdrawalAmount = "7,000,000 تومان"
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Task 2.4: Query #3 - GetMyWithdrawalHistory
|
|
|
|
**فایل 1: GetMyWithdrawalHistoryQuery.cs**
|
|
```csharp
|
|
using MediatR;
|
|
|
|
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyWithdrawalHistory;
|
|
|
|
public record GetMyWithdrawalHistoryQuery : IRequest<MyWithdrawalHistoryResponseDto>
|
|
{
|
|
public int PageSize { get; init; } = 10;
|
|
public int PageNumber { get; init; } = 1;
|
|
}
|
|
```
|
|
|
|
**فایل 2: MyWithdrawalHistoryResponseDto.cs**
|
|
```csharp
|
|
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyWithdrawalHistory;
|
|
|
|
public class MyWithdrawalHistoryResponseDto
|
|
{
|
|
public List<WithdrawalItemDto> Withdrawals { get; set; }
|
|
public int TotalCount { get; set; }
|
|
}
|
|
|
|
public class WithdrawalItemDto
|
|
{
|
|
public long Id { get; set; }
|
|
public decimal Amount { get; set; }
|
|
public string AmountFormatted { get; set; }
|
|
public string Status { get; set; } // "در انتظار" / "تایید" / "رد" / "پرداخت شده"
|
|
public string StatusColor { get; set; } // "warning" / "success" / "error" / "info"
|
|
public string RequestDatePersian { get; set; }
|
|
public string ProcessDatePersian { get; set; }
|
|
public string BankAccount { get; set; } // "6037-****-****-1234"
|
|
public string RejectReason { get; set; } // دلیل رد (اگر رد شده)
|
|
}
|
|
```
|
|
|
|
**فایل 3: GetMyWithdrawalHistoryQueryHandler.cs**
|
|
```csharp
|
|
using MediatR;
|
|
using FrontOffice.BFF.Application.Common.Interfaces;
|
|
|
|
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyWithdrawalHistory;
|
|
|
|
public class GetMyWithdrawalHistoryQueryHandler
|
|
: IRequestHandler<GetMyWithdrawalHistoryQuery, MyWithdrawalHistoryResponseDto>
|
|
{
|
|
private readonly ICurrentUserService _currentUser;
|
|
|
|
public GetMyWithdrawalHistoryQueryHandler(ICurrentUserService currentUser)
|
|
{
|
|
_currentUser = currentUser;
|
|
}
|
|
|
|
public async Task<MyWithdrawalHistoryResponseDto> Handle(
|
|
GetMyWithdrawalHistoryQuery request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
// TODO: Call CMS
|
|
|
|
return new MyWithdrawalHistoryResponseDto
|
|
{
|
|
Withdrawals = new List<WithdrawalItemDto>
|
|
{
|
|
new() {
|
|
Id = 1,
|
|
Amount = 1500000,
|
|
AmountFormatted = "1,500,000 تومان",
|
|
Status = "در انتظار",
|
|
StatusColor = "warning",
|
|
RequestDatePersian = "25 آذر 1403",
|
|
ProcessDatePersian = "-",
|
|
BankAccount = "6037-****-****-1234"
|
|
},
|
|
new() {
|
|
Id = 2,
|
|
Amount = 2000000,
|
|
AmountFormatted = "2,000,000 تومان",
|
|
Status = "پرداخت شده",
|
|
StatusColor = "success",
|
|
RequestDatePersian = "15 آذر 1403",
|
|
ProcessDatePersian = "18 آذر 1403",
|
|
BankAccount = "6037-****-****-1234"
|
|
}
|
|
},
|
|
TotalCount = 2
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Task 2.5: Command - RequestMyWithdrawal
|
|
|
|
**فایل 1: RequestMyWithdrawalCommand.cs**
|
|
```csharp
|
|
using MediatR;
|
|
|
|
namespace FrontOffice.BFF.Application.CommissionCQ.Commands.RequestMyWithdrawal;
|
|
|
|
public record RequestMyWithdrawalCommand : IRequest<RequestMyWithdrawalResponseDto>
|
|
{
|
|
public decimal Amount { get; init; }
|
|
public string BankAccountNumber { get; init; } // شماره کارت یا شبا
|
|
}
|
|
```
|
|
|
|
**فایل 2: RequestMyWithdrawalResponseDto.cs**
|
|
```csharp
|
|
namespace FrontOffice.BFF.Application.CommissionCQ.Commands.RequestMyWithdrawal;
|
|
|
|
public class RequestMyWithdrawalResponseDto
|
|
{
|
|
public bool Success { get; set; }
|
|
public long WithdrawalRequestId { get; set; }
|
|
public string Message { get; set; } // "درخواست شما با موفقیت ثبت شد"
|
|
public string NewWithdrawableBalance { get; set; } // موجودی جدید قابل برداشت
|
|
}
|
|
```
|
|
|
|
**فایل 3: RequestMyWithdrawalCommandHandler.cs**
|
|
```csharp
|
|
using MediatR;
|
|
using FrontOffice.BFF.Application.Common.Interfaces;
|
|
|
|
namespace FrontOffice.BFF.Application.CommissionCQ.Commands.RequestMyWithdrawal;
|
|
|
|
public class RequestMyWithdrawalCommandHandler
|
|
: IRequestHandler<RequestMyWithdrawalCommand, RequestMyWithdrawalResponseDto>
|
|
{
|
|
private readonly ICurrentUserService _currentUser;
|
|
// TODO: private readonly CommissionServiceClient _cmsClient;
|
|
|
|
public RequestMyWithdrawalCommandHandler(ICurrentUserService currentUser)
|
|
{
|
|
_currentUser = currentUser;
|
|
}
|
|
|
|
public async Task<RequestMyWithdrawalResponseDto> Handle(
|
|
RequestMyWithdrawalCommand request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var userId = _currentUser.UserId;
|
|
|
|
// TODO: فراخوانی CMS
|
|
// var cmsResult = await _cmsClient.RequestWithdrawalAsync(
|
|
// new RequestWithdrawalRequest {
|
|
// UserId = userId,
|
|
// Amount = request.Amount,
|
|
// BankAccountNumber = request.BankAccountNumber
|
|
// });
|
|
|
|
// Mock Response
|
|
return new RequestMyWithdrawalResponseDto
|
|
{
|
|
Success = true,
|
|
WithdrawalRequestId = 123,
|
|
Message = "درخواست برداشت شما با موفقیت ثبت شد و در انتظار تایید است.",
|
|
NewWithdrawableBalance = "5,500,000 تومان"
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
**فایل 4: RequestMyWithdrawalCommandValidator.cs**
|
|
```csharp
|
|
using FluentValidation;
|
|
|
|
namespace FrontOffice.BFF.Application.CommissionCQ.Commands.RequestMyWithdrawal;
|
|
|
|
public class RequestMyWithdrawalCommandValidator : AbstractValidator<RequestMyWithdrawalCommand>
|
|
{
|
|
public RequestMyWithdrawalCommandValidator()
|
|
{
|
|
RuleFor(x => x.Amount)
|
|
.GreaterThan(0).WithMessage("مبلغ باید بیشتر از صفر باشد")
|
|
.LessThanOrEqualTo(50000000).WithMessage("حداکثر مبلغ برداشت 50 میلیون تومان است");
|
|
|
|
RuleFor(x => x.BankAccountNumber)
|
|
.NotEmpty().WithMessage("شماره کارت الزامی است")
|
|
.Length(16, 24).WithMessage("شماره کارت یا شبا نامعتبر است");
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 📝 STEP 3: اضافه کردن Controller
|
|
|
|
**فایل: CommissionController.cs**
|
|
```csharp
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using MediatR;
|
|
using FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
|
|
using FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyBalance;
|
|
using FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyWithdrawalHistory;
|
|
using FrontOffice.BFF.Application.CommissionCQ.Commands.RequestMyWithdrawal;
|
|
|
|
namespace FrontOffice.BFF.WebApi.Controllers;
|
|
|
|
[Authorize]
|
|
[ApiController]
|
|
[Route("api/[controller]")]
|
|
public class CommissionController : ControllerBase
|
|
{
|
|
private readonly IMediator _mediator;
|
|
|
|
public CommissionController(IMediator mediator)
|
|
{
|
|
_mediator = mediator;
|
|
}
|
|
|
|
/// <summary>
|
|
/// دریافت لیست کمیسیونهای من
|
|
/// </summary>
|
|
[HttpGet("my-payouts")]
|
|
[ProducesResponseType(typeof(MyCommissionPayoutsResponseDto), 200)]
|
|
public async Task<IActionResult> GetMyPayouts(
|
|
[FromQuery] int pageNumber = 1,
|
|
[FromQuery] int pageSize = 10)
|
|
{
|
|
var query = new GetMyCommissionPayoutsQuery
|
|
{
|
|
PageNumber = pageNumber,
|
|
PageSize = pageSize
|
|
};
|
|
var result = await _mediator.Send(query);
|
|
return Ok(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// دریافت موجودی من
|
|
/// </summary>
|
|
[HttpGet("my-balance")]
|
|
[ProducesResponseType(typeof(MyBalanceResponseDto), 200)]
|
|
public async Task<IActionResult> GetMyBalance()
|
|
{
|
|
var query = new GetMyBalanceQuery();
|
|
var result = await _mediator.Send(query);
|
|
return Ok(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// دریافت تاریخچه برداشتهای من
|
|
/// </summary>
|
|
[HttpGet("my-withdrawal-history")]
|
|
[ProducesResponseType(typeof(MyWithdrawalHistoryResponseDto), 200)]
|
|
public async Task<IActionResult> GetMyWithdrawalHistory(
|
|
[FromQuery] int pageNumber = 1,
|
|
[FromQuery] int pageSize = 10)
|
|
{
|
|
var query = new GetMyWithdrawalHistoryQuery
|
|
{
|
|
PageNumber = pageNumber,
|
|
PageSize = pageSize
|
|
};
|
|
var result = await _mediator.Send(query);
|
|
return Ok(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// درخواست برداشت
|
|
/// </summary>
|
|
[HttpPost("request-withdrawal")]
|
|
[ProducesResponseType(typeof(RequestMyWithdrawalResponseDto), 200)]
|
|
public async Task<IActionResult> RequestWithdrawal(
|
|
[FromBody] RequestMyWithdrawalCommand command)
|
|
{
|
|
var result = await _mediator.Send(command);
|
|
return Ok(result);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Test Endpoints:**
|
|
```bash
|
|
# Test 1: Get Payouts
|
|
curl -H "Authorization: Bearer TOKEN" \
|
|
"http://localhost:5002/api/commission/my-payouts?pageNumber=1&pageSize=10"
|
|
|
|
# Test 2: Get Balance
|
|
curl -H "Authorization: Bearer TOKEN" \
|
|
http://localhost:5002/api/commission/my-balance
|
|
|
|
# Test 3: Get Withdrawal History
|
|
curl -H "Authorization: Bearer TOKEN" \
|
|
http://localhost:5002/api/commission/my-withdrawal-history
|
|
|
|
# Test 4: Request Withdrawal
|
|
curl -X POST \
|
|
-H "Authorization: Bearer TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"amount": 1500000, "bankAccountNumber": "6037997012345678"}' \
|
|
http://localhost:5002/api/commission/request-withdrawal
|
|
```
|
|
|
|
---
|
|
|
|
### 📝 STEP 4: ایجاد UI - Commission Pages
|
|
|
|
#### Task 4.1: Service Layer
|
|
```bash
|
|
cd /home/masoud/Apps/project/FourSat/FrontOffice/src/FrontOffice.Main/Services/
|
|
nano CommissionService.cs
|
|
```
|
|
|
|
```csharp
|
|
using System.Net.Http.Json;
|
|
using FrontOffice.Main.Models;
|
|
|
|
namespace FrontOffice.Main.Services;
|
|
|
|
public class CommissionService
|
|
{
|
|
private readonly HttpClient _httpClient;
|
|
|
|
public CommissionService(HttpClient httpClient)
|
|
{
|
|
_httpClient = httpClient;
|
|
}
|
|
|
|
public async Task<MyCommissionPayoutsDto> GetMyPayoutsAsync(int pageNumber = 1, int pageSize = 10)
|
|
{
|
|
var response = await _httpClient.GetAsync(
|
|
$"/api/commission/my-payouts?pageNumber={pageNumber}&pageSize={pageSize}");
|
|
response.EnsureSuccessStatusCode();
|
|
return await response.Content.ReadFromJsonAsync<MyCommissionPayoutsDto>();
|
|
}
|
|
|
|
public async Task<MyBalanceDto> GetMyBalanceAsync()
|
|
{
|
|
var response = await _httpClient.GetAsync("/api/commission/my-balance");
|
|
response.EnsureSuccessStatusCode();
|
|
return await response.Content.ReadFromJsonAsync<MyBalanceDto>();
|
|
}
|
|
|
|
public async Task<MyWithdrawalHistoryDto> GetMyWithdrawalHistoryAsync(int pageNumber = 1)
|
|
{
|
|
var response = await _httpClient.GetAsync(
|
|
$"/api/commission/my-withdrawal-history?pageNumber={pageNumber}");
|
|
response.EnsureSuccessStatusCode();
|
|
return await response.Content.ReadFromJsonAsync<MyWithdrawalHistoryDto>();
|
|
}
|
|
|
|
public async Task<RequestWithdrawalResultDto> RequestWithdrawalAsync(decimal amount, string bankAccount)
|
|
{
|
|
var request = new { Amount = amount, BankAccountNumber = bankAccount };
|
|
var response = await _httpClient.PostAsJsonAsync("/api/commission/request-withdrawal", request);
|
|
response.EnsureSuccessStatusCode();
|
|
return await response.Content.ReadFromJsonAsync<RequestWithdrawalResultDto>();
|
|
}
|
|
}
|
|
```
|
|
|
|
**ثبت در Program.cs:**
|
|
```csharp
|
|
builder.Services.AddScoped<CommissionService>();
|
|
```
|
|
|
|
#### Task 4.2: Models (در فولدر Models/)
|
|
```csharp
|
|
// کپی DTOها از BFF به FrontOffice.Main/Models/
|
|
// MyCommissionPayoutsDto.cs
|
|
// MyBalanceDto.cs
|
|
// MyWithdrawalHistoryDto.cs
|
|
// RequestWithdrawalResultDto.cs
|
|
```
|
|
|
|
#### Task 4.3: Page - CommissionPayoutsPage (صفحه کمیسیونها)
|
|
```razor
|
|
@page "/commission/payouts"
|
|
@inject CommissionService CommissionService
|
|
@inject ISnackbar Snackbar
|
|
|
|
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
|
|
<MudText Typo="Typo.h4" Class="mb-4">کمیسیونهای من</MudText>
|
|
|
|
@if (_loading)
|
|
{
|
|
<MudProgressLinear Indeterminate="true" />
|
|
}
|
|
else if (_payouts != null)
|
|
{
|
|
<MudCard Class="mb-4">
|
|
<MudCardContent>
|
|
<MudText Typo="Typo.h6">
|
|
مجموع کل: <strong style="color: green;">@_payouts.TotalAmount.ToString("N0") تومان</strong>
|
|
</MudText>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
|
|
<MudTable Items="@_payouts.Payouts" Hover="true" Striped="true">
|
|
<HeaderContent>
|
|
<MudTh>هفته</MudTh>
|
|
<MudTh>نوع</MudTh>
|
|
<MudTh>مبلغ</MudTh>
|
|
<MudTh>وضعیت</MudTh>
|
|
<MudTh>تاریخ پرداخت</MudTh>
|
|
</HeaderContent>
|
|
<RowTemplate>
|
|
<MudTd>@context.WeekDisplay</MudTd>
|
|
<MudTd>
|
|
<MudIcon Icon="@context.PayoutTypeIcon" Size="Size.Small" />
|
|
@context.PayoutType
|
|
</MudTd>
|
|
<MudTd><strong>@context.AmountFormatted</strong></MudTd>
|
|
<MudTd>
|
|
<MudChip Size="Size.Small" Color="@GetStatusColor(context.StatusColor)">
|
|
@context.Status
|
|
</MudChip>
|
|
</MudTd>
|
|
<MudTd>@context.PayoutDatePersian</MudTd>
|
|
</RowTemplate>
|
|
</MudTable>
|
|
|
|
<MudPagination Class="mt-4"
|
|
Count="_payouts.TotalPages"
|
|
Selected="_currentPage"
|
|
SelectedChanged="OnPageChanged" />
|
|
}
|
|
</MudContainer>
|
|
|
|
@code {
|
|
private MyCommissionPayoutsDto? _payouts;
|
|
private bool _loading = true;
|
|
private int _currentPage = 1;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await LoadPayouts();
|
|
}
|
|
|
|
private async Task LoadPayouts()
|
|
{
|
|
try
|
|
{
|
|
_loading = true;
|
|
_payouts = await CommissionService.GetMyPayoutsAsync(_currentPage, 10);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
_loading = false;
|
|
}
|
|
}
|
|
|
|
private async Task OnPageChanged(int page)
|
|
{
|
|
_currentPage = page;
|
|
await LoadPayouts();
|
|
}
|
|
|
|
private Color GetStatusColor(string color)
|
|
{
|
|
return color switch
|
|
{
|
|
"success" => Color.Success,
|
|
"warning" => Color.Warning,
|
|
"error" => Color.Error,
|
|
"info" => Color.Info,
|
|
_ => Color.Default
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Task 4.4: Page - WithdrawalPage (صفحه برداشت)
|
|
```razor
|
|
@page "/commission/withdrawal"
|
|
@inject CommissionService CommissionService
|
|
@inject ISnackbar Snackbar
|
|
|
|
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
|
|
<MudText Typo="Typo.h4" Class="mb-4">برداشت وجه</MudText>
|
|
|
|
@if (_loadingBalance)
|
|
{
|
|
<MudProgressLinear Indeterminate="true" />
|
|
}
|
|
else if (_balance != null)
|
|
{
|
|
<MudGrid>
|
|
<MudItem xs="12" md="6">
|
|
<MudCard Class="mb-4">
|
|
<MudCardHeader>
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.h6">موجودی من</MudText>
|
|
</CardHeaderContent>
|
|
</MudCardHeader>
|
|
<MudCardContent>
|
|
<MudStack Spacing="2">
|
|
<MudText>
|
|
<strong>کل درآمد:</strong>
|
|
<span style="color: blue;">@_balance.TotalEarnedFormatted</span>
|
|
</MudText>
|
|
<MudText>
|
|
<strong>موجودی فعلی:</strong>
|
|
<span style="color: green;">@_balance.CurrentBalanceFormatted</span>
|
|
</MudText>
|
|
<MudText>
|
|
<strong>قابل برداشت:</strong>
|
|
<span style="color: orange; font-size: 1.2em;">@_balance.WithdrawableBalanceFormatted</span>
|
|
</MudText>
|
|
<MudText>
|
|
<strong>در انتظار برداشت:</strong> @_balance.PendingWithdrawalFormatted
|
|
</MudText>
|
|
</MudStack>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
<MudItem xs="12" md="6">
|
|
<MudCard>
|
|
<MudCardHeader>
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.h6">درخواست برداشت جدید</MudText>
|
|
</CardHeaderContent>
|
|
</MudCardHeader>
|
|
<MudCardContent>
|
|
<MudForm @ref="_form">
|
|
<MudNumericField @bind-Value="_withdrawalAmount"
|
|
Label="مبلغ (تومان)"
|
|
Variant="Variant.Outlined"
|
|
Required="true"
|
|
Min="100000"
|
|
Max="_balance.WithdrawableBalance" />
|
|
|
|
<MudTextField @bind-Value="_bankAccount"
|
|
Label="شماره کارت یا شبا"
|
|
Variant="Variant.Outlined"
|
|
Required="true"
|
|
MaxLength="24"
|
|
Class="mt-3" />
|
|
|
|
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="mt-2">
|
|
حداقل: @_balance.MinWithdrawalAmount | حداکثر: @_balance.MaxWithdrawalAmount
|
|
</MudText>
|
|
</MudForm>
|
|
</MudCardContent>
|
|
<MudCardActions>
|
|
<MudButton Variant="Variant.Filled"
|
|
Color="Color.Primary"
|
|
Disabled="@(!_balance.CanRequestWithdrawal || _submitting)"
|
|
OnClick="SubmitWithdrawal">
|
|
@if (_submitting)
|
|
{
|
|
<MudProgressCircular Size="Size.Small" Indeterminate="true" />
|
|
<span class="ms-2">در حال ارسال...</span>
|
|
}
|
|
else
|
|
{
|
|
<span>ثبت درخواست</span>
|
|
}
|
|
</MudButton>
|
|
</MudCardActions>
|
|
</MudCard>
|
|
</MudItem>
|
|
</MudGrid>
|
|
|
|
<MudText Typo="Typo.h5" Class="mt-6 mb-3">تاریخچه برداشتها</MudText>
|
|
|
|
@if (_loadingHistory)
|
|
{
|
|
<MudProgressLinear Indeterminate="true" />
|
|
}
|
|
else if (_history != null)
|
|
{
|
|
<MudTable Items="@_history.Withdrawals" Hover="true">
|
|
<HeaderContent>
|
|
<MudTh>مبلغ</MudTh>
|
|
<MudTh>وضعیت</MudTh>
|
|
<MudTh>تاریخ درخواست</MudTh>
|
|
<MudTh>تاریخ پردازش</MudTh>
|
|
<MudTh>شماره کارت</MudTh>
|
|
</HeaderContent>
|
|
<RowTemplate>
|
|
<MudTd><strong>@context.AmountFormatted</strong></MudTd>
|
|
<MudTd>
|
|
<MudChip Size="Size.Small" Color="@GetStatusColor(context.StatusColor)">
|
|
@context.Status
|
|
</MudChip>
|
|
</MudTd>
|
|
<MudTd>@context.RequestDatePersian</MudTd>
|
|
<MudTd>@context.ProcessDatePersian</MudTd>
|
|
<MudTd>@context.BankAccount</MudTd>
|
|
</RowTemplate>
|
|
</MudTable>
|
|
}
|
|
}
|
|
</MudContainer>
|
|
|
|
@code {
|
|
private MyBalanceDto? _balance;
|
|
private MyWithdrawalHistoryDto? _history;
|
|
private bool _loadingBalance = true;
|
|
private bool _loadingHistory = true;
|
|
private bool _submitting = false;
|
|
|
|
private MudForm _form;
|
|
private decimal _withdrawalAmount;
|
|
private string _bankAccount = "";
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await Task.WhenAll(LoadBalance(), LoadHistory());
|
|
}
|
|
|
|
private async Task LoadBalance()
|
|
{
|
|
try
|
|
{
|
|
_loadingBalance = true;
|
|
_balance = await CommissionService.GetMyBalanceAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"خطا در بارگذاری موجودی: {ex.Message}", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
_loadingBalance = false;
|
|
}
|
|
}
|
|
|
|
private async Task LoadHistory()
|
|
{
|
|
try
|
|
{
|
|
_loadingHistory = true;
|
|
_history = await CommissionService.GetMyWithdrawalHistoryAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"خطا در بارگذاری تاریخچه: {ex.Message}", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
_loadingHistory = false;
|
|
}
|
|
}
|
|
|
|
private async Task SubmitWithdrawal()
|
|
{
|
|
await _form.Validate();
|
|
if (!_form.IsValid) return;
|
|
|
|
try
|
|
{
|
|
_submitting = true;
|
|
var result = await CommissionService.RequestWithdrawalAsync(_withdrawalAmount, _bankAccount);
|
|
|
|
if (result.Success)
|
|
{
|
|
Snackbar.Add(result.Message, Severity.Success);
|
|
_withdrawalAmount = 0;
|
|
_bankAccount = "";
|
|
await Task.WhenAll(LoadBalance(), LoadHistory());
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
_submitting = false;
|
|
}
|
|
}
|
|
|
|
private Color GetStatusColor(string color)
|
|
{
|
|
return color switch
|
|
{
|
|
"success" => Color.Success,
|
|
"warning" => Color.Warning,
|
|
"error" => Color.Error,
|
|
_ => Color.Default
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Task 4.5: اضافه کردن به NavMenu
|
|
```razor
|
|
<MudNavGroup Title="کمیسیون و برداشت" Icon="@Icons.Material.Filled.AccountBalanceWallet">
|
|
<MudNavLink Href="/commission/payouts" Icon="@Icons.Material.Filled.Payments">
|
|
کمیسیونهای من
|
|
</MudNavLink>
|
|
<MudNavLink Href="/commission/withdrawal" Icon="@Icons.Material.Filled.LocalAtm">
|
|
برداشت وجه
|
|
</MudNavLink>
|
|
</MudNavGroup>
|
|
```
|
|
|
|
---
|
|
|
|
### ✅ Checkpoint نهایی
|
|
|
|
```bash
|
|
cd /home/masoud/Apps/project/FourSat/FrontOffice.BFF/src/
|
|
dotnet build
|
|
|
|
cd /home/masoud/Apps/project/FourSat/FrontOffice/src/
|
|
dotnet build
|
|
```
|
|
|
|
**چیزهایی که باید کار کنند:**
|
|
```
|
|
[ ] BFF Build شود (3 Queries + 1 Command + Validator)
|
|
[ ] FrontOffice Build شود
|
|
[ ] صفحه /commission/payouts نمایش داده شود
|
|
[ ] صفحه /commission/withdrawal کار کند
|
|
[ ] فرم درخواست برداشت Validate شود
|
|
[ ] Mock data نمایش داده شود
|
|
```
|
|
|
|
---
|
|
|
|
### 📊 آماری از کارهای انجام شده
|
|
|
|
| مورد | تعداد | وضعیت |
|
|
|------|-------|-------|
|
|
| Queries پیاده شده | 3 از 8 | 37.5% |
|
|
| Commands پیاده شده | 1 از 8 | 12.5% |
|
|
| Handlers | 4 | Mock Data |
|
|
| Validators | 1 | FluentValidation ✅ |
|
|
| Controllers | 1 | 4 Endpoints |
|
|
| UI Pages | 2 | ✅ |
|
|
| Services | 1 | ✅ |
|
|
|
|
**زمان تخمینی تا اینجا:** 6 ساعت
|
|
**کارهای باقیمانده:**
|
|
- 5 Query دیگر (Weekly Report, Pool Share, Downline, Statistics, Available)
|
|
- 7 Command دیگر (Approve, Reject, Pay, Cancel, Recalculate, Adjust, Transfer)
|
|
- اتصال واقعی به CMS
|
|
|
|
---
|
|
|
|
### 💡 نکات بسیار مهم برای Developer
|
|
|
|
1. **Validation**: از FluentValidation استفاده شد - حتماً Validator را در DI ثبت کن
|
|
2. **Amount Formatting**: همه مبالغ با Format "N0" نمایش داده میشوند (1,250,000)
|
|
3. **Bank Account Masking**: شماره کارت را Mask کن: "6037-****-****-1234"
|
|
4. **Minimum Withdrawal**: در CMS حداقل مبلغ برداشت را Check کن (معمولاً 100,000 تومان)
|
|
5. **Concurrent Requests**: کاربر نباید بتواند همزمان چند درخواست برداشت بزند
|
|
6. **Status Colors**: از Color mapping استفاده کن برای نمایش بهتر وضعیتها
|
|
|
|
|
|
---
|
|
|
|
## 🎒 مرحله 5: تکمیل UserWallet (کیف پول)
|
|
|
|
### 📊 وضعیت فعلی
|
|
|
|
**موجود در BFF (60%):**
|
|
- ✅ GetUserWallet Query
|
|
- ✅ GetWalletTransactions Query
|
|
- ✅ ChargeWallet Command (ولی ناقص)
|
|
- ⚠️ Withdrawal Handler خالی است (TODO)
|
|
|
|
**غایب (40%):**
|
|
- ❌ DiscountBalance (موجودی تخفیف) - هیچ Query و UI ندارد
|
|
- ❌ GetDiscountTransactions Query
|
|
- ❌ UseDiscount Command (استفاده از تخفیف در خرید)
|
|
- ❌ صفحه نمایش موجودی تخفیف در UI
|
|
|
|
---
|
|
|
|
### 📝 STEP 1: بررسی DiscountBalance در CMS
|
|
|
|
#### Task 1.1: بررسی UserWallet Entity
|
|
```bash
|
|
cd /home/masoud/Apps/project/FourSat/CMS/src/
|
|
|
|
cat CMSMicroservice.Domain/Entities/UserWallet.cs
|
|
# باید ببینی:
|
|
# - MainBalance: موجودی اصلی ✅
|
|
# - DiscountBalance: موجودی تخفیف ❌ (این قسمت غایب است)
|
|
# - RewardBalance: موجودی پاداش ✅
|
|
```
|
|
|
|
#### Task 1.2: بررسی Queries موجود
|
|
```bash
|
|
ls CMSMicroservice.Application/UserWalletCQ/Queries/
|
|
# باید ببینی:
|
|
# - GetUserWallet/ ✅
|
|
# - GetWalletTransactions/ ✅
|
|
# - GetDiscountTransactions/ (ممکن است وجود نداشته باشد)
|
|
|
|
# اگر GetDiscountTransactions وجود داشت:
|
|
cat CMSMicroservice.Application/UserWalletCQ/Queries/GetDiscountTransactions/GetDiscountTransactionsQueryHandler.cs
|
|
```
|
|
|
|
**Output Task 1.2:**
|
|
```
|
|
[ ] GetUserWallet Query را بررسی کردم
|
|
[ ] چک کردم DiscountBalance در DTO موجود است یا خیر
|
|
[ ] GetDiscountTransactions را پیدا کردم (یا متوجه شدم که وجود ندارد)
|
|
```
|
|
|
|
---
|
|
|
|
### 📝 STEP 2: تکمیل BFF - DiscountBalance
|
|
|
|
#### Task 2.1: اضافه کردن DiscountBalance به GetMyWallet
|
|
|
|
**فایل موجود: FrontOffice.BFF.Application/UserWalletCQ/Queries/GetMyWallet/MyWalletResponseDto.cs**
|
|
|
|
اگر DiscountBalance وجود ندارد، اضافه کن:
|
|
|
|
```csharp
|
|
namespace FrontOffice.BFF.Application.UserWalletCQ.Queries.GetMyWallet;
|
|
|
|
public class MyWalletResponseDto
|
|
{
|
|
// موجود:
|
|
public decimal MainBalance { get; set; }
|
|
public string MainBalanceFormatted { get; set; }
|
|
|
|
public decimal RewardBalance { get; set; }
|
|
public string RewardBalanceFormatted { get; set; }
|
|
|
|
// اضافه کن:
|
|
public decimal DiscountBalance { get; set; }
|
|
public string DiscountBalanceFormatted { get; set; }
|
|
|
|
// مجموع کل
|
|
public decimal TotalBalance { get; set; }
|
|
public string TotalBalanceFormatted { get; set; }
|
|
|
|
// UI Helpers
|
|
public bool HasDiscount { get; set; } // آیا تخفیف دارد؟
|
|
public string DiscountPercentage { get; set; } // "15%" (اگر applicable)
|
|
}
|
|
```
|
|
|
|
**آپدیت Handler:**
|
|
```csharp
|
|
// در GetMyWalletQueryHandler.cs
|
|
public async Task<MyWalletResponseDto> Handle(...)
|
|
{
|
|
var userId = _currentUser.UserId;
|
|
|
|
// TODO: Call CMS
|
|
// var wallet = await _cmsClient.GetUserWalletAsync(new { UserId = userId });
|
|
|
|
// Mock Data با DiscountBalance
|
|
var mainBalance = 5000000m;
|
|
var rewardBalance = 1200000m;
|
|
var discountBalance = 800000m; // اضافه شد
|
|
var total = mainBalance + rewardBalance + discountBalance;
|
|
|
|
return new MyWalletResponseDto
|
|
{
|
|
MainBalance = mainBalance,
|
|
MainBalanceFormatted = mainBalance.ToString("N0") + " تومان",
|
|
|
|
RewardBalance = rewardBalance,
|
|
RewardBalanceFormatted = rewardBalance.ToString("N0") + " تومان",
|
|
|
|
DiscountBalance = discountBalance,
|
|
DiscountBalanceFormatted = discountBalance.ToString("N0") + " تومان",
|
|
|
|
TotalBalance = total,
|
|
TotalBalanceFormatted = total.ToString("N0") + " تومان",
|
|
|
|
HasDiscount = discountBalance > 0,
|
|
DiscountPercentage = "15%"
|
|
};
|
|
}
|
|
```
|
|
|
|
#### Task 2.2: ایجاد Query جدید - GetMyDiscountTransactions
|
|
|
|
**فایل 1: GetMyDiscountTransactionsQuery.cs**
|
|
```bash
|
|
mkdir -p FrontOffice.BFF.Application/UserWalletCQ/Queries/GetMyDiscountTransactions/
|
|
nano GetMyDiscountTransactionsQuery.cs
|
|
```
|
|
|
|
```csharp
|
|
using MediatR;
|
|
|
|
namespace FrontOffice.BFF.Application.UserWalletCQ.Queries.GetMyDiscountTransactions;
|
|
|
|
public record GetMyDiscountTransactionsQuery : IRequest<MyDiscountTransactionsResponseDto>
|
|
{
|
|
public int PageNumber { get; init; } = 1;
|
|
public int PageSize { get; init; } = 10;
|
|
}
|
|
```
|
|
|
|
**فایل 2: MyDiscountTransactionsResponseDto.cs**
|
|
```csharp
|
|
namespace FrontOffice.BFF.Application.UserWalletCQ.Queries.GetMyDiscountTransactions;
|
|
|
|
public class MyDiscountTransactionsResponseDto
|
|
{
|
|
public List<DiscountTransactionItemDto> Transactions { get; set; }
|
|
public int TotalCount { get; set; }
|
|
}
|
|
|
|
public class DiscountTransactionItemDto
|
|
{
|
|
public long Id { get; set; }
|
|
public string Type { get; set; } // "دریافت" / "استفاده"
|
|
public string TypeIcon { get; set; } // "add_circle" / "remove_circle"
|
|
public string TypeColor { get; set; } // "success" / "error"
|
|
public decimal Amount { get; set; }
|
|
public string AmountFormatted { get; set; }
|
|
public string Description { get; set; } // "تخفیف خرید محصول X"
|
|
public string DatePersian { get; set; }
|
|
}
|
|
```
|
|
|
|
**فایل 3: GetMyDiscountTransactionsQueryHandler.cs**
|
|
```csharp
|
|
using MediatR;
|
|
using FrontOffice.BFF.Application.Common.Interfaces;
|
|
|
|
namespace FrontOffice.BFF.Application.UserWalletCQ.Queries.GetMyDiscountTransactions;
|
|
|
|
public class GetMyDiscountTransactionsQueryHandler
|
|
: IRequestHandler<GetMyDiscountTransactionsQuery, MyDiscountTransactionsResponseDto>
|
|
{
|
|
private readonly ICurrentUserService _currentUser;
|
|
|
|
public GetMyDiscountTransactionsQueryHandler(ICurrentUserService currentUser)
|
|
{
|
|
_currentUser = currentUser;
|
|
}
|
|
|
|
public async Task<MyDiscountTransactionsResponseDto> Handle(
|
|
GetMyDiscountTransactionsQuery request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var userId = _currentUser.UserId;
|
|
|
|
// TODO: Call CMS
|
|
|
|
// Mock Data
|
|
return new MyDiscountTransactionsResponseDto
|
|
{
|
|
Transactions = new List<DiscountTransactionItemDto>
|
|
{
|
|
new() {
|
|
Id = 1,
|
|
Type = "دریافت",
|
|
TypeIcon = "add_circle",
|
|
TypeColor = "success",
|
|
Amount = 500000,
|
|
AmountFormatted = "500,000 تومان",
|
|
Description = "تخفیف خرید بسته طلایی",
|
|
DatePersian = "20 آذر 1403"
|
|
},
|
|
new() {
|
|
Id = 2,
|
|
Type = "استفاده",
|
|
TypeIcon = "remove_circle",
|
|
TypeColor = "error",
|
|
Amount = -200000,
|
|
AmountFormatted = "200,000 تومان",
|
|
Description = "استفاده در خرید محصول A",
|
|
DatePersian = "22 آذر 1403"
|
|
}
|
|
},
|
|
TotalCount = 2
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Task 2.3: آپدیت Controller
|
|
|
|
**فایل موجود: UserWalletController.cs**
|
|
```csharp
|
|
// اضافه کردن endpoint جدید
|
|
using FrontOffice.BFF.Application.UserWalletCQ.Queries.GetMyDiscountTransactions;
|
|
|
|
[HttpGet("my-discount-transactions")]
|
|
[ProducesResponseType(typeof(MyDiscountTransactionsResponseDto), 200)]
|
|
public async Task<IActionResult> GetMyDiscountTransactions(
|
|
[FromQuery] int pageNumber = 1,
|
|
[FromQuery] int pageSize = 10)
|
|
{
|
|
var query = new GetMyDiscountTransactionsQuery
|
|
{
|
|
PageNumber = pageNumber,
|
|
PageSize = pageSize
|
|
};
|
|
var result = await _mediator.Send(query);
|
|
return Ok(result);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 📝 STEP 3: تکمیل Withdrawal Handler
|
|
|
|
**Task 3.1: پیدا کردن WithdrawalFromWallet Handler**
|
|
```bash
|
|
find FrontOffice.BFF.Application/UserWalletCQ/ -name "*Withdrawal*"
|
|
# باید پیدا کنی: Commands/WithdrawalFromWallet/WithdrawalFromWalletCommandHandler.cs
|
|
```
|
|
|
|
**Task 3.2: تکمیل Handler خالی**
|
|
```csharp
|
|
// فایل موجود: WithdrawalFromWalletCommandHandler.cs
|
|
using MediatR;
|
|
using FrontOffice.BFF.Application.Common.Interfaces;
|
|
|
|
namespace FrontOffice.BFF.Application.UserWalletCQ.Commands.WithdrawalFromWallet;
|
|
|
|
public class WithdrawalFromWalletCommandHandler
|
|
: IRequestHandler<WithdrawalFromWalletCommand, WithdrawalFromWalletResponseDto>
|
|
{
|
|
private readonly ICurrentUserService _currentUser;
|
|
// TODO: private readonly UserWalletServiceClient _cmsClient;
|
|
|
|
public WithdrawalFromWalletCommandHandler(ICurrentUserService currentUser)
|
|
{
|
|
_currentUser = currentUser;
|
|
}
|
|
|
|
public async Task<WithdrawalFromWalletResponseDto> Handle(
|
|
WithdrawalFromWalletCommand request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var userId = _currentUser.UserId;
|
|
|
|
// TODO: فراخوانی CMS
|
|
// var result = await _cmsClient.WithdrawalFromWalletAsync(new {
|
|
// UserId = userId,
|
|
// Amount = request.Amount,
|
|
// WalletType = request.WalletType // Main / Reward / Discount
|
|
// });
|
|
|
|
// Mock Response
|
|
return new WithdrawalFromWalletResponseDto
|
|
{
|
|
Success = true,
|
|
TransactionId = 456,
|
|
Message = "برداشت با موفقیت انجام شد",
|
|
NewBalance = "4,500,000 تومان"
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
**Task 3.3: اضافه کردن Validator**
|
|
```csharp
|
|
// فایل جدید: WithdrawalFromWalletCommandValidator.cs
|
|
using FluentValidation;
|
|
|
|
namespace FrontOffice.BFF.Application.UserWalletCQ.Commands.WithdrawalFromWallet;
|
|
|
|
public class WithdrawalFromWalletCommandValidator : AbstractValidator<WithdrawalFromWalletCommand>
|
|
{
|
|
public WithdrawalFromWalletCommandValidator()
|
|
{
|
|
RuleFor(x => x.Amount)
|
|
.GreaterThan(0).WithMessage("مبلغ باید بیشتر از صفر باشد")
|
|
.LessThanOrEqualTo(10000000).WithMessage("حداکثر مبلغ برداشت 10 میلیون تومان است");
|
|
|
|
RuleFor(x => x.WalletType)
|
|
.NotEmpty().WithMessage("نوع کیف پول الزامی است")
|
|
.Must(x => new[] { "Main", "Reward", "Discount" }.Contains(x))
|
|
.WithMessage("نوع کیف پول نامعتبر است");
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 📝 STEP 4: آپدیت UI - WalletPage
|
|
|
|
#### Task 4.1: اضافه کردن DiscountBalance به Service
|
|
```csharp
|
|
// فایل موجود: FrontOffice.Main/Services/UserWalletService.cs
|
|
public async Task<MyDiscountTransactionsDto> GetMyDiscountTransactionsAsync(int pageNumber = 1)
|
|
{
|
|
var response = await _httpClient.GetAsync(
|
|
$"/api/userwallet/my-discount-transactions?pageNumber={pageNumber}");
|
|
response.EnsureSuccessStatusCode();
|
|
return await response.Content.ReadFromJsonAsync<MyDiscountTransactionsDto>();
|
|
}
|
|
```
|
|
|
|
#### Task 4.2: آپدیت WalletPage.razor
|
|
|
|
**اضافه کردن Card برای DiscountBalance:**
|
|
```razor
|
|
@page "/wallet"
|
|
@inject UserWalletService WalletService
|
|
@inject ISnackbar Snackbar
|
|
|
|
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
|
|
<MudText Typo="Typo.h4" Class="mb-4">کیف پول من</MudText>
|
|
|
|
@if (_loading)
|
|
{
|
|
<MudProgressLinear Indeterminate="true" />
|
|
}
|
|
else if (_wallet != null)
|
|
{
|
|
<MudGrid>
|
|
<!-- موجودی اصلی -->
|
|
<MudItem xs="12" md="4">
|
|
<MudCard Style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
|
|
<MudCardContent>
|
|
<MudText Typo="Typo.h6">موجودی اصلی</MudText>
|
|
<MudText Typo="Typo.h4" Class="mt-2">
|
|
<strong>@_wallet.MainBalanceFormatted</strong>
|
|
</MudText>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
<!-- موجودی پاداش -->
|
|
<MudItem xs="12" md="4">
|
|
<MudCard Style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: white;">
|
|
<MudCardContent>
|
|
<MudText Typo="Typo.h6">موجودی پاداش</MudText>
|
|
<MudText Typo="Typo.h4" Class="mt-2">
|
|
<strong>@_wallet.RewardBalanceFormatted</strong>
|
|
</MudText>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
<!-- موجودی تخفیف (جدید) -->
|
|
<MudItem xs="12" md="4">
|
|
<MudCard Style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); color: white;">
|
|
<MudCardContent>
|
|
<MudText Typo="Typo.h6">موجودی تخفیف</MudText>
|
|
<MudText Typo="Typo.h4" Class="mt-2">
|
|
<strong>@_wallet.DiscountBalanceFormatted</strong>
|
|
</MudText>
|
|
@if (_wallet.HasDiscount)
|
|
{
|
|
<MudChip Size="Size.Small" Color="Color.Success" Class="mt-2">
|
|
@_wallet.DiscountPercentage تخفیف
|
|
</MudChip>
|
|
}
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
<!-- مجموع کل -->
|
|
<MudItem xs="12">
|
|
<MudCard Elevation="5">
|
|
<MudCardContent>
|
|
<MudText Typo="Typo.h5" Align="Align.Center">
|
|
مجموع کل: <strong style="color: green;">@_wallet.TotalBalanceFormatted</strong>
|
|
</MudText>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
</MudGrid>
|
|
|
|
<!-- Tabs برای تراکنشها -->
|
|
<MudTabs Class="mt-6" ApplyEffectsToContainer="true" PanelClass="pa-4">
|
|
<MudTabPanel Text="تراکنشهای اصلی" Icon="@Icons.Material.Filled.Receipt">
|
|
<!-- کد موجود برای Main Transactions -->
|
|
</MudTabPanel>
|
|
|
|
<MudTabPanel Text="تراکنشهای پاداش" Icon="@Icons.Material.Filled.CardGiftcard">
|
|
<!-- کد موجود برای Reward Transactions -->
|
|
</MudTabPanel>
|
|
|
|
<!-- Tab جدید برای DiscountTransactions -->
|
|
<MudTabPanel Text="تراکنشهای تخفیف" Icon="@Icons.Material.Filled.Discount">
|
|
@if (_loadingDiscountTxs)
|
|
{
|
|
<MudProgressLinear Indeterminate="true" />
|
|
}
|
|
else if (_discountTransactions != null)
|
|
{
|
|
<MudTable Items="@_discountTransactions.Transactions" Hover="true">
|
|
<HeaderContent>
|
|
<MudTh>نوع</MudTh>
|
|
<MudTh>مبلغ</MudTh>
|
|
<MudTh>شرح</MudTh>
|
|
<MudTh>تاریخ</MudTh>
|
|
</HeaderContent>
|
|
<RowTemplate>
|
|
<MudTd>
|
|
<MudIcon Icon="@context.TypeIcon"
|
|
Color="@(context.TypeColor == "success" ? Color.Success : Color.Error)" />
|
|
@context.Type
|
|
</MudTd>
|
|
<MudTd>
|
|
<strong style="color: @(context.Amount > 0 ? "green" : "red")">
|
|
@context.AmountFormatted
|
|
</strong>
|
|
</MudTd>
|
|
<MudTd>@context.Description</MudTd>
|
|
<MudTd>@context.DatePersian</MudTd>
|
|
</RowTemplate>
|
|
</MudTable>
|
|
}
|
|
</MudTabPanel>
|
|
</MudTabs>
|
|
}
|
|
</MudContainer>
|
|
|
|
@code {
|
|
private MyWalletDto? _wallet;
|
|
private MyDiscountTransactionsDto? _discountTransactions;
|
|
private bool _loading = true;
|
|
private bool _loadingDiscountTxs = true;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await LoadWallet();
|
|
await LoadDiscountTransactions();
|
|
}
|
|
|
|
private async Task LoadWallet()
|
|
{
|
|
try
|
|
{
|
|
_loading = true;
|
|
_wallet = await WalletService.GetMyWalletAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
_loading = false;
|
|
}
|
|
}
|
|
|
|
private async Task LoadDiscountTransactions()
|
|
{
|
|
try
|
|
{
|
|
_loadingDiscountTxs = true;
|
|
_discountTransactions = await WalletService.GetMyDiscountTransactionsAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"خطا در بارگذاری تراکنشهای تخفیف: {ex.Message}", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
_loadingDiscountTxs = false;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### ✅ Checkpoint نهایی
|
|
|
|
```bash
|
|
cd /home/masoud/Apps/project/FourSat/FrontOffice.BFF/src/
|
|
dotnet build
|
|
|
|
cd /home/masoud/Apps/project/FourSat/FrontOffice/src/
|
|
dotnet build
|
|
```
|
|
|
|
**چیزهایی که باید کار کنند:**
|
|
```
|
|
[ ] GetMyWallet شامل DiscountBalance است
|
|
[ ] GetMyDiscountTransactions Query کار میکند
|
|
[ ] WithdrawalFromWallet Handler تکمیل شده
|
|
[ ] Validator برای Withdrawal اضافه شده
|
|
[ ] UI سه کارت موجودی نمایش میدهد
|
|
[ ] Tab جدید "تراکنشهای تخفیف" کار میکند
|
|
```
|
|
|
|
---
|
|
|
|
### 📊 آماری از تکمیل UserWallet
|
|
|
|
| مورد | قبل | بعد | وضعیت |
|
|
|------|-----|-----|-------|
|
|
| Queries | 2 | 3 | ✅ +1 |
|
|
| Commands | 2 | 2 | ✅ Handler تکمیل شد |
|
|
| Validators | 1 | 2 | ✅ +1 |
|
|
| UI Cards | 2 | 3 | ✅ +1 |
|
|
| UI Tabs | 2 | 3 | ✅ +1 |
|
|
| درصد تکمیل | 60% | 100% | 🎉 |
|
|
|
|
**زمان تخمینی:** 2 ساعت
|
|
|
|
---
|
|
|
|
### 💡 نکات مهم
|
|
|
|
1. **DiscountBalance vs RewardBalance**: تخفیف فقط در خرید استفاده میشود، پاداش قابل برداشت است
|
|
2. **Gradient Colors**: از Linear Gradient برای Cards استفاده شد - زیباتر است
|
|
3. **Tabs Performance**: از `MudTabs` استفاده کن - بهتر از Separate Pages
|
|
4. **Amount Sign**: در DiscountTransactions مبلغهای منفی را با رنگ قرمز نشان بده
|
|
5. **Validator Registration**: فراموش نکن Validator را در DI ثبت کنی
|
|
|
|
|
|
---
|
|
|
|
## 🛒 مرحله 6: تکمیل ShoppingCart (سبد خرید)
|
|
|
|
### 📊 وضعیت فعلی
|
|
|
|
**موجود در BFF (50%):**
|
|
- ✅ GetMyCart Query
|
|
- ✅ AddToCart Command
|
|
- ✅ UpdateCartItemQuantity Command
|
|
|
|
**غایب (50%):**
|
|
- ❌ ClearCart Command (پاک کردن کل سبد)
|
|
- ❌ DeleteCartItem Command (حذف یک آیتم)
|
|
- ❌ MergeGuestCart Command (ادغام سبد مهمان با سبد کاربر لاگین شده)
|
|
- ❌ ApplyDiscount Command (اعمال کد تخفیف)
|
|
|
|
**UI غایب:**
|
|
- ❌ دکمه "پاک کردن سبد"
|
|
- ❌ دکمه "حذف" برای هر آیتم
|
|
- ❌ فرم اعمال کد تخفیف
|
|
|
|
---
|
|
|
|
### 📝 STEP 1: بررسی CMS ShoppingCart
|
|
|
|
#### Task 1.1: بررسی Commands موجود
|
|
```bash
|
|
cd /home/masoud/Apps/project/FourSat/CMS/src/
|
|
|
|
ls CMSMicroservice.Application/ShoppingCartCQ/Commands/
|
|
# باید ببینی:
|
|
# - AddToCart/ ✅
|
|
# - UpdateCartItemQuantity/ ✅
|
|
# - DeleteCartItem/ (چک کن وجود دارد؟)
|
|
# - ClearCart/ (چک کن وجود دارد؟)
|
|
# - MergeGuestCart/ (چک کن وجود دارد؟)
|
|
# - ApplyDiscountCode/ (چک کن وجود دارد؟)
|
|
```
|
|
|
|
#### Task 1.2: بررسی DeleteCartItem در CMS
|
|
```bash
|
|
# اگر وجود داشت:
|
|
cat CMSMicroservice.Application/ShoppingCartCQ/Commands/DeleteCartItem/DeleteCartItemCommandHandler.cs
|
|
# Input:
|
|
# - UserId
|
|
# - CartItemId
|
|
# Logic:
|
|
# - پیدا کردن CartItem
|
|
# - حذف از دیتابیس
|
|
# - بهروزرسانی TotalPrice سبد
|
|
```
|
|
|
|
#### Task 1.3: بررسی ClearCart در CMS
|
|
```bash
|
|
# اگر وجود داشت:
|
|
cat CMSMicroservice.Application/ShoppingCartCQ/Commands/ClearCart/ClearCartCommandHandler.cs
|
|
# Input:
|
|
# - UserId
|
|
# Logic:
|
|
# - حذف همه CartItems کاربر
|
|
# - TotalPrice = 0
|
|
```
|
|
|
|
---
|
|
|
|
### 📝 STEP 2: پیادهسازی Commands غایب در BFF
|
|
|
|
#### Task 2.1: Command - DeleteMyCartItem
|
|
|
|
**فایل 1: DeleteMyCartItemCommand.cs**
|
|
```bash
|
|
mkdir -p FrontOffice.BFF.Application/ShoppingCartCQ/Commands/DeleteMyCartItem/
|
|
nano DeleteMyCartItemCommand.cs
|
|
```
|
|
|
|
```csharp
|
|
using MediatR;
|
|
|
|
namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.DeleteMyCartItem;
|
|
|
|
/// <summary>
|
|
/// حذف یک آیتم از سبد خرید من
|
|
/// </summary>
|
|
public record DeleteMyCartItemCommand : IRequest<DeleteMyCartItemResponseDto>
|
|
{
|
|
public long CartItemId { get; init; }
|
|
}
|
|
```
|
|
|
|
**فایل 2: DeleteMyCartItemResponseDto.cs**
|
|
```csharp
|
|
namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.DeleteMyCartItem;
|
|
|
|
public class DeleteMyCartItemResponseDto
|
|
{
|
|
public bool Success { get; set; }
|
|
public string Message { get; set; } // "آیتم با موفقیت حذف شد"
|
|
public decimal NewTotalPrice { get; set; }
|
|
public string NewTotalPriceFormatted { get; set; }
|
|
public int RemainingItemsCount { get; set; } // تعداد آیتمهای باقیمانده
|
|
}
|
|
```
|
|
|
|
**فایل 3: DeleteMyCartItemCommandHandler.cs**
|
|
```csharp
|
|
using MediatR;
|
|
using FrontOffice.BFF.Application.Common.Interfaces;
|
|
|
|
namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.DeleteMyCartItem;
|
|
|
|
public class DeleteMyCartItemCommandHandler
|
|
: IRequestHandler<DeleteMyCartItemCommand, DeleteMyCartItemResponseDto>
|
|
{
|
|
private readonly ICurrentUserService _currentUser;
|
|
// TODO: private readonly ShoppingCartServiceClient _cmsClient;
|
|
|
|
public DeleteMyCartItemCommandHandler(ICurrentUserService currentUser)
|
|
{
|
|
_currentUser = currentUser;
|
|
}
|
|
|
|
public async Task<DeleteMyCartItemResponseDto> Handle(
|
|
DeleteMyCartItemCommand request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var userId = _currentUser.UserId;
|
|
|
|
// TODO: Call CMS
|
|
// var result = await _cmsClient.DeleteCartItemAsync(new {
|
|
// UserId = userId,
|
|
// CartItemId = request.CartItemId
|
|
// });
|
|
|
|
// Mock Response
|
|
return new DeleteMyCartItemResponseDto
|
|
{
|
|
Success = true,
|
|
Message = "محصول از سبد خرید حذف شد",
|
|
NewTotalPrice = 4500000,
|
|
NewTotalPriceFormatted = "4,500,000 تومان",
|
|
RemainingItemsCount = 2
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
**فایل 4: DeleteMyCartItemCommandValidator.cs**
|
|
```csharp
|
|
using FluentValidation;
|
|
|
|
namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.DeleteMyCartItem;
|
|
|
|
public class DeleteMyCartItemCommandValidator : AbstractValidator<DeleteMyCartItemCommand>
|
|
{
|
|
public DeleteMyCartItemCommandValidator()
|
|
{
|
|
RuleFor(x => x.CartItemId)
|
|
.GreaterThan(0).WithMessage("شناسه آیتم نامعتبر است");
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Task 2.2: Command - ClearMyCart
|
|
|
|
**فایل 1: ClearMyCartCommand.cs**
|
|
```bash
|
|
mkdir -p FrontOffice.BFF.Application/ShoppingCartCQ/Commands/ClearMyCart/
|
|
nano ClearMyCartCommand.cs
|
|
```
|
|
|
|
```csharp
|
|
using MediatR;
|
|
|
|
namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.ClearMyCart;
|
|
|
|
/// <summary>
|
|
/// پاک کردن کل سبد خرید من
|
|
/// </summary>
|
|
public record ClearMyCartCommand : IRequest<ClearMyCartResponseDto>
|
|
{
|
|
// هیچ ورودی ندارد - UserId از Token میآید
|
|
}
|
|
```
|
|
|
|
**فایل 2: ClearMyCartResponseDto.cs**
|
|
```csharp
|
|
namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.ClearMyCart;
|
|
|
|
public class ClearMyCartResponseDto
|
|
{
|
|
public bool Success { get; set; }
|
|
public string Message { get; set; } // "سبد خرید شما خالی شد"
|
|
public int DeletedItemsCount { get; set; }
|
|
}
|
|
```
|
|
|
|
**فایل 3: ClearMyCartCommandHandler.cs**
|
|
```csharp
|
|
using MediatR;
|
|
using FrontOffice.BFF.Application.Common.Interfaces;
|
|
|
|
namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.ClearMyCart;
|
|
|
|
public class ClearMyCartCommandHandler
|
|
: IRequestHandler<ClearMyCartCommand, ClearMyCartResponseDto>
|
|
{
|
|
private readonly ICurrentUserService _currentUser;
|
|
|
|
public ClearMyCartCommandHandler(ICurrentUserService currentUser)
|
|
{
|
|
_currentUser = currentUser;
|
|
}
|
|
|
|
public async Task<ClearMyCartResponseDto> Handle(
|
|
ClearMyCartCommand request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var userId = _currentUser.UserId;
|
|
|
|
// TODO: Call CMS
|
|
// var result = await _cmsClient.ClearCartAsync(new { UserId = userId });
|
|
|
|
return new ClearMyCartResponseDto
|
|
{
|
|
Success = true,
|
|
Message = "سبد خرید شما با موفقیت خالی شد",
|
|
DeletedItemsCount = 3
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Task 2.3: Command - MergeGuestCart (اختیاری - پیچیدهتر)
|
|
|
|
**فایل 1: MergeGuestCartCommand.cs**
|
|
```bash
|
|
mkdir -p FrontOffice.BFF.Application/ShoppingCartCQ/Commands/MergeGuestCart/
|
|
nano MergeGuestCartCommand.cs
|
|
```
|
|
|
|
```csharp
|
|
using MediatR;
|
|
|
|
namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.MergeGuestCart;
|
|
|
|
/// <summary>
|
|
/// ادغام سبد مهمان با سبد کاربر لاگین شده
|
|
/// زمانی استفاده میشود که کاربر بدون لاگین خرید میکند و بعد لاگین میکند
|
|
/// </summary>
|
|
public record MergeGuestCartCommand : IRequest<MergeGuestCartResponseDto>
|
|
{
|
|
public string GuestCartId { get; init; } // GUID سبد مهمان (از LocalStorage)
|
|
}
|
|
```
|
|
|
|
**فایل 2: MergeGuestCartResponseDto.cs**
|
|
```csharp
|
|
namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.MergeGuestCart;
|
|
|
|
public class MergeGuestCartResponseDto
|
|
{
|
|
public bool Success { get; set; }
|
|
public string Message { get; set; }
|
|
public int MergedItemsCount { get; set; } // تعداد آیتمهای ادغام شده
|
|
public decimal NewTotalPrice { get; set; }
|
|
public string NewTotalPriceFormatted { get; set; }
|
|
}
|
|
```
|
|
|
|
**فایل 3: MergeGuestCartCommandHandler.cs**
|
|
```csharp
|
|
using MediatR;
|
|
using FrontOffice.BFF.Application.Common.Interfaces;
|
|
|
|
namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.MergeGuestCart;
|
|
|
|
public class MergeGuestCartCommandHandler
|
|
: IRequestHandler<MergeGuestCartCommand, MergeGuestCartResponseDto>
|
|
{
|
|
private readonly ICurrentUserService _currentUser;
|
|
|
|
public MergeGuestCartCommandHandler(ICurrentUserService currentUser)
|
|
{
|
|
_currentUser = currentUser;
|
|
}
|
|
|
|
public async Task<MergeGuestCartResponseDto> Handle(
|
|
MergeGuestCartCommand request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var userId = _currentUser.UserId;
|
|
|
|
// TODO: Call CMS
|
|
// Logic:
|
|
// 1. دریافت سبد مهمان از GuestCartId
|
|
// 2. دریافت سبد کاربر فعلی
|
|
// 3. ادغام آیتمها (اگر محصول تکراری بود، Quantity جمع شود)
|
|
// 4. حذف سبد مهمان
|
|
|
|
return new MergeGuestCartResponseDto
|
|
{
|
|
Success = true,
|
|
Message = "سبد خرید شما با موفقیت ادغام شد",
|
|
MergedItemsCount = 2,
|
|
NewTotalPrice = 6500000,
|
|
NewTotalPriceFormatted = "6,500,000 تومان"
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
**فایل 4: MergeGuestCartCommandValidator.cs**
|
|
```csharp
|
|
using FluentValidation;
|
|
|
|
namespace FrontOffice.BFF.Application.ShoppingCartCQ.Commands.MergeGuestCart;
|
|
|
|
public class MergeGuestCartCommandValidator : AbstractValidator<MergeGuestCartCommand>
|
|
{
|
|
public MergeGuestCartCommandValidator()
|
|
{
|
|
RuleFor(x => x.GuestCartId)
|
|
.NotEmpty().WithMessage("شناسه سبد مهمان الزامی است")
|
|
.Must(BeValidGuid).WithMessage("شناسه سبد نامعتبر است");
|
|
}
|
|
|
|
private bool BeValidGuid(string guestCartId)
|
|
{
|
|
return Guid.TryParse(guestCartId, out _);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 📝 STEP 3: آپدیت Controller
|
|
|
|
**فایل موجود: ShoppingCartController.cs**
|
|
|
|
اضافه کردن 3 endpoint جدید:
|
|
|
|
```csharp
|
|
using FrontOffice.BFF.Application.ShoppingCartCQ.Commands.DeleteMyCartItem;
|
|
using FrontOffice.BFF.Application.ShoppingCartCQ.Commands.ClearMyCart;
|
|
using FrontOffice.BFF.Application.ShoppingCartCQ.Commands.MergeGuestCart;
|
|
|
|
/// <summary>
|
|
/// حذف یک آیتم از سبد خرید
|
|
/// </summary>
|
|
[HttpDelete("items/{cartItemId}")]
|
|
[ProducesResponseType(typeof(DeleteMyCartItemResponseDto), 200)]
|
|
public async Task<IActionResult> DeleteCartItem(long cartItemId)
|
|
{
|
|
var command = new DeleteMyCartItemCommand { CartItemId = cartItemId };
|
|
var result = await _mediator.Send(command);
|
|
return Ok(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// پاک کردن کل سبد خرید
|
|
/// </summary>
|
|
[HttpDelete("clear")]
|
|
[ProducesResponseType(typeof(ClearMyCartResponseDto), 200)]
|
|
public async Task<IActionResult> ClearCart()
|
|
{
|
|
var command = new ClearMyCartCommand();
|
|
var result = await _mediator.Send(command);
|
|
return Ok(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// ادغام سبد مهمان
|
|
/// </summary>
|
|
[HttpPost("merge-guest")]
|
|
[ProducesResponseType(typeof(MergeGuestCartResponseDto), 200)]
|
|
public async Task<IActionResult> MergeGuestCart([FromBody] MergeGuestCartCommand command)
|
|
{
|
|
var result = await _mediator.Send(command);
|
|
return Ok(result);
|
|
}
|
|
```
|
|
|
|
**Test Endpoints:**
|
|
```bash
|
|
# Test 1: Delete Item
|
|
curl -X DELETE \
|
|
-H "Authorization: Bearer TOKEN" \
|
|
http://localhost:5002/api/shoppingcart/items/123
|
|
|
|
# Test 2: Clear Cart
|
|
curl -X DELETE \
|
|
-H "Authorization: Bearer TOKEN" \
|
|
http://localhost:5002/api/shoppingcart/clear
|
|
|
|
# Test 3: Merge Guest Cart
|
|
curl -X POST \
|
|
-H "Authorization: Bearer TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"guestCartId": "550e8400-e29b-41d4-a716-446655440000"}' \
|
|
http://localhost:5002/api/shoppingcart/merge-guest
|
|
```
|
|
|
|
---
|
|
|
|
### 📝 STEP 4: آپدیت UI - CartPage
|
|
|
|
#### Task 4.1: اضافه کردن متدها به Service
|
|
```csharp
|
|
// فایل موجود: FrontOffice.Main/Services/ShoppingCartService.cs
|
|
|
|
public async Task<DeleteCartItemResultDto> DeleteCartItemAsync(long cartItemId)
|
|
{
|
|
var response = await _httpClient.DeleteAsync($"/api/shoppingcart/items/{cartItemId}");
|
|
response.EnsureSuccessStatusCode();
|
|
return await response.Content.ReadFromJsonAsync<DeleteCartItemResultDto>();
|
|
}
|
|
|
|
public async Task<ClearCartResultDto> ClearCartAsync()
|
|
{
|
|
var response = await _httpClient.DeleteAsync("/api/shoppingcart/clear");
|
|
response.EnsureSuccessStatusCode();
|
|
return await response.Content.ReadFromJsonAsync<ClearCartResultDto>();
|
|
}
|
|
|
|
public async Task<MergeGuestCartResultDto> MergeGuestCartAsync(string guestCartId)
|
|
{
|
|
var request = new { GuestCartId = guestCartId };
|
|
var response = await _httpClient.PostAsJsonAsync("/api/shoppingcart/merge-guest", request);
|
|
response.EnsureSuccessStatusCode();
|
|
return await response.Content.ReadFromJsonAsync<MergeGuestCartResultDto>();
|
|
}
|
|
```
|
|
|
|
#### Task 4.2: آپدیت CartPage.razor
|
|
|
|
**اضافه کردن دکمههای حذف:**
|
|
|
|
```razor
|
|
@page "/cart"
|
|
@inject ShoppingCartService CartService
|
|
@inject ISnackbar Snackbar
|
|
@inject IDialogService DialogService
|
|
|
|
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
|
|
<MudGrid>
|
|
<MudItem xs="12">
|
|
<MudText Typo="Typo.h4" Class="mb-4">سبد خرید من</MudText>
|
|
</MudItem>
|
|
|
|
@if (_loading)
|
|
{
|
|
<MudProgressLinear Indeterminate="true" />
|
|
}
|
|
else if (_cart != null && _cart.Items.Any())
|
|
{
|
|
<MudItem xs="12" md="8">
|
|
<MudCard>
|
|
<MudCardHeader>
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.h6">
|
|
محصولات (@_cart.TotalItemsCount مورد)
|
|
</MudText>
|
|
</CardHeaderContent>
|
|
<CardHeaderActions>
|
|
<MudButton StartIcon="@Icons.Material.Filled.DeleteSweep"
|
|
Color="Color.Error"
|
|
Variant="Variant.Text"
|
|
OnClick="ClearCartWithConfirm">
|
|
پاک کردن سبد
|
|
</MudButton>
|
|
</CardHeaderActions>
|
|
</MudCardHeader>
|
|
<MudCardContent>
|
|
@foreach (var item in _cart.Items)
|
|
{
|
|
<MudPaper Class="pa-3 mb-3" Elevation="2">
|
|
<MudGrid>
|
|
<MudItem xs="12" md="6">
|
|
<MudText Typo="Typo.h6">@item.ProductName</MudText>
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary">
|
|
@item.ProductDescription
|
|
</MudText>
|
|
</MudItem>
|
|
<MudItem xs="6" md="2">
|
|
<MudNumericField @bind-Value="item.Quantity"
|
|
Label="تعداد"
|
|
Min="1"
|
|
Max="10"
|
|
Variant="Variant.Outlined"
|
|
OnChange="() => UpdateQuantity(item.CartItemId, item.Quantity)" />
|
|
</MudItem>
|
|
<MudItem xs="6" md="2">
|
|
<MudText Typo="Typo.body1">
|
|
<strong>@item.TotalPriceFormatted</strong>
|
|
</MudText>
|
|
</MudItem>
|
|
<MudItem xs="12" md="2">
|
|
<MudButton StartIcon="@Icons.Material.Filled.Delete"
|
|
Color="Color.Error"
|
|
Variant="Variant.Text"
|
|
FullWidth="true"
|
|
OnClick="() => DeleteItem(item.CartItemId)">
|
|
حذف
|
|
</MudButton>
|
|
</MudItem>
|
|
</MudGrid>
|
|
</MudPaper>
|
|
}
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
<MudItem xs="12" md="4">
|
|
<MudCard>
|
|
<MudCardHeader>
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.h6">خلاصه سبد خرید</MudText>
|
|
</CardHeaderContent>
|
|
</MudCardHeader>
|
|
<MudCardContent>
|
|
<MudStack Spacing="2">
|
|
<MudText>
|
|
<strong>تعداد کل:</strong> @_cart.TotalItemsCount مورد
|
|
</MudText>
|
|
<MudText>
|
|
<strong>قیمت کل:</strong>
|
|
<span style="color: green; font-size: 1.2em;">
|
|
@_cart.TotalPriceFormatted
|
|
</span>
|
|
</MudText>
|
|
<MudDivider />
|
|
<MudButton Variant="Variant.Filled"
|
|
Color="Color.Primary"
|
|
FullWidth="true"
|
|
StartIcon="@Icons.Material.Filled.ShoppingCart">
|
|
تکمیل خرید
|
|
</MudButton>
|
|
</MudStack>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
}
|
|
else
|
|
{
|
|
<MudItem xs="12">
|
|
<MudAlert Severity="Severity.Info">
|
|
سبد خرید شما خالی است
|
|
</MudAlert>
|
|
</MudItem>
|
|
}
|
|
</MudGrid>
|
|
</MudContainer>
|
|
|
|
@code {
|
|
private MyCartDto? _cart;
|
|
private bool _loading = true;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await LoadCart();
|
|
}
|
|
|
|
private async Task LoadCart()
|
|
{
|
|
try
|
|
{
|
|
_loading = true;
|
|
_cart = await CartService.GetMyCartAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
_loading = false;
|
|
}
|
|
}
|
|
|
|
private async Task UpdateQuantity(long cartItemId, int newQuantity)
|
|
{
|
|
try
|
|
{
|
|
await CartService.UpdateCartItemQuantityAsync(cartItemId, newQuantity);
|
|
Snackbar.Add("تعداد بهروزرسانی شد", Severity.Success);
|
|
await LoadCart();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
|
|
private async Task DeleteItem(long cartItemId)
|
|
{
|
|
bool? confirm = await DialogService.ShowMessageBox(
|
|
"تایید حذف",
|
|
"آیا از حذف این محصول اطمینان دارید؟",
|
|
yesText: "بله", cancelText: "خیر");
|
|
|
|
if (confirm == true)
|
|
{
|
|
try
|
|
{
|
|
var result = await CartService.DeleteCartItemAsync(cartItemId);
|
|
Snackbar.Add(result.Message, Severity.Success);
|
|
await LoadCart();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task ClearCartWithConfirm()
|
|
{
|
|
bool? confirm = await DialogService.ShowMessageBox(
|
|
"پاک کردن سبد",
|
|
"آیا از پاک کردن کل سبد خرید اطمینان دارید؟",
|
|
yesText: "بله، پاک کن", cancelText: "خیر");
|
|
|
|
if (confirm == true)
|
|
{
|
|
try
|
|
{
|
|
var result = await CartService.ClearCartAsync();
|
|
Snackbar.Add(result.Message, Severity.Success);
|
|
await LoadCart();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### ✅ Checkpoint نهایی
|
|
|
|
```bash
|
|
cd /home/masoud/Apps/project/FourSat/FrontOffice.BFF/src/
|
|
dotnet build
|
|
|
|
cd /home/masoud/Apps/project/FourSat/FrontOffice/src/
|
|
dotnet build
|
|
```
|
|
|
|
**چیزهایی که باید کار کنند:**
|
|
```
|
|
[ ] DeleteMyCartItem Command کار میکند
|
|
[ ] ClearMyCart Command کار میکند
|
|
[ ] MergeGuestCart Command کار میکند
|
|
[ ] 3 Validator اضافه شده
|
|
[ ] 3 endpoint جدید در Controller
|
|
[ ] UI دکمه "حذف" برای هر آیتم دارد
|
|
[ ] UI دکمه "پاک کردن سبد" دارد
|
|
[ ] Confirmation Dialog نمایش داده میشود
|
|
```
|
|
|
|
---
|
|
|
|
### 📊 آماری از تکمیل ShoppingCart
|
|
|
|
| مورد | قبل | بعد | وضعیت |
|
|
|------|-----|-----|-------|
|
|
| Commands | 3 | 6 | ✅ +3 |
|
|
| Validators | 1 | 4 | ✅ +3 |
|
|
| Controller Endpoints | 3 | 6 | ✅ +3 |
|
|
| UI Delete Button | ❌ | ✅ | ✅ |
|
|
| UI Clear Button | ❌ | ✅ | ✅ |
|
|
| Confirmation Dialogs | ❌ | ✅ | ✅ |
|
|
| درصد تکمیل | 50% | 100% | 🎉 |
|
|
|
|
**زمان تخمینی:** 3 ساعت
|
|
|
|
---
|
|
|
|
### 💡 نکات بسیار مهم
|
|
|
|
1. **Confirmation Dialog**: همیشه قبل از حذف از کاربر تایید بگیر (UX بهتر)
|
|
2. **DeleteCartItem vs ClearCart**: Delete یک آیتم حذف میکند، Clear همه را پاک میکند
|
|
3. **MergeGuestCart**: این قابلیت برای زمانی است که کاربر بدون لاگین خرید کرده و بعد لاگین کند
|
|
4. **LocalStorage**: سبد مهمان در LocalStorage ذخیره شود (GuestCartId = Guid)
|
|
5. **Quantity Update**: بلافاصله بعد از تغییر Quantity، Cart را reload کن
|
|
6. **HTTP Methods**: Delete → `DeleteAsync`, Clear → `DeleteAsync`, Merge → `PostAsync`
|
|
7. **Icon Usage**: از `DeleteSweep` برای Clear و `Delete` برای DeleteItem استفاده کن
|
|
|
|
|
|
---
|
|
|
|
## 💳 مرحله 7: DayaLoan UI + Contract Completion
|
|
|
|
### 📊 خلاصه این مرحله
|
|
|
|
**دو کار اصلی:**
|
|
1. **DayaLoan UI**: نمایش وضعیت وام دایا برای مشتری (Backend در CMS آماده است)
|
|
2. **Contract Completion**: تکمیل Queries غایب در Contract Module
|
|
|
|
---
|
|
|
|
## بخش اول: DayaLoan - نمایش وضعیت وام
|
|
|
|
### 📝 STEP 1: بررسی DayaLoan در CMS
|
|
|
|
#### Task 1.1: بررسی موجودیها
|
|
```bash
|
|
cd /home/masoud/Apps/project/FourSat/CMS/src/
|
|
|
|
# بررسی Entity
|
|
cat CMSMicroservice.Domain/Entities/DayaLoanContract.cs
|
|
# باید ببینی:
|
|
# - NationalCode: کد ملی
|
|
# - LoanStatus: PendingReceive / Approved / Rejected
|
|
# - ContractNumber: شماره قرارداد (بعد از تایید)
|
|
# - RequestAmount: 56,000,000 (برای هر کیف پول)
|
|
# - LastCheckDate: آخرین بار استعلام
|
|
# - ApprovalDate: تاریخ تایید
|
|
|
|
# بررسی Commands
|
|
ls CMSMicroservice.Application/DayaLoanCQ/Commands/
|
|
# باید ببینی:
|
|
# - CheckDayaLoanStatus/ ✅
|
|
# - ProcessDayaLoanApproval/ ✅
|
|
|
|
# بررسی Worker
|
|
find . -name "*DayaLoanWorker*"
|
|
# Worker که هر 15 دقیقه استعلام میکند
|
|
```
|
|
|
|
**Output Task 1.1:**
|
|
```
|
|
[ ] DayaLoanContract Entity را بررسی کردم
|
|
[ ] CheckDayaLoanStatus Command را دیدم
|
|
[ ] ProcessDayaLoanApproval Command را دیدم
|
|
[ ] Worker را پیدا کردم
|
|
```
|
|
|
|
---
|
|
|
|
### 📝 STEP 2: ایجاد BFF Module - DayaLoanCQ
|
|
|
|
#### Task 2.1: ساخت فولدرها
|
|
```bash
|
|
cd /home/masoud/Apps/project/FourSat/FrontOffice.BFF/src/FrontOffice.BFF.Application/
|
|
|
|
mkdir -p DayaLoanCQ/Queries/GetMyDayaLoanStatus
|
|
mkdir -p DayaLoanCQ/Commands/RequestDayaLoanCheck
|
|
|
|
tree DayaLoanCQ/
|
|
```
|
|
|
|
**Expected Output:**
|
|
```
|
|
DayaLoanCQ/
|
|
├── Commands/
|
|
│ └── RequestDayaLoanCheck/
|
|
└── Queries/
|
|
└── GetMyDayaLoanStatus/
|
|
```
|
|
|
|
#### Task 2.2: Query - GetMyDayaLoanStatus
|
|
|
|
**فایل 1: GetMyDayaLoanStatusQuery.cs**
|
|
```csharp
|
|
using MediatR;
|
|
|
|
namespace FrontOffice.BFF.Application.DayaLoanCQ.Queries.GetMyDayaLoanStatus;
|
|
|
|
/// <summary>
|
|
/// دریافت وضعیت وام دایا برای کاربر جاری
|
|
/// </summary>
|
|
public record GetMyDayaLoanStatusQuery : IRequest<MyDayaLoanStatusResponseDto>
|
|
{
|
|
}
|
|
```
|
|
|
|
**فایل 2: MyDayaLoanStatusResponseDto.cs**
|
|
```csharp
|
|
namespace FrontOffice.BFF.Application.DayaLoanCQ.Queries.GetMyDayaLoanStatus;
|
|
|
|
public class MyDayaLoanStatusResponseDto
|
|
{
|
|
public bool HasActiveLoan { get; set; } // آیا وام فعال دارد؟
|
|
public string Status { get; set; } // "در انتظار دریافت" / "تایید شده" / "رد شده"
|
|
public string StatusColor { get; set; } // "warning" / "success" / "error"
|
|
public string StatusIcon { get; set; } // Icon name
|
|
|
|
public string NationalCode { get; set; }
|
|
public decimal RequestAmount { get; set; } // 56,000,000
|
|
public string RequestAmountFormatted { get; set; }
|
|
|
|
public string ContractNumber { get; set; } // شماره قرارداد (اگر تایید شده)
|
|
public string LastCheckDatePersian { get; set; } // آخرین استعلام
|
|
public string ApprovalDatePersian { get; set; } // تاریخ تایید (اگر تایید شده)
|
|
|
|
public bool CanRequestCheck { get; set; } // آیا میتواند درخواست استعلام دهد؟
|
|
public string NextCheckAvailable { get; set; } // "امکان استعلام بعد از 1 ساعت"
|
|
|
|
// برای نمایش جزئیات شارژ کیف پول
|
|
public List<WalletChargeDto> WalletCharges { get; set; }
|
|
}
|
|
|
|
public class WalletChargeDto
|
|
{
|
|
public string WalletType { get; set; } // "Main" / "Reward" / "Discount"
|
|
public string WalletTypePersian { get; set; } // "کیف پول اصلی"
|
|
public decimal Amount { get; set; } // 56,000,000
|
|
public string AmountFormatted { get; set; }
|
|
public bool IsCharged { get; set; } // آیا شارژ شده؟
|
|
public string ChargedDatePersian { get; set; }
|
|
}
|
|
```
|
|
|
|
**فایل 3: GetMyDayaLoanStatusQueryHandler.cs**
|
|
```csharp
|
|
using MediatR;
|
|
using FrontOffice.BFF.Application.Common.Interfaces;
|
|
|
|
namespace FrontOffice.BFF.Application.DayaLoanCQ.Queries.GetMyDayaLoanStatus;
|
|
|
|
public class GetMyDayaLoanStatusQueryHandler
|
|
: IRequestHandler<GetMyDayaLoanStatusQuery, MyDayaLoanStatusResponseDto>
|
|
{
|
|
private readonly ICurrentUserService _currentUser;
|
|
// TODO: private readonly DayaLoanServiceClient _cmsClient;
|
|
|
|
public GetMyDayaLoanStatusQueryHandler(ICurrentUserService currentUser)
|
|
{
|
|
_currentUser = currentUser;
|
|
}
|
|
|
|
public async Task<MyDayaLoanStatusResponseDto> Handle(
|
|
GetMyDayaLoanStatusQuery request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var userId = _currentUser.UserId;
|
|
|
|
// TODO: Call CMS
|
|
// var result = await _cmsClient.GetDayaLoanStatusAsync(new { UserId = userId });
|
|
|
|
// Mock Data - وضعیت "تایید شده"
|
|
return new MyDayaLoanStatusResponseDto
|
|
{
|
|
HasActiveLoan = true,
|
|
Status = "تایید شده",
|
|
StatusColor = "success",
|
|
StatusIcon = "check_circle",
|
|
|
|
NationalCode = "1234567890",
|
|
RequestAmount = 56000000,
|
|
RequestAmountFormatted = "56,000,000 تومان",
|
|
|
|
ContractNumber = "DL-1403-001234",
|
|
LastCheckDatePersian = "25 آذر 1403",
|
|
ApprovalDatePersian = "25 آذر 1403",
|
|
|
|
CanRequestCheck = false,
|
|
NextCheckAvailable = "وام شما قبلاً تایید شده است",
|
|
|
|
WalletCharges = new List<WalletChargeDto>
|
|
{
|
|
new() {
|
|
WalletType = "Main",
|
|
WalletTypePersian = "کیف پول اصلی",
|
|
Amount = 56000000,
|
|
AmountFormatted = "56,000,000 تومان",
|
|
IsCharged = true,
|
|
ChargedDatePersian = "25 آذر 1403"
|
|
},
|
|
new() {
|
|
WalletType = "Reward",
|
|
WalletTypePersian = "کیف پول پاداش",
|
|
Amount = 56000000,
|
|
AmountFormatted = "56,000,000 تومان",
|
|
IsCharged = true,
|
|
ChargedDatePersian = "25 آذر 1403"
|
|
},
|
|
new() {
|
|
WalletType = "Discount",
|
|
WalletTypePersian = "کیف پول تخفیف",
|
|
Amount = 56000000,
|
|
AmountFormatted = "56,000,000 تومان",
|
|
IsCharged = true,
|
|
ChargedDatePersian = "25 آذر 1403"
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Task 2.3: Command - RequestDayaLoanCheck (اختیاری)
|
|
|
|
**فایل 1: RequestDayaLoanCheckCommand.cs**
|
|
```csharp
|
|
using MediatR;
|
|
|
|
namespace FrontOffice.BFF.Application.DayaLoanCQ.Commands.RequestDayaLoanCheck;
|
|
|
|
/// <summary>
|
|
/// درخواست استعلام فوری وضعیت وام دایا
|
|
/// معمولاً Worker این کار را انجام میدهد، اما کاربر میتواند استعلام فوری بزند
|
|
/// </summary>
|
|
public record RequestDayaLoanCheckCommand : IRequest<RequestDayaLoanCheckResponseDto>
|
|
{
|
|
}
|
|
```
|
|
|
|
**فایل 2: RequestDayaLoanCheckResponseDto.cs**
|
|
```csharp
|
|
namespace FrontOffice.BFF.Application.DayaLoanCQ.Commands.RequestDayaLoanCheck;
|
|
|
|
public class RequestDayaLoanCheckResponseDto
|
|
{
|
|
public bool Success { get; set; }
|
|
public string Message { get; set; } // "استعلام با موفقیت انجام شد"
|
|
public string NewStatus { get; set; } // وضعیت جدید
|
|
}
|
|
```
|
|
|
|
**فایل 3: RequestDayaLoanCheckCommandHandler.cs**
|
|
```csharp
|
|
using MediatR;
|
|
using FrontOffice.BFF.Application.Common.Interfaces;
|
|
|
|
namespace FrontOffice.BFF.Application.DayaLoanCQ.Commands.RequestDayaLoanCheck;
|
|
|
|
public class RequestDayaLoanCheckCommandHandler
|
|
: IRequestHandler<RequestDayaLoanCheckCommand, RequestDayaLoanCheckResponseDto>
|
|
{
|
|
private readonly ICurrentUserService _currentUser;
|
|
|
|
public RequestDayaLoanCheckCommandHandler(ICurrentUserService currentUser)
|
|
{
|
|
_currentUser = currentUser;
|
|
}
|
|
|
|
public async Task<RequestDayaLoanCheckResponseDto> Handle(
|
|
RequestDayaLoanCheckCommand request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var userId = _currentUser.UserId;
|
|
|
|
// TODO: Call CMS CheckDayaLoanStatus Command
|
|
|
|
return new RequestDayaLoanCheckResponseDto
|
|
{
|
|
Success = true,
|
|
Message = "استعلام وضعیت وام با موفقیت انجام شد. نتیجه در صفحه نمایش داده میشود.",
|
|
NewStatus = "در انتظار دریافت"
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 📝 STEP 3: Controller - DayaLoanController
|
|
|
|
**فایل جدید: DayaLoanController.cs**
|
|
```csharp
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using MediatR;
|
|
using FrontOffice.BFF.Application.DayaLoanCQ.Queries.GetMyDayaLoanStatus;
|
|
using FrontOffice.BFF.Application.DayaLoanCQ.Commands.RequestDayaLoanCheck;
|
|
|
|
namespace FrontOffice.BFF.WebApi.Controllers;
|
|
|
|
[Authorize]
|
|
[ApiController]
|
|
[Route("api/[controller]")]
|
|
public class DayaLoanController : ControllerBase
|
|
{
|
|
private readonly IMediator _mediator;
|
|
|
|
public DayaLoanController(IMediator mediator)
|
|
{
|
|
_mediator = mediator;
|
|
}
|
|
|
|
/// <summary>
|
|
/// دریافت وضعیت وام دایا من
|
|
/// </summary>
|
|
[HttpGet("my-status")]
|
|
[ProducesResponseType(typeof(MyDayaLoanStatusResponseDto), 200)]
|
|
public async Task<IActionResult> GetMyStatus()
|
|
{
|
|
var query = new GetMyDayaLoanStatusQuery();
|
|
var result = await _mediator.Send(query);
|
|
return Ok(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// درخواست استعلام فوری
|
|
/// </summary>
|
|
[HttpPost("request-check")]
|
|
[ProducesResponseType(typeof(RequestDayaLoanCheckResponseDto), 200)]
|
|
public async Task<IActionResult> RequestCheck()
|
|
{
|
|
var command = new RequestDayaLoanCheckCommand();
|
|
var result = await _mediator.Send(command);
|
|
return Ok(result);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 📝 STEP 4: UI - DayaLoanPage
|
|
|
|
#### Task 4.1: Service
|
|
```csharp
|
|
// فایل جدید: FrontOffice.Main/Services/DayaLoanService.cs
|
|
using System.Net.Http.Json;
|
|
using FrontOffice.Main.Models;
|
|
|
|
namespace FrontOffice.Main.Services;
|
|
|
|
public class DayaLoanService
|
|
{
|
|
private readonly HttpClient _httpClient;
|
|
|
|
public DayaLoanService(HttpClient httpClient)
|
|
{
|
|
_httpClient = httpClient;
|
|
}
|
|
|
|
public async Task<MyDayaLoanStatusDto> GetMyStatusAsync()
|
|
{
|
|
var response = await _httpClient.GetAsync("/api/dayaloan/my-status");
|
|
response.EnsureSuccessStatusCode();
|
|
return await response.Content.ReadFromJsonAsync<MyDayaLoanStatusDto>();
|
|
}
|
|
|
|
public async Task<RequestCheckResultDto> RequestCheckAsync()
|
|
{
|
|
var response = await _httpClient.PostAsync("/api/dayaloan/request-check", null);
|
|
response.EnsureSuccessStatusCode();
|
|
return await response.Content.ReadFromJsonAsync<RequestCheckResultDto>();
|
|
}
|
|
}
|
|
```
|
|
|
|
**ثبت در Program.cs:**
|
|
```csharp
|
|
builder.Services.AddScoped<DayaLoanService>();
|
|
```
|
|
|
|
#### Task 4.2: Page - DayaLoanStatusPage.razor
|
|
```bash
|
|
mkdir -p FrontOffice/src/FrontOffice.Main/Pages/Loan/
|
|
nano DayaLoanStatusPage.razor
|
|
```
|
|
|
|
```razor
|
|
@page "/loan/daya-status"
|
|
@inject DayaLoanService LoanService
|
|
@inject ISnackbar Snackbar
|
|
|
|
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
|
|
<MudText Typo="Typo.h4" Class="mb-4">وضعیت وام دایا</MudText>
|
|
|
|
@if (_loading)
|
|
{
|
|
<MudProgressLinear Indeterminate="true" />
|
|
}
|
|
else if (_status != null)
|
|
{
|
|
<MudGrid>
|
|
<!-- کارت وضعیت اصلی -->
|
|
<MudItem xs="12" md="6">
|
|
<MudCard Elevation="5">
|
|
<MudCardHeader Style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.h5">وضعیت درخواست</MudText>
|
|
</CardHeaderContent>
|
|
<CardHeaderActions>
|
|
<MudIcon Icon="@_status.StatusIcon" Size="Size.Large" />
|
|
</CardHeaderActions>
|
|
</MudCardHeader>
|
|
<MudCardContent>
|
|
<MudStack Spacing="3">
|
|
<MudAlert Severity="@GetSeverity(_status.StatusColor)" Dense="true">
|
|
<strong>@_status.Status</strong>
|
|
</MudAlert>
|
|
|
|
<MudText>
|
|
<strong>کد ملی:</strong> @_status.NationalCode
|
|
</MudText>
|
|
|
|
<MudText>
|
|
<strong>مبلغ درخواستی (هر کیف پول):</strong>
|
|
<span style="color: green; font-size: 1.1em;">
|
|
@_status.RequestAmountFormatted
|
|
</span>
|
|
</MudText>
|
|
|
|
@if (!string.IsNullOrEmpty(_status.ContractNumber))
|
|
{
|
|
<MudText>
|
|
<strong>شماره قرارداد:</strong>
|
|
<MudChip Size="Size.Small" Color="Color.Info">
|
|
@_status.ContractNumber
|
|
</MudChip>
|
|
</MudText>
|
|
}
|
|
|
|
<MudText>
|
|
<strong>آخرین استعلام:</strong> @_status.LastCheckDatePersian
|
|
</MudText>
|
|
|
|
@if (!string.IsNullOrEmpty(_status.ApprovalDatePersian))
|
|
{
|
|
<MudText>
|
|
<strong>تاریخ تایید:</strong> @_status.ApprovalDatePersian
|
|
</MudText>
|
|
}
|
|
</MudStack>
|
|
</MudCardContent>
|
|
|
|
@if (_status.CanRequestCheck)
|
|
{
|
|
<MudCardActions>
|
|
<MudButton Variant="Variant.Filled"
|
|
Color="Color.Primary"
|
|
StartIcon="@Icons.Material.Filled.Refresh"
|
|
Disabled="_checking"
|
|
OnClick="RequestCheck">
|
|
@if (_checking)
|
|
{
|
|
<MudProgressCircular Size="Size.Small" Indeterminate="true" />
|
|
<span class="ms-2">در حال استعلام...</span>
|
|
}
|
|
else
|
|
{
|
|
<span>استعلام فوری</span>
|
|
}
|
|
</MudButton>
|
|
</MudCardActions>
|
|
}
|
|
else
|
|
{
|
|
<MudCardActions>
|
|
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
|
@_status.NextCheckAvailable
|
|
</MudText>
|
|
</MudCardActions>
|
|
}
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
<!-- کارت جزئیات شارژ کیف پولها -->
|
|
<MudItem xs="12" md="6">
|
|
<MudCard Elevation="5">
|
|
<MudCardHeader>
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.h6">جزئیات شارژ کیف پولها</MudText>
|
|
</CardHeaderContent>
|
|
</MudCardHeader>
|
|
<MudCardContent>
|
|
@if (_status.WalletCharges != null && _status.WalletCharges.Any())
|
|
{
|
|
<MudStack Spacing="3">
|
|
@foreach (var wallet in _status.WalletCharges)
|
|
{
|
|
<MudPaper Class="pa-3" Elevation="2">
|
|
<MudGrid>
|
|
<MudItem xs="8">
|
|
<MudText Typo="Typo.body1">
|
|
<strong>@wallet.WalletTypePersian</strong>
|
|
</MudText>
|
|
<MudText Typo="Typo.body2" Color="Color.Success">
|
|
@wallet.AmountFormatted
|
|
</MudText>
|
|
</MudItem>
|
|
<MudItem xs="4" Class="d-flex align-center justify-end">
|
|
@if (wallet.IsCharged)
|
|
{
|
|
<MudIcon Icon="@Icons.Material.Filled.CheckCircle"
|
|
Color="Color.Success"
|
|
Size="Size.Large" />
|
|
}
|
|
else
|
|
{
|
|
<MudIcon Icon="@Icons.Material.Filled.HourglassEmpty"
|
|
Color="Color.Warning"
|
|
Size="Size.Large" />
|
|
}
|
|
</MudItem>
|
|
</MudGrid>
|
|
@if (wallet.IsCharged)
|
|
{
|
|
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
|
شارژ شده در: @wallet.ChargedDatePersian
|
|
</MudText>
|
|
}
|
|
</MudPaper>
|
|
}
|
|
|
|
<MudDivider />
|
|
|
|
<MudText Typo="Typo.h6" Align="Align.Center">
|
|
مجموع کل شارژ:
|
|
<strong style="color: green;">
|
|
@((56000000m * 3).ToString("N0")) تومان
|
|
</strong>
|
|
</MudText>
|
|
</MudStack>
|
|
}
|
|
else
|
|
{
|
|
<MudAlert Severity="Severity.Info">
|
|
هنوز کیف پولی شارژ نشده است
|
|
</MudAlert>
|
|
}
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
<!-- راهنما -->
|
|
<MudItem xs="12">
|
|
<MudCard>
|
|
<MudCardContent>
|
|
<MudText Typo="Typo.h6" Class="mb-2">
|
|
<MudIcon Icon="@Icons.Material.Filled.Info" /> راهنما
|
|
</MudText>
|
|
<MudList Dense="true">
|
|
<MudListItem Icon="@Icons.Material.Filled.Circle" IconSize="Size.Small">
|
|
وام دایا برای هر کیف پول (اصلی، پاداش، تخفیف) به مبلغ 56 میلیون تومان است
|
|
</MudListItem>
|
|
<MudListItem Icon="@Icons.Material.Filled.Circle" IconSize="Size.Small">
|
|
سیستم هر 15 دقیقه یکبار وضعیت وام شما را بررسی میکند
|
|
</MudListItem>
|
|
<MudListItem Icon="@Icons.Material.Filled.Circle" IconSize="Size.Small">
|
|
در صورت تایید، کیف پولهای شما به صورت خودکار شارژ خواهند شد
|
|
</MudListItem>
|
|
</MudList>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
</MudGrid>
|
|
}
|
|
</MudContainer>
|
|
|
|
@code {
|
|
private MyDayaLoanStatusDto? _status;
|
|
private bool _loading = true;
|
|
private bool _checking = false;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await LoadStatus();
|
|
}
|
|
|
|
private async Task LoadStatus()
|
|
{
|
|
try
|
|
{
|
|
_loading = true;
|
|
_status = await LoanService.GetMyStatusAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
_loading = false;
|
|
}
|
|
}
|
|
|
|
private async Task RequestCheck()
|
|
{
|
|
try
|
|
{
|
|
_checking = true;
|
|
var result = await LoanService.RequestCheckAsync();
|
|
Snackbar.Add(result.Message, Severity.Success);
|
|
|
|
// Reload status after 2 seconds
|
|
await Task.Delay(2000);
|
|
await LoadStatus();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
_checking = false;
|
|
}
|
|
}
|
|
|
|
private Severity GetSeverity(string color)
|
|
{
|
|
return color switch
|
|
{
|
|
"success" => Severity.Success,
|
|
"warning" => Severity.Warning,
|
|
"error" => Severity.Error,
|
|
"info" => Severity.Info,
|
|
_ => Severity.Normal
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Task 4.3: اضافه کردن به NavMenu
|
|
```razor
|
|
<MudNavLink Href="/loan/daya-status" Icon="@Icons.Material.Filled.AccountBalance">
|
|
وام دایا
|
|
</MudNavLink>
|
|
```
|
|
|
|
---
|
|
|
|
## بخش دوم: Contract Completion
|
|
|
|
### 📝 STEP 5: تکمیل Contract Module
|
|
|
|
**وضعیت فعلی (80%):**
|
|
- ✅ CreateContract Command
|
|
- ✅ UpdateContract Command
|
|
- ❌ GetContract Query (غایب)
|
|
- ❌ GetAllMyContracts Query (غایب)
|
|
|
|
#### Task 5.1: Query - GetMyContract
|
|
|
|
**فایل 1: GetMyContractQuery.cs**
|
|
```bash
|
|
mkdir -p FrontOffice.BFF.Application/ContractCQ/Queries/GetMyContract/
|
|
nano GetMyContractQuery.cs
|
|
```
|
|
|
|
```csharp
|
|
using MediatR;
|
|
|
|
namespace FrontOffice.BFF.Application.ContractCQ.Queries.GetMyContract;
|
|
|
|
public record GetMyContractQuery : IRequest<MyContractResponseDto>
|
|
{
|
|
public long ContractId { get; init; }
|
|
}
|
|
```
|
|
|
|
**فایل 2: MyContractResponseDto.cs**
|
|
```csharp
|
|
namespace FrontOffice.BFF.Application.ContractCQ.Queries.GetMyContract;
|
|
|
|
public class MyContractResponseDto
|
|
{
|
|
public long Id { get; set; }
|
|
public string ContractNumber { get; set; }
|
|
public string Type { get; set; } // "خرید" / "عضویت" / "وام"
|
|
public string Status { get; set; } // "فعال" / "غیرفعال" / "منقضی"
|
|
public string StatusColor { get; set; }
|
|
|
|
public decimal TotalAmount { get; set; }
|
|
public string TotalAmountFormatted { get; set; }
|
|
|
|
public string StartDatePersian { get; set; }
|
|
public string EndDatePersian { get; set; }
|
|
|
|
public string Description { get; set; }
|
|
public string Terms { get; set; } // شرایط قرارداد
|
|
}
|
|
```
|
|
|
|
**فایل 3: GetMyContractQueryHandler.cs**
|
|
```csharp
|
|
using MediatR;
|
|
using FrontOffice.BFF.Application.Common.Interfaces;
|
|
|
|
namespace FrontOffice.BFF.Application.ContractCQ.Queries.GetMyContract;
|
|
|
|
public class GetMyContractQueryHandler
|
|
: IRequestHandler<GetMyContractQuery, MyContractResponseDto>
|
|
{
|
|
private readonly ICurrentUserService _currentUser;
|
|
|
|
public GetMyContractQueryHandler(ICurrentUserService currentUser)
|
|
{
|
|
_currentUser = currentUser;
|
|
}
|
|
|
|
public async Task<MyContractResponseDto> Handle(
|
|
GetMyContractQuery request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var userId = _currentUser.UserId;
|
|
|
|
// TODO: Call CMS
|
|
|
|
return new MyContractResponseDto
|
|
{
|
|
Id = request.ContractId,
|
|
ContractNumber = "CNT-1403-001234",
|
|
Type = "خرید محصول",
|
|
Status = "فعال",
|
|
StatusColor = "success",
|
|
TotalAmount = 5000000,
|
|
TotalAmountFormatted = "5,000,000 تومان",
|
|
StartDatePersian = "1 آذر 1403",
|
|
EndDatePersian = "1 آذر 1404",
|
|
Description = "قرارداد خرید بسته طلایی",
|
|
Terms = "شرایط و قوانین قرارداد..."
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Task 5.2: Query - GetMyContracts
|
|
|
|
**فایل 1: GetMyContractsQuery.cs**
|
|
```csharp
|
|
using MediatR;
|
|
|
|
namespace FrontOffice.BFF.Application.ContractCQ.Queries.GetMyContracts;
|
|
|
|
public record GetMyContractsQuery : IRequest<MyContractsResponseDto>
|
|
{
|
|
public int PageNumber { get; init; } = 1;
|
|
public int PageSize { get; init; } = 10;
|
|
}
|
|
```
|
|
|
|
**فایل 2: MyContractsResponseDto.cs**
|
|
```csharp
|
|
namespace FrontOffice.BFF.Application.ContractCQ.Queries.GetMyContracts;
|
|
|
|
public class MyContractsResponseDto
|
|
{
|
|
public List<ContractItemDto> Contracts { get; set; }
|
|
public int TotalCount { get; set; }
|
|
}
|
|
|
|
public class ContractItemDto
|
|
{
|
|
public long Id { get; set; }
|
|
public string ContractNumber { get; set; }
|
|
public string Type { get; set; }
|
|
public string Status { get; set; }
|
|
public string StatusColor { get; set; }
|
|
public string TotalAmountFormatted { get; set; }
|
|
public string StartDatePersian { get; set; }
|
|
}
|
|
```
|
|
|
|
#### Task 5.3: آپدیت ContractController
|
|
|
|
```csharp
|
|
using FrontOffice.BFF.Application.ContractCQ.Queries.GetMyContract;
|
|
using FrontOffice.BFF.Application.ContractCQ.Queries.GetMyContracts;
|
|
|
|
[HttpGet("{contractId}")]
|
|
[ProducesResponseType(typeof(MyContractResponseDto), 200)]
|
|
public async Task<IActionResult> GetContract(long contractId)
|
|
{
|
|
var query = new GetMyContractQuery { ContractId = contractId };
|
|
var result = await _mediator.Send(query);
|
|
return Ok(result);
|
|
}
|
|
|
|
[HttpGet("my-contracts")]
|
|
[ProducesResponseType(typeof(MyContractsResponseDto), 200)]
|
|
public async Task<IActionResult> GetMyContracts(
|
|
[FromQuery] int pageNumber = 1,
|
|
[FromQuery] int pageSize = 10)
|
|
{
|
|
var query = new GetMyContractsQuery { PageNumber = pageNumber, PageSize = pageSize };
|
|
var result = await _mediator.Send(query);
|
|
return Ok(result);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### ✅ Checkpoint نهایی
|
|
|
|
```bash
|
|
cd /home/masoud/Apps/project/FourSat/FrontOffice.BFF/src/
|
|
dotnet build
|
|
|
|
cd /home/masoud/Apps/project/FourSat/FrontOffice/src/
|
|
dotnet build
|
|
```
|
|
|
|
**چیزهایی که باید کار کنند:**
|
|
```
|
|
[ ] DayaLoan: GetMyDayaLoanStatus Query کار میکند
|
|
[ ] DayaLoan: RequestDayaLoanCheck Command کار میکند
|
|
[ ] DayaLoan: Controller با 2 endpoint
|
|
[ ] DayaLoan: UI صفحه کامل با نمایش 3 کیف پول
|
|
[ ] Contract: GetMyContract Query کار میکند
|
|
[ ] Contract: GetMyContracts Query کار میکند
|
|
[ ] Contract: Controller آپدیت شد
|
|
```
|
|
|
|
---
|
|
|
|
### 📊 آماری از مرحله 7
|
|
|
|
| ماژول | Queries قبل | Queries بعد | Commands قبل | Commands بعد | وضعیت |
|
|
|-------|------------|------------|-------------|-------------|-------|
|
|
| DayaLoan | 0 | 1 | 0 | 1 | ✅ 100% |
|
|
| Contract | 0 | 2 | 2 | 2 | ✅ 100% |
|
|
|
|
**زمان تخمینی:** 4 ساعت
|
|
|
|
---
|
|
|
|
### 💡 نکات مهم
|
|
|
|
1. **Worker**: Worker در CMS هر 15 دقیقه استعلام میکند - کاربر نباید بیش از حد استعلام فوری بزند
|
|
2. **168M Total**: 56M × 3 کیف پول = 168 میلیون تومان کل شارژ
|
|
3. **Status Icons**: از Icons.Material.Filled استفاده کن برای نمایش بهتر
|
|
4. **Gradient Background**: برای کارت وضعیت از Gradient استفاده شد
|
|
5. **Contract Module**: فقط 2 Query اضافه شد تا 100% شود
|
|
|
|
|
|
---
|
|
|
|
## 🔍 مرحله 8: بررسی نهایی - فقط کارهای ناتمام قبلی (بدون فیچرهای جدید)
|
|
|
|
### ⚠️ تذکر مهم
|
|
|
|
این مرحله **فقط** روی قابلیتهایی تمرکز دارد که:
|
|
1. ✅ در CMS **از قبل موجود** است
|
|
2. ❌ در FrontOffice.BFF یا FrontOffice **پیادهسازی نشده**
|
|
3. 🎯 **مختص مشتری** است (نه Admin)
|
|
|
|
**حذف شده از لیست:**
|
|
- ❌ DayaLoan (فیچر جدید - هنوز در CMS کامل نیست)
|
|
- ❌ Manual Payment (فیچر Admin)
|
|
- ❌ ClubMembership Admin Commands (مثل Deactivate, AssignFeature)
|
|
|
|
---
|
|
|
|
### 📝 STEP 1: بررسی دقیق CMS vs BFF
|
|
|
|
#### Task 1.1: مقایسه Commands/Queries موجود
|
|
```bash
|
|
cd /home/masoud/Apps/project/FourSat
|
|
|
|
# بررسی CMS Modules
|
|
echo "=== CMS Modules ===" > /tmp/cms_modules.txt
|
|
find CMS/src/CMSMicroservice.Application -type d -name "*CQ" | grep -v "bin\|obj" | sort >> /tmp/cms_modules.txt
|
|
|
|
# بررسی BFF Modules
|
|
echo "=== BFF Modules ===" > /tmp/bff_modules.txt
|
|
find FrontOffice.BFF/src/FrontOffice.BFF.Application -type d -name "*CQ" | grep -v "bin\|obj" | sort >> /tmp/bff_modules.txt
|
|
|
|
# مقایسه
|
|
echo "=== Comparison ===" > /tmp/comparison.txt
|
|
comm -3 <(find CMS/src/CMSMicroservice.Application -type d -name "*CQ" | xargs -I {} basename {} | sort -u) \
|
|
<(find FrontOffice.BFF/src/FrontOffice.BFF.Application -type d -name "*CQ" | xargs -I {} basename {} | sort -u) \
|
|
>> /tmp/comparison.txt
|
|
|
|
cat /tmp/comparison.txt
|
|
```
|
|
|
|
#### Task 1.2: فیلتر کردن Customer-Facing فقط
|
|
```bash
|
|
# ماژولهایی که حتماً Customer-Facing هستند:
|
|
echo "Customer-Facing Modules که در BFF غایب هستند:" > /tmp/customer_missing.txt
|
|
echo "1. ClubMembershipCQ - نیاز به UI برای مشتری" >> /tmp/customer_missing.txt
|
|
echo "2. NetworkMembershipCQ - نیاز به UI درخت" >> /tmp/customer_missing.txt
|
|
echo "3. CommissionCQ - بخشهای ناقص (Pool, Downline)" >> /tmp/customer_missing.txt
|
|
|
|
cat /tmp/customer_missing.txt
|
|
```
|
|
|
|
---
|
|
|
|
### 📊 ماژولهای ناقص واقعی (بدون فیچرهای جدید)
|
|
|
|
#### 1. ClubMembership (Priority 0 - حیاتی)
|
|
|
|
**موجود در CMS:**
|
|
```bash
|
|
ls CMS/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/
|
|
# ActivateClubMembership/ ← مشتری میخواهد عضو شود
|
|
# DeactivateClubMembership/ ← Admin only
|
|
# AssignClubFeature/ ← Admin only
|
|
|
|
ls CMS/src/CMSMicroservice.Application/ClubMembershipCQ/Queries/
|
|
# GetClubMembership/ ← مشتری میخواهد ببیند
|
|
# GetAllClubMemberships/ ← Admin only
|
|
# GetClubMembershipHistory/ ← مشتری میخواهد تاریخچه ببیند
|
|
# GetClubStatistics/ ← Admin + مشتری
|
|
```
|
|
|
|
**غایب در BFF:**
|
|
- ❌ Query: GetMyClubMembership (نمایش عضویت من)
|
|
- ❌ Query: GetMyClubHistory (تاریخچه عضویت من)
|
|
- ❌ Query: GetClubFeatures (لیست امکانات باشگاه برای انتخاب)
|
|
- ❌ Command: ActivateMyClubMembership (فعالسازی عضویت - پرداخت 56M)
|
|
|
|
**UI غایب:**
|
|
- ❌ صفحه نمایش وضعیت عضویت
|
|
- ❌ صفحه لیست امکانات باشگاه
|
|
- ❌ دکمه فعالسازی عضویت
|
|
|
|
---
|
|
|
|
#### 2. NetworkMembership (Priority 0 - حیاتی)
|
|
|
|
**موجود در CMS:**
|
|
```bash
|
|
ls CMS/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/
|
|
# GetNetworkTree/ ← مشتری میخواهد درخت ببیند
|
|
# GetUserPosition/ ← مشتری میخواهد موقعیت خود را ببیند
|
|
# GetNetworkHistory/ ← مشتری میخواهد تاریخچه ببیند
|
|
# GetNetworkStatistics/ ← مشتری میخواهد آمار ببیند
|
|
|
|
ls CMS/src/CMSMicroservice.Application/NetworkMembershipCQ/Commands/
|
|
# JoinNetwork/ ← Admin (وقت ثبتنام)
|
|
# MoveInNetwork/ ← Admin only
|
|
# RemoveFromNetwork/ ← Admin only
|
|
```
|
|
|
|
**غایب در BFF:**
|
|
- ❌ Query: GetMyNetworkTree (درخت شبکه من)
|
|
- ❌ Query: GetMyNetworkPosition (موقعیت من)
|
|
- ❌ Query: GetMyNetworkHistory (تاریخچه جابجاییها)
|
|
- ❌ Query: GetMyNetworkStatistics (آمار شبکه من: تعداد افراد، عمق، ...)
|
|
|
|
**UI غایب:**
|
|
- ❌ صفحه نمایش درخت باینری
|
|
- ❌ Component نمایش Recursive Tree
|
|
- ❌ صفحه آمار شبکه
|
|
|
|
---
|
|
|
|
#### 3. Commission (Priority 0 - حیاتی)
|
|
|
|
**موجود در CMS:**
|
|
```bash
|
|
ls CMS/src/CMSMicroservice.Application/CommissionCQ/Queries/
|
|
# GetUserCommissionPayouts/ ← مشتری میخواهد کمیسیونها را ببیند
|
|
# GetUserBalance/ ← مشتری میخواهد موجودی ببیند
|
|
# GetWithdrawalHistory/ ← مشتری میخواهد تاریخچه برداشت ببیند
|
|
# GetWeeklyReport/ ← مشتری میخواهد گزارش هفتگی ببیند
|
|
# GetPoolShare/ ← مشتری میخواهد سهم پول ببیند
|
|
# GetDownlineCommissions/ ← مشتری میخواهد کمیسیون زیرمجموعه ببیند
|
|
# GetCommissionStatistics/ ← مشتری میخواهد آمار ببیند
|
|
# GetAvailableBalance/ ← مشتری میخواهد مبلغ قابل برداشت ببیند
|
|
|
|
ls CMS/src/CMSMicroservice.Application/CommissionCQ/Commands/
|
|
# RequestWithdrawal/ ← مشتری میخواهد برداشت کند
|
|
# ApproveWithdrawal/ ← Admin only
|
|
# RejectWithdrawal/ ← Admin only
|
|
# PayWithdrawal/ ← Admin only
|
|
# CancelWithdrawal/ ← مشتری میتواند لغو کند
|
|
# RecalculateCommission/ ← Admin only
|
|
# AdjustBalance/ ← Admin only
|
|
# TransferCommission/ ← Admin only
|
|
```
|
|
|
|
**موجود در BFF (10%):**
|
|
- ✅ Query: GetUserCommissionPayouts (ولی ناقص)
|
|
|
|
**غایب در BFF (90%):**
|
|
- ❌ Query: GetMyBalance (موجودی کامل)
|
|
- ❌ Query: GetMyWithdrawalHistory (تاریخچه برداشتها)
|
|
- ❌ Query: GetMyWeeklyReport (گزارش هفتگی)
|
|
- ❌ Query: GetMyPoolShare (سهم من از پول)
|
|
- ❌ Query: GetMyDownlineCommissions (کمیسیون زیرمجموعههای من)
|
|
- ❌ Query: GetMyCommissionStatistics (آمار کمیسیونهای من)
|
|
- ❌ Command: RequestMyWithdrawal (درخواست برداشت)
|
|
- ❌ Command: CancelMyWithdrawal (لغو درخواست برداشت)
|
|
|
|
**UI غایب:**
|
|
- ❌ صفحه نمایش موجودی کامل
|
|
- ❌ صفحه درخواست برداشت
|
|
- ❌ صفحه تاریخچه برداشتها
|
|
- ❌ صفحه گزارش هفتگی
|
|
- ❌ صفحه سهم پول
|
|
- ❌ صفحه کمیسیون زیرمجموعهها
|
|
|
|
---
|
|
|
|
#### 4. UserWallet (Priority 1)
|
|
|
|
**موجود در CMS:**
|
|
```bash
|
|
ls CMS/src/CMSMicroservice.Application/UserWalletCQ/Queries/
|
|
# GetUserWallet/ ← مشتری میخواهد موجودی ببیند
|
|
# GetWalletTransactions/ ← مشتری میخواهد تراکنشها را ببیند
|
|
# GetDiscountTransactions/ ← مشتری میخواهد تراکنشهای تخفیف ببیند (اگر وجود دارد)
|
|
|
|
ls CMS/src/CMSMicroservice.Application/UserWalletCQ/Commands/
|
|
# ChargeWallet/ ← Admin یا Gateway
|
|
# WithdrawFromWallet/ ← مشتری میتواند برداشت کند
|
|
# TransferBetweenWallets/ ← مشتری میتواند انتقال دهد (اگر مجاز باشد)
|
|
```
|
|
|
|
**موجود در BFF (60%):**
|
|
- ✅ Query: GetUserWallet
|
|
- ✅ Query: GetWalletTransactions
|
|
- ✅ Command: ChargeWallet (ناقص)
|
|
- ⚠️ Command: WithdrawFromWallet (Handler خالی است)
|
|
|
|
**غایب در BFF (40%):**
|
|
- ❌ Query: GetDiscountTransactions (اگر در CMS هست)
|
|
- ❌ تکمیل WithdrawFromWallet Handler
|
|
- ❌ Command: TransferBetweenWallets (اگر مجاز باشد)
|
|
|
|
**UI غایب:**
|
|
- ❌ تب تراکنشهای تخفیف (اگر DiscountBalance موجود است)
|
|
- ❌ دکمه/فرم برداشت از کیف پول
|
|
- ❌ فرم انتقال بین کیف پولها
|
|
|
|
---
|
|
|
|
#### 5. ShoppingCart (Priority 1)
|
|
|
|
**موجود در CMS:**
|
|
```bash
|
|
ls CMS/src/CMSMicroservice.Application/ShoppingCartCQ/Commands/
|
|
# AddToCart/ ← مشتری اضافه میکند
|
|
# UpdateCartItemQuantity/ ← مشتری تغییر میدهد
|
|
# DeleteCartItem/ ← مشتری حذف میکند
|
|
# ClearCart/ ← مشتری پاک میکند
|
|
# MergeGuestCart/ ← سیستم ادغام میکند (بعد از Login)
|
|
# ApplyDiscountCode/ ← مشتری کد تخفیف وارد میکند
|
|
```
|
|
|
|
**موجود در BFF (50%):**
|
|
- ✅ Query: GetMyCart
|
|
- ✅ Command: AddToCart
|
|
- ✅ Command: UpdateCartItemQuantity
|
|
|
|
**غایب در BFF (50%):**
|
|
- ❌ Command: DeleteCartItem
|
|
- ❌ Command: ClearCart
|
|
- ❌ Command: MergeGuestCart
|
|
- ❌ Command: ApplyDiscountCode
|
|
|
|
**UI غایب:**
|
|
- ❌ دکمه حذف آیتم
|
|
- ❌ دکمه پاک کردن سبد
|
|
- ❌ فرم کد تخفیف
|
|
|
|
---
|
|
|
|
#### 6. Contract (Priority 2)
|
|
|
|
**موجود در CMS:**
|
|
```bash
|
|
ls CMS/src/CMSMicroservice.Application/ContractCQ/Queries/
|
|
# GetContract/ ← مشتری میخواهد قرارداد ببیند
|
|
# GetAllContracts/ ← مشتری میخواهد لیست قراردادها را ببیند
|
|
# GetContractDetails/ ← مشتری میخواهد جزئیات ببیند
|
|
|
|
ls CMS/src/CMSMicroservice.Application/ContractCQ/Commands/
|
|
# CreateContract/ ← سیستم ایجاد میکند
|
|
# UpdateContract/ ← Admin
|
|
# SignContract/ ← مشتری امضا میکند (اگر نیاز باشد)
|
|
```
|
|
|
|
**موجود در BFF (20%):**
|
|
- ✅ Command: CreateContract (ولی مشتری استفاده نمیکند - سیستم استفاده میکند)
|
|
|
|
**غایب در BFF (80%):**
|
|
- ❌ Query: GetMyContract
|
|
- ❌ Query: GetMyContracts
|
|
- ❌ Query: GetMyContractDetails
|
|
- ❌ Command: SignMyContract (اگر نیاز باشد)
|
|
|
|
**UI غایب:**
|
|
- ❌ صفحه لیست قراردادهای من
|
|
- ❌ صفحه جزئیات قرارداد
|
|
- ❌ دکمه امضای قرارداد
|
|
|
|
---
|
|
|
|
### 📋 خلاصه کارهای باقیمانده (فقط Customer-Facing)
|
|
|
|
| ماژول | Queries غایب | Commands غایب | UI Pages غایب | اولویت |
|
|
|-------|-------------|--------------|---------------|--------|
|
|
| **ClubMembership** | 3 | 1 | 2 | P0 🔥 |
|
|
| **NetworkMembership** | 4 | 0 | 3 | P0 🔥 |
|
|
| **Commission** | 7 | 2 | 6 | P0 🔥 |
|
|
| **UserWallet** | 1 | 1 (تکمیل) | 2 | P1 |
|
|
| **ShoppingCart** | 0 | 4 | 1 | P1 |
|
|
| **Contract** | 3 | 1 | 2 | P2 |
|
|
|
|
**جمع کل:**
|
|
- Queries: 18
|
|
- Commands: 9
|
|
- UI Pages: 16
|
|
|
|
---
|
|
|
|
### 🎯 توصیه نهایی برای Developer
|
|
|
|
#### اولویت 1 (حیاتی - باید حتماً باشد):
|
|
1. **Commission + Withdrawal**: مشتری باید بتواند پولش را ببیند و برداشت کند
|
|
2. **ClubMembership**: مشتری باید بتواند عضو باشگاه شود
|
|
3. **NetworkMembership**: مشتری باید درخت شبکه خود را ببیند
|
|
|
|
#### اولویت 2 (مهم):
|
|
4. **UserWallet Completion**: تکمیل برداشت + تخفیف
|
|
5. **ShoppingCart Completion**: حذف آیتم + پاک کردن سبد + کد تخفیف
|
|
|
|
#### اولویت 3 (نرمال):
|
|
6. **Contract**: نمایش قراردادها
|
|
|
|
---
|
|
|
|
### 💡 نکته بسیار مهم
|
|
|
|
**چیزهایی که حذف شدند (چون جدید هستند یا Admin هستند):**
|
|
- ❌ DayaLoan (فیچر جدید - هنوز در CMS کامل نیست)
|
|
- ❌ Manual Payment (فیچر جدید)
|
|
- ❌ Admin Commands در همه ماژولها (Approve, Reject, Recalculate, Adjust, ...)
|
|
- ❌ Admin Queries (GetAll, GetStatistics با دسترسی Admin)
|
|
|
|
**فقط روی اینها تمرکز کن:**
|
|
- ✅ Queries که مشتری میخواهد ببیند (GetMy...)
|
|
- ✅ Commands که مشتری میخواهد اجرا کند (ActivateMy..., RequestMy..., DeleteMy...)
|
|
- ✅ UI Pages که مشتری میخواهد استفاده کند
|
|
|
|
---
|
|
|
|
### 📊 تخمین زمان واقعی (بدون فیچرهای جدید)
|
|
|
|
| کار | زمان تخمینی |
|
|
|-----|-------------|
|
|
| Commission (7 Query + 2 Command + 6 Page) | 12 ساعت |
|
|
| ClubMembership (3 Query + 1 Command + 2 Page) | 6 ساعت |
|
|
| NetworkMembership (4 Query + 3 Page) | 8 ساعت |
|
|
| UserWallet Completion (1 Query + 1 تکمیل + 2 Page) | 3 ساعت |
|
|
| ShoppingCart Completion (4 Command + 1 Page) | 4 ساعت |
|
|
| Contract (3 Query + 1 Command + 2 Page) | 4 ساعت |
|
|
| **جمع کل** | **37 ساعت (تقریباً 5 روز کاری)** |
|
|
|
|
این زمان واقعبینانهتر است چون فیچرهای جدید (DayaLoan, Manual Payment) حذف شدند.
|
|
|
|
---
|
|
|
|
## 📝 اصطلاحات جایگزین (MLM-Sensitive Terminology)
|
|
|
|
> **آخرین بروزرسانی**: ۹ دی ۱۴۰۴ (29 دسامبر 2025)
|
|
|
|
برای جلوگیری از حساسیت مشتریان به کلمات مرتبط با MLM، از اصطلاحات جایگزین زیر در UI مشتری استفاده شود:
|
|
|
|
| کلمه حساس (فارسی) | جایگزین پیشنهادی | توضیح |
|
|
|-------------------|------------------|-------|
|
|
| کمیسیون | **پاداش** | Commission → Reward |
|
|
| شبکهسازی | **تیمسازی** | Network Building → Team Building |
|
|
| شبکه | **تیم** | Network → Team (در context MLM) |
|
|
| شاخه چپ/راست | **تیم اول/دوم** | Left/Right Leg → Team 1/2 |
|
|
| زیرمجموعه | **اعضای تیم** | Downline → Team Members |
|
|
| تعادل | **امتیاز/جفت** | Balance → Points/Pairs |
|
|
| درخت شبکه | **نمودار سازمانی** | Network Tree → Org Chart |
|
|
| سقف | **حداکثر** | Cap → Maximum |
|
|
| Binary | **دوبخشی** | Binary → Two-part |
|
|
|
|
### ⚠️ موارد استثنا (نباید تغییر کنند):
|
|
- **شبکههای اجتماعی** - Social Networks (مرتبط با MLM نیست)
|
|
- **درخت دستهبندی** - Category Tree (مرتبط با محصولات)
|
|
- **پنل ادمین (BackOffice)** - نیاز به صراحت اصطلاحات دارد
|
|
|
|
### ✅ فایلهای تغییر یافته (۹ دی):
|
|
- `WeeklyBalancePage.razor` - کمیسیون → پاداش
|
|
- `CommissionDashboardPage.razor` - کمیسیون → پاداش
|
|
- `MyPackages.razor` - مشاهده شبکه → مشاهده تیم
|
|
- `Index.razor` - شبکهسازی → تیمسازی
|
|
- `About.razor` - شبکههای فروش → تیمهای فروش
|
|
- `Footer.razor` - شبکههای فروش → تیمهای فروش
|
|
- `NetworkStatisticsPage.razor` - آمار شبکه → آمار تیم
|