Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b42d9e141d | |||
| f64b6be7da | |||
| 9185aa227d |
@@ -1,318 +0,0 @@
|
||||
# FrontOffice to CMS API Compatibility Analysis
|
||||
|
||||
**تاریخ:** 5 فوریه 2026
|
||||
**وضعیت:** در حال بررسی
|
||||
|
||||
## خلاصه اجرایی
|
||||
|
||||
این سند مقایسه APIهای مورد نیاز FrontOffice با APIهای موجود در CMS را نشان میدهد.
|
||||
|
||||
---
|
||||
|
||||
## 1. User APIs (Authentication & Profile)
|
||||
|
||||
### استفاده شده در FrontOffice
|
||||
|
||||
| API Method | استفاده در Service | Status در CMS | یادداشت |
|
||||
|------------|-------------------|---------------|---------|
|
||||
| `GetUser()` | AuthService, Personal.razor | ✅ موجود | `GetUser(GetUserRequest)` |
|
||||
| `UpdateUser()` | Personal.razor | ✅ موجود | `UpdateUser(UpdateUserRequest)` |
|
||||
| `RefreshToken()` | AuthService | ✅ موجود | `RefreshToken(RefreshTokenRequest)` |
|
||||
| `CreateNewOtpToken()` | AuthDialog | ✅ موجود | `CreateNewOtpToken(CreateNewOtpTokenRequest)` |
|
||||
| `VerifyOtpToken()` | AuthDialog | ✅ موجود | `VerifyOtpToken(VerifyOtpTokenRequest)` |
|
||||
| `AcceptContract()` | RegisterWizard | ✅ موجود | `AcceptContract(AcceptContractRequest)` |
|
||||
| `GetCustomerProfile()` | Profile Pages | ✅ موجود | **پیاده شد در Task قبل** |
|
||||
| `GetCustomerReferrals()` | Tree.razor | ✅ موجود | **پیاده شد در Task قبل** |
|
||||
| `GetCustomerSettings()` | Settings.razor | ✅ موجود | **پیاده شد در Task قبل** |
|
||||
| `UpdateCustomerProfile()` | Personal.razor | ✅ موجود | Proto موجود است |
|
||||
| `ChangeCustomerPassword()` | ChangePassword.razor | ✅ موجود | Proto موجود است |
|
||||
| `UpdateCustomerSettings()` | Settings.razor | ✅ موجود | Proto موجود است |
|
||||
|
||||
**نتیجه:** ✅ تمام User APIs موجود است
|
||||
|
||||
---
|
||||
|
||||
## 2. Products APIs
|
||||
|
||||
### استفاده شده در FrontOffice
|
||||
|
||||
| API Method | استفاده در Service | Status در CMS | یادداشت |
|
||||
|------------|-------------------|---------------|---------|
|
||||
| `GetCustomerProducts()` | ProductService | ✅ موجود | **پیاده شد در Task قبل** |
|
||||
| `GetCustomerProductsByFilter()` | ProductService | ✅ موجود | **پیاده شد در Task قبل** |
|
||||
|
||||
**نتیجه:** ✅ تمام Products APIs موجود است
|
||||
|
||||
---
|
||||
|
||||
## 3. Category APIs
|
||||
|
||||
### استفاده شده در FrontOffice
|
||||
|
||||
| API Method | استفاده در Service | Status در CMS | یادداشت |
|
||||
|------------|-------------------|---------------|---------|
|
||||
| `GetAllCategories()` | CategoryService | ✅ موجود | Admin API: `GetAllCategoryByFilter()` |
|
||||
| `GetCategoryById()` | CategoryService | ✅ موجود | Admin API: `GetCategory()` |
|
||||
|
||||
**یادداشت:** CategoryService در FrontOffice از Admin APIs استفاده میکند (بدون احراز هویت). این مشکلی ندارد چون Categories عمومی هستند.
|
||||
|
||||
**نتیجه:** ✅ Category APIs موجود است
|
||||
|
||||
---
|
||||
|
||||
## 4. UserOrder APIs
|
||||
|
||||
### استفاده شده در FrontOffice
|
||||
|
||||
| API Method | استفاده در Service | Status در CMS | یادداشت |
|
||||
|------------|-------------------|---------------|---------|
|
||||
| `GetAllUserOrderByFilter()` | OrderService | ✅ موجود | Proto: `GetCustomerOrders()` **پیاده شد** |
|
||||
| `GetUserOrder()` | OrderService | ✅ موجود | Proto: `GetCustomerOrder()` **پیاده شد** |
|
||||
| `GetUserOrderHistory()` | OrderService | ⚠️ نیاز به بررسی | Proto: `GetCustomerOrderHistory()` **پیاده شد** |
|
||||
| `PlaceOrder()` (Checkout) | Checkout.razor | ❓ نیاز به بررسی | باید از Transaction یا UserOrder باشد |
|
||||
|
||||
**نتیجه:** ⚠️ نیاز به بررسی PlaceOrder workflow
|
||||
|
||||
---
|
||||
|
||||
## 5. UserWallet APIs
|
||||
|
||||
### استفاده شده در FrontOffice
|
||||
|
||||
| API Method | استفاده در Service | Status در CMS | یادداشت |
|
||||
|------------|-------------------|---------------|---------|
|
||||
| `GetCustomerWallet()` | WalletService | ✅ موجود | **پیاده شد در Task قبل** |
|
||||
| `GetCustomerWalletChangeLog()` | WalletService | ✅ موجود | **پیاده شد در Task قبل** |
|
||||
| `CustomerWithdrawBalance()` | WalletService | ✅ موجود | **پیاده شد در Task قبل** |
|
||||
| `GetCustomerWithdrawals()` | WithdrawalRequests.razor | ✅ موجود | **پیاده شد در Task قبل** |
|
||||
| `GetCustomerWithdrawalSettings()` | WalletService | ✅ موجود | **پیاده شد در Task قبل** |
|
||||
|
||||
**نتیجه:** ✅ تمام UserWallet APIs موجود است
|
||||
|
||||
---
|
||||
|
||||
## 6. Transaction APIs
|
||||
|
||||
### استفاده شده در FrontOffice
|
||||
|
||||
| API Method | استفاده در Service | Status در CMS | یادداشت |
|
||||
|------------|-------------------|---------------|---------|
|
||||
| `GetCustomerTransaction()` | TransactionService (در BFF) | ✅ موجود | **پیاده شد در Task قبل** |
|
||||
| `GetCustomerTransactionsByFilter()` | TransactionService | ✅ موجود | **پیاده شد در Task قبل** |
|
||||
| `CustomerPaymentRequest()` | Checkout workflow | ✅ موجود | Proto موجود است |
|
||||
| `CustomerPaymentVerification()` | PaymentCallback.razor | ✅ موجود | Proto موجود است |
|
||||
|
||||
**نتیجه:** ✅ تمام Transaction APIs موجود است
|
||||
|
||||
---
|
||||
|
||||
## 7. UserCarts APIs
|
||||
|
||||
### استفاده شده در FrontOffice
|
||||
|
||||
| API Method | استفاده در Service | Status در CMS | یادداشت |
|
||||
|------------|-------------------|---------------|---------|
|
||||
| `GetCustomerCart()` | CartService | ✅ پیاده شد | **Query Handler تکمیل شد - Feb 5** |
|
||||
| `AddToCustomerCart()` | CartService | ✅ پیاده شد | **Command Handler تکمیل شد - Feb 5** |
|
||||
| `UpdateCustomerCartItem()` | CartService | ✅ پیاده شد | **Command Handler تکمیل شد - Feb 5** |
|
||||
| `RemoveFromCustomerCart()` | CartService | ✅ پیاده شد | **Command Handler تکمیل شد - Feb 5** |
|
||||
|
||||
**نتیجه:** ✅ تمام UserCart Customer APIs پیاده شده
|
||||
|
||||
---
|
||||
|
||||
## 8. UserAddress APIs
|
||||
|
||||
### استفاده شده در FrontOffice
|
||||
|
||||
| API Method | استفاده در Service | Status در CMS | یادداشت |
|
||||
|------------|-------------------|---------------|---------|
|
||||
| `GetCustomerAddresses()` | Addresses.razor | ✅ پیاده شد | **Query Handler تکمیل شد - Feb 5** |
|
||||
| `CreateCustomerAddress()` | AddAddressDialog.razor | ✅ پیاده شد | **Command Handler تکمیل شد - Feb 5** |
|
||||
| `UpdateCustomerAddress()` | EditAddressDialog.razor | ✅ پیاده شد | **Command Handler تکمیل شد - Feb 5** |
|
||||
| `DeleteCustomerAddress()` | Addresses.razor | ✅ پیاده شد | **Command Handler تکمیل شد - Feb 5** |
|
||||
| `SetCustomerDefaultAddress()` | Addresses.razor | ✅ پیاده شد | **Command Handler تکمیل شد - Feb 5** |
|
||||
|
||||
**یادداشت:** CityName و ProvinceName در response خالی است - FrontOffice باید از City API جداگانه استفاده کند.
|
||||
|
||||
**نتیجه:** ✅ تمام UserAddress Customer APIs پیاده شده
|
||||
|
||||
---
|
||||
|
||||
## 9. City APIs
|
||||
|
||||
### استفاده شده در FrontOffice
|
||||
|
||||
| API Method | استفاده در Service | Status در CMS | یادداشت |
|
||||
|------------|-------------------|---------------|---------|
|
||||
| `GetAllCities()` | AddressDialog components | ✅ موجود | Public API |
|
||||
|
||||
**نتیجه:** ✅ City APIs موجود است
|
||||
|
||||
---
|
||||
|
||||
## 10. Package APIs
|
||||
|
||||
### استفاده شده در FrontOffice
|
||||
|
||||
| API Method | استفاده در Service | Status در CMS | یادداشت |
|
||||
|------------|-------------------|---------------|---------|
|
||||
| `GetCustomerPackages()` | PackageService | ✅ موجود | **پیاده شد در Task قبل** |
|
||||
| `GetCustomerPackageDetails()` | PackageService | ✅ موجود | **پیاده شد در Task قبل** |
|
||||
| `CustomerPurchasePackage()` | Package purchase flow | ✅ موجود | Proto موجود است |
|
||||
| `CustomerVerifyPackagePurchase()` | Package verification | ✅ موجود | Proto موجود است |
|
||||
| `GetCustomerPurchaseHistory()` | MyPackages.razor | ✅ موجود | **پیاده شد در Task قبل** |
|
||||
|
||||
**نتیجه:** ✅ تمام Package APIs موجود است
|
||||
|
||||
---
|
||||
|
||||
## 11. NetworkMembership APIs
|
||||
|
||||
### استفاده شده در FrontOffice
|
||||
|
||||
| API Method | استفاده در Service | Status در CMS | یادداشت |
|
||||
|------------|-------------------|---------------|---------|
|
||||
| `GetMyNetworkTree()` | NetworkMembershipService | ✅ موجود | **پیاده شد در Task قبل** |
|
||||
| `GetSubordinateTree()` | NetworkMembershipService | ✅ موجود | **پیاده شد در Task قبل** |
|
||||
| `GetMyNetworkStatistics()` | NetworkStatisticsPage.razor | ✅ موجود | **پیاده شد در Task قبل** |
|
||||
|
||||
**نتیجه:** ✅ تمام NetworkMembership APIs موجود است
|
||||
|
||||
---
|
||||
|
||||
## 12. Commission APIs
|
||||
|
||||
### استفاده شده در FrontOffice
|
||||
|
||||
| API Method | استفاده در Service | Status در CMS | یادداشت |
|
||||
|------------|-------------------|---------------|---------|
|
||||
| `GetWeekDefinitions()` | CommissionService | ✅ موجود | **پیاده شد در Task قبل** |
|
||||
| `GetCommissionBalances()` | CommissionDashboardPage | ✅ موجود | **پیاده شد در Task قبل** |
|
||||
|
||||
**نتیجه:** ✅ تمام Commission APIs موجود است
|
||||
|
||||
---
|
||||
|
||||
## 13. ClubMembership APIs
|
||||
|
||||
### استفاده شده در FrontOffice
|
||||
|
||||
| API Method | استفاده در Service | Status در CMS | یادداشت |
|
||||
|------------|-------------------|---------------|---------|
|
||||
| `ActivateClubMembership()` | ClubMembershipService | ✅ موجود | Proto موجود در CMS |
|
||||
| `GetClubMembershipStatus()` | MembershipPage.razor | ✅ موجود | Proto موجود در CMS |
|
||||
|
||||
**نتیجه:** ✅ ClubMembership APIs موجود است
|
||||
|
||||
---
|
||||
|
||||
## 14. Configuration APIs
|
||||
|
||||
### استفاده شده در FrontOffice
|
||||
|
||||
| API Method | استفاده در Service | Status در CMS | یادداشت |
|
||||
|------------|-------------------|---------------|---------|
|
||||
| `GetClubConfiguration()` | ClubConfigurationService | ✅ موجود | Proto موجود در CMS |
|
||||
| `GetClubFeatures()` | FeaturesPage.razor | ✅ موجود | Proto موجود در CMS |
|
||||
|
||||
**نتیجه:** ✅ Configuration APIs موجود است
|
||||
|
||||
---
|
||||
|
||||
## 15. AppVersion APIs
|
||||
|
||||
### استفاده شده در FrontOffice
|
||||
|
||||
| API Method | استفاده در Service | Status در CMS | یادداشت |
|
||||
|------------|-------------------|---------------|---------|
|
||||
| `GetAppVersion()` | AppVersionService | ✅ موجود | Proto موجود در CMS |
|
||||
|
||||
**نتیجه:** ✅ AppVersion APIs موجود است
|
||||
|
||||
---
|
||||
|
||||
## نتیجهگیری کلی
|
||||
|
||||
### ✅ API های کامل (100% پیاده شده)
|
||||
1. ✅ User APIs - همه Customer endpoints پیاده شده
|
||||
2. ✅ Products APIs - GetCustomerProducts و Filter پیاده شده
|
||||
3. ✅ UserWallet APIs - تمام Customer endpoints پیاده شده
|
||||
4. ✅ Transaction APIs - Customer endpoints پیاده شده
|
||||
5. ✅ Package APIs - تمام Customer endpoints پیاده شده
|
||||
6. ✅ NetworkMembership APIs - پیاده شده
|
||||
7. ✅ Commission APIs - پیاده شده
|
||||
8. ✅ Category APIs - از Admin API استفاده میکند (OK)
|
||||
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)** 🆕
|
||||
|
||||
### ⚠️ نیاز به توجه
|
||||
|
||||
~~1. **UserCarts APIs** - نیاز به Customer-specific endpoints~~
|
||||
**✅ تکمیل شد - Feb 5, 2026**
|
||||
|
||||
~~2. **UserAddress APIs** - نیاز به Customer-specific endpoints~~
|
||||
**✅ تکمیل شد - Feb 5, 2026**
|
||||
|
||||
3. **UserOrder/Checkout APIs** - نیاز به بررسی:
|
||||
- `PlaceOrder()` یا `CreateOrder()` - ❓ نیاز به بررسی workflow
|
||||
- `CancelOrder()` - ❓ نیاز به بررسی
|
||||
|
||||
4. **UpdateCustomerProfile, ChangeCustomerPassword, UpdateCustomerSettings** - Proto موجود اما Query/Handler نیاز است
|
||||
|
||||
---
|
||||
|
||||
## اقدامات لازم
|
||||
|
||||
~~### Priority 1: UserCarts Customer Endpoints~~
|
||||
~~این APIs برای سبد خرید ضروری هستند.~~
|
||||
**✅ تکمیل شد - Feb 5, 2026:**
|
||||
- ✅ GetCustomerCartQuery و Handler
|
||||
- ✅ AddToCustomerCartCommand و Handler
|
||||
- ✅ UpdateCustomerCartItemCommand و Handler
|
||||
- ✅ RemoveFromCustomerCartCommand و Handler
|
||||
- ✅ UserCartsService با ISender
|
||||
|
||||
~~### Priority 2: UserAddress Customer Endpoints~~
|
||||
~~این APIs برای Checkout و مدیریت آدرسها ضروری هستند.~~
|
||||
**✅ تکمیل شد - Feb 5, 2026:**
|
||||
- ✅ GetCustomerAddressesQuery و Handler
|
||||
- ✅ CreateCustomerAddressCommand و Handler
|
||||
- ✅ UpdateCustomerAddressCommand و Handler
|
||||
- ✅ DeleteCustomerAddressCommand و Handler
|
||||
- ✅ SetCustomerDefaultAddressCommand و Handler
|
||||
- ✅ UserAddressService با ISender
|
||||
- ⚠️ **یادداشت:** CityName/ProvinceName در response خالی است - FrontOffice باید از City API استفاده کند
|
||||
|
||||
### Priority 3: Checkout/Order Creation
|
||||
باید workflow ثبت سفارش بررسی شود.
|
||||
|
||||
### Priority 4: Customer Profile Updates
|
||||
پیادهسازی Handler های Update برای Customer.
|
||||
|
||||
---
|
||||
|
||||
## وضعیت پروژه
|
||||
|
||||
**تکمیل شده:** ~95%
|
||||
**آخرین بهروزرسانی:** 5 فوریه 2026
|
||||
|
||||
**تغییرات امروز:**
|
||||
- ✅ پیادهسازی کامل UserCart Customer endpoints (4 Handler + Service)
|
||||
- ✅ پیادهسازی کامل UserAddress Customer endpoints (5 Handler + Service)
|
||||
- ✅ اضافه کردن Proto definitions برای Customer Address
|
||||
- ✅ Build موفقیتآمیز: 0 Errors
|
||||
|
||||
**باقی مانده:**
|
||||
- ⚠️ Checkout workflow و Order creation (نیاز به بررسی)
|
||||
- ⚠️ Profile update handlers (UpdateCustomerProfile, ChangePassword, UpdateSettings)
|
||||
- 📝 CityName/ProvinceName در GetCustomerAddresses خالی است (نیاز به City API lookup در FrontOffice)
|
||||
|
||||
**Build Status:**
|
||||
- ✅ 0 Errors
|
||||
- ⚠️ ~315 Warnings (nullable reference types - غیر بحرانی)
|
||||
|
||||
@@ -1,591 +0,0 @@
|
||||
# پیادهسازی ICurrentUserService در سرویسهای Customer
|
||||
|
||||
## خلاصه تغییرات
|
||||
این سند تمام تغییرات انجام شده برای پیادهسازی احراز هویت مبتنی بر JWT در endpointهای Customer را مستند میکند. هدف اصلی حذف نیاز به ارسال صریح UserId از سمت کلاینت و استخراج خودکار آن از JWT Claims است.
|
||||
|
||||
## الگوی پیادهسازی
|
||||
|
||||
### الگوی Query Handler (با ICurrentUserService)
|
||||
```csharp
|
||||
public class SomeQueryHandler : IRequestHandler<SomeQuery, SomeResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ICurrentUserService _currentUser;
|
||||
|
||||
public SomeQueryHandler(IApplicationDbContext context, ICurrentUserService currentUser)
|
||||
{
|
||||
_context = context;
|
||||
_currentUser = currentUser;
|
||||
}
|
||||
|
||||
public async Task<SomeResponseDto> Handle(SomeQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// رزولو کردن UserId از JWT اگر در request مشخص نشده باشد
|
||||
var userId = request.UserId == 0
|
||||
? (long.TryParse(_currentUser.UserId, out var currentUserId) ? currentUserId : 0)
|
||||
: request.UserId;
|
||||
|
||||
if (userId == 0)
|
||||
throw new UnauthorizedAccessException("User ID not found");
|
||||
|
||||
var query = _context.SomeEntity
|
||||
.Where(x => x.UserId == userId)
|
||||
.AsNoTracking();
|
||||
|
||||
// ... ادامه پیادهسازی
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### الگوی Service (استفاده از ISender)
|
||||
```csharp
|
||||
public class SomeService : SomeContract.SomeContractBase
|
||||
{
|
||||
private readonly ISender _sender;
|
||||
|
||||
public SomeService(ISender sender)
|
||||
{
|
||||
_sender = sender;
|
||||
}
|
||||
|
||||
public override async Task<Response> CustomerEndpoint(Request request, ServerCallContext context)
|
||||
{
|
||||
var query = new SomeQuery { UserId = 0 }; // 0 = استفاده از ICurrentUserService
|
||||
var result = await _sender.Send(query, context.CancellationToken);
|
||||
return MapToProtoResponse(result);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## تصمیمات معماری
|
||||
|
||||
### 1. ISender vs IDispatchRequestToCQRS
|
||||
- **IDispatchRequestToCQRS**: برای endpointهای Admin که ساختار Proto بهطور مستقیم به CQRS نگاشت میشود
|
||||
- **ISender**: برای endpointهای Customer که نیاز به ساخت دستی Query و ساختار متفاوت دارند
|
||||
|
||||
### 2. قرارداد UserId = 0
|
||||
- `0` یا مقدار مشخص نشده = استفاده از ICurrentUserService برای دریافت کاربر فعلی از JWT
|
||||
- مقدار غیر صفر = کاربر صریح (برای عملیات admin/support)
|
||||
|
||||
### 3. مسئولیت Query Handler
|
||||
- Query Handler باید پس از رزولو کردن userId، وجود آن را validate کند
|
||||
- در صورت عدم موفقیت در تعیین userId، UnauthorizedAccessException پرتاب شود
|
||||
|
||||
## سرویسهای پیادهسازی شده
|
||||
|
||||
### ✅ 1. UserWallet Service (5 endpoints)
|
||||
|
||||
#### 1.1 GetUserWalletQueryHandler
|
||||
**فایل**: `CMSMicroservice.Application/UserWalletCQ/Queries/GetUserWallet/GetUserWalletQueryHandler.cs`
|
||||
|
||||
**تغییرات**:
|
||||
- افزودن `ICurrentUserService` به constructor
|
||||
- اضافه شدن فیلد `DiscountBalance` به DTO
|
||||
- پشتیبانی از `Id = 0` برای استفاده از کاربر فعلی
|
||||
|
||||
```csharp
|
||||
var userId = request.Id == 0
|
||||
? (long.TryParse(_currentUser.UserId, out var currentUserId) ? currentUserId : 0)
|
||||
: request.Id;
|
||||
```
|
||||
|
||||
#### 1.2 GetCustomerWalletChangeLogQueryHandler (جدید)
|
||||
**فایل**: `CMSMicroservice.Application/UserWalletCQ/Queries/GetCustomerWalletChangeLog/`
|
||||
|
||||
**پیادهسازی**:
|
||||
- Query/Handler جدید برای دریافت تاریخچه تغییرات کیف پول
|
||||
- استفاده از entity `UserWalletChangeLog`
|
||||
- پشتیبانی از Pagination
|
||||
- فیلتر بر اساس userId از ICurrentUserService
|
||||
|
||||
#### 1.3 GetCustomerWithdrawalsQueryHandler (جدید)
|
||||
**فایل**: `CMSMicroservice.Application/UserWalletCQ/Queries/GetCustomerWithdrawals/`
|
||||
|
||||
**پیادهسازی**:
|
||||
- Query/Handler جدید برای دریافت درخواستهای برداشت
|
||||
- استفاده از entity `UserCommissionPayout`
|
||||
- فیلتر بر اساس `WithdrawalRequestDate` و `status = PayoutRequested`
|
||||
- پشتیبانی از Pagination
|
||||
|
||||
#### 1.4 GetCustomerWithdrawalSettingsQueryHandler (جدید)
|
||||
**فایل**: `CMSMicroservice.Application/UserWalletCQ/Queries/GetCustomerWithdrawalSettings/`
|
||||
|
||||
**پیادهسازی**:
|
||||
- Query/Handler جدید برای دریافت تنظیمات برداشت
|
||||
- مقدار ثابت `MIN_WITHDRAWAL_AMOUNT = 50000`
|
||||
- برگرداندن موجودی کیف پول کاربر فعلی
|
||||
|
||||
#### 1.5 UserWalletService
|
||||
**فایل**: `CMSMicroservice.WebApi/Services/UserWalletService.cs`
|
||||
|
||||
**تغییرات**:
|
||||
- افزودن `ISender` به constructor
|
||||
- پیادهسازی 4 متد Customer با استفاده از Query Handlerهای واقعی:
|
||||
- `GetCustomerWallet`
|
||||
- `GetCustomerWalletChangeLog`
|
||||
- `GetCustomerWithdrawals`
|
||||
- `GetCustomerWithdrawalSettings`
|
||||
|
||||
---
|
||||
|
||||
### ✅ 2. Commission Service (2 endpoints)
|
||||
|
||||
#### 2.1 GetUserCommissionPayoutsQueryHandler
|
||||
**فایل**: `CMSMicroservice.Application/CommissionCQ/Queries/GetUserCommissionPayouts/GetUserCommissionPayoutsQueryHandler.cs`
|
||||
|
||||
**تغییرات**:
|
||||
- افزودن `ICurrentUserService` به constructor
|
||||
- پشتیبانی از `UserId = null` یا `0` برای استفاده از کاربر فعلی
|
||||
- کوئری از `UserCommissionPayouts` با Include کردن `WeekDefinition`
|
||||
|
||||
#### 2.2 GetUserWeeklyBalancesQueryHandler
|
||||
**فایل**: `CMSMicroservice.Application/CommissionCQ/Queries/GetUserWeeklyBalances/GetUserWeeklyBalancesQueryHandler.cs`
|
||||
|
||||
**تغییرات**:
|
||||
- افزودن `ICurrentUserService` به constructor
|
||||
- همان الگوی رزولو UserId
|
||||
- کوئری از `UserWeeklyBalances` با Include کردن `WeekDefinition`
|
||||
|
||||
---
|
||||
|
||||
### ✅ 3. NetworkMembership Service (3 endpoints)
|
||||
|
||||
#### 3.1 GetNetworkTreeQueryHandler
|
||||
**فایل**: `CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQueryHandler.cs`
|
||||
|
||||
**تغییرات**:
|
||||
- افزودن `ICurrentUserService` به constructor
|
||||
- پشتیبانی از `UserId = 0` برای استفاده از کاربر فعلی
|
||||
- اجرای Stored Procedure `[CMS].[GetNetworkTree]`
|
||||
- تبدیل نتایج flat SP به ساختار درختی hierarchical
|
||||
|
||||
#### 3.2 GetNetworkStatisticsQueryHandler
|
||||
**فایل**: `CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkStatistics/GetNetworkStatisticsQueryHandler.cs`
|
||||
|
||||
**تغییرات**:
|
||||
- افزودن پارامتر `UserId` به Query
|
||||
- افزودن `ICurrentUserService` به constructor
|
||||
- تغییر منطق از آمار کل سیستم به آمار شبکه زیرمجموعه کاربر
|
||||
- فیلتر: `x.NetworkParentId == userId` (نه `x.NetworkParentId != null`)
|
||||
|
||||
#### 3.3 NetworkMembershipService
|
||||
**فایل**: `CMSMicroservice.WebApi/Services/NetworkMembershipService.cs`
|
||||
|
||||
**تغییرات**:
|
||||
- افزودن `ISender` به constructor
|
||||
- پیادهسازی 3 متد Customer:
|
||||
- `GetMyNetworkTree`: درخت شبکه کاربر فعلی با UserId=0
|
||||
- `GetSubordinateTree`: درخت زیرمجموعه خاص (برای admin)
|
||||
- `GetMyNetworkStatistics`: آمار شبکه کاربر فعلی
|
||||
- متدهای helper:
|
||||
- `ConvertToNodeModel()`: تبدیل بازگشتی DTO به Proto Model
|
||||
- `CountNodes()`: شمارش بازگشتی nodeهای درخت
|
||||
|
||||
**رفع باگ**:
|
||||
- حذف فیلدهای `IsClubActive` و `ActivationWeekDefinitionId` که در Proto request وجود نداشتند
|
||||
|
||||
---
|
||||
|
||||
### ✅ 4. Package Service (3 query endpoints)
|
||||
|
||||
#### 4.1 GetCustomerPackagesQueryHandler (جدید)
|
||||
**فایل**: `CMSMicroservice.Application/PackageCQ/Queries/GetCustomerPackages/`
|
||||
|
||||
**پیادهسازی**:
|
||||
- Query/Handler جدید برای دریافت لیست پکیجها
|
||||
- کوئری از entity `Package`
|
||||
- نگاشت فیلدهای اضافی:
|
||||
- `Name = Title`
|
||||
- `ImageUrl = ImagePath`
|
||||
- `Currency = "IRR"`
|
||||
- `ValidityDays = 365`
|
||||
- پشتیبانی از فیلتر `PackageType` (در صورت وجود در entity)
|
||||
|
||||
#### 4.2 GetCustomerPackageDetailsQueryHandler (جدید)
|
||||
**فایل**: `CMSMicroservice.Application/PackageCQ/Queries/GetCustomerPackageDetails/`
|
||||
|
||||
**پیادهسازی**:
|
||||
- Query/Handler جدید برای دریافت جزئیات یک پکیج
|
||||
- کوئری بر اساس `PackageId`
|
||||
- افزودن Features (کمیسیون، پشتیبانی، آموزش)
|
||||
- افزودن Requirements (عضویت، موجودی کیف پول، محدودیتها)
|
||||
|
||||
#### 4.3 GetCustomerPurchaseHistoryQueryHandler (جدید)
|
||||
**فایل**: `CMSMicroservice.Application/PackageCQ/Queries/GetCustomerPurchaseHistory/`
|
||||
|
||||
**پیادهسازی**:
|
||||
- Query/Handler جدید با ICurrentUserService
|
||||
- کوئری از `UserOrders` با فیلتر `PackageId != null`
|
||||
- Include کردن navigation property `Package`
|
||||
- پشتیبانی از:
|
||||
- Pagination
|
||||
- فیلتر تاریخ (FromDate, ToDate)
|
||||
- فیلتر نوع پکیج
|
||||
- نگاشت `PaymentStatus` صحیح (Success/Reject/Pending)
|
||||
- دریافت `RefId` از Transaction (نه `ReferenceId`)
|
||||
|
||||
#### 4.4 PackageService
|
||||
**فایل**: `CMSMicroservice.WebApi/Services/PackageService.cs`
|
||||
|
||||
**تغییرات**:
|
||||
- افزودن `ISender` به constructor
|
||||
- افزودن namespace alias: `using AppModels = CMSMicroservice.Application.Common.Models;`
|
||||
- جایگزینی 3 متد MOCK با Query Handler واقعی:
|
||||
- `GetCustomerPackages`
|
||||
- `GetCustomerPackageDetails`
|
||||
- `GetCustomerPurchaseHistory`
|
||||
- رفع ابهام در typeهای `PaginationState` و `MetaData` با استفاده از alias
|
||||
- متدهای Command (Purchase, Verify) همچنان MOCK باقی ماندند
|
||||
|
||||
---
|
||||
|
||||
## مشکلات رفع شده
|
||||
|
||||
### 1. خطای Type Inference با IDispatchRequestToCQRS
|
||||
**خطا**: `CS1061: 'Empty' does not contain definition for 'Balance'`
|
||||
|
||||
**علت**: استفاده از overload نادرست `Handle<TCommand, TResponse>` که compiler نوعها را اشتباه استنباط میکرد
|
||||
|
||||
**راه حل**: استفاده از `ISender.Send()` بهجای `IDispatchRequestToCQRS` برای endpointهای Customer
|
||||
|
||||
### 2. عدم تطابق فیلدهای Proto
|
||||
**خطا**: `CS1061: GetSubordinateTreeRequest doesn't have ActivationWeekDefinitionId`
|
||||
|
||||
**علت**: کد سرویس فیلدهایی را فرض میکرد که در Proto تعریف نشده بودند
|
||||
|
||||
**راه حل**: حذف فیلدهای غیرموجود از نگاشت request
|
||||
|
||||
### 3. خطای Nullable Protobuf Wrapper
|
||||
**خطا**: `CS1061: 'long' doesn't contain 'Value' property`
|
||||
|
||||
**علت**: تلاش برای فراخوانی `.Value` روی typeهای non-nullable
|
||||
|
||||
**راه حل**: حذف فراخوانی `.Value` و انتساب مستقیم
|
||||
|
||||
### 4. خطای Transaction.ReferenceId
|
||||
**خطا**: `CS1061: 'Transaction' does not contain a definition for 'ReferenceId'`
|
||||
|
||||
**علت**: نام صحیح فیلد `RefId` است نه `ReferenceId`
|
||||
|
||||
**راه حل**: تغییر به `Transaction.RefId`
|
||||
|
||||
### 5. خطای PaymentStatus Enum Values
|
||||
**خطا**: `CS0117: 'PaymentStatus' does not contain a definition for 'Failed'/'Refunded'`
|
||||
|
||||
**علت**: enum فقط دارای مقادیر `Success`, `Reject`, `Pending` است
|
||||
|
||||
**راه حل**: تصحیح switch statement به مقادیر صحیح
|
||||
|
||||
### 6. خطای Ambiguous Reference
|
||||
**خطا**: `CS0104: 'PaginationState'/'MetaData' is ambiguous`
|
||||
|
||||
**علت**: typeها هم در `CMSMicroservice.Application.Common.Models` و هم در `CMSMicroservice.Protobuf.Protos` وجود دارند
|
||||
|
||||
**راه حل**: افزودن namespace alias: `using AppModels = CMSMicroservice.Application.Common.Models;`
|
||||
|
||||
### 7. خطای MetaData Constructor
|
||||
**خطا**: `CS1729: 'MetaData' does not contain a constructor that takes 3 arguments`
|
||||
|
||||
**علت**: MetaData class در Application layer بدون constructor است
|
||||
|
||||
**راه حل**: استفاده از object initializer بهجای constructor:
|
||||
```csharp
|
||||
var metaData = new MetaData
|
||||
{
|
||||
TotalCount = totalCount,
|
||||
CurrentPage = pageNumber,
|
||||
PageSize = pageSize,
|
||||
TotalPage = (int)Math.Ceiling((double)totalCount / pageSize),
|
||||
HasPrevious = pageNumber > 1,
|
||||
HasNext = pageNumber < totalPages
|
||||
};
|
||||
```
|
||||
|
||||
### 8. خطای CategoryIds در Proto
|
||||
**خطا**: `CS1061: 'GetAllProductsByFilterFilter' does not contain 'CategoryIds'`
|
||||
|
||||
**علت**: Proto فقط `category_id` (singular) دارد نه `category_ids`
|
||||
|
||||
**راه حل**: تبدیل single value به List:
|
||||
```csharp
|
||||
CategoryIds = request.Filter?.CategoryId != null
|
||||
? new List<long> { request.Filter.CategoryId.Value }
|
||||
: null
|
||||
```
|
||||
|
||||
### 9. خطای OrderVAT و DeliveryStatus
|
||||
**خطا**: `CS1061: 'OrderVAT' does not contain 'VATPercentage'`
|
||||
|
||||
**علت**:
|
||||
- فیلد صحیح `VATRate` است (decimal)
|
||||
- enumهای `Processing` و `Shipped` وجود ندارند
|
||||
|
||||
**راه حل**:
|
||||
- استفاده از `VATRate * 100` برای درصد
|
||||
- تصحیح enum values: `Pending`, `InTransit`, `Delivered`, `Cancelled`, `Returned`
|
||||
|
||||
### 10. خطای Transaction/UserWalletChangeLog بدون UserId
|
||||
**خطا**: `CS1061: 'Transaction/UserWalletChangeLog' does not contain 'UserId'`
|
||||
|
||||
**علت**: این entityها direct UserId ندارند
|
||||
|
||||
**راه حل**: query از طریق navigation properties:
|
||||
```csharp
|
||||
// Transaction
|
||||
.Include(x => x.UserOrders)
|
||||
.Where(x => x.UserOrders.Any(o => o.UserId == userId))
|
||||
|
||||
// UserWalletChangeLog
|
||||
.Include(x => x.Wallet)
|
||||
.Where(x => x.Wallet.UserId == userId)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ✅ 5. UserOrder Service (3 endpoints)
|
||||
|
||||
#### 5.1 GetCustomerOrdersQueryHandler (جدید)
|
||||
**فایل**: `CMSMicroservice.Application/UserOrderCQ/Queries/GetCustomerOrders/`
|
||||
|
||||
**پیادهسازی**:
|
||||
- Query/Handler جدید با ICurrentUserService
|
||||
- کوئری از `UserOrders` با Include:
|
||||
- Package, Transaction, UserAddress, User, FactorDetails, OrderVAT
|
||||
- پشتیبانی از Pagination
|
||||
- محاسبه `TotalAmount` با احتساب مالیات (`VATRate * 100`)
|
||||
|
||||
**رفع باگ**:
|
||||
- `OrderVAT.VATPercentage` وجود ندارد → استفاده از `VATRate * 100`
|
||||
- `DeliveryStatus.Processing/Shipped` وجود ندارد → `Pending/InTransit`
|
||||
|
||||
#### 5.2 GetCustomerOrderQueryHandler (جدید)
|
||||
**فایل**: `CMSMicroservice.Application/UserOrderCQ/Queries/GetCustomerOrder/`
|
||||
|
||||
**پیادهسازی**:
|
||||
- Query/Handler برای دریافت یک سفارش با OrderId
|
||||
- Validation: بررسی تعلق Order به UserId فعلی
|
||||
- Include همان navigation properties
|
||||
|
||||
#### 5.3 GetCustomerOrderHistoryQueryHandler (جدید)
|
||||
**فایل**: `CMSMicroservice.Application/UserOrderCQ/Queries/GetCustomerOrderHistory/`
|
||||
|
||||
**پیادهسازی**:
|
||||
- Query/Handler با Pagination و فیلترها
|
||||
- فیلترهای پشتیبانی شده:
|
||||
- FromDate, ToDate
|
||||
- PaymentStatus, DeliveryStatus
|
||||
- محاسبه `CanCancelOrder` بر اساس شرایط:
|
||||
- PaymentStatus = Pending
|
||||
- DeliveryStatus = None یا Pending
|
||||
|
||||
#### 5.4 UserOrderService
|
||||
**فایل**: `CMSMicroservice.WebApi/Services/UserOrderService.cs`
|
||||
|
||||
**تغییرات**:
|
||||
- افزودن ISender به constructor
|
||||
- پیادهسازی 3 متد Customer با Query Handler واقعی
|
||||
- استفاده از namespace alias برای حل ambiguity
|
||||
|
||||
---
|
||||
|
||||
### ✅ 6. Transaction Service (2 endpoints)
|
||||
|
||||
#### 6.1 GetCustomerTransactionQueryHandler (جدید)
|
||||
**فایل**: `CMSMicroservice.Application/TransactionsCQ/Queries/GetCustomerTransaction/`
|
||||
|
||||
**پیادهسازی**:
|
||||
- Query/Handler با ICurrentUserService
|
||||
- **چالش**: Transaction entity بدون UserId
|
||||
- **راه حل**: query از طریق `UserOrders` navigation:
|
||||
```csharp
|
||||
.Include(x => x.UserOrders)
|
||||
.Where(x => x.UserOrders.Any(o => o.UserId == userId))
|
||||
```
|
||||
- فیلتر بر اساس Id یا Authority
|
||||
|
||||
#### 6.2 GetCustomerTransactionsByFilterQueryHandler (جدید)
|
||||
**فایل**: `CMSMicroservice.Application/TransactionsCQ/Queries/GetCustomerTransactionsByFilter/`
|
||||
|
||||
**پیادهسازی**:
|
||||
- Query/Handler با Pagination
|
||||
- فیلترهای پشتیبانی شده:
|
||||
- Id, Amount, Description
|
||||
- PaymentStatus (bool), RefId, Type
|
||||
- همان الگوی query از طریق UserOrders
|
||||
|
||||
#### 6.3 TransactionsService
|
||||
**فایل**: `CMSMicroservice.WebApi/Services/TransactionsService.cs`
|
||||
|
||||
**تغییرات**:
|
||||
- افزودن ISender و Query imports
|
||||
- جایگزینی MOCK با Query Handler واقعی
|
||||
- mapping صحیح Proto enums
|
||||
|
||||
---
|
||||
|
||||
### ✅ 7. Products Service (2 endpoints)
|
||||
|
||||
#### 7.1 GetCustomerProductsQueryHandler (جدید)
|
||||
**فایل**: `CMSMicroservice.Application/ProductsCQ/Queries/GetCustomerProducts/`
|
||||
|
||||
**پیادهسازی**:
|
||||
- Query/Handler بدون ICurrentUserService (محصولات عمومی)
|
||||
- کوئری از `Products` با Include:
|
||||
- ProductGalleries.ProductImage
|
||||
- ProductCategories.Category
|
||||
- ساخت درختی Category Path با متد `BuildCategoryPath()`
|
||||
- بازگشت بازگشتی به parent categories
|
||||
|
||||
#### 7.2 GetCustomerProductsByFilterQueryHandler (جدید)
|
||||
**فایل**: `CMSMicroservice.Application/ProductsCQ/Queries/GetCustomerProductsByFilter/`
|
||||
|
||||
**پیادهسازی**:
|
||||
- Query/Handler با Pagination
|
||||
- فیلترهای کامل:
|
||||
- Id, Title, Description, ShortInfomation, FullInformation
|
||||
- Price, Discount, Rate
|
||||
- SaleCount, ViewCount, RemainingCount
|
||||
- CategoryIds (لیست شناسه دستهبندیها)
|
||||
- Sorting پویا با `ApplyOrder()`
|
||||
|
||||
#### 7.3 ProductsService
|
||||
**فایل**: `CMSMicroservice.WebApi/Services/ProductsService.cs`
|
||||
|
||||
**تغییرات**:
|
||||
- افزودن ISender به constructor
|
||||
- پیادهسازی 2 متد Customer
|
||||
- mapping دستی Gallery و Categories به Proto structures
|
||||
- **رفع باگ**: Proto فقط `category_id` دارد نه `category_ids`
|
||||
- تبدیل single value به List<long>
|
||||
|
||||
---
|
||||
|
||||
### ✅ 8. User Service (3 endpoints)
|
||||
|
||||
#### 8.1 GetCustomerProfileQueryHandler (جدید)
|
||||
**فایل**: `CMSMicroservice.Application/UserCQ/Queries/GetCustomerProfile/`
|
||||
|
||||
**پیادهسازی**:
|
||||
- Query/Handler با ICurrentUserService
|
||||
- دریافت پروفایل کامل کاربر فعلی
|
||||
- محاسبه `ProfileCompletionPercentage` بر اساس 10 فیلد:
|
||||
- FirstName, LastName, Mobile, Email, NationalCode
|
||||
- AvatarPath, BirthDate, IsMobileVerified
|
||||
- NetworkParentId, ReferralCode
|
||||
- محاسبه `FullName` از FirstName + LastName
|
||||
|
||||
#### 8.2 GetCustomerReferralsQueryHandler (جدید)
|
||||
**فایل**: `CMSMicroservice.Application/UserCQ/Queries/GetCustomerReferrals/`
|
||||
|
||||
**پیادهسازی**:
|
||||
- Query/Handler با ICurrentUserService و Pagination
|
||||
- کوئری کاربران با `NetworkParentId == userId`
|
||||
- فیلتر بر اساس StatusFilter (ACTIVE/INACTIVE/ALL)
|
||||
- محاسبه آمار:
|
||||
- TotalReferrals, ActiveReferrals
|
||||
- TotalCommissionEarned از `UserWallet.NetworkBalance`
|
||||
- ThisMonthCommission از `UserWalletChangeLog`
|
||||
- **رفع باگ**: UserWalletChangeLog بدون UserId
|
||||
- راه حل: `.Include(x => x.Wallet).Where(x => x.Wallet.UserId == userId)`
|
||||
|
||||
#### 8.3 GetCustomerSettingsQueryHandler (جدید)
|
||||
**فایل**: `CMSMicroservice.Application/UserCQ/Queries/GetCustomerSettings/`
|
||||
|
||||
**پیادهسازی**:
|
||||
- Query/Handler ساده برای دریافت تنظیمات کاربر
|
||||
- فیلدهای موجود در User entity:
|
||||
- EmailNotifications, SmsNotifications, PushNotifications
|
||||
- مقادیر پیشفرض برای فیلدهای ناموجود:
|
||||
- MarketingNotifications = false
|
||||
- PreferredLanguage = "fa"
|
||||
- TimeZone = "Asia/Tehran"
|
||||
- TwoFactorAuthEnabled = false
|
||||
|
||||
#### 8.4 UserService
|
||||
**فایل**: `CMSMicroservice.WebApi/Services/UserService.cs`
|
||||
|
||||
**تغییرات**:
|
||||
- افزودن ISender و Query imports
|
||||
- پیادهسازی 3 متد Customer با Query Handler واقعی
|
||||
- تبدیل DateTime به Timestamp با `SpecifyKind(DateTimeKind.Utc)`
|
||||
- **رفع ambiguity**: fully qualified names برای CustomerReferralStats و CustomerReferralModel
|
||||
|
||||
---
|
||||
|
||||
## آمار پیشرفت
|
||||
|
||||
### سرویسهای تکمیل شده (8/8): ✅ 100%
|
||||
✅ **UserWallet** (5 endpoints)
|
||||
✅ **Commission** (2 endpoints)
|
||||
✅ **NetworkMembership** (3 endpoints)
|
||||
✅ **Package** (3 endpoints)
|
||||
✅ **UserOrder** (3 endpoints)
|
||||
✅ **Transaction** (2 endpoints)
|
||||
✅ **Products** (2 endpoints)
|
||||
✅ **User** (3 endpoints)
|
||||
|
||||
**جمع کل**: **25 endpoint** با الگوی ICurrentUserService پیادهسازی شد
|
||||
|
||||
---
|
||||
|
||||
## نکات فنی
|
||||
|
||||
### Entity Navigation Properties
|
||||
همیشه از `.Include()` برای load کردن navigation propertyهای مورد نیاز استفاده شود:
|
||||
```csharp
|
||||
query = query.Include(x => x.Package)
|
||||
.Include(x => x.Transaction);
|
||||
```
|
||||
|
||||
### Pagination
|
||||
از extension methodهای `GetMetaData` و `PaginatedListAsync` استفاده شود:
|
||||
```csharp
|
||||
var metaData = await query.GetMetaData(request.PaginationState, cancellationToken);
|
||||
var items = await query.PaginatedListAsync(request.PaginationState).ToListAsync(cancellationToken);
|
||||
```
|
||||
|
||||
### DateTime Mapping
|
||||
برای تبدیل به Protobuf Timestamp، DateTime باید UTC باشد:
|
||||
```csharp
|
||||
Timestamp.FromDateTime(DateTime.SpecifyKind(dateTime, DateTimeKind.Utc))
|
||||
```
|
||||
|
||||
### Enum Casting
|
||||
برای نگاشت enumها بین Application و Proto:
|
||||
```csharp
|
||||
Status = (PaymentStatusEnum)order.PaymentStatus
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build Status
|
||||
✅ **آخرین Build موفق**: 0 Error(s), 66 Warning(s) - Time Elapsed 00:00:03.55
|
||||
|
||||
---
|
||||
|
||||
## تاریخ آخرین بهروزرسانی
|
||||
5 فوریه 2026
|
||||
|
||||
---
|
||||
|
||||
## نتیجهگیری
|
||||
پیادهسازی ICurrentUserService در **25 endpoint** مربوط به **8 سرویس** با موفقیت کامل شد.
|
||||
|
||||
### دستاوردها:
|
||||
- ✅ **100% Coverage**: تمام endpointهای Customer پیادهسازی شدند
|
||||
- ✅ **الگوی Consistent**: pattern مشخص برای تمام سرویسها
|
||||
- ✅ **امنیت بالا**: استخراج خودکار UserId از JWT
|
||||
- ✅ **قابلیت نگهداری**: کد تمیز و قابل فهم
|
||||
- ✅ **Build موفق**: بدون هیچ خطا
|
||||
|
||||
### چالشهای حل شده:
|
||||
- Entityهای بدون UserId (Transaction, UserWalletChangeLog)
|
||||
- Proto/Application type ambiguity
|
||||
- MetaData بدون constructor
|
||||
- Category path building
|
||||
- Proto enum mapping
|
||||
- DateTime UTC conversion
|
||||
|
||||
تمام تغییرات compile میشوند و آماده تست و deployment هستند.
|
||||
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
# FrontOffice.BFF to CMS Migration Progress
|
||||
|
||||
## Migration Overview
|
||||
مهاجرت سرویسهای FrontOffice.BFF به CMS Microservice با معماری Clean Architecture و gRPC.
|
||||
|
||||
## ✅ Completed Services
|
||||
|
||||
### 1. Categories Service
|
||||
- **Status**: ✅ Complete
|
||||
- **Proto Definition**: `categories.proto`
|
||||
- **Service Implementation**: `CategoryService.cs`
|
||||
- **Methods Migrated**:
|
||||
- Admin Methods:
|
||||
- `AddNewCategory` - افزودن دستهبندی جدید
|
||||
- `UpdateCategory` - بروزرسانی دستهبندی
|
||||
- `DeleteCategory` - حذف دستهبندی
|
||||
- `GetCategory` - دریافت یک دستهبندی
|
||||
- `GetAllCategoriesByFilter` - دریافت لیست دستهبندیها
|
||||
- Customer Methods:
|
||||
- `GetActiveCategoriesForCustomer` - دریافت دستهبندیهای فعال برای مشتری
|
||||
|
||||
### 2. City Service
|
||||
- **Status**: ✅ Complete
|
||||
- **Proto Definition**: `city.proto`
|
||||
- **Service Implementation**: `CityService.cs`
|
||||
- **Methods Migrated**:
|
||||
- Admin Methods:
|
||||
- `AddNewCity` - افزودن شهر جدید
|
||||
- `UpdateCity` - بروزرسانی شهر
|
||||
- `DeleteCity` - حذف شهر
|
||||
- `GetCity` - دریافت یک شهر
|
||||
- `GetAllCitiesByFilter` - دریافت لیست شهرها
|
||||
- Customer Methods:
|
||||
- `GetActiveCitiesForCustomer` - دریافت شهرهای فعال برای مشتری
|
||||
|
||||
### 3. UserCarts Service
|
||||
- **Status**: ✅ Complete
|
||||
- **Proto Definition**: `usercarts.proto`
|
||||
- **Service Implementation**: `UserCartsService.cs`
|
||||
- **Methods Migrated**:
|
||||
- Admin Methods:
|
||||
- `AddNewUserCart` - افزودن سبد خرید جدید
|
||||
- `UpdateUserCart` - بروزرسانی سبد خرید
|
||||
- `DeleteUserCart` - حذف سبد خرید
|
||||
- `GetUserCart` - دریافت سبد خرید (Admin)
|
||||
- `GetAllUserCartsByFilter` - دریافت لیست سبدهای خرید
|
||||
- Customer Methods:
|
||||
- `AddNewUserCartForCustomer` - افزودن محصول به سبد (Customer)
|
||||
- `UpdateUserCartForCustomer` - بروزرسانی تعداد محصول در سبد
|
||||
- `RemoveUserCartForCustomer` - حذف محصول از سبد
|
||||
- `GetCustomerCart` - دریافت سبد خرید مشتری
|
||||
|
||||
## 🛠️ Technical Implementation Details
|
||||
|
||||
### gRPC HTTP Annotations
|
||||
تمام سرویسها با HTTP annotations تعریف شدهاند:
|
||||
- Admin endpoints: `/ServiceName` pattern
|
||||
- Customer endpoints: `/Customer/Action` pattern
|
||||
|
||||
### Clean Architecture Structure
|
||||
```
|
||||
CMSMicroservice.Domain/ # Core business entities
|
||||
CMSMicroservice.Application/ # Business logic & CQRS
|
||||
CMSMicroservice.Infrastructure/ # Data access & external services
|
||||
CMSMicroservice.WebApi/ # gRPC services & controllers
|
||||
CMSMicroservice.Protobuf/ # Protocol buffer definitions
|
||||
```
|
||||
|
||||
### Swagger Integration
|
||||
- Multiple Swagger documents: cms, admin, customer, unified
|
||||
- gRPC HTTP transcoding enabled
|
||||
- Custom CSS styling applied
|
||||
- Conflict resolution implemented
|
||||
|
||||
## 🔧 Issues Resolved
|
||||
|
||||
### 1. Swagger Conflict Resolution
|
||||
**Problem**:
|
||||
```
|
||||
Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorException:
|
||||
Conflicting method/path combination "GET GetUserCart"
|
||||
```
|
||||
|
||||
**Root Cause**:
|
||||
- دو method با operation ID یکسان: `GetUserCart` و `GetUserCartForCustomer`
|
||||
- Swagger از method name برای operation ID استفاده میکند
|
||||
|
||||
**Solutions Attempted**:
|
||||
1. ❌ `CustomOperationIds` - ineffective
|
||||
2. ❌ `ResolveConflictingActions` - incomplete resolution
|
||||
3. ✅ **Method Renaming** - successful
|
||||
|
||||
**Final Solution**:
|
||||
```protobuf
|
||||
// Before (conflicting):
|
||||
rpc GetUserCartForCustomer(GetUserCartForCustomerRequest) returns (GetUserCartForCustomerResponse)
|
||||
|
||||
// After (resolved):
|
||||
rpc GetCustomerCart(GetUserCartForCustomerRequest) returns (GetUserCartForCustomerResponse)
|
||||
```
|
||||
|
||||
### 2. Application Layer Dependencies
|
||||
**Problem**: Build errors در Application layer
|
||||
**Solution**: پاکسازی dependencies و rebuild پروژه
|
||||
|
||||
## 📊 Migration Status Summary
|
||||
|
||||
| Service | Proto ✅ | Implementation ✅ | Build ✅ | Swagger ✅ |
|
||||
|---------|----------|-------------------|----------|------------|
|
||||
| Categories | ✅ | ✅ | ✅ | ✅ |
|
||||
| City | ✅ | ✅ | ✅ | ✅ |
|
||||
| UserCarts | ✅ | ✅ | ✅ | ✅ |
|
||||
|
||||
## 🎯 Next Steps
|
||||
1. **Service Integration Testing** - تست عملکرد سرویسهای migrate شده
|
||||
2. **Business Logic Implementation** - پیادهسازی منطق کسبوکار واقعی
|
||||
3. **Database Integration** - اتصال به لایه دیتا
|
||||
4. **Continue Migration** - ادامه migration سایر سرویسها
|
||||
|
||||
## 🏗️ Technical Architecture
|
||||
|
||||
### gRPC Service Pattern
|
||||
```csharp
|
||||
public class ServiceName : ServiceContract.ServiceContractBase
|
||||
{
|
||||
private readonly IDispatchRequestToCQRS _dispatcher;
|
||||
|
||||
// Customer Methods Section
|
||||
#region Customer Methods
|
||||
public override async Task<Response> CustomerMethod(Request request, ServerCallContext context)
|
||||
{
|
||||
// Implementation
|
||||
}
|
||||
#endregion
|
||||
|
||||
// Admin Methods Section
|
||||
#region Admin Methods
|
||||
public override async Task<Response> AdminMethod(Request request, ServerCallContext context)
|
||||
{
|
||||
// Implementation
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
```
|
||||
|
||||
### Proto File Structure
|
||||
```protobuf
|
||||
syntax = "proto3";
|
||||
import "google/api/annotations.proto";
|
||||
|
||||
service ServiceContract {
|
||||
// ============= Admin Methods =============
|
||||
rpc AdminMethod(Request) returns (Response) {
|
||||
option (google.api.http) = {
|
||||
post: "/AdminEndpoint"
|
||||
body: "*"
|
||||
};
|
||||
};
|
||||
|
||||
// ============= Customer Methods =============
|
||||
rpc CustomerMethod(Request) returns (Response) {
|
||||
option (google.api.http) = {
|
||||
get: "/Customer/Endpoint"
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 📈 Performance & Quality
|
||||
- ✅ All services compile successfully
|
||||
- ✅ Swagger documentation accessible
|
||||
- ✅ gRPC HTTP transcoding working
|
||||
- ✅ Clean separation of Admin/Customer concerns
|
||||
- ✅ Consistent naming conventions applied
|
||||
|
||||
---
|
||||
**Last Updated**: January 30, 2026
|
||||
**Migration Phase**: Foundation Services Complete
|
||||
**Next Milestone**: Business Logic Implementation
|
||||
@@ -1,190 +0,0 @@
|
||||
# وضعیت Refactoring سیستم انبارداری (Inventory)
|
||||
|
||||
**تاریخ:** ۳ ژانویه ۲۰۲۶
|
||||
**وضعیت:** ✅ تکمیل شده - Build موفق
|
||||
|
||||
---
|
||||
|
||||
## 📊 وضعیت Build
|
||||
|
||||
| پروژه | وضعیت |
|
||||
|-------|--------|
|
||||
| CMSMicroservice.Domain | ✅ OK |
|
||||
| CMSMicroservice.Application | ✅ OK |
|
||||
| CMSMicroservice.Infrastructure | ✅ OK |
|
||||
| CMSMicroservice.WebApi | ✅ OK |
|
||||
|
||||
---
|
||||
|
||||
## ✅ کارهای انجام شده
|
||||
|
||||
### 1. حذف Repository Pattern
|
||||
فایلهای حذف شده:
|
||||
- `Application/Common/Interfaces/Repositories/IInventoryItemRepository.cs`
|
||||
- `Application/Common/Interfaces/Repositories/IStockMovementRepository.cs`
|
||||
- `Application/Common/Interfaces/Repositories/IWarehouseRepository.cs`
|
||||
- `Infrastructure/Persistence/Repositories/InventoryItemRepository.cs`
|
||||
- `Infrastructure/Persistence/Repositories/StockMovementRepository.cs`
|
||||
- `Infrastructure/Persistence/Repositories/WarehouseRepository.cs`
|
||||
|
||||
### 2. حذف Features قدیمی
|
||||
فولدر حذف شده:
|
||||
- `Application/Features/` (کل فولدر)
|
||||
|
||||
### 3. ایجاد ساختار CQ جدید
|
||||
|
||||
#### WarehouseCQ/
|
||||
```
|
||||
WarehouseCQ/
|
||||
├── Commands/
|
||||
│ ├── CreateWarehouse/
|
||||
│ ├── UpdateWarehouse/
|
||||
│ ├── DeleteWarehouse/
|
||||
│ └── SetDefaultWarehouse/
|
||||
└── Queries/
|
||||
├── GetWarehouse/
|
||||
├── GetAllWarehouses/
|
||||
└── SearchWarehouses/
|
||||
```
|
||||
|
||||
#### InventoryItemCQ/
|
||||
```
|
||||
InventoryItemCQ/
|
||||
├── Commands/
|
||||
│ ├── CreateInventoryItem/
|
||||
│ ├── UpdateInventoryItem/
|
||||
│ ├── DeleteInventoryItem/
|
||||
│ ├── UpdateInventoryQuantity/
|
||||
│ ├── ReserveInventory/
|
||||
│ ├── ReleaseReservedInventory/
|
||||
│ ├── ReduceInventory/
|
||||
│ └── IncreaseInventory/
|
||||
└── Queries/
|
||||
├── GetInventoryItem/
|
||||
├── GetInventoryByProduct/
|
||||
├── GetAllInventoryItems/
|
||||
└── GetLowStockItems/
|
||||
```
|
||||
|
||||
#### StockMovementCQ/
|
||||
```
|
||||
StockMovementCQ/
|
||||
├── Commands/
|
||||
│ └── CreateStockMovement/
|
||||
└── Queries/
|
||||
├── GetStockMovements/
|
||||
└── GetStockMovementsByInventoryItem/
|
||||
```
|
||||
|
||||
### 4. Fix شدن InventoryProfile.cs
|
||||
- اصلاح enum names: `ProtoProductType.Unspecified` بجای `ProductTypeUnspecified`
|
||||
- حذف `new Int64Value` - Proto مستقیم `long?` میگیره
|
||||
- اصلاح expression tree برای `?.` operator
|
||||
|
||||
### 5. سادهسازی InventoryService.cs
|
||||
- متدهای اصلی (Warehouse, Query ها) کامل پیادهسازی شدن
|
||||
- متدهای پیچیده که نیاز به lookup دارن فعلاً TODO هستن
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ متدهای TODO در InventoryService
|
||||
|
||||
این متدها نیاز به پیادهسازی دارن (وقتی لازم شد):
|
||||
|
||||
| متد | دلیل TODO |
|
||||
|-----|-----------|
|
||||
| `AddStock` | نیاز به lookup با ProductId/ProductType |
|
||||
| `AdjustStock` | نیاز به lookup با ProductId/ProductType |
|
||||
| `ReserveStock` | نیاز به lookup با ProductId/ProductType |
|
||||
| `ReleaseReservation` | نیاز به lookup با ProductId/ProductType |
|
||||
| `ConfirmSale` | نیاز به lookup با ProductId/ProductType |
|
||||
| `ProcessReturn` | نیاز به lookup با ProductId/ProductType |
|
||||
| `RecordLoss` | نیاز به lookup با ProductId/ProductType |
|
||||
| `BulkAddStock` | نیاز به loop و lookup |
|
||||
| `BulkAdjustStock` | نیاز به loop و lookup |
|
||||
| `GetInventorySummary` | نیاز به Query جدید |
|
||||
| `GetStockValueReport` | نیاز به Query جدید |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 درسهای آموخته شده
|
||||
|
||||
1. **همیشه اول Proto رو بررسی کن** - Proto مرجع اصلی API هست
|
||||
2. **ساختار موجود رو تحلیل کن** - قبل از ساختن فایل جدید، نمونههای موجود رو ببین
|
||||
3. **Mapping از Proto به Command** - نه برعکس!
|
||||
4. **IApplicationDbContext** - الگوی استاندارد این پروژه برای دسترسی به DB
|
||||
5. **بدون Repository** - این پروژه از Repository pattern استفاده نمیکنه
|
||||
6. **Proto enum names** - نامها در C# متفاوت هستن (مثلاً `Unspecified` بجای `PRODUCT_TYPE_UNSPECIFIED`)
|
||||
7. **Int64Value در Proto** - در C# به `long?` تبدیل میشه، نیازی به `new Int64Value` نیست
|
||||
|
||||
---
|
||||
|
||||
## 🔄 همگامسازی BFF با CMS (۳ ژانویه ۲۰۲۶)
|
||||
|
||||
### تغییرات Proto
|
||||
BackOffice.BFF.Inventory.Protobuf با CMS همگام شد:
|
||||
|
||||
| آیتم | قبل | بعد |
|
||||
|------|-----|-----|
|
||||
| ProductType enum | `REGULAR`, `DISCOUNT` | `REGULAR_PRODUCT`, `DISCOUNT_PRODUCT` |
|
||||
| StockMovementType | Sequential (0-9) | Grouped (10, 20, 30, 40, 50) |
|
||||
| Pagination | `page_index` | `page` |
|
||||
| Search | `search_term` | `search` |
|
||||
| Product name | `product_name` | `product_title` |
|
||||
|
||||
### فایلهای آپدیت شده در BFF
|
||||
|
||||
**Commands:**
|
||||
- `AddStock` - حذف Success, Message از Response
|
||||
- `AdjustStock` - Note→Reason, +ReferenceNumber
|
||||
- `RecordLoss` - Note→Reason, +ReferenceNumber
|
||||
- `UpdateInventorySettings` - InventoryItemId→Id
|
||||
|
||||
**Queries:**
|
||||
- `GetAllInventoryItems` - PageIndex→Page, SearchTerm→Search, +ProductPrice
|
||||
- `GetStockMovements` - PageIndex→Page, +ProductTitle, +Created
|
||||
- `GetLowStockItems` - حذف Count، استفاده از Page/PageSize
|
||||
- `GetAllWarehouses` - ActiveOnly→IsActive, +Created, +LastModified
|
||||
|
||||
**Mappings:**
|
||||
- `InventoryProfile.cs` - بازنویسی کامل برای فیلدهای جدید
|
||||
|
||||
### وضعیت Build BFF
|
||||
```
|
||||
Build succeeded.
|
||||
0 Warning(s)
|
||||
0 Error(s)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 پوشش API - مقایسه CMS و BFF
|
||||
|
||||
| عملیات | CMS | BFF | یادداشت |
|
||||
|--------|-----|-----|---------|
|
||||
| GetAllInventoryItems | ✅ | ✅ | همگام |
|
||||
| GetInventoryItem | ✅ | ✅ | همگام |
|
||||
| GetLowStockItems | ✅ | ✅ | همگام |
|
||||
| GetStockMovements | ✅ | ✅ | همگام |
|
||||
| GetAllWarehouses | ✅ | ✅ | همگام |
|
||||
| AddStock | ✅ | ✅ | همگام |
|
||||
| AdjustStock | ✅ | ✅ | همگام |
|
||||
| RecordLoss | ✅ | ✅ | همگام |
|
||||
| CreateWarehouse | ✅ | ✅ | همگام |
|
||||
| UpdateWarehouse | ✅ | ❌ | نیاز به پیادهسازی |
|
||||
| UpdateInventorySettings | ✅ | ✅ | همگام |
|
||||
| GetInventorySummary | TODO | ❌ | اولویت بالا |
|
||||
| GetStockValueReport | TODO | ❌ | اولویت بالا |
|
||||
| ProcessReturn | TODO | ❌ | اولویت متوسط |
|
||||
|
||||
---
|
||||
|
||||
## 📝 نتیجهگیری
|
||||
|
||||
✅ **Refactoring با موفقیت تکمیل شد!**
|
||||
|
||||
- Application layer با ساختار `*CQ/Commands/[Action]/` سازگار شد
|
||||
- Repository pattern کاملاً حذف شد
|
||||
- WebApi layer با Proto سازگار شد
|
||||
- Build همه پروژهها موفق هست
|
||||
- **BFF کاملاً با CMS همگام شد (۳ ژانویه ۲۰۲۶)**
|
||||
@@ -0,0 +1 @@
|
||||
Docs moved to /totalDoc — see totalDoc/INDEX.md
|
||||
@@ -1,490 +0,0 @@
|
||||
# Club Feature Management Services - Implementation Guide
|
||||
|
||||
## Overview
|
||||
Admin services for managing user club features (enable/disable features per user).
|
||||
|
||||
## Created Files
|
||||
|
||||
### 1. CQRS Layer (Application)
|
||||
|
||||
#### Query: GetUserClubFeatures
|
||||
**Location:** `/CMS/src/CMSMicroservice.Application/ClubFeatureCQ/Queries/GetUserClubFeatures/`
|
||||
|
||||
**Files:**
|
||||
- `GetUserClubFeaturesQuery.cs` - Query definition
|
||||
- `GetUserClubFeaturesQueryHandler.cs` - Query handler
|
||||
- `UserClubFeatureDto.cs` - Response DTO
|
||||
|
||||
**Purpose:** Get list of all club features for a specific user with their active status.
|
||||
|
||||
**Input:**
|
||||
```csharp
|
||||
public record GetUserClubFeaturesQuery : IRequest<List<UserClubFeatureDto>>
|
||||
{
|
||||
public long UserId { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```csharp
|
||||
public class UserClubFeatureDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long UserId { get; set; }
|
||||
public long ClubMembershipId { get; set; }
|
||||
public long ClubFeatureId { get; set; }
|
||||
public string FeatureTitle { get; set; }
|
||||
public string? FeatureDescription { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public DateTime GrantedAt { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
**Logic:**
|
||||
- Joins `UserClubFeatures` with `ClubFeature` table
|
||||
- Filters by `UserId` and `!IsDeleted`
|
||||
- Returns list of features with their active status
|
||||
|
||||
---
|
||||
|
||||
#### Command: ToggleUserClubFeature
|
||||
**Location:** `/CMS/src/CMSMicroservice.Application/ClubFeatureCQ/Commands/ToggleUserClubFeature/`
|
||||
|
||||
**Files:**
|
||||
- `ToggleUserClubFeatureCommand.cs` - Command definition
|
||||
- `ToggleUserClubFeatureCommandHandler.cs` - Command handler
|
||||
- `ToggleUserClubFeatureResponse.cs` - Response DTO
|
||||
|
||||
**Purpose:** Enable or disable a specific club feature for a user.
|
||||
|
||||
**Input:**
|
||||
```csharp
|
||||
public record ToggleUserClubFeatureCommand : IRequest<ToggleUserClubFeatureResponse>
|
||||
{
|
||||
public long UserId { get; init; }
|
||||
public long ClubFeatureId { get; init; }
|
||||
public bool IsActive { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```csharp
|
||||
public class ToggleUserClubFeatureResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string Message { get; set; }
|
||||
public long? UserClubFeatureId { get; set; }
|
||||
public bool? IsActive { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
**Validations:**
|
||||
1. ✅ User exists and not deleted
|
||||
2. ✅ Club feature exists and not deleted
|
||||
3. ✅ User has this feature assigned (exists in UserClubFeatures)
|
||||
|
||||
**Logic:**
|
||||
- Find `UserClubFeature` record by `UserId` + `ClubFeatureId`
|
||||
- Update `IsActive` field
|
||||
- Set `LastModified` timestamp
|
||||
- Save changes
|
||||
|
||||
**Error Messages:**
|
||||
- "کاربر یافت نشد" - User not found
|
||||
- "ویژگی باشگاه یافت نشد" - Club feature not found
|
||||
- "این ویژگی برای کاربر یافت نشد" - User doesn't have this feature
|
||||
|
||||
**Success Messages:**
|
||||
- "ویژگی با موفقیت فعال شد" - Feature activated successfully
|
||||
- "ویژگی با موفقیت غیرفعال شد" - Feature deactivated successfully
|
||||
|
||||
---
|
||||
|
||||
### 2. gRPC Layer (Protobuf + WebApi)
|
||||
|
||||
#### Proto Definition
|
||||
**File:** `/CMS/src/CMSMicroservice.Protobuf/Protos/clubmembership.proto`
|
||||
|
||||
**Added RPC Methods:**
|
||||
```protobuf
|
||||
rpc GetUserClubFeatures(GetUserClubFeaturesRequest) returns (GetUserClubFeaturesResponse){
|
||||
option (google.api.http) = {
|
||||
get: "/ClubFeature/GetUserFeatures"
|
||||
};
|
||||
};
|
||||
|
||||
rpc ToggleUserClubFeature(ToggleUserClubFeatureRequest) returns (ToggleUserClubFeatureResponse){
|
||||
option (google.api.http) = {
|
||||
post: "/ClubFeature/ToggleFeature"
|
||||
body: "*"
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
**Message Definitions:**
|
||||
```protobuf
|
||||
message GetUserClubFeaturesRequest {
|
||||
int64 user_id = 1;
|
||||
}
|
||||
|
||||
message GetUserClubFeaturesResponse {
|
||||
repeated UserClubFeatureModel features = 1;
|
||||
}
|
||||
|
||||
message UserClubFeatureModel {
|
||||
int64 id = 1;
|
||||
int64 user_id = 2;
|
||||
int64 club_membership_id = 3;
|
||||
int64 club_feature_id = 4;
|
||||
string feature_title = 5;
|
||||
string feature_description = 6;
|
||||
bool is_active = 7;
|
||||
google.protobuf.Timestamp granted_at = 8;
|
||||
string notes = 9;
|
||||
}
|
||||
|
||||
message ToggleUserClubFeatureRequest {
|
||||
int64 user_id = 1;
|
||||
int64 club_feature_id = 2;
|
||||
bool is_active = 3;
|
||||
}
|
||||
|
||||
message ToggleUserClubFeatureResponse {
|
||||
bool success = 1;
|
||||
string message = 2;
|
||||
google.protobuf.Int64Value user_club_feature_id = 3;
|
||||
google.protobuf.BoolValue is_active = 4;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### gRPC Service Implementation
|
||||
**File:** `/CMS/src/CMSMicroservice.WebApi/Services/ClubMembershipService.cs`
|
||||
|
||||
**Added Methods:**
|
||||
```csharp
|
||||
public override async Task<GetUserClubFeaturesResponse> GetUserClubFeatures(
|
||||
GetUserClubFeaturesRequest request,
|
||||
ServerCallContext context)
|
||||
{
|
||||
return await _dispatchRequestToCQRS.Handle<
|
||||
GetUserClubFeaturesRequest,
|
||||
GetUserClubFeaturesQuery,
|
||||
GetUserClubFeaturesResponse>(request, context);
|
||||
}
|
||||
|
||||
public override async Task<Protobuf.Protos.ClubMembership.ToggleUserClubFeatureResponse>
|
||||
ToggleUserClubFeature(
|
||||
ToggleUserClubFeatureRequest request,
|
||||
ServerCallContext context)
|
||||
{
|
||||
return await _dispatchRequestToCQRS.Handle<
|
||||
ToggleUserClubFeatureRequest,
|
||||
ToggleUserClubFeatureCommand,
|
||||
Protobuf.Protos.ClubMembership.ToggleUserClubFeatureResponse>(request, context);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### AutoMapper Profile
|
||||
**File:** `/CMS/src/CMSMicroservice.WebApi/Common/Mappings/ClubFeatureProfile.cs`
|
||||
|
||||
**Mappings:**
|
||||
1. `GetUserClubFeaturesRequest` → `GetUserClubFeaturesQuery`
|
||||
2. `UserClubFeatureDto` → `UserClubFeatureModel` (Proto)
|
||||
3. `List<UserClubFeatureDto>` → `GetUserClubFeaturesResponse`
|
||||
4. `ToggleUserClubFeatureRequest` → `ToggleUserClubFeatureCommand`
|
||||
5. `ToggleUserClubFeatureResponse` (App) → `ToggleUserClubFeatureResponse` (Proto)
|
||||
|
||||
**Special Handling:**
|
||||
- DateTime conversion to `Timestamp` (Protobuf format)
|
||||
- Null-safe mapping for optional fields
|
||||
- Fully qualified type names to avoid ambiguity
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### 1. Get User Club Features
|
||||
**Method:** GET
|
||||
**Endpoint:** `/ClubFeature/GetUserFeatures`
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"user_id": 123
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"features": [
|
||||
{
|
||||
"id": 1,
|
||||
"user_id": 123,
|
||||
"club_membership_id": 456,
|
||||
"club_feature_id": 1,
|
||||
"feature_title": "دسترسی به فروشگاه تخفیف",
|
||||
"feature_description": "امکان خرید از فروشگاه تخفیف",
|
||||
"is_active": true,
|
||||
"granted_at": "2025-12-09T18:30:00Z",
|
||||
"notes": "اعطا شده بهطور خودکار هنگام فعالسازی"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Toggle User Club Feature
|
||||
**Method:** POST
|
||||
**Endpoint:** `/ClubFeature/ToggleFeature`
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"user_id": 123,
|
||||
"club_feature_id": 1,
|
||||
"is_active": false
|
||||
}
|
||||
```
|
||||
|
||||
**Response (Success):**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "ویژگی با موفقیت غیرفعال شد",
|
||||
"user_club_feature_id": 1,
|
||||
"is_active": false
|
||||
}
|
||||
```
|
||||
|
||||
**Response (Error - User Not Found):**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "کاربر یافت نشد"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (Error - Feature Not Found):**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "ویژگی باشگاه یافت نشد"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (Error - User Doesn't Have Feature):**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "این ویژگی برای کاربر یافت نشد"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Table: UserClubFeatures
|
||||
Existing table with newly added `IsActive` field:
|
||||
|
||||
```sql
|
||||
CREATE TABLE [CMS].[UserClubFeatures]
|
||||
(
|
||||
[Id] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||
[UserId] BIGINT NOT NULL,
|
||||
[ClubMembershipId] BIGINT NOT NULL,
|
||||
[ClubFeatureId] BIGINT NOT NULL,
|
||||
[GrantedAt] DATETIME2 NOT NULL,
|
||||
[IsActive] BIT NOT NULL DEFAULT 1, -- ← NEW FIELD
|
||||
[Notes] NVARCHAR(MAX) NULL,
|
||||
[Created] DATETIME2 NOT NULL,
|
||||
[CreatedBy] NVARCHAR(MAX) NULL,
|
||||
[LastModified] DATETIME2 NULL,
|
||||
[LastModifiedBy] NVARCHAR(MAX) NULL,
|
||||
[IsDeleted] BIT NOT NULL DEFAULT 0,
|
||||
|
||||
CONSTRAINT FK_UserClubFeatures_Users FOREIGN KEY ([UserId])
|
||||
REFERENCES [Identity].[Users]([Id]),
|
||||
CONSTRAINT FK_UserClubFeatures_ClubMembership FOREIGN KEY ([ClubMembershipId])
|
||||
REFERENCES [CMS].[ClubMembership]([Id]),
|
||||
CONSTRAINT FK_UserClubFeatures_ClubFeatures FOREIGN KEY ([ClubFeatureId])
|
||||
REFERENCES [CMS].[ClubFeatures]([Id])
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Admin Panel Scenario
|
||||
|
||||
#### 1. View User's Club Features
|
||||
```csharp
|
||||
// Admin selects user ID: 123
|
||||
var request = new GetUserClubFeaturesRequest { UserId = 123 };
|
||||
var response = await client.GetUserClubFeaturesAsync(request);
|
||||
|
||||
// Display in grid:
|
||||
foreach (var feature in response.Features)
|
||||
{
|
||||
Console.WriteLine($"Feature: {feature.FeatureTitle}");
|
||||
Console.WriteLine($"Status: {(feature.IsActive ? "فعال" : "غیرفعال")}");
|
||||
Console.WriteLine($"Granted: {feature.GrantedAt}");
|
||||
Console.WriteLine("---");
|
||||
}
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```
|
||||
Feature: دسترسی به فروشگاه تخفیف
|
||||
Status: فعال
|
||||
Granted: 2025-12-09 18:30:00
|
||||
---
|
||||
Feature: دسترسی به کمیسیون هفتگی
|
||||
Status: فعال
|
||||
Granted: 2025-12-09 18:30:00
|
||||
---
|
||||
Feature: دسترسی به شارژ شبکه
|
||||
Status: غیرفعال
|
||||
Granted: 2025-12-09 18:30:00
|
||||
---
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 2. Disable a Feature
|
||||
```csharp
|
||||
// Admin clicks "Disable" on Feature ID: 3
|
||||
var request = new ToggleUserClubFeatureRequest
|
||||
{
|
||||
UserId = 123,
|
||||
ClubFeatureId = 3,
|
||||
IsActive = false
|
||||
};
|
||||
|
||||
var response = await client.ToggleUserClubFeatureAsync(request);
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
Console.WriteLine(response.Message);
|
||||
// Output: ویژگی با موفقیت غیرفعال شد
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 3. Re-enable a Feature
|
||||
```csharp
|
||||
// Admin clicks "Enable" on Feature ID: 3
|
||||
var request = new ToggleUserClubFeatureRequest
|
||||
{
|
||||
UserId = 123,
|
||||
ClubFeatureId = 3,
|
||||
IsActive = true
|
||||
};
|
||||
|
||||
var response = await client.ToggleUserClubFeatureAsync(request);
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
Console.WriteLine(response.Message);
|
||||
// Output: ویژگی با موفقیت فعال شد
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Unit Tests (Recommended)
|
||||
- [ ] GetUserClubFeaturesQueryHandler returns correct DTOs
|
||||
- [ ] ToggleUserClubFeatureCommandHandler validates user exists
|
||||
- [ ] ToggleUserClubFeatureCommandHandler validates feature exists
|
||||
- [ ] ToggleUserClubFeatureCommandHandler validates user has feature
|
||||
- [ ] ToggleUserClubFeatureCommandHandler updates IsActive correctly
|
||||
- [ ] ToggleUserClubFeatureCommandHandler sets LastModified timestamp
|
||||
|
||||
### Integration Tests
|
||||
- [ ] gRPC GetUserClubFeatures endpoint returns data
|
||||
- [ ] gRPC ToggleUserClubFeature endpoint updates database
|
||||
- [ ] AutoMapper mappings work correctly
|
||||
- [ ] Proto serialization/deserialization works
|
||||
|
||||
### Manual Testing
|
||||
1. **Get Features:**
|
||||
```bash
|
||||
grpcurl -d '{"user_id": 123}' \
|
||||
-plaintext localhost:5000 \
|
||||
clubmembership.ClubMembershipContract/GetUserClubFeatures
|
||||
```
|
||||
|
||||
2. **Disable Feature:**
|
||||
```bash
|
||||
grpcurl -d '{"user_id": 123, "club_feature_id": 1, "is_active": false}' \
|
||||
-plaintext localhost:5000 \
|
||||
clubmembership.ClubMembershipContract/ToggleUserClubFeature
|
||||
```
|
||||
|
||||
3. **Verify in Database:**
|
||||
```sql
|
||||
SELECT Id, UserId, ClubFeatureId, IsActive, LastModified
|
||||
FROM CMS.UserClubFeatures
|
||||
WHERE UserId = 123;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build Status
|
||||
✅ **All projects build successfully**
|
||||
- CMSMicroservice.Domain: ✅
|
||||
- CMSMicroservice.Application: ✅ (0 errors, 274 warnings)
|
||||
- CMSMicroservice.Protobuf: ✅
|
||||
- CMSMicroservice.WebApi: ✅ (0 errors, 17 warnings)
|
||||
|
||||
---
|
||||
|
||||
## Next Steps (Optional Enhancements)
|
||||
|
||||
1. **Authorization:**
|
||||
- Add `[Authorize(Roles = "Admin")]` attribute
|
||||
- Validate admin permissions before toggling
|
||||
|
||||
2. **Audit Logging:**
|
||||
- Log who changed the feature status
|
||||
- Track `LastModifiedBy` field
|
||||
|
||||
3. **Bulk Operations:**
|
||||
- Add endpoint to toggle multiple features at once
|
||||
- Add endpoint to enable/disable all features for a user
|
||||
|
||||
4. **History Tracking:**
|
||||
- Create `UserClubFeatureHistory` table
|
||||
- Log every status change with timestamp and reason
|
||||
|
||||
5. **Notifications:**
|
||||
- Send notification to user when feature is disabled
|
||||
- Email/SMS alert for important features
|
||||
|
||||
6. **Business Rules:**
|
||||
- Add validation: prevent disabling critical features
|
||||
- Add expiration dates for features
|
||||
- Add feature dependencies (e.g., Feature B requires Feature A)
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
✅ Created CQRS Query + Command for club feature management
|
||||
✅ Created gRPC Proto definitions and services
|
||||
✅ Created AutoMapper mappings
|
||||
✅ All builds successful
|
||||
✅ Ready for deployment and testing
|
||||
|
||||
**Total Files Created:** 8
|
||||
**Total Lines of Code:** ~350
|
||||
**Build Errors:** 0
|
||||
**Status:** ✅ Complete and ready for use
|
||||
+25
-1
@@ -3,14 +3,18 @@ namespace CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubMembership
|
||||
public class GetClubMembershipQueryHandler : IRequestHandler<GetClubMembershipQuery, ClubMembershipDto?>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ILogger<GetClubMembershipQueryHandler> _logger;
|
||||
|
||||
public GetClubMembershipQueryHandler(IApplicationDbContext context)
|
||||
public GetClubMembershipQueryHandler(IApplicationDbContext context, ILogger<GetClubMembershipQueryHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ClubMembershipDto?> Handle(GetClubMembershipQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("GetClubMembership called for UserId: {UserId}", request.UserId);
|
||||
|
||||
var membership = await _context.ClubMemberships
|
||||
.AsNoTracking()
|
||||
.Where(x => x.UserId == request.UserId)
|
||||
@@ -27,6 +31,26 @@ public class GetClubMembershipQueryHandler : IRequestHandler<GetClubMembershipQu
|
||||
})
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
// اگر کاربر عضویت نداره، یک DTO با وضعیت غیرفعال برگردون
|
||||
if (membership == null)
|
||||
{
|
||||
_logger.LogInformation("No membership found for UserId: {UserId}, returning inactive status", request.UserId);
|
||||
return new ClubMembershipDto
|
||||
{
|
||||
Id = 0,
|
||||
UserId = request.UserId,
|
||||
IsActive = false,
|
||||
ActivatedAt = null,
|
||||
InitialContribution = 0,
|
||||
TotalEarned = 0,
|
||||
Created = DateTimeOffset.UtcNow,
|
||||
LastModified = null
|
||||
};
|
||||
}
|
||||
|
||||
_logger.LogInformation("Membership found for UserId: {UserId}, IsActive: {IsActive}, Id: {Id}",
|
||||
request.UserId, membership.IsActive, membership.Id);
|
||||
|
||||
return membership;
|
||||
}
|
||||
}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
|
||||
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
|
||||
|
||||
/// <summary>
|
||||
/// Query برای دریافت پرداختهای کمیسیون کاربر جاری (از JWT)
|
||||
/// </summary>
|
||||
public record GetMyCommissionPayoutsQuery : IRequest<GetMyCommissionPayoutsResponseDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// فیلتر وضعیت
|
||||
/// </summary>
|
||||
public CommissionPayoutStatus? Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// شماره هفته (اختیاری)
|
||||
/// </summary>
|
||||
public long? WeekDefinitionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Pagination
|
||||
/// </summary>
|
||||
public PaginationState? PaginationState { get; init; }
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Application.Common.Extensions;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
|
||||
|
||||
public class GetMyCommissionPayoutsQueryHandler : IRequestHandler<GetMyCommissionPayoutsQuery, GetMyCommissionPayoutsResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ICurrentUserService _currentUser;
|
||||
|
||||
public GetMyCommissionPayoutsQueryHandler(
|
||||
IApplicationDbContext context,
|
||||
ICurrentUserService currentUser)
|
||||
{
|
||||
_context = context;
|
||||
_currentUser = currentUser;
|
||||
}
|
||||
|
||||
public async Task<GetMyCommissionPayoutsResponseDto> Handle(GetMyCommissionPayoutsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// دریافت UserId از JWT (فقط برای Customer API)
|
||||
if (!long.TryParse(_currentUser.UserId, out var userId))
|
||||
{
|
||||
throw new UnauthorizedAccessException("User not authenticated");
|
||||
}
|
||||
|
||||
var query = _context.UserCommissionPayouts
|
||||
.Include(x => x.WeekDefinition)
|
||||
.Where(x => x.UserId == userId)
|
||||
.AsNoTracking()
|
||||
.AsQueryable();
|
||||
|
||||
// فیلترها
|
||||
if (request.Status.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.Status == request.Status.Value);
|
||||
}
|
||||
|
||||
if (request.WeekDefinitionId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.WeekDefinitionId == request.WeekDefinitionId.Value);
|
||||
}
|
||||
|
||||
// مرتبسازی: جدیدترین اول
|
||||
query = query.OrderByDescending(x => x.Created);
|
||||
|
||||
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
|
||||
|
||||
var models = await query
|
||||
.PaginatedListAsync(paginationState: request.PaginationState)
|
||||
.Select(x => new GetMyCommissionPayoutsResponseModel
|
||||
{
|
||||
Id = x.Id,
|
||||
WeekDefinitionId = x.WeekDefinitionId,
|
||||
WeekDisplayName = x.WeekDefinition != null ? x.WeekDefinition.DisplayName : "",
|
||||
BalancesEarned = x.BalancesEarned,
|
||||
TotalAmount = x.TotalAmount,
|
||||
AmountFormatted = x.TotalAmount.ToString("N0") + " تومان",
|
||||
Status = x.Status,
|
||||
CalculatedDate = x.PaidAt ?? (DateTime?)x.Created,
|
||||
DatePersian = ""
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return new GetMyCommissionPayoutsResponseDto
|
||||
{
|
||||
MetaData = meta,
|
||||
Models = models
|
||||
};
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
|
||||
|
||||
public class GetMyCommissionPayoutsQueryValidator : AbstractValidator<GetMyCommissionPayoutsQuery>
|
||||
{
|
||||
public GetMyCommissionPayoutsQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.PaginationState)
|
||||
.NotNull()
|
||||
.WithMessage("Pagination state is required");
|
||||
|
||||
When(x => x.PaginationState != null, () =>
|
||||
{
|
||||
RuleFor(x => x.PaginationState!.PageNumber)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("Page number must be greater than 0");
|
||||
|
||||
RuleFor(x => x.PaginationState!.PageSize)
|
||||
.GreaterThan(0)
|
||||
.LessThanOrEqualTo(100)
|
||||
.WithMessage("Page size must be between 1 and 100");
|
||||
});
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
|
||||
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
|
||||
|
||||
public class GetMyCommissionPayoutsResponseDto
|
||||
{
|
||||
public MetaData? MetaData { get; set; }
|
||||
public List<GetMyCommissionPayoutsResponseModel> Models { get; set; } = new();
|
||||
}
|
||||
|
||||
public class GetMyCommissionPayoutsResponseModel
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long WeekDefinitionId { get; set; }
|
||||
public string WeekDisplayName { get; set; } = string.Empty;
|
||||
public int BalancesEarned { get; set; }
|
||||
public long TotalAmount { get; set; }
|
||||
public string AmountFormatted { get; set; } = string.Empty;
|
||||
public CommissionPayoutStatus Status { get; set; }
|
||||
public DateTime? CalculatedDate { get; set; }
|
||||
public string DatePersian { get; set; } = string.Empty;
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetUserWeeklyBalances;
|
||||
|
||||
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetMyWeeklyBalances;
|
||||
|
||||
/// <summary>
|
||||
/// Query برای دریافت تعادلهای هفتگی کاربر جاری (از JWT)
|
||||
/// </summary>
|
||||
public record GetMyWeeklyBalancesQuery : IRequest<GetUserWeeklyBalancesResponseDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// شناسه تعریف هفته (اختیاری)
|
||||
/// </summary>
|
||||
public long? WeekDefinitionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// فقط موارد Expired نشده؟
|
||||
/// </summary>
|
||||
public bool OnlyActive { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Pagination
|
||||
/// </summary>
|
||||
public PaginationState? PaginationState { get; init; }
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetUserWeeklyBalances;
|
||||
|
||||
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetMyWeeklyBalances;
|
||||
|
||||
/// <summary>
|
||||
/// Handler برای دریافت تعادلهای هفتگی کاربر جاری
|
||||
/// </summary>
|
||||
public class GetMyWeeklyBalancesQueryHandler : IRequestHandler<GetMyWeeklyBalancesQuery, GetUserWeeklyBalancesResponseDto>
|
||||
{
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
private readonly IMediator _mediator;
|
||||
private readonly ILogger<GetMyWeeklyBalancesQueryHandler> _logger;
|
||||
|
||||
public GetMyWeeklyBalancesQueryHandler(
|
||||
ICurrentUserService currentUserService,
|
||||
IMediator mediator,
|
||||
ILogger<GetMyWeeklyBalancesQueryHandler> logger)
|
||||
{
|
||||
_currentUserService = currentUserService;
|
||||
_mediator = mediator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<GetUserWeeklyBalancesResponseDto> Handle(GetMyWeeklyBalancesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// دریافت UserId از JWT
|
||||
if (!long.TryParse(_currentUserService.UserId, out var userId) || userId <= 0)
|
||||
{
|
||||
_logger.LogWarning("GetMyWeeklyBalances called without valid user authentication");
|
||||
throw new UnauthorizedAccessException("کاربر احراز هویت نشده است");
|
||||
}
|
||||
|
||||
_logger.LogInformation("GetMyWeeklyBalances for UserId: {UserId}, WeekDefinitionId: {WeekDefinitionId}",
|
||||
userId, request.WeekDefinitionId);
|
||||
|
||||
// فراخوانی GetUserWeeklyBalancesQuery با UserId از JWT
|
||||
var query = new GetUserWeeklyBalancesQuery
|
||||
{
|
||||
UserId = userId,
|
||||
WeekDefinitionId = request.WeekDefinitionId,
|
||||
OnlyActive = request.OnlyActive,
|
||||
PaginationState = request.PaginationState
|
||||
};
|
||||
|
||||
return await _mediator.Send(query, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace CMSMicroservice.Application.Common.Authorization;
|
||||
|
||||
/// <summary>
|
||||
/// سرویس بررسی مجوز کاربر بر اساس نقشهای JWT
|
||||
/// </summary>
|
||||
public interface IPermissionService
|
||||
{
|
||||
/// <summary>
|
||||
/// دریافت نقشهای کاربر فعلی از JWT Claims
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<string>> GetUserRolesAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// بررسی اینکه آیا کاربر فعلی مجوز مشخصی دارد
|
||||
/// </summary>
|
||||
Task<bool> HasPermissionAsync(string permission, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
namespace CMSMicroservice.Application.Common.Authorization;
|
||||
|
||||
/// <summary>
|
||||
/// ثوابت نام مجوزها — دستهبندی شده بر اساس حوزه
|
||||
/// </summary>
|
||||
public static class PermissionNames
|
||||
{
|
||||
// Dashboard
|
||||
public const string DashboardView = "dashboard.view";
|
||||
|
||||
// Orders
|
||||
public const string OrdersView = "orders.view";
|
||||
public const string OrdersCreate = "orders.create";
|
||||
public const string OrdersUpdate = "orders.update";
|
||||
public const string OrdersDelete = "orders.delete";
|
||||
public const string OrdersCancel = "orders.cancel";
|
||||
public const string OrdersApprove = "orders.approve";
|
||||
|
||||
// Products
|
||||
public const string ProductsView = "products.view";
|
||||
public const string ProductsCreate = "products.create";
|
||||
public const string ProductsUpdate = "products.update";
|
||||
public const string ProductsDelete = "products.delete";
|
||||
|
||||
// Users
|
||||
public const string UsersView = "users.view";
|
||||
public const string UsersUpdate = "users.update";
|
||||
public const string UsersDelete = "users.delete";
|
||||
|
||||
// Commission
|
||||
public const string CommissionView = "commission.view";
|
||||
public const string CommissionApproveWithdrawal = "commission.approve_withdrawal";
|
||||
|
||||
// Public Messages
|
||||
public const string PublicMessagesView = "publicmessages.view";
|
||||
public const string PublicMessagesCreate = "publicmessages.create";
|
||||
public const string PublicMessagesUpdate = "publicmessages.update";
|
||||
public const string PublicMessagesPublish = "publicmessages.publish";
|
||||
|
||||
// Manual Payments
|
||||
public const string ManualPaymentsView = "manualpayments.view";
|
||||
public const string ManualPaymentsCreate = "manualpayments.create";
|
||||
public const string ManualPaymentsApprove = "manualpayments.approve";
|
||||
|
||||
// Settings
|
||||
public const string SettingsView = "settings.view";
|
||||
public const string SettingsUpdate = "settings.update";
|
||||
public const string SettingsDelete = "settings.delete";
|
||||
public const string SettingsManageConfiguration = "settings.manage_configuration";
|
||||
public const string SettingsManageVat = "settings.manage_vat";
|
||||
|
||||
// Reports
|
||||
public const string ReportsView = "reports.view";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// نام نقشها
|
||||
/// </summary>
|
||||
public static class RoleNames
|
||||
{
|
||||
public const string SuperAdmin = "Administrator";
|
||||
public const string Admin = "Admin";
|
||||
public const string Inspector = "Inspector";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// تنظیمات نقش→مجوز — ماتریس دسترسی
|
||||
/// </summary>
|
||||
public static class RolePermissionConfig
|
||||
{
|
||||
private static readonly Dictionary<string, HashSet<string>> RolePermissions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
[RoleNames.SuperAdmin] = new(StringComparer.OrdinalIgnoreCase) { "*" }, // Full access
|
||||
|
||||
[RoleNames.Admin] = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
PermissionNames.DashboardView,
|
||||
PermissionNames.OrdersView,
|
||||
PermissionNames.OrdersCreate,
|
||||
PermissionNames.OrdersUpdate,
|
||||
PermissionNames.OrdersCancel,
|
||||
PermissionNames.ProductsView,
|
||||
PermissionNames.ProductsCreate,
|
||||
PermissionNames.ProductsUpdate,
|
||||
PermissionNames.ProductsDelete,
|
||||
PermissionNames.UsersView,
|
||||
PermissionNames.UsersUpdate,
|
||||
PermissionNames.CommissionView,
|
||||
PermissionNames.CommissionApproveWithdrawal,
|
||||
PermissionNames.PublicMessagesView,
|
||||
PermissionNames.PublicMessagesCreate,
|
||||
PermissionNames.PublicMessagesUpdate,
|
||||
PermissionNames.PublicMessagesPublish,
|
||||
PermissionNames.ManualPaymentsView,
|
||||
PermissionNames.ManualPaymentsCreate,
|
||||
PermissionNames.ReportsView
|
||||
},
|
||||
|
||||
[RoleNames.Inspector] = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
PermissionNames.DashboardView,
|
||||
PermissionNames.OrdersView,
|
||||
PermissionNames.UsersView,
|
||||
PermissionNames.CommissionView,
|
||||
PermissionNames.PublicMessagesView,
|
||||
PermissionNames.ReportsView
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// بررسی اینکه آیا نقش مشخصی مجوز خاصی دارد
|
||||
/// </summary>
|
||||
public static bool HasPermission(string role, string permission)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(role) || string.IsNullOrWhiteSpace(permission))
|
||||
return false;
|
||||
|
||||
if (!RolePermissions.TryGetValue(role, out var permissions))
|
||||
return false;
|
||||
|
||||
// Wildcard: SuperAdmin has full access
|
||||
if (permissions.Contains("*"))
|
||||
return true;
|
||||
|
||||
return permissions.Contains(permission);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace CMSMicroservice.Application.Common.Authorization;
|
||||
|
||||
/// <summary>
|
||||
/// Attribute برای مشخص کردن مجوز لازم برای دسترسی به یک متد gRPC
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
|
||||
public sealed class RequiresPermissionAttribute : Attribute
|
||||
{
|
||||
public RequiresPermissionAttribute(string permission)
|
||||
{
|
||||
Permission = permission ?? throw new ArgumentNullException(nameof(permission));
|
||||
}
|
||||
|
||||
public string Permission { get; }
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
namespace CMSMicroservice.Application.Common.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Service for uploading files to FMS (File Management Service)
|
||||
/// </summary>
|
||||
public interface IFileManagementService
|
||||
{
|
||||
/// <summary>
|
||||
/// Uploads a file to FMS and returns the stored file path
|
||||
/// </summary>
|
||||
/// <param name="directory">Target directory path (e.g. "Images/Products")</param>
|
||||
/// <param name="fileBytes">Raw file bytes</param>
|
||||
/// <param name="mime">MIME type (e.g. "image/jpeg")</param>
|
||||
/// <param name="fileName">Original file name</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>The stored file path returned by FMS, or null if upload failed</returns>
|
||||
Task<string?> UploadFileAsync(string directory, byte[] fileBytes, string mime, string? fileName, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Uploads an image to FMS with optimization (resize + compress)
|
||||
/// Returns both main image path and thumbnail path
|
||||
/// </summary>
|
||||
/// <param name="directory">Target directory path (e.g. "Images/Products")</param>
|
||||
/// <param name="fileBytes">Raw image bytes</param>
|
||||
/// <param name="mime">MIME type (e.g. "image/jpeg")</param>
|
||||
/// <param name="fileName">Original file name</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Tuple of (mainImagePath, thumbnailPath), either can be null if upload failed</returns>
|
||||
Task<(string? MainImagePath, string? ThumbnailPath)> UploadImageWithThumbnailAsync(
|
||||
string directory, byte[] fileBytes, string mime, string? fileName,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a file from FMS by its ID
|
||||
/// </summary>
|
||||
Task<bool> DeleteFileAsync(long fileId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
+1
-1
@@ -57,7 +57,7 @@ public class CreateNewOtpTokenCommandHandler : IRequestHandler<CreateNewOtpToken
|
||||
|
||||
};
|
||||
await _context.OtpTokens.AddAsync(entity, cancellationToken);
|
||||
entity.AddDomainEvent(new CreateNewOtpTokenEvent(entity));
|
||||
entity.AddDomainEvent(new CreateNewOtpTokenEvent(entity, code));
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return new CreateNewOtpTokenResponseDto()
|
||||
{
|
||||
|
||||
+18
-4
@@ -1,3 +1,4 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Events;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -6,16 +7,29 @@ namespace CMSMicroservice.Application.OtpTokenCQ.EventHandlers;
|
||||
public class CreateNewOtpTokenEventHandler : INotificationHandler<CreateNewOtpTokenEvent>
|
||||
{
|
||||
private readonly ILogger<CreateNewOtpTokenEventHandler> _logger;
|
||||
private readonly IKavenegarService _kavenegarService;
|
||||
|
||||
public CreateNewOtpTokenEventHandler(ILogger<CreateNewOtpTokenEventHandler> logger)
|
||||
public CreateNewOtpTokenEventHandler(
|
||||
ILogger<CreateNewOtpTokenEventHandler> logger,
|
||||
IKavenegarService kavenegarService)
|
||||
{
|
||||
_logger = logger;
|
||||
_kavenegarService = kavenegarService;
|
||||
}
|
||||
|
||||
public Task Handle(CreateNewOtpTokenEvent notification, CancellationToken cancellationToken)
|
||||
public async Task Handle(CreateNewOtpTokenEvent notification, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Domain Event: {DomainEvent}", notification.GetType().Name);
|
||||
_logger.LogInformation("Domain Event: {DomainEvent} for mobile {Mobile}",
|
||||
notification.GetType().Name, notification.Item.Mobile);
|
||||
|
||||
return Task.CompletedTask;
|
||||
try
|
||||
{
|
||||
await _kavenegarService.VerifyLookupAsync(notification.Item.Mobile, notification.PlainCode);
|
||||
_logger.LogInformation("OTP SMS sent successfully to {Mobile}", notification.Item.Mobile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to send OTP SMS to {Mobile}", notification.Item.Mobile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.AddProductImage;
|
||||
|
||||
public record AddProductImageCommand : IRequest<AddProductImageResponseDto>
|
||||
{
|
||||
public long ProductId { get; init; }
|
||||
public string Title { get; init; } = string.Empty;
|
||||
public byte[]? ImageFileBytes { get; init; }
|
||||
public string? ImageFileMime { get; init; }
|
||||
public string? ImageFileName { get; init; }
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.AddProductImage;
|
||||
|
||||
public class AddProductImageCommandHandler : IRequestHandler<AddProductImageCommand, AddProductImageResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IFileManagementService _fileManagementService;
|
||||
private readonly ILogger<AddProductImageCommandHandler> _logger;
|
||||
|
||||
public AddProductImageCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
IFileManagementService fileManagementService,
|
||||
ILogger<AddProductImageCommandHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_fileManagementService = fileManagementService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<AddProductImageResponseDto> Handle(AddProductImageCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Verify product exists
|
||||
var productExists = await _context.Products
|
||||
.AnyAsync(p => p.Id == request.ProductId, cancellationToken);
|
||||
if (!productExists)
|
||||
throw new NotFoundException(nameof(Product), request.ProductId);
|
||||
|
||||
string imagePath = string.Empty;
|
||||
string thumbnailPath = string.Empty;
|
||||
|
||||
// Upload image to FMS
|
||||
if (request.ImageFileBytes is { Length: > 0 })
|
||||
{
|
||||
try
|
||||
{
|
||||
var (mainPath, thumbPath) = await _fileManagementService.UploadImageWithThumbnailAsync(
|
||||
"Images/Products/Gallery",
|
||||
request.ImageFileBytes,
|
||||
request.ImageFileMime ?? "image/jpeg",
|
||||
request.ImageFileName,
|
||||
cancellationToken);
|
||||
|
||||
imagePath = mainPath ?? string.Empty;
|
||||
thumbnailPath = thumbPath ?? string.Empty;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to upload gallery image to FMS for product {ProductId}", request.ProductId);
|
||||
}
|
||||
}
|
||||
|
||||
// Create ProductImage entity
|
||||
var productImage = new ProductImage
|
||||
{
|
||||
Title = request.Title,
|
||||
ImagePath = imagePath,
|
||||
ImageThumbnailPath = thumbnailPath
|
||||
};
|
||||
|
||||
await _context.ProductImages.AddAsync(productImage, cancellationToken);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// Create ProductGallery join entity
|
||||
var productGallery = new ProductGallery
|
||||
{
|
||||
ProductId = request.ProductId,
|
||||
ProductImageId = productImage.Id
|
||||
};
|
||||
|
||||
await _context.ProductGalleries.AddAsync(productGallery, cancellationToken);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return new AddProductImageResponseDto
|
||||
{
|
||||
ProductGalleryId = productGallery.Id,
|
||||
ProductImageId = productImage.Id,
|
||||
Title = productImage.Title,
|
||||
ImagePath = productImage.ImagePath,
|
||||
ImageThumbnailPath = productImage.ImageThumbnailPath
|
||||
};
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.AddProductImage;
|
||||
|
||||
public class AddProductImageResponseDto
|
||||
{
|
||||
public long ProductGalleryId { get; set; }
|
||||
public long ProductImageId { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string ImagePath { get; set; } = string.Empty;
|
||||
public string ImageThumbnailPath { get; set; } = string.Empty;
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.CreateNewProducts;
|
||||
|
||||
public record CreateNewProductsCommand : IRequest<CreateNewProductsResponseDto>
|
||||
{
|
||||
public string Title { get; init; } = string.Empty;
|
||||
public string Description { get; init; } = string.Empty;
|
||||
public string ShortInfomation { get; init; } = string.Empty;
|
||||
public string FullInformation { get; init; } = string.Empty;
|
||||
public long Price { get; init; }
|
||||
public int Discount { get; init; }
|
||||
public int Rate { get; init; }
|
||||
public string? ImagePath { get; init; }
|
||||
public string? ThumbnailPath { get; init; }
|
||||
public int SaleCount { get; init; }
|
||||
public int ViewCount { get; init; }
|
||||
public int RemainingCount { get; init; }
|
||||
public List<long> CategoryIds { get; init; } = new();
|
||||
|
||||
// File upload fields (raw bytes from client)
|
||||
public byte[]? ImageFileBytes { get; init; }
|
||||
public string? ImageFileMime { get; init; }
|
||||
public string? ImageFileName { get; init; }
|
||||
public byte[]? ThumbnailFileBytes { get; init; }
|
||||
public string? ThumbnailFileMime { get; init; }
|
||||
public string? ThumbnailFileName { get; init; }
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.CreateNewProducts;
|
||||
|
||||
public class CreateNewProductsCommandHandler : IRequestHandler<CreateNewProductsCommand, CreateNewProductsResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IFileManagementService _fileManagementService;
|
||||
private readonly ILogger<CreateNewProductsCommandHandler> _logger;
|
||||
|
||||
public CreateNewProductsCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
IFileManagementService fileManagementService,
|
||||
ILogger<CreateNewProductsCommandHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_fileManagementService = fileManagementService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<CreateNewProductsResponseDto> Handle(CreateNewProductsCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = new Product
|
||||
{
|
||||
Title = request.Title,
|
||||
Description = request.Description,
|
||||
ShortInfomation = request.ShortInfomation,
|
||||
FullInformation = request.FullInformation,
|
||||
Price = request.Price,
|
||||
Discount = request.Discount,
|
||||
Rate = request.Rate,
|
||||
ImagePath = request.ImagePath ?? string.Empty,
|
||||
ThumbnailPath = request.ThumbnailPath ?? string.Empty,
|
||||
SaleCount = request.SaleCount,
|
||||
ViewCount = request.ViewCount,
|
||||
RemainingCount = request.RemainingCount
|
||||
};
|
||||
|
||||
// Handle image upload to FMS if file bytes provided
|
||||
if (request.ImageFileBytes is { Length: > 0 })
|
||||
{
|
||||
try
|
||||
{
|
||||
var (mainPath, thumbPath) = await _fileManagementService.UploadImageWithThumbnailAsync(
|
||||
"Images/Products",
|
||||
request.ImageFileBytes,
|
||||
request.ImageFileMime ?? "image/jpeg",
|
||||
request.ImageFileName,
|
||||
cancellationToken);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(mainPath))
|
||||
entity.ImagePath = mainPath;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(thumbPath))
|
||||
entity.ThumbnailPath = thumbPath;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to upload product image to FMS");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle separate thumbnail upload if provided (and not already set from main image)
|
||||
if (request.ThumbnailFileBytes is { Length: > 0 } && string.IsNullOrWhiteSpace(entity.ThumbnailPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
var thumbPath = await _fileManagementService.UploadFileAsync(
|
||||
"Images/Products/Thumbnails",
|
||||
request.ThumbnailFileBytes,
|
||||
request.ThumbnailFileMime ?? "image/jpeg",
|
||||
request.ThumbnailFileName,
|
||||
cancellationToken);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(thumbPath))
|
||||
entity.ThumbnailPath = thumbPath;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to upload product thumbnail to FMS");
|
||||
}
|
||||
}
|
||||
|
||||
await _context.Products.AddAsync(entity, cancellationToken);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// Handle category assignments
|
||||
if (request.CategoryIds is { Count: > 0 })
|
||||
{
|
||||
foreach (var categoryId in request.CategoryIds)
|
||||
{
|
||||
var categoryExists = await _context.Categories
|
||||
.AnyAsync(c => c.Id == categoryId, cancellationToken);
|
||||
|
||||
if (categoryExists)
|
||||
{
|
||||
await _context.ProductCategories.AddAsync(new ProductCategory
|
||||
{
|
||||
ProductId = entity.Id,
|
||||
CategoryId = categoryId
|
||||
}, cancellationToken);
|
||||
}
|
||||
}
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return new CreateNewProductsResponseDto { Id = entity.Id };
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.CreateNewProducts;
|
||||
|
||||
public class CreateNewProductsCommandValidator : AbstractValidator<CreateNewProductsCommand>
|
||||
{
|
||||
public CreateNewProductsCommandValidator()
|
||||
{
|
||||
RuleFor(model => model.Title)
|
||||
.NotEmpty().WithMessage("عنوان محصول الزامی است");
|
||||
|
||||
RuleFor(model => model.Price)
|
||||
.GreaterThanOrEqualTo(0).WithMessage("قیمت نمیتواند منفی باشد");
|
||||
|
||||
RuleFor(model => model.Discount)
|
||||
.InclusiveBetween(0, 100).WithMessage("تخفیف باید بین 0 تا 100 باشد");
|
||||
}
|
||||
|
||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||
{
|
||||
var result = await ValidateAsync(
|
||||
ValidationContext<CreateNewProductsCommand>.CreateWithOptions(
|
||||
(CreateNewProductsCommand)model, x => x.IncludeProperties(propertyName)));
|
||||
if (result.IsValid)
|
||||
return Array.Empty<string>();
|
||||
return result.Errors.Select(e => e.ErrorMessage);
|
||||
};
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.CreateNewProducts;
|
||||
|
||||
public class CreateNewProductsResponseDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.DeleteProducts;
|
||||
|
||||
public record DeleteProductsCommand : IRequest<Unit>
|
||||
{
|
||||
public long Id { get; init; }
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
|
||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.DeleteProducts;
|
||||
|
||||
public class DeleteProductsCommandHandler : IRequestHandler<DeleteProductsCommand, Unit>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public DeleteProductsCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(DeleteProductsCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await _context.Products
|
||||
.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken)
|
||||
?? throw new NotFoundException(nameof(Product), request.Id);
|
||||
|
||||
// Remove category associations
|
||||
var productCategories = await _context.ProductCategories
|
||||
.Where(pc => pc.ProductId == entity.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
_context.ProductCategories.RemoveRange(productCategories);
|
||||
|
||||
// Remove gallery associations
|
||||
var productGalleries = await _context.ProductGalleries
|
||||
.Where(pg => pg.ProductId == entity.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
_context.ProductGalleries.RemoveRange(productGalleries);
|
||||
|
||||
_context.Products.Remove(entity);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.RemoveProductImage;
|
||||
|
||||
public record RemoveProductImageCommand : IRequest<Unit>
|
||||
{
|
||||
public long ProductGalleryId { get; init; }
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
|
||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.RemoveProductImage;
|
||||
|
||||
public class RemoveProductImageCommandHandler : IRequestHandler<RemoveProductImageCommand, Unit>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public RemoveProductImageCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(RemoveProductImageCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var gallery = await _context.ProductGalleries
|
||||
.Include(pg => pg.ProductImage)
|
||||
.FirstOrDefaultAsync(pg => pg.Id == request.ProductGalleryId, cancellationToken)
|
||||
?? throw new NotFoundException(nameof(ProductGallery), request.ProductGalleryId);
|
||||
|
||||
// Remove gallery entry
|
||||
_context.ProductGalleries.Remove(gallery);
|
||||
|
||||
// Remove the product image if it exists and is not referenced by other galleries
|
||||
if (gallery.ProductImage != null)
|
||||
{
|
||||
var otherReferences = await _context.ProductGalleries
|
||||
.AnyAsync(pg => pg.ProductImageId == gallery.ProductImageId && pg.Id != gallery.Id, cancellationToken);
|
||||
|
||||
if (!otherReferences)
|
||||
{
|
||||
_context.ProductImages.Remove(gallery.ProductImage);
|
||||
}
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.UpdateProducts;
|
||||
|
||||
public record UpdateProductsCommand : IRequest<Unit>
|
||||
{
|
||||
public long Id { get; init; }
|
||||
public string Title { get; init; } = string.Empty;
|
||||
public string Description { get; init; } = string.Empty;
|
||||
public string ShortInfomation { get; init; } = string.Empty;
|
||||
public string FullInformation { get; init; } = string.Empty;
|
||||
public long Price { get; init; }
|
||||
public int Discount { get; init; }
|
||||
public int Rate { get; init; }
|
||||
public string? ImagePath { get; init; }
|
||||
public string? ThumbnailPath { get; init; }
|
||||
public int SaleCount { get; init; }
|
||||
public int ViewCount { get; init; }
|
||||
public int RemainingCount { get; init; }
|
||||
public List<long> CategoryIds { get; init; } = new();
|
||||
|
||||
// File upload fields (raw bytes from client)
|
||||
public byte[]? ImageFileBytes { get; init; }
|
||||
public string? ImageFileMime { get; init; }
|
||||
public string? ImageFileName { get; init; }
|
||||
public byte[]? ThumbnailFileBytes { get; init; }
|
||||
public string? ThumbnailFileMime { get; init; }
|
||||
public string? ThumbnailFileName { get; init; }
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.UpdateProducts;
|
||||
|
||||
public class UpdateProductsCommandHandler : IRequestHandler<UpdateProductsCommand, Unit>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IFileManagementService _fileManagementService;
|
||||
private readonly ILogger<UpdateProductsCommandHandler> _logger;
|
||||
|
||||
public UpdateProductsCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
IFileManagementService fileManagementService,
|
||||
ILogger<UpdateProductsCommandHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_fileManagementService = fileManagementService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(UpdateProductsCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await _context.Products
|
||||
.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken)
|
||||
?? throw new NotFoundException(nameof(Product), request.Id);
|
||||
|
||||
// Update basic properties
|
||||
entity.Title = request.Title;
|
||||
entity.Description = request.Description;
|
||||
entity.ShortInfomation = request.ShortInfomation;
|
||||
entity.FullInformation = request.FullInformation;
|
||||
entity.Price = request.Price;
|
||||
entity.Discount = request.Discount;
|
||||
entity.Rate = request.Rate;
|
||||
entity.SaleCount = request.SaleCount;
|
||||
entity.ViewCount = request.ViewCount;
|
||||
entity.RemainingCount = request.RemainingCount;
|
||||
|
||||
// Handle image upload to FMS if new file bytes provided
|
||||
if (request.ImageFileBytes is { Length: > 0 })
|
||||
{
|
||||
try
|
||||
{
|
||||
var (mainPath, thumbPath) = await _fileManagementService.UploadImageWithThumbnailAsync(
|
||||
"Images/Products",
|
||||
request.ImageFileBytes,
|
||||
request.ImageFileMime ?? "image/jpeg",
|
||||
request.ImageFileName,
|
||||
cancellationToken);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(mainPath))
|
||||
entity.ImagePath = mainPath;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(thumbPath))
|
||||
entity.ThumbnailPath = thumbPath;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to upload updated product image to FMS for product {ProductId}", request.Id);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If no new file uploaded, keep existing paths or update from request
|
||||
if (!string.IsNullOrWhiteSpace(request.ImagePath))
|
||||
entity.ImagePath = request.ImagePath;
|
||||
if (!string.IsNullOrWhiteSpace(request.ThumbnailPath))
|
||||
entity.ThumbnailPath = request.ThumbnailPath;
|
||||
}
|
||||
|
||||
// Handle separate thumbnail upload if provided
|
||||
if (request.ThumbnailFileBytes is { Length: > 0 })
|
||||
{
|
||||
try
|
||||
{
|
||||
var thumbPath = await _fileManagementService.UploadFileAsync(
|
||||
"Images/Products/Thumbnails",
|
||||
request.ThumbnailFileBytes,
|
||||
request.ThumbnailFileMime ?? "image/jpeg",
|
||||
request.ThumbnailFileName,
|
||||
cancellationToken);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(thumbPath))
|
||||
entity.ThumbnailPath = thumbPath;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to upload updated product thumbnail to FMS for product {ProductId}", request.Id);
|
||||
}
|
||||
}
|
||||
|
||||
_context.Products.Update(entity);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// Update category assignments
|
||||
if (request.CategoryIds != null)
|
||||
{
|
||||
// Remove existing categories
|
||||
var existingCategories = await _context.ProductCategories
|
||||
.Where(pc => pc.ProductId == entity.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
_context.ProductCategories.RemoveRange(existingCategories);
|
||||
|
||||
// Add new categories
|
||||
foreach (var categoryId in request.CategoryIds)
|
||||
{
|
||||
var categoryExists = await _context.Categories
|
||||
.AnyAsync(c => c.Id == categoryId, cancellationToken);
|
||||
|
||||
if (categoryExists)
|
||||
{
|
||||
await _context.ProductCategories.AddAsync(new ProductCategory
|
||||
{
|
||||
ProductId = entity.Id,
|
||||
CategoryId = categoryId
|
||||
}, cancellationToken);
|
||||
}
|
||||
}
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.UpdateProducts;
|
||||
|
||||
public class UpdateProductsCommandValidator : AbstractValidator<UpdateProductsCommand>
|
||||
{
|
||||
public UpdateProductsCommandValidator()
|
||||
{
|
||||
RuleFor(model => model.Id)
|
||||
.NotNull().WithMessage("شناسه محصول الزامی است");
|
||||
|
||||
RuleFor(model => model.Title)
|
||||
.NotEmpty().WithMessage("عنوان محصول الزامی است");
|
||||
|
||||
RuleFor(model => model.Price)
|
||||
.GreaterThanOrEqualTo(0).WithMessage("قیمت نمیتواند منفی باشد");
|
||||
|
||||
RuleFor(model => model.Discount)
|
||||
.InclusiveBetween(0, 100).WithMessage("تخفیف باید بین 0 تا 100 باشد");
|
||||
}
|
||||
|
||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||
{
|
||||
var result = await ValidateAsync(
|
||||
ValidationContext<UpdateProductsCommand>.CreateWithOptions(
|
||||
(UpdateProductsCommand)model, x => x.IncludeProperties(propertyName)));
|
||||
if (result.IsValid)
|
||||
return Array.Empty<string>();
|
||||
return result.Errors.Select(e => e.ErrorMessage);
|
||||
};
|
||||
}
|
||||
+1
-1
@@ -24,7 +24,7 @@ public class GetCustomerProductsQueryHandler : IRequestHandler<GetCustomerProduc
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (product == null)
|
||||
throw new NotFoundException(nameof(Products), request.Id);
|
||||
throw new NotFoundException(nameof(Product), request.Id);
|
||||
|
||||
var response = new GetCustomerProductsResponseDto
|
||||
{
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
namespace CMSMicroservice.Application.ProductsCQ.Queries.GetProductGallery;
|
||||
|
||||
public class GetProductGalleryQuery : IRequest<GetProductGalleryResponseDto>
|
||||
{
|
||||
public long ProductId { get; set; }
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
|
||||
namespace CMSMicroservice.Application.ProductsCQ.Queries.GetProductGallery;
|
||||
|
||||
public class GetProductGalleryQueryHandler : IRequestHandler<GetProductGalleryQuery, GetProductGalleryResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public GetProductGalleryQueryHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<GetProductGalleryResponseDto> Handle(GetProductGalleryQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var galleries = await _context.ProductGalleries
|
||||
.AsNoTracking()
|
||||
.Where(pg => pg.ProductId == request.ProductId)
|
||||
.Include(pg => pg.ProductImage)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return new GetProductGalleryResponseDto
|
||||
{
|
||||
Items = galleries.Select(pg => new ProductGalleryItemDto
|
||||
{
|
||||
ProductGalleryId = pg.Id,
|
||||
ProductImageId = pg.ProductImageId,
|
||||
Title = pg.ProductImage?.Title ?? string.Empty,
|
||||
ImagePath = pg.ProductImage?.ImagePath ?? string.Empty,
|
||||
ImageThumbnailPath = pg.ProductImage?.ImageThumbnailPath ?? string.Empty
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
namespace CMSMicroservice.Application.ProductsCQ.Queries.GetProductGallery;
|
||||
|
||||
public class GetProductGalleryResponseDto
|
||||
{
|
||||
public List<ProductGalleryItemDto> Items { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ProductGalleryItemDto
|
||||
{
|
||||
public long ProductGalleryId { get; set; }
|
||||
public long ProductImageId { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string ImagePath { get; set; } = string.Empty;
|
||||
public string ImageThumbnailPath { get; set; } = string.Empty;
|
||||
}
|
||||
+22
-6
@@ -1,5 +1,6 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
|
||||
namespace CMSMicroservice.Application.UserCQ.Commands.AcceptContract;
|
||||
@@ -7,25 +8,38 @@ public class AcceptContractCommandHandler : IRequestHandler<AcceptContractComman
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
private readonly IHashService _hashService;
|
||||
private readonly IGenerateJwtToken _generateJwt;
|
||||
private readonly IConfiguration _cfg;
|
||||
|
||||
public AcceptContractCommandHandler(IApplicationDbContext context, ICurrentUserService currentUserService)
|
||||
public AcceptContractCommandHandler(IApplicationDbContext context, ICurrentUserService currentUserService,
|
||||
IHashService hashService, IGenerateJwtToken generateJwt, IConfiguration cfg)
|
||||
{
|
||||
_context = context;
|
||||
_currentUserService = currentUserService;
|
||||
_hashService = hashService;
|
||||
_generateJwt = generateJwt;
|
||||
_cfg = cfg;
|
||||
}
|
||||
|
||||
public async Task<AcceptContractResponseDto> Handle(AcceptContractCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Verify OTP first
|
||||
var otpToken = await _context.OtpTokens
|
||||
.Where(x => x.Mobile == _currentUserService.Username && x.Purpose == "signContract" && !x.IsUsed && x.Code == request.Code)
|
||||
.OrderByDescending(x => x.Id) // Use Id instead of CreatedAt for now
|
||||
.Where(x => x.Mobile == _currentUserService.Username && x.Purpose == "signContract" && !x.IsUsed)
|
||||
.OrderByDescending(x => x.Id)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (otpToken == null || !otpToken.IsValid(request.Code))
|
||||
var secret = _cfg["Otp:Secret"] ?? throw new InvalidOperationException("Otp:Secret not set");
|
||||
if (otpToken == null || !otpToken.IsValid() || !_hashService.VerifyHmacSha256Hex(request.Code, otpToken.CodeHash, secret))
|
||||
return new AcceptContractResponseDto { IsSuccess = false, Message = "کد تایید نامعتبر است" };
|
||||
|
||||
var user = await _context.Users
|
||||
.Include(u => u.UserContracts)
|
||||
.ThenInclude(uc => uc.Contract)
|
||||
.Include(u => u.UserRoles)
|
||||
.ThenInclude(ur => ur.Role)
|
||||
.Include(u => u.ClubMembership)
|
||||
.Where(x => x.Mobile == _currentUserService.Username)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
@@ -48,12 +62,14 @@ public class AcceptContractCommandHandler : IRequestHandler<AcceptContractComman
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// TODO: Implement JWT token generation
|
||||
// Generate JWT token with updated contract status
|
||||
var token = await _generateJwt.GenerateJwtToken(user);
|
||||
|
||||
return new AcceptContractResponseDto
|
||||
{
|
||||
IsSuccess = true,
|
||||
Message = "قرارداد با موفقیت تایید شد",
|
||||
Token = "TODO_IMPLEMENT_JWT_GENERATION"
|
||||
Token = token
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+18
-3
@@ -1,26 +1,41 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace CMSMicroservice.Application.UserCQ.Commands.VerifyOtpToken;
|
||||
public class VerifyOtpTokenCommandHandler : IRequestHandler<VerifyOtpTokenCommand, VerifyOtpTokenResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IGenerateJwtToken _generateJwt;
|
||||
private readonly IHashService _hashService;
|
||||
private readonly IConfiguration _cfg;
|
||||
|
||||
public VerifyOtpTokenCommandHandler(IApplicationDbContext context, IGenerateJwtToken generateJwt)
|
||||
public VerifyOtpTokenCommandHandler(IApplicationDbContext context, IGenerateJwtToken generateJwt,
|
||||
IHashService hashService, IConfiguration cfg)
|
||||
{
|
||||
_context = context;
|
||||
_generateJwt = generateJwt;
|
||||
_hashService = hashService;
|
||||
_cfg = cfg;
|
||||
}
|
||||
|
||||
public async Task<VerifyOtpTokenResponseDto> Handle(VerifyOtpTokenCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var otpToken = await _context.OtpTokens
|
||||
.Where(x => x.Mobile == request.Mobile && x.Purpose == request.Purpose && !x.IsUsed)
|
||||
.OrderByDescending(x => x.Id) // Use Id instead of CreatedAt for now
|
||||
.OrderByDescending(x => x.Id)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (otpToken == null || !otpToken.IsValid(request.Code))
|
||||
if (otpToken == null)
|
||||
return new VerifyOtpTokenResponseDto { Success = false, Message = "کد تایید نامعتبر است" };
|
||||
|
||||
// Check expiry and usage
|
||||
if (otpToken.IsUsed || DateTime.Now > otpToken.ExpiresAt)
|
||||
return new VerifyOtpTokenResponseDto { Success = false, Message = "کد تایید منقضی شده است" };
|
||||
|
||||
// Verify using the same HMAC-SHA256 method used during creation
|
||||
var secret = _cfg["Otp:Secret"] ?? throw new InvalidOperationException("Otp:Secret not set");
|
||||
if (!_hashService.VerifyHmacSha256Hex(request.Code, otpToken.CodeHash, secret))
|
||||
return new VerifyOtpTokenResponseDto { Success = false, Message = "کد تایید نامعتبر است" };
|
||||
|
||||
var user = await _context.Users
|
||||
|
||||
+10
@@ -22,6 +22,16 @@ public class GetCustomerWalletChangeLogResponseDto
|
||||
/// </summary>
|
||||
public long ChangeNerworkValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// موجودی جاری تخفیف
|
||||
/// </summary>
|
||||
public long CurrentDiscountBalance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// مقدار تغییر تخفیف
|
||||
/// </summary>
|
||||
public long ChangeDiscountValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// افزایشی است؟
|
||||
/// </summary>
|
||||
|
||||
@@ -18,14 +18,11 @@ public class OtpToken : BaseAuditableEntity
|
||||
public bool IsUsed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Validates if the provided code is correct and token is not expired
|
||||
/// Checks if token is still valid (not used and not expired).
|
||||
/// Code verification should be done in the handler using IHashService.
|
||||
/// </summary>
|
||||
public bool IsValid(string providedCode)
|
||||
public bool IsValid()
|
||||
{
|
||||
if (IsUsed || DateTime.UtcNow > ExpiresAt)
|
||||
return false;
|
||||
|
||||
// Use BCrypt to verify the provided code against the stored hash
|
||||
return BCrypt.Net.BCrypt.Verify(providedCode, CodeHash);
|
||||
return !IsUsed && DateTime.Now > ExpiresAt == false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
namespace CMSMicroservice.Domain.Entities;
|
||||
//گالری تصاویر محصول
|
||||
public class ProductGalleries : BaseAuditableEntity
|
||||
{
|
||||
public long ProductImageId { get; set; }
|
||||
//ProductImage Navigation Property
|
||||
public virtual ProductImage ProductImage { get; set; }
|
||||
public long ProductId { get; set; }
|
||||
//Product Navigation Property
|
||||
public virtual Products Product { get; set; }
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace CMSMicroservice.Domain.Entities;
|
||||
//توکن Otp
|
||||
public class ProductImages : BaseAuditableEntity
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string ImagePath { get; set; }
|
||||
public string ImageThumbnailPath { get; set; }
|
||||
//ProductGalleries Collection Navigation Reference
|
||||
public virtual ICollection<ProductGalleries> ProductGalleries { get; set; }
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
namespace CMSMicroservice.Domain.Entities;
|
||||
//توکن Otp
|
||||
public class Products : BaseAuditableEntity
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string ShortInfomation { get; set; }
|
||||
public string FullInformation { get; set; }
|
||||
public long Price { get; set; }
|
||||
public int Discount { get; set; }
|
||||
public int Rate { get; set; }
|
||||
public string ImagePath { get; set; }
|
||||
public string ThumbnailPath { get; set; }
|
||||
public int SaleCount { get; set; }
|
||||
public int ViewCount { get; set; }
|
||||
public int RemainingCount { get; set; }
|
||||
|
||||
// ============= Club Shop Fields =============
|
||||
|
||||
/// <summary>
|
||||
/// آیا این محصول فقط در فروشگاه باشگاه موجود است
|
||||
/// </summary>
|
||||
public bool IsClubExclusive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// درصد تخفیف باشگاه (0 تا 100)
|
||||
/// </summary>
|
||||
public int ClubDiscountPercent { get; set; }
|
||||
|
||||
// ============= Navigation Properties =============
|
||||
|
||||
//UserCarts Collection Navigation Reference
|
||||
public virtual ICollection<UserCart> UserCarts { get; set; }
|
||||
//ProductGalleries Collection Navigation Reference
|
||||
public virtual ICollection<ProductGallery> ProductGalleries { get; set; }
|
||||
//FactorDetails Collection Navigation Reference
|
||||
public virtual ICollection<FactorDetails> FactorDetails { get; set; }
|
||||
//ProductCategory Collection Navigation Reference
|
||||
public virtual ICollection<ProductCategory> ProductCategories { get; set; }
|
||||
//ProductTag Collection Navigation Reference
|
||||
public virtual ICollection<ProductTag> ProductTags { get; set; }
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
namespace CMSMicroservice.Domain.Events;
|
||||
public class CreateNewOtpTokenEvent : BaseEvent
|
||||
{
|
||||
public CreateNewOtpTokenEvent(OtpToken item)
|
||||
public CreateNewOtpTokenEvent(OtpToken item, string plainCode)
|
||||
{
|
||||
Item = item;
|
||||
PlainCode = plainCode;
|
||||
}
|
||||
|
||||
public OtpToken Item { get; }
|
||||
/// <summary>
|
||||
/// کد OTP به صورت plain text برای ارسال SMS
|
||||
/// </summary>
|
||||
public string PlainCode { get; }
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Grpc.Net.Client" Version="2.54.0" />
|
||||
<PackageReference Include="Kavenegar" Version="1.2.5" />
|
||||
<PackageReference Include="MailKit" Version="4.14.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.11" />
|
||||
@@ -18,10 +19,12 @@
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.11" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.11" />
|
||||
<PackageReference Include="Polly" Version="8.5.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CMSMicroservice.Application\CMSMicroservice.Application.csproj" />
|
||||
<ProjectReference Include="..\CMSMicroservice.Protobuf\CMSMicroservice.Protobuf.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Application.Common.Authorization;
|
||||
using CMSMicroservice.Application.DayaLoanCQ.Services;
|
||||
using CMSMicroservice.Infrastructure.Persistence;
|
||||
using CMSMicroservice.Infrastructure.Persistence.Interceptors;
|
||||
using CMSMicroservice.Infrastructure.BackgroundJobs;
|
||||
using CMSMicroservice.Infrastructure.Services.Monitoring;
|
||||
using CMSMicroservice.Infrastructure.Services.Authorization;
|
||||
using CMSMicroservice.Infrastructure.Configuration;
|
||||
using CMSMicroservice.Infrastructure.Services.Payment;
|
||||
using CMSMicroservice.Infrastructure.Services.Commission;
|
||||
@@ -35,6 +37,8 @@ public static class ConfigureServices
|
||||
services.AddScoped<IAlertService, AlertService>();
|
||||
services.AddScoped<IUserNotificationService, UserNotificationService>();
|
||||
services.AddScoped<IKavenegarService, KavenegarService>();
|
||||
services.AddScoped<IFileManagementService, FileManagementService>();
|
||||
services.AddScoped<IPermissionService, PermissionService>();
|
||||
|
||||
// Daya Loan API Service - قابل تغییر بین Mock و Real
|
||||
var useMockDayaApi = configuration.GetValue<bool>("DayaApi:UseMock", false);
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using CMSMicroservice.Application.Common.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Services.Authorization;
|
||||
|
||||
/// <summary>
|
||||
/// پیادهسازی سرویس مجوز — نقشها از JWT Claims خوانده میشن
|
||||
/// </summary>
|
||||
public class PermissionService : IPermissionService
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
public PermissionService(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<string>> GetUserRolesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var user = _httpContextAccessor.HttpContext?.User;
|
||||
if (user?.Identity is not { IsAuthenticated: true })
|
||||
return Task.FromResult<IReadOnlyList<string>>(Array.Empty<string>());
|
||||
|
||||
var roles = user.Claims
|
||||
.Where(c => c.Type == ClaimTypes.Role
|
||||
|| string.Equals(c.Type, "role", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(c => c.Value)
|
||||
.Where(v => !string.IsNullOrWhiteSpace(v))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<string>>(roles);
|
||||
}
|
||||
|
||||
public async Task<bool> HasPermissionAsync(string permission, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(permission)) return true;
|
||||
|
||||
var roles = await GetUserRolesAsync(cancellationToken);
|
||||
if (roles.Count == 0) return false;
|
||||
|
||||
foreach (var role in roles)
|
||||
{
|
||||
if (RolePermissionConfig.HasPermission(role, permission))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Protobuf.Protos.FMS;
|
||||
using Google.Protobuf;
|
||||
using Grpc.Net.Client;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using System.IO;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Services;
|
||||
|
||||
public class FileManagementService : IFileManagementService, IDisposable
|
||||
{
|
||||
private readonly ILogger<FileManagementService> _logger;
|
||||
private readonly FileInfoContract.FileInfoContractClient _client;
|
||||
private readonly GrpcChannel _channel;
|
||||
|
||||
private const int MainImageMaxWidth = 1200;
|
||||
private const int MainImageMaxHeight = 1200;
|
||||
private const int ThumbnailMaxWidth = 300;
|
||||
private const int ThumbnailMaxHeight = 300;
|
||||
private const int JpegQuality = 75;
|
||||
|
||||
public FileManagementService(IConfiguration configuration, ILogger<FileManagementService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
var fmsAddress = configuration["FMS:Address"] ?? "https://dl.afrino.co";
|
||||
|
||||
_channel = GrpcChannel.ForAddress(fmsAddress, new GrpcChannelOptions
|
||||
{
|
||||
MaxReceiveMessageSize = 100 * 1024 * 1024, // 100 MB
|
||||
MaxSendMessageSize = 100 * 1024 * 1024
|
||||
});
|
||||
|
||||
_client = new FileInfoContract.FileInfoContractClient(_channel);
|
||||
}
|
||||
|
||||
public async Task<string?> UploadFileAsync(string directory, byte[] fileBytes, string mime, string? fileName,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new CreateNewFileInfoRequest
|
||||
{
|
||||
Directory = directory,
|
||||
File = ByteString.CopyFrom(fileBytes),
|
||||
Mime = mime,
|
||||
IsBase64 = false
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(fileName))
|
||||
request.FileName = fileName;
|
||||
|
||||
var response = await _client.CreateNewFileInfoAsync(request, cancellationToken: cancellationToken);
|
||||
|
||||
if (response != null && !string.IsNullOrWhiteSpace(response.File))
|
||||
{
|
||||
_logger.LogInformation("File uploaded to FMS successfully. Id: {Id}, Path: {Path}", response.Id, response.File);
|
||||
return response.File;
|
||||
}
|
||||
|
||||
_logger.LogWarning("FMS upload returned null or empty path for file: {FileName}", fileName);
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error uploading file to FMS. Directory: {Directory}, FileName: {FileName}", directory, fileName);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(string? MainImagePath, string? ThumbnailPath)> UploadImageWithThumbnailAsync(
|
||||
string directory, byte[] fileBytes, string mime, string? fileName,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
string? mainImagePath = null;
|
||||
string? thumbnailPath = null;
|
||||
|
||||
try
|
||||
{
|
||||
// Optimize main image
|
||||
var mainImageBytes = await OptimizeImageAsync(fileBytes, MainImageMaxWidth, MainImageMaxHeight);
|
||||
mainImagePath = await UploadFileAsync(directory, mainImageBytes, "image/jpeg", fileName, cancellationToken);
|
||||
|
||||
// Create and upload thumbnail
|
||||
var thumbnailBytes = await OptimizeImageAsync(fileBytes, ThumbnailMaxWidth, ThumbnailMaxHeight);
|
||||
var thumbFileName = fileName != null ? $"thumb_{fileName}" : null;
|
||||
thumbnailPath = await UploadFileAsync($"{directory}/Thumbnails", thumbnailBytes, "image/jpeg", thumbFileName, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing and uploading image with thumbnail. Directory: {Directory}", directory);
|
||||
}
|
||||
|
||||
return (mainImagePath, thumbnailPath);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteFileAsync(long fileId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new DeleteFileInfoRequest { Id = fileId };
|
||||
var response = await _client.DeleteFileInfoAsync(request, cancellationToken: cancellationToken);
|
||||
return response?.Success ?? false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting file from FMS. FileId: {FileId}", fileId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<byte[]> OptimizeImageAsync(byte[] imageBytes, int maxWidth, int maxHeight)
|
||||
{
|
||||
using var image = Image.Load(imageBytes);
|
||||
|
||||
// Only resize if larger than max dimensions
|
||||
if (image.Width > maxWidth || image.Height > maxHeight)
|
||||
{
|
||||
image.Mutate(x => x.Resize(new ResizeOptions
|
||||
{
|
||||
Size = new Size(maxWidth, maxHeight),
|
||||
Mode = ResizeMode.Max
|
||||
}));
|
||||
}
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
await image.SaveAsJpegAsync(ms, new JpegEncoder { Quality = JpegQuality });
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_channel?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,8 @@
|
||||
<Protobuf Include="Protos\appversion.proto" ProtoRoot="Protos\" GrpcServices="Both" />
|
||||
<!-- Inventory Management System -->
|
||||
<Protobuf Include="Protos\inventory.proto" ProtoRoot="Protos\" GrpcServices="Both" />
|
||||
<!-- FMS (File Management Service) - gRPC Client only -->
|
||||
<Protobuf Include="Protos\fms.proto" ProtoRoot="Protos\" GrpcServices="Client" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PushToFoursatNuget" AfterTargets="Pack" Condition="'$(CI)' != 'true'">
|
||||
|
||||
@@ -87,6 +87,8 @@ message CreateDiscountProductRequest
|
||||
int32 sort_order = 9;
|
||||
bool is_active = 10;
|
||||
repeated int64 category_ids = 11;
|
||||
ImageFileModel image_file = 12;
|
||||
ImageFileModel thumbnail_file = 13;
|
||||
}
|
||||
|
||||
message CreateDiscountProductResponse
|
||||
@@ -108,6 +110,8 @@ message UpdateDiscountProductRequest
|
||||
int32 sort_order = 9;
|
||||
bool is_active = 10;
|
||||
repeated int64 category_ids = 11;
|
||||
ImageFileModel image_file = 12;
|
||||
ImageFileModel thumbnail_file = 13;
|
||||
}
|
||||
|
||||
// Delete Product
|
||||
@@ -246,3 +250,11 @@ message DiscountProductImageDto
|
||||
int32 sort_order = 7;
|
||||
bool is_active = 8;
|
||||
}
|
||||
|
||||
// File upload model for binary image uploads from BackOffice
|
||||
message ImageFileModel
|
||||
{
|
||||
bytes file = 1;
|
||||
string mime = 2;
|
||||
string file_name = 3;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package fms;
|
||||
|
||||
import "google/protobuf/wrappers.proto";
|
||||
|
||||
option csharp_namespace = "CMSMicroservice.Protobuf.Protos.FMS";
|
||||
|
||||
service FileInfoContract
|
||||
{
|
||||
rpc CreateNewFileInfo(CreateNewFileInfoRequest) returns (CreateNewFileInfoResponse);
|
||||
rpc DeleteFileInfo(DeleteFileInfoRequest) returns (DeleteFileInfoResponse);
|
||||
}
|
||||
|
||||
message CreateNewFileInfoRequest
|
||||
{
|
||||
string directory = 1;
|
||||
bytes file = 2;
|
||||
string mime = 3;
|
||||
bool is_base64 = 4;
|
||||
google.protobuf.StringValue file_name = 5;
|
||||
}
|
||||
|
||||
message CreateNewFileInfoResponse
|
||||
{
|
||||
int64 id = 1;
|
||||
string file = 2;
|
||||
}
|
||||
|
||||
message DeleteFileInfoRequest
|
||||
{
|
||||
int64 id = 1;
|
||||
}
|
||||
|
||||
message DeleteFileInfoResponse
|
||||
{
|
||||
bool success = 1;
|
||||
}
|
||||
@@ -270,6 +270,8 @@ message InventoryItemDto {
|
||||
string product_title = 15;
|
||||
int64 product_price = 16;
|
||||
google.protobuf.Timestamp created = 17;
|
||||
// آیا موجودی کم است (موجودی قابل فروش کمتر از حد هشدار)
|
||||
bool is_low_stock = 18;
|
||||
}
|
||||
|
||||
message GetInventoryItemRequest {
|
||||
|
||||
@@ -78,6 +78,7 @@ message CreateManualPaymentRequest
|
||||
google.protobuf.StringValue reference_number = 5;
|
||||
google.protobuf.StringValue image_path = 6;
|
||||
google.protobuf.Int64Value image_document_id = 7;
|
||||
FileUploadModel image_file = 8;
|
||||
}
|
||||
|
||||
message CreateManualPaymentResponse
|
||||
@@ -155,3 +156,11 @@ message ProcessManualMembershipPaymentResponse
|
||||
string message = 4;
|
||||
}
|
||||
|
||||
// File upload model for binary image uploads from BackOffice
|
||||
message FileUploadModel
|
||||
{
|
||||
bytes file = 1;
|
||||
string file_name = 2;
|
||||
string mime = 3;
|
||||
}
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@ message CreateNewPackageRequest
|
||||
string description = 2;
|
||||
string image_path = 3;
|
||||
int64 price = 4;
|
||||
BoostCardFileModel image_file = 5;
|
||||
}
|
||||
message CreateNewPackageResponse
|
||||
{
|
||||
@@ -125,6 +126,7 @@ message UpdatePackageRequest
|
||||
string description = 3;
|
||||
string image_path = 4;
|
||||
int64 price = 5;
|
||||
BoostCardFileModel image_file = 6;
|
||||
}
|
||||
message DeletePackageRequest
|
||||
{
|
||||
@@ -412,3 +414,11 @@ enum PaymentStatusEnum
|
||||
PAYMENT_STATUS_FAILED = 2;
|
||||
PAYMENT_STATUS_REFUNDED = 3;
|
||||
}
|
||||
|
||||
// File upload model for binary image uploads from BackOffice
|
||||
message BoostCardFileModel
|
||||
{
|
||||
bytes file = 1;
|
||||
string file_name = 2;
|
||||
string mime = 3;
|
||||
}
|
||||
|
||||
@@ -79,6 +79,50 @@ service ProductsContract
|
||||
get: "/Customer/GetProducts"
|
||||
};
|
||||
};
|
||||
|
||||
// ============= Category-Product DragDrop Methods =============
|
||||
|
||||
rpc GetProductsForCategory(GetProductsForCategoryRequest) returns (GetProductsForCategoryResponse){
|
||||
option (google.api.http) = {
|
||||
get: "/GetProductsForCategory"
|
||||
};
|
||||
};
|
||||
rpc GetCategories(GetCategoriesRequest) returns (GetCategoriesResponse){
|
||||
option (google.api.http) = {
|
||||
get: "/GetCategories"
|
||||
};
|
||||
};
|
||||
rpc UpdateProductCategories(UpdateProductCategoriesRequest) returns (google.protobuf.Empty){
|
||||
option (google.api.http) = {
|
||||
post: "/UpdateProductCategories"
|
||||
body: "*"
|
||||
};
|
||||
};
|
||||
rpc UpdateCategoryProducts(UpdateCategoryProductsRequest) returns (google.protobuf.Empty){
|
||||
option (google.api.http) = {
|
||||
post: "/UpdateCategoryProducts"
|
||||
body: "*"
|
||||
};
|
||||
};
|
||||
|
||||
// ============= Product Image Management =============
|
||||
|
||||
rpc AddProductImage(AddProductImageRequest) returns (AddProductImageResponse){
|
||||
option (google.api.http) = {
|
||||
post: "/AddProductImage"
|
||||
body: "*"
|
||||
};
|
||||
};
|
||||
rpc RemoveProductImage(RemoveProductImageRequest) returns (google.protobuf.Empty){
|
||||
option (google.api.http) = {
|
||||
delete: "/RemoveProductImage"
|
||||
};
|
||||
};
|
||||
rpc GetProductGallery(GetProductGalleryRequest) returns (GetProductGalleryResponse){
|
||||
option (google.api.http) = {
|
||||
get: "/GetProductGallery"
|
||||
};
|
||||
};
|
||||
}
|
||||
message CreateNewProductsRequest
|
||||
{
|
||||
@@ -96,6 +140,10 @@ message CreateNewProductsRequest
|
||||
int32 remaining_count = 12;
|
||||
// لیست شناسه دستهبندیهای محصول
|
||||
repeated int64 category_ids = 13;
|
||||
// فایل تصویر اصلی محصول (آپلود باینری)
|
||||
ImageFileModel image_file = 14;
|
||||
// فایل تصویر بندانگشتی محصول (آپلود باینری)
|
||||
ImageFileModel thumbnail_file = 15;
|
||||
}
|
||||
message CreateNewProductsResponse
|
||||
{
|
||||
@@ -118,6 +166,10 @@ message UpdateProductsRequest
|
||||
int32 remaining_count = 13;
|
||||
// لیست شناسه دستهبندیهای محصول
|
||||
repeated int64 category_ids = 14;
|
||||
// فایل تصویر اصلی محصول (آپلود باینری)
|
||||
ImageFileModel image_file = 15;
|
||||
// فایل تصویر بندانگشتی محصول (آپلود باینری)
|
||||
ImageFileModel thumbnail_file = 16;
|
||||
}
|
||||
message DeleteProductsRequest
|
||||
{
|
||||
@@ -334,3 +386,97 @@ message ToggleProductStatusResponse
|
||||
int32 failed = 3;
|
||||
repeated BulkOperationError errors = 4;
|
||||
}
|
||||
|
||||
// Category Product Item (for drag-drop UI)
|
||||
message CategoryProductItem
|
||||
{
|
||||
int64 id = 1;
|
||||
string title = 2;
|
||||
bool selected = 3;
|
||||
}
|
||||
|
||||
// Get Products for Category
|
||||
message GetProductsForCategoryRequest
|
||||
{
|
||||
int64 category_id = 1;
|
||||
}
|
||||
|
||||
message GetProductsForCategoryResponse
|
||||
{
|
||||
repeated CategoryProductItem items = 1;
|
||||
}
|
||||
|
||||
// Category Item (for product categories drag-drop)
|
||||
message CategoryItem
|
||||
{
|
||||
int64 id = 1;
|
||||
string title = 2;
|
||||
bool selected = 3;
|
||||
}
|
||||
|
||||
// Get Categories for Product
|
||||
message GetCategoriesRequest
|
||||
{
|
||||
int64 product_id = 1;
|
||||
}
|
||||
|
||||
message GetCategoriesResponse
|
||||
{
|
||||
repeated CategoryItem items = 1;
|
||||
}
|
||||
|
||||
// Update Product Categories
|
||||
message UpdateProductCategoriesRequest
|
||||
{
|
||||
int64 product_id = 1;
|
||||
repeated int64 category_ids = 2;
|
||||
}
|
||||
|
||||
// Update Category Products
|
||||
message UpdateCategoryProductsRequest
|
||||
{
|
||||
int64 category_id = 1;
|
||||
repeated int64 product_ids = 2;
|
||||
}
|
||||
|
||||
// Image File Model
|
||||
message ImageFileModel
|
||||
{
|
||||
bytes file = 1;
|
||||
string mime = 2;
|
||||
string file_name = 3;
|
||||
}
|
||||
|
||||
// Get Product Gallery
|
||||
message GetProductGalleryRequest
|
||||
{
|
||||
int64 product_id = 1;
|
||||
}
|
||||
|
||||
message GetProductGalleryResponse
|
||||
{
|
||||
repeated ProductGalleryItem items = 1;
|
||||
}
|
||||
|
||||
// Add Product Image
|
||||
message AddProductImageRequest
|
||||
{
|
||||
int64 product_id = 1;
|
||||
string title = 2;
|
||||
ImageFileModel image_file = 3;
|
||||
}
|
||||
|
||||
message AddProductImageResponse
|
||||
{
|
||||
int64 product_gallery_id = 1;
|
||||
int64 product_image_id = 2;
|
||||
string title = 3;
|
||||
string image_path = 4;
|
||||
string image_thumbnail_path = 5;
|
||||
}
|
||||
|
||||
// Remove Product Image
|
||||
message RemoveProductImageRequest
|
||||
{
|
||||
int64 product_gallery_id = 1;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using FluentValidation;
|
||||
using CMSMicroservice.Protobuf.Protos.User;
|
||||
namespace CMSMicroservice.Protobuf.Validator.User;
|
||||
|
||||
public class VerifyOtpTokenRequestValidator : AbstractValidator<VerifyOtpTokenRequest>
|
||||
{
|
||||
public VerifyOtpTokenRequestValidator()
|
||||
{
|
||||
RuleFor(model => model.Mobile)
|
||||
.NotEmpty();
|
||||
RuleFor(model => model.Purpose)
|
||||
.NotEmpty();
|
||||
RuleFor(model => model.Code)
|
||||
.NotEmpty();
|
||||
}
|
||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||
{
|
||||
var result = await ValidateAsync(ValidationContext<VerifyOtpTokenRequest>.CreateWithOptions((VerifyOtpTokenRequest)model, x => x.IncludeProperties(propertyName)));
|
||||
if (result.IsValid)
|
||||
return Array.Empty<string>();
|
||||
return result.Errors.Select(e => e.ErrorMessage);
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetAllClubMemberships;
|
||||
using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubMembership;
|
||||
using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubStatistics;
|
||||
using CMSMicroservice.Protobuf.Protos.ClubMembership;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
@@ -11,6 +12,37 @@ public class ClubMembershipProfile : IRegister
|
||||
{
|
||||
public void Register(TypeAdapterConfig config)
|
||||
{
|
||||
// =====================
|
||||
// GetClubMembership
|
||||
// =====================
|
||||
|
||||
// GetClubMembershipRequest -> GetClubMembershipQuery
|
||||
config.NewConfig<GetClubMembershipRequest, GetClubMembershipQuery>()
|
||||
.MapWith(src => new GetClubMembershipQuery
|
||||
{
|
||||
UserId = src.UserId
|
||||
});
|
||||
|
||||
// ClubMembershipDto -> GetClubMembershipResponse
|
||||
config.NewConfig<ClubMembershipDto, GetClubMembershipResponse>()
|
||||
.MapWith(src => new GetClubMembershipResponse
|
||||
{
|
||||
Id = src.Id,
|
||||
UserId = src.UserId,
|
||||
PackageId = 0, // Not available
|
||||
PackageName = string.Empty, // Not available
|
||||
ActivationCode = string.Empty, // Not available
|
||||
ActivatedAt = src.ActivatedAt.HasValue
|
||||
? Timestamp.FromDateTime(DateTime.SpecifyKind(src.ActivatedAt.Value, DateTimeKind.Utc))
|
||||
: null,
|
||||
ExpiresAt = null, // Not available
|
||||
IsActive = src.IsActive,
|
||||
Created = Timestamp.FromDateTimeOffset(src.Created),
|
||||
Features = { }, // Empty for now
|
||||
Status = src.IsActive ? "Active" : "Inactive",
|
||||
DaysRemaining = 0 // Calculate if ExpiresAt is available
|
||||
});
|
||||
|
||||
// =====================
|
||||
// GetAllClubMemberships
|
||||
// =====================
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
using System.Linq;
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetUserWeeklyBalances;
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetMyWeeklyBalances;
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetAvailableWeeks;
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetUserCommissionPayouts;
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetWeekDefinitions;
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using CMSMicroservice.Protobuf.Protos.Commission;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Mapster;
|
||||
@@ -124,5 +128,77 @@ public class CommissionProfile : IRegister
|
||||
: null)
|
||||
.Map(dest => dest.IsExpired, src => src.IsExpired)
|
||||
.Map(dest => dest.Created, src => Timestamp.FromDateTimeOffset(src.Created));
|
||||
|
||||
// GetMyCommissionPayouts Request Mapping
|
||||
config.NewConfig<GetMyCommissionPayoutsRequest, GetMyCommissionPayoutsQuery>()
|
||||
.Map(dest => dest.Status, src => src.Status != null && src.Status.Value >= 0
|
||||
? (CommissionPayoutStatus?)src.Status.Value
|
||||
: null)
|
||||
.Map(dest => dest.WeekDefinitionId, src => src.WeekDefinitionId != null ? src.WeekDefinitionId.Value : (long?)null)
|
||||
.Map(dest => dest.PaginationState, src => new PaginationState
|
||||
{
|
||||
PageNumber = src.PageNumber > 0 ? src.PageNumber : 1,
|
||||
PageSize = src.PageSize > 0 ? src.PageSize : 10
|
||||
});
|
||||
|
||||
// GetMyCommissionPayouts Response Mapping
|
||||
config.NewConfig<GetMyCommissionPayoutsResponseDto, GetMyCommissionPayoutsResponse>()
|
||||
.Map(dest => dest.MetaData, src => src.MetaData != null
|
||||
? new CustomerMetaData { TotalCount = src.MetaData.TotalCount }
|
||||
: new CustomerMetaData())
|
||||
.Map(dest => dest.Payouts, src => src.Models);
|
||||
|
||||
// CustomerCommissionPayoutModel Mapping
|
||||
config.NewConfig<GetMyCommissionPayoutsResponseModel, CustomerCommissionPayoutModel>()
|
||||
.Map(dest => dest.Id, src => src.Id)
|
||||
.Map(dest => dest.WeekDefinitionId, src => src.WeekDefinitionId)
|
||||
.Map(dest => dest.WeekDisplayName, src => src.WeekDisplayName)
|
||||
.Map(dest => dest.BalancesEarned, src => src.BalancesEarned)
|
||||
.Map(dest => dest.TotalAmount, src => src.TotalAmount)
|
||||
.Map(dest => dest.AmountFormatted, src => src.AmountFormatted)
|
||||
.Map(dest => dest.Status, src => (int)src.Status)
|
||||
.Map(dest => dest.CalculatedDate, src => src.CalculatedDate.HasValue
|
||||
? Timestamp.FromDateTime(src.CalculatedDate.Value.ToUniversalTime())
|
||||
: null)
|
||||
.Map(dest => dest.DatePersian, src => src.DatePersian);
|
||||
|
||||
// GetMyWeeklyBalances Request Mapping
|
||||
config.NewConfig<GetMyWeeklyBalancesRequest, GetMyWeeklyBalancesQuery>()
|
||||
.Map(dest => dest.WeekDefinitionId, src => src.WeekDefinitionId != null ? src.WeekDefinitionId.Value : (long?)null)
|
||||
.Map(dest => dest.OnlyActive, src => src.OnlyActive)
|
||||
.Map(dest => dest.PaginationState, src => new PaginationState
|
||||
{
|
||||
PageNumber = src.PageNumber > 0 ? src.PageNumber : 1,
|
||||
PageSize = src.PageSize > 0 ? src.PageSize : 10
|
||||
});
|
||||
|
||||
// GetMyWeeklyBalances Response Mapping
|
||||
config.NewConfig<GetUserWeeklyBalancesResponseDto, GetMyWeeklyBalancesResponse>()
|
||||
.Map(dest => dest.MetaData, src => src.MetaData != null
|
||||
? new CustomerMetaData { TotalCount = src.MetaData.TotalCount }
|
||||
: new CustomerMetaData())
|
||||
.Map(dest => dest.Balances, src => src.Models)
|
||||
.Map(dest => dest.TotalLeftBalances, src => src.Models.Sum(m => m.LeftLegTotal))
|
||||
.Map(dest => dest.TotalRightBalances, src => src.Models.Sum(m => m.RightLegTotal))
|
||||
.Map(dest => dest.WeakerLeg, src =>
|
||||
src.Models.Sum(m => m.LeftLegTotal) < src.Models.Sum(m => m.RightLegTotal)
|
||||
? "Left"
|
||||
: src.Models.Sum(m => m.LeftLegTotal) > src.Models.Sum(m => m.RightLegTotal)
|
||||
? "Right"
|
||||
: "Equal");
|
||||
|
||||
// CustomerWeeklyBalanceModel Mapping
|
||||
config.NewConfig<GetUserWeeklyBalancesResponseModel, CustomerWeeklyBalanceModel>()
|
||||
.Map(dest => dest.Id, src => src.Id)
|
||||
.Map(dest => dest.WeekDefinitionId, src => src.WeekDefinitionId)
|
||||
.Map(dest => dest.WeekDisplayName, src => src.WeekDisplayName)
|
||||
.Map(dest => dest.LeftLegBalances, src => src.LeftLegTotal)
|
||||
.Map(dest => dest.RightLegBalances, src => src.RightLegTotal)
|
||||
.Map(dest => dest.TotalBalances, src => src.TotalBalances)
|
||||
.Map(dest => dest.WeeklyPoolContribution, src => src.WeeklyPoolContribution)
|
||||
.Map(dest => dest.IsExpired, src => src.IsExpired)
|
||||
.Map(dest => dest.CalculatedAt, src => src.CalculatedAt.HasValue
|
||||
? Timestamp.FromDateTime(DateTime.SpecifyKind(src.CalculatedAt.Value, DateTimeKind.Utc))
|
||||
: null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
using CMSMicroservice.Application.Common.Authorization;
|
||||
using Grpc.Core;
|
||||
using Grpc.Core.Interceptors;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.WebApi.Interceptors;
|
||||
|
||||
/// <summary>
|
||||
/// gRPC Interceptor برای بررسی مجوز دسترسی
|
||||
/// بر اساس [RequiresPermission] attribute روی سرویسها/متدها
|
||||
/// </summary>
|
||||
public class PermissionInterceptor : Interceptor
|
||||
{
|
||||
private readonly IPermissionService _permissionService;
|
||||
private readonly ILogger<PermissionInterceptor> _logger;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
public PermissionInterceptor(
|
||||
IPermissionService permissionService,
|
||||
ILogger<PermissionInterceptor> logger,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_permissionService = permissionService;
|
||||
_logger = logger;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
|
||||
TRequest request, ServerCallContext context,
|
||||
UnaryServerMethod<TRequest, TResponse> continuation)
|
||||
{
|
||||
await EnsureHasPermissionAsync(context);
|
||||
return await continuation(request, context);
|
||||
}
|
||||
|
||||
public override async Task<TResponse> ClientStreamingServerHandler<TRequest, TResponse>(
|
||||
IAsyncStreamReader<TRequest> requestStream, ServerCallContext context,
|
||||
ClientStreamingServerMethod<TRequest, TResponse> continuation)
|
||||
{
|
||||
await EnsureHasPermissionAsync(context);
|
||||
return await continuation(requestStream, context);
|
||||
}
|
||||
|
||||
private async Task EnsureHasPermissionAsync(ServerCallContext context)
|
||||
{
|
||||
var httpContext = context.GetHttpContext() ?? _httpContextAccessor.HttpContext;
|
||||
if (httpContext == null) return;
|
||||
|
||||
var endpoint = httpContext.GetEndpoint();
|
||||
if (endpoint == null) return;
|
||||
|
||||
var permissionAttributes = endpoint.Metadata.GetOrderedMetadata<RequiresPermissionAttribute>();
|
||||
if (permissionAttributes == null || permissionAttributes.Count == 0) return;
|
||||
|
||||
foreach (var attribute in permissionAttributes)
|
||||
{
|
||||
var hasPermission = await _permissionService.HasPermissionAsync(
|
||||
attribute.Permission, httpContext.RequestAborted);
|
||||
|
||||
if (!hasPermission)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Permission denied: {Permission} for method {Method}",
|
||||
attribute.Permission, context.Method);
|
||||
|
||||
throw new RpcException(new Status(
|
||||
StatusCode.PermissionDenied,
|
||||
$"شما مجوز دسترسی به این عملیات را ندارید ({attribute.Permission})"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,7 @@ builder.Services.AddGrpc(options =>
|
||||
{
|
||||
options.Interceptors.Add<LoggingBehaviour>();
|
||||
options.Interceptors.Add<PerformanceBehaviour>();
|
||||
options.Interceptors.Add<CMSMicroservice.WebApi.Interceptors.PermissionInterceptor>();
|
||||
//options.Interceptors.Add<ExceptionHandlingBehaviour>();
|
||||
options.EnableDetailedErrors = true;
|
||||
options.MaxReceiveMessageSize = 1000 * 1024 * 1024; // 1 GB
|
||||
@@ -339,6 +340,7 @@ app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true }); // Configure the H
|
||||
|
||||
// Map SignalR Hub for token notifications
|
||||
app.MapHub<TokenNotificationHub>("/hubs/token-notification");
|
||||
app.MapHub<TokenNotificationHub>("/hubs/token-relay"); // Alias for FrontOffice backward compatibility
|
||||
|
||||
app.ConfigureGrpcEndpoints(Assembly.GetExecutingAssembly(), endpoints =>
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using CMSMicroservice.Application.Common.Authorization;
|
||||
using CMSMicroservice.Protobuf.Protos.AppVersion;
|
||||
using CMSMicroservice.WebApi.Common.Services;
|
||||
using CMSMicroservice.Application.AppVersionCQ.Queries.GetAppVersion;
|
||||
@@ -15,16 +16,19 @@ public class AppVersionService : AppVersionContract.AppVersionContractBase
|
||||
_dispatchRequestToCQRS = dispatchRequestToCQRS;
|
||||
}
|
||||
|
||||
[RequiresPermission(PermissionNames.SettingsView)]
|
||||
public override async Task<GetAppVersionResponse> GetAppVersion(GetAppVersionRequest request, ServerCallContext context)
|
||||
{
|
||||
return await _dispatchRequestToCQRS.Handle<GetAppVersionRequest, GetAppVersionQuery, GetAppVersionResponse>(request, context);
|
||||
}
|
||||
|
||||
[RequiresPermission(PermissionNames.SettingsManageConfiguration)]
|
||||
public override async Task<Empty> UpdateAppVersion(UpdateAppVersionRequest request, ServerCallContext context)
|
||||
{
|
||||
return await _dispatchRequestToCQRS.Handle<UpdateAppVersionRequest, UpdateAppVersionCommand>(request, context);
|
||||
}
|
||||
|
||||
[RequiresPermission(PermissionNames.SettingsView)]
|
||||
public override async Task<GetAllAppVersionsResponse> GetAllAppVersions(GetAllAppVersionsRequest request, ServerCallContext context)
|
||||
{
|
||||
return await _dispatchRequestToCQRS.Handle<GetAllAppVersionsRequest, GetAllAppVersionsQuery, GetAllAppVersionsResponse>(request, context);
|
||||
|
||||
@@ -5,14 +5,19 @@ using CMSMicroservice.Application.CategoryCQ.Commands.UpdateCategory;
|
||||
using CMSMicroservice.Application.CategoryCQ.Commands.DeleteCategory;
|
||||
using CMSMicroservice.Application.CategoryCQ.Queries.GetCategory;
|
||||
using CMSMicroservice.Application.CategoryCQ.Queries.GetAllCategoryByFilter;
|
||||
using MediatR;
|
||||
using AppModels = CMSMicroservice.Application.Common.Models;
|
||||
|
||||
namespace CMSMicroservice.WebApi.Services;
|
||||
public class CategoryService : CategoryContract.CategoryContractBase
|
||||
{
|
||||
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
|
||||
private readonly ISender _sender;
|
||||
|
||||
public CategoryService(IDispatchRequestToCQRS dispatchRequestToCQRS)
|
||||
public CategoryService(IDispatchRequestToCQRS dispatchRequestToCQRS, ISender sender)
|
||||
{
|
||||
_dispatchRequestToCQRS = dispatchRequestToCQRS;
|
||||
_sender = sender;
|
||||
}
|
||||
public override async Task<CreateNewCategoryResponse> CreateNewCategory(CreateNewCategoryRequest request, ServerCallContext context)
|
||||
{
|
||||
@@ -45,25 +50,72 @@ public class CategoryService : CategoryContract.CategoryContractBase
|
||||
|
||||
public override async Task<GetAllCategoriesForCustomerResponse> GetAllCategoriesForCustomer(GetAllCategoriesForCustomerRequest request, ServerCallContext context)
|
||||
{
|
||||
// TODO: Implement using existing CMS Category Application layer
|
||||
// For now, return empty response
|
||||
return new GetAllCategoriesForCustomerResponse
|
||||
// Use GetAllCategoryByFilter to get all active categories
|
||||
var query = new GetAllCategoryByFilterQuery
|
||||
{
|
||||
Filter = new CMSMicroservice.Application.CategoryCQ.Queries.GetAllCategoryByFilter.GetAllCategoryByFilterFilter
|
||||
{
|
||||
IsActive = true // Only active categories for customers
|
||||
},
|
||||
PaginationState = new AppModels.PaginationState
|
||||
{
|
||||
PageNumber = request.PageNumber > 0 ? request.PageNumber : 1,
|
||||
PageSize = request.PageSize > 0 ? request.PageSize : 100 // Default to large page size to get all
|
||||
},
|
||||
SortBy = "SortOrder" // Sort by display order
|
||||
};
|
||||
|
||||
var result = await _sender.Send(query, context.CancellationToken);
|
||||
|
||||
var response = new GetAllCategoriesForCustomerResponse
|
||||
{
|
||||
MetaData = new CMSMicroservice.Protobuf.Protos.MetaData
|
||||
{
|
||||
CurrentPage = request.PageNumber,
|
||||
PageSize = request.PageSize,
|
||||
TotalCount = 0,
|
||||
TotalPage = 0,
|
||||
HasNext = false,
|
||||
HasPrevious = false
|
||||
CurrentPage = result.MetaData.CurrentPage,
|
||||
PageSize = result.MetaData.PageSize,
|
||||
TotalCount = result.MetaData.TotalCount,
|
||||
TotalPage = result.MetaData.TotalPage,
|
||||
HasNext = result.MetaData.HasNext,
|
||||
HasPrevious = result.MetaData.HasPrevious
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var cat in result.Models)
|
||||
{
|
||||
response.Models.Add(new GetAllCategoryFilterResponseModel
|
||||
{
|
||||
Id = cat.Id,
|
||||
Name = cat.Name,
|
||||
Title = cat.Title,
|
||||
Description = cat.Description ?? string.Empty,
|
||||
ImagePath = cat.ImagePath ?? string.Empty,
|
||||
ParentId = cat.ParentId ?? 0,
|
||||
IsActive = cat.IsActive,
|
||||
SortOrder = cat.SortOrder
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public override async Task<GetCategoryByIdForCustomerResponse> GetCategoryByIdForCustomer(GetCategoryByIdForCustomerRequest request, ServerCallContext context)
|
||||
{
|
||||
// TODO: Implement using existing CMS Category Application layer
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "GetCategoryByIdForCustomer not implemented yet"));
|
||||
var query = new GetCategoryQuery { Id = request.Id };
|
||||
var result = await _sender.Send(query, context.CancellationToken);
|
||||
|
||||
return new GetCategoryByIdForCustomerResponse
|
||||
{
|
||||
Category = new GetAllCategoryFilterResponseModel
|
||||
{
|
||||
Id = result.Id,
|
||||
Name = result.Name,
|
||||
Title = result.Title,
|
||||
Description = result.Description ?? string.Empty,
|
||||
ImagePath = result.ImagePath ?? string.Empty,
|
||||
ParentId = result.ParentId ?? 0,
|
||||
IsActive = result.IsActive,
|
||||
SortOrder = result.SortOrder
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
using CMSMicroservice.Protobuf.Protos.City;
|
||||
using CMSMicroservice.WebApi.Common.Services;
|
||||
using CMSMicroservice.Application.CityCQ.Queries.GetAllCitiesByFilter;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using System.Linq;
|
||||
|
||||
namespace CMSMicroservice.WebApi.Services;
|
||||
|
||||
public class CityService : CityContract.CityContractBase
|
||||
{
|
||||
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public CityService(IDispatchRequestToCQRS dispatchRequestToCQRS)
|
||||
public CityService(IDispatchRequestToCQRS dispatchRequestToCQRS, IApplicationDbContext context)
|
||||
{
|
||||
_dispatchRequestToCQRS = dispatchRequestToCQRS;
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public override async Task<GetAllCitiesByFilterResponse> GetAllCitiesByFilter(
|
||||
@@ -28,18 +34,50 @@ public class CityService : CityContract.CityContractBase
|
||||
public override async Task<GetCitiesForCustomerResponse> GetCitiesForCustomer(
|
||||
GetCitiesForCustomerRequest request, ServerCallContext context)
|
||||
{
|
||||
// TODO: Implement using existing CMS City Application layer
|
||||
// For now, return empty response
|
||||
var pageNumber = request.PageNumber > 0 ? request.PageNumber : 1;
|
||||
var pageSize = request.PageSize > 0 ? request.PageSize : 50;
|
||||
|
||||
var query = _context.Cities
|
||||
.Include(c => c.State)
|
||||
.Where(c => !c.IsDeleted);
|
||||
|
||||
if (request.StateId != null)
|
||||
query = query.Where(c => c.StateId == request.StateId.Value);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.SearchTerm))
|
||||
query = query.Where(c => c.Name.Contains(request.SearchTerm) || c.Native.Contains(request.SearchTerm));
|
||||
|
||||
var totalCount = await query.CountAsync(context.CancellationToken);
|
||||
|
||||
var cities = await query
|
||||
.OrderBy(c => c.Native)
|
||||
.Skip((pageNumber - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.Select(c => new CityDto
|
||||
{
|
||||
Id = c.Id,
|
||||
ExternalId = c.ExternalId,
|
||||
Name = c.Name,
|
||||
Native = c.Native,
|
||||
Latitude = c.Latitude ?? string.Empty,
|
||||
Longitude = c.Longitude ?? string.Empty,
|
||||
StateId = c.StateId,
|
||||
StateName = c.State != null ? c.State.Name : string.Empty,
|
||||
StateNative = c.State != null ? c.State.Native : string.Empty
|
||||
})
|
||||
.ToListAsync(context.CancellationToken);
|
||||
|
||||
return new GetCitiesForCustomerResponse
|
||||
{
|
||||
Cities = { cities },
|
||||
MetaData = new CMSMicroservice.Protobuf.Protos.City.MetaData
|
||||
{
|
||||
CurrentPage = request.PageNumber,
|
||||
PageSize = request.PageSize,
|
||||
TotalCount = 0,
|
||||
TotalPage = 0,
|
||||
HasNext = false,
|
||||
HasPrevious = false
|
||||
CurrentPage = pageNumber,
|
||||
PageSize = pageSize,
|
||||
TotalCount = totalCount,
|
||||
TotalPage = (int)Math.Ceiling(totalCount / (double)pageSize),
|
||||
HasNext = pageNumber * pageSize < totalCount,
|
||||
HasPrevious = pageNumber > 1
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -47,34 +85,126 @@ public class CityService : CityContract.CityContractBase
|
||||
public override async Task<GetCityByIdForCustomerResponse> GetCityByIdForCustomer(
|
||||
GetCityByIdForCustomerRequest request, ServerCallContext context)
|
||||
{
|
||||
// TODO: Implement using existing CMS City Application layer
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "GetCityByIdForCustomer not implemented yet"));
|
||||
var city = await _context.Cities
|
||||
.Include(c => c.State)
|
||||
.Where(c => c.Id == request.Id && !c.IsDeleted)
|
||||
.Select(c => new CityDto
|
||||
{
|
||||
Id = c.Id,
|
||||
ExternalId = c.ExternalId,
|
||||
Name = c.Name,
|
||||
Native = c.Native,
|
||||
Latitude = c.Latitude ?? string.Empty,
|
||||
Longitude = c.Longitude ?? string.Empty,
|
||||
StateId = c.StateId,
|
||||
StateName = c.State != null ? c.State.Name : string.Empty,
|
||||
StateNative = c.State != null ? c.State.Native : string.Empty
|
||||
})
|
||||
.FirstOrDefaultAsync(context.CancellationToken);
|
||||
|
||||
if (city == null)
|
||||
throw new RpcException(new Status(StatusCode.NotFound, "شهر یافت نشد"));
|
||||
|
||||
return new GetCityByIdForCustomerResponse { City = city };
|
||||
}
|
||||
|
||||
public override async Task<GetCitiesByStateForCustomerResponse> GetCitiesByStateForCustomer(
|
||||
GetCitiesByStateForCustomerRequest request, ServerCallContext context)
|
||||
{
|
||||
// TODO: Implement using existing CMS City Application layer
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "GetCitiesByStateForCustomer not implemented yet"));
|
||||
var pageNumber = request.PageNumber > 0 ? request.PageNumber : 1;
|
||||
var pageSize = request.PageSize > 0 ? request.PageSize : 100;
|
||||
|
||||
var query = _context.Cities
|
||||
.Include(c => c.State)
|
||||
.Where(c => c.StateId == request.StateId && !c.IsDeleted);
|
||||
|
||||
var totalCount = await query.CountAsync(context.CancellationToken);
|
||||
|
||||
var cities = await query
|
||||
.OrderBy(c => c.Native)
|
||||
.Skip((pageNumber - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.Select(c => new CityDto
|
||||
{
|
||||
Id = c.Id,
|
||||
ExternalId = c.ExternalId,
|
||||
Name = c.Name,
|
||||
Native = c.Native,
|
||||
Latitude = c.Latitude ?? string.Empty,
|
||||
Longitude = c.Longitude ?? string.Empty,
|
||||
StateId = c.StateId,
|
||||
StateName = c.State != null ? c.State.Name : string.Empty,
|
||||
StateNative = c.State != null ? c.State.Native : string.Empty
|
||||
})
|
||||
.ToListAsync(context.CancellationToken);
|
||||
|
||||
return new GetCitiesByStateForCustomerResponse
|
||||
{
|
||||
Cities = { cities },
|
||||
MetaData = new CMSMicroservice.Protobuf.Protos.City.MetaData
|
||||
{
|
||||
CurrentPage = pageNumber,
|
||||
PageSize = pageSize,
|
||||
TotalCount = totalCount,
|
||||
TotalPage = (int)Math.Ceiling(totalCount / (double)pageSize),
|
||||
HasNext = pageNumber * pageSize < totalCount,
|
||||
HasPrevious = pageNumber > 1
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Admin Methods placeholder for future expansion
|
||||
// Admin Methods
|
||||
public override async Task<CreateCityResponse> CreateCity(
|
||||
CreateCityRequest request, ServerCallContext context)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "CreateCity not implemented yet"));
|
||||
var city = new Domain.Entities.Geography.City
|
||||
{
|
||||
ExternalId = request.ExternalId,
|
||||
Name = request.Name,
|
||||
Native = request.Native,
|
||||
Latitude = request.Latitude ?? string.Empty,
|
||||
Longitude = request.Longitude ?? string.Empty,
|
||||
StateId = request.StateId
|
||||
};
|
||||
|
||||
_context.Cities.Add(city);
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
|
||||
return new CreateCityResponse
|
||||
{
|
||||
Id = city.Id,
|
||||
Message = "شهر با موفقیت ایجاد شد"
|
||||
};
|
||||
}
|
||||
|
||||
public override async Task<Empty> UpdateCity(
|
||||
UpdateCityRequest request, ServerCallContext context)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "UpdateCity not implemented yet"));
|
||||
var city = await _context.Cities.FindAsync(new object[] { request.Id }, context.CancellationToken);
|
||||
if (city == null)
|
||||
throw new RpcException(new Status(StatusCode.NotFound, "شهر یافت نشد"));
|
||||
|
||||
city.ExternalId = request.ExternalId;
|
||||
city.Name = request.Name;
|
||||
city.Native = request.Native;
|
||||
if (request.Latitude != null) city.Latitude = request.Latitude;
|
||||
if (request.Longitude != null) city.Longitude = request.Longitude;
|
||||
city.StateId = request.StateId;
|
||||
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
return new Empty();
|
||||
}
|
||||
|
||||
public override async Task<Empty> DeleteCity(
|
||||
DeleteCityRequest request, ServerCallContext context)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "DeleteCity not implemented yet"));
|
||||
var city = await _context.Cities.FindAsync(new object[] { request.Id }, context.CancellationToken);
|
||||
if (city == null)
|
||||
throw new RpcException(new Status(StatusCode.NotFound, "شهر یافت نشد"));
|
||||
|
||||
city.IsDeleted = true;
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
return new Empty();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Protobuf.Protos.ClubMembership;
|
||||
using CMSMicroservice.WebApi.Common.Services;
|
||||
using CMSMicroservice.Application.ClubMembershipCQ.Commands.ActivateClubMembership;
|
||||
@@ -10,16 +11,24 @@ using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubMembershipHist
|
||||
using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubStatistics;
|
||||
using CMSMicroservice.Application.ClubFeatureCQ.Queries.GetUserClubFeatures;
|
||||
using CMSMicroservice.Application.ClubFeatureCQ.Commands.ToggleUserClubFeature;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.WebApi.Services;
|
||||
|
||||
public class ClubMembershipService : ClubMembershipContract.ClubMembershipContractBase
|
||||
{
|
||||
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
private readonly ILogger<ClubMembershipService> _logger;
|
||||
|
||||
public ClubMembershipService(IDispatchRequestToCQRS dispatchRequestToCQRS)
|
||||
public ClubMembershipService(
|
||||
IDispatchRequestToCQRS dispatchRequestToCQRS,
|
||||
ICurrentUserService currentUserService,
|
||||
ILogger<ClubMembershipService> logger)
|
||||
{
|
||||
_dispatchRequestToCQRS = dispatchRequestToCQRS;
|
||||
_currentUserService = currentUserService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override async Task<Empty> ActivateClubMembership(ActivateClubMembershipRequest request, ServerCallContext context)
|
||||
@@ -39,6 +48,25 @@ public class ClubMembershipService : ClubMembershipContract.ClubMembershipContra
|
||||
|
||||
public override async Task<GetClubMembershipResponse> GetClubMembership(GetClubMembershipRequest request, ServerCallContext context)
|
||||
{
|
||||
// اگر UserId در request نیست یا صفر است، از JWT بخوان
|
||||
if (request.UserId <= 0)
|
||||
{
|
||||
if (long.TryParse(_currentUserService.UserId, out var tokenUserId) && tokenUserId > 0)
|
||||
{
|
||||
_logger.LogInformation("GetClubMembership: Reading UserId from JWT token: {UserId}", tokenUserId);
|
||||
request = new GetClubMembershipRequest { UserId = tokenUserId };
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("GetClubMembership: No valid UserId in request or JWT token");
|
||||
throw new RpcException(new Status(StatusCode.Unauthenticated, "کاربر احراز هویت نشده است"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("GetClubMembership: Using UserId from request: {UserId}", request.UserId);
|
||||
}
|
||||
|
||||
return await _dispatchRequestToCQRS.Handle<GetClubMembershipRequest, GetClubMembershipQuery, GetClubMembershipResponse>(request, context);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ using CMSMicroservice.Application.CommissionCQ.Queries.GetWeeklyCommissionPool;
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetUserCommissionPayouts;
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetCommissionPayoutHistory;
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetUserWeeklyBalances;
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetMyWeeklyBalances;
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetAllWeeklyPools;
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetWithdrawalRequests;
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetWorkerStatus;
|
||||
@@ -19,6 +20,7 @@ using CMSMicroservice.Application.CommissionCQ.Queries.GetWorkerExecutionLogs;
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetWithdrawalReports;
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetAvailableWeeks;
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetWeekDefinitions;
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
|
||||
|
||||
namespace CMSMicroservice.WebApi.Services;
|
||||
|
||||
@@ -68,6 +70,11 @@ public class CommissionService : CommissionContract.CommissionContractBase
|
||||
return await _dispatchRequestToCQRS.Handle<GetUserCommissionPayoutsRequest, GetUserCommissionPayoutsQuery, GetUserCommissionPayoutsResponse>(request, context);
|
||||
}
|
||||
|
||||
public override async Task<GetMyCommissionPayoutsResponse> GetMyCommissionPayouts(GetMyCommissionPayoutsRequest request, ServerCallContext context)
|
||||
{
|
||||
return await _dispatchRequestToCQRS.Handle<GetMyCommissionPayoutsRequest, GetMyCommissionPayoutsQuery, GetMyCommissionPayoutsResponse>(request, context);
|
||||
}
|
||||
|
||||
public override async Task<GetCommissionPayoutHistoryResponse> GetCommissionPayoutHistory(GetCommissionPayoutHistoryRequest request, ServerCallContext context)
|
||||
{
|
||||
return await _dispatchRequestToCQRS.Handle<GetCommissionPayoutHistoryRequest, GetCommissionPayoutHistoryQuery, GetCommissionPayoutHistoryResponse>(request, context);
|
||||
@@ -128,4 +135,9 @@ public class CommissionService : CommissionContract.CommissionContractBase
|
||||
{
|
||||
return await _dispatchRequestToCQRS.Handle<GetWithdrawalReportsRequest, GetWithdrawalReportsQuery, GetWithdrawalReportsResponse>(request, context);
|
||||
}
|
||||
|
||||
public override async Task<GetMyWeeklyBalancesResponse> GetMyWeeklyBalances(GetMyWeeklyBalancesRequest request, ServerCallContext context)
|
||||
{
|
||||
return await _dispatchRequestToCQRS.Handle<GetMyWeeklyBalancesRequest, GetMyWeeklyBalancesQuery, GetMyWeeklyBalancesResponse>(request, context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
using CMSMicroservice.Application.ClubFeatureCQ.Queries.GetUserClubFeatures;
|
||||
using CMSMicroservice.Application.Common.Authorization;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Common;
|
||||
using CMSMicroservice.Protobuf.Protos.Configuration;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Grpc.Core;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.WebApi.Services;
|
||||
@@ -12,10 +16,17 @@ namespace CMSMicroservice.WebApi.Services;
|
||||
public class ConfigurationService : ConfigurationContract.ConfigurationContractBase
|
||||
{
|
||||
private readonly ILogger<ConfigurationService> _logger;
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public ConfigurationService(ILogger<ConfigurationService> logger)
|
||||
public ConfigurationService(
|
||||
ILogger<ConfigurationService> logger,
|
||||
ICurrentUserService currentUserService,
|
||||
IMediator mediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_currentUserService = currentUserService;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -41,9 +52,65 @@ public class ConfigurationService : ConfigurationContract.ConfigurationContractB
|
||||
return Task.FromResult(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت تنظیمات باشگاه مشتریان
|
||||
/// </summary>
|
||||
public override Task<GetClubConfigurationResponse> GetClubConfiguration(Empty request, ServerCallContext context)
|
||||
{
|
||||
var response = new GetClubConfigurationResponse
|
||||
{
|
||||
ActivationFee = SystemConstants.ClubActivationFee,
|
||||
MembershipGiftValue = SystemConstants.ClubMembershipGiftValue
|
||||
};
|
||||
|
||||
_logger.LogDebug("Club configuration requested: ActivationFee={ActivationFee}, GiftValue={GiftValue}",
|
||||
response.ActivationFee, response.MembershipGiftValue);
|
||||
|
||||
return Task.FromResult(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت ویژگیهای باشگاه مشتریان برای کاربر جاری
|
||||
/// </summary>
|
||||
public override async Task<GetClubFeaturesResponse> GetClubFeatures(Empty request, ServerCallContext context)
|
||||
{
|
||||
// دریافت UserId از JWT
|
||||
if (!long.TryParse(_currentUserService.UserId, out var userId) || userId <= 0)
|
||||
{
|
||||
_logger.LogWarning("GetClubFeatures called without valid user authentication");
|
||||
return new GetClubFeaturesResponse(); // لیست خالی برای کاربران غیر احراز هویت شده
|
||||
}
|
||||
|
||||
// فراخوانی GetUserClubFeatures از طریق MediatR (CQRS داخلی)
|
||||
var query = new GetUserClubFeaturesQuery { UserId = userId };
|
||||
var userFeatures = await _mediator.Send(query, context.CancellationToken);
|
||||
|
||||
// تبدیل به فرمت GetClubFeaturesResponse
|
||||
var response = new GetClubFeaturesResponse();
|
||||
foreach (var feature in userFeatures)
|
||||
{
|
||||
response.Features.Add(new ClubFeatureModel
|
||||
{
|
||||
Id = feature.ClubFeatureId,
|
||||
Title = feature.FeatureTitle ?? string.Empty,
|
||||
Description = feature.FeatureDescription ?? string.Empty,
|
||||
IsEnabled = feature.IsActive,
|
||||
DisplayOrder = feature.SortOrder,
|
||||
GrantedAt = Timestamp.FromDateTime(DateTime.SpecifyKind(feature.GrantedAt, DateTimeKind.Utc)),
|
||||
CreatedAt = Timestamp.FromDateTime(DateTime.SpecifyKind(feature.CreatedAt, DateTimeKind.Utc)),
|
||||
Notes = feature.Notes ?? string.Empty
|
||||
});
|
||||
}
|
||||
|
||||
_logger.LogDebug("Club features requested for user {UserId}: {Count} features returned", userId, response.Features.Count);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت تمام تنظیمات
|
||||
/// </summary>
|
||||
[RequiresPermission(PermissionNames.SettingsView)]
|
||||
public override Task<GetAllConfigurationsResponse> GetAllConfigurations(GetAllConfigurationsRequest request, ServerCallContext context)
|
||||
{
|
||||
var response = new GetAllConfigurationsResponse();
|
||||
@@ -80,16 +147,18 @@ public class ConfigurationService : ConfigurationContract.ConfigurationContractB
|
||||
/// <summary>
|
||||
/// سایر عملیاتها که فعلاً پیادهسازی نشدهاند (چون از constant استفاده میکنیم)
|
||||
/// </summary>
|
||||
[RequiresPermission(PermissionNames.SettingsManageConfiguration)]
|
||||
public override Task<Empty> CreateOrUpdateConfiguration(CreateOrUpdateConfigurationRequest request, ServerCallContext context)
|
||||
{
|
||||
_logger.LogWarning("CreateOrUpdateConfiguration called but SystemConstants are read-only");
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "تنظیمات سیستم فقط خواندنی هستند"));
|
||||
_logger.LogWarning("CreateOrUpdateConfiguration called but SystemConstants are read-only. Key: {Key}", request.Key);
|
||||
throw new RpcException(new Status(StatusCode.FailedPrecondition, "تنظیمات سیستم فقط خواندنی هستند. تغییر از طریق کد انجام میشود"));
|
||||
}
|
||||
|
||||
[RequiresPermission(PermissionNames.SettingsManageConfiguration)]
|
||||
public override Task<Empty> DeactivateConfiguration(DeactivateConfigurationRequest request, ServerCallContext context)
|
||||
{
|
||||
_logger.LogWarning("DeactivateConfiguration called but SystemConstants are read-only");
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "تنظیمات سیستم فقط خواندنی هستند"));
|
||||
_logger.LogWarning("DeactivateConfiguration called but SystemConstants are read-only. Key: {Key}", request.Key);
|
||||
throw new RpcException(new Status(StatusCode.FailedPrecondition, "تنظیمات سیستم فقط خواندنی هستند. غیرفعالسازی از طریق کد انجام میشود"));
|
||||
}
|
||||
|
||||
public override Task<GetConfigurationHistoryResponse> GetConfigurationHistory(GetConfigurationHistoryRequest request, ServerCallContext context)
|
||||
|
||||
@@ -18,9 +18,12 @@ using CMSMicroservice.Application.InventoryItemCQ.Queries.GetLowStockItems;
|
||||
using CMSMicroservice.Application.StockMovementCQ.Commands.CreateStockMovement;
|
||||
using CMSMicroservice.Application.StockMovementCQ.Queries.GetStockMovements;
|
||||
using CMSMicroservice.Application.StockMovementCQ.Queries.GetStockMovementsByInventoryItem;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Grpc.Core;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Linq;
|
||||
|
||||
namespace CMSMicroservice.WebApi.Services;
|
||||
|
||||
@@ -28,11 +31,13 @@ public class InventoryService : InventoryContract.InventoryContractBase
|
||||
{
|
||||
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
|
||||
private readonly IMediator _mediator;
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public InventoryService(IDispatchRequestToCQRS dispatchRequestToCQRS, IMediator mediator)
|
||||
public InventoryService(IDispatchRequestToCQRS dispatchRequestToCQRS, IMediator mediator, IApplicationDbContext context)
|
||||
{
|
||||
_dispatchRequestToCQRS = dispatchRequestToCQRS;
|
||||
_mediator = mediator;
|
||||
_context = context;
|
||||
}
|
||||
|
||||
// ========== Warehouse Management ==========
|
||||
@@ -198,28 +203,109 @@ public class InventoryService : InventoryContract.InventoryContractBase
|
||||
}
|
||||
}
|
||||
|
||||
public override Task<ReserveStockResponse> ReserveStock(ReserveStockRequest request, ServerCallContext context)
|
||||
public override async Task<ReserveStockResponse> ReserveStock(ReserveStockRequest request, ServerCallContext context)
|
||||
{
|
||||
// TODO: Implement with product lookup
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "ReserveStock requires product lookup - not yet implemented"));
|
||||
var inventoryItem = await _mediator.Send(
|
||||
new GetInventoryByProductQuery
|
||||
{
|
||||
ProductId = request.ProductId,
|
||||
ProductType = (Domain.Enums.ProductType)request.ProductType
|
||||
},
|
||||
context.CancellationToken);
|
||||
|
||||
if (inventoryItem == null)
|
||||
return new ReserveStockResponse { Success = false, Message = "آیتم موجودی یافت نشد", AvailableQuantity = 0 };
|
||||
|
||||
var available = inventoryItem.Quantity - inventoryItem.ReservedQuantity;
|
||||
if (available < request.Quantity)
|
||||
return new ReserveStockResponse { Success = false, Message = $"موجودی کافی نیست. موجود: {available}", AvailableQuantity = available };
|
||||
|
||||
await _mediator.Send(
|
||||
new ReserveInventoryCommand
|
||||
{
|
||||
Id = inventoryItem.Id,
|
||||
Quantity = request.Quantity,
|
||||
ReferenceNumber = request.OrderId != null ? $"ORDER-{request.OrderId.Value}" : $"RESERVE-{DateTime.UtcNow.Ticks}"
|
||||
},
|
||||
context.CancellationToken);
|
||||
|
||||
return new ReserveStockResponse { Success = true, Message = "رزرو با موفقیت انجام شد", AvailableQuantity = available - request.Quantity };
|
||||
}
|
||||
|
||||
public override Task<Empty> ReleaseReservation(ReleaseReservationRequest request, ServerCallContext context)
|
||||
public override async Task<Empty> ReleaseReservation(ReleaseReservationRequest request, ServerCallContext context)
|
||||
{
|
||||
// TODO: Implement with product lookup
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "ReleaseReservation requires product lookup - not yet implemented"));
|
||||
var inventoryItem = await _mediator.Send(
|
||||
new GetInventoryByProductQuery
|
||||
{
|
||||
ProductId = request.ProductId,
|
||||
ProductType = (Domain.Enums.ProductType)request.ProductType
|
||||
},
|
||||
context.CancellationToken);
|
||||
|
||||
if (inventoryItem == null)
|
||||
throw new RpcException(new Status(StatusCode.NotFound, "آیتم موجودی یافت نشد"));
|
||||
|
||||
await _mediator.Send(
|
||||
new ReleaseReservedInventoryCommand
|
||||
{
|
||||
Id = inventoryItem.Id,
|
||||
Quantity = request.Quantity,
|
||||
ReferenceNumber = request.OrderId != null ? $"RELEASE-ORDER-{request.OrderId.Value}" : $"RELEASE-{DateTime.UtcNow.Ticks}"
|
||||
},
|
||||
context.CancellationToken);
|
||||
|
||||
return new Empty();
|
||||
}
|
||||
|
||||
public override Task<Empty> ConfirmSale(ConfirmSaleRequest request, ServerCallContext context)
|
||||
public override async Task<Empty> ConfirmSale(ConfirmSaleRequest request, ServerCallContext context)
|
||||
{
|
||||
// TODO: Implement with product lookup
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "ConfirmSale requires product lookup - not yet implemented"));
|
||||
var inventoryItem = await _mediator.Send(
|
||||
new GetInventoryByProductQuery
|
||||
{
|
||||
ProductId = request.ProductId,
|
||||
ProductType = (Domain.Enums.ProductType)request.ProductType
|
||||
},
|
||||
context.CancellationToken);
|
||||
|
||||
if (inventoryItem == null)
|
||||
throw new RpcException(new Status(StatusCode.NotFound, "آیتم موجودی یافت نشد"));
|
||||
|
||||
await _mediator.Send(
|
||||
new ReduceInventoryCommand
|
||||
{
|
||||
Id = inventoryItem.Id,
|
||||
Quantity = request.Quantity,
|
||||
FromReserved = request.FromReservation,
|
||||
ReferenceNumber = request.OrderId != null ? $"SALE-ORDER-{request.OrderId.Value}" : $"SALE-{DateTime.UtcNow.Ticks}"
|
||||
},
|
||||
context.CancellationToken);
|
||||
|
||||
return new Empty();
|
||||
}
|
||||
|
||||
public override Task<ProcessReturnResponse> ProcessReturn(ProcessReturnRequest request, ServerCallContext context)
|
||||
public override async Task<ProcessReturnResponse> ProcessReturn(ProcessReturnRequest request, ServerCallContext context)
|
||||
{
|
||||
// TODO: Implement with product lookup
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "ProcessReturn requires product lookup - not yet implemented"));
|
||||
var inventoryItem = await _mediator.Send(
|
||||
new GetInventoryByProductQuery
|
||||
{
|
||||
ProductId = request.ProductId,
|
||||
ProductType = (Domain.Enums.ProductType)request.ProductType
|
||||
},
|
||||
context.CancellationToken);
|
||||
|
||||
if (inventoryItem == null)
|
||||
throw new RpcException(new Status(StatusCode.NotFound, "آیتم موجودی یافت نشد"));
|
||||
|
||||
var result = await _mediator.Send(
|
||||
new IncreaseInventoryCommand
|
||||
{
|
||||
Id = inventoryItem.Id,
|
||||
Quantity = request.Quantity,
|
||||
ReferenceNumber = request.OrderId != null ? $"RETURN-ORDER-{request.OrderId.Value}" : $"RETURN-{DateTime.UtcNow.Ticks}"
|
||||
},
|
||||
context.CancellationToken);
|
||||
|
||||
return new ProcessReturnResponse { NewQuantity = result.NewQuantity };
|
||||
}
|
||||
|
||||
public override async Task<Empty> RecordLoss(RecordLossRequest request, ServerCallContext context)
|
||||
@@ -255,10 +341,48 @@ public class InventoryService : InventoryContract.InventoryContractBase
|
||||
|
||||
// ========== Bulk Operations ==========
|
||||
|
||||
public override Task<BulkAddStockResponse> BulkAddStock(BulkAddStockRequest request, ServerCallContext context)
|
||||
public override async Task<BulkAddStockResponse> BulkAddStock(BulkAddStockRequest request, ServerCallContext context)
|
||||
{
|
||||
// TODO: Implement with product lookup
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "BulkAddStock requires product lookup - not yet implemented"));
|
||||
var response = new BulkAddStockResponse();
|
||||
|
||||
foreach (var item in request.Items)
|
||||
{
|
||||
try
|
||||
{
|
||||
var inventoryItem = await _mediator.Send(
|
||||
new GetInventoryByProductQuery
|
||||
{
|
||||
ProductId = item.ProductId,
|
||||
ProductType = (Domain.Enums.ProductType)item.ProductType
|
||||
},
|
||||
context.CancellationToken);
|
||||
|
||||
if (inventoryItem == null)
|
||||
{
|
||||
response.FailedCount++;
|
||||
response.Errors.Add($"ProductId={item.ProductId}: آیتم موجودی یافت نشد");
|
||||
continue;
|
||||
}
|
||||
|
||||
await _mediator.Send(
|
||||
new IncreaseInventoryCommand
|
||||
{
|
||||
Id = inventoryItem.Id,
|
||||
Quantity = item.Quantity,
|
||||
ReferenceNumber = request.ReferenceNumber ?? $"BULK-{DateTime.UtcNow.Ticks}"
|
||||
},
|
||||
context.CancellationToken);
|
||||
|
||||
response.SuccessCount++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
response.FailedCount++;
|
||||
response.Errors.Add($"ProductId={item.ProductId}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// ========== Stock Movements ==========
|
||||
@@ -275,28 +399,74 @@ public class InventoryService : InventoryContract.InventoryContractBase
|
||||
|
||||
// ========== Reports ==========
|
||||
|
||||
public override Task<GetInventorySummaryResponse> GetInventorySummary(GetInventorySummaryRequest request, ServerCallContext context)
|
||||
public override async Task<GetInventorySummaryResponse> GetInventorySummary(GetInventorySummaryRequest request, ServerCallContext context)
|
||||
{
|
||||
// TODO: Implement summary query
|
||||
return Task.FromResult(new GetInventorySummaryResponse
|
||||
var query = _context.InventoryItems
|
||||
.Include(i => i.Product)
|
||||
.Include(i => i.DiscountProduct)
|
||||
.Where(i => !i.IsDeleted);
|
||||
|
||||
if (request.WarehouseId != null)
|
||||
query = query.Where(i => i.WarehouseId == request.WarehouseId.Value);
|
||||
|
||||
var items = await query.ToListAsync(context.CancellationToken);
|
||||
|
||||
var regularProducts = items.Where(i => i.ProductType == Domain.Enums.ProductType.RegularProduct).ToList();
|
||||
var discountProducts = items.Where(i => i.ProductType == Domain.Enums.ProductType.DiscountProduct).ToList();
|
||||
|
||||
long totalStockValue = items.Sum(i =>
|
||||
{
|
||||
TotalProducts = 0,
|
||||
TotalDiscountProducts = 0,
|
||||
TotalQuantity = 0,
|
||||
TotalReserved = 0,
|
||||
LowStockCount = 0,
|
||||
OutOfStockCount = 0,
|
||||
TotalStockValue = 0
|
||||
long unitPrice = i.Product?.Price ?? i.DiscountProduct?.Price ?? 0;
|
||||
return (long)i.Quantity * unitPrice;
|
||||
});
|
||||
|
||||
return new GetInventorySummaryResponse
|
||||
{
|
||||
TotalProducts = regularProducts.Count,
|
||||
TotalDiscountProducts = discountProducts.Count,
|
||||
TotalQuantity = items.Sum(i => i.Quantity),
|
||||
TotalReserved = items.Sum(i => i.ReservedQuantity),
|
||||
LowStockCount = items.Count(i => i.Quantity <= i.LowStockThreshold && i.Quantity > 0),
|
||||
OutOfStockCount = items.Count(i => i.Quantity == 0),
|
||||
TotalStockValue = totalStockValue
|
||||
};
|
||||
}
|
||||
|
||||
public override Task<GetStockValueReportResponse> GetStockValueReport(GetStockValueReportRequest request, ServerCallContext context)
|
||||
public override async Task<GetStockValueReportResponse> GetStockValueReport(GetStockValueReportRequest request, ServerCallContext context)
|
||||
{
|
||||
// TODO: Implement stock value report query
|
||||
return Task.FromResult(new GetStockValueReportResponse
|
||||
var query = _context.InventoryItems
|
||||
.Include(i => i.Product)
|
||||
.Include(i => i.DiscountProduct)
|
||||
.Where(i => !i.IsDeleted);
|
||||
|
||||
if (request.WarehouseId != null)
|
||||
query = query.Where(i => i.WarehouseId == request.WarehouseId.Value);
|
||||
|
||||
if (request.ProductType != ProductType.Unspecified)
|
||||
query = query.Where(i => i.ProductType == (Domain.Enums.ProductType)request.ProductType);
|
||||
|
||||
var dbItems = await query.ToListAsync(context.CancellationToken);
|
||||
|
||||
var items = dbItems.Select(i =>
|
||||
{
|
||||
TotalValue = 0,
|
||||
TotalItems = 0
|
||||
});
|
||||
long unitPrice = i.Product?.Price ?? i.DiscountProduct?.Price ?? 0;
|
||||
string title = i.Product?.Title ?? i.DiscountProduct?.Title ?? string.Empty;
|
||||
return new StockValueItem
|
||||
{
|
||||
ProductId = i.ProductId ?? i.DiscountProductId ?? 0,
|
||||
ProductTitle = title,
|
||||
ProductType = (ProductType)i.ProductType,
|
||||
Quantity = i.Quantity,
|
||||
UnitPrice = unitPrice,
|
||||
TotalValue = (long)i.Quantity * unitPrice
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
return new GetStockValueReportResponse
|
||||
{
|
||||
Items = { items },
|
||||
TotalValue = items.Sum(i => i.TotalValue),
|
||||
TotalItems = items.Count
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using CMSMicroservice.Application.Common.Authorization;
|
||||
using CMSMicroservice.Protobuf.Protos.ManualPayment;
|
||||
using CMSMicroservice.WebApi.Common.Services;
|
||||
using CMSMicroservice.Application.ManualPaymentCQ.Commands.CreateManualPayment;
|
||||
@@ -24,6 +25,7 @@ public class ManualPaymentService : ManualPaymentContract.ManualPaymentContractB
|
||||
_sender = sender;
|
||||
}
|
||||
|
||||
[RequiresPermission(PermissionNames.ManualPaymentsCreate)]
|
||||
public override async Task<CreateManualPaymentResponse> CreateManualPayment(
|
||||
CreateManualPaymentRequest request,
|
||||
ServerCallContext context)
|
||||
@@ -37,6 +39,7 @@ public class ManualPaymentService : ManualPaymentContract.ManualPaymentContractB
|
||||
};
|
||||
}
|
||||
|
||||
[RequiresPermission(PermissionNames.ManualPaymentsApprove)]
|
||||
public override async Task<Google.Protobuf.WellKnownTypes.Empty> ApproveManualPayment(
|
||||
ApproveManualPaymentRequest request,
|
||||
ServerCallContext context)
|
||||
@@ -44,6 +47,7 @@ public class ManualPaymentService : ManualPaymentContract.ManualPaymentContractB
|
||||
return await _dispatchRequestToCQRS.Handle<ApproveManualPaymentRequest, ApproveManualPaymentCommand>(request, context);
|
||||
}
|
||||
|
||||
[RequiresPermission(PermissionNames.ManualPaymentsApprove)]
|
||||
public override async Task<Google.Protobuf.WellKnownTypes.Empty> RejectManualPayment(
|
||||
RejectManualPaymentRequest request,
|
||||
ServerCallContext context)
|
||||
@@ -51,6 +55,7 @@ public class ManualPaymentService : ManualPaymentContract.ManualPaymentContractB
|
||||
return await _dispatchRequestToCQRS.Handle<RejectManualPaymentRequest, RejectManualPaymentCommand>(request, context);
|
||||
}
|
||||
|
||||
[RequiresPermission(PermissionNames.ManualPaymentsView)]
|
||||
public override async Task<GetAllManualPaymentsResponse> GetAllManualPayments(
|
||||
GetAllManualPaymentsRequest request,
|
||||
ServerCallContext context)
|
||||
@@ -58,6 +63,7 @@ public class ManualPaymentService : ManualPaymentContract.ManualPaymentContractB
|
||||
return await _dispatchRequestToCQRS.Handle<GetAllManualPaymentsRequest, GetAllManualPaymentsQuery, GetAllManualPaymentsResponse>(request, context);
|
||||
}
|
||||
|
||||
[RequiresPermission(PermissionNames.ManualPaymentsCreate)]
|
||||
public override async Task<ProcessManualMembershipPaymentResponse> ProcessManualMembershipPayment(
|
||||
ProcessManualMembershipPaymentRequest request,
|
||||
ServerCallContext context)
|
||||
|
||||
@@ -13,11 +13,14 @@ using CMSMicroservice.Application.PackageCQ.Queries.GetUserPackageStatus;
|
||||
using CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPackages;
|
||||
using CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPackageDetails;
|
||||
using CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPurchaseHistory;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
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 Microsoft.EntityFrameworkCore;
|
||||
using MediatR;
|
||||
using Mapster;
|
||||
|
||||
@@ -26,11 +29,22 @@ public class PackageService : PackageContract.PackageContractBase
|
||||
{
|
||||
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
|
||||
private readonly ISender _sender;
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
private readonly IPaymentGatewayService _paymentGateway;
|
||||
|
||||
public PackageService(IDispatchRequestToCQRS dispatchRequestToCQRS, ISender sender)
|
||||
public PackageService(
|
||||
IDispatchRequestToCQRS dispatchRequestToCQRS,
|
||||
ISender sender,
|
||||
IApplicationDbContext context,
|
||||
ICurrentUserService currentUserService,
|
||||
IPaymentGatewayService paymentGateway)
|
||||
{
|
||||
_dispatchRequestToCQRS = dispatchRequestToCQRS;
|
||||
_sender = sender;
|
||||
_context = context;
|
||||
_currentUserService = currentUserService;
|
||||
_paymentGateway = paymentGateway;
|
||||
}
|
||||
public override async Task<CreateNewPackageResponse> CreateNewPackage(CreateNewPackageRequest request, ServerCallContext context)
|
||||
{
|
||||
@@ -142,45 +156,161 @@ public class PackageService : PackageContract.PackageContractBase
|
||||
|
||||
public override async Task<CustomerPurchasePackageResponse> CustomerPurchasePackage(CustomerPurchasePackageRequest request, ServerCallContext context)
|
||||
{
|
||||
// Mock Customer package purchase with realistic Persian response
|
||||
var orderId = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var authority = "A" + orderId.ToString("D19");
|
||||
var userId = GetCurrentUserId();
|
||||
|
||||
// Lookup package
|
||||
var package = await _context.Packages
|
||||
.AsNoTracking()
|
||||
.Where(p => p.Id == request.PackageId && !p.IsDeleted)
|
||||
.FirstOrDefaultAsync(context.CancellationToken);
|
||||
|
||||
if (package == null)
|
||||
throw new RpcException(new Status(StatusCode.NotFound, "پکیج مورد نظر یافت نشد"));
|
||||
|
||||
// Create transaction
|
||||
var transaction = new CMSMicroservice.Domain.Entities.Transaction
|
||||
{
|
||||
Amount = package.Price,
|
||||
Description = $"خرید پکیج {package.Title}",
|
||||
PaymentStatus = Domain.Enums.PaymentStatus.Pending,
|
||||
Type = Domain.Enums.TransactionType.Buy
|
||||
};
|
||||
_context.Transactions.Add(transaction);
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
|
||||
// Create purchase record
|
||||
var purchaseMethod = request.PurchaseMethod == PurchaseMethodEnum.PurchaseMethodGateway
|
||||
? Domain.Enums.PackagePurchaseMethod.DirectPurchase
|
||||
: Domain.Enums.PackagePurchaseMethod.DayaLoan;
|
||||
|
||||
var purchase = new CMSMicroservice.Domain.Entities.UserPackagePurchase
|
||||
{
|
||||
UserId = userId,
|
||||
PackageId = package.Id,
|
||||
PurchaseMethod = purchaseMethod,
|
||||
PurchasedAt = DateTime.UtcNow,
|
||||
Amount = package.Price,
|
||||
TransactionId = transaction.Id
|
||||
};
|
||||
_context.UserPackagePurchases.Add(purchase);
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
|
||||
// Initiate payment with gateway
|
||||
var user = await _context.Users
|
||||
.AsNoTracking()
|
||||
.Where(u => u.Id == userId)
|
||||
.Select(u => new { u.Mobile })
|
||||
.FirstOrDefaultAsync(context.CancellationToken);
|
||||
|
||||
var paymentResult = await _paymentGateway.InitiatePaymentAsync(new PaymentRequest
|
||||
{
|
||||
Amount = package.Price,
|
||||
UserId = userId,
|
||||
Mobile = user?.Mobile ?? string.Empty,
|
||||
Description = $"خرید پکیج {package.Title}",
|
||||
CallbackUrl = request.CallbackUrl
|
||||
}, context.CancellationToken);
|
||||
|
||||
if (!paymentResult.IsSuccess)
|
||||
{
|
||||
transaction.PaymentStatus = Domain.Enums.PaymentStatus.Reject;
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
|
||||
return new CustomerPurchasePackageResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = paymentResult.ErrorMessage ?? "خطا در ارتباط با درگاه پرداخت"
|
||||
};
|
||||
}
|
||||
|
||||
// Save RefId
|
||||
transaction.RefId = paymentResult.RefId;
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
|
||||
return new CustomerPurchasePackageResponse
|
||||
{
|
||||
Success = true,
|
||||
Message = "درخواست خرید پکیج با موفقیت ثبت شد",
|
||||
OrderId = orderId,
|
||||
PaymentGatewayUrl = $"https://payment.gateway.com/payment?authority={authority}&amount=5600000",
|
||||
Authority = authority
|
||||
OrderId = purchase.Id,
|
||||
PaymentGatewayUrl = paymentResult.GatewayUrl ?? string.Empty,
|
||||
Authority = paymentResult.RefId ?? string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
public override async Task<CustomerVerifyPackagePurchaseResponse> CustomerVerifyPackagePurchase(CustomerVerifyPackagePurchaseRequest request, ServerCallContext context)
|
||||
{
|
||||
// Mock Customer purchase verification with realistic Persian data
|
||||
var transactionId = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var referenceCode = "REF" + transactionId.ToString();
|
||||
// Find purchase record
|
||||
var purchase = await _context.UserPackagePurchases
|
||||
.Include(p => p.Package)
|
||||
.Where(p => p.Id == request.OrderId && !p.IsDeleted)
|
||||
.FirstOrDefaultAsync(context.CancellationToken);
|
||||
|
||||
var isSuccessful = request.Status == "OK";
|
||||
if (purchase == null)
|
||||
throw new RpcException(new Status(StatusCode.NotFound, "سفارش یافت نشد"));
|
||||
|
||||
// Find the associated transaction
|
||||
var transaction = purchase.TransactionId.HasValue
|
||||
? await _context.Transactions
|
||||
.Where(t => t.Id == purchase.TransactionId.Value)
|
||||
.FirstOrDefaultAsync(context.CancellationToken)
|
||||
: null;
|
||||
|
||||
// If status from gateway callback is not OK
|
||||
if (request.Status != "OK")
|
||||
{
|
||||
if (transaction != null)
|
||||
{
|
||||
transaction.PaymentStatus = Domain.Enums.PaymentStatus.Reject;
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
}
|
||||
|
||||
return new CustomerVerifyPackagePurchaseResponse
|
||||
{
|
||||
Success = isSuccessful,
|
||||
Message = isSuccessful ? "خرید پکیج با موفقیت تایید شد" : "خرید پکیج ناموفق بود",
|
||||
TransactionId = transactionId,
|
||||
ReferenceCode = referenceCode,
|
||||
PurchaseInfo = isSuccessful ? new PackagePurchaseInfo
|
||||
Success = false,
|
||||
Message = "پرداخت توسط کاربر لغو شد"
|
||||
};
|
||||
}
|
||||
|
||||
// Verify with payment gateway
|
||||
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
|
||||
request.Authority, request.Status, context.CancellationToken);
|
||||
|
||||
if (verifyResult.IsSuccess && transaction != null)
|
||||
{
|
||||
PackageId = 1,
|
||||
PackageName = "پکیج طلایی",
|
||||
AmountPaid = 5600000,
|
||||
PurchaseDate = Timestamp.FromDateTime(DateTime.UtcNow),
|
||||
ExpiryDate = Timestamp.FromDateTime(DateTime.UtcNow.AddDays(365))
|
||||
transaction.PaymentStatus = Domain.Enums.PaymentStatus.Success;
|
||||
transaction.PaymentDate = DateTime.UtcNow;
|
||||
transaction.RefId = verifyResult.RefId;
|
||||
}
|
||||
else if (transaction != null)
|
||||
{
|
||||
transaction.PaymentStatus = Domain.Enums.PaymentStatus.Reject;
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
|
||||
return new CustomerVerifyPackagePurchaseResponse
|
||||
{
|
||||
Success = verifyResult.IsSuccess,
|
||||
Message = verifyResult.IsSuccess ? "خرید پکیج با موفقیت تایید شد" : (verifyResult.Message ?? "خرید پکیج ناموفق بود"),
|
||||
TransactionId = transaction?.Id ?? 0,
|
||||
ReferenceCode = verifyResult.RefId ?? string.Empty,
|
||||
PurchaseInfo = verifyResult.IsSuccess ? new PackagePurchaseInfo
|
||||
{
|
||||
PackageId = purchase.PackageId,
|
||||
PackageName = purchase.Package?.Title ?? string.Empty,
|
||||
AmountPaid = purchase.Amount,
|
||||
PurchaseDate = Timestamp.FromDateTime(DateTime.SpecifyKind(purchase.PurchasedAt, DateTimeKind.Utc))
|
||||
} : null
|
||||
};
|
||||
}
|
||||
|
||||
private long GetCurrentUserId()
|
||||
{
|
||||
if (long.TryParse(_currentUserService.UserId, out var userId) && userId > 0)
|
||||
return userId;
|
||||
throw new RpcException(new Status(StatusCode.Unauthenticated, "لطفاً وارد حساب کاربری خود شوید"));
|
||||
}
|
||||
|
||||
public override async Task<GetCustomerPurchaseHistoryResponse> GetCustomerPurchaseHistory(GetCustomerPurchaseHistoryRequest request, ServerCallContext context)
|
||||
{
|
||||
var query = new GetCustomerPurchaseHistoryQuery
|
||||
|
||||
@@ -1,65 +1,478 @@
|
||||
using CMSMicroservice.Protobuf.Protos.Products;
|
||||
using Grpc.Core;
|
||||
using MediatR;
|
||||
using CMSMicroservice.Application.ProductsCQ.Commands.CreateNewProducts;
|
||||
using CMSMicroservice.Application.ProductsCQ.Commands.UpdateProducts;
|
||||
using CMSMicroservice.Application.ProductsCQ.Commands.DeleteProducts;
|
||||
using CMSMicroservice.Application.ProductsCQ.Commands.AddProductImage;
|
||||
using CMSMicroservice.Application.ProductsCQ.Commands.RemoveProductImage;
|
||||
using CMSMicroservice.Application.ProductsCQ.Queries.GetCustomerProducts;
|
||||
using CMSMicroservice.Application.ProductsCQ.Queries.GetCustomerProductsByFilter;
|
||||
using CMSMicroservice.Application.ProductsCQ.Queries.GetProductGallery;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using Mapster;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using AppModels = CMSMicroservice.Application.Common.Models;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace CMSMicroservice.WebApi.Services;
|
||||
|
||||
public class ProductsService : ProductsContract.ProductsContractBase
|
||||
{
|
||||
private readonly ISender _sender;
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public ProductsService(ISender sender)
|
||||
public ProductsService(ISender sender, IApplicationDbContext context)
|
||||
{
|
||||
_sender = sender;
|
||||
_context = context;
|
||||
}
|
||||
public override async Task<CreateNewProductsResponse> CreateNewProducts(CreateNewProductsRequest request, ServerCallContext context)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
|
||||
var command = new CreateNewProductsCommand
|
||||
{
|
||||
Title = request.Title,
|
||||
Description = request.Description,
|
||||
ShortInfomation = request.ShortInfomation,
|
||||
FullInformation = request.FullInformation,
|
||||
Price = request.Price,
|
||||
Discount = request.Discount,
|
||||
Rate = request.Rate,
|
||||
ImagePath = request.ImagePath,
|
||||
ThumbnailPath = request.ThumbnailPath,
|
||||
SaleCount = request.SaleCount,
|
||||
ViewCount = request.ViewCount,
|
||||
RemainingCount = request.RemainingCount,
|
||||
CategoryIds = request.CategoryIds?.ToList() ?? new List<long>(),
|
||||
ImageFileBytes = request.ImageFile?.File?.ToByteArray(),
|
||||
ImageFileMime = request.ImageFile?.Mime,
|
||||
ImageFileName = request.ImageFile?.FileName,
|
||||
ThumbnailFileBytes = request.ThumbnailFile?.File?.ToByteArray(),
|
||||
ThumbnailFileMime = request.ThumbnailFile?.Mime,
|
||||
ThumbnailFileName = request.ThumbnailFile?.FileName
|
||||
};
|
||||
|
||||
var result = await _sender.Send(command, context.CancellationToken);
|
||||
return new CreateNewProductsResponse { Id = result.Id };
|
||||
}
|
||||
|
||||
public override async Task<Google.Protobuf.WellKnownTypes.Empty> UpdateProducts(UpdateProductsRequest request, ServerCallContext context)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
|
||||
var command = new UpdateProductsCommand
|
||||
{
|
||||
Id = request.Id,
|
||||
Title = request.Title,
|
||||
Description = request.Description,
|
||||
ShortInfomation = request.ShortInfomation,
|
||||
FullInformation = request.FullInformation,
|
||||
Price = request.Price,
|
||||
Discount = request.Discount,
|
||||
Rate = request.Rate,
|
||||
ImagePath = request.ImagePath,
|
||||
ThumbnailPath = request.ThumbnailPath,
|
||||
SaleCount = request.SaleCount,
|
||||
ViewCount = request.ViewCount,
|
||||
RemainingCount = request.RemainingCount,
|
||||
CategoryIds = request.CategoryIds?.ToList() ?? new List<long>(),
|
||||
ImageFileBytes = request.ImageFile?.File?.ToByteArray(),
|
||||
ImageFileMime = request.ImageFile?.Mime,
|
||||
ImageFileName = request.ImageFile?.FileName,
|
||||
ThumbnailFileBytes = request.ThumbnailFile?.File?.ToByteArray(),
|
||||
ThumbnailFileMime = request.ThumbnailFile?.Mime,
|
||||
ThumbnailFileName = request.ThumbnailFile?.FileName
|
||||
};
|
||||
|
||||
await _sender.Send(command, context.CancellationToken);
|
||||
return new Google.Protobuf.WellKnownTypes.Empty();
|
||||
}
|
||||
|
||||
public override async Task<Google.Protobuf.WellKnownTypes.Empty> DeleteProducts(DeleteProductsRequest request, ServerCallContext context)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
|
||||
var command = new DeleteProductsCommand { Id = request.Id };
|
||||
await _sender.Send(command, context.CancellationToken);
|
||||
return new Google.Protobuf.WellKnownTypes.Empty();
|
||||
}
|
||||
|
||||
public override async Task<GetProductsResponse> GetProducts(GetProductsRequest request, ServerCallContext context)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
|
||||
var query = new GetCustomerProductsQuery { Id = request.Id };
|
||||
var result = await _sender.Send(query, context.CancellationToken);
|
||||
|
||||
var response = new GetProductsResponse
|
||||
{
|
||||
Id = result.Id,
|
||||
Title = result.Title,
|
||||
Description = result.Description,
|
||||
ShortInfomation = result.ShortInfomation,
|
||||
FullInformation = result.FullInformation,
|
||||
Price = result.Price,
|
||||
Discount = result.Discount,
|
||||
Rate = result.Rate,
|
||||
ImagePath = result.ImagePath,
|
||||
ThumbnailPath = result.ThumbnailPath,
|
||||
SaleCount = result.SaleCount,
|
||||
ViewCount = result.ViewCount,
|
||||
RemainingCount = result.RemainingCount
|
||||
};
|
||||
|
||||
if (result.Gallery != null)
|
||||
{
|
||||
foreach (var item in result.Gallery)
|
||||
{
|
||||
response.Gallery.Add(new ProductGalleryItem
|
||||
{
|
||||
ProductGalleryId = item.ProductGalleryId,
|
||||
ProductImageId = item.ProductImageId,
|
||||
Title = item.Title,
|
||||
ImagePath = item.ImagePath,
|
||||
ImageThumbnailPath = item.ImageThumbnailPath
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (result.Categories != null)
|
||||
{
|
||||
foreach (var cat in result.Categories)
|
||||
{
|
||||
var categoryPath = new ProductCategoryPath
|
||||
{
|
||||
CategoryId = cat.CategoryId,
|
||||
Title = cat.Title
|
||||
};
|
||||
if (cat.Path != null)
|
||||
{
|
||||
foreach (var node in cat.Path)
|
||||
{
|
||||
categoryPath.Path.Add(new CategoryNode
|
||||
{
|
||||
Id = node.Id,
|
||||
Title = node.Title,
|
||||
ParentId = node.ParentId
|
||||
});
|
||||
}
|
||||
}
|
||||
response.Categories.Add(categoryPath);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
|
||||
var response = new BulkUpdateProductPricesResponse { Total = request.Products.Count };
|
||||
|
||||
foreach (var item in request.Products)
|
||||
{
|
||||
try
|
||||
{
|
||||
var product = await _context.Products.FindAsync(new object[] { item.ProductId }, context.CancellationToken);
|
||||
if (product == null)
|
||||
{
|
||||
response.Failed++;
|
||||
response.Errors.Add(new BulkOperationError { ProductId = item.ProductId, ErrorMessage = "محصول یافت نشد" });
|
||||
continue;
|
||||
}
|
||||
|
||||
product.Price = item.NewPrice;
|
||||
if (item.NewDiscount != null) product.Discount = item.NewDiscount.Value;
|
||||
if (item.NewClubDiscountPercent != null) product.ClubDiscountPercent = item.NewClubDiscountPercent.Value;
|
||||
|
||||
response.Succeeded++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
response.Failed++;
|
||||
response.Errors.Add(new BulkOperationError { ProductId = item.ProductId, ErrorMessage = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
return response;
|
||||
}
|
||||
|
||||
public override async Task<BulkUpdateProductStockResponse> BulkUpdateProductStock(BulkUpdateProductStockRequest request, ServerCallContext context)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
|
||||
var response = new BulkUpdateProductStockResponse { Total = request.Products.Count };
|
||||
|
||||
foreach (var item in request.Products)
|
||||
{
|
||||
try
|
||||
{
|
||||
var product = await _context.Products.FindAsync(new object[] { item.ProductId }, context.CancellationToken);
|
||||
if (product == null)
|
||||
{
|
||||
response.Failed++;
|
||||
response.Errors.Add(new BulkOperationError { ProductId = item.ProductId, ErrorMessage = "محصول یافت نشد" });
|
||||
continue;
|
||||
}
|
||||
|
||||
product.RemainingCount = request.UpdateType switch
|
||||
{
|
||||
StockUpdateType.Set => item.Quantity,
|
||||
StockUpdateType.Add => product.RemainingCount + item.Quantity,
|
||||
StockUpdateType.Subtract => Math.Max(0, product.RemainingCount - item.Quantity),
|
||||
_ => item.Quantity
|
||||
};
|
||||
|
||||
response.Succeeded++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
response.Failed++;
|
||||
response.Errors.Add(new BulkOperationError { ProductId = item.ProductId, ErrorMessage = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
return response;
|
||||
}
|
||||
|
||||
public override async Task<GetLowStockProductsResponse> GetLowStockProducts(GetLowStockProductsRequest request, ServerCallContext context)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
|
||||
var threshold = request.Threshold > 0 ? request.Threshold : 10;
|
||||
var pageIndex = request.PageIndex > 0 ? request.PageIndex : 1;
|
||||
var pageSize = request.PageSize > 0 ? request.PageSize : 20;
|
||||
|
||||
var query = _context.Products
|
||||
.Where(p => !p.IsDeleted && p.RemainingCount <= threshold);
|
||||
|
||||
if (request.IsClubExclusive != null)
|
||||
query = query.Where(p => p.IsClubExclusive == request.IsClubExclusive.Value);
|
||||
|
||||
var totalCount = await query.CountAsync(context.CancellationToken);
|
||||
|
||||
var products = await query
|
||||
.OrderBy(p => p.RemainingCount)
|
||||
.Skip((pageIndex - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.Select(p => new LowStockProduct
|
||||
{
|
||||
Id = p.Id,
|
||||
Title = p.Title,
|
||||
RemainingCount = p.RemainingCount,
|
||||
Price = p.Price,
|
||||
IsClubExclusive = p.IsClubExclusive
|
||||
})
|
||||
.ToListAsync(context.CancellationToken);
|
||||
|
||||
return new GetLowStockProductsResponse
|
||||
{
|
||||
MetaData = new CMSMicroservice.Protobuf.Protos.MetaData
|
||||
{
|
||||
CurrentPage = pageIndex,
|
||||
PageSize = pageSize,
|
||||
TotalCount = totalCount,
|
||||
TotalPage = (int)Math.Ceiling(totalCount / (double)pageSize),
|
||||
HasPrevious = pageIndex > 1,
|
||||
HasNext = pageIndex * pageSize < totalCount
|
||||
},
|
||||
Products = { products }
|
||||
};
|
||||
}
|
||||
|
||||
public override async Task<ToggleProductStatusResponse> ToggleProductStatus(ToggleProductStatusRequest request, ServerCallContext context)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
|
||||
var response = new ToggleProductStatusResponse { Total = request.ProductIds.Count };
|
||||
|
||||
foreach (var productId in request.ProductIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
var product = await _context.Products.FindAsync(new object[] { productId }, context.CancellationToken);
|
||||
if (product == null)
|
||||
{
|
||||
response.Failed++;
|
||||
response.Errors.Add(new BulkOperationError { ProductId = productId, ErrorMessage = "محصول یافت نشد" });
|
||||
continue;
|
||||
}
|
||||
|
||||
product.IsDeleted = !request.Enable;
|
||||
if (request.Enable && request.DefaultStock > 0 && product.RemainingCount == 0)
|
||||
product.RemainingCount = request.DefaultStock;
|
||||
|
||||
response.Succeeded++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
response.Failed++;
|
||||
response.Errors.Add(new BulkOperationError { ProductId = productId, ErrorMessage = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
return response;
|
||||
}
|
||||
|
||||
// ============= Category-Product DragDrop Methods =============
|
||||
|
||||
public override async Task<GetProductsForCategoryResponse> GetProductsForCategory(GetProductsForCategoryRequest request, ServerCallContext context)
|
||||
{
|
||||
var assignedProductIds = await _context.ProductCategories
|
||||
.Where(pc => pc.CategoryId == request.CategoryId && !pc.IsDeleted)
|
||||
.Select(pc => pc.ProductId)
|
||||
.ToListAsync(context.CancellationToken);
|
||||
|
||||
var allProducts = await _context.Products
|
||||
.Where(p => !p.IsDeleted)
|
||||
.OrderBy(p => p.Title)
|
||||
.Select(p => new CategoryProductItem
|
||||
{
|
||||
Id = p.Id,
|
||||
Title = p.Title,
|
||||
Selected = assignedProductIds.Contains(p.Id)
|
||||
})
|
||||
.ToListAsync(context.CancellationToken);
|
||||
|
||||
return new GetProductsForCategoryResponse { Items = { allProducts } };
|
||||
}
|
||||
|
||||
public override async Task<GetCategoriesResponse> GetCategories(GetCategoriesRequest request, ServerCallContext context)
|
||||
{
|
||||
var assignedCategoryIds = await _context.ProductCategories
|
||||
.Where(pc => pc.ProductId == request.ProductId && !pc.IsDeleted)
|
||||
.Select(pc => pc.CategoryId)
|
||||
.ToListAsync(context.CancellationToken);
|
||||
|
||||
var allCategories = await _context.Categories
|
||||
.Where(c => !c.IsDeleted && c.IsActive)
|
||||
.OrderBy(c => c.SortOrder)
|
||||
.Select(c => new CategoryItem
|
||||
{
|
||||
Id = c.Id,
|
||||
Title = c.Title,
|
||||
Selected = assignedCategoryIds.Contains(c.Id)
|
||||
})
|
||||
.ToListAsync(context.CancellationToken);
|
||||
|
||||
return new GetCategoriesResponse { Items = { allCategories } };
|
||||
}
|
||||
|
||||
public override async Task<Google.Protobuf.WellKnownTypes.Empty> UpdateProductCategories(UpdateProductCategoriesRequest request, ServerCallContext context)
|
||||
{
|
||||
var existingLinks = await _context.ProductCategories
|
||||
.Where(pc => pc.ProductId == request.ProductId)
|
||||
.ToListAsync(context.CancellationToken);
|
||||
|
||||
// حذف لینکهای قبلی
|
||||
foreach (var link in existingLinks)
|
||||
link.IsDeleted = true;
|
||||
|
||||
// ایجاد لینکهای جدید
|
||||
foreach (var categoryId in request.CategoryIds)
|
||||
{
|
||||
var existing = existingLinks.FirstOrDefault(l => l.CategoryId == categoryId);
|
||||
if (existing != null)
|
||||
{
|
||||
existing.IsDeleted = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_context.ProductCategories.Add(new Domain.Entities.ProductCategory
|
||||
{
|
||||
ProductId = request.ProductId,
|
||||
CategoryId = categoryId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
return new Google.Protobuf.WellKnownTypes.Empty();
|
||||
}
|
||||
|
||||
public override async Task<Google.Protobuf.WellKnownTypes.Empty> UpdateCategoryProducts(UpdateCategoryProductsRequest request, ServerCallContext context)
|
||||
{
|
||||
var existingLinks = await _context.ProductCategories
|
||||
.Where(pc => pc.CategoryId == request.CategoryId)
|
||||
.ToListAsync(context.CancellationToken);
|
||||
|
||||
// حذف لینکهای قبلی
|
||||
foreach (var link in existingLinks)
|
||||
link.IsDeleted = true;
|
||||
|
||||
// ایجاد لینکهای جدید
|
||||
foreach (var productId in request.ProductIds)
|
||||
{
|
||||
var existing = existingLinks.FirstOrDefault(l => l.ProductId == productId);
|
||||
if (existing != null)
|
||||
{
|
||||
existing.IsDeleted = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_context.ProductCategories.Add(new Domain.Entities.ProductCategory
|
||||
{
|
||||
ProductId = productId,
|
||||
CategoryId = request.CategoryId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
return new Google.Protobuf.WellKnownTypes.Empty();
|
||||
}
|
||||
|
||||
// ============= Customer-specific Methods =============
|
||||
@@ -221,4 +634,56 @@ public class ProductsService : ProductsContract.ProductsContractBase
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// ============= Product Image Management =============
|
||||
|
||||
public override async Task<AddProductImageResponse> AddProductImage(AddProductImageRequest request, ServerCallContext context)
|
||||
{
|
||||
var command = new AddProductImageCommand
|
||||
{
|
||||
ProductId = request.ProductId,
|
||||
Title = request.Title,
|
||||
ImageFileBytes = request.ImageFile?.File?.ToByteArray(),
|
||||
ImageFileMime = request.ImageFile?.Mime,
|
||||
ImageFileName = request.ImageFile?.FileName
|
||||
};
|
||||
|
||||
var result = await _sender.Send(command, context.CancellationToken);
|
||||
|
||||
return new AddProductImageResponse
|
||||
{
|
||||
ProductGalleryId = result.ProductGalleryId,
|
||||
ProductImageId = result.ProductImageId,
|
||||
Title = result.Title,
|
||||
ImagePath = result.ImagePath,
|
||||
ImageThumbnailPath = result.ImageThumbnailPath
|
||||
};
|
||||
}
|
||||
|
||||
public override async Task<Google.Protobuf.WellKnownTypes.Empty> RemoveProductImage(RemoveProductImageRequest request, ServerCallContext context)
|
||||
{
|
||||
var command = new RemoveProductImageCommand { ProductGalleryId = request.ProductGalleryId };
|
||||
await _sender.Send(command, context.CancellationToken);
|
||||
return new Google.Protobuf.WellKnownTypes.Empty();
|
||||
}
|
||||
|
||||
public override async Task<GetProductGalleryResponse> GetProductGallery(GetProductGalleryRequest request, ServerCallContext context)
|
||||
{
|
||||
var query = new GetProductGalleryQuery { ProductId = request.ProductId };
|
||||
var result = await _sender.Send(query, context.CancellationToken);
|
||||
|
||||
var response = new GetProductGalleryResponse();
|
||||
foreach (var item in result.Items)
|
||||
{
|
||||
response.Items.Add(new ProductGalleryItem
|
||||
{
|
||||
ProductGalleryId = item.ProductGalleryId,
|
||||
ProductImageId = item.ProductImageId,
|
||||
Title = item.Title,
|
||||
ImagePath = item.ImagePath,
|
||||
ImageThumbnailPath = item.ImageThumbnailPath
|
||||
});
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,20 +9,35 @@ using CMSMicroservice.Application.TransactionsCQ.Commands.VerifyTransaction;
|
||||
using CMSMicroservice.Application.TransactionsCQ.Commands.RefundTransaction;
|
||||
using CMSMicroservice.Application.TransactionsCQ.Queries.GetCustomerTransaction;
|
||||
using CMSMicroservice.Application.TransactionsCQ.Queries.GetCustomerTransactionsByFilter;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using AppModels = CMSMicroservice.Application.Common.Models;
|
||||
using Grpc.Core;
|
||||
using MediatR;
|
||||
using Mapster;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Linq;
|
||||
|
||||
namespace CMSMicroservice.WebApi.Services;
|
||||
public class TransactionsService : TransactionsContract.TransactionsContractBase
|
||||
{
|
||||
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
|
||||
private readonly ISender _sender;
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
private readonly IPaymentGatewayService _paymentGateway;
|
||||
|
||||
public TransactionsService(IDispatchRequestToCQRS dispatchRequestToCQRS, ISender sender)
|
||||
public TransactionsService(
|
||||
IDispatchRequestToCQRS dispatchRequestToCQRS,
|
||||
ISender sender,
|
||||
IApplicationDbContext context,
|
||||
ICurrentUserService currentUserService,
|
||||
IPaymentGatewayService paymentGateway)
|
||||
{
|
||||
_dispatchRequestToCQRS = dispatchRequestToCQRS;
|
||||
_sender = sender;
|
||||
_context = context;
|
||||
_currentUserService = currentUserService;
|
||||
_paymentGateway = paymentGateway;
|
||||
}
|
||||
public override async Task<CreateNewTransactionsResponse> CreateNewTransactions(CreateNewTransactionsRequest request, ServerCallContext context)
|
||||
{
|
||||
@@ -121,26 +136,114 @@ public class TransactionsService : TransactionsContract.TransactionsContractBase
|
||||
|
||||
public override async Task<CustomerPaymentRequestResponse> CustomerPaymentRequest(CustomerPaymentRequestRequest request, ServerCallContext context)
|
||||
{
|
||||
// Mock payment gateway response
|
||||
var userId = GetCurrentUserId();
|
||||
|
||||
// Get user mobile for payment gateway
|
||||
var user = await _context.Users
|
||||
.AsNoTracking()
|
||||
.Where(u => u.Id == userId)
|
||||
.Select(u => new { u.Mobile, u.Email })
|
||||
.FirstOrDefaultAsync(context.CancellationToken);
|
||||
|
||||
// Create transaction record in DB
|
||||
var transaction = new CMSMicroservice.Domain.Entities.Transaction
|
||||
{
|
||||
Amount = request.Amount,
|
||||
Description = request.Description ?? "پرداخت آنلاین",
|
||||
PaymentStatus = Domain.Enums.PaymentStatus.Pending,
|
||||
Type = Domain.Enums.TransactionType.DepositIpg
|
||||
};
|
||||
|
||||
_context.Transactions.Add(transaction);
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
|
||||
// Initiate payment with gateway
|
||||
var paymentResult = await _paymentGateway.InitiatePaymentAsync(new PaymentRequest
|
||||
{
|
||||
Amount = request.Amount,
|
||||
UserId = userId,
|
||||
Mobile = request.Mobile ?? user?.Mobile ?? string.Empty,
|
||||
Description = request.Description ?? "پرداخت آنلاین",
|
||||
CallbackUrl = request.CallbackUrl
|
||||
}, context.CancellationToken);
|
||||
|
||||
if (!paymentResult.IsSuccess)
|
||||
{
|
||||
// Update transaction status to failed
|
||||
transaction.PaymentStatus = Domain.Enums.PaymentStatus.Reject;
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
|
||||
throw new RpcException(new Status(StatusCode.Internal,
|
||||
paymentResult.ErrorMessage ?? "خطا در ارتباط با درگاه پرداخت"));
|
||||
}
|
||||
|
||||
// Save RefId from gateway
|
||||
transaction.RefId = paymentResult.RefId;
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
|
||||
return new CustomerPaymentRequestResponse
|
||||
{
|
||||
PaymentGWUrl = $"https://payment.gateway.com/payment?amount={request.Amount}&callback={request.CallbackUrl}&description={request.Description}"
|
||||
PaymentGWUrl = paymentResult.GatewayUrl ?? string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
public override async Task<CustomerPaymentVerificationResponse> CustomerPaymentVerification(CustomerPaymentVerificationRequest request, ServerCallContext context)
|
||||
{
|
||||
// Mock payment verification response
|
||||
bool isSuccessful = request.Status == "OK";
|
||||
// Find the transaction by authority/refId
|
||||
var transaction = await _context.Transactions
|
||||
.Where(t => t.RefId == request.Authority && !t.IsDeleted)
|
||||
.FirstOrDefaultAsync(context.CancellationToken);
|
||||
|
||||
if (transaction == null)
|
||||
throw new RpcException(new Status(StatusCode.NotFound, "تراکنش یافت نشد"));
|
||||
|
||||
// If status from gateway callback is not OK, mark as rejected
|
||||
if (request.Status != "OK")
|
||||
{
|
||||
transaction.PaymentStatus = Domain.Enums.PaymentStatus.Reject;
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
|
||||
return new CustomerPaymentVerificationResponse
|
||||
{
|
||||
Id = 12345,
|
||||
PaymentStatus = isSuccessful,
|
||||
Message = isSuccessful ? "پرداخت با موفقیت انجام شد" : "پرداخت ناموفق",
|
||||
RefId = isSuccessful ? "REF123456789" : null,
|
||||
OrderId = "ORDER001",
|
||||
VerificationStatusCode = isSuccessful ? 101 : 102
|
||||
Id = transaction.Id,
|
||||
PaymentStatus = false,
|
||||
Message = "پرداخت توسط کاربر لغو شد",
|
||||
VerificationStatusCode = -1
|
||||
};
|
||||
}
|
||||
|
||||
// Verify with gateway
|
||||
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
|
||||
request.Authority, request.Status, context.CancellationToken);
|
||||
|
||||
if (verifyResult.IsSuccess)
|
||||
{
|
||||
transaction.PaymentStatus = Domain.Enums.PaymentStatus.Success;
|
||||
transaction.PaymentDate = DateTime.UtcNow;
|
||||
transaction.RefId = verifyResult.RefId;
|
||||
}
|
||||
else
|
||||
{
|
||||
transaction.PaymentStatus = Domain.Enums.PaymentStatus.Reject;
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
|
||||
return new CustomerPaymentVerificationResponse
|
||||
{
|
||||
Id = transaction.Id,
|
||||
PaymentStatus = verifyResult.IsSuccess,
|
||||
Message = verifyResult.IsSuccess ? "پرداخت با موفقیت انجام شد" : (verifyResult.Message ?? "پرداخت ناموفق"),
|
||||
RefId = verifyResult.RefId ?? string.Empty,
|
||||
OrderId = string.Empty,
|
||||
VerificationStatusCode = verifyResult.IsSuccess ? 100 : -1
|
||||
};
|
||||
}
|
||||
|
||||
private long GetCurrentUserId()
|
||||
{
|
||||
if (long.TryParse(_currentUserService.UserId, out var userId) && userId > 0)
|
||||
return userId;
|
||||
throw new RpcException(new Status(StatusCode.Unauthenticated, "لطفاً وارد حساب کاربری خود شوید"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,13 @@ using CMSMicroservice.Application.DiscountShopCQ.Commands.AddToCustomerCart;
|
||||
using CMSMicroservice.Application.DiscountShopCQ.Commands.RemoveFromCustomerCart;
|
||||
using CMSMicroservice.Application.DiscountShopCQ.Commands.UpdateCustomerCartItem;
|
||||
using CMSMicroservice.Application.DiscountShopCQ.Queries.GetCustomerCart;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Protobuf.Protos.UserCarts;
|
||||
using CMSMicroservice.WebApi.Common.Services;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MediatR;
|
||||
using System.Linq;
|
||||
|
||||
namespace CMSMicroservice.WebApi.Services;
|
||||
|
||||
@@ -13,11 +16,13 @@ public class UserCartsService : UserCartsContract.UserCartsContractBase
|
||||
{
|
||||
private readonly IDispatchRequestToCQRS _dispatcher;
|
||||
private readonly ISender _sender;
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public UserCartsService(IDispatchRequestToCQRS dispatcher, ISender sender)
|
||||
public UserCartsService(IDispatchRequestToCQRS dispatcher, ISender sender, IApplicationDbContext context)
|
||||
{
|
||||
_dispatcher = dispatcher;
|
||||
_sender = sender;
|
||||
_context = context;
|
||||
}
|
||||
|
||||
#region Customer Methods
|
||||
@@ -117,31 +122,116 @@ public class UserCartsService : UserCartsContract.UserCartsContractBase
|
||||
public override async Task<AddNewUserCartResponse> AddNewUserCart(
|
||||
AddNewUserCartRequest request, ServerCallContext context)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "AddNewUserCart not implemented yet"));
|
||||
var entity = new Domain.Entities.UserCart
|
||||
{
|
||||
ProductId = request.ProductId,
|
||||
UserId = request.UserId,
|
||||
Count = request.Count
|
||||
};
|
||||
|
||||
_context.UserCarts.Add(entity);
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
|
||||
return new AddNewUserCartResponse { Id = entity.Id };
|
||||
}
|
||||
|
||||
public override async Task<Empty> UpdateUserCart(
|
||||
UpdateUserCartRequest request, ServerCallContext context)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "UpdateUserCart not implemented yet"));
|
||||
var cartId = request.Id > 0 ? request.Id : request.UserCartId;
|
||||
var cart = await _context.UserCarts.FindAsync(new object[] { cartId }, context.CancellationToken);
|
||||
if (cart == null)
|
||||
throw new RpcException(new Status(StatusCode.NotFound, "آیتم سبد خرید یافت نشد"));
|
||||
|
||||
cart.Count = request.Count;
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
return new Empty();
|
||||
}
|
||||
|
||||
public override async Task<Empty> DeleteUserCart(
|
||||
DeleteUserCartRequest request, ServerCallContext context)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "DeleteUserCart not implemented yet"));
|
||||
var cart = await _context.UserCarts.FindAsync(new object[] { request.Id }, context.CancellationToken);
|
||||
if (cart == null)
|
||||
throw new RpcException(new Status(StatusCode.NotFound, "آیتم سبد خرید یافت نشد"));
|
||||
|
||||
cart.IsDeleted = true;
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
return new Empty();
|
||||
}
|
||||
|
||||
public override async Task<GetUserCartResponse> GetUserCart(
|
||||
GetUserCartRequest request, ServerCallContext context)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "GetUserCart not implemented yet"));
|
||||
var cart = await _context.UserCarts
|
||||
.Where(c => c.Id == request.Id && !c.IsDeleted)
|
||||
.FirstOrDefaultAsync(context.CancellationToken);
|
||||
|
||||
if (cart == null)
|
||||
throw new RpcException(new Status(StatusCode.NotFound, "آیتم سبد خرید یافت نشد"));
|
||||
|
||||
return new GetUserCartResponse
|
||||
{
|
||||
Id = cart.Id,
|
||||
ProductId = cart.ProductId,
|
||||
UserId = cart.UserId,
|
||||
Count = cart.Count
|
||||
};
|
||||
}
|
||||
|
||||
public override async Task<GetAllUserCartsByFilterResponse> GetAllUserCartsByFilter(
|
||||
GetAllUserCartsByFilterRequest request, ServerCallContext context)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "GetAllUserCartsByFilter not implemented yet"));
|
||||
var pageNumber = request.PaginationState?.PageNumber ?? 1;
|
||||
var pageSize = request.PaginationState?.PageSize ?? 20;
|
||||
if (pageNumber < 1) pageNumber = 1;
|
||||
if (pageSize < 1) pageSize = 20;
|
||||
|
||||
var query = _context.UserCarts
|
||||
.Include(c => c.Product)
|
||||
.Where(c => !c.IsDeleted);
|
||||
|
||||
if (request.Filter?.Id != null)
|
||||
query = query.Where(c => c.Id == request.Filter.Id.Value);
|
||||
if (request.Filter?.ProductId != null)
|
||||
query = query.Where(c => c.ProductId == request.Filter.ProductId.Value);
|
||||
if (request.Filter?.UserId != null)
|
||||
query = query.Where(c => c.UserId == request.Filter.UserId.Value);
|
||||
|
||||
var totalCount = await query.CountAsync(context.CancellationToken);
|
||||
|
||||
var items = await query
|
||||
.OrderByDescending(c => c.Created)
|
||||
.Skip((pageNumber - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.Select(c => new GetAllUserCartsByFilterResponseModel
|
||||
{
|
||||
Id = c.Id,
|
||||
ProductId = c.ProductId,
|
||||
UserId = c.UserId,
|
||||
Count = c.Count,
|
||||
ProductTitle = c.Product != null ? c.Product.Title : string.Empty,
|
||||
ProductShortInfomation = c.Product != null ? (c.Product.ShortInfomation ?? string.Empty) : string.Empty,
|
||||
ProductPrice = c.Product != null ? c.Product.Price : 0,
|
||||
ProductDiscount = c.Product != null ? c.Product.Discount : 0,
|
||||
ProductThumbnailPath = c.Product != null ? (c.Product.ThumbnailPath ?? string.Empty) : string.Empty,
|
||||
Created = Timestamp.FromDateTime(DateTime.SpecifyKind(c.Created, DateTimeKind.Utc))
|
||||
})
|
||||
.ToListAsync(context.CancellationToken);
|
||||
|
||||
return new GetAllUserCartsByFilterResponse
|
||||
{
|
||||
MetaData = new CMSMicroservice.Protobuf.Protos.MetaData
|
||||
{
|
||||
CurrentPage = pageNumber,
|
||||
PageSize = pageSize,
|
||||
TotalCount = totalCount,
|
||||
TotalPage = (int)Math.Ceiling(totalCount / (double)pageSize),
|
||||
HasPrevious = pageNumber > 1,
|
||||
HasNext = pageNumber * pageSize < totalCount
|
||||
},
|
||||
Models = { items }
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,78 +1,569 @@
|
||||
using CMSMicroservice.Application.Common.Authorization;
|
||||
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.OrderManagementCQ.Commands.UpdateOrderStatus;
|
||||
using CMSMicroservice.Application.OrderManagementCQ.Commands.CancelOrderByAdmin;
|
||||
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;
|
||||
using System.Linq;
|
||||
|
||||
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;
|
||||
}
|
||||
[RequiresPermission(PermissionNames.OrdersCreate)]
|
||||
public override async Task<CreateNewUserOrderResponse> CreateNewUserOrder(CreateNewUserOrderRequest request, ServerCallContext context)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
|
||||
var order = new UserOrder
|
||||
{
|
||||
Amount = request.Amount,
|
||||
PackageId = request.PackageId > 0 ? request.PackageId : null,
|
||||
TransactionId = request.TransactionId,
|
||||
PaymentStatus = request.HasPaymentStatus
|
||||
? (Domain.Enums.PaymentStatus)(int)request.PaymentStatus
|
||||
: Domain.Enums.PaymentStatus.Pending,
|
||||
PaymentDate = request.PaymentDate?.ToDateTime(),
|
||||
UserId = request.UserId,
|
||||
UserAddressId = request.UserAddressId,
|
||||
PaymentMethod = request.HasPaymentMethod
|
||||
? (Domain.Enums.PaymentMethod)(int)request.PaymentMethod
|
||||
: null,
|
||||
DeliveryStatus = Domain.Enums.DeliveryStatus.Pending
|
||||
};
|
||||
|
||||
_context.UserOrders.Add(order);
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
|
||||
return new CreateNewUserOrderResponse { Id = order.Id };
|
||||
}
|
||||
|
||||
[RequiresPermission(PermissionNames.OrdersUpdate)]
|
||||
public override async Task<Google.Protobuf.WellKnownTypes.Empty> UpdateUserOrder(UpdateUserOrderRequest request, ServerCallContext context)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
|
||||
var order = await _context.UserOrders.FindAsync(new object[] { request.Id }, context.CancellationToken);
|
||||
if (order == null)
|
||||
throw new RpcException(new Status(StatusCode.NotFound, "سفارش یافت نشد"));
|
||||
|
||||
if (request.Amount != null) order.Amount = request.Amount.Value;
|
||||
if (request.PackageId != null) order.PackageId = request.PackageId.Value;
|
||||
if (request.TransactionId != null) order.TransactionId = request.TransactionId.Value;
|
||||
if (request.HasPaymentStatus) order.PaymentStatus = (Domain.Enums.PaymentStatus)(int)request.PaymentStatus;
|
||||
if (request.PaymentDate != null) order.PaymentDate = request.PaymentDate.ToDateTime();
|
||||
if (request.UserId != null) order.UserId = request.UserId.Value;
|
||||
if (request.UserAddressId != null) order.UserAddressId = request.UserAddressId.Value;
|
||||
if (request.HasPaymentMethod) order.PaymentMethod = (Domain.Enums.PaymentMethod)(int)request.PaymentMethod;
|
||||
if (request.HasDeliveryStatus) order.DeliveryStatus = (Domain.Enums.DeliveryStatus)(int)request.DeliveryStatus;
|
||||
if (request.TrackingCode != null) order.TrackingCode = request.TrackingCode;
|
||||
if (request.DeliveryDescription != null) order.DeliveryDescription = request.DeliveryDescription;
|
||||
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
return new Google.Protobuf.WellKnownTypes.Empty();
|
||||
}
|
||||
|
||||
[RequiresPermission(PermissionNames.OrdersDelete)]
|
||||
public override async Task<Google.Protobuf.WellKnownTypes.Empty> DeleteUserOrder(DeleteUserOrderRequest request, ServerCallContext context)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
|
||||
var order = await _context.UserOrders.FindAsync(new object[] { request.Id }, context.CancellationToken);
|
||||
if (order == null)
|
||||
throw new RpcException(new Status(StatusCode.NotFound, "سفارش یافت نشد"));
|
||||
|
||||
order.IsDeleted = true;
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
return new Google.Protobuf.WellKnownTypes.Empty();
|
||||
}
|
||||
|
||||
[RequiresPermission(PermissionNames.OrdersView)]
|
||||
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;
|
||||
}
|
||||
|
||||
[RequiresPermission(PermissionNames.OrdersView)]
|
||||
public override async Task<GetAllUserOrderByFilterResponse> GetAllUserOrderByFilter(GetAllUserOrderByFilterRequest request, ServerCallContext context)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
|
||||
// Admin API - can view all orders or filter by specific user
|
||||
var query = new GetCustomerOrdersQuery
|
||||
{
|
||||
UserId = request.Filter?.UserId ?? 0, // 0 means all users (admin view)
|
||||
PaginationState = request.PaginationState?.Adapt<AppModels.PaginationState>(),
|
||||
PaymentStatusFilter = request.Filter?.PaymentStatus != null
|
||||
? (int?)request.Filter.PaymentStatus
|
||||
: null,
|
||||
DeliveryStatusFilter = request.Filter?.DeliveryStatus != null
|
||||
? (int?)request.Filter.DeliveryStatus
|
||||
: null,
|
||||
FromDate = request.Filter?.PaymentDate?.ToDateTime(),
|
||||
ToDate = null
|
||||
};
|
||||
|
||||
var result = await _sender.Send(query, context.CancellationToken);
|
||||
|
||||
var response = new GetAllUserOrderByFilterResponse
|
||||
{
|
||||
MetaData = result.MetaData.Adapt<MetaData>()
|
||||
};
|
||||
|
||||
foreach (var model in result.Models)
|
||||
{
|
||||
var orderModel = new GetAllUserOrderByFilterResponseModel
|
||||
{
|
||||
Id = model.Id,
|
||||
Amount = model.Amount,
|
||||
PackageId = model.PackageId ?? 0,
|
||||
TransactionId = model.TransactionId,
|
||||
UserId = model.UserId,
|
||||
UserAddressId = model.UserAddressId,
|
||||
UserAddressText = model.UserAddressText,
|
||||
TrackingCode = model.TrackingCode,
|
||||
DeliveryDescription = model.DeliveryDescription,
|
||||
UserFullName = model.UserFullName,
|
||||
UserNationalCode = model.UserNationalCode,
|
||||
VatAmount = model.VatAmount,
|
||||
VatPercentage = model.VatPercentage
|
||||
};
|
||||
|
||||
orderModel.PaymentStatus = (CMSMicroservice.Protobuf.Protos.PaymentStatus)model.PaymentStatus;
|
||||
if (model.PaymentDate.HasValue)
|
||||
orderModel.PaymentDate = Timestamp.FromDateTime(DateTime.SpecifyKind(model.PaymentDate.Value, DateTimeKind.Utc));
|
||||
|
||||
if (model.PaymentMethod.HasValue)
|
||||
orderModel.PaymentMethod = (CMSMicroservice.Protobuf.Protos.PaymentMethod)model.PaymentMethod.Value;
|
||||
|
||||
orderModel.DeliveryStatus = (CMSMicroservice.Protobuf.Protos.DeliveryStatus)model.DeliveryStatus;
|
||||
|
||||
foreach (var fd in model.FactorDetails)
|
||||
{
|
||||
orderModel.FactorDetails.Add(new GetAllUserOrderByFilterResponseModelFactorDetail
|
||||
{
|
||||
ProductId = fd.ProductId,
|
||||
ProductTitle = fd.ProductTitle,
|
||||
ProductThumbnailPath = fd.ProductThumbnailPath,
|
||||
UnitPrice = fd.UnitPrice,
|
||||
Count = fd.Count,
|
||||
UnitDiscountPrice = fd.UnitDiscountPrice
|
||||
});
|
||||
}
|
||||
|
||||
response.Models.Add(orderModel);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public override async Task<SubmitShopBuyOrderResponse> SubmitShopBuyOrder(SubmitShopBuyOrderRequest request, ServerCallContext context)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
|
||||
// Get UserId from JWT or request
|
||||
var userId = !string.IsNullOrEmpty(_currentUserService.UserId)
|
||||
? long.Parse(_currentUserService.UserId)
|
||||
: request.UserId;
|
||||
|
||||
if (userId == 0)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unauthenticated, "User not authenticated"));
|
||||
}
|
||||
|
||||
// Get user's cart items with product details
|
||||
var cartItems = await _context.UserCarts
|
||||
.Include(c => c.Product)
|
||||
.Where(c => c.UserId == userId && !c.IsDeleted)
|
||||
.ToListAsync(context.CancellationToken);
|
||||
|
||||
if (!cartItems.Any())
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.FailedPrecondition, "سبد خرید خالی است"));
|
||||
}
|
||||
|
||||
// Get user's default address
|
||||
var defaultAddress = await _context.UserAddresses
|
||||
.Where(a => a.UserId == userId && a.IsDefault && !a.IsDeleted)
|
||||
.FirstOrDefaultAsync(context.CancellationToken);
|
||||
|
||||
if (defaultAddress == null)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.FailedPrecondition, "آدرس پیشفرض یافت نشد"));
|
||||
}
|
||||
|
||||
// Calculate amounts
|
||||
const decimal vatRate = 0.09m;
|
||||
long baseAmount = cartItems.Sum(c => c.Product.Price * c.Count);
|
||||
long vatAmount = (long)(baseAmount * vatRate);
|
||||
long totalAmount = baseAmount + vatAmount;
|
||||
|
||||
// Validate total amount
|
||||
if (request.TotalAmount > 0 && Math.Abs(totalAmount - request.TotalAmount) > 100)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.InvalidArgument,
|
||||
$"مبلغ نامعتبر است. محاسبه شده: {totalAmount}, دریافتی: {request.TotalAmount}"));
|
||||
}
|
||||
|
||||
// Get user's wallet
|
||||
var wallet = await _context.UserWallets
|
||||
.Where(w => w.UserId == userId)
|
||||
.FirstOrDefaultAsync(context.CancellationToken);
|
||||
|
||||
if (wallet == null)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.FailedPrecondition, "کیف پول یافت نشد"));
|
||||
}
|
||||
|
||||
// Check wallet balance
|
||||
if (wallet.Balance < totalAmount)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.FailedPrecondition,
|
||||
$"موجودی کیف پول کافی نیست. موجودی: {wallet.Balance:N0} تومان، مورد نیاز: {totalAmount:N0} تومان"));
|
||||
}
|
||||
|
||||
// Create transaction
|
||||
var transaction = new Transaction
|
||||
{
|
||||
Amount = totalAmount,
|
||||
Description = $"خرید محصولات - سفارش شماره در حال ایجاد",
|
||||
PaymentStatus = CMSMicroservice.Domain.Enums.PaymentStatus.Success,
|
||||
PaymentDate = DateTime.UtcNow,
|
||||
Type = CMSMicroservice.Domain.Enums.TransactionType.Buy,
|
||||
RefId = $"SHOP_{DateTime.UtcNow.Ticks}"
|
||||
};
|
||||
|
||||
_context.Transactions.Add(transaction);
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
|
||||
// Deduct from wallet
|
||||
var oldBalance = wallet.Balance;
|
||||
wallet.Balance -= totalAmount;
|
||||
|
||||
// Create wallet change log
|
||||
var walletLog = new UserWalletChangeLog
|
||||
{
|
||||
WalletId = wallet.Id,
|
||||
CurrentBalance = wallet.Balance,
|
||||
ChangeValue = -totalAmount,
|
||||
CurrentNetworkBalance = wallet.NetworkBalance,
|
||||
ChangeNerworkValue = 0,
|
||||
CurrentDiscountBalance = wallet.DiscountBalance,
|
||||
ChangeDiscountValue = 0,
|
||||
IsIncrease = false,
|
||||
RefrenceId = transaction.Id
|
||||
};
|
||||
|
||||
_context.UserWalletChangeLogs.Add(walletLog);
|
||||
|
||||
// Create order
|
||||
var order = new UserOrder
|
||||
{
|
||||
UserId = userId,
|
||||
UserAddressId = defaultAddress.Id,
|
||||
Amount = totalAmount,
|
||||
TransactionId = transaction.Id,
|
||||
PaymentStatus = CMSMicroservice.Domain.Enums.PaymentStatus.Success,
|
||||
PaymentDate = DateTime.UtcNow,
|
||||
PaymentMethod = CMSMicroservice.Domain.Enums.PaymentMethod.Wallet,
|
||||
DeliveryStatus = CMSMicroservice.Domain.Enums.DeliveryStatus.Pending,
|
||||
HasVAT = true
|
||||
};
|
||||
|
||||
_context.UserOrders.Add(order);
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
|
||||
// Update transaction description with order ID
|
||||
transaction.Description = $"خرید محصولات - سفارش #{order.Id}";
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
|
||||
// Create order VAT record
|
||||
var orderVat = new OrderVAT
|
||||
{
|
||||
OrderId = order.Id,
|
||||
VATRate = vatRate,
|
||||
BaseAmount = baseAmount,
|
||||
VATAmount = vatAmount,
|
||||
TotalAmount = totalAmount
|
||||
};
|
||||
|
||||
_context.OrderVATs.Add(orderVat);
|
||||
|
||||
// Create factor details for each cart item
|
||||
foreach (var cartItem in cartItems)
|
||||
{
|
||||
var factorDetail = new FactorDetails
|
||||
{
|
||||
OrderId = order.Id,
|
||||
ProductId = cartItem.ProductId,
|
||||
Count = cartItem.Count,
|
||||
UnitPrice = cartItem.Product.Price,
|
||||
UnitDiscount = 0,
|
||||
UnitDiscountPrice = cartItem.Product.Price,
|
||||
IsChangePrice = false
|
||||
};
|
||||
|
||||
_context.FactorDetails.Add(factorDetail);
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
|
||||
// Clear user's cart
|
||||
foreach (var cartItem in cartItems)
|
||||
{
|
||||
cartItem.IsDeleted = true;
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
|
||||
return new SubmitShopBuyOrderResponse
|
||||
{
|
||||
Id = order.Id
|
||||
};
|
||||
}
|
||||
|
||||
[RequiresPermission(PermissionNames.OrdersCancel)]
|
||||
public override async Task<CancelOrderResponse> CancelOrder(CancelOrderRequest request, ServerCallContext context)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
|
||||
var command = new CancelOrderByAdminCommand
|
||||
{
|
||||
OrderId = request.OrderId,
|
||||
CancelReason = request.CancelReason,
|
||||
RefundToWallet = request.RefundPayment
|
||||
};
|
||||
|
||||
await _sender.Send(command, context.CancellationToken);
|
||||
|
||||
return new CancelOrderResponse
|
||||
{
|
||||
OrderId = request.OrderId,
|
||||
Status = (CMSMicroservice.Protobuf.Protos.DeliveryStatus)(int)Domain.Enums.DeliveryStatus.Cancelled,
|
||||
Message = "سفارش با موفقیت لغو شد",
|
||||
RefundProcessed = request.RefundPayment
|
||||
};
|
||||
}
|
||||
|
||||
[RequiresPermission(PermissionNames.OrdersUpdate)]
|
||||
public override async Task<UpdateOrderStatusResponse> UpdateOrderStatus(UpdateOrderStatusRequest request, ServerCallContext context)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
|
||||
var order = await _context.UserOrders.FindAsync(new object[] { request.OrderId }, context.CancellationToken);
|
||||
if (order == null)
|
||||
throw new RpcException(new Status(StatusCode.NotFound, "سفارش یافت نشد"));
|
||||
|
||||
var oldStatus = (int)order.DeliveryStatus;
|
||||
|
||||
var command = new Application.OrderManagementCQ.Commands.UpdateOrderStatus.UpdateOrderStatusCommand
|
||||
{
|
||||
OrderId = request.OrderId,
|
||||
NewStatus = (Domain.Enums.DeliveryStatus)request.NewStatus
|
||||
};
|
||||
|
||||
await _sender.Send(command, context.CancellationToken);
|
||||
|
||||
return new UpdateOrderStatusResponse
|
||||
{
|
||||
Success = true,
|
||||
Message = "وضعیت سفارش با موفقیت تغییر کرد",
|
||||
OldStatus = oldStatus,
|
||||
NewStatus = request.NewStatus
|
||||
};
|
||||
}
|
||||
|
||||
[RequiresPermission(PermissionNames.ReportsView)]
|
||||
public override async Task<GetOrdersByDateRangeResponse> GetOrdersByDateRange(GetOrdersByDateRangeRequest request, ServerCallContext context)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
|
||||
var pageNumber = request.PageNumber > 0 ? request.PageNumber : 1;
|
||||
var pageSize = request.PageSize > 0 ? request.PageSize : 20;
|
||||
|
||||
var query = _context.UserOrders
|
||||
.Include(o => o.User)
|
||||
.Include(o => o.FactorDetails)
|
||||
.Where(o => !o.IsDeleted);
|
||||
|
||||
if (request.StartDate != null)
|
||||
query = query.Where(o => o.Created >= request.StartDate.ToDateTime());
|
||||
if (request.EndDate != null)
|
||||
query = query.Where(o => o.Created <= request.EndDate.ToDateTime());
|
||||
if (request.Status != null)
|
||||
query = query.Where(o => (int)o.DeliveryStatus == request.Status.Value);
|
||||
if (request.UserId != null)
|
||||
query = query.Where(o => o.UserId == request.UserId.Value);
|
||||
|
||||
var totalCount = await query.CountAsync(context.CancellationToken);
|
||||
|
||||
var orders = await query
|
||||
.OrderByDescending(o => o.Created)
|
||||
.Skip((pageNumber - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.Select(o => new OrderSummaryDto
|
||||
{
|
||||
OrderId = o.Id,
|
||||
OrderNumber = $"ORD-{o.Id:D6}",
|
||||
UserId = o.UserId,
|
||||
UserFullName = o.User != null ? (o.User.FirstName + " " + o.User.LastName) : string.Empty,
|
||||
TotalAmount = o.Amount,
|
||||
Status = (int)o.DeliveryStatus,
|
||||
StatusName = o.DeliveryStatus.ToString(),
|
||||
ItemsCount = o.FactorDetails.Count,
|
||||
CreatedAt = Timestamp.FromDateTime(DateTime.SpecifyKind(o.Created, DateTimeKind.Utc))
|
||||
})
|
||||
.ToListAsync(context.CancellationToken);
|
||||
|
||||
return new GetOrdersByDateRangeResponse
|
||||
{
|
||||
MetaData = new CMSMicroservice.Protobuf.Protos.MetaData
|
||||
{
|
||||
CurrentPage = pageNumber,
|
||||
PageSize = pageSize,
|
||||
TotalCount = totalCount,
|
||||
TotalPage = (int)Math.Ceiling(totalCount / (double)pageSize),
|
||||
HasPrevious = pageNumber > 1,
|
||||
HasNext = pageNumber * pageSize < totalCount
|
||||
},
|
||||
Orders = { orders }
|
||||
};
|
||||
}
|
||||
|
||||
[RequiresPermission(PermissionNames.OrdersUpdate)]
|
||||
public override async Task<ApplyDiscountToOrderResponse> ApplyDiscountToOrder(ApplyDiscountToOrderRequest request, ServerCallContext context)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
|
||||
var order = await _context.UserOrders.FindAsync(new object[] { request.OrderId }, context.CancellationToken);
|
||||
if (order == null)
|
||||
throw new RpcException(new Status(StatusCode.NotFound, "سفارش یافت نشد"));
|
||||
|
||||
if (order.PaymentStatus == Domain.Enums.PaymentStatus.Success)
|
||||
return new ApplyDiscountToOrderResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "امکان اعمال تخفیف برای سفارش پرداخت شده وجود ندارد"
|
||||
};
|
||||
|
||||
var originalAmount = order.Amount;
|
||||
var discountAmount = Math.Min(request.DiscountAmount, originalAmount);
|
||||
order.Amount = originalAmount - discountAmount;
|
||||
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
|
||||
return new ApplyDiscountToOrderResponse
|
||||
{
|
||||
Success = true,
|
||||
Message = $"تخفیف {discountAmount:N0} تومان با موفقیت اعمال شد",
|
||||
OriginalAmount = originalAmount,
|
||||
DiscountAmount = discountAmount,
|
||||
FinalAmount = order.Amount
|
||||
};
|
||||
}
|
||||
|
||||
[RequiresPermission(PermissionNames.OrdersView)]
|
||||
public override async Task<CalculateOrderPVResponse> CalculateOrderPV(CalculateOrderPVRequest request, ServerCallContext context)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
|
||||
var order = await _context.UserOrders
|
||||
.Include(o => o.FactorDetails)
|
||||
.ThenInclude(fd => fd.Product)
|
||||
.Where(o => o.Id == request.OrderId && !o.IsDeleted)
|
||||
.FirstOrDefaultAsync(context.CancellationToken);
|
||||
|
||||
if (order == null)
|
||||
throw new RpcException(new Status(StatusCode.NotFound, "سفارش یافت نشد"));
|
||||
|
||||
var products = new List<ProductPVDto>();
|
||||
long totalPV = 0;
|
||||
|
||||
foreach (var fd in order.FactorDetails.Where(f => !f.IsDeleted))
|
||||
{
|
||||
// PV = UnitPrice * Count (simplified - can be customized)
|
||||
long unitPV = fd.UnitPrice;
|
||||
long itemPV = unitPV * fd.Count;
|
||||
totalPV += itemPV;
|
||||
|
||||
products.Add(new ProductPVDto
|
||||
{
|
||||
ProductId = fd.ProductId,
|
||||
ProductTitle = fd.Product?.Title ?? string.Empty,
|
||||
Quantity = fd.Count,
|
||||
UnitPv = unitPV,
|
||||
TotalPv = itemPV
|
||||
});
|
||||
}
|
||||
|
||||
return new CalculateOrderPVResponse
|
||||
{
|
||||
OrderId = request.OrderId,
|
||||
TotalPv = totalPV,
|
||||
Products = { products }
|
||||
};
|
||||
}
|
||||
|
||||
// ============= Customer-specific Methods =============
|
||||
@@ -131,14 +622,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 +688,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)
|
||||
{
|
||||
@@ -226,13 +717,53 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
|
||||
|
||||
public override async Task<CustomerCancelOrderResponse> CustomerCancelOrder(CustomerCancelOrderRequest request, ServerCallContext context)
|
||||
{
|
||||
// Mock Customer order cancellation with realistic Persian response
|
||||
var order = await _context.UserOrders
|
||||
.Where(o => o.Id == request.OrderId && !o.IsDeleted)
|
||||
.FirstOrDefaultAsync(context.CancellationToken);
|
||||
|
||||
if (order == null)
|
||||
return new CustomerCancelOrderResponse { Success = false, Message = "سفارش یافت نشد" };
|
||||
|
||||
if (order.DeliveryStatus != Domain.Enums.DeliveryStatus.Pending)
|
||||
return new CustomerCancelOrderResponse { Success = false, Message = "فقط سفارشهای در انتظار قابل لغو هستند" };
|
||||
|
||||
var refundAmount = order.Amount;
|
||||
|
||||
order.DeliveryStatus = Domain.Enums.DeliveryStatus.Cancelled;
|
||||
|
||||
// Refund to wallet if payment was from wallet
|
||||
if (order.PaymentStatus == Domain.Enums.PaymentStatus.Success && order.PaymentMethod == Domain.Enums.PaymentMethod.Wallet)
|
||||
{
|
||||
var wallet = await _context.UserWallets
|
||||
.Where(w => w.UserId == order.UserId)
|
||||
.FirstOrDefaultAsync(context.CancellationToken);
|
||||
|
||||
if (wallet != null)
|
||||
{
|
||||
wallet.Balance += refundAmount;
|
||||
_context.UserWalletChangeLogs.Add(new UserWalletChangeLog
|
||||
{
|
||||
WalletId = wallet.Id,
|
||||
CurrentBalance = wallet.Balance,
|
||||
ChangeValue = refundAmount,
|
||||
CurrentNetworkBalance = wallet.NetworkBalance,
|
||||
ChangeNerworkValue = 0,
|
||||
CurrentDiscountBalance = wallet.DiscountBalance,
|
||||
ChangeDiscountValue = 0,
|
||||
IsIncrease = true,
|
||||
RefrenceId = order.TransactionId ?? 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
|
||||
return new CustomerCancelOrderResponse
|
||||
{
|
||||
Success = true,
|
||||
Message = "سفارش شما با موفقیت لغو شد",
|
||||
RefundAmount = 180000,
|
||||
RefundTransactionId = "REF" + DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||
RefundAmount = refundAmount,
|
||||
RefundTransactionId = $"REF{order.Id}"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -282,106 +813,130 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
|
||||
|
||||
public override async Task<CustomerTrackOrderResponse> CustomerTrackOrder(CustomerTrackOrderRequest request, ServerCallContext context)
|
||||
{
|
||||
// Mock Customer order tracking with detailed Persian information
|
||||
var statusHistory = new List<OrderStatusHistory>
|
||||
var order = await _context.UserOrders
|
||||
.Include(o => o.FactorDetails)
|
||||
.Include(o => o.Package)
|
||||
.Where(o => o.Id == request.OrderId && !o.IsDeleted)
|
||||
.FirstOrDefaultAsync(context.CancellationToken);
|
||||
|
||||
if (order == null)
|
||||
throw new RpcException(new Status(StatusCode.NotFound, "سفارش یافت نشد"));
|
||||
|
||||
var orderModel = new Protobuf.Protos.UserOrder.CustomerOrderModel
|
||||
{
|
||||
new OrderStatusHistory
|
||||
{
|
||||
Status = OrderStatusEnum.OrderStatusPending,
|
||||
StatusMessage = "در انتظار تایید",
|
||||
ChangedAt = Timestamp.FromDateTime(DateTime.UtcNow.AddDays(-5)),
|
||||
ChangedBy = "سیستم"
|
||||
},
|
||||
new OrderStatusHistory
|
||||
{
|
||||
Status = OrderStatusEnum.OrderStatusConfirmed,
|
||||
StatusMessage = "تایید شده",
|
||||
ChangedAt = Timestamp.FromDateTime(DateTime.UtcNow.AddDays(-4)),
|
||||
ChangedBy = "کارشناس فروش"
|
||||
},
|
||||
new OrderStatusHistory
|
||||
{
|
||||
Status = OrderStatusEnum.OrderStatusProcessing,
|
||||
StatusMessage = "در حال آمادهسازی",
|
||||
ChangedAt = Timestamp.FromDateTime(DateTime.UtcNow.AddDays(-3)),
|
||||
ChangedBy = "انبار"
|
||||
},
|
||||
new OrderStatusHistory
|
||||
{
|
||||
Status = OrderStatusEnum.OrderStatusShipped,
|
||||
StatusMessage = "ارسال شده",
|
||||
ChangedAt = Timestamp.FromDateTime(DateTime.UtcNow.AddDays(-2)),
|
||||
ChangedBy = "پست پیشتاز"
|
||||
}
|
||||
Id = order.Id,
|
||||
Amount = order.Amount,
|
||||
PackageId = order.PackageId ?? 0,
|
||||
PackageName = order.Package?.Title ?? string.Empty,
|
||||
Status = (OrderStatusEnum)(int)order.DeliveryStatus,
|
||||
StatusMessage = GetDeliveryStatusPersian(order.DeliveryStatus),
|
||||
OrderDate = Timestamp.FromDateTime(DateTime.SpecifyKind(order.Created, DateTimeKind.Utc)),
|
||||
TrackingCode = order.TrackingCode ?? string.Empty,
|
||||
ItemsCount = order.FactorDetails.Count(f => !f.IsDeleted),
|
||||
CanCancel = order.DeliveryStatus == Domain.Enums.DeliveryStatus.Pending,
|
||||
CanReorder = order.DeliveryStatus == Domain.Enums.DeliveryStatus.Delivered
|
||||
};
|
||||
|
||||
var deliverySteps = new List<DeliveryStep>
|
||||
var response = new CustomerTrackOrderResponse
|
||||
{
|
||||
new DeliveryStep
|
||||
{
|
||||
StepName = "دریافت از فروشنده",
|
||||
StepDescription = "بسته از فروشنده دریافت شد",
|
||||
StepTime = Timestamp.FromDateTime(DateTime.UtcNow.AddDays(-2)),
|
||||
IsCompleted = true
|
||||
},
|
||||
new DeliveryStep
|
||||
{
|
||||
StepName = "مرکز پردازش تهران",
|
||||
StepDescription = "بسته در مرکز پردازش تهران",
|
||||
StepTime = Timestamp.FromDateTime(DateTime.UtcNow.AddDays(-1)),
|
||||
IsCompleted = true
|
||||
},
|
||||
new DeliveryStep
|
||||
{
|
||||
StepName = "در حال ارسال",
|
||||
StepDescription = "بسته در حال ارسال به آدرس مقصد",
|
||||
StepTime = Timestamp.FromDateTime(DateTime.UtcNow.AddHours(-8)),
|
||||
IsCompleted = false
|
||||
}
|
||||
};
|
||||
|
||||
return new CustomerTrackOrderResponse
|
||||
{
|
||||
Order = new Protobuf.Protos.UserOrder.CustomerOrderModel
|
||||
{
|
||||
Id = request.OrderId,
|
||||
Amount = 180000,
|
||||
PackageId = 1,
|
||||
PackageName = "پکیج ویژه",
|
||||
Status = OrderStatusEnum.OrderStatusShipped,
|
||||
StatusMessage = "در حال ارسال",
|
||||
OrderDate = Timestamp.FromDateTime(DateTime.UtcNow.AddDays(-5)),
|
||||
TrackingCode = "TRK" + request.OrderId.ToString("000"),
|
||||
ItemsCount = 4,
|
||||
CanCancel = false,
|
||||
CanReorder = true
|
||||
},
|
||||
StatusHistory = { statusHistory },
|
||||
Order = orderModel,
|
||||
DeliveryInfo = new DeliveryTrackingInfo
|
||||
{
|
||||
TrackingCode = "TRK" + request.OrderId.ToString("000"),
|
||||
TrackingCode = order.TrackingCode ?? string.Empty,
|
||||
CourierName = "پست پیشتاز",
|
||||
EstimatedDelivery = "فردا تا ساعت 18:00",
|
||||
CurrentLocation = "مرکز پخش منطقه 5 تهران",
|
||||
DeliverySteps = { deliverySteps }
|
||||
EstimatedDelivery = order.DeliveryDescription ?? string.Empty,
|
||||
CurrentLocation = string.Empty
|
||||
}
|
||||
};
|
||||
|
||||
// Add current status to history
|
||||
response.StatusHistory.Add(new OrderStatusHistory
|
||||
{
|
||||
Status = (OrderStatusEnum)(int)order.DeliveryStatus,
|
||||
StatusMessage = GetDeliveryStatusPersian(order.DeliveryStatus),
|
||||
ChangedAt = order.LastModified.HasValue
|
||||
? Timestamp.FromDateTime(DateTime.SpecifyKind(order.LastModified.Value, DateTimeKind.Utc))
|
||||
: Timestamp.FromDateTime(DateTime.SpecifyKind(order.Created, DateTimeKind.Utc)),
|
||||
ChangedBy = "سیستم"
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public override async Task<CustomerReorderResponse> CustomerReorderPreviousOrder(CustomerReorderRequest request, ServerCallContext context)
|
||||
{
|
||||
// Mock Customer reorder functionality
|
||||
var newOrderId = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var totalAmount = request.UseCurrentPrices ? 280000 : 250000;
|
||||
var originalOrder = await _context.UserOrders
|
||||
.Include(o => o.FactorDetails)
|
||||
.ThenInclude(fd => fd.Product)
|
||||
.Where(o => o.Id == request.OriginalOrderId && !o.IsDeleted)
|
||||
.FirstOrDefaultAsync(context.CancellationToken);
|
||||
|
||||
if (originalOrder == null)
|
||||
return new CustomerReorderResponse { Success = false, Message = "سفارش اصلی یافت نشد" };
|
||||
|
||||
var userId = originalOrder.UserId;
|
||||
|
||||
// Add items from old order to cart
|
||||
foreach (var fd in originalOrder.FactorDetails.Where(f => !f.IsDeleted))
|
||||
{
|
||||
var existingCartItem = await _context.UserCarts
|
||||
.Where(c => c.UserId == userId && c.ProductId == fd.ProductId && !c.IsDeleted)
|
||||
.FirstOrDefaultAsync(context.CancellationToken);
|
||||
|
||||
if (existingCartItem != null)
|
||||
{
|
||||
existingCartItem.Count += fd.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
_context.UserCarts.Add(new UserCart
|
||||
{
|
||||
UserId = userId,
|
||||
ProductId = fd.ProductId,
|
||||
Count = fd.Count
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
|
||||
// Calculate total with current prices
|
||||
long totalAmount = originalOrder.FactorDetails
|
||||
.Where(f => !f.IsDeleted)
|
||||
.Sum(fd => request.UseCurrentPrices && fd.Product != null
|
||||
? fd.Product.Price * fd.Count
|
||||
: fd.UnitPrice * fd.Count);
|
||||
|
||||
return new CustomerReorderResponse
|
||||
{
|
||||
Success = true,
|
||||
Message = request.UseCurrentPrices ?
|
||||
"سفارش مجدد با قیمتهای جدید ثبت شد" :
|
||||
"سفارش مجدد با قیمتهای قبلی ثبت شد",
|
||||
NewOrderId = newOrderId,
|
||||
Message = "محصولات به سبد خرید اضافه شدند",
|
||||
NewOrderId = 0, // Cart items added, no order created yet
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
// ============= Helper Methods =============
|
||||
|
||||
private static string GetDeliveryStatusPersian(Domain.Enums.DeliveryStatus status) => status switch
|
||||
{
|
||||
Domain.Enums.DeliveryStatus.None => "نامشخص",
|
||||
Domain.Enums.DeliveryStatus.Pending => "در انتظار ارسال",
|
||||
Domain.Enums.DeliveryStatus.InTransit => "در حال ارسال",
|
||||
Domain.Enums.DeliveryStatus.Delivered => "تحویل داده شده",
|
||||
Domain.Enums.DeliveryStatus.Returned => "مرجوع شده",
|
||||
Domain.Enums.DeliveryStatus.Cancelled => "لغو شده",
|
||||
_ => status.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,7 +16,10 @@ using CMSMicroservice.Application.UserCQ.Commands.AcceptContract;
|
||||
using CMSMicroservice.Application.UserCQ.Queries.GetCustomerProfile;
|
||||
using CMSMicroservice.Application.UserCQ.Queries.GetCustomerReferrals;
|
||||
using CMSMicroservice.Application.UserCQ.Queries.GetCustomerSettings;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Grpc.Core;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MediatR;
|
||||
@@ -28,11 +31,25 @@ public class UserService : UserContract.UserContractBase
|
||||
{
|
||||
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
|
||||
private readonly ISender _sender;
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
private readonly IHashService _hashService;
|
||||
private readonly IFileManagementService _fileManagementService;
|
||||
|
||||
public UserService(IDispatchRequestToCQRS dispatchRequestToCQRS, ISender sender)
|
||||
public UserService(
|
||||
IDispatchRequestToCQRS dispatchRequestToCQRS,
|
||||
ISender sender,
|
||||
IApplicationDbContext context,
|
||||
ICurrentUserService currentUserService,
|
||||
IHashService hashService,
|
||||
IFileManagementService fileManagementService)
|
||||
{
|
||||
_dispatchRequestToCQRS = dispatchRequestToCQRS;
|
||||
_sender = sender;
|
||||
_context = context;
|
||||
_currentUserService = currentUserService;
|
||||
_hashService = hashService;
|
||||
_fileManagementService = fileManagementService;
|
||||
}
|
||||
public override async Task<CreateNewUserResponse> CreateNewUser(CreateNewUserRequest request, ServerCallContext context)
|
||||
{
|
||||
@@ -90,33 +107,58 @@ public class UserService : UserContract.UserContractBase
|
||||
|
||||
public override async Task<GetUserForCustomerResponse> GetUserForCustomer(GetUserForCustomerRequest request, ServerCallContext context)
|
||||
{
|
||||
// Mock implementation for Customer Get User
|
||||
await Task.Delay(10); // Simulate async operation
|
||||
var userId = GetCurrentUserId();
|
||||
|
||||
var user = await _context.Users
|
||||
.AsNoTracking()
|
||||
.Where(u => u.Id == userId && !u.IsDeleted)
|
||||
.FirstOrDefaultAsync(context.CancellationToken);
|
||||
|
||||
if (user == null)
|
||||
throw new RpcException(new Status(StatusCode.NotFound, "کاربر یافت نشد"));
|
||||
|
||||
return new GetUserForCustomerResponse
|
||||
{
|
||||
Id = 123,
|
||||
FirstName = "احمد",
|
||||
LastName = "محمدی",
|
||||
Mobile = "09123456789",
|
||||
Email = "ahmad.mohammadi@example.com",
|
||||
NationalCode = "1234567890",
|
||||
AvatarPath = "/avatars/user_123.jpg",
|
||||
ParentId = 100,
|
||||
ReferralCode = "REF123456",
|
||||
IsMobileVerified = true,
|
||||
MobileVerifiedAt = Timestamp.FromDateTime(DateTime.SpecifyKind(new DateTime(2024, 1, 15), DateTimeKind.Utc)),
|
||||
EmailNotifications = true,
|
||||
SmsNotifications = true,
|
||||
PushNotifications = false,
|
||||
BirthDate = Timestamp.FromDateTime(DateTime.SpecifyKind(new DateTime(1990, 5, 20), DateTimeKind.Utc))
|
||||
Id = user.Id,
|
||||
FirstName = user.FirstName ?? string.Empty,
|
||||
LastName = user.LastName ?? string.Empty,
|
||||
Mobile = user.Mobile,
|
||||
Email = user.Email ?? string.Empty,
|
||||
NationalCode = user.NationalCode ?? string.Empty,
|
||||
AvatarPath = user.AvatarPath ?? string.Empty,
|
||||
ParentId = user.NetworkParentId,
|
||||
ReferralCode = user.ReferralCode ?? string.Empty,
|
||||
IsMobileVerified = user.IsMobileVerified,
|
||||
MobileVerifiedAt = user.MobileVerifiedAt.HasValue
|
||||
? Timestamp.FromDateTime(DateTime.SpecifyKind(user.MobileVerifiedAt.Value, DateTimeKind.Utc))
|
||||
: null,
|
||||
EmailNotifications = user.EmailNotifications,
|
||||
SmsNotifications = user.SmsNotifications,
|
||||
PushNotifications = user.PushNotifications,
|
||||
BirthDate = user.BirthDate.HasValue
|
||||
? Timestamp.FromDateTime(DateTime.SpecifyKind(user.BirthDate.Value, DateTimeKind.Utc))
|
||||
: null
|
||||
};
|
||||
}
|
||||
|
||||
public override async Task<Empty> UpdateCustomerProfile(UpdateCustomerProfileRequest request, ServerCallContext context)
|
||||
{
|
||||
// Mock implementation for Update Customer Profile
|
||||
await Task.Delay(10);
|
||||
var userId = GetCurrentUserId();
|
||||
|
||||
var user = await _context.Users
|
||||
.Where(u => u.Id == userId && !u.IsDeleted)
|
||||
.FirstOrDefaultAsync(context.CancellationToken);
|
||||
|
||||
if (user == null)
|
||||
throw new RpcException(new Status(StatusCode.NotFound, "کاربر یافت نشد"));
|
||||
|
||||
if (request.FirstName != null) user.FirstName = request.FirstName;
|
||||
if (request.LastName != null) user.LastName = request.LastName;
|
||||
if (request.Email != null) user.Email = request.Email;
|
||||
if (request.NationalCode != null) user.NationalCode = request.NationalCode;
|
||||
if (request.BirthDate != null) user.BirthDate = request.BirthDate.ToDateTime();
|
||||
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
return new Empty();
|
||||
}
|
||||
|
||||
@@ -153,9 +195,6 @@ public class UserService : UserContract.UserContractBase
|
||||
|
||||
public override async Task<ChangeCustomerPasswordResponse> ChangeCustomerPassword(ChangeCustomerPasswordRequest request, ServerCallContext context)
|
||||
{
|
||||
// Mock implementation for Change Customer Password
|
||||
await Task.Delay(10);
|
||||
|
||||
if (request.NewPassword != request.ConfirmPassword)
|
||||
{
|
||||
return new ChangeCustomerPasswordResponse
|
||||
@@ -174,6 +213,32 @@ public class UserService : UserContract.UserContractBase
|
||||
};
|
||||
}
|
||||
|
||||
var userId = GetCurrentUserId();
|
||||
|
||||
var user = await _context.Users
|
||||
.Where(u => u.Id == userId && !u.IsDeleted)
|
||||
.FirstOrDefaultAsync(context.CancellationToken);
|
||||
|
||||
if (user == null)
|
||||
throw new RpcException(new Status(StatusCode.NotFound, "کاربر یافت نشد"));
|
||||
|
||||
// Verify current password
|
||||
if (!string.IsNullOrEmpty(user.HashPassword))
|
||||
{
|
||||
if (!_hashService.VerifyPassword(request.CurrentPassword, user.HashPassword))
|
||||
{
|
||||
return new ChangeCustomerPasswordResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "رمز عبور فعلی نادرست است"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Hash and save new password
|
||||
user.HashPassword = _hashService.HashPassword(request.NewPassword);
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
|
||||
return new ChangeCustomerPasswordResponse
|
||||
{
|
||||
Success = true,
|
||||
@@ -233,9 +298,6 @@ public class UserService : UserContract.UserContractBase
|
||||
|
||||
public override async Task<UploadCustomerAvatarResponse> UploadCustomerAvatar(UploadCustomerAvatarRequest request, ServerCallContext context)
|
||||
{
|
||||
// Mock implementation for Upload Customer Avatar
|
||||
await Task.Delay(10);
|
||||
|
||||
if (request.FileData == null || request.FileData.Length == 0)
|
||||
{
|
||||
return new UploadCustomerAvatarResponse
|
||||
@@ -264,9 +326,35 @@ public class UserService : UserContract.UserContractBase
|
||||
};
|
||||
}
|
||||
|
||||
// Simulate file upload and generate URL
|
||||
var fileName = $"avatar_{DateTime.Now.Ticks}.{request.FileMimeType?.Split('/').LastOrDefault()}";
|
||||
var avatarUrl = $"/uploads/avatars/{fileName}";
|
||||
var userId = GetCurrentUserId();
|
||||
|
||||
var user = await _context.Users
|
||||
.Where(u => u.Id == userId && !u.IsDeleted)
|
||||
.FirstOrDefaultAsync(context.CancellationToken);
|
||||
|
||||
if (user == null)
|
||||
throw new RpcException(new Status(StatusCode.NotFound, "کاربر یافت نشد"));
|
||||
|
||||
// Upload to FMS
|
||||
var fileBytes = request.FileData.ToByteArray();
|
||||
var fileName = $"avatar_{userId}_{DateTime.UtcNow.Ticks}";
|
||||
var mime = request.FileMimeType ?? "image/jpeg";
|
||||
|
||||
var avatarUrl = await _fileManagementService.UploadFileAsync(
|
||||
"Avatars", fileBytes, mime, fileName, context.CancellationToken);
|
||||
|
||||
if (string.IsNullOrEmpty(avatarUrl))
|
||||
{
|
||||
return new UploadCustomerAvatarResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "خطا در آپلود فایل. لطفاً مجدد تلاش کنید"
|
||||
};
|
||||
}
|
||||
|
||||
// Update user avatar path in DB
|
||||
user.AvatarPath = avatarUrl;
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
|
||||
return new UploadCustomerAvatarResponse
|
||||
{
|
||||
@@ -295,8 +383,32 @@ public class UserService : UserContract.UserContractBase
|
||||
|
||||
public override async Task<Empty> UpdateCustomerSettings(UpdateCustomerSettingsRequest request, ServerCallContext context)
|
||||
{
|
||||
// Mock implementation for Update Customer Settings
|
||||
await Task.Delay(10);
|
||||
var userId = GetCurrentUserId();
|
||||
|
||||
var user = await _context.Users
|
||||
.Where(u => u.Id == userId && !u.IsDeleted)
|
||||
.FirstOrDefaultAsync(context.CancellationToken);
|
||||
|
||||
if (user == null)
|
||||
throw new RpcException(new Status(StatusCode.NotFound, "کاربر یافت نشد"));
|
||||
|
||||
user.EmailNotifications = request.EmailNotifications;
|
||||
user.SmsNotifications = request.SmsNotifications;
|
||||
user.PushNotifications = request.PushNotifications;
|
||||
// MarketingNotifications, PreferredLanguage, TimeZone, TwoFactorAuth
|
||||
// are stored at application level if needed in future
|
||||
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
return new Empty();
|
||||
}
|
||||
|
||||
// ============= Helper Methods =============
|
||||
|
||||
private long GetCurrentUserId()
|
||||
{
|
||||
if (long.TryParse(_currentUserService.UserId, out var userId) && userId > 0)
|
||||
return userId;
|
||||
|
||||
throw new RpcException(new Status(StatusCode.Unauthenticated, "لطفاً وارد حساب کاربری خود شوید"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,16 +8,29 @@ using CMSMicroservice.Application.UserWalletCQ.Queries.GetAllUserWalletByFilter;
|
||||
using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWalletChangeLog;
|
||||
using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWithdrawals;
|
||||
using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWithdrawalSettings;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using Grpc.Core;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Linq;
|
||||
|
||||
namespace CMSMicroservice.WebApi.Services;
|
||||
public class UserWalletService : UserWalletContract.UserWalletContractBase
|
||||
{
|
||||
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
|
||||
private readonly ISender _sender;
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
|
||||
public UserWalletService(IDispatchRequestToCQRS dispatchRequestToCQRS, ISender sender)
|
||||
public UserWalletService(
|
||||
IDispatchRequestToCQRS dispatchRequestToCQRS,
|
||||
ISender sender,
|
||||
IApplicationDbContext context,
|
||||
ICurrentUserService currentUserService)
|
||||
{
|
||||
_dispatchRequestToCQRS = dispatchRequestToCQRS;
|
||||
_sender = sender;
|
||||
_context = context;
|
||||
_currentUserService = currentUserService;
|
||||
}
|
||||
public override async Task<CreateNewUserWalletResponse> CreateNewUserWallet(CreateNewUserWalletRequest request, ServerCallContext context)
|
||||
{
|
||||
@@ -87,6 +100,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))
|
||||
@@ -98,10 +113,38 @@ public class UserWalletService : UserWalletContract.UserWalletContractBase
|
||||
|
||||
public override async Task<Google.Protobuf.WellKnownTypes.Empty> CustomerWithdrawBalance(CustomerWithdrawBalanceRequest request, ServerCallContext context)
|
||||
{
|
||||
// Mock implementation - would handle withdrawal
|
||||
var userId = GetCurrentUserId();
|
||||
|
||||
// Find the commission payout record
|
||||
var payout = await _context.UserCommissionPayouts
|
||||
.Where(p => p.Id == request.PayoutId && p.UserId == userId && !p.IsDeleted)
|
||||
.FirstOrDefaultAsync(context.CancellationToken);
|
||||
|
||||
if (payout == null)
|
||||
throw new RpcException(new Status(StatusCode.NotFound, "رکورد پرداخت کمیسیون یافت نشد"));
|
||||
|
||||
// Validate status - can only withdraw if already paid to wallet
|
||||
if (payout.Status != Domain.Enums.CommissionPayoutStatus.Paid)
|
||||
throw new RpcException(new Status(StatusCode.FailedPrecondition,
|
||||
"فقط کمیسیونهای واریز شده به کیف پول قابل برداشت هستند"));
|
||||
|
||||
// Update payout with withdrawal info
|
||||
payout.WithdrawalMethod = (Domain.Enums.WithdrawalMethod)request.WithdrawalMethod;
|
||||
payout.IbanNumber = request.IbanNumber;
|
||||
payout.Status = Domain.Enums.CommissionPayoutStatus.WithdrawRequested;
|
||||
|
||||
await _context.SaveChangesAsync(context.CancellationToken);
|
||||
|
||||
return new Google.Protobuf.WellKnownTypes.Empty();
|
||||
}
|
||||
|
||||
private long GetCurrentUserId()
|
||||
{
|
||||
if (long.TryParse(_currentUserService.UserId, out var userId) && userId > 0)
|
||||
return userId;
|
||||
throw new RpcException(new Status(StatusCode.Unauthenticated, "لطفاً وارد حساب کاربری خود شوید"));
|
||||
}
|
||||
|
||||
public override async Task<GetCustomerWithdrawalsResponse> GetCustomerWithdrawals(GetCustomerWithdrawalsRequest request, ServerCallContext context)
|
||||
{
|
||||
var query = new GetCustomerWithdrawalsQuery
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"UseRealPaymentGateway": false,
|
||||
"FMS": {
|
||||
"Address": "https://dl.afrino.co"
|
||||
},
|
||||
"JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=",
|
||||
"JwtIssuer": "https://localhost",
|
||||
"JwtAudience": "https://localhost",
|
||||
|
||||
Reference in New Issue
Block a user