Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f64b6be7da | |||
| 9185aa227d |
@@ -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 کیف پول با تراکنشهای کامل)
|
||||
|
||||
|
||||
+25
-1
@@ -3,14 +3,18 @@ namespace CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubMembership
|
||||
public class GetClubMembershipQueryHandler : IRequestHandler<GetClubMembershipQuery, ClubMembershipDto?>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ILogger<GetClubMembershipQueryHandler> _logger;
|
||||
|
||||
public GetClubMembershipQueryHandler(IApplicationDbContext context)
|
||||
public GetClubMembershipQueryHandler(IApplicationDbContext context, ILogger<GetClubMembershipQueryHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ClubMembershipDto?> Handle(GetClubMembershipQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("GetClubMembership called for UserId: {UserId}", request.UserId);
|
||||
|
||||
var membership = await _context.ClubMemberships
|
||||
.AsNoTracking()
|
||||
.Where(x => x.UserId == request.UserId)
|
||||
@@ -27,6 +31,26 @@ public class GetClubMembershipQueryHandler : IRequestHandler<GetClubMembershipQu
|
||||
})
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
// اگر کاربر عضویت نداره، یک DTO با وضعیت غیرفعال برگردون
|
||||
if (membership == null)
|
||||
{
|
||||
_logger.LogInformation("No membership found for UserId: {UserId}, returning inactive status", request.UserId);
|
||||
return new ClubMembershipDto
|
||||
{
|
||||
Id = 0,
|
||||
UserId = request.UserId,
|
||||
IsActive = false,
|
||||
ActivatedAt = null,
|
||||
InitialContribution = 0,
|
||||
TotalEarned = 0,
|
||||
Created = DateTimeOffset.UtcNow,
|
||||
LastModified = null
|
||||
};
|
||||
}
|
||||
|
||||
_logger.LogInformation("Membership found for UserId: {UserId}, IsActive: {IsActive}, Id: {Id}",
|
||||
request.UserId, membership.IsActive, membership.Id);
|
||||
|
||||
return membership;
|
||||
}
|
||||
}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
|
||||
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
|
||||
|
||||
/// <summary>
|
||||
/// Query برای دریافت پرداختهای کمیسیون کاربر جاری (از JWT)
|
||||
/// </summary>
|
||||
public record GetMyCommissionPayoutsQuery : IRequest<GetMyCommissionPayoutsResponseDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// فیلتر وضعیت
|
||||
/// </summary>
|
||||
public CommissionPayoutStatus? Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// شماره هفته (اختیاری)
|
||||
/// </summary>
|
||||
public long? WeekDefinitionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Pagination
|
||||
/// </summary>
|
||||
public PaginationState? PaginationState { get; init; }
|
||||
}
|
||||
+73
@@ -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<GetMyCommissionPayoutsQuery, GetMyCommissionPayoutsResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ICurrentUserService _currentUser;
|
||||
|
||||
public GetMyCommissionPayoutsQueryHandler(
|
||||
IApplicationDbContext context,
|
||||
ICurrentUserService currentUser)
|
||||
{
|
||||
_context = context;
|
||||
_currentUser = currentUser;
|
||||
}
|
||||
|
||||
public async Task<GetMyCommissionPayoutsResponseDto> 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
|
||||
};
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
|
||||
|
||||
public class GetMyCommissionPayoutsQueryValidator : AbstractValidator<GetMyCommissionPayoutsQuery>
|
||||
{
|
||||
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");
|
||||
});
|
||||
}
|
||||
}
|
||||
+23
@@ -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<GetMyCommissionPayoutsResponseModel> 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;
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetUserWeeklyBalances;
|
||||
|
||||
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetMyWeeklyBalances;
|
||||
|
||||
/// <summary>
|
||||
/// Query برای دریافت تعادلهای هفتگی کاربر جاری (از JWT)
|
||||
/// </summary>
|
||||
public record GetMyWeeklyBalancesQuery : IRequest<GetUserWeeklyBalancesResponseDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// شناسه تعریف هفته (اختیاری)
|
||||
/// </summary>
|
||||
public long? WeekDefinitionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// فقط موارد Expired نشده؟
|
||||
/// </summary>
|
||||
public bool OnlyActive { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Pagination
|
||||
/// </summary>
|
||||
public PaginationState? PaginationState { get; init; }
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetUserWeeklyBalances;
|
||||
|
||||
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetMyWeeklyBalances;
|
||||
|
||||
/// <summary>
|
||||
/// Handler برای دریافت تعادلهای هفتگی کاربر جاری
|
||||
/// </summary>
|
||||
public class GetMyWeeklyBalancesQueryHandler : IRequestHandler<GetMyWeeklyBalancesQuery, GetUserWeeklyBalancesResponseDto>
|
||||
{
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
private readonly IMediator _mediator;
|
||||
private readonly ILogger<GetMyWeeklyBalancesQueryHandler> _logger;
|
||||
|
||||
public GetMyWeeklyBalancesQueryHandler(
|
||||
ICurrentUserService currentUserService,
|
||||
IMediator mediator,
|
||||
ILogger<GetMyWeeklyBalancesQueryHandler> logger)
|
||||
{
|
||||
_currentUserService = currentUserService;
|
||||
_mediator = mediator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<GetUserWeeklyBalancesResponseDto> Handle(GetMyWeeklyBalancesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// دریافت UserId از JWT
|
||||
if (!long.TryParse(_currentUserService.UserId, out var userId) || userId <= 0)
|
||||
{
|
||||
_logger.LogWarning("GetMyWeeklyBalances called without valid user authentication");
|
||||
throw new UnauthorizedAccessException("کاربر احراز هویت نشده است");
|
||||
}
|
||||
|
||||
_logger.LogInformation("GetMyWeeklyBalances for UserId: {UserId}, WeekDefinitionId: {WeekDefinitionId}",
|
||||
userId, request.WeekDefinitionId);
|
||||
|
||||
// فراخوانی GetUserWeeklyBalancesQuery با UserId از JWT
|
||||
var query = new GetUserWeeklyBalancesQuery
|
||||
{
|
||||
UserId = userId,
|
||||
WeekDefinitionId = request.WeekDefinitionId,
|
||||
OnlyActive = request.OnlyActive,
|
||||
PaginationState = request.PaginationState
|
||||
};
|
||||
|
||||
return await _mediator.Send(query, cancellationToken);
|
||||
}
|
||||
}
|
||||
+10
@@ -22,6 +22,16 @@ public class GetCustomerWalletChangeLogResponseDto
|
||||
/// </summary>
|
||||
public long ChangeNerworkValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// موجودی جاری تخفیف
|
||||
/// </summary>
|
||||
public long CurrentDiscountBalance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// مقدار تغییر تخفیف
|
||||
/// </summary>
|
||||
public long ChangeDiscountValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// افزایشی است؟
|
||||
/// </summary>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetAllClubMemberships;
|
||||
using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubMembership;
|
||||
using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubStatistics;
|
||||
using CMSMicroservice.Protobuf.Protos.ClubMembership;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
@@ -11,6 +12,37 @@ public class ClubMembershipProfile : IRegister
|
||||
{
|
||||
public void Register(TypeAdapterConfig config)
|
||||
{
|
||||
// =====================
|
||||
// GetClubMembership
|
||||
// =====================
|
||||
|
||||
// GetClubMembershipRequest -> GetClubMembershipQuery
|
||||
config.NewConfig<GetClubMembershipRequest, GetClubMembershipQuery>()
|
||||
.MapWith(src => new GetClubMembershipQuery
|
||||
{
|
||||
UserId = src.UserId
|
||||
});
|
||||
|
||||
// ClubMembershipDto -> GetClubMembershipResponse
|
||||
config.NewConfig<ClubMembershipDto, GetClubMembershipResponse>()
|
||||
.MapWith(src => new GetClubMembershipResponse
|
||||
{
|
||||
Id = src.Id,
|
||||
UserId = src.UserId,
|
||||
PackageId = 0, // Not available
|
||||
PackageName = string.Empty, // Not available
|
||||
ActivationCode = string.Empty, // Not available
|
||||
ActivatedAt = src.ActivatedAt.HasValue
|
||||
? Timestamp.FromDateTime(DateTime.SpecifyKind(src.ActivatedAt.Value, DateTimeKind.Utc))
|
||||
: null,
|
||||
ExpiresAt = null, // Not available
|
||||
IsActive = src.IsActive,
|
||||
Created = Timestamp.FromDateTimeOffset(src.Created),
|
||||
Features = { }, // Empty for now
|
||||
Status = src.IsActive ? "Active" : "Inactive",
|
||||
DaysRemaining = 0 // Calculate if ExpiresAt is available
|
||||
});
|
||||
|
||||
// =====================
|
||||
// GetAllClubMemberships
|
||||
// =====================
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
using System.Linq;
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetUserWeeklyBalances;
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetMyWeeklyBalances;
|
||||
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 +128,77 @@ 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<GetMyCommissionPayoutsRequest, GetMyCommissionPayoutsQuery>()
|
||||
.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<GetMyCommissionPayoutsResponseDto, GetMyCommissionPayoutsResponse>()
|
||||
.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<GetMyCommissionPayoutsResponseModel, CustomerCommissionPayoutModel>()
|
||||
.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);
|
||||
|
||||
// GetMyWeeklyBalances Request Mapping
|
||||
config.NewConfig<GetMyWeeklyBalancesRequest, GetMyWeeklyBalancesQuery>()
|
||||
.Map(dest => dest.WeekDefinitionId, src => src.WeekDefinitionId != null ? src.WeekDefinitionId.Value : (long?)null)
|
||||
.Map(dest => dest.OnlyActive, src => src.OnlyActive)
|
||||
.Map(dest => dest.PaginationState, src => new PaginationState
|
||||
{
|
||||
PageNumber = src.PageNumber > 0 ? src.PageNumber : 1,
|
||||
PageSize = src.PageSize > 0 ? src.PageSize : 10
|
||||
});
|
||||
|
||||
// GetMyWeeklyBalances Response Mapping
|
||||
config.NewConfig<GetUserWeeklyBalancesResponseDto, GetMyWeeklyBalancesResponse>()
|
||||
.Map(dest => dest.MetaData, src => src.MetaData != null
|
||||
? new CustomerMetaData { TotalCount = src.MetaData.TotalCount }
|
||||
: new CustomerMetaData())
|
||||
.Map(dest => dest.Balances, src => src.Models)
|
||||
.Map(dest => dest.TotalLeftBalances, src => src.Models.Sum(m => m.LeftLegTotal))
|
||||
.Map(dest => dest.TotalRightBalances, src => src.Models.Sum(m => m.RightLegTotal))
|
||||
.Map(dest => dest.WeakerLeg, src =>
|
||||
src.Models.Sum(m => m.LeftLegTotal) < src.Models.Sum(m => m.RightLegTotal)
|
||||
? "Left"
|
||||
: src.Models.Sum(m => m.LeftLegTotal) > src.Models.Sum(m => m.RightLegTotal)
|
||||
? "Right"
|
||||
: "Equal");
|
||||
|
||||
// CustomerWeeklyBalanceModel Mapping
|
||||
config.NewConfig<GetUserWeeklyBalancesResponseModel, CustomerWeeklyBalanceModel>()
|
||||
.Map(dest => dest.Id, src => src.Id)
|
||||
.Map(dest => dest.WeekDefinitionId, src => src.WeekDefinitionId)
|
||||
.Map(dest => dest.WeekDisplayName, src => src.WeekDisplayName)
|
||||
.Map(dest => dest.LeftLegBalances, src => src.LeftLegTotal)
|
||||
.Map(dest => dest.RightLegBalances, src => src.RightLegTotal)
|
||||
.Map(dest => dest.TotalBalances, src => src.TotalBalances)
|
||||
.Map(dest => dest.WeeklyPoolContribution, src => src.WeeklyPoolContribution)
|
||||
.Map(dest => dest.IsExpired, src => src.IsExpired)
|
||||
.Map(dest => dest.CalculatedAt, src => src.CalculatedAt.HasValue
|
||||
? Timestamp.FromDateTime(DateTime.SpecifyKind(src.CalculatedAt.Value, DateTimeKind.Utc))
|
||||
: null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<CreateNewCategoryResponse> CreateNewCategory(CreateNewCategoryRequest request, ServerCallContext context)
|
||||
{
|
||||
@@ -45,20 +50,52 @@ public class CategoryService : CategoryContract.CategoryContractBase
|
||||
|
||||
public override async Task<GetAllCategoriesForCustomerResponse> 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<GetCategoryByIdForCustomerResponse> GetCategoryByIdForCustomer(GetCategoryByIdForCustomerRequest request, ServerCallContext context)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Protobuf.Protos.ClubMembership;
|
||||
using CMSMicroservice.WebApi.Common.Services;
|
||||
using CMSMicroservice.Application.ClubMembershipCQ.Commands.ActivateClubMembership;
|
||||
@@ -10,16 +11,24 @@ using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubMembershipHist
|
||||
using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubStatistics;
|
||||
using CMSMicroservice.Application.ClubFeatureCQ.Queries.GetUserClubFeatures;
|
||||
using CMSMicroservice.Application.ClubFeatureCQ.Commands.ToggleUserClubFeature;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.WebApi.Services;
|
||||
|
||||
public class ClubMembershipService : ClubMembershipContract.ClubMembershipContractBase
|
||||
{
|
||||
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
private readonly ILogger<ClubMembershipService> _logger;
|
||||
|
||||
public ClubMembershipService(IDispatchRequestToCQRS dispatchRequestToCQRS)
|
||||
public ClubMembershipService(
|
||||
IDispatchRequestToCQRS dispatchRequestToCQRS,
|
||||
ICurrentUserService currentUserService,
|
||||
ILogger<ClubMembershipService> logger)
|
||||
{
|
||||
_dispatchRequestToCQRS = dispatchRequestToCQRS;
|
||||
_currentUserService = currentUserService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override async Task<Empty> ActivateClubMembership(ActivateClubMembershipRequest request, ServerCallContext context)
|
||||
@@ -39,6 +48,25 @@ public class ClubMembershipService : ClubMembershipContract.ClubMembershipContra
|
||||
|
||||
public override async Task<GetClubMembershipResponse> GetClubMembership(GetClubMembershipRequest request, ServerCallContext context)
|
||||
{
|
||||
// اگر UserId در request نیست یا صفر است، از JWT بخوان
|
||||
if (request.UserId <= 0)
|
||||
{
|
||||
if (long.TryParse(_currentUserService.UserId, out var tokenUserId) && tokenUserId > 0)
|
||||
{
|
||||
_logger.LogInformation("GetClubMembership: Reading UserId from JWT token: {UserId}", tokenUserId);
|
||||
request = new GetClubMembershipRequest { UserId = tokenUserId };
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("GetClubMembership: No valid UserId in request or JWT token");
|
||||
throw new RpcException(new Status(StatusCode.Unauthenticated, "کاربر احراز هویت نشده است"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("GetClubMembership: Using UserId from request: {UserId}", request.UserId);
|
||||
}
|
||||
|
||||
return await _dispatchRequestToCQRS.Handle<GetClubMembershipRequest, GetClubMembershipQuery, GetClubMembershipResponse>(request, context);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ using CMSMicroservice.Application.CommissionCQ.Queries.GetWeeklyCommissionPool;
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetUserCommissionPayouts;
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetCommissionPayoutHistory;
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetUserWeeklyBalances;
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetMyWeeklyBalances;
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetAllWeeklyPools;
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetWithdrawalRequests;
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetWorkerStatus;
|
||||
@@ -19,6 +20,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 +70,11 @@ public class CommissionService : CommissionContract.CommissionContractBase
|
||||
return await _dispatchRequestToCQRS.Handle<GetUserCommissionPayoutsRequest, GetUserCommissionPayoutsQuery, GetUserCommissionPayoutsResponse>(request, context);
|
||||
}
|
||||
|
||||
public override async Task<GetMyCommissionPayoutsResponse> GetMyCommissionPayouts(GetMyCommissionPayoutsRequest request, ServerCallContext context)
|
||||
{
|
||||
return await _dispatchRequestToCQRS.Handle<GetMyCommissionPayoutsRequest, GetMyCommissionPayoutsQuery, GetMyCommissionPayoutsResponse>(request, context);
|
||||
}
|
||||
|
||||
public override async Task<GetCommissionPayoutHistoryResponse> GetCommissionPayoutHistory(GetCommissionPayoutHistoryRequest request, ServerCallContext context)
|
||||
{
|
||||
return await _dispatchRequestToCQRS.Handle<GetCommissionPayoutHistoryRequest, GetCommissionPayoutHistoryQuery, GetCommissionPayoutHistoryResponse>(request, context);
|
||||
@@ -128,4 +135,9 @@ public class CommissionService : CommissionContract.CommissionContractBase
|
||||
{
|
||||
return await _dispatchRequestToCQRS.Handle<GetWithdrawalReportsRequest, GetWithdrawalReportsQuery, GetWithdrawalReportsResponse>(request, context);
|
||||
}
|
||||
|
||||
public override async Task<GetMyWeeklyBalancesResponse> GetMyWeeklyBalances(GetMyWeeklyBalancesRequest request, ServerCallContext context)
|
||||
{
|
||||
return await _dispatchRequestToCQRS.Handle<GetMyWeeklyBalancesRequest, GetMyWeeklyBalancesQuery, GetMyWeeklyBalancesResponse>(request, context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using CMSMicroservice.Application.ClubFeatureCQ.Queries.GetUserClubFeatures;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Common;
|
||||
using CMSMicroservice.Protobuf.Protos.Configuration;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Grpc.Core;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.WebApi.Services;
|
||||
@@ -12,10 +15,17 @@ namespace CMSMicroservice.WebApi.Services;
|
||||
public class ConfigurationService : ConfigurationContract.ConfigurationContractBase
|
||||
{
|
||||
private readonly ILogger<ConfigurationService> _logger;
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public ConfigurationService(ILogger<ConfigurationService> logger)
|
||||
public ConfigurationService(
|
||||
ILogger<ConfigurationService> logger,
|
||||
ICurrentUserService currentUserService,
|
||||
IMediator mediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_currentUserService = currentUserService;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -41,6 +51,61 @@ public class ConfigurationService : ConfigurationContract.ConfigurationContractB
|
||||
return Task.FromResult(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت تنظیمات باشگاه مشتریان
|
||||
/// </summary>
|
||||
public override Task<GetClubConfigurationResponse> GetClubConfiguration(Empty request, ServerCallContext context)
|
||||
{
|
||||
var response = new GetClubConfigurationResponse
|
||||
{
|
||||
ActivationFee = SystemConstants.ClubActivationFee,
|
||||
MembershipGiftValue = SystemConstants.ClubMembershipGiftValue
|
||||
};
|
||||
|
||||
_logger.LogDebug("Club configuration requested: ActivationFee={ActivationFee}, GiftValue={GiftValue}",
|
||||
response.ActivationFee, response.MembershipGiftValue);
|
||||
|
||||
return Task.FromResult(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت ویژگیهای باشگاه مشتریان برای کاربر جاری
|
||||
/// </summary>
|
||||
public override async Task<GetClubFeaturesResponse> GetClubFeatures(Empty request, ServerCallContext context)
|
||||
{
|
||||
// دریافت UserId از JWT
|
||||
if (!long.TryParse(_currentUserService.UserId, out var userId) || userId <= 0)
|
||||
{
|
||||
_logger.LogWarning("GetClubFeatures called without valid user authentication");
|
||||
return new GetClubFeaturesResponse(); // لیست خالی برای کاربران غیر احراز هویت شده
|
||||
}
|
||||
|
||||
// فراخوانی GetUserClubFeatures از طریق MediatR (CQRS داخلی)
|
||||
var query = new GetUserClubFeaturesQuery { UserId = userId };
|
||||
var userFeatures = await _mediator.Send(query, context.CancellationToken);
|
||||
|
||||
// تبدیل به فرمت GetClubFeaturesResponse
|
||||
var response = new GetClubFeaturesResponse();
|
||||
foreach (var feature in userFeatures)
|
||||
{
|
||||
response.Features.Add(new ClubFeatureModel
|
||||
{
|
||||
Id = feature.ClubFeatureId,
|
||||
Title = feature.FeatureTitle ?? string.Empty,
|
||||
Description = feature.FeatureDescription ?? string.Empty,
|
||||
IsEnabled = feature.IsActive,
|
||||
DisplayOrder = feature.SortOrder,
|
||||
GrantedAt = Timestamp.FromDateTime(DateTime.SpecifyKind(feature.GrantedAt, DateTimeKind.Utc)),
|
||||
CreatedAt = Timestamp.FromDateTime(DateTime.SpecifyKind(feature.CreatedAt, DateTimeKind.Utc)),
|
||||
Notes = feature.Notes ?? string.Empty
|
||||
});
|
||||
}
|
||||
|
||||
_logger.LogDebug("Club features requested for user {UserId}: {Count} features returned", userId, response.Features.Count);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت تمام تنظیمات
|
||||
/// </summary>
|
||||
|
||||
@@ -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<GetAllProductsByFilterResponse> 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<long> { request.Filter.CategoryId.Value }
|
||||
: new List<long>(),
|
||||
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<long>() }
|
||||
}) }
|
||||
};
|
||||
}
|
||||
|
||||
public override async Task<BulkUpdateProductPricesResponse> BulkUpdateProductPrices(BulkUpdateProductPricesRequest request, ServerCallContext context)
|
||||
|
||||
@@ -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<CreateNewUserOrderResponse> CreateNewUserOrder(CreateNewUserOrderRequest request, ServerCallContext context)
|
||||
{
|
||||
@@ -37,17 +47,299 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
|
||||
|
||||
public override async Task<GetUserOrderResponse> 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<GetAllUserOrderByFilterResponse> 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<AppModels.PaginationState>(),
|
||||
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<MetaData>()
|
||||
};
|
||||
|
||||
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<SubmitShopBuyOrderResponse> 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<CancelOrderResponse> 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<GetVATRateResponse> 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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user