diff --git a/FRONTOFFICE-CMS-API-COMPATIBILITY.md b/FRONTOFFICE-CMS-API-COMPATIBILITY.md
index 5b7d08b..f31cdcb 100644
--- a/FRONTOFFICE-CMS-API-COMPATIBILITY.md
+++ b/FRONTOFFICE-CMS-API-COMPATIBILITY.md
@@ -1,6 +1,6 @@
# FrontOffice to CMS API Compatibility Analysis
-**تاریخ:** 5 فوریه 2026
+**تاریخ:** 6 فوریه 2026
**وضعیت:** در حال بررسی
## خلاصه اجرایی
@@ -40,8 +40,16 @@
|------------|-------------------|---------------|---------|
| `GetCustomerProducts()` | ProductService | ✅ موجود | **پیاده شد در Task قبل** |
| `GetCustomerProductsByFilter()` | ProductService | ✅ موجود | **پیاده شد در Task قبل** |
+| `GetAllProductsByFilter()` | Products.razor | ✅ پیاده شد | **Public API - Feb 6, 2026** |
-**نتیجه:** ✅ تمام Products APIs موجود است
+**GetAllProductsByFilter Details:**
+- از `GetCustomerProductsByFilterQuery` استفاده میکند
+- پشتیبانی از فیلترها: Title, Price, Discount, CategoryId, SaleCount, و...
+- Sorting: پشتیبانی کامل (مثلاً "price desc")
+- Pagination: با MetaData کامل
+- CategoryIds: لیست شناسه دستهبندیهای محصول
+
+**نتیجه:** ✅ تمام Products APIs موجود و پیاده شده
---
@@ -51,12 +59,18 @@
| API Method | استفاده در Service | Status در CMS | یادداشت |
|------------|-------------------|---------------|---------|
-| `GetAllCategories()` | CategoryService | ✅ موجود | Admin API: `GetAllCategoryByFilter()` |
+| `GetAllCategoriesForCustomer()` | CategoryService | ✅ پیاده شد | **Customer API - Feb 6, 2026** |
| `GetCategoryById()` | CategoryService | ✅ موجود | Admin API: `GetCategory()` |
-**یادداشت:** CategoryService در FrontOffice از Admin APIs استفاده میکند (بدون احراز هویت). این مشکلی ندارد چون Categories عمومی هستند.
+**GetAllCategoriesForCustomer Details:**
+- از `GetAllCategoryByFilterQuery` استفاده میکند
+- فقط دستهبندیهای فعال (IsActive = true)
+- مرتبسازی بر اساس SortOrder
+- پشتیبانی Pagination (default: PageSize=100)
+- شامل: Id, Name, Title, Description, ImagePath, ParentId, IsActive, SortOrder
+- ISender به CategoryService اضافه شد
-**نتیجه:** ✅ Category APIs موجود است
+**نتیجه:** ✅ تمام Category APIs پیاده شده
---
@@ -66,12 +80,127 @@
| API Method | استفاده در Service | Status در CMS | یادداشت |
|------------|-------------------|---------------|---------|
-| `GetAllUserOrderByFilter()` | OrderService | ✅ موجود | Proto: `GetCustomerOrders()` **پیاده شد** |
-| `GetUserOrder()` | OrderService | ✅ موجود | Proto: `GetCustomerOrder()` **پیاده شد** |
-| `GetUserOrderHistory()` | OrderService | ⚠️ نیاز به بررسی | Proto: `GetCustomerOrderHistory()` **پیاده شد** |
-| `PlaceOrder()` (Checkout) | Checkout.razor | ❓ نیاز به بررسی | باید از Transaction یا UserOrder باشد |
+| `GetAllUserOrderByFilter()` | OrderService, Orders.razor | ✅ پیاده شد | **Feb 6, 2026** - Admin API |
+| `GetUserOrder()` | OrderService, OrderDetail.razor | ✅ پیاده شد | **Feb 6, 2026** - جزئیات کامل سفارش |
+| `GetCustomerOrders()` | OrderService | ✅ موجود | Customer API با فیلتر UserId |
+| `GetCustomerOrder()` | OrderService | ✅ موجود | Customer API با فیلتر UserId |
+| `GetUserOrderHistory()` | OrderService | ✅ موجود | Proto: `GetCustomerOrderHistory()` |
+| `GetVATRate()` | VATService, OrderService | ✅ پیاده شد | **Feb 6, 2026** |
+| `SubmitShopBuyOrder()` | CheckoutSummary.razor | ✅ پیاده شد | **Feb 6, 2026** - تکمیل فرآیند خرید |
-**نتیجه:** ⚠️ نیاز به بررسی PlaceOrder workflow
+**GetVATRate Details:**
+- نرخ مالیات بر ارزش افزوده ایران: 9%
+- `VatRate = 0.09` (decimal)
+- `VatPercentage = 9` (int)
+- `IsEnabled = true`
+- استفاده در VATService برای محاسبه مالیات محصولات
+
+**SubmitShopBuyOrder Details (Feb 6, 2026 - Updated with Wallet Payment):**
+تبدیل سبد خرید به سفارش نهایی با پرداخت از کیف پول:
+
+1. **احراز هویت**: استخراج UserId از JWT Token (ICurrentUserService)
+2. **اعتبارسنجی سبد خرید**:
+ - بازیابی محصولات سبد خرید با Include(Product)
+ - چک کردن خالی نبودن سبد
+3. **اعتبارسنجی آدرس**:
+ - دریافت آدرس پیشفرض کاربر
+ - اجباری بودن وجود آدرس
+4. **محاسبات مالی**:
+ - مبلغ پایه: جمع (قیمت × تعداد) تمام آیتمها
+ - مالیات: 9% از مبلغ پایه
+ - مبلغ کل: مبلغ پایه + مالیات
+ - اعتبارسنجی مبلغ: |serverTotal - clientTotal| < 100
+5. **اعتبارسنجی کیف پول (New - Feb 6)**:
+ - بازیابی کیف پول کاربر (UserWallet)
+ - چک موجودی: Balance >= TotalAmount
+ - خطا در صورت کمبود موجودی با نمایش موجودی فعلی و مبلغ مورد نیاز
+6. **ایجاد تراکنش (New - Feb 6)**:
+ - Type: TransactionType.Buy (0)
+ - Amount: TotalAmount
+ - PaymentStatus: Success
+ - PaymentDate: DateTime.UtcNow
+ - RefId: SHOP_{timestamp}
+ - Description: "خرید محصولات - سفارش #{OrderId}"
+7. **کسر از کیف پول (New - Feb 6)**:
+ - Balance -= TotalAmount
+ - ثبت موجودی جدید در UserWallet
+8. **لاگ تغییرات کیف پول (New - Feb 6)**:
+ - CurrentBalance: موجودی جدید
+ - ChangeValue: -TotalAmount (منفی برای برداشت)
+ - CurrentNetworkBalance: بدون تغییر
+ - CurrentDiscountBalance: بدون تغییر
+ - IsIncrease: false (برداشت)
+ - RefrenceId: TransactionId
+9. **ایجاد سفارش (UserOrder) - Updated**:
+ - TransactionId: لینک به تراکنش (New)
+ - PaymentStatus: Success (Changed from Pending)
+ - PaymentDate: DateTime.UtcNow (New)
+ - PaymentMethod: Wallet (New)
+ - DeliveryStatus: Pending
+ - HasVAT: true
+10. **ثبت مالیات (OrderVAT)**:
+ - VATRate: 0.09m (decimal)
+ - BaseAmount: مبلغ قبل از مالیات
+ - VATAmount: مبلغ مالیات
+ - TotalAmount: مبلغ کل
+11. **جزئیات فاکتور (FactorDetails)**:
+ - یک رکورد برای هر آیتم سبد خرید
+ - ذخیره ProductId, Count, UnitPrice, UnitDiscountPrice
+12. **پاکسازی سبد خرید**:
+ - Soft delete تمام آیتمهای سبد (IsDeleted = true)
+
+**Transaction Flow:**
+```
+User → Cart → SubmitShopBuyOrder →
+1. Validate Cart
+2. Validate Address
+3. Calculate Amount (Base + 9% VAT)
+4. Validate Wallet Balance
+5. Create Transaction (Type=Buy, Status=Success)
+6. Deduct from Wallet.Balance
+7. Create UserWalletChangeLog (audit trail)
+8. Create Order (linked to Transaction, PaymentStatus=Success, PaymentMethod=Wallet)
+9. Create OrderVAT
+10. Create FactorDetails
+11. Clear Cart
+→ Return OrderId
+```
+
+**Wallet Types:**
+- **Balance** (موجودی عادی): Used for purchases - deducted in this flow
+- **NetworkBalance** (موجودی شبکه): Commission wallet - not touched
+- **DiscountBalance** (موجودی تخفیف): Discount-only wallet - not touched
+
+**Error Handling:**
+- "کیف پول یافت نشد": User has no wallet record
+- "موجودی کیف پول کافی نیست. موجودی: X تومان، مورد نیاز: Y تومان": Insufficient funds
+
+**خروجی**: شناسه سفارش (OrderId) برای redirect به صفحه جزئیات
+
+**GetUserOrder Details (Feb 6, 2026):**
+نمایش جزئیات کامل یک سفارش:
+- اطلاعات سفارش: Id, Amount, PaymentStatus, PaymentDate, DeliveryStatus
+- اطلاعات کاربر: UserFullName, UserNationalCode
+- آدرس: UserAddressText
+- مالیات (OrderVAT): VATRate, BaseAmount, VATAmount, TotalAmount, IsPaid
+- ردیابی: TrackingCode, DeliveryDescription
+- محصولات (FactorDetails): ProductId, ProductTitle, ProductThumbnailPath, UnitPrice, Count, UnitDiscountPrice
+
+**اصلاحات صفحه OrderDetail.razor:**
+- ✅ رفع NullReferenceException برای PaymentDate
+- ✅ نمایش "تاریخ ثبت" برای سفارشات Pending (بدون PaymentDate)
+- ✅ رفع نمایش اشتباه ProductThumbnailPath به جای ProductTitle
+- ✅ رفع خطاهای nullable value access (.Value → ?? 0)
+- ✅ محاسبه صحیح subtotal با nullable handling
+
+**GetAllUserOrderByFilter Details (Feb 6, 2026):**
+لیست تمام سفارشات با فیلترهای پیشرفته:
+- فیلترها: UserId (optional - 0 = همه کاربران), PaymentStatus, DeliveryStatus, PaymentDate
+- Pagination: MetaData کامل
+- Sorting: بر اساس فیلدهای مختلف
+- جزئیات هر سفارش: اطلاعات کاربر، آدرس، مالیات، محصولات، وضعیت ارسال
+
+**نتیجه:** ✅ تمام UserOrder APIs پیاده شده - فرآیند خرید کامل است
---
@@ -81,13 +210,32 @@
| API Method | استفاده در Service | Status در CMS | یادداشت |
|------------|-------------------|---------------|---------|
-| `GetCustomerWallet()` | WalletService | ✅ موجود | **پیاده شد در Task قبل** |
-| `GetCustomerWalletChangeLog()` | WalletService | ✅ موجود | **پیاده شد در Task قبل** |
-| `CustomerWithdrawBalance()` | WalletService | ✅ موجود | **پیاده شد در Task قبل** |
-| `GetCustomerWithdrawals()` | WithdrawalRequests.razor | ✅ موجود | **پیاده شد در Task قبل** |
-| `GetCustomerWithdrawalSettings()` | WalletService | ✅ موجود | **پیاده شد در Task قبل** |
+| `GetCustomerWallet()` | WalletService | ✅ موجود | 3 نوع کیف پول: Balance, NetworkBalance, DiscountBalance |
+| `GetCustomerWalletChangeLog()` | WalletService | ✅ موجود | 6 فیلد موجودی: Current+Change برای هر 3 کیف پول |
+| `CustomerWithdrawBalance()` | WalletService | ✅ موجود | Proto موجود است |
+| `GetCustomerWithdrawals()` | WithdrawalRequests.razor | ✅ موجود | لیست درخواستهای برداشت |
+| `GetCustomerWithdrawalSettings()` | WalletService | ✅ موجود | حداقل مبلغ برداشت |
-**نتیجه:** ✅ تمام UserWallet APIs موجود است
+**سه نوع کیف پول:**
+1. **عادی (Regular)**: Balance & ChangeValue - برای خرید و شارژ عادی
+2. **شبکه (Network)**: NetworkBalance & ChangeNerworkValue - پاداش تیمی و کمیسیون
+3. **تخفیفی (Discount)**: DiscountBalance & ChangeDiscountValue - برای خرید تخفیفی
+
+**ساختار تراکنش (CustomerWalletChangeLogModel):**
+- `CurrentBalance` + `ChangeValue` - موجودی و تغییر کیف پول عادی
+- `CurrentNetworkBalance` + `ChangeNerworkValue` - موجودی و تغییر کیف پول شبکه
+- `CurrentDiscountBalance` + `ChangeDiscountValue` - موجودی و تغییر کیف پول تخفیفی
+- `IsIncrease` - آیا افزایش است یا کاهش
+- `RefrenceId` - شناسه ارجاع (سفارش، پرداخت، و...)
+- `CreatedAt` - تاریخ تراکنش (UTC Timestamp)
+
+**UI تراکنشها:**
+- Desktop: جدول با ستونهای جداگانه برای هر 3 کیف پول (تغییرات/مانده)
+- Mobile: کارتها با 3 باکس افقی (عادی آبی، شبکه سبز، تخفیفی زرد)
+- تاریخ: تبدیل UTC به Local Time و نمایش جلالی
+- توضیحات: نمایش اینکه کدام کیف پولها تغییر کردهاند
+
+**نتیجه:** ✅ تمام UserWallet APIs موجود و پیاده شده با UI کامل (Feb 5, 2026)
---
@@ -117,7 +265,24 @@
| `UpdateCustomerCartItem()` | CartService | ✅ پیاده شد | **Command Handler تکمیل شد - Feb 5** |
| `RemoveFromCustomerCart()` | CartService | ✅ پیاده شد | **Command Handler تکمیل شد - Feb 5** |
-**نتیجه:** ✅ تمام UserCart Customer APIs پیاده شده
+**اصلاحات Feb 6, 2026:**
+- ✅ **رفع باگ Cart APIs در CheckoutSummary**: تمام صفحات از Admin APIs استفاده میکردند
+- ✅ تغییر `AddNewUserCartAsync` → `AddNewUserCartForCustomerAsync`
+- ✅ تغییر `UpdateUserCartAsync` → `UpdateUserCartForCustomerAsync`
+- ✅ تغییر request model: `AddNewUserCartRequest` → `AddNewUserCartForCustomerRequest`
+- ✅ تغییر request model: `UpdateUserCartRequest` → `UpdateUserCartForCustomerRequest`
+- ✅ اضافه `RemoveUserCartForCustomerAsync` برای حذف صحیح آیتم
+- ✅ اصلاح field name: `UserCartId` → `CartItemId` (Proto: cart_item_id)
+- ✅ رفع منطق حذف: از Update با Count=0 به RemoveUserCartForCustomer تغییر یافت
+
+**Field Naming Convention:**
+- Proto: `cart_item_id` (snake_case)
+- C# Generated: `CartItemId` (PascalCase)
+- ❌ نباید: `UserCartId` (نام قدیمی Admin API)
+
+**تاثیر:** حالا عملیات سبد خرید (افزودن/ویرایش/حذف) صحیح کار میکند و فقط سبد کاربر جاری را تغییر میدهد
+
+**نتیجه:** ✅ تمام UserCart Customer APIs پیاده شده و باگهای Security و Field Naming رفع شد
---
@@ -135,7 +300,19 @@
**یادداشت:** CityName و ProvinceName در response خالی است - FrontOffice باید از City API جداگانه استفاده کند.
-**نتیجه:** ✅ تمام UserAddress Customer APIs پیاده شده
+**اصلاحات Feb 6, 2026:**
+- ✅ **رفع باگ صفحه Addresses**: تمام صفحات FrontOffice از Admin APIs استفاده میکردند
+- ✅ تغییر `GetAllUserAddressByFilter` → `GetCustomerAddresses` در Addresses.razor
+- ✅ تغییر `CreateNewUserAddress` → `CreateCustomerAddress` در AddAddressDialog
+- ✅ تغییر `UpdateUserAddress` → `UpdateCustomerAddress` در EditAddressDialog
+- ✅ تغییر `DeleteUserAddress` → `DeleteCustomerAddress` در Addresses.razor
+- ✅ تغییر `SetAddressAsDefault` → `SetCustomerDefaultAddress` در Addresses.razor
+- ✅ اصلاح Model type: `GetAllUserAddressByFilterResponseModel` → `CustomerAddressModel`
+- ✅ اصلاح field name: `response.Addresses` → `response.Models`
+
+**تاثیر:** حالا کاربران فقط آدرسهای خودشان را میبینند (قبلاً همه آدرسها نمایش داده میشد)
+
+**نتیجه:** ✅ تمام UserAddress Customer APIs پیاده شده و باگ Security رفع شد
---
@@ -173,11 +350,23 @@
| API Method | استفاده در Service | Status در CMS | یادداشت |
|------------|-------------------|---------------|---------|
-| `GetMyNetworkTree()` | NetworkMembershipService | ✅ موجود | **پیاده شد در Task قبل** |
-| `GetSubordinateTree()` | NetworkMembershipService | ✅ موجود | **پیاده شد در Task قبل** |
-| `GetMyNetworkStatistics()` | NetworkStatisticsPage.razor | ✅ موجود | **پیاده شد در Task قبل** |
+| `GetMyNetworkTree()` | NetworkMembershipService | ✅ موجود | Customer Query جداگانه با ICurrentUserService |
+| `GetSubordinateTree()` | NetworkMembershipService | ✅ موجود | Recursive tree traversal |
+| `GetMyNetworkStatistics()` | NetworkStatisticsPage.razor | ✅ موجود | با شمارش recursive تمام descendants |
-**نتیجه:** ✅ تمام NetworkMembership APIs موجود است
+**اصلاحات انجام شده (Feb 5, 2026):**
+1. ✅ **GetMyNetworkTree Customer Query**:
+ - ایجاد Query و Handler جداگانه برای Customer
+ - استفاده از ICurrentUserService به جای UserId در request
+ - رفع خطای Validation (UserId=0 قبلاً غیرمجاز بود)
+
+2. ✅ **GetNetworkStatistics Bug Fix**:
+ - قبلاً: فقط direct children (depth=1) شمارش میشد
+ - بعد: recursive counting تمام descendants در leftLeg و rightLeg
+ - متدهای کمکی: `GetAllDescendants()` و `CalculateDepths()`
+ - فرمول: `leftLegCount = GetAllDescendants(leftChild).Count + 1`
+
+**نتیجه:** ✅ تمام NetworkMembership APIs موجود و اصلاح شده
---
@@ -242,25 +431,31 @@
5. ✅ Package APIs - تمام Customer endpoints پیاده شده
6. ✅ NetworkMembership APIs - پیاده شده
7. ✅ Commission APIs - پیاده شده
-8. ✅ Category APIs - از Admin API استفاده میکند (OK)
+8. ✅ Category APIs - GetAllCategoriesForCustomer پیاده شد (Feb 6, 2026)
9. ✅ City APIs - Public API موجود
10. ✅ ClubMembership APIs - Proto موجود
11. ✅ Configuration APIs - Proto موجود
12. ✅ AppVersion APIs - Proto موجود
-13. ✅ **UserCarts APIs - تمام Customer endpoints پیاده شد (Feb 5, 2026)** 🆕
-14. ✅ **UserAddress APIs - تمام Customer endpoints پیاده شد (Feb 5, 2026)** 🆕
+13. ✅ **UserCarts APIs - تمام Customer endpoints پیاده شد (Feb 5, 2026) + اصلاحات Feb 6** 🆕
+14. ✅ **UserAddress APIs - تمام Customer endpoints پیاده شد (Feb 5, 2026) + باگ Security رفع شد Feb 6** 🆕
+15. ✅ **UserOrder APIs - Checkout workflow کامل شد (Feb 6, 2026)** 🆕
+16. ✅ **Products APIs - GetAllProductsByFilter پیاده شد (Feb 6, 2026)** 🆕
### ⚠️ نیاز به توجه
~~1. **UserCarts APIs** - نیاز به Customer-specific endpoints~~
- **✅ تکمیل شد - Feb 5, 2026**
+ **✅ تکمیل شد - Feb 5, 2026 + اصلاحات Feb 6, 2026**
~~2. **UserAddress APIs** - نیاز به Customer-specific endpoints~~
- **✅ تکمیل شد - Feb 5, 2026**
+ **✅ تکمیل شد - Feb 5, 2026 + باگ Security رفع شد Feb 6, 2026**
-3. **UserOrder/Checkout APIs** - نیاز به بررسی:
- - `PlaceOrder()` یا `CreateOrder()` - ❓ نیاز به بررسی workflow
- - `CancelOrder()` - ❓ نیاز به بررسی
+~~3. **UserOrder/Checkout APIs** - نیاز به بررسی~~
+ **✅ تکمیل شد - Feb 6, 2026:**
+ - ✅ SubmitShopBuyOrder - تبدیل سبد خرید به سفارش
+ - ✅ GetUserOrder - نمایش جزئیات سفارش
+ - ✅ GetAllUserOrderByFilter - لیست سفارشات
+ - ✅ GetVATRate - دریافت نرخ مالیات 9%
+ - ✅ OrderDetail.razor - رفع باگهای NullReference
4. **UpdateCustomerProfile, ChangeCustomerPassword, UpdateCustomerSettings** - Proto موجود اما Query/Handler نیاز است
@@ -277,6 +472,11 @@
- ✅ RemoveFromCustomerCartCommand و Handler
- ✅ UserCartsService با ISender
+**✅ اصلاحات Security - Feb 6, 2026:**
+- ✅ CartService.cs: تمام عملیات به Customer APIs تغییر یافت
+- ✅ رفع باگ Field Naming: UserCartId → CartItemId
+- ✅ رفع منطق حذف: از Update به RemoveUserCartForCustomer
+
~~### Priority 2: UserAddress Customer Endpoints~~
~~این APIs برای Checkout و مدیریت آدرسها ضروری هستند.~~
**✅ تکمیل شد - Feb 5, 2026:**
@@ -288,7 +488,15 @@
- ✅ UserAddressService با ISender
- ⚠️ **یادداشت:** CityName/ProvinceName در response خالی است - FrontOffice باید از City API استفاده کند
-### Priority 3: Checkout/Order Creation
+**✅ اصلاحات Security - Feb 6, 2026:**
+- ✅ Addresses.razor: GetCustomerAddresses (قبلاً تمام آدرسها نمایش مییافت)
+- ✅ Index.razor (Profile): GetCustomerAddresses
+- ✅ CheckoutSummary.razor: GetCustomerAddresses
+- ✅ Checkout.razor: GetCustomerAddresses
+- ✅ AddAddressDialog.razor: CreateCustomerAddress
+- ✅ EditAddressDialog.razor: UpdateCustomerAddress
+
+~~### Priority 3: Checkout/Order Creation~~
باید workflow ثبت سفارش بررسی شود.
### Priority 4: Customer Profile Updates
@@ -298,14 +506,101 @@
## وضعیت پروژه
-**تکمیل شده:** ~95%
-**آخرین بهروزرسانی:** 5 فوریه 2026
+**تکمیل شده:** ~97%
+**آخرین بهروزرسانی:** 6 فوریه 2026
-**تغییرات امروز:**
+**تغییرات Feb 6, 2026:**
+
+**Phase 1: رفع باگهای Critical Security در FrontOffice**
+- ✅ **UserAddress Security Bug Fix**: تغییر از Admin APIs به Customer APIs در تمام صفحات
+ - Addresses.razor, Index.razor (Profile), CheckoutSummary.razor, Checkout.razor
+ - AddAddressDialog, EditAddressDialog
+ - قبلاً همه آدرسهای تمام کاربران نمایش داده میشد ⚠️
+ - حالا فقط آدرسهای کاربر لاگین شده (با ICurrentUserService)
+
+- ✅ **UserCart Security Bug Fix**: تغییر از Admin APIs به Customer APIs در CartService
+ - تمام عملیات: Add, Update, Remove, Clear
+ - رفع باگ Field Naming: UserCartId → CartItemId (Proto: cart_item_id)
+ - رفع منطق حذف: از UpdateUserCart با Count=0 به RemoveUserCartForCustomer
+ - قبلاً تمام سبدهای خرید تمام کاربران قابل دسترسی بود ⚠️
+
+**Phase 2: پیادهسازی APIs گمشده**
+- ✅ **GetVATRate**: پیادهسازی در UserOrderService
+ - نرخ مالیات بر ارزش افزوده ایران: 9%
+ - استفاده در VATService و Products page
+
+- ✅ **GetAllProductsByFilter**: پیادهسازی در ProductsService
+ - استفاده از GetCustomerProductsByFilterQuery
+ - پشتیبانی کامل از filtering, sorting, pagination
+ - CategoryIds mapping به درستی
+
+- ✅ **GetAllCategoriesForCustomer**: پیادهسازی در CategoryService
+ - استفاده از GetAllCategoryByFilterQuery
+ - فقط دستهبندیهای فعال (IsActive = true)
+ - ISender به CategoryService اضافه شد
+ - مرتبسازی بر اساس SortOrder
+
+**Phase 3: تکمیل Checkout Workflow**
+- ✅ **SubmitShopBuyOrder**: تبدیل سبد خرید به سفارش نهایی با **پرداخت از کیف پول** (Updated Feb 6)
+ - احراز هویت با ICurrentUserService (UserId از JWT)
+ - اعتبارسنجی سبد خرید (خالی نباشد) و آدرس پیشفرض
+ - محاسبات مالی: مبلغ پایه + مالیات 9% = مبلغ کل
+ - **اعتبارسنجی موجودی کیف پول**: Balance >= TotalAmount 🆕
+ - **ایجاد تراکنش**: Type=Buy, PaymentStatus=Success, RefId=SHOP_{timestamp} 🆕
+ - **کسر از کیف پول**: Balance -= TotalAmount 🆕
+ - **ثبت لاگ تغییرات**: UserWalletChangeLog با تمام جزئیات (audit trail) 🆕
+ - ایجاد سفارش (UserOrder): **PaymentStatus=Success, PaymentMethod=Wallet, TransactionId** (Updated from Pending)
+ - ثبت مالیات (OrderVAT): VATRate, BaseAmount, VATAmount, TotalAmount
+ - ایجاد جزئیات فاکتور (FactorDetails) برای هر محصول
+ - پاکسازی سبد خرید (soft delete)
+ - بازگشت OrderId برای redirect
+ - **خطاها**: "کیف پول یافت نشد", "موجودی کیف پول کافی نیست"
+
+- ✅ **GetUserOrder**: نمایش جزئیات کامل سفارش
+ - استفاده از GetCustomerOrderQuery
+ - اطلاعات سفارش + کاربر + آدرس + مالیات + محصولات + ردیابی
+ - پشتیبانی از nullable fields (PaymentDate, PaymentMethod)
+
+- ✅ **GetAllUserOrderByFilter**: لیست سفارشات با فیلتر
+ - Admin API - میتواند همه سفارشات را ببیند
+ - فیلترها: UserId, PaymentStatus, DeliveryStatus, PaymentDate
+ - Pagination + Sorting کامل
+
+- ✅ **OrderDetail.razor - رفع باگهای UI**:
+ - رفع NullReferenceException برای PaymentDate (null برای سفارشات Pending)
+ - نمایش "تاریخ ثبت" به جای "تاریخ پرداخت" برای سفارشات بدون پرداخت
+ - رفع نمایش ProductThumbnailPath به جای ProductTitle
+ - رفع خطاهای nullable value access: .Value → ?? 0
+ - محاسبه صحیح subtotal با null coalescing
+
+**خلاصه تغییرات:**
+- 🔒 **Security**: رفع باگهای critical در UserAddress و UserCart (همه کاربران قابل مشاهده بودند)
+- 📦 **Products**: GetAllProductsByFilter + GetAllCategoriesForCustomer پیاده شد
+- 💰 **VAT**: GetVATRate با نرخ 9% ایران
+- 🛒 **Checkout**: workflow کامل - سبد خرید → سفارش → نمایش جزئیات
+- 🐛 **Bug Fixes**: OrderDetail null handling + Field naming (UserCartId → CartItemId)
+
+**تغییرات قبلی (Feb 5, 2026):**
+
+**Phase 1: UserCart & UserAddress Customer Endpoints**
- ✅ پیادهسازی کامل UserCart Customer endpoints (4 Handler + Service)
- ✅ پیادهسازی کامل UserAddress Customer endpoints (5 Handler + Service)
- ✅ اضافه کردن Proto definitions برای Customer Address
-- ✅ Build موفقیتآمیز: 0 Errors
+
+**Phase 2: NetworkMembership Bug Fixes**
+- ✅ GetMyNetworkTree Customer Query (رفع خطای Validation)
+- ✅ GetNetworkStatistics Recursive Counting (رفع باگ شمارش نادرست)
+
+**Phase 3: UserWallet UI Enhancement**
+- ✅ رفع باگ نمایش 0 در مبالغ تراکنشها
+- ✅ اضافه کردن CurrentDiscountBalance و ChangeDiscountValue به Proto (v0.0.177)
+- ✅ جداسازی تراکنشها به 3 نوع کیف پول (عادی، شبکه، تخفیفی)
+- ✅ اصلاح نامگذاری: "اعتباری" → "عادی"
+- ✅ رفع باگ تاریخ: اضافه کردن ToLocalTime() برای تبدیل UTC
+- ✅ UI Desktop: جدول با ستونهای جداگانه برای هر 3 کیف پول
+- ✅ UI Mobile: کارتها با 3 باکس افقی (عادی آبی، شبکه سبز، تخفیفی زرد)
+- ✅ نمایش همزمان تغییرات و موجودی مانده برای هر کیف پول
+- ✅ تغییر FrontOffice.Main.csproj: PackageReference → ProjectReference
**باقی مانده:**
- ⚠️ Checkout workflow و Order creation (نیاز به بررسی)
@@ -313,6 +608,12 @@
- 📝 CityName/ProvinceName در GetCustomerAddresses خالی است (نیاز به City API lookup در FrontOffice)
**Build Status:**
-- ✅ 0 Errors
-- ⚠️ ~315 Warnings (nullable reference types - غیر بحرانی)
+- ✅ CMS: 0 Errors, ~60 Warnings (unused proto imports)
+- ✅ FrontOffice: 0 Errors, ~120 Warnings (nullable references)
+
+**صفحات تست شده (Feb 6):**
+- ✅ /profile/addresses - کار میکند (فقط آدرسهای خود کاربر)
+- ✅ /products - کار میکند (لیست محصولات با filtering و sorting)
+- ✅ /categories - کار میکند (لیست دستهبندیهای فعال)
+- ✅ /profile/wallet - کار میکند (3 کیف پول با تراکنشهای کامل)
diff --git a/src/CMSMicroservice.Application/CommissionCQ/Queries/GetMyCommissionPayouts/GetMyCommissionPayoutsQuery.cs b/src/CMSMicroservice.Application/CommissionCQ/Queries/GetMyCommissionPayouts/GetMyCommissionPayoutsQuery.cs
new file mode 100644
index 0000000..46ec9cc
--- /dev/null
+++ b/src/CMSMicroservice.Application/CommissionCQ/Queries/GetMyCommissionPayouts/GetMyCommissionPayoutsQuery.cs
@@ -0,0 +1,25 @@
+using CMSMicroservice.Application.Common.Models;
+using CMSMicroservice.Domain.Enums;
+
+namespace CMSMicroservice.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
+
+///
+/// Query برای دریافت پرداختهای کمیسیون کاربر جاری (از JWT)
+///
+public record GetMyCommissionPayoutsQuery : IRequest
+{
+ ///
+ /// فیلتر وضعیت
+ ///
+ public CommissionPayoutStatus? Status { get; init; }
+
+ ///
+ /// شماره هفته (اختیاری)
+ ///
+ public long? WeekDefinitionId { get; init; }
+
+ ///
+ /// Pagination
+ ///
+ public PaginationState? PaginationState { get; init; }
+}
diff --git a/src/CMSMicroservice.Application/CommissionCQ/Queries/GetMyCommissionPayouts/GetMyCommissionPayoutsQueryHandler.cs b/src/CMSMicroservice.Application/CommissionCQ/Queries/GetMyCommissionPayouts/GetMyCommissionPayoutsQueryHandler.cs
new file mode 100644
index 0000000..6011d84
--- /dev/null
+++ b/src/CMSMicroservice.Application/CommissionCQ/Queries/GetMyCommissionPayouts/GetMyCommissionPayoutsQueryHandler.cs
@@ -0,0 +1,73 @@
+using CMSMicroservice.Application.Common.Interfaces;
+using CMSMicroservice.Application.Common.Extensions;
+using CMSMicroservice.Domain.Enums;
+using Microsoft.EntityFrameworkCore;
+
+namespace CMSMicroservice.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
+
+public class GetMyCommissionPayoutsQueryHandler : IRequestHandler
+{
+ private readonly IApplicationDbContext _context;
+ private readonly ICurrentUserService _currentUser;
+
+ public GetMyCommissionPayoutsQueryHandler(
+ IApplicationDbContext context,
+ ICurrentUserService currentUser)
+ {
+ _context = context;
+ _currentUser = currentUser;
+ }
+
+ public async Task Handle(GetMyCommissionPayoutsQuery request, CancellationToken cancellationToken)
+ {
+ // دریافت UserId از JWT (فقط برای Customer API)
+ if (!long.TryParse(_currentUser.UserId, out var userId))
+ {
+ throw new UnauthorizedAccessException("User not authenticated");
+ }
+
+ var query = _context.UserCommissionPayouts
+ .Include(x => x.WeekDefinition)
+ .Where(x => x.UserId == userId)
+ .AsNoTracking()
+ .AsQueryable();
+
+ // فیلترها
+ if (request.Status.HasValue)
+ {
+ query = query.Where(x => x.Status == request.Status.Value);
+ }
+
+ if (request.WeekDefinitionId.HasValue)
+ {
+ query = query.Where(x => x.WeekDefinitionId == request.WeekDefinitionId.Value);
+ }
+
+ // مرتبسازی: جدیدترین اول
+ query = query.OrderByDescending(x => x.Created);
+
+ var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
+
+ var models = await query
+ .PaginatedListAsync(paginationState: request.PaginationState)
+ .Select(x => new GetMyCommissionPayoutsResponseModel
+ {
+ Id = x.Id,
+ WeekDefinitionId = x.WeekDefinitionId,
+ WeekDisplayName = x.WeekDefinition != null ? x.WeekDefinition.DisplayName : "",
+ BalancesEarned = x.BalancesEarned,
+ TotalAmount = x.TotalAmount,
+ AmountFormatted = x.TotalAmount.ToString("N0") + " تومان",
+ Status = x.Status,
+ CalculatedDate = x.PaidAt ?? (DateTime?)x.Created,
+ DatePersian = ""
+ })
+ .ToListAsync(cancellationToken);
+
+ return new GetMyCommissionPayoutsResponseDto
+ {
+ MetaData = meta,
+ Models = models
+ };
+ }
+}
diff --git a/src/CMSMicroservice.Application/CommissionCQ/Queries/GetMyCommissionPayouts/GetMyCommissionPayoutsQueryValidator.cs b/src/CMSMicroservice.Application/CommissionCQ/Queries/GetMyCommissionPayouts/GetMyCommissionPayoutsQueryValidator.cs
new file mode 100644
index 0000000..63b3f81
--- /dev/null
+++ b/src/CMSMicroservice.Application/CommissionCQ/Queries/GetMyCommissionPayouts/GetMyCommissionPayoutsQueryValidator.cs
@@ -0,0 +1,25 @@
+using FluentValidation;
+
+namespace CMSMicroservice.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
+
+public class GetMyCommissionPayoutsQueryValidator : AbstractValidator
+{
+ public GetMyCommissionPayoutsQueryValidator()
+ {
+ RuleFor(x => x.PaginationState)
+ .NotNull()
+ .WithMessage("Pagination state is required");
+
+ When(x => x.PaginationState != null, () =>
+ {
+ RuleFor(x => x.PaginationState!.PageNumber)
+ .GreaterThan(0)
+ .WithMessage("Page number must be greater than 0");
+
+ RuleFor(x => x.PaginationState!.PageSize)
+ .GreaterThan(0)
+ .LessThanOrEqualTo(100)
+ .WithMessage("Page size must be between 1 and 100");
+ });
+ }
+}
diff --git a/src/CMSMicroservice.Application/CommissionCQ/Queries/GetMyCommissionPayouts/GetMyCommissionPayoutsResponseDto.cs b/src/CMSMicroservice.Application/CommissionCQ/Queries/GetMyCommissionPayouts/GetMyCommissionPayoutsResponseDto.cs
new file mode 100644
index 0000000..5f85a43
--- /dev/null
+++ b/src/CMSMicroservice.Application/CommissionCQ/Queries/GetMyCommissionPayouts/GetMyCommissionPayoutsResponseDto.cs
@@ -0,0 +1,23 @@
+using CMSMicroservice.Application.Common.Models;
+using CMSMicroservice.Domain.Enums;
+
+namespace CMSMicroservice.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
+
+public class GetMyCommissionPayoutsResponseDto
+{
+ public MetaData? MetaData { get; set; }
+ public List Models { get; set; } = new();
+}
+
+public class GetMyCommissionPayoutsResponseModel
+{
+ public long Id { get; set; }
+ public long WeekDefinitionId { get; set; }
+ public string WeekDisplayName { get; set; } = string.Empty;
+ public int BalancesEarned { get; set; }
+ public long TotalAmount { get; set; }
+ public string AmountFormatted { get; set; } = string.Empty;
+ public CommissionPayoutStatus Status { get; set; }
+ public DateTime? CalculatedDate { get; set; }
+ public string DatePersian { get; set; } = string.Empty;
+}
diff --git a/src/CMSMicroservice.Application/UserWalletCQ/Queries/GetCustomerWalletChangeLog/GetCustomerWalletChangeLogResponseDto.cs b/src/CMSMicroservice.Application/UserWalletCQ/Queries/GetCustomerWalletChangeLog/GetCustomerWalletChangeLogResponseDto.cs
index 721023b..0460c89 100644
--- a/src/CMSMicroservice.Application/UserWalletCQ/Queries/GetCustomerWalletChangeLog/GetCustomerWalletChangeLogResponseDto.cs
+++ b/src/CMSMicroservice.Application/UserWalletCQ/Queries/GetCustomerWalletChangeLog/GetCustomerWalletChangeLogResponseDto.cs
@@ -22,6 +22,16 @@ public class GetCustomerWalletChangeLogResponseDto
///
public long ChangeNerworkValue { get; set; }
+ ///
+ /// موجودی جاری تخفیف
+ ///
+ public long CurrentDiscountBalance { get; set; }
+
+ ///
+ /// مقدار تغییر تخفیف
+ ///
+ public long ChangeDiscountValue { get; set; }
+
///
/// افزایشی است؟
///
diff --git a/src/CMSMicroservice.Protobuf/Protos/userwallet.proto b/src/CMSMicroservice.Protobuf/Protos/userwallet.proto
index c036c24..76c31f4 100644
--- a/src/CMSMicroservice.Protobuf/Protos/userwallet.proto
+++ b/src/CMSMicroservice.Protobuf/Protos/userwallet.proto
@@ -160,9 +160,11 @@ message CustomerWalletChangeLogModel
int64 change_value = 2;
int64 current_network_balance = 3;
int64 change_nerwork_value = 4;
- bool is_increase = 5;
- google.protobuf.Int64Value refrence_id = 6;
- google.protobuf.Timestamp created_at = 7;
+ int64 current_discount_balance = 5;
+ int64 change_discount_value = 6;
+ bool is_increase = 7;
+ google.protobuf.Int64Value refrence_id = 8;
+ google.protobuf.Timestamp created_at = 9;
}
message CustomerWithdrawBalanceRequest
diff --git a/src/CMSMicroservice.WebApi/Common/Mappings/CommissionProfile.cs b/src/CMSMicroservice.WebApi/Common/Mappings/CommissionProfile.cs
index e75847c..001f3bd 100644
--- a/src/CMSMicroservice.WebApi/Common/Mappings/CommissionProfile.cs
+++ b/src/CMSMicroservice.WebApi/Common/Mappings/CommissionProfile.cs
@@ -1,8 +1,10 @@
using CMSMicroservice.Application.CommissionCQ.Queries.GetUserWeeklyBalances;
using CMSMicroservice.Application.CommissionCQ.Queries.GetAvailableWeeks;
using CMSMicroservice.Application.CommissionCQ.Queries.GetUserCommissionPayouts;
+using CMSMicroservice.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
using CMSMicroservice.Application.CommissionCQ.Queries.GetWeekDefinitions;
using CMSMicroservice.Application.Common.Models;
+using CMSMicroservice.Domain.Enums;
using CMSMicroservice.Protobuf.Protos.Commission;
using Google.Protobuf.WellKnownTypes;
using Mapster;
@@ -124,5 +126,38 @@ public class CommissionProfile : IRegister
: null)
.Map(dest => dest.IsExpired, src => src.IsExpired)
.Map(dest => dest.Created, src => Timestamp.FromDateTimeOffset(src.Created));
+
+ // GetMyCommissionPayouts Request Mapping
+ config.NewConfig()
+ .Map(dest => dest.Status, src => src.Status != null && src.Status.Value >= 0
+ ? (CommissionPayoutStatus?)src.Status.Value
+ : null)
+ .Map(dest => dest.WeekDefinitionId, src => src.WeekDefinitionId != null ? src.WeekDefinitionId.Value : (long?)null)
+ .Map(dest => dest.PaginationState, src => new PaginationState
+ {
+ PageNumber = src.PageNumber > 0 ? src.PageNumber : 1,
+ PageSize = src.PageSize > 0 ? src.PageSize : 10
+ });
+
+ // GetMyCommissionPayouts Response Mapping
+ config.NewConfig()
+ .Map(dest => dest.MetaData, src => src.MetaData != null
+ ? new CustomerMetaData { TotalCount = src.MetaData.TotalCount }
+ : new CustomerMetaData())
+ .Map(dest => dest.Payouts, src => src.Models);
+
+ // CustomerCommissionPayoutModel Mapping
+ config.NewConfig()
+ .Map(dest => dest.Id, src => src.Id)
+ .Map(dest => dest.WeekDefinitionId, src => src.WeekDefinitionId)
+ .Map(dest => dest.WeekDisplayName, src => src.WeekDisplayName)
+ .Map(dest => dest.BalancesEarned, src => src.BalancesEarned)
+ .Map(dest => dest.TotalAmount, src => src.TotalAmount)
+ .Map(dest => dest.AmountFormatted, src => src.AmountFormatted)
+ .Map(dest => dest.Status, src => (int)src.Status)
+ .Map(dest => dest.CalculatedDate, src => src.CalculatedDate.HasValue
+ ? Timestamp.FromDateTime(src.CalculatedDate.Value.ToUniversalTime())
+ : null)
+ .Map(dest => dest.DatePersian, src => src.DatePersian);
}
}
diff --git a/src/CMSMicroservice.WebApi/Services/CategoryService.cs b/src/CMSMicroservice.WebApi/Services/CategoryService.cs
index d5e0738..dc46a4b 100644
--- a/src/CMSMicroservice.WebApi/Services/CategoryService.cs
+++ b/src/CMSMicroservice.WebApi/Services/CategoryService.cs
@@ -5,14 +5,19 @@ using CMSMicroservice.Application.CategoryCQ.Commands.UpdateCategory;
using CMSMicroservice.Application.CategoryCQ.Commands.DeleteCategory;
using CMSMicroservice.Application.CategoryCQ.Queries.GetCategory;
using CMSMicroservice.Application.CategoryCQ.Queries.GetAllCategoryByFilter;
+using MediatR;
+using AppModels = CMSMicroservice.Application.Common.Models;
+
namespace CMSMicroservice.WebApi.Services;
public class CategoryService : CategoryContract.CategoryContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
+ private readonly ISender _sender;
- public CategoryService(IDispatchRequestToCQRS dispatchRequestToCQRS)
+ public CategoryService(IDispatchRequestToCQRS dispatchRequestToCQRS, ISender sender)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
+ _sender = sender;
}
public override async Task CreateNewCategory(CreateNewCategoryRequest request, ServerCallContext context)
{
@@ -45,20 +50,52 @@ public class CategoryService : CategoryContract.CategoryContractBase
public override async Task GetAllCategoriesForCustomer(GetAllCategoriesForCustomerRequest request, ServerCallContext context)
{
- // TODO: Implement using existing CMS Category Application layer
- // For now, return empty response
- return new GetAllCategoriesForCustomerResponse
+ // Use GetAllCategoryByFilter to get all active categories
+ var query = new GetAllCategoryByFilterQuery
+ {
+ Filter = new CMSMicroservice.Application.CategoryCQ.Queries.GetAllCategoryByFilter.GetAllCategoryByFilterFilter
+ {
+ IsActive = true // Only active categories for customers
+ },
+ PaginationState = new AppModels.PaginationState
+ {
+ PageNumber = request.PageNumber > 0 ? request.PageNumber : 1,
+ PageSize = request.PageSize > 0 ? request.PageSize : 100 // Default to large page size to get all
+ },
+ SortBy = "SortOrder" // Sort by display order
+ };
+
+ var result = await _sender.Send(query, context.CancellationToken);
+
+ var response = new GetAllCategoriesForCustomerResponse
{
MetaData = new CMSMicroservice.Protobuf.Protos.MetaData
{
- CurrentPage = request.PageNumber,
- PageSize = request.PageSize,
- TotalCount = 0,
- TotalPage = 0,
- HasNext = false,
- HasPrevious = false
+ CurrentPage = result.MetaData.CurrentPage,
+ PageSize = result.MetaData.PageSize,
+ TotalCount = result.MetaData.TotalCount,
+ TotalPage = result.MetaData.TotalPage,
+ HasNext = result.MetaData.HasNext,
+ HasPrevious = result.MetaData.HasPrevious
}
};
+
+ foreach (var cat in result.Models)
+ {
+ response.Models.Add(new GetAllCategoryFilterResponseModel
+ {
+ Id = cat.Id,
+ Name = cat.Name,
+ Title = cat.Title,
+ Description = cat.Description ?? string.Empty,
+ ImagePath = cat.ImagePath ?? string.Empty,
+ ParentId = cat.ParentId ?? 0,
+ IsActive = cat.IsActive,
+ SortOrder = cat.SortOrder
+ });
+ }
+
+ return response;
}
public override async Task GetCategoryByIdForCustomer(GetCategoryByIdForCustomerRequest request, ServerCallContext context)
diff --git a/src/CMSMicroservice.WebApi/Services/CommissionService.cs b/src/CMSMicroservice.WebApi/Services/CommissionService.cs
index a4c6799..dd89ade 100644
--- a/src/CMSMicroservice.WebApi/Services/CommissionService.cs
+++ b/src/CMSMicroservice.WebApi/Services/CommissionService.cs
@@ -19,6 +19,7 @@ using CMSMicroservice.Application.CommissionCQ.Queries.GetWorkerExecutionLogs;
using CMSMicroservice.Application.CommissionCQ.Queries.GetWithdrawalReports;
using CMSMicroservice.Application.CommissionCQ.Queries.GetAvailableWeeks;
using CMSMicroservice.Application.CommissionCQ.Queries.GetWeekDefinitions;
+using CMSMicroservice.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
namespace CMSMicroservice.WebApi.Services;
@@ -68,6 +69,11 @@ public class CommissionService : CommissionContract.CommissionContractBase
return await _dispatchRequestToCQRS.Handle(request, context);
}
+ public override async Task GetMyCommissionPayouts(GetMyCommissionPayoutsRequest request, ServerCallContext context)
+ {
+ return await _dispatchRequestToCQRS.Handle(request, context);
+ }
+
public override async Task GetCommissionPayoutHistory(GetCommissionPayoutHistoryRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle(request, context);
diff --git a/src/CMSMicroservice.WebApi/Services/ProductsService.cs b/src/CMSMicroservice.WebApi/Services/ProductsService.cs
index 70b45db..ba3e971 100644
--- a/src/CMSMicroservice.WebApi/Services/ProductsService.cs
+++ b/src/CMSMicroservice.WebApi/Services/ProductsService.cs
@@ -6,6 +6,7 @@ using CMSMicroservice.Application.ProductsCQ.Queries.GetCustomerProductsByFilter
using Mapster;
using AppModels = CMSMicroservice.Application.Common.Models;
using System.Collections.Generic;
+using System.Linq;
namespace CMSMicroservice.WebApi.Services;
@@ -39,7 +40,64 @@ public class ProductsService : ProductsContract.ProductsContractBase
public override async Task GetAllProductsByFilter(GetAllProductsByFilterRequest request, ServerCallContext context)
{
- throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
+ // Map to GetCustomerProductsByFilter query (public products API)
+ var query = new GetCustomerProductsByFilterQuery
+ {
+ Id = request.Filter?.Id,
+ Title = request.Filter?.Title ?? string.Empty,
+ Description = request.Filter?.Description ?? string.Empty,
+ ShortInfomation = request.Filter?.ShortInfomation ?? string.Empty,
+ FullInformation = request.Filter?.FullInformation ?? string.Empty,
+ Price = request.Filter?.Price,
+ Discount = request.Filter?.Discount,
+ Rate = request.Filter?.Rate,
+ SaleCount = request.Filter?.SaleCount,
+ ViewCount = request.Filter?.ViewCount,
+ RemainingCount = request.Filter?.RemainingCount,
+ CategoryIds = request.Filter?.CategoryId.HasValue == true
+ ? new List { request.Filter.CategoryId.Value }
+ : new List(),
+ SortBy = request.SortBy ?? string.Empty,
+ PaginationState = request.PaginationState != null
+ ? new AppModels.PaginationState
+ {
+ PageNumber = request.PaginationState.PageNumber,
+ PageSize = request.PaginationState.PageSize
+ }
+ : new AppModels.PaginationState { PageNumber = 1, PageSize = 20 }
+ };
+
+ var result = await _sender.Send(query, context.CancellationToken);
+
+ return new GetAllProductsByFilterResponse
+ {
+ MetaData = new CMSMicroservice.Protobuf.Protos.MetaData
+ {
+ CurrentPage = result.MetaData.CurrentPage,
+ TotalPage = result.MetaData.TotalPage,
+ PageSize = result.MetaData.PageSize,
+ TotalCount = result.MetaData.TotalCount,
+ HasPrevious = result.MetaData.HasPrevious,
+ HasNext = result.MetaData.HasNext
+ },
+ Models = { result.Models.Select(m => new GetAllProductsByFilterResponseModel
+ {
+ Id = m.Id,
+ Title = m.Title,
+ Description = m.Description,
+ ShortInfomation = m.ShortInfomation,
+ FullInformation = m.FullInformation,
+ Price = m.Price,
+ Discount = m.Discount,
+ Rate = m.Rate,
+ ImagePath = m.ImagePath,
+ ThumbnailPath = m.ThumbnailPath,
+ SaleCount = m.SaleCount,
+ ViewCount = m.ViewCount,
+ RemainingCount = m.RemainingCount,
+ CategoryIds = { m.Categories?.Select(c => c.CategoryId) ?? Enumerable.Empty() }
+ }) }
+ };
}
public override async Task BulkUpdateProductPrices(BulkUpdateProductPricesRequest request, ServerCallContext context)
diff --git a/src/CMSMicroservice.WebApi/Services/UserOrderService.cs b/src/CMSMicroservice.WebApi/Services/UserOrderService.cs
index 452ff6c..40af2aa 100644
--- a/src/CMSMicroservice.WebApi/Services/UserOrderService.cs
+++ b/src/CMSMicroservice.WebApi/Services/UserOrderService.cs
@@ -2,23 +2,33 @@ using CMSMicroservice.Protobuf.Protos.UserOrder;
using CMSMicroservice.Application.UserOrderCQ.Queries.GetCustomerOrders;
using CMSMicroservice.Application.UserOrderCQ.Queries.GetCustomerOrder;
using CMSMicroservice.Application.UserOrderCQ.Queries.GetCustomerOrderHistory;
+using CMSMicroservice.Application.Common.Interfaces;
+using CMSMicroservice.Domain.Entities;
+using CMSMicroservice.Domain.Entities.Order;
+using CMSMicroservice.Domain.Enums;
using AppModels = CMSMicroservice.Application.Common.Models;
using Grpc.Core;
using Google.Protobuf.WellKnownTypes;
using System.Collections.Generic;
+using System.Linq;
using CMSMicroservice.Protobuf.Protos;
using MediatR;
using Mapster;
+using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.WebApi.Services;
public class UserOrderService : UserOrderContract.UserOrderContractBase
{
private readonly ISender _sender;
+ private readonly IApplicationDbContext _context;
+ private readonly ICurrentUserService _currentUserService;
- public UserOrderService(ISender sender)
+ public UserOrderService(ISender sender, IApplicationDbContext context, ICurrentUserService currentUserService)
{
_sender = sender;
+ _context = context;
+ _currentUserService = currentUserService;
}
public override async Task CreateNewUserOrder(CreateNewUserOrderRequest request, ServerCallContext context)
{
@@ -37,17 +47,299 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
public override async Task GetUserOrder(GetUserOrderRequest request, ServerCallContext context)
{
- throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
+ var query = new GetCustomerOrderQuery
+ {
+ OrderId = request.Id,
+ UserId = 0 // از JWT دریافت میشود
+ };
+
+ var result = await _sender.Send(query, context.CancellationToken);
+
+ var response = new GetUserOrderResponse
+ {
+ Id = result.Id,
+ Amount = result.Amount,
+ PackageId = result.PackageId ?? 0,
+ TransactionId = result.TransactionId,
+ UserId = result.UserId,
+ UserAddressId = result.UserAddressId,
+ UserAddressText = result.UserAddressText,
+ TrackingCode = result.TrackingCode,
+ DeliveryDescription = result.DeliveryDescription,
+ UserFullName = result.UserFullName,
+ UserNationalCode = result.UserNationalCode
+ };
+
+ // VAT Info
+ if (result.VatAmount > 0)
+ {
+ response.VatInfo = new OrderVATInfo
+ {
+ VatRate = result.VatPercentage / 100,
+ BaseAmount = result.Amount - result.VatAmount,
+ VatAmount = result.VatAmount,
+ TotalAmount = result.Amount,
+ IsPaid = result.PaymentStatus == Domain.Enums.PaymentStatus.Success
+ };
+ }
+
+ response.PaymentStatus = (CMSMicroservice.Protobuf.Protos.PaymentStatus)result.PaymentStatus;
+ if (result.PaymentDate.HasValue)
+ response.PaymentDate = Timestamp.FromDateTime(DateTime.SpecifyKind(result.PaymentDate.Value, DateTimeKind.Utc));
+
+ if (result.PaymentMethod.HasValue)
+ response.PaymentMethod = (CMSMicroservice.Protobuf.Protos.PaymentMethod)result.PaymentMethod.Value;
+
+ response.DeliveryStatus = (CMSMicroservice.Protobuf.Protos.DeliveryStatus)result.DeliveryStatus;
+
+ foreach (var fd in result.FactorDetails)
+ {
+ response.FactorDetails.Add(new GetUserOrderResponseFactorDetail
+ {
+ ProductId = fd.ProductId,
+ ProductTitle = fd.ProductTitle,
+ ProductThumbnailPath = fd.ProductThumbnailPath,
+ UnitPrice = fd.UnitPrice,
+ Count = fd.Count,
+ UnitDiscountPrice = fd.UnitDiscountPrice
+ });
+ }
+
+ return response;
}
public override async Task GetAllUserOrderByFilter(GetAllUserOrderByFilterRequest request, ServerCallContext context)
{
- throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
+ // Admin API - can view all orders or filter by specific user
+ var query = new GetCustomerOrdersQuery
+ {
+ UserId = request.Filter?.UserId ?? 0, // 0 means all users (admin view)
+ PaginationState = request.PaginationState?.Adapt(),
+ PaymentStatusFilter = request.Filter?.PaymentStatus != null
+ ? (int?)request.Filter.PaymentStatus
+ : null,
+ DeliveryStatusFilter = request.Filter?.DeliveryStatus != null
+ ? (int?)request.Filter.DeliveryStatus
+ : null,
+ FromDate = request.Filter?.PaymentDate?.ToDateTime(),
+ ToDate = null
+ };
+
+ var result = await _sender.Send(query, context.CancellationToken);
+
+ var response = new GetAllUserOrderByFilterResponse
+ {
+ MetaData = result.MetaData.Adapt()
+ };
+
+ foreach (var model in result.Models)
+ {
+ var orderModel = new GetAllUserOrderByFilterResponseModel
+ {
+ Id = model.Id,
+ Amount = model.Amount,
+ PackageId = model.PackageId ?? 0,
+ TransactionId = model.TransactionId,
+ UserId = model.UserId,
+ UserAddressId = model.UserAddressId,
+ UserAddressText = model.UserAddressText,
+ TrackingCode = model.TrackingCode,
+ DeliveryDescription = model.DeliveryDescription,
+ UserFullName = model.UserFullName,
+ UserNationalCode = model.UserNationalCode,
+ VatAmount = model.VatAmount,
+ VatPercentage = model.VatPercentage
+ };
+
+ orderModel.PaymentStatus = (CMSMicroservice.Protobuf.Protos.PaymentStatus)model.PaymentStatus;
+ if (model.PaymentDate.HasValue)
+ orderModel.PaymentDate = Timestamp.FromDateTime(DateTime.SpecifyKind(model.PaymentDate.Value, DateTimeKind.Utc));
+
+ if (model.PaymentMethod.HasValue)
+ orderModel.PaymentMethod = (CMSMicroservice.Protobuf.Protos.PaymentMethod)model.PaymentMethod.Value;
+
+ orderModel.DeliveryStatus = (CMSMicroservice.Protobuf.Protos.DeliveryStatus)model.DeliveryStatus;
+
+ foreach (var fd in model.FactorDetails)
+ {
+ orderModel.FactorDetails.Add(new GetAllUserOrderByFilterResponseModelFactorDetail
+ {
+ ProductId = fd.ProductId,
+ ProductTitle = fd.ProductTitle,
+ ProductThumbnailPath = fd.ProductThumbnailPath,
+ UnitPrice = fd.UnitPrice,
+ Count = fd.Count,
+ UnitDiscountPrice = fd.UnitDiscountPrice
+ });
+ }
+
+ response.Models.Add(orderModel);
+ }
+
+ return response;
}
public override async Task SubmitShopBuyOrder(SubmitShopBuyOrderRequest request, ServerCallContext context)
{
- throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
+ // Get UserId from JWT or request
+ var userId = !string.IsNullOrEmpty(_currentUserService.UserId)
+ ? long.Parse(_currentUserService.UserId)
+ : request.UserId;
+
+ if (userId == 0)
+ {
+ throw new RpcException(new Status(StatusCode.Unauthenticated, "User not authenticated"));
+ }
+
+ // Get user's cart items with product details
+ var cartItems = await _context.UserCarts
+ .Include(c => c.Product)
+ .Where(c => c.UserId == userId && !c.IsDeleted)
+ .ToListAsync(context.CancellationToken);
+
+ if (!cartItems.Any())
+ {
+ throw new RpcException(new Status(StatusCode.FailedPrecondition, "سبد خرید خالی است"));
+ }
+
+ // Get user's default address
+ var defaultAddress = await _context.UserAddresses
+ .Where(a => a.UserId == userId && a.IsDefault && !a.IsDeleted)
+ .FirstOrDefaultAsync(context.CancellationToken);
+
+ if (defaultAddress == null)
+ {
+ throw new RpcException(new Status(StatusCode.FailedPrecondition, "آدرس پیشفرض یافت نشد"));
+ }
+
+ // Calculate amounts
+ const decimal vatRate = 0.09m;
+ long baseAmount = cartItems.Sum(c => c.Product.Price * c.Count);
+ long vatAmount = (long)(baseAmount * vatRate);
+ long totalAmount = baseAmount + vatAmount;
+
+ // Validate total amount
+ if (request.TotalAmount > 0 && Math.Abs(totalAmount - request.TotalAmount) > 100)
+ {
+ throw new RpcException(new Status(StatusCode.InvalidArgument,
+ $"مبلغ نامعتبر است. محاسبه شده: {totalAmount}, دریافتی: {request.TotalAmount}"));
+ }
+
+ // Get user's wallet
+ var wallet = await _context.UserWallets
+ .Where(w => w.UserId == userId)
+ .FirstOrDefaultAsync(context.CancellationToken);
+
+ if (wallet == null)
+ {
+ throw new RpcException(new Status(StatusCode.FailedPrecondition, "کیف پول یافت نشد"));
+ }
+
+ // Check wallet balance
+ if (wallet.Balance < totalAmount)
+ {
+ throw new RpcException(new Status(StatusCode.FailedPrecondition,
+ $"موجودی کیف پول کافی نیست. موجودی: {wallet.Balance:N0} تومان، مورد نیاز: {totalAmount:N0} تومان"));
+ }
+
+ // Create transaction
+ var transaction = new Transaction
+ {
+ Amount = totalAmount,
+ Description = $"خرید محصولات - سفارش شماره در حال ایجاد",
+ PaymentStatus = CMSMicroservice.Domain.Enums.PaymentStatus.Success,
+ PaymentDate = DateTime.UtcNow,
+ Type = CMSMicroservice.Domain.Enums.TransactionType.Buy,
+ RefId = $"SHOP_{DateTime.UtcNow.Ticks}"
+ };
+
+ _context.Transactions.Add(transaction);
+ await _context.SaveChangesAsync(context.CancellationToken);
+
+ // Deduct from wallet
+ var oldBalance = wallet.Balance;
+ wallet.Balance -= totalAmount;
+
+ // Create wallet change log
+ var walletLog = new UserWalletChangeLog
+ {
+ WalletId = wallet.Id,
+ CurrentBalance = wallet.Balance,
+ ChangeValue = -totalAmount,
+ CurrentNetworkBalance = wallet.NetworkBalance,
+ ChangeNerworkValue = 0,
+ CurrentDiscountBalance = wallet.DiscountBalance,
+ ChangeDiscountValue = 0,
+ IsIncrease = false,
+ RefrenceId = transaction.Id
+ };
+
+ _context.UserWalletChangeLogs.Add(walletLog);
+
+ // Create order
+ var order = new UserOrder
+ {
+ UserId = userId,
+ UserAddressId = defaultAddress.Id,
+ Amount = totalAmount,
+ TransactionId = transaction.Id,
+ PaymentStatus = CMSMicroservice.Domain.Enums.PaymentStatus.Success,
+ PaymentDate = DateTime.UtcNow,
+ PaymentMethod = CMSMicroservice.Domain.Enums.PaymentMethod.Wallet,
+ DeliveryStatus = CMSMicroservice.Domain.Enums.DeliveryStatus.Pending,
+ HasVAT = true
+ };
+
+ _context.UserOrders.Add(order);
+ await _context.SaveChangesAsync(context.CancellationToken);
+
+ // Update transaction description with order ID
+ transaction.Description = $"خرید محصولات - سفارش #{order.Id}";
+ await _context.SaveChangesAsync(context.CancellationToken);
+
+ // Create order VAT record
+ var orderVat = new OrderVAT
+ {
+ OrderId = order.Id,
+ VATRate = vatRate,
+ BaseAmount = baseAmount,
+ VATAmount = vatAmount,
+ TotalAmount = totalAmount
+ };
+
+ _context.OrderVATs.Add(orderVat);
+
+ // Create factor details for each cart item
+ foreach (var cartItem in cartItems)
+ {
+ var factorDetail = new FactorDetails
+ {
+ OrderId = order.Id,
+ ProductId = cartItem.ProductId,
+ Count = cartItem.Count,
+ UnitPrice = cartItem.Product.Price,
+ UnitDiscount = 0,
+ UnitDiscountPrice = cartItem.Product.Price,
+ IsChangePrice = false
+ };
+
+ _context.FactorDetails.Add(factorDetail);
+ }
+
+ await _context.SaveChangesAsync(context.CancellationToken);
+
+ // Clear user's cart
+ foreach (var cartItem in cartItems)
+ {
+ cartItem.IsDeleted = true;
+ }
+
+ await _context.SaveChangesAsync(context.CancellationToken);
+
+ return new SubmitShopBuyOrderResponse
+ {
+ Id = order.Id
+ };
}
public override async Task CancelOrder(CancelOrderRequest request, ServerCallContext context)
@@ -131,14 +423,14 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
VatPercentage = model.VatPercentage
};
- orderModel.PaymentStatus = (PaymentStatus)model.PaymentStatus;
+ orderModel.PaymentStatus = (CMSMicroservice.Protobuf.Protos.PaymentStatus)model.PaymentStatus;
if (model.PaymentDate.HasValue)
orderModel.PaymentDate = Timestamp.FromDateTime(DateTime.SpecifyKind(model.PaymentDate.Value, DateTimeKind.Utc));
if (model.PaymentMethod.HasValue)
- orderModel.PaymentMethod = (PaymentMethod)model.PaymentMethod.Value;
+ orderModel.PaymentMethod = (CMSMicroservice.Protobuf.Protos.PaymentMethod)model.PaymentMethod.Value;
- orderModel.DeliveryStatus = (DeliveryStatus)model.DeliveryStatus;
+ orderModel.DeliveryStatus = (CMSMicroservice.Protobuf.Protos.DeliveryStatus)model.DeliveryStatus;
foreach (var fd in model.FactorDetails)
{
@@ -197,14 +489,14 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
};
}
- response.PaymentStatus = (PaymentStatus)result.PaymentStatus;
+ response.PaymentStatus = (CMSMicroservice.Protobuf.Protos.PaymentStatus)result.PaymentStatus;
if (result.PaymentDate.HasValue)
response.PaymentDate = Timestamp.FromDateTime(DateTime.SpecifyKind(result.PaymentDate.Value, DateTimeKind.Utc));
if (result.PaymentMethod.HasValue)
- response.PaymentMethod = (PaymentMethod)result.PaymentMethod.Value;
+ response.PaymentMethod = (CMSMicroservice.Protobuf.Protos.PaymentMethod)result.PaymentMethod.Value;
- response.DeliveryStatus = (DeliveryStatus)result.DeliveryStatus;
+ response.DeliveryStatus = (CMSMicroservice.Protobuf.Protos.DeliveryStatus)result.DeliveryStatus;
foreach (var fd in result.FactorDetails)
{
@@ -384,4 +676,15 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
TotalAmount = totalAmount
};
}
+
+ public override Task GetVATRate(Google.Protobuf.WellKnownTypes.Empty request, ServerCallContext context)
+ {
+ // VAT Rate for Iran: 9% (نرخ مالیات بر ارزش افزوده ایران)
+ return Task.FromResult(new GetVATRateResponse
+ {
+ VatRate = 0.09,
+ VatPercentage = 9,
+ IsEnabled = true
+ });
+ }
}
diff --git a/src/CMSMicroservice.WebApi/Services/UserWalletService.cs b/src/CMSMicroservice.WebApi/Services/UserWalletService.cs
index b7b9c27..58e1e79 100644
--- a/src/CMSMicroservice.WebApi/Services/UserWalletService.cs
+++ b/src/CMSMicroservice.WebApi/Services/UserWalletService.cs
@@ -87,6 +87,8 @@ public class UserWalletService : UserWalletContract.UserWalletContractBase
ChangeValue = log.ChangeValue,
CurrentNetworkBalance = log.CurrentNetworkBalance,
ChangeNerworkValue = log.ChangeNerworkValue,
+ CurrentDiscountBalance = log.CurrentDiscountBalance,
+ ChangeDiscountValue = log.ChangeDiscountValue,
IsIncrease = log.IsIncrease,
RefrenceId = log.RefrenceId,
CreatedAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(DateTime.SpecifyKind(log.Created, DateTimeKind.Utc))