2 Commits

Author SHA1 Message Date
masoodafar-web f64b6be7da feat: Implement GetMyWeeklyBalances query and handler, enhance ClubMembership service with JWT userId handling 2026-02-08 00:24:59 +03:30
masoodafar-web 9185aa227d feat: Add discount balance and change value to wallet change log
- Added CurrentDiscountBalance and ChangeDiscountValue properties to GetCustomerWalletChangeLogResponseDto.
- Updated userwallet.proto to include current_discount_balance and change_discount_value fields.
- Enhanced CommissionProfile mapping to support GetMyCommissionPayouts requests and responses.
- Implemented GetMyCommissionPayouts query and handler to retrieve user commission payouts.
- Added validation for GetMyCommissionPayouts query.
- Updated UserOrderService to handle user orders, including VAT calculations and wallet transactions.
- Enhanced UserWalletService to include new properties in wallet change log responses.
2026-02-07 22:03:49 +03:30
19 changed files with 1231 additions and 64 deletions
+338 -37
View File
@@ -1,6 +1,6 @@
# FrontOffice to CMS API Compatibility Analysis # FrontOffice to CMS API Compatibility Analysis
**تاریخ:** 5 فوریه 2026 **تاریخ:** 6 فوریه 2026
**وضعیت:** در حال بررسی **وضعیت:** در حال بررسی
## خلاصه اجرایی ## خلاصه اجرایی
@@ -40,8 +40,16 @@
|------------|-------------------|---------------|---------| |------------|-------------------|---------------|---------|
| `GetCustomerProducts()` | ProductService | ✅ موجود | **پیاده شد در Task قبل** | | `GetCustomerProducts()` | ProductService | ✅ موجود | **پیاده شد در Task قبل** |
| `GetCustomerProductsByFilter()` | 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 | یادداشت | | API Method | استفاده در Service | Status در CMS | یادداشت |
|------------|-------------------|---------------|---------| |------------|-------------------|---------------|---------|
| `GetAllCategories()` | CategoryService | ✅ موجود | Admin API: `GetAllCategoryByFilter()` | | `GetAllCategoriesForCustomer()` | CategoryService | ✅ پیاده شد | **Customer API - Feb 6, 2026** |
| `GetCategoryById()` | CategoryService | ✅ موجود | Admin API: `GetCategory()` | | `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 | یادداشت | | API Method | استفاده در Service | Status در CMS | یادداشت |
|------------|-------------------|---------------|---------| |------------|-------------------|---------------|---------|
| `GetAllUserOrderByFilter()` | OrderService | ✅ موجود | Proto: `GetCustomerOrders()` **پیاده شد** | | `GetAllUserOrderByFilter()` | OrderService, Orders.razor | ✅ پیاده شد | **Feb 6, 2026** - Admin API |
| `GetUserOrder()` | OrderService | ✅ موجود | Proto: `GetCustomerOrder()` **پیاده شد** | | `GetUserOrder()` | OrderService, OrderDetail.razor | ✅ پیاده شد | **Feb 6, 2026** - جزئیات کامل سفارش |
| `GetUserOrderHistory()` | OrderService | ⚠️ نیاز به بررسی | Proto: `GetCustomerOrderHistory()` **پیاده شد** | | `GetCustomerOrders()` | OrderService | ✅ موجود | Customer API با فیلتر UserId |
| `PlaceOrder()` (Checkout) | Checkout.razor | ❓ نیاز به بررسی | باید از Transaction یا UserOrder باشد | | `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 | یادداشت | | API Method | استفاده در Service | Status در CMS | یادداشت |
|------------|-------------------|---------------|---------| |------------|-------------------|---------------|---------|
| `GetCustomerWallet()` | WalletService | ✅ موجود | **پیاده شد در Task قبل** | | `GetCustomerWallet()` | WalletService | ✅ موجود | 3 نوع کیف پول: Balance, NetworkBalance, DiscountBalance |
| `GetCustomerWalletChangeLog()` | WalletService | ✅ موجود | **پیاده شد در Task قبل** | | `GetCustomerWalletChangeLog()` | WalletService | ✅ موجود | 6 فیلد موجودی: Current+Change برای هر 3 کیف پول |
| `CustomerWithdrawBalance()` | WalletService | ✅ موجود | **پیاده شد در Task قبل** | | `CustomerWithdrawBalance()` | WalletService | ✅ موجود | Proto موجود است |
| `GetCustomerWithdrawals()` | WithdrawalRequests.razor | ✅ موجود | **پیاده شد در Task قبل** | | `GetCustomerWithdrawals()` | WithdrawalRequests.razor | ✅ موجود | لیست درخواست‌های برداشت |
| `GetCustomerWithdrawalSettings()` | WalletService | ✅ موجود | **پیاده شد در Task قبل** | | `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** | | `UpdateCustomerCartItem()` | CartService | ✅ پیاده شد | **Command Handler تکمیل شد - Feb 5** |
| `RemoveFromCustomerCart()` | 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 جداگانه استفاده کند. **یادداشت:** 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 | یادداشت | | API Method | استفاده در Service | Status در CMS | یادداشت |
|------------|-------------------|---------------|---------| |------------|-------------------|---------------|---------|
| `GetMyNetworkTree()` | NetworkMembershipService | ✅ موجود | **پیاده شد در Task قبل** | | `GetMyNetworkTree()` | NetworkMembershipService | ✅ موجود | Customer Query جداگانه با ICurrentUserService |
| `GetSubordinateTree()` | NetworkMembershipService | ✅ موجود | **پیاده شد در Task قبل** | | `GetSubordinateTree()` | NetworkMembershipService | ✅ موجود | Recursive tree traversal |
| `GetMyNetworkStatistics()` | NetworkStatisticsPage.razor | ✅ موجود | **پیاده شد در Task قبل** | | `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 پیاده شده 5. ✅ Package APIs - تمام Customer endpoints پیاده شده
6. ✅ NetworkMembership APIs - پیاده شده 6. ✅ NetworkMembership APIs - پیاده شده
7. ✅ Commission APIs - پیاده شده 7. ✅ Commission APIs - پیاده شده
8. ✅ Category APIs - از Admin API استفاده می‌کند (OK) 8. ✅ Category APIs - GetAllCategoriesForCustomer پیاده شد (Feb 6, 2026)
9. ✅ City APIs - Public API موجود 9. ✅ City APIs - Public API موجود
10. ✅ ClubMembership APIs - Proto موجود 10. ✅ ClubMembership APIs - Proto موجود
11. ✅ Configuration APIs - Proto موجود 11. ✅ Configuration APIs - Proto موجود
12. ✅ AppVersion APIs - Proto موجود 12. ✅ AppVersion APIs - Proto موجود
13.**UserCarts APIs - تمام Customer endpoints پیاده شد (Feb 5, 2026)** 🆕 13.**UserCarts APIs - تمام Customer endpoints پیاده شد (Feb 5, 2026) + اصلاحات Feb 6** 🆕
14.**UserAddress APIs - تمام Customer endpoints پیاده شد (Feb 5, 2026)** 🆕 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~~ ~~1. **UserCarts APIs** - نیاز به Customer-specific endpoints~~
**✅ تکمیل شد - Feb 5, 2026** **✅ تکمیل شد - Feb 5, 2026 + اصلاحات Feb 6, 2026**
~~2. **UserAddress APIs** - نیاز به Customer-specific endpoints~~ ~~2. **UserAddress APIs** - نیاز به Customer-specific endpoints~~
**✅ تکمیل شد - Feb 5, 2026** **✅ تکمیل شد - Feb 5, 2026 + باگ Security رفع شد Feb 6, 2026**
3. **UserOrder/Checkout APIs** - نیاز به بررسی: ~~3. **UserOrder/Checkout APIs** - نیاز به بررسی~~
- `PlaceOrder()` یا `CreateOrder()` - ❓ نیاز به بررسی workflow **✅ تکمیل شد - Feb 6, 2026:**
- `CancelOrder()` - ❓ نیاز به بررسی - ✅ SubmitShopBuyOrder - تبدیل سبد خرید به سفارش
- ✅ GetUserOrder - نمایش جزئیات سفارش
- ✅ GetAllUserOrderByFilter - لیست سفارشات
- ✅ GetVATRate - دریافت نرخ مالیات 9%
- ✅ OrderDetail.razor - رفع باگ‌های NullReference
4. **UpdateCustomerProfile, ChangeCustomerPassword, UpdateCustomerSettings** - Proto موجود اما Query/Handler نیاز است 4. **UpdateCustomerProfile, ChangeCustomerPassword, UpdateCustomerSettings** - Proto موجود اما Query/Handler نیاز است
@@ -277,6 +472,11 @@
- ✅ RemoveFromCustomerCartCommand و Handler - ✅ RemoveFromCustomerCartCommand و Handler
- ✅ UserCartsService با ISender - ✅ UserCartsService با ISender
**✅ اصلاحات Security - Feb 6, 2026:**
- ✅ CartService.cs: تمام عملیات به Customer APIs تغییر یافت
- ✅ رفع باگ Field Naming: UserCartId → CartItemId
- ✅ رفع منطق حذف: از Update به RemoveUserCartForCustomer
~~### Priority 2: UserAddress Customer Endpoints~~ ~~### Priority 2: UserAddress Customer Endpoints~~
~~این APIs برای Checkout و مدیریت آدرس‌ها ضروری هستند.~~ ~~این APIs برای Checkout و مدیریت آدرس‌ها ضروری هستند.~~
**✅ تکمیل شد - Feb 5, 2026:** **✅ تکمیل شد - Feb 5, 2026:**
@@ -288,7 +488,15 @@
- ✅ UserAddressService با ISender - ✅ UserAddressService با ISender
- ⚠️ **یادداشت:** CityName/ProvinceName در response خالی است - FrontOffice باید از City API استفاده کند - ⚠️ **یادداشت:** 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 ثبت سفارش بررسی شود. باید workflow ثبت سفارش بررسی شود.
### Priority 4: Customer Profile Updates ### Priority 4: Customer Profile Updates
@@ -298,14 +506,101 @@
## وضعیت پروژه ## وضعیت پروژه
**تکمیل شده:** ~95% **تکمیل شده:** ~97%
**آخرین به‌روزرسانی:** 5 فوریه 2026 **آخرین به‌روزرسانی:** 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) - ✅ پیاده‌سازی کامل UserCart Customer endpoints (4 Handler + Service)
- ✅ پیاده‌سازی کامل UserAddress Customer endpoints (5 Handler + Service) - ✅ پیاده‌سازی کامل UserAddress Customer endpoints (5 Handler + Service)
- ✅ اضافه کردن Proto definitions برای Customer Address - ✅ اضافه کردن 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 (نیاز به بررسی) - ⚠️ Checkout workflow و Order creation (نیاز به بررسی)
@@ -313,6 +608,12 @@
- 📝 CityName/ProvinceName در GetCustomerAddresses خالی است (نیاز به City API lookup در FrontOffice) - 📝 CityName/ProvinceName در GetCustomerAddresses خالی است (نیاز به City API lookup در FrontOffice)
**Build Status:** **Build Status:**
- ✅ 0 Errors - CMS: 0 Errors, ~60 Warnings (unused proto imports)
- ⚠️ ~315 Warnings (nullable reference types - غیر بحرانی) - ✅ FrontOffice: 0 Errors, ~120 Warnings (nullable references)
**صفحات تست شده (Feb 6):**
- ✅ /profile/addresses - کار می‌کند (فقط آدرس‌های خود کاربر)
- ✅ /products - کار می‌کند (لیست محصولات با filtering و sorting)
- ✅ /categories - کار می‌کند (لیست دسته‌بندی‌های فعال)
- ✅ /profile/wallet - کار می‌کند (3 کیف پول با تراکنش‌های کامل)
@@ -3,14 +3,18 @@ namespace CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubMembership
public class GetClubMembershipQueryHandler : IRequestHandler<GetClubMembershipQuery, ClubMembershipDto?> public class GetClubMembershipQueryHandler : IRequestHandler<GetClubMembershipQuery, ClubMembershipDto?>
{ {
private readonly IApplicationDbContext _context; private readonly IApplicationDbContext _context;
private readonly ILogger<GetClubMembershipQueryHandler> _logger;
public GetClubMembershipQueryHandler(IApplicationDbContext context) public GetClubMembershipQueryHandler(IApplicationDbContext context, ILogger<GetClubMembershipQueryHandler> logger)
{ {
_context = context; _context = context;
_logger = logger;
} }
public async Task<ClubMembershipDto?> Handle(GetClubMembershipQuery request, CancellationToken cancellationToken) public async Task<ClubMembershipDto?> Handle(GetClubMembershipQuery request, CancellationToken cancellationToken)
{ {
_logger.LogInformation("GetClubMembership called for UserId: {UserId}", request.UserId);
var membership = await _context.ClubMemberships var membership = await _context.ClubMemberships
.AsNoTracking() .AsNoTracking()
.Where(x => x.UserId == request.UserId) .Where(x => x.UserId == request.UserId)
@@ -27,6 +31,26 @@ public class GetClubMembershipQueryHandler : IRequestHandler<GetClubMembershipQu
}) })
.FirstOrDefaultAsync(cancellationToken); .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; return membership;
} }
} }
@@ -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; }
}
@@ -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
};
}
}
@@ -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");
});
}
}
@@ -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;
}
@@ -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; }
}
@@ -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);
}
}
@@ -22,6 +22,16 @@ public class GetCustomerWalletChangeLogResponseDto
/// </summary> /// </summary>
public long ChangeNerworkValue { get; set; } public long ChangeNerworkValue { get; set; }
/// <summary>
/// موجودی جاری تخفیف
/// </summary>
public long CurrentDiscountBalance { get; set; }
/// <summary>
/// مقدار تغییر تخفیف
/// </summary>
public long ChangeDiscountValue { get; set; }
/// <summary> /// <summary>
/// افزایشی است؟ /// افزایشی است؟
/// </summary> /// </summary>
@@ -160,9 +160,11 @@ message CustomerWalletChangeLogModel
int64 change_value = 2; int64 change_value = 2;
int64 current_network_balance = 3; int64 current_network_balance = 3;
int64 change_nerwork_value = 4; int64 change_nerwork_value = 4;
bool is_increase = 5; int64 current_discount_balance = 5;
google.protobuf.Int64Value refrence_id = 6; int64 change_discount_value = 6;
google.protobuf.Timestamp created_at = 7; bool is_increase = 7;
google.protobuf.Int64Value refrence_id = 8;
google.protobuf.Timestamp created_at = 9;
} }
message CustomerWithdrawBalanceRequest message CustomerWithdrawBalanceRequest
@@ -1,4 +1,5 @@
using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetAllClubMemberships; using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetAllClubMemberships;
using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubMembership;
using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubStatistics; using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubStatistics;
using CMSMicroservice.Protobuf.Protos.ClubMembership; using CMSMicroservice.Protobuf.Protos.ClubMembership;
using Google.Protobuf.WellKnownTypes; using Google.Protobuf.WellKnownTypes;
@@ -11,6 +12,37 @@ public class ClubMembershipProfile : IRegister
{ {
public void Register(TypeAdapterConfig config) 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 // GetAllClubMemberships
// ===================== // =====================
@@ -1,8 +1,12 @@
using System.Linq;
using CMSMicroservice.Application.CommissionCQ.Queries.GetUserWeeklyBalances; using CMSMicroservice.Application.CommissionCQ.Queries.GetUserWeeklyBalances;
using CMSMicroservice.Application.CommissionCQ.Queries.GetMyWeeklyBalances;
using CMSMicroservice.Application.CommissionCQ.Queries.GetAvailableWeeks; using CMSMicroservice.Application.CommissionCQ.Queries.GetAvailableWeeks;
using CMSMicroservice.Application.CommissionCQ.Queries.GetUserCommissionPayouts; using CMSMicroservice.Application.CommissionCQ.Queries.GetUserCommissionPayouts;
using CMSMicroservice.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
using CMSMicroservice.Application.CommissionCQ.Queries.GetWeekDefinitions; using CMSMicroservice.Application.CommissionCQ.Queries.GetWeekDefinitions;
using CMSMicroservice.Application.Common.Models; using CMSMicroservice.Application.Common.Models;
using CMSMicroservice.Domain.Enums;
using CMSMicroservice.Protobuf.Protos.Commission; using CMSMicroservice.Protobuf.Protos.Commission;
using Google.Protobuf.WellKnownTypes; using Google.Protobuf.WellKnownTypes;
using Mapster; using Mapster;
@@ -124,5 +128,77 @@ public class CommissionProfile : IRegister
: null) : null)
.Map(dest => dest.IsExpired, src => src.IsExpired) .Map(dest => dest.IsExpired, src => src.IsExpired)
.Map(dest => dest.Created, src => Timestamp.FromDateTimeOffset(src.Created)); .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.Commands.DeleteCategory;
using CMSMicroservice.Application.CategoryCQ.Queries.GetCategory; using CMSMicroservice.Application.CategoryCQ.Queries.GetCategory;
using CMSMicroservice.Application.CategoryCQ.Queries.GetAllCategoryByFilter; using CMSMicroservice.Application.CategoryCQ.Queries.GetAllCategoryByFilter;
using MediatR;
using AppModels = CMSMicroservice.Application.Common.Models;
namespace CMSMicroservice.WebApi.Services; namespace CMSMicroservice.WebApi.Services;
public class CategoryService : CategoryContract.CategoryContractBase public class CategoryService : CategoryContract.CategoryContractBase
{ {
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS; private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
private readonly ISender _sender;
public CategoryService(IDispatchRequestToCQRS dispatchRequestToCQRS) public CategoryService(IDispatchRequestToCQRS dispatchRequestToCQRS, ISender sender)
{ {
_dispatchRequestToCQRS = dispatchRequestToCQRS; _dispatchRequestToCQRS = dispatchRequestToCQRS;
_sender = sender;
} }
public override async Task<CreateNewCategoryResponse> CreateNewCategory(CreateNewCategoryRequest request, ServerCallContext context) 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) public override async Task<GetAllCategoriesForCustomerResponse> GetAllCategoriesForCustomer(GetAllCategoriesForCustomerRequest request, ServerCallContext context)
{ {
// TODO: Implement using existing CMS Category Application layer // Use GetAllCategoryByFilter to get all active categories
// For now, return empty response var query = new GetAllCategoryByFilterQuery
return new GetAllCategoriesForCustomerResponse {
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 MetaData = new CMSMicroservice.Protobuf.Protos.MetaData
{ {
CurrentPage = request.PageNumber, CurrentPage = result.MetaData.CurrentPage,
PageSize = request.PageSize, PageSize = result.MetaData.PageSize,
TotalCount = 0, TotalCount = result.MetaData.TotalCount,
TotalPage = 0, TotalPage = result.MetaData.TotalPage,
HasNext = false, HasNext = result.MetaData.HasNext,
HasPrevious = false 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) 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.Protobuf.Protos.ClubMembership;
using CMSMicroservice.WebApi.Common.Services; using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.ClubMembershipCQ.Commands.ActivateClubMembership; 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.ClubMembershipCQ.Queries.GetClubStatistics;
using CMSMicroservice.Application.ClubFeatureCQ.Queries.GetUserClubFeatures; using CMSMicroservice.Application.ClubFeatureCQ.Queries.GetUserClubFeatures;
using CMSMicroservice.Application.ClubFeatureCQ.Commands.ToggleUserClubFeature; using CMSMicroservice.Application.ClubFeatureCQ.Commands.ToggleUserClubFeature;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.WebApi.Services; namespace CMSMicroservice.WebApi.Services;
public class ClubMembershipService : ClubMembershipContract.ClubMembershipContractBase public class ClubMembershipService : ClubMembershipContract.ClubMembershipContractBase
{ {
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS; 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; _dispatchRequestToCQRS = dispatchRequestToCQRS;
_currentUserService = currentUserService;
_logger = logger;
} }
public override async Task<Empty> ActivateClubMembership(ActivateClubMembershipRequest request, ServerCallContext context) 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) 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); 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.GetUserCommissionPayouts;
using CMSMicroservice.Application.CommissionCQ.Queries.GetCommissionPayoutHistory; using CMSMicroservice.Application.CommissionCQ.Queries.GetCommissionPayoutHistory;
using CMSMicroservice.Application.CommissionCQ.Queries.GetUserWeeklyBalances; using CMSMicroservice.Application.CommissionCQ.Queries.GetUserWeeklyBalances;
using CMSMicroservice.Application.CommissionCQ.Queries.GetMyWeeklyBalances;
using CMSMicroservice.Application.CommissionCQ.Queries.GetAllWeeklyPools; using CMSMicroservice.Application.CommissionCQ.Queries.GetAllWeeklyPools;
using CMSMicroservice.Application.CommissionCQ.Queries.GetWithdrawalRequests; using CMSMicroservice.Application.CommissionCQ.Queries.GetWithdrawalRequests;
using CMSMicroservice.Application.CommissionCQ.Queries.GetWorkerStatus; 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.GetWithdrawalReports;
using CMSMicroservice.Application.CommissionCQ.Queries.GetAvailableWeeks; using CMSMicroservice.Application.CommissionCQ.Queries.GetAvailableWeeks;
using CMSMicroservice.Application.CommissionCQ.Queries.GetWeekDefinitions; using CMSMicroservice.Application.CommissionCQ.Queries.GetWeekDefinitions;
using CMSMicroservice.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
namespace CMSMicroservice.WebApi.Services; namespace CMSMicroservice.WebApi.Services;
@@ -68,6 +70,11 @@ public class CommissionService : CommissionContract.CommissionContractBase
return await _dispatchRequestToCQRS.Handle<GetUserCommissionPayoutsRequest, GetUserCommissionPayoutsQuery, GetUserCommissionPayoutsResponse>(request, context); 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) public override async Task<GetCommissionPayoutHistoryResponse> GetCommissionPayoutHistory(GetCommissionPayoutHistoryRequest request, ServerCallContext context)
{ {
return await _dispatchRequestToCQRS.Handle<GetCommissionPayoutHistoryRequest, GetCommissionPayoutHistoryQuery, GetCommissionPayoutHistoryResponse>(request, 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); 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.Domain.Common;
using CMSMicroservice.Protobuf.Protos.Configuration; using CMSMicroservice.Protobuf.Protos.Configuration;
using Google.Protobuf.WellKnownTypes; using Google.Protobuf.WellKnownTypes;
using Grpc.Core; using Grpc.Core;
using MediatR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace CMSMicroservice.WebApi.Services; namespace CMSMicroservice.WebApi.Services;
@@ -12,10 +15,17 @@ namespace CMSMicroservice.WebApi.Services;
public class ConfigurationService : ConfigurationContract.ConfigurationContractBase public class ConfigurationService : ConfigurationContract.ConfigurationContractBase
{ {
private readonly ILogger<ConfigurationService> _logger; 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; _logger = logger;
_currentUserService = currentUserService;
_mediator = mediator;
} }
/// <summary> /// <summary>
@@ -41,6 +51,61 @@ public class ConfigurationService : ConfigurationContract.ConfigurationContractB
return Task.FromResult(response); 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>
/// دریافت تمام تنظیمات /// دریافت تمام تنظیمات
/// </summary> /// </summary>
@@ -6,6 +6,7 @@ using CMSMicroservice.Application.ProductsCQ.Queries.GetCustomerProductsByFilter
using Mapster; using Mapster;
using AppModels = CMSMicroservice.Application.Common.Models; using AppModels = CMSMicroservice.Application.Common.Models;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace CMSMicroservice.WebApi.Services; namespace CMSMicroservice.WebApi.Services;
@@ -39,7 +40,64 @@ public class ProductsService : ProductsContract.ProductsContractBase
public override async Task<GetAllProductsByFilterResponse> GetAllProductsByFilter(GetAllProductsByFilterRequest request, ServerCallContext context) 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) 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.GetCustomerOrders;
using CMSMicroservice.Application.UserOrderCQ.Queries.GetCustomerOrder; using CMSMicroservice.Application.UserOrderCQ.Queries.GetCustomerOrder;
using CMSMicroservice.Application.UserOrderCQ.Queries.GetCustomerOrderHistory; 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 AppModels = CMSMicroservice.Application.Common.Models;
using Grpc.Core; using Grpc.Core;
using Google.Protobuf.WellKnownTypes; using Google.Protobuf.WellKnownTypes;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using CMSMicroservice.Protobuf.Protos; using CMSMicroservice.Protobuf.Protos;
using MediatR; using MediatR;
using Mapster; using Mapster;
using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.WebApi.Services; namespace CMSMicroservice.WebApi.Services;
public class UserOrderService : UserOrderContract.UserOrderContractBase public class UserOrderService : UserOrderContract.UserOrderContractBase
{ {
private readonly ISender _sender; 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; _sender = sender;
_context = context;
_currentUserService = currentUserService;
} }
public override async Task<CreateNewUserOrderResponse> CreateNewUserOrder(CreateNewUserOrderRequest request, ServerCallContext context) 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) 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) 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) 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) public override async Task<CancelOrderResponse> CancelOrder(CancelOrderRequest request, ServerCallContext context)
@@ -131,14 +423,14 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
VatPercentage = model.VatPercentage VatPercentage = model.VatPercentage
}; };
orderModel.PaymentStatus = (PaymentStatus)model.PaymentStatus; orderModel.PaymentStatus = (CMSMicroservice.Protobuf.Protos.PaymentStatus)model.PaymentStatus;
if (model.PaymentDate.HasValue) if (model.PaymentDate.HasValue)
orderModel.PaymentDate = Timestamp.FromDateTime(DateTime.SpecifyKind(model.PaymentDate.Value, DateTimeKind.Utc)); orderModel.PaymentDate = Timestamp.FromDateTime(DateTime.SpecifyKind(model.PaymentDate.Value, DateTimeKind.Utc));
if (model.PaymentMethod.HasValue) 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) 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) if (result.PaymentDate.HasValue)
response.PaymentDate = Timestamp.FromDateTime(DateTime.SpecifyKind(result.PaymentDate.Value, DateTimeKind.Utc)); response.PaymentDate = Timestamp.FromDateTime(DateTime.SpecifyKind(result.PaymentDate.Value, DateTimeKind.Utc));
if (result.PaymentMethod.HasValue) 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) foreach (var fd in result.FactorDetails)
{ {
@@ -384,4 +676,15 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
TotalAmount = totalAmount 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, ChangeValue = log.ChangeValue,
CurrentNetworkBalance = log.CurrentNetworkBalance, CurrentNetworkBalance = log.CurrentNetworkBalance,
ChangeNerworkValue = log.ChangeNerworkValue, ChangeNerworkValue = log.ChangeNerworkValue,
CurrentDiscountBalance = log.CurrentDiscountBalance,
ChangeDiscountValue = log.ChangeDiscountValue,
IsIncrease = log.IsIncrease, IsIncrease = log.IsIncrease,
RefrenceId = log.RefrenceId, RefrenceId = log.RefrenceId,
CreatedAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(DateTime.SpecifyKind(log.Created, DateTimeKind.Utc)) CreatedAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(DateTime.SpecifyKind(log.Created, DateTimeKind.Utc))