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