Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b42d9e141d | |||
| f64b6be7da | |||
| 9185aa227d | |||
| b2d676b555 | |||
| b41342dcad | |||
| c3eeb16856 | |||
| 794dd01ac0 | |||
| 658d076bdf |
@@ -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
|
|
||||||
@@ -7,6 +7,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
||||||
|
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||||
|
|
||||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.2.2" />
|
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.2.2" />
|
||||||
<PackageReference Include="Mapster" Version="7.4.0" />
|
<PackageReference Include="Mapster" Version="7.4.0" />
|
||||||
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
|
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
|
||||||
|
|||||||
+25
-1
@@ -3,14 +3,18 @@ namespace CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubMembership
|
|||||||
public class GetClubMembershipQueryHandler : IRequestHandler<GetClubMembershipQuery, ClubMembershipDto?>
|
public class GetClubMembershipQueryHandler : IRequestHandler<GetClubMembershipQuery, ClubMembershipDto?>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly ILogger<GetClubMembershipQueryHandler> _logger;
|
||||||
|
|
||||||
public GetClubMembershipQueryHandler(IApplicationDbContext context)
|
public GetClubMembershipQueryHandler(IApplicationDbContext context, ILogger<GetClubMembershipQueryHandler> logger)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ClubMembershipDto?> Handle(GetClubMembershipQuery request, CancellationToken cancellationToken)
|
public async Task<ClubMembershipDto?> Handle(GetClubMembershipQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("GetClubMembership called for UserId: {UserId}", request.UserId);
|
||||||
|
|
||||||
var membership = await _context.ClubMemberships
|
var membership = await _context.ClubMemberships
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(x => x.UserId == request.UserId)
|
.Where(x => x.UserId == request.UserId)
|
||||||
@@ -27,6 +31,26 @@ public class GetClubMembershipQueryHandler : IRequestHandler<GetClubMembershipQu
|
|||||||
})
|
})
|
||||||
.FirstOrDefaultAsync(cancellationToken);
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
// اگر کاربر عضویت نداره، یک DTO با وضعیت غیرفعال برگردون
|
||||||
|
if (membership == null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("No membership found for UserId: {UserId}, returning inactive status", request.UserId);
|
||||||
|
return new ClubMembershipDto
|
||||||
|
{
|
||||||
|
Id = 0,
|
||||||
|
UserId = request.UserId,
|
||||||
|
IsActive = false,
|
||||||
|
ActivatedAt = null,
|
||||||
|
InitialContribution = 0,
|
||||||
|
TotalEarned = 0,
|
||||||
|
Created = DateTimeOffset.UtcNow,
|
||||||
|
LastModified = null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Membership found for UserId: {UserId}, IsActive: {IsActive}, Id: {Id}",
|
||||||
|
request.UserId, membership.IsActive, membership.Id);
|
||||||
|
|
||||||
return membership;
|
return membership;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
-4
@@ -4,13 +4,16 @@ public class GetUserCommissionPayoutsQueryHandler : IRequestHandler<GetUserCommi
|
|||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
||||||
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
|
||||||
public GetUserCommissionPayoutsQueryHandler(
|
public GetUserCommissionPayoutsQueryHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
IWeekDefinitionRepository weekDefinitionRepository)
|
IWeekDefinitionRepository weekDefinitionRepository,
|
||||||
|
ICurrentUserService currentUser)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_weekDefinitionRepository = weekDefinitionRepository;
|
_weekDefinitionRepository = weekDefinitionRepository;
|
||||||
|
_currentUser = currentUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GetUserCommissionPayoutsResponseDto> Handle(GetUserCommissionPayoutsQuery request, CancellationToken cancellationToken)
|
public async Task<GetUserCommissionPayoutsResponseDto> Handle(GetUserCommissionPayoutsQuery request, CancellationToken cancellationToken)
|
||||||
@@ -21,10 +24,20 @@ public class GetUserCommissionPayoutsQueryHandler : IRequestHandler<GetUserCommi
|
|||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.AsQueryable();
|
.AsQueryable();
|
||||||
|
|
||||||
// فیلترها
|
// اگر UserId داده نشده، از CurrentUser بگیر (برای Customer API)
|
||||||
if (request.UserId.HasValue)
|
long? userId = request.UserId;
|
||||||
|
if (!userId.HasValue || userId.Value == 0)
|
||||||
{
|
{
|
||||||
query = query.Where(x => x.UserId == request.UserId.Value);
|
if (long.TryParse(_currentUser.UserId, out var currentUserId))
|
||||||
|
{
|
||||||
|
userId = currentUserId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// فیلترها
|
||||||
|
if (userId.HasValue && userId.Value > 0)
|
||||||
|
{
|
||||||
|
query = query.Where(x => x.UserId == userId.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.Status.HasValue)
|
if (request.Status.HasValue)
|
||||||
|
|||||||
+17
-4
@@ -4,13 +4,16 @@ public class GetUserWeeklyBalancesQueryHandler : IRequestHandler<GetUserWeeklyBa
|
|||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
||||||
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
|
||||||
public GetUserWeeklyBalancesQueryHandler(
|
public GetUserWeeklyBalancesQueryHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
IWeekDefinitionRepository weekDefinitionRepository)
|
IWeekDefinitionRepository weekDefinitionRepository,
|
||||||
|
ICurrentUserService currentUser)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_weekDefinitionRepository = weekDefinitionRepository;
|
_weekDefinitionRepository = weekDefinitionRepository;
|
||||||
|
_currentUser = currentUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GetUserWeeklyBalancesResponseDto> Handle(GetUserWeeklyBalancesQuery request, CancellationToken cancellationToken)
|
public async Task<GetUserWeeklyBalancesResponseDto> Handle(GetUserWeeklyBalancesQuery request, CancellationToken cancellationToken)
|
||||||
@@ -21,10 +24,20 @@ public class GetUserWeeklyBalancesQueryHandler : IRequestHandler<GetUserWeeklyBa
|
|||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.AsQueryable();
|
.AsQueryable();
|
||||||
|
|
||||||
// فیلترها
|
// اگر UserId داده نشده، از CurrentUser بگیر (برای Customer API)
|
||||||
if (request.UserId.HasValue)
|
long? userId = request.UserId;
|
||||||
|
if (!userId.HasValue || userId.Value == 0)
|
||||||
{
|
{
|
||||||
query = query.Where(x => x.UserId == request.UserId.Value);
|
if (long.TryParse(_currentUser.UserId, out var currentUserId))
|
||||||
|
{
|
||||||
|
userId = currentUserId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// فیلترها
|
||||||
|
if (userId.HasValue && userId.Value > 0)
|
||||||
|
{
|
||||||
|
query = query.Where(x => x.UserId == userId.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// فیلتر بر اساس WeekDefinitionId (روش ترجیحی)
|
// فیلتر بر اساس WeekDefinitionId (روش ترجیحی)
|
||||||
|
|||||||
@@ -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,21 +0,0 @@
|
|||||||
using CMSMicroservice.Application.UserCartsCQ.Queries.GetAllUserCartsByFilter;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.Common.Mappings;
|
|
||||||
|
|
||||||
public class UserCartsProfile : IRegister
|
|
||||||
{
|
|
||||||
void IRegister.Register(TypeAdapterConfig config)
|
|
||||||
{
|
|
||||||
config.NewConfig<UserCart,GetAllUserCartsByFilterResponseModel>()
|
|
||||||
.Map(dest => dest.Id, src => src.Id)
|
|
||||||
.Map(dest => dest.Count, src => src.Count)
|
|
||||||
.Map(dest => dest.ProductId, src => src.ProductId)
|
|
||||||
.Map(dest => dest.ProductTitle, src => src.Product.Title)
|
|
||||||
.Map(dest => dest.ProductShortInfomation, src => src.Product.ShortInfomation)
|
|
||||||
.Map(dest => dest.ProductDiscount, src => src.Product.Discount)
|
|
||||||
.Map(dest => dest.ProductPrice, src => src.Product.Price)
|
|
||||||
.Map(dest => dest.ProductThumbnailPath, src => src.Product.ThumbnailPath)
|
|
||||||
.Map(dest => dest.Created, src => src.Created)
|
|
||||||
;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
using CMSMicroservice.Application.UserOrderCQ.Queries.GetAllUserOrderByFilter;
|
|
||||||
using CMSMicroservice.Application.UserOrderCQ.Queries.GetUserOrder;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.Common.Mappings;
|
|
||||||
|
|
||||||
public class UserOrderProfile : IRegister
|
|
||||||
{
|
|
||||||
void IRegister.Register(TypeAdapterConfig config)
|
|
||||||
{
|
|
||||||
config.NewConfig<UserOrder,GetUserOrderResponseDto>()
|
|
||||||
.Map(dest => dest.Id, src => src.Id)
|
|
||||||
.Map(dest => dest.Amount, src => src.Amount)
|
|
||||||
.Map(dest => dest.PackageId, src => src.PackageId)
|
|
||||||
.Map(dest => dest.TransactionId, src => src.TransactionId)
|
|
||||||
.Map(dest => dest.PaymentStatus, src => src.PaymentStatus)
|
|
||||||
.Map(dest => dest.PaymentDate, src => src.PaymentDate)
|
|
||||||
.Map(dest => dest.UserId, src => src.UserId)
|
|
||||||
.Map(dest => dest.UserAddressId, src => src.UserAddressId)
|
|
||||||
.Map(dest => dest.PaymentMethod, src => src.PaymentMethod)
|
|
||||||
.Map(dest => dest.UserAddressText, src => src.UserAddress.Address)
|
|
||||||
.Map(dest => dest.FactorDetails, src => src.FactorDetails.Select(s=>s.Adapt<GetUserOrderResponseFactorDetail>()))
|
|
||||||
|
|
||||||
;
|
|
||||||
|
|
||||||
config.NewConfig<UserOrder,GetAllUserOrderByFilterResponseModel>()
|
|
||||||
.Map(dest => dest.Id, src => src.Id)
|
|
||||||
.Map(dest => dest.Amount, src => src.Amount)
|
|
||||||
.Map(dest => dest.PackageId, src => src.PackageId)
|
|
||||||
.Map(dest => dest.TransactionId, src => src.TransactionId)
|
|
||||||
.Map(dest => dest.PaymentStatus, src => src.PaymentStatus)
|
|
||||||
.Map(dest => dest.PaymentDate, src => src.PaymentDate)
|
|
||||||
.Map(dest => dest.UserId, src => src.UserId)
|
|
||||||
.Map(dest => dest.UserAddressId, src => src.UserAddressId)
|
|
||||||
.Map(dest => dest.PaymentMethod, src => src.PaymentMethod)
|
|
||||||
.Map(dest => dest.UserAddressText, src => src.UserAddress.Address)
|
|
||||||
.Map(dest => dest.FactorDetails, src => src.FactorDetails.Select(s=>s.Adapt<GetUserOrderResponseFactorDetail>()))
|
|
||||||
;
|
|
||||||
|
|
||||||
config.NewConfig<FactorDetails,GetUserOrderResponseFactorDetail>()
|
|
||||||
.Map(dest => dest.ProductId, src => src.ProductId)
|
|
||||||
.Map(dest => dest.ProductTitle, src => src.Product.Title)
|
|
||||||
.Map(dest => dest.ProductThumbnailPath, src => src.Product.ThumbnailPath)
|
|
||||||
.Map(dest => dest.UnitPrice, src => src.Product.Price)
|
|
||||||
.Map(dest => dest.Count, src => src.Count)
|
|
||||||
.Map(dest => dest.UnitDiscountPrice, src => src.Product.Price*(src.Product.Discount/100))
|
|
||||||
;
|
|
||||||
|
|
||||||
config.NewConfig<FactorDetails,GetAllUserOrderByFilterResponseModelFactorDetail>()
|
|
||||||
.Map(dest => dest.ProductId, src => src.ProductId)
|
|
||||||
.Map(dest => dest.ProductTitle, src => src.Product.Title)
|
|
||||||
.Map(dest => dest.ProductThumbnailPath, src => src.Product.ThumbnailPath)
|
|
||||||
.Map(dest => dest.UnitPrice, src => src.Product.Price)
|
|
||||||
.Map(dest => dest.Count, src => src.Count)
|
|
||||||
.Map(dest => dest.UnitDiscountPrice, src => src.Product.Price*(src.Product.Discount/100))
|
|
||||||
;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.AddToCustomerCart;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command برای افزودن محصول به سبد خرید کاربر فعلی
|
||||||
|
/// </summary>
|
||||||
|
public class AddToCustomerCartCommand : IRequest<AddToCustomerCartCommandResponse>
|
||||||
|
{
|
||||||
|
public long ProductId { get; set; }
|
||||||
|
public int Count { get; set; }
|
||||||
|
// UserId from ICurrentUserService
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AddToCustomerCartCommandResponse
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public bool Success { get; set; }
|
||||||
|
}
|
||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.AddToCustomerCart;
|
||||||
|
|
||||||
|
public class AddToCustomerCartCommandHandler : IRequestHandler<AddToCustomerCartCommand, AddToCustomerCartCommandResponse>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
|
||||||
|
public AddToCustomerCartCommandHandler(IApplicationDbContext context, ICurrentUserService currentUser)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_currentUser = currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AddToCustomerCartCommandResponse> Handle(AddToCustomerCartCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Extract UserId from JWT token
|
||||||
|
var userId = long.TryParse(_currentUser.UserId, out var id) ? id : 0;
|
||||||
|
if (userId == 0)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException("User not authenticated");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if product exists and is not deleted
|
||||||
|
var product = await _context.Products
|
||||||
|
.FirstOrDefaultAsync(p => p.Id == request.ProductId && !p.IsDeleted, cancellationToken);
|
||||||
|
|
||||||
|
if (product == null)
|
||||||
|
{
|
||||||
|
return new AddToCustomerCartCommandResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "محصول یافت نشد یا حذف شده است"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if item already exists in cart
|
||||||
|
var existingCartItem = await _context.UserCarts
|
||||||
|
.FirstOrDefaultAsync(uc => uc.UserId == userId && uc.ProductId == request.ProductId, cancellationToken);
|
||||||
|
|
||||||
|
if (existingCartItem != null)
|
||||||
|
{
|
||||||
|
// Update count
|
||||||
|
existingCartItem.Count += request.Count;
|
||||||
|
_context.UserCarts.Update(existingCartItem);
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return new AddToCustomerCartCommandResponse
|
||||||
|
{
|
||||||
|
Id = existingCartItem.Id,
|
||||||
|
Success = true,
|
||||||
|
Message = "تعداد محصول در سبد خرید بهروزرسانی شد"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new cart item
|
||||||
|
var cartItem = new UserCart
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
ProductId = request.ProductId,
|
||||||
|
Count = request.Count,
|
||||||
|
Created = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.UserCarts.Add(cartItem);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return new AddToCustomerCartCommandResponse
|
||||||
|
{
|
||||||
|
Id = cartItem.Id,
|
||||||
|
Success = true,
|
||||||
|
Message = "محصول به سبد خرید اضافه شد"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.RemoveFromCustomerCart;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command برای حذف محصول از سبد خرید
|
||||||
|
/// </summary>
|
||||||
|
public class RemoveFromCustomerCartCommand : IRequest<RemoveFromCustomerCartCommandResponse>
|
||||||
|
{
|
||||||
|
public long CartItemId { get; set; }
|
||||||
|
// UserId from ICurrentUserService
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RemoveFromCustomerCartCommandResponse
|
||||||
|
{
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public bool Success { get; set; }
|
||||||
|
}
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.RemoveFromCustomerCart;
|
||||||
|
|
||||||
|
public class RemoveFromCustomerCartCommandHandler : IRequestHandler<RemoveFromCustomerCartCommand, RemoveFromCustomerCartCommandResponse>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
|
||||||
|
public RemoveFromCustomerCartCommandHandler(IApplicationDbContext context, ICurrentUserService currentUser)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_currentUser = currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RemoveFromCustomerCartCommandResponse> Handle(RemoveFromCustomerCartCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Extract UserId from JWT token
|
||||||
|
var userId = long.TryParse(_currentUser.UserId, out var id) ? id : 0;
|
||||||
|
if (userId == 0)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException("User not authenticated");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find and remove cart item
|
||||||
|
var cartItem = await _context.UserCarts
|
||||||
|
.FirstOrDefaultAsync(uc => uc.Id == request.CartItemId && uc.UserId == userId, cancellationToken);
|
||||||
|
|
||||||
|
if (cartItem == null)
|
||||||
|
{
|
||||||
|
return new RemoveFromCustomerCartCommandResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "آیتم سبد خرید یافت نشد"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_context.UserCarts.Remove(cartItem);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return new RemoveFromCustomerCartCommandResponse
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "آیتم از سبد خرید حذف شد"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.UpdateCustomerCartItem;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command برای بهروزرسانی تعداد محصول در سبد خرید
|
||||||
|
/// </summary>
|
||||||
|
public class UpdateCustomerCartItemCommand : IRequest<UpdateCustomerCartItemCommandResponse>
|
||||||
|
{
|
||||||
|
public long CartItemId { get; set; }
|
||||||
|
public int Count { get; set; }
|
||||||
|
// UserId from ICurrentUserService
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UpdateCustomerCartItemCommandResponse
|
||||||
|
{
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public bool Success { get; set; }
|
||||||
|
}
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.UpdateCustomerCartItem;
|
||||||
|
|
||||||
|
public class UpdateCustomerCartItemCommandHandler : IRequestHandler<UpdateCustomerCartItemCommand, UpdateCustomerCartItemCommandResponse>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
|
||||||
|
public UpdateCustomerCartItemCommandHandler(IApplicationDbContext context, ICurrentUserService currentUser)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_currentUser = currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UpdateCustomerCartItemCommandResponse> Handle(UpdateCustomerCartItemCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Extract UserId from JWT token
|
||||||
|
var userId = long.TryParse(_currentUser.UserId, out var id) ? id : 0;
|
||||||
|
if (userId == 0)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException("User not authenticated");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find cart item
|
||||||
|
var cartItem = await _context.UserCarts
|
||||||
|
.FirstOrDefaultAsync(uc => uc.Id == request.CartItemId && uc.UserId == userId, cancellationToken);
|
||||||
|
|
||||||
|
if (cartItem == null)
|
||||||
|
{
|
||||||
|
return new UpdateCustomerCartItemCommandResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "آیتم سبد خرید یافت نشد"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update count
|
||||||
|
if (request.Count <= 0)
|
||||||
|
{
|
||||||
|
// Remove item if count is 0 or negative
|
||||||
|
_context.UserCarts.Remove(cartItem);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return new UpdateCustomerCartItemCommandResponse
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "آیتم از سبد خرید حذف شد"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
cartItem.Count = request.Count;
|
||||||
|
_context.UserCarts.Update(cartItem);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return new UpdateCustomerCartItemCommandResponse
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "تعداد آیتم بهروزرسانی شد"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Queries.GetCustomerCart;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query برای دریافت سبد خرید کاربر فعلی
|
||||||
|
/// </summary>
|
||||||
|
public class GetCustomerCartQuery : IRequest<GetCustomerCartQueryResponse>
|
||||||
|
{
|
||||||
|
// UserId from ICurrentUserService
|
||||||
|
}
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Queries.GetCustomerCart;
|
||||||
|
|
||||||
|
public class GetCustomerCartQueryHandler : IRequestHandler<GetCustomerCartQuery, GetCustomerCartQueryResponse>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
|
||||||
|
public GetCustomerCartQueryHandler(IApplicationDbContext context, ICurrentUserService currentUser)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_currentUser = currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GetCustomerCartQueryResponse> Handle(GetCustomerCartQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Extract UserId from JWT token
|
||||||
|
var userId = long.TryParse(_currentUser.UserId, out var id) ? id : 0;
|
||||||
|
if (userId == 0)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException("User not authenticated");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all cart items for the current user
|
||||||
|
var cartItems = await _context.UserCarts
|
||||||
|
.Include(uc => uc.Product)
|
||||||
|
.Where(uc => uc.UserId == userId)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var response = new GetCustomerCartQueryResponse
|
||||||
|
{
|
||||||
|
TotalItemsCount = cartItems.Sum(c => c.Count),
|
||||||
|
Message = cartItems.Count > 0 ? "سبد خرید با موفقیت بازیابی شد" : "سبد خرید خالی است"
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var item in cartItems)
|
||||||
|
{
|
||||||
|
// Use Product.ThumbnailPath directly
|
||||||
|
var thumbnailPath = item.Product?.ThumbnailPath ?? string.Empty;
|
||||||
|
var itemPrice = item.Product?.Price ?? 0;
|
||||||
|
var itemDiscount = item.Product?.Discount ?? 0;
|
||||||
|
var finalPrice = itemPrice * (100 - itemDiscount) / 100;
|
||||||
|
var totalItemPrice = finalPrice * item.Count;
|
||||||
|
|
||||||
|
response.Items.Add(new CustomerCartItemModel
|
||||||
|
{
|
||||||
|
Id = item.Id,
|
||||||
|
ProductId = item.ProductId,
|
||||||
|
ProductTitle = item.Product?.Title ?? string.Empty,
|
||||||
|
ProductShortInformation = item.Product?.ShortInfomation ?? string.Empty, // Typo in DB: ShortInfomation
|
||||||
|
ProductPrice = itemPrice,
|
||||||
|
ProductDiscount = itemDiscount,
|
||||||
|
ProductThumbnailPath = thumbnailPath,
|
||||||
|
Count = item.Count,
|
||||||
|
TotalItemPrice = totalItemPrice,
|
||||||
|
Created = item.Created
|
||||||
|
});
|
||||||
|
|
||||||
|
response.TotalPrice += totalItemPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
namespace CMSMicroservice.Application.DiscountShopCQ.Queries.GetCustomerCart;
|
||||||
|
|
||||||
|
public class GetCustomerCartQueryResponse
|
||||||
|
{
|
||||||
|
public List<CustomerCartItemModel> Items { get; set; } = new();
|
||||||
|
public long TotalPrice { get; set; }
|
||||||
|
public int TotalItemsCount { get; set; }
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CustomerCartItemModel
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public long ProductId { get; set; }
|
||||||
|
public string ProductTitle { get; set; } = string.Empty;
|
||||||
|
public string ProductShortInformation { get; set; } = string.Empty;
|
||||||
|
public long ProductPrice { get; set; }
|
||||||
|
public int ProductDiscount { get; set; }
|
||||||
|
public string ProductThumbnailPath { get; set; } = string.Empty;
|
||||||
|
public int Count { get; set; }
|
||||||
|
public long TotalItemPrice { get; set; }
|
||||||
|
public DateTime Created { get; set; }
|
||||||
|
}
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
namespace CMSMicroservice.Application.HealthCQ.Queries.GetSystemHealth;
|
||||||
|
|
||||||
|
public class GetSystemHealthQuery : IRequest<GetSystemHealthResponseDto>
|
||||||
|
{
|
||||||
|
}
|
||||||
+112
@@ -0,0 +1,112 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.HealthCQ.Queries.GetSystemHealth;
|
||||||
|
|
||||||
|
public class GetSystemHealthQueryHandler : IRequestHandler<GetSystemHealthQuery, GetSystemHealthResponseDto>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
public GetSystemHealthQueryHandler(IApplicationDbContext context, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GetSystemHealthResponseDto> Handle(GetSystemHealthQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var services = new List<ServiceHealthDto>();
|
||||||
|
var overallHealthy = true;
|
||||||
|
|
||||||
|
// Database Health Check
|
||||||
|
var dbHealth = await CheckDatabaseHealth(cancellationToken);
|
||||||
|
services.Add(dbHealth);
|
||||||
|
if (dbHealth.Status != HealthStatusDto.Healthy) overallHealthy = false;
|
||||||
|
|
||||||
|
// Memory Health Check
|
||||||
|
var memoryHealth = CheckMemoryHealth();
|
||||||
|
services.Add(memoryHealth);
|
||||||
|
if (memoryHealth.Status != HealthStatusDto.Healthy) overallHealthy = false;
|
||||||
|
|
||||||
|
// External Services Health (if any)
|
||||||
|
// TODO: Add external service health checks
|
||||||
|
|
||||||
|
return new GetSystemHealthResponseDto
|
||||||
|
{
|
||||||
|
OverallHealthy = overallHealthy,
|
||||||
|
Services = services,
|
||||||
|
CheckedAt = DateTime.UtcNow,
|
||||||
|
Version = GetApplicationVersion(),
|
||||||
|
Environment = _configuration["Environment"] ?? "Unknown"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ServiceHealthDto> CheckDatabaseHealth(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var startTime = DateTime.UtcNow;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Simple database connectivity check
|
||||||
|
var canConnect = await _context.Users.AnyAsync(cancellationToken);
|
||||||
|
var responseTime = (DateTime.UtcNow - startTime).TotalMilliseconds;
|
||||||
|
|
||||||
|
return new ServiceHealthDto
|
||||||
|
{
|
||||||
|
ServiceName = "Database",
|
||||||
|
Status = HealthStatusDto.Healthy,
|
||||||
|
Description = "Database connection is healthy",
|
||||||
|
ResponseTimeMs = (long)responseTime,
|
||||||
|
LastCheck = DateTime.UtcNow,
|
||||||
|
Details = new List<HealthDetailDto>
|
||||||
|
{
|
||||||
|
new() { Key = "ConnectionString", Value = "Connected", Status = HealthStatusDto.Healthy },
|
||||||
|
new() { Key = "ResponseTime", Value = $"{responseTime:F2}ms", Status = responseTime < 1000 ? HealthStatusDto.Healthy : HealthStatusDto.Degraded }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var responseTime = (DateTime.UtcNow - startTime).TotalMilliseconds;
|
||||||
|
return new ServiceHealthDto
|
||||||
|
{
|
||||||
|
ServiceName = "Database",
|
||||||
|
Status = HealthStatusDto.Unhealthy,
|
||||||
|
Description = $"Database connection failed: {ex.Message}",
|
||||||
|
ResponseTimeMs = (long)responseTime,
|
||||||
|
LastCheck = DateTime.UtcNow,
|
||||||
|
Details = new List<HealthDetailDto>
|
||||||
|
{
|
||||||
|
new() { Key = "Error", Value = ex.Message, Status = HealthStatusDto.Unhealthy }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ServiceHealthDto CheckMemoryHealth()
|
||||||
|
{
|
||||||
|
var process = System.Diagnostics.Process.GetCurrentProcess();
|
||||||
|
var workingSetMB = process.WorkingSet64 / 1024 / 1024;
|
||||||
|
var status = workingSetMB < 500 ? HealthStatusDto.Healthy :
|
||||||
|
workingSetMB < 1000 ? HealthStatusDto.Degraded : HealthStatusDto.Unhealthy;
|
||||||
|
|
||||||
|
return new ServiceHealthDto
|
||||||
|
{
|
||||||
|
ServiceName = "Memory",
|
||||||
|
Status = status,
|
||||||
|
Description = $"Current memory usage: {workingSetMB}MB",
|
||||||
|
ResponseTimeMs = 0,
|
||||||
|
LastCheck = DateTime.UtcNow,
|
||||||
|
Details = new List<HealthDetailDto>
|
||||||
|
{
|
||||||
|
new() { Key = "WorkingSet", Value = $"{workingSetMB}MB", Status = status },
|
||||||
|
new() { Key = "ProcessName", Value = process.ProcessName, Status = HealthStatusDto.Healthy }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetApplicationVersion()
|
||||||
|
{
|
||||||
|
return System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
namespace CMSMicroservice.Application.HealthCQ.Queries.GetSystemHealth;
|
||||||
|
|
||||||
|
public class GetSystemHealthResponseDto
|
||||||
|
{
|
||||||
|
public bool OverallHealthy { get; set; }
|
||||||
|
public List<ServiceHealthDto> Services { get; set; } = new();
|
||||||
|
public DateTime CheckedAt { get; set; }
|
||||||
|
public string Version { get; set; } = string.Empty;
|
||||||
|
public string Environment { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ServiceHealthDto
|
||||||
|
{
|
||||||
|
public string ServiceName { get; set; } = string.Empty;
|
||||||
|
public HealthStatusDto Status { get; set; }
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public long ResponseTimeMs { get; set; }
|
||||||
|
public DateTime LastCheck { get; set; }
|
||||||
|
public List<HealthDetailDto> Details { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HealthDetailDto
|
||||||
|
{
|
||||||
|
public string Key { get; set; } = string.Empty;
|
||||||
|
public string Value { get; set; } = string.Empty;
|
||||||
|
public HealthStatusDto Status { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum HealthStatusDto
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
Healthy = 1,
|
||||||
|
Degraded = 2,
|
||||||
|
Unhealthy = 3
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
using CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkTree;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query برای دریافت درخت شبکه کاربر جاری (Customer-facing)
|
||||||
|
/// از ICurrentUserService برای دریافت UserId استفاده میکند
|
||||||
|
/// </summary>
|
||||||
|
public record GetMyNetworkTreeQuery : IRequest<NetworkTreeDto?>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// تعداد سطوح (Depth) که میخواهیم نمایش دهیم (پیشفرض: 3)
|
||||||
|
/// </summary>
|
||||||
|
public int MaxDepth { get; init; } = 3;
|
||||||
|
}
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkTree;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler برای دریافت درخت شبکه کاربر جاری
|
||||||
|
/// </summary>
|
||||||
|
public class GetMyNetworkTreeQueryHandler : IRequestHandler<GetMyNetworkTreeQuery, NetworkTreeDto?>
|
||||||
|
{
|
||||||
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
private readonly ISender _sender;
|
||||||
|
|
||||||
|
public GetMyNetworkTreeQueryHandler(
|
||||||
|
ICurrentUserService currentUser,
|
||||||
|
ISender sender)
|
||||||
|
{
|
||||||
|
_currentUser = currentUser;
|
||||||
|
_sender = sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<NetworkTreeDto?> Handle(GetMyNetworkTreeQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// دریافت UserId از JWT
|
||||||
|
var userId = long.TryParse(_currentUser.UserId, out var id) ? id : 0;
|
||||||
|
if (userId == 0)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException("User not authenticated");
|
||||||
|
}
|
||||||
|
|
||||||
|
// استفاده از GetNetworkTreeQuery موجود با UserId از JWT
|
||||||
|
var query = new GetNetworkTreeQuery
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
MaxDepth = request.MaxDepth > 0 ? request.MaxDepth : 3
|
||||||
|
};
|
||||||
|
|
||||||
|
return await _sender.Send(query, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
|
||||||
|
|
||||||
|
public class GetMyNetworkTreeQueryValidator : AbstractValidator<GetMyNetworkTreeQuery>
|
||||||
|
{
|
||||||
|
public GetMyNetworkTreeQueryValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.MaxDepth)
|
||||||
|
.InclusiveBetween(1, 100)
|
||||||
|
.WithMessage("عمق درخت باید بین 1 تا 100 باشد");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||||
|
{
|
||||||
|
var result = await ValidateAsync(
|
||||||
|
ValidationContext<GetMyNetworkTreeQuery>.CreateWithOptions(
|
||||||
|
(GetMyNetworkTreeQuery)model,
|
||||||
|
x => x.IncludeProperties(propertyName)));
|
||||||
|
|
||||||
|
if (result.IsValid)
|
||||||
|
return Array.Empty<string>();
|
||||||
|
|
||||||
|
return result.Errors.Select(e => e.ErrorMessage);
|
||||||
|
};
|
||||||
|
}
|
||||||
+4
-1
@@ -2,5 +2,8 @@ namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkStat
|
|||||||
|
|
||||||
public class GetNetworkStatisticsQuery : IRequest<GetNetworkStatisticsResponseDto>
|
public class GetNetworkStatisticsQuery : IRequest<GetNetworkStatisticsResponseDto>
|
||||||
{
|
{
|
||||||
// No parameters - returns overall statistics
|
/// <summary>
|
||||||
|
/// شناسه کاربر برای محاسبه آمار شبکه او - 0 یا null یعنی کاربر جاری
|
||||||
|
/// </summary>
|
||||||
|
public long UserId { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
+106
-45
@@ -5,61 +5,79 @@ namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkStat
|
|||||||
public class GetNetworkStatisticsQueryHandler : IRequestHandler<GetNetworkStatisticsQuery, GetNetworkStatisticsResponseDto>
|
public class GetNetworkStatisticsQueryHandler : IRequestHandler<GetNetworkStatisticsQuery, GetNetworkStatisticsResponseDto>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
|
||||||
public GetNetworkStatisticsQueryHandler(IApplicationDbContext context)
|
public GetNetworkStatisticsQueryHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
ICurrentUserService currentUser)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_currentUser = currentUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GetNetworkStatisticsResponseDto> Handle(GetNetworkStatisticsQuery request, CancellationToken cancellationToken)
|
public async Task<GetNetworkStatisticsResponseDto> Handle(GetNetworkStatisticsQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Basic statistics - using Users table with NetworkParentId
|
// Get userId - use current user if not specified or is 0
|
||||||
var totalMembers = await _context.Users
|
var userId = request.UserId == 0
|
||||||
.Where(x => x.NetworkParentId != null)
|
? (long.TryParse(_currentUser.UserId, out var currentUserId) ? currentUserId : 0)
|
||||||
.CountAsync(cancellationToken);
|
: request.UserId;
|
||||||
|
|
||||||
var activeMembers = await _context.Users
|
if (userId == 0)
|
||||||
.Where(x => x.NetworkParentId != null)
|
{
|
||||||
.CountAsync(cancellationToken);
|
throw new UnauthorizedAccessException("User ID not found");
|
||||||
|
}
|
||||||
|
|
||||||
var leftLegCount = await _context.Users
|
// Get all descendants recursively
|
||||||
.Where(x => x.LegPosition == NetworkLeg.Left)
|
var allUsers = await _context.Users.ToListAsync(cancellationToken);
|
||||||
.CountAsync(cancellationToken);
|
var allDescendants = GetAllDescendants(userId, allUsers);
|
||||||
|
|
||||||
var rightLegCount = await _context.Users
|
// Statistics for the user's network (all descendants)
|
||||||
.Where(x => x.LegPosition == NetworkLeg.Right)
|
var totalMembers = allDescendants.Count;
|
||||||
.CountAsync(cancellationToken);
|
var activeMembers = allDescendants.Count(x => !x.IsDeleted);
|
||||||
|
|
||||||
|
// Get direct left and right children
|
||||||
|
var leftChild = allUsers.FirstOrDefault(x => x.NetworkParentId == userId && x.LegPosition == NetworkLeg.Left);
|
||||||
|
var rightChild = allUsers.FirstOrDefault(x => x.NetworkParentId == userId && x.LegPosition == NetworkLeg.Right);
|
||||||
|
|
||||||
|
// Count all descendants in left and right subtrees
|
||||||
|
var leftLegCount = leftChild != null ? GetAllDescendants(leftChild.Id, allUsers).Count + 1 : 0; // +1 for leftChild itself
|
||||||
|
var rightLegCount = rightChild != null ? GetAllDescendants(rightChild.Id, allUsers).Count + 1 : 0; // +1 for rightChild itself
|
||||||
|
|
||||||
double leftPercentage = totalMembers > 0 ? (leftLegCount / (double)totalMembers) * 100 : 0;
|
double leftPercentage = totalMembers > 0 ? (leftLegCount / (double)totalMembers) * 100 : 0;
|
||||||
double rightPercentage = totalMembers > 0 ? (rightLegCount / (double)totalMembers) * 100 : 0;
|
double rightPercentage = totalMembers > 0 ? (rightLegCount / (double)totalMembers) * 100 : 0;
|
||||||
|
|
||||||
// Calculate depth based on network parent relationships
|
// Calculate actual depth
|
||||||
// For simplicity, we'll estimate average depth as 3-5 levels
|
int maxDepth = 0;
|
||||||
double averageDepth = 4.5; // Estimated average
|
double totalDepthSum = 0;
|
||||||
int maxDepth = 10; // Estimated max depth
|
var userDepths = new Dictionary<long, int>();
|
||||||
|
CalculateDepths(userId, allUsers, 0, userDepths, ref maxDepth);
|
||||||
|
|
||||||
// Level distribution - simplified estimation based on growth pattern
|
if (allDescendants.Count > 0)
|
||||||
var levelDistribution = new List<LevelDistributionModel>();
|
|
||||||
if (totalMembers > 0)
|
|
||||||
{
|
{
|
||||||
// Approximate distribution: Level 1 (10%), Level 2 (20%), Level 3 (30%), Level 4 (20%), Level 5+ (20%)
|
totalDepthSum = allDescendants.Sum(d => userDepths.ContainsKey(d.Id) ? userDepths[d.Id] : 0);
|
||||||
levelDistribution = new List<LevelDistributionModel>
|
}
|
||||||
{
|
double averageDepth = allDescendants.Count > 0 ? totalDepthSum / allDescendants.Count : 0;
|
||||||
new() { Level = 1, Count = (int)(totalMembers * 0.1) },
|
|
||||||
new() { Level = 2, Count = (int)(totalMembers * 0.2) },
|
// Level distribution - calculate from depths
|
||||||
new() { Level = 3, Count = (int)(totalMembers * 0.3) },
|
var levelDistribution = new List<LevelDistributionModel>();
|
||||||
new() { Level = 4, Count = (int)(totalMembers * 0.2) },
|
if (allDescendants.Count > 0)
|
||||||
new() { Level = 5, Count = (int)(totalMembers * 0.15) },
|
{
|
||||||
new() { Level = 6, Count = totalMembers - (int)(totalMembers * 0.95) }
|
var levelCounts = allDescendants
|
||||||
};
|
.Where(d => userDepths.ContainsKey(d.Id))
|
||||||
|
.GroupBy(d => userDepths[d.Id])
|
||||||
|
.OrderBy(g => g.Key)
|
||||||
|
.Select(g => new LevelDistributionModel { Level = g.Key, Count = g.Count() })
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
levelDistribution = levelCounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Monthly growth (last 6 months) - using Created date
|
// Monthly growth (last 6 months) - using descendants Created date
|
||||||
var sixMonthsAgo = DateTime.Now.AddMonths(-6);
|
var sixMonthsAgo = DateTime.Now.AddMonths(-6);
|
||||||
var monthlyGrowthRaw = await _context.Users
|
var monthlyGrowthRaw = allDescendants
|
||||||
.Where(x => x.NetworkParentId != null && x.Created >= sixMonthsAgo)
|
.Where(x => x.Created >= sixMonthsAgo)
|
||||||
.Select(x => new { x.Created.Year, x.Created.Month })
|
.Select(x => new { x.Created.Year, x.Created.Month })
|
||||||
.ToListAsync(cancellationToken);
|
.ToList();
|
||||||
|
|
||||||
var monthlyGrowth = monthlyGrowthRaw
|
var monthlyGrowth = monthlyGrowthRaw
|
||||||
.GroupBy(x => new { x.Year, x.Month })
|
.GroupBy(x => new { x.Year, x.Month })
|
||||||
@@ -71,27 +89,34 @@ public class GetNetworkStatisticsQueryHandler : IRequestHandler<GetNetworkStatis
|
|||||||
.OrderBy(x => x.Month)
|
.OrderBy(x => x.Month)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// Top users by total children count
|
// Top users by total descendants count
|
||||||
var topUsers = await _context.Users
|
var userDescendantCounts = new Dictionary<long, int>();
|
||||||
.Where(x => x.NetworkParentId != null)
|
foreach (var user in allDescendants)
|
||||||
|
{
|
||||||
|
var descendants = GetAllDescendants(user.Id, allUsers);
|
||||||
|
userDescendantCounts[user.Id] = descendants.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
var topUserData = allDescendants
|
||||||
|
.Where(x => x.Id != userId && userDescendantCounts[x.Id] > 0)
|
||||||
.Select(x => new
|
.Select(x => new
|
||||||
{
|
{
|
||||||
x.Id,
|
x.Id,
|
||||||
UserName = (x.FirstName + " " + x.LastName).Trim(),
|
UserName = (x.FirstName + " " + x.LastName).Trim(),
|
||||||
LeftCount = _context.Users.Count(c => c.NetworkParentId == x.Id && c.LegPosition == NetworkLeg.Left),
|
DescendantCount = userDescendantCounts[x.Id],
|
||||||
RightCount = _context.Users.Count(c => c.NetworkParentId == x.Id && c.LegPosition == NetworkLeg.Right)
|
LeftCount = allUsers.Count(c => c.NetworkParentId == x.Id && c.LegPosition == NetworkLeg.Left),
|
||||||
|
RightCount = allUsers.Count(c => c.NetworkParentId == x.Id && c.LegPosition == NetworkLeg.Right)
|
||||||
})
|
})
|
||||||
.Where(x => x.LeftCount + x.RightCount > 0)
|
.OrderByDescending(x => x.DescendantCount)
|
||||||
.OrderByDescending(x => x.LeftCount + x.RightCount)
|
|
||||||
.Take(10)
|
.Take(10)
|
||||||
.ToListAsync(cancellationToken);
|
.ToList();
|
||||||
|
|
||||||
var topUserModels = topUsers.Select((x, index) => new TopNetworkUserModel
|
var topUserModels = topUserData.Select((x, index) => new TopNetworkUserModel
|
||||||
{
|
{
|
||||||
Rank = index + 1,
|
Rank = index + 1,
|
||||||
UserId = x.Id,
|
UserId = x.Id,
|
||||||
UserName = x.UserName,
|
UserName = x.UserName,
|
||||||
TotalChildren = x.LeftCount + x.RightCount,
|
TotalChildren = x.DescendantCount,
|
||||||
LeftCount = x.LeftCount,
|
LeftCount = x.LeftCount,
|
||||||
RightCount = x.RightCount
|
RightCount = x.RightCount
|
||||||
}).ToList();
|
}).ToList();
|
||||||
@@ -111,4 +136,40 @@ public class GetNetworkStatisticsQueryHandler : IRequestHandler<GetNetworkStatis
|
|||||||
TopUsers = topUserModels
|
TopUsers = topUserModels
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recursively get all descendants of a user
|
||||||
|
/// </summary>
|
||||||
|
private List<User> GetAllDescendants(long userId, List<User> allUsers)
|
||||||
|
{
|
||||||
|
var descendants = new List<User>();
|
||||||
|
var directChildren = allUsers.Where(x => x.NetworkParentId == userId).ToList();
|
||||||
|
|
||||||
|
foreach (var child in directChildren)
|
||||||
|
{
|
||||||
|
descendants.Add(child);
|
||||||
|
descendants.AddRange(GetAllDescendants(child.Id, allUsers));
|
||||||
|
}
|
||||||
|
|
||||||
|
return descendants;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculate depth for all descendants recursively
|
||||||
|
/// </summary>
|
||||||
|
private void CalculateDepths(long userId, List<User> allUsers, int currentDepth, Dictionary<long, int> depths, ref int maxDepth)
|
||||||
|
{
|
||||||
|
var children = allUsers.Where(x => x.NetworkParentId == userId).ToList();
|
||||||
|
|
||||||
|
foreach (var child in children)
|
||||||
|
{
|
||||||
|
var childDepth = currentDepth + 1;
|
||||||
|
depths[child.Id] = childDepth;
|
||||||
|
|
||||||
|
if (childDepth > maxDepth)
|
||||||
|
maxDepth = childDepth;
|
||||||
|
|
||||||
|
CalculateDepths(child.Id, allUsers, childDepth, depths, ref maxDepth);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-2
@@ -8,21 +8,37 @@ public class GetNetworkTreeQueryHandler : IRequestHandler<GetNetworkTreeQuery, N
|
|||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly ILogger<GetNetworkTreeQueryHandler> _logger;
|
private readonly ILogger<GetNetworkTreeQueryHandler> _logger;
|
||||||
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
|
||||||
public GetNetworkTreeQueryHandler(
|
public GetNetworkTreeQueryHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
ILogger<GetNetworkTreeQueryHandler> logger)
|
ILogger<GetNetworkTreeQueryHandler> logger,
|
||||||
|
ICurrentUserService currentUser)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_currentUser = currentUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<NetworkTreeDto?> Handle(GetNetworkTreeQuery request, CancellationToken cancellationToken)
|
public async Task<NetworkTreeDto?> Handle(GetNetworkTreeQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
// Get userId - use current user if UserId is 0
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new request with the resolved userId
|
||||||
|
var resolvedRequest = request with { UserId = userId };
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// دریافت نتایج flat از Stored Procedure
|
// دریافت نتایج flat از Stored Procedure
|
||||||
var flatNodes = await ExecuteStoredProcedureAsync(request, cancellationToken);
|
var flatNodes = await ExecuteStoredProcedureAsync(resolvedRequest, cancellationToken);
|
||||||
|
|
||||||
if (flatNodes == null || !flatNodes.Any())
|
if (flatNodes == null || !flatNodes.Any())
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-1
@@ -57,7 +57,7 @@ public class CreateNewOtpTokenCommandHandler : IRequestHandler<CreateNewOtpToken
|
|||||||
|
|
||||||
};
|
};
|
||||||
await _context.OtpTokens.AddAsync(entity, cancellationToken);
|
await _context.OtpTokens.AddAsync(entity, cancellationToken);
|
||||||
entity.AddDomainEvent(new CreateNewOtpTokenEvent(entity));
|
entity.AddDomainEvent(new CreateNewOtpTokenEvent(entity, code));
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
return new CreateNewOtpTokenResponseDto()
|
return new CreateNewOtpTokenResponseDto()
|
||||||
{
|
{
|
||||||
|
|||||||
+18
-4
@@ -1,3 +1,4 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Domain.Events;
|
using CMSMicroservice.Domain.Events;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@@ -6,16 +7,29 @@ namespace CMSMicroservice.Application.OtpTokenCQ.EventHandlers;
|
|||||||
public class CreateNewOtpTokenEventHandler : INotificationHandler<CreateNewOtpTokenEvent>
|
public class CreateNewOtpTokenEventHandler : INotificationHandler<CreateNewOtpTokenEvent>
|
||||||
{
|
{
|
||||||
private readonly ILogger<CreateNewOtpTokenEventHandler> _logger;
|
private readonly ILogger<CreateNewOtpTokenEventHandler> _logger;
|
||||||
|
private readonly IKavenegarService _kavenegarService;
|
||||||
|
|
||||||
public CreateNewOtpTokenEventHandler(ILogger<CreateNewOtpTokenEventHandler> logger)
|
public CreateNewOtpTokenEventHandler(
|
||||||
|
ILogger<CreateNewOtpTokenEventHandler> logger,
|
||||||
|
IKavenegarService kavenegarService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
namespace CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPackageDetails;
|
||||||
|
|
||||||
|
public class GetCustomerPackageDetailsQuery : IRequest<GetCustomerPackageDetailsResponseDto>
|
||||||
|
{
|
||||||
|
public long PackageId { get; set; }
|
||||||
|
}
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Exceptions;
|
||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Entities;
|
||||||
|
using Mapster;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPackageDetails;
|
||||||
|
|
||||||
|
public class GetCustomerPackageDetailsQueryHandler : IRequestHandler<GetCustomerPackageDetailsQuery, GetCustomerPackageDetailsResponseDto>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public GetCustomerPackageDetailsQueryHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GetCustomerPackageDetailsResponseDto> Handle(GetCustomerPackageDetailsQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var package = await _context.Packages
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.Id == request.PackageId)
|
||||||
|
.ProjectToType<GetCustomerPackageDetailsResponseDto>()
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (package == null)
|
||||||
|
throw new NotFoundException(nameof(Package), request.PackageId);
|
||||||
|
|
||||||
|
// Add features based on package (this could be stored in DB in future)
|
||||||
|
package.Features = new List<PackageFeatureDto>
|
||||||
|
{
|
||||||
|
new PackageFeatureDto
|
||||||
|
{
|
||||||
|
Title = "درآمد کمیسیون",
|
||||||
|
Description = "دریافت کمیسیون از فروش محصولات",
|
||||||
|
Icon = "commission",
|
||||||
|
IsHighlighted = true
|
||||||
|
},
|
||||||
|
new PackageFeatureDto
|
||||||
|
{
|
||||||
|
Title = "پشتیبانی 24/7",
|
||||||
|
Description = "دسترسی به پشتیبانی در تمام ساعات شبانه روز",
|
||||||
|
Icon = "support",
|
||||||
|
IsHighlighted = false
|
||||||
|
},
|
||||||
|
new PackageFeatureDto
|
||||||
|
{
|
||||||
|
Title = "آموزشهای تخصصی",
|
||||||
|
Description = "دسترسی به دورههای آموزشی و وبینارها",
|
||||||
|
Icon = "education",
|
||||||
|
IsHighlighted = true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set purchase requirements
|
||||||
|
package.Requirements = new PurchaseRequirementsDto
|
||||||
|
{
|
||||||
|
RequiresMembership = false,
|
||||||
|
MinimumWalletBalance = package.Price / 10, // 10% minimum
|
||||||
|
Restrictions = new List<string>
|
||||||
|
{
|
||||||
|
"باید حداقل 18 سال سن داشته باشید",
|
||||||
|
"تایید هویت الزامی است"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return package;
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
namespace CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPackageDetails;
|
||||||
|
|
||||||
|
public class GetCustomerPackageDetailsResponseDto
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
public long Price { get; set; }
|
||||||
|
public string ImagePath { get; set; }
|
||||||
|
public List<PackageFeatureDto> Features { get; set; } = new();
|
||||||
|
public PurchaseRequirementsDto Requirements { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PackageFeatureDto
|
||||||
|
{
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
public string Icon { get; set; }
|
||||||
|
public bool IsHighlighted { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PurchaseRequirementsDto
|
||||||
|
{
|
||||||
|
public bool RequiresMembership { get; set; }
|
||||||
|
public long MinimumWalletBalance { get; set; }
|
||||||
|
public List<string> Restrictions { get; set; } = new();
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
namespace CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPackages;
|
||||||
|
|
||||||
|
public class GetCustomerPackagesQuery : IRequest<List<GetCustomerPackagesResponseDto>>
|
||||||
|
{
|
||||||
|
public bool IncludeInactive { get; set; }
|
||||||
|
public int? PackageTypeFilter { get; set; }
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Entities;
|
||||||
|
using Mapster;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPackages;
|
||||||
|
|
||||||
|
public class GetCustomerPackagesQueryHandler : IRequestHandler<GetCustomerPackagesQuery, List<GetCustomerPackagesResponseDto>>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public GetCustomerPackagesQueryHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<GetCustomerPackagesResponseDto>> Handle(GetCustomerPackagesQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var query = _context.Packages
|
||||||
|
.AsNoTracking()
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
// Filter by PackageType if specified
|
||||||
|
if (request.PackageTypeFilter.HasValue)
|
||||||
|
{
|
||||||
|
// Note: Package entity doesn't have PackageType enum, so we filter by convention
|
||||||
|
// Assuming Title or Description contains the package type indicator
|
||||||
|
// If Package entity needs PackageType field, it should be added to migration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all packages (assuming all are available unless marked otherwise)
|
||||||
|
var packages = await query
|
||||||
|
.ProjectToType<GetCustomerPackagesResponseDto>()
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
// Map additional fields
|
||||||
|
foreach (var package in packages)
|
||||||
|
{
|
||||||
|
package.Name = package.Title;
|
||||||
|
package.ImageUrl = package.ImagePath;
|
||||||
|
package.Currency = "IRR";
|
||||||
|
package.IsAvailable = true;
|
||||||
|
package.ValidityDays = 365; // Default validity
|
||||||
|
package.IsPopular = false;
|
||||||
|
package.ShortDescription = package.Description?.Length > 100
|
||||||
|
? package.Description.Substring(0, 100) + "..."
|
||||||
|
: package.Description;
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages;
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
namespace CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPackages;
|
||||||
|
|
||||||
|
public class GetCustomerPackagesResponseDto
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
public long Price { get; set; }
|
||||||
|
public string Currency { get; set; } = "IRR";
|
||||||
|
public int PackageType { get; set; }
|
||||||
|
public bool IsAvailable { get; set; } = true;
|
||||||
|
public string ImageUrl { get; set; }
|
||||||
|
public string ImagePath { get; set; }
|
||||||
|
public int ValidityDays { get; set; }
|
||||||
|
public bool IsPopular { get; set; }
|
||||||
|
public string ShortDescription { get; set; }
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Models;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPurchaseHistory;
|
||||||
|
|
||||||
|
public class GetCustomerPurchaseHistoryQuery : IRequest<GetCustomerPurchaseHistoryResponseDto>
|
||||||
|
{
|
||||||
|
public long UserId { get; set; }
|
||||||
|
public PaginationState PaginationState { get; set; }
|
||||||
|
public int? PackageTypeFilter { get; set; }
|
||||||
|
public DateTime? FromDate { get; set; }
|
||||||
|
public DateTime? ToDate { get; set; }
|
||||||
|
}
|
||||||
+90
@@ -0,0 +1,90 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Extensions;
|
||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Application.Common.Models;
|
||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
using Mapster;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPurchaseHistory;
|
||||||
|
|
||||||
|
public class GetCustomerPurchaseHistoryQueryHandler : IRequestHandler<GetCustomerPurchaseHistoryQuery, GetCustomerPurchaseHistoryResponseDto>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
|
||||||
|
public GetCustomerPurchaseHistoryQueryHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
ICurrentUserService currentUser)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_currentUser = currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GetCustomerPurchaseHistoryResponseDto> Handle(GetCustomerPurchaseHistoryQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Resolve UserId from JWT if not specified
|
||||||
|
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.UserOrders
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.UserId == userId && x.PackageId != null)
|
||||||
|
.Include(x => x.Package)
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
// Apply date filters if specified
|
||||||
|
if (request.FromDate.HasValue)
|
||||||
|
query = query.Where(x => x.Created >= request.FromDate.Value);
|
||||||
|
|
||||||
|
if (request.ToDate.HasValue)
|
||||||
|
query = query.Where(x => x.Created <= request.ToDate.Value);
|
||||||
|
|
||||||
|
// Apply PackageType filter if needed (Package entity doesn't have Type enum currently)
|
||||||
|
// This would require Package entity to have a PackageType field
|
||||||
|
|
||||||
|
// Order by most recent first
|
||||||
|
query = query.OrderByDescending(x => x.Created);
|
||||||
|
|
||||||
|
// Get metadata
|
||||||
|
var metaData = await query.GetMetaData(request.PaginationState, cancellationToken);
|
||||||
|
|
||||||
|
// Get paginated results
|
||||||
|
var orders = await query
|
||||||
|
.PaginatedListAsync(request.PaginationState)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var purchases = orders.Select(order => new PackagePurchaseHistoryDto
|
||||||
|
{
|
||||||
|
Id = order.Id,
|
||||||
|
PackageId = order.PackageId ?? 0,
|
||||||
|
PackageName = order.Package?.Title ?? "نامشخص",
|
||||||
|
Amount = order.Amount,
|
||||||
|
PackageType = 0, // Default, needs Package.PackageType field
|
||||||
|
PurchaseDate = order.Created,
|
||||||
|
ExpiryDate = order.PaymentDate?.AddDays(365), // Assuming 1 year validity
|
||||||
|
Status = order.PaymentStatus,
|
||||||
|
StatusMessage = GetStatusMessage(order.PaymentStatus),
|
||||||
|
ReferenceCode = order.Transaction?.RefId ?? order.Id.ToString()
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
return new GetCustomerPurchaseHistoryResponseDto
|
||||||
|
{
|
||||||
|
MetaData = metaData,
|
||||||
|
Purchases = purchases
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetStatusMessage(PaymentStatus status)
|
||||||
|
{
|
||||||
|
return status switch
|
||||||
|
{
|
||||||
|
PaymentStatus.Pending => "در انتظار پرداخت",
|
||||||
|
PaymentStatus.Success => "فعال",
|
||||||
|
PaymentStatus.Reject => "رد شده",
|
||||||
|
_ => "نامشخص"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Models;
|
||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPurchaseHistory;
|
||||||
|
|
||||||
|
public class GetCustomerPurchaseHistoryResponseDto
|
||||||
|
{
|
||||||
|
public MetaData MetaData { get; set; }
|
||||||
|
public List<PackagePurchaseHistoryDto> Purchases { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PackagePurchaseHistoryDto
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public long PackageId { get; set; }
|
||||||
|
public string PackageName { get; set; }
|
||||||
|
public long Amount { get; set; }
|
||||||
|
public int PackageType { get; set; }
|
||||||
|
public DateTime PurchaseDate { get; set; }
|
||||||
|
public DateTime? ExpiryDate { get; set; }
|
||||||
|
public PaymentStatus Status { get; set; }
|
||||||
|
public string StatusMessage { get; set; }
|
||||||
|
public string ReferenceCode { get; set; }
|
||||||
|
}
|
||||||
+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;
|
||||||
|
}
|
||||||
-64
@@ -1,64 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.BulkUpdateProductPrices;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// بهروزرسانی دستهای قیمت محصولات
|
|
||||||
/// </summary>
|
|
||||||
public record BulkUpdateProductPricesCommand : IRequest<BulkUpdateProductPricesResponseDto>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// لیست محصولات و قیمتهای جدید
|
|
||||||
/// </summary>
|
|
||||||
public List<ProductPriceUpdate> Products { get; init; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// مدل بهروزرسانی قیمت یک محصول
|
|
||||||
/// </summary>
|
|
||||||
public class ProductPriceUpdate
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// شناسه محصول
|
|
||||||
/// </summary>
|
|
||||||
public long ProductId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// قیمت جدید (ریال)
|
|
||||||
/// </summary>
|
|
||||||
public long NewPrice { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// درصد تخفیف جدید (اختیاری)
|
|
||||||
/// </summary>
|
|
||||||
public int? NewDiscount { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// درصد تخفیف باشگاه جدید (اختیاری)
|
|
||||||
/// </summary>
|
|
||||||
public int? NewClubDiscountPercent { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// پاسخ بهروزرسانی دستهای قیمت
|
|
||||||
/// </summary>
|
|
||||||
public class BulkUpdateProductPricesResponseDto
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// تعداد محصولات بهروزرسانی شده
|
|
||||||
/// </summary>
|
|
||||||
public int UpdatedCount { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// تعداد محصولات ناموفق
|
|
||||||
/// </summary>
|
|
||||||
public int FailedCount { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// جزئیات خطاها
|
|
||||||
/// </summary>
|
|
||||||
public List<string> Errors { get; set; } = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// آیا همه موفق بودند
|
|
||||||
/// </summary>
|
|
||||||
public bool IsSuccess => FailedCount == 0;
|
|
||||||
}
|
|
||||||
-77
@@ -1,77 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.BulkUpdateProductPrices;
|
|
||||||
|
|
||||||
public class BulkUpdateProductPricesCommandHandler : IRequestHandler<BulkUpdateProductPricesCommand, BulkUpdateProductPricesResponseDto>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
private readonly ILogger<BulkUpdateProductPricesCommandHandler> _logger;
|
|
||||||
|
|
||||||
public BulkUpdateProductPricesCommandHandler(
|
|
||||||
IApplicationDbContext context,
|
|
||||||
ILogger<BulkUpdateProductPricesCommandHandler> logger)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<BulkUpdateProductPricesResponseDto> Handle(BulkUpdateProductPricesCommand request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var response = new BulkUpdateProductPricesResponseDto();
|
|
||||||
var productIds = request.Products.Select(p => p.ProductId).ToList();
|
|
||||||
|
|
||||||
// دریافت محصولات از دیتابیس
|
|
||||||
var products = await _context.Products
|
|
||||||
.Where(p => productIds.Contains(p.Id))
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
var productDict = products.ToDictionary(p => p.Id);
|
|
||||||
|
|
||||||
foreach (var update in request.Products)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!productDict.TryGetValue(update.ProductId, out var product))
|
|
||||||
{
|
|
||||||
response.FailedCount++;
|
|
||||||
response.Errors.Add($"محصول با شناسه {update.ProductId} یافت نشد");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// بهروزرسانی قیمت
|
|
||||||
product.Price = update.NewPrice;
|
|
||||||
|
|
||||||
// بهروزرسانی تخفیف (اگر ارسال شده باشد)
|
|
||||||
if (update.NewDiscount.HasValue)
|
|
||||||
{
|
|
||||||
product.Discount = update.NewDiscount.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// بهروزرسانی تخفیف باشگاه (اگر ارسال شده باشد)
|
|
||||||
if (update.NewClubDiscountPercent.HasValue)
|
|
||||||
{
|
|
||||||
product.ClubDiscountPercent = update.NewClubDiscountPercent.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
response.UpdatedCount++;
|
|
||||||
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Product {ProductId} price updated to {NewPrice} (Discount: {Discount}%, ClubDiscount: {ClubDiscount}%)",
|
|
||||||
product.Id, product.Price, product.Discount, product.ClubDiscountPercent);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
response.FailedCount++;
|
|
||||||
response.Errors.Add($"خطا در بهروزرسانی محصول {update.ProductId}: {ex.Message}");
|
|
||||||
_logger.LogError(ex, "Error updating product {ProductId} price", update.ProductId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.UpdatedCount > 0)
|
|
||||||
{
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
|
||||||
_logger.LogInformation("Bulk price update completed: {UpdatedCount} succeeded, {FailedCount} failed",
|
|
||||||
response.UpdatedCount, response.FailedCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-30
@@ -1,30 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.BulkUpdateProductPrices;
|
|
||||||
|
|
||||||
public class BulkUpdateProductPricesCommandValidator : AbstractValidator<BulkUpdateProductPricesCommand>
|
|
||||||
{
|
|
||||||
public BulkUpdateProductPricesCommandValidator()
|
|
||||||
{
|
|
||||||
RuleFor(x => x.Products)
|
|
||||||
.NotEmpty().WithMessage("لیست محصولات نمیتواند خالی باشد")
|
|
||||||
.Must(x => x.Count <= 100).WithMessage("حداکثر 100 محصول در هر بار قابل بهروزرسانی است");
|
|
||||||
|
|
||||||
RuleForEach(x => x.Products).ChildRules(product =>
|
|
||||||
{
|
|
||||||
product.RuleFor(p => p.ProductId)
|
|
||||||
.GreaterThan(0).WithMessage("شناسه محصول باید بزرگتر از 0 باشد");
|
|
||||||
|
|
||||||
product.RuleFor(p => p.NewPrice)
|
|
||||||
.GreaterThanOrEqualTo(0).WithMessage("قیمت نمیتواند منفی باشد");
|
|
||||||
|
|
||||||
product.RuleFor(p => p.NewDiscount)
|
|
||||||
.InclusiveBetween(0, 100)
|
|
||||||
.When(p => p.NewDiscount.HasValue)
|
|
||||||
.WithMessage("درصد تخفیف باید بین 0 تا 100 باشد");
|
|
||||||
|
|
||||||
product.RuleFor(p => p.NewClubDiscountPercent)
|
|
||||||
.InclusiveBetween(0, 100)
|
|
||||||
.When(p => p.NewClubDiscountPercent.HasValue)
|
|
||||||
.WithMessage("درصد تخفیف باشگاه باید بین 0 تا 100 باشد");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-80
@@ -1,80 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.BulkUpdateProductStock;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// بهروزرسانی دستهای موجودی محصولات
|
|
||||||
/// </summary>
|
|
||||||
public record BulkUpdateProductStockCommand : IRequest<BulkUpdateProductStockResponseDto>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// لیست محصولات و موجودیهای جدید
|
|
||||||
/// </summary>
|
|
||||||
public List<ProductStockUpdate> Products { get; init; } = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// نوع بهروزرسانی
|
|
||||||
/// </summary>
|
|
||||||
public StockUpdateType UpdateType { get; init; } = StockUpdateType.Set;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// نوع بهروزرسانی موجودی
|
|
||||||
/// </summary>
|
|
||||||
public enum StockUpdateType
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// تنظیم مقدار مطلق
|
|
||||||
/// </summary>
|
|
||||||
Set = 1,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// اضافه کردن به موجودی فعلی
|
|
||||||
/// </summary>
|
|
||||||
Add = 2,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// کم کردن از موجودی فعلی
|
|
||||||
/// </summary>
|
|
||||||
Subtract = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// مدل بهروزرسانی موجودی یک محصول
|
|
||||||
/// </summary>
|
|
||||||
public class ProductStockUpdate
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// شناسه محصول
|
|
||||||
/// </summary>
|
|
||||||
public long ProductId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// مقدار جدید/تغییر موجودی
|
|
||||||
/// </summary>
|
|
||||||
public int Quantity { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// پاسخ بهروزرسانی دستهای موجودی
|
|
||||||
/// </summary>
|
|
||||||
public class BulkUpdateProductStockResponseDto
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// تعداد محصولات بهروزرسانی شده
|
|
||||||
/// </summary>
|
|
||||||
public int UpdatedCount { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// تعداد محصولات ناموفق
|
|
||||||
/// </summary>
|
|
||||||
public int FailedCount { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// جزئیات خطاها
|
|
||||||
/// </summary>
|
|
||||||
public List<string> Errors { get; set; } = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// آیا همه موفق بودند
|
|
||||||
/// </summary>
|
|
||||||
public bool IsSuccess => FailedCount == 0;
|
|
||||||
}
|
|
||||||
-88
@@ -1,88 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.BulkUpdateProductStock;
|
|
||||||
|
|
||||||
public class BulkUpdateProductStockCommandHandler : IRequestHandler<BulkUpdateProductStockCommand, BulkUpdateProductStockResponseDto>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
private readonly ILogger<BulkUpdateProductStockCommandHandler> _logger;
|
|
||||||
|
|
||||||
public BulkUpdateProductStockCommandHandler(
|
|
||||||
IApplicationDbContext context,
|
|
||||||
ILogger<BulkUpdateProductStockCommandHandler> logger)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<BulkUpdateProductStockResponseDto> Handle(BulkUpdateProductStockCommand request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var response = new BulkUpdateProductStockResponseDto();
|
|
||||||
var productIds = request.Products.Select(p => p.ProductId).ToList();
|
|
||||||
|
|
||||||
// دریافت محصولات از دیتابیس
|
|
||||||
var products = await _context.Products
|
|
||||||
.Where(p => productIds.Contains(p.Id))
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
var productDict = products.ToDictionary(p => p.Id);
|
|
||||||
|
|
||||||
foreach (var update in request.Products)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!productDict.TryGetValue(update.ProductId, out var product))
|
|
||||||
{
|
|
||||||
response.FailedCount++;
|
|
||||||
response.Errors.Add($"محصول با شناسه {update.ProductId} یافت نشد");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var oldStock = product.RemainingCount;
|
|
||||||
|
|
||||||
// بهروزرسانی موجودی بر اساس نوع
|
|
||||||
switch (request.UpdateType)
|
|
||||||
{
|
|
||||||
case StockUpdateType.Set:
|
|
||||||
product.RemainingCount = update.Quantity;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case StockUpdateType.Add:
|
|
||||||
product.RemainingCount += update.Quantity;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case StockUpdateType.Subtract:
|
|
||||||
product.RemainingCount -= update.Quantity;
|
|
||||||
// جلوگیری از موجودی منفی
|
|
||||||
if (product.RemainingCount < 0)
|
|
||||||
{
|
|
||||||
response.FailedCount++;
|
|
||||||
response.Errors.Add($"محصول {update.ProductId}: موجودی منفی شد (موجودی فعلی: {oldStock}, کم کردن: {update.Quantity})");
|
|
||||||
product.RemainingCount = oldStock; // بازگرداندن مقدار قبلی
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
response.UpdatedCount++;
|
|
||||||
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Product {ProductId} stock updated from {OldStock} to {NewStock} (Type: {UpdateType})",
|
|
||||||
product.Id, oldStock, product.RemainingCount, request.UpdateType);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
response.FailedCount++;
|
|
||||||
response.Errors.Add($"خطا در بهروزرسانی محصول {update.ProductId}: {ex.Message}");
|
|
||||||
_logger.LogError(ex, "Error updating product {ProductId} stock", update.ProductId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.UpdatedCount > 0)
|
|
||||||
{
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
|
||||||
_logger.LogInformation("Bulk stock update completed: {UpdatedCount} succeeded, {FailedCount} failed",
|
|
||||||
response.UpdatedCount, response.FailedCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-22
@@ -1,22 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.BulkUpdateProductStock;
|
|
||||||
|
|
||||||
public class BulkUpdateProductStockCommandValidator : AbstractValidator<BulkUpdateProductStockCommand>
|
|
||||||
{
|
|
||||||
public BulkUpdateProductStockCommandValidator()
|
|
||||||
{
|
|
||||||
RuleFor(x => x.Products)
|
|
||||||
.NotEmpty().WithMessage("لیست محصولات نمیتواند خالی باشد")
|
|
||||||
.Must(x => x.Count <= 100).WithMessage("حداکثر 100 محصول در هر بار قابل بهروزرسانی است");
|
|
||||||
|
|
||||||
RuleForEach(x => x.Products).ChildRules(product =>
|
|
||||||
{
|
|
||||||
product.RuleFor(p => p.ProductId)
|
|
||||||
.GreaterThan(0).WithMessage("شناسه محصول باید بزرگتر از 0 باشد");
|
|
||||||
|
|
||||||
// برای Set mode، مقدار نمیتواند منفی باشد (چک در Handler انجام میشود)
|
|
||||||
product.RuleFor(p => p.Quantity)
|
|
||||||
.GreaterThanOrEqualTo(-10000)
|
|
||||||
.WithMessage("مقدار موجودی نامعتبر است");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+16
-19
@@ -1,29 +1,26 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.CreateNewProducts;
|
namespace CMSMicroservice.Application.ProductsCQ.Commands.CreateNewProducts;
|
||||||
|
|
||||||
public record CreateNewProductsCommand : IRequest<CreateNewProductsResponseDto>
|
public record CreateNewProductsCommand : IRequest<CreateNewProductsResponseDto>
|
||||||
{
|
{
|
||||||
//
|
public string Title { get; init; } = string.Empty;
|
||||||
public string Title { get; init; }
|
public string Description { get; init; } = string.Empty;
|
||||||
//
|
public string ShortInfomation { get; init; } = string.Empty;
|
||||||
public string Description { get; init; }
|
public string FullInformation { get; init; } = string.Empty;
|
||||||
//
|
|
||||||
public string ShortInfomation { get; init; }
|
|
||||||
//
|
|
||||||
public string FullInformation { get; init; }
|
|
||||||
//
|
|
||||||
public long Price { get; init; }
|
public long Price { get; init; }
|
||||||
//
|
|
||||||
public int Discount { get; init; }
|
public int Discount { get; init; }
|
||||||
//
|
|
||||||
public int Rate { get; init; }
|
public int Rate { get; init; }
|
||||||
//
|
public string? ImagePath { get; init; }
|
||||||
public string ImagePath { get; init; }
|
public string? ThumbnailPath { get; init; }
|
||||||
//
|
|
||||||
public string ThumbnailPath { get; init; }
|
|
||||||
//
|
|
||||||
public int SaleCount { get; init; }
|
public int SaleCount { get; init; }
|
||||||
//
|
|
||||||
public int ViewCount { get; init; }
|
public int ViewCount { get; init; }
|
||||||
// لیست شناسه دستهبندیهای محصول
|
public int RemainingCount { get; init; }
|
||||||
public ICollection<long>? CategoryIds { 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; }
|
||||||
}
|
}
|
||||||
|
|||||||
+82
-29
@@ -1,59 +1,112 @@
|
|||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Domain.Entities;
|
using CMSMicroservice.Domain.Entities;
|
||||||
using CMSMicroservice.Domain.Enums;
|
using Microsoft.Extensions.Logging;
|
||||||
using CMSMicroservice.Domain.Events;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.CreateNewProducts;
|
namespace CMSMicroservice.Application.ProductsCQ.Commands.CreateNewProducts;
|
||||||
|
|
||||||
public class CreateNewProductsCommandHandler : IRequestHandler<CreateNewProductsCommand, CreateNewProductsResponseDto>
|
public class CreateNewProductsCommandHandler : IRequestHandler<CreateNewProductsCommand, CreateNewProductsResponseDto>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly IInventoryService _inventoryService;
|
private readonly IFileManagementService _fileManagementService;
|
||||||
|
private readonly ILogger<CreateNewProductsCommandHandler> _logger;
|
||||||
|
|
||||||
public CreateNewProductsCommandHandler(
|
public CreateNewProductsCommandHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
IInventoryService inventoryService)
|
IFileManagementService fileManagementService,
|
||||||
|
ILogger<CreateNewProductsCommandHandler> logger)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_inventoryService = inventoryService;
|
_fileManagementService = fileManagementService;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CreateNewProductsResponseDto> Handle(CreateNewProductsCommand request,
|
public async Task<CreateNewProductsResponseDto> Handle(CreateNewProductsCommand request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var entity = request.Adapt<Product>();
|
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.Products.AddAsync(entity, cancellationToken);
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
// ایجاد رکورد موجودی در سیستم انبارداری با موجودی اولیه صفر
|
// Handle category assignments
|
||||||
await _inventoryService.InitializeInventoryAsync(
|
|
||||||
entity.Id,
|
|
||||||
ProductType.RegularProduct,
|
|
||||||
0, // موجودی اولیه صفر - باید از طریق Inventory اضافه شود
|
|
||||||
ct: cancellationToken);
|
|
||||||
|
|
||||||
// ثبت دستهبندیهای محصول (در صورت ارسال)
|
|
||||||
if (request.CategoryIds is { Count: > 0 })
|
if (request.CategoryIds is { Count: > 0 })
|
||||||
{
|
{
|
||||||
var distinctCategoryIds = request.CategoryIds
|
foreach (var categoryId in request.CategoryIds)
|
||||||
.Where(id => id > 0)
|
|
||||||
.Distinct()
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
foreach (var categoryId in distinctCategoryIds)
|
|
||||||
{
|
{
|
||||||
var rel = new ProductCategory
|
var categoryExists = await _context.Categories
|
||||||
{
|
.AnyAsync(c => c.Id == categoryId, cancellationToken);
|
||||||
ProductId = entity.Id,
|
|
||||||
CategoryId = categoryId
|
|
||||||
};
|
|
||||||
await _context.ProductCategories.AddAsync(rel, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (categoryExists)
|
||||||
|
{
|
||||||
|
await _context.ProductCategories.AddAsync(new ProductCategory
|
||||||
|
{
|
||||||
|
ProductId = entity.Id,
|
||||||
|
CategoryId = categoryId
|
||||||
|
}, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
entity.AddDomainEvent(new CreateNewProductsEvent(entity));
|
return new CreateNewProductsResponseDto { Id = entity.Id };
|
||||||
return entity.Adapt<CreateNewProductsResponseDto>();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-20
@@ -1,34 +1,24 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.CreateNewProducts;
|
namespace CMSMicroservice.Application.ProductsCQ.Commands.CreateNewProducts;
|
||||||
|
|
||||||
public class CreateNewProductsCommandValidator : AbstractValidator<CreateNewProductsCommand>
|
public class CreateNewProductsCommandValidator : AbstractValidator<CreateNewProductsCommand>
|
||||||
{
|
{
|
||||||
public CreateNewProductsCommandValidator()
|
public CreateNewProductsCommandValidator()
|
||||||
{
|
{
|
||||||
RuleFor(model => model.Title)
|
RuleFor(model => model.Title)
|
||||||
.NotEmpty();
|
.NotEmpty().WithMessage("عنوان محصول الزامی است");
|
||||||
RuleFor(model => model.Description)
|
|
||||||
.NotEmpty();
|
|
||||||
RuleFor(model => model.ShortInfomation)
|
|
||||||
.NotEmpty();
|
|
||||||
RuleFor(model => model.FullInformation)
|
|
||||||
.NotEmpty();
|
|
||||||
RuleFor(model => model.Price)
|
RuleFor(model => model.Price)
|
||||||
.NotNull();
|
.GreaterThanOrEqualTo(0).WithMessage("قیمت نمیتواند منفی باشد");
|
||||||
|
|
||||||
RuleFor(model => model.Discount)
|
RuleFor(model => model.Discount)
|
||||||
.NotNull();
|
.InclusiveBetween(0, 100).WithMessage("تخفیف باید بین 0 تا 100 باشد");
|
||||||
RuleFor(model => model.Rate)
|
|
||||||
.NotNull();
|
|
||||||
RuleFor(model => model.ImagePath)
|
|
||||||
.NotEmpty();
|
|
||||||
RuleFor(model => model.ThumbnailPath)
|
|
||||||
.NotEmpty();
|
|
||||||
RuleFor(model => model.SaleCount)
|
|
||||||
.NotNull();
|
|
||||||
RuleFor(model => model.ViewCount)
|
|
||||||
.NotNull();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||||
{
|
{
|
||||||
var result = await ValidateAsync(ValidationContext<CreateNewProductsCommand>.CreateWithOptions((CreateNewProductsCommand)model, x => x.IncludeProperties(propertyName)));
|
var result = await ValidateAsync(
|
||||||
|
ValidationContext<CreateNewProductsCommand>.CreateWithOptions(
|
||||||
|
(CreateNewProductsCommand)model, x => x.IncludeProperties(propertyName)));
|
||||||
if (result.IsValid)
|
if (result.IsValid)
|
||||||
return Array.Empty<string>();
|
return Array.Empty<string>();
|
||||||
return result.Errors.Select(e => e.ErrorMessage);
|
return result.Errors.Select(e => e.ErrorMessage);
|
||||||
|
|||||||
+1
-2
@@ -1,7 +1,6 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.CreateNewProducts;
|
namespace CMSMicroservice.Application.ProductsCQ.Commands.CreateNewProducts;
|
||||||
|
|
||||||
public class CreateNewProductsResponseDto
|
public class CreateNewProductsResponseDto
|
||||||
{
|
{
|
||||||
//
|
|
||||||
public long Id { get; set; }
|
public long Id { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
+1
-2
@@ -1,7 +1,6 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.DeleteProducts;
|
namespace CMSMicroservice.Application.ProductsCQ.Commands.DeleteProducts;
|
||||||
|
|
||||||
public record DeleteProductsCommand : IRequest<Unit>
|
public record DeleteProductsCommand : IRequest<Unit>
|
||||||
{
|
{
|
||||||
//
|
|
||||||
public long Id { get; init; }
|
public long Id { get; init; }
|
||||||
|
|
||||||
}
|
}
|
||||||
+21
-5
@@ -1,5 +1,8 @@
|
|||||||
using CMSMicroservice.Domain.Events;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Entities;
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.DeleteProducts;
|
namespace CMSMicroservice.Application.ProductsCQ.Commands.DeleteProducts;
|
||||||
|
|
||||||
public class DeleteProductsCommandHandler : IRequestHandler<DeleteProductsCommand, Unit>
|
public class DeleteProductsCommandHandler : IRequestHandler<DeleteProductsCommand, Unit>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
@@ -12,11 +15,24 @@ public class DeleteProductsCommandHandler : IRequestHandler<DeleteProductsComman
|
|||||||
public async Task<Unit> Handle(DeleteProductsCommand request, CancellationToken cancellationToken)
|
public async Task<Unit> Handle(DeleteProductsCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var entity = await _context.Products
|
var entity = await _context.Products
|
||||||
.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken) ?? throw new NotFoundException(nameof(Product), request.Id);
|
.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken)
|
||||||
entity.IsDeleted = true;
|
?? throw new NotFoundException(nameof(Product), request.Id);
|
||||||
_context.Products.Update(entity);
|
|
||||||
entity.AddDomainEvent(new DeleteProductsEvent(entity));
|
// 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);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
return Unit.Value;
|
return Unit.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-16
@@ -1,16 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.DeleteProducts;
|
|
||||||
public class DeleteProductsCommandValidator : AbstractValidator<DeleteProductsCommand>
|
|
||||||
{
|
|
||||||
public DeleteProductsCommandValidator()
|
|
||||||
{
|
|
||||||
RuleFor(model => model.Id)
|
|
||||||
.NotNull();
|
|
||||||
}
|
|
||||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
|
||||||
{
|
|
||||||
var result = await ValidateAsync(ValidationContext<DeleteProductsCommand>.CreateWithOptions((DeleteProductsCommand)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.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
-49
@@ -1,49 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.ToggleProductStatus;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// فعال/غیرفعال کردن دستهای محصولات
|
|
||||||
/// (با تنظیم موجودی به 0 برای غیرفعال کردن)
|
|
||||||
/// </summary>
|
|
||||||
public record ToggleProductStatusCommand : IRequest<ToggleProductStatusResponseDto>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// لیست شناسه محصولات
|
|
||||||
/// </summary>
|
|
||||||
public List<long> ProductIds { get; init; } = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// فعال کردن یا غیرفعال کردن
|
|
||||||
/// </summary>
|
|
||||||
public bool Enable { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// موجودی پیشفرض برای فعالسازی (پیشفرض: 1)
|
|
||||||
/// </summary>
|
|
||||||
public int DefaultStock { get; init; } = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// پاسخ فعال/غیرفعال کردن دستهای
|
|
||||||
/// </summary>
|
|
||||||
public class ToggleProductStatusResponseDto
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// تعداد محصولات بهروزرسانی شده
|
|
||||||
/// </summary>
|
|
||||||
public int UpdatedCount { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// تعداد محصولات ناموفق
|
|
||||||
/// </summary>
|
|
||||||
public int FailedCount { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// جزئیات خطاها
|
|
||||||
/// </summary>
|
|
||||||
public List<string> Errors { get; set; } = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// آیا همه موفق بودند
|
|
||||||
/// </summary>
|
|
||||||
public bool IsSuccess => FailedCount == 0;
|
|
||||||
}
|
|
||||||
-82
@@ -1,82 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.ToggleProductStatus;
|
|
||||||
|
|
||||||
public class ToggleProductStatusCommandHandler : IRequestHandler<ToggleProductStatusCommand, ToggleProductStatusResponseDto>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
private readonly ILogger<ToggleProductStatusCommandHandler> _logger;
|
|
||||||
|
|
||||||
public ToggleProductStatusCommandHandler(
|
|
||||||
IApplicationDbContext context,
|
|
||||||
ILogger<ToggleProductStatusCommandHandler> logger)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ToggleProductStatusResponseDto> Handle(ToggleProductStatusCommand request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var response = new ToggleProductStatusResponseDto();
|
|
||||||
|
|
||||||
// دریافت محصولات از دیتابیس
|
|
||||||
var products = await _context.Products
|
|
||||||
.Where(p => request.ProductIds.Contains(p.Id))
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
if (products.Count == 0)
|
|
||||||
{
|
|
||||||
response.Errors.Add("هیچ محصولی با شناسههای داده شده یافت نشد");
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var product in products)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (request.Enable)
|
|
||||||
{
|
|
||||||
// فعالسازی: اگر موجودی 0 است، آن را به مقدار پیشفرض تنظیم کن
|
|
||||||
if (product.RemainingCount == 0)
|
|
||||||
{
|
|
||||||
product.RemainingCount = request.DefaultStock;
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Product {ProductId} enabled with stock {Stock}",
|
|
||||||
product.Id, request.DefaultStock);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Product {ProductId} already has stock {Stock}, no change needed",
|
|
||||||
product.Id, product.RemainingCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// غیرفعالسازی: موجودی را به 0 تنظیم کن
|
|
||||||
var oldStock = product.RemainingCount;
|
|
||||||
product.RemainingCount = 0;
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Product {ProductId} disabled (stock changed from {OldStock} to 0)",
|
|
||||||
product.Id, oldStock);
|
|
||||||
}
|
|
||||||
|
|
||||||
response.UpdatedCount++;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
response.FailedCount++;
|
|
||||||
response.Errors.Add($"خطا در بهروزرسانی محصول {product.Id}: {ex.Message}");
|
|
||||||
_logger.LogError(ex, "Error toggling product {ProductId} status", product.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.UpdatedCount > 0)
|
|
||||||
{
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Toggle product status completed: {UpdatedCount} succeeded, {FailedCount} failed (Enable: {Enable})",
|
|
||||||
response.UpdatedCount, response.FailedCount, request.Enable);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-16
@@ -1,16 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.ToggleProductStatus;
|
|
||||||
|
|
||||||
public class ToggleProductStatusCommandValidator : AbstractValidator<ToggleProductStatusCommand>
|
|
||||||
{
|
|
||||||
public ToggleProductStatusCommandValidator()
|
|
||||||
{
|
|
||||||
RuleFor(x => x.ProductIds)
|
|
||||||
.NotEmpty().WithMessage("لیست محصولات نمیتواند خالی باشد")
|
|
||||||
.Must(x => x.Count <= 100).WithMessage("حداکثر 100 محصول در هر بار قابل بهروزرسانی است");
|
|
||||||
|
|
||||||
RuleFor(x => x.DefaultStock)
|
|
||||||
.GreaterThanOrEqualTo(0)
|
|
||||||
.When(x => x.Enable)
|
|
||||||
.WithMessage("موجودی پیشفرض نمیتواند منفی باشد");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-36
@@ -1,36 +0,0 @@
|
|||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.UpdateProductBulk;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// دستور بهروزرسانی گروهی محصولات
|
|
||||||
/// Admin میتواند چندین محصول را همزمان ویرایش کند
|
|
||||||
/// </summary>
|
|
||||||
public class UpdateProductBulkCommand : IRequest<UpdateProductBulkResponseDto>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// لیست شناسه محصولات برای بهروزرسانی
|
|
||||||
/// </summary>
|
|
||||||
public List<long> ProductIds { get; set; } = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// قیمت جدید (اختیاری - اگر null باشد تغییر نمیکند)
|
|
||||||
/// </summary>
|
|
||||||
public long? NewPrice { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// درصد افزایش/کاهش قیمت (اختیاری)
|
|
||||||
/// مثلاً: 10 = افزایش 10%، -15 = کاهش 15%
|
|
||||||
/// </summary>
|
|
||||||
public decimal? PriceChangePercent { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// موجودی (اختیاری)
|
|
||||||
/// </summary>
|
|
||||||
public int? Stock { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// افزودن مقدار به موجودی (اختیاری)
|
|
||||||
/// </summary>
|
|
||||||
public int? StockIncrement { get; set; }
|
|
||||||
}
|
|
||||||
-92
@@ -1,92 +0,0 @@
|
|||||||
using CMSMicroservice.Application.Common.Interfaces;
|
|
||||||
using MediatR;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.UpdateProductBulk;
|
|
||||||
|
|
||||||
public class UpdateProductBulkCommandHandler : IRequestHandler<UpdateProductBulkCommand, UpdateProductBulkResponseDto>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
private readonly ILogger<UpdateProductBulkCommandHandler> _logger;
|
|
||||||
|
|
||||||
public UpdateProductBulkCommandHandler(
|
|
||||||
IApplicationDbContext context,
|
|
||||||
ILogger<UpdateProductBulkCommandHandler> logger)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<UpdateProductBulkResponseDto> Handle(UpdateProductBulkCommand request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var response = new UpdateProductBulkResponseDto
|
|
||||||
{
|
|
||||||
TotalRequested = request.ProductIds.Count
|
|
||||||
};
|
|
||||||
|
|
||||||
var products = await _context.Products
|
|
||||||
.Where(x => request.ProductIds.Contains(x.Id) && !x.IsDeleted)
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
if (products.Count == 0)
|
|
||||||
{
|
|
||||||
response.Errors.Add("هیچ محصولی با شناسههای داده شده یافت نشد");
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var product in products)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// تغییر قیمت
|
|
||||||
if (request.NewPrice.HasValue)
|
|
||||||
{
|
|
||||||
product.Price = request.NewPrice.Value;
|
|
||||||
}
|
|
||||||
else if (request.PriceChangePercent.HasValue)
|
|
||||||
{
|
|
||||||
var changeAmount = (long)(product.Price * (request.PriceChangePercent.Value / 100));
|
|
||||||
product.Price += changeAmount;
|
|
||||||
|
|
||||||
// اطمینان از مثبت بودن قیمت
|
|
||||||
if (product.Price < 0)
|
|
||||||
product.Price = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// تغییر موجودی
|
|
||||||
if (request.Stock.HasValue)
|
|
||||||
{
|
|
||||||
product.RemainingCount = request.Stock.Value;
|
|
||||||
}
|
|
||||||
else if (request.StockIncrement.HasValue)
|
|
||||||
{
|
|
||||||
product.RemainingCount += request.StockIncrement.Value;
|
|
||||||
|
|
||||||
// اطمینان از غیرمنفی بودن موجودی
|
|
||||||
if (product.RemainingCount < 0)
|
|
||||||
product.RemainingCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
response.UpdatedProductIds.Add(product.Id);
|
|
||||||
response.SuccessCount++;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
response.Errors.Add($"خطا در بهروزرسانی محصول {product.Id}: {ex.Message}");
|
|
||||||
response.FailedCount++;
|
|
||||||
_logger.LogError(ex, "Error updating product {ProductId}", product.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
|
||||||
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Bulk update completed. Success: {Success}, Failed: {Failed}",
|
|
||||||
response.SuccessCount,
|
|
||||||
response.FailedCount
|
|
||||||
);
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-40
@@ -1,40 +0,0 @@
|
|||||||
using FluentValidation;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.UpdateProductBulk;
|
|
||||||
|
|
||||||
public class UpdateProductBulkCommandValidator : AbstractValidator<UpdateProductBulkCommand>
|
|
||||||
{
|
|
||||||
public UpdateProductBulkCommandValidator()
|
|
||||||
{
|
|
||||||
RuleFor(x => x.ProductIds)
|
|
||||||
.NotEmpty().WithMessage("حداقل یک محصول باید انتخاب شود")
|
|
||||||
.Must(x => x.Count <= 100).WithMessage("حداکثر 100 محصول را میتوان همزمان بهروزرسانی کرد");
|
|
||||||
|
|
||||||
RuleFor(x => x.NewPrice)
|
|
||||||
.GreaterThan(0).WithMessage("قیمت باید بزرگتر از صفر باشد")
|
|
||||||
.LessThanOrEqualTo(1_000_000_000).WithMessage("قیمت نامعتبر است")
|
|
||||||
.When(x => x.NewPrice.HasValue);
|
|
||||||
|
|
||||||
RuleFor(x => x.PriceChangePercent)
|
|
||||||
.GreaterThanOrEqualTo(-100).WithMessage("درصد تخفیف نمیتواند بیشتر از 100% باشد")
|
|
||||||
.LessThanOrEqualTo(1000).WithMessage("درصد افزایش نامعتبر است")
|
|
||||||
.When(x => x.PriceChangePercent.HasValue);
|
|
||||||
|
|
||||||
RuleFor(x => x.Stock)
|
|
||||||
.GreaterThanOrEqualTo(0).WithMessage("موجودی نمیتواند منفی باشد")
|
|
||||||
.When(x => x.Stock.HasValue);
|
|
||||||
|
|
||||||
RuleFor(x => x)
|
|
||||||
.Must(x => x.NewPrice.HasValue || x.PriceChangePercent.HasValue ||
|
|
||||||
x.Stock.HasValue || x.StockIncrement.HasValue)
|
|
||||||
.WithMessage("حداقل یک فیلد برای بهروزرسانی باید مشخص شود");
|
|
||||||
|
|
||||||
RuleFor(x => x)
|
|
||||||
.Must(x => !(x.NewPrice.HasValue && x.PriceChangePercent.HasValue))
|
|
||||||
.WithMessage("نمیتوان همزمان قیمت جدید و درصد تغییر قیمت را مشخص کرد");
|
|
||||||
|
|
||||||
RuleFor(x => x)
|
|
||||||
.Must(x => !(x.Stock.HasValue && x.StockIncrement.HasValue))
|
|
||||||
.WithMessage("نمیتوان همزمان موجودی جدید و افزایش موجودی را مشخص کرد");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.UpdateProductBulk;
|
|
||||||
|
|
||||||
public class UpdateProductBulkResponseDto
|
|
||||||
{
|
|
||||||
public int TotalRequested { get; set; }
|
|
||||||
public int SuccessCount { get; set; }
|
|
||||||
public int FailedCount { get; set; }
|
|
||||||
public List<long> UpdatedProductIds { get; set; } = new();
|
|
||||||
public List<string> Errors { get; set; } = new();
|
|
||||||
}
|
|
||||||
+16
-20
@@ -1,31 +1,27 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.UpdateProducts;
|
namespace CMSMicroservice.Application.ProductsCQ.Commands.UpdateProducts;
|
||||||
|
|
||||||
public record UpdateProductsCommand : IRequest<Unit>
|
public record UpdateProductsCommand : IRequest<Unit>
|
||||||
{
|
{
|
||||||
//
|
|
||||||
public long Id { get; init; }
|
public long Id { get; init; }
|
||||||
//
|
public string Title { get; init; } = string.Empty;
|
||||||
public string Title { get; init; }
|
public string Description { get; init; } = string.Empty;
|
||||||
//
|
public string ShortInfomation { get; init; } = string.Empty;
|
||||||
public string Description { get; init; }
|
public string FullInformation { get; init; } = string.Empty;
|
||||||
//
|
|
||||||
public string ShortInfomation { get; init; }
|
|
||||||
//
|
|
||||||
public string FullInformation { get; init; }
|
|
||||||
//
|
|
||||||
public long Price { get; init; }
|
public long Price { get; init; }
|
||||||
//
|
|
||||||
public int Discount { get; init; }
|
public int Discount { get; init; }
|
||||||
//
|
|
||||||
public int Rate { get; init; }
|
public int Rate { get; init; }
|
||||||
//
|
public string? ImagePath { get; init; }
|
||||||
public string ImagePath { get; init; }
|
public string? ThumbnailPath { get; init; }
|
||||||
//
|
|
||||||
public string ThumbnailPath { get; init; }
|
|
||||||
//
|
|
||||||
public int SaleCount { get; init; }
|
public int SaleCount { get; init; }
|
||||||
//
|
|
||||||
public int ViewCount { get; init; }
|
public int ViewCount { get; init; }
|
||||||
// لیست شناسه دستهبندیهای محصول
|
public int RemainingCount { get; init; }
|
||||||
public ICollection<long>? CategoryIds { 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; }
|
||||||
}
|
}
|
||||||
|
|||||||
+99
-36
@@ -1,64 +1,127 @@
|
|||||||
using CMSMicroservice.Application.Common.Exceptions;
|
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Domain.Entities;
|
using CMSMicroservice.Domain.Entities;
|
||||||
using CMSMicroservice.Domain.Events;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.UpdateProducts;
|
namespace CMSMicroservice.Application.ProductsCQ.Commands.UpdateProducts;
|
||||||
|
|
||||||
public class UpdateProductsCommandHandler : IRequestHandler<UpdateProductsCommand, Unit>
|
public class UpdateProductsCommandHandler : IRequestHandler<UpdateProductsCommand, Unit>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly IFileManagementService _fileManagementService;
|
||||||
|
private readonly ILogger<UpdateProductsCommandHandler> _logger;
|
||||||
|
|
||||||
public UpdateProductsCommandHandler(IApplicationDbContext context)
|
public UpdateProductsCommandHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
IFileManagementService fileManagementService,
|
||||||
|
ILogger<UpdateProductsCommandHandler> logger)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_fileManagementService = fileManagementService;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Unit> Handle(UpdateProductsCommand request, CancellationToken cancellationToken)
|
public async Task<Unit> Handle(UpdateProductsCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var entity = await _context.Products
|
var entity = await _context.Products
|
||||||
.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken)
|
.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken)
|
||||||
?? throw new NotFoundException(nameof(Product), request.Id);
|
?? throw new NotFoundException(nameof(Product), request.Id);
|
||||||
|
|
||||||
request.Adapt(entity);
|
// Update basic properties
|
||||||
_context.Products.Update(entity);
|
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;
|
||||||
|
|
||||||
// بهروزرسانی دستهبندیهای محصول در صورت ارسال CategoryIds
|
// Handle image upload to FMS if new file bytes provided
|
||||||
if (request.CategoryIds is not null)
|
if (request.ImageFileBytes is { Length: > 0 })
|
||||||
{
|
{
|
||||||
var targetIds = (request.CategoryIds ?? Array.Empty<long>())
|
try
|
||||||
.Where(id => id > 0)
|
|
||||||
.Distinct()
|
|
||||||
.ToHashSet();
|
|
||||||
|
|
||||||
var existingRelations = await _context.ProductCategories
|
|
||||||
.Where(x => x.ProductId == entity.Id)
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
var existingIds = existingRelations
|
|
||||||
.Select(x => x.CategoryId)
|
|
||||||
.ToHashSet();
|
|
||||||
|
|
||||||
var toAdd = targetIds.Except(existingIds).ToList();
|
|
||||||
var toRemove = existingRelations.Where(x => !targetIds.Contains(x.CategoryId)).ToList();
|
|
||||||
|
|
||||||
foreach (var categoryId in toAdd)
|
|
||||||
{
|
{
|
||||||
var rel = new ProductCategory
|
var (mainPath, thumbPath) = await _fileManagementService.UploadImageWithThumbnailAsync(
|
||||||
{
|
"Images/Products",
|
||||||
ProductId = entity.Id,
|
request.ImageFileBytes,
|
||||||
CategoryId = categoryId
|
request.ImageFileMime ?? "image/jpeg",
|
||||||
};
|
request.ImageFileName,
|
||||||
await _context.ProductCategories.AddAsync(rel, cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(mainPath))
|
||||||
|
entity.ImagePath = mainPath;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(thumbPath))
|
||||||
|
entity.ThumbnailPath = thumbPath;
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
if (toRemove.Count > 0)
|
|
||||||
{
|
{
|
||||||
_context.ProductCategories.RemoveRange(toRemove);
|
_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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
entity.AddDomainEvent(new UpdateProductsEvent(entity));
|
_context.Products.Update(entity);
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
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;
|
return Unit.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-21
@@ -1,36 +1,27 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.UpdateProducts;
|
namespace CMSMicroservice.Application.ProductsCQ.Commands.UpdateProducts;
|
||||||
|
|
||||||
public class UpdateProductsCommandValidator : AbstractValidator<UpdateProductsCommand>
|
public class UpdateProductsCommandValidator : AbstractValidator<UpdateProductsCommand>
|
||||||
{
|
{
|
||||||
public UpdateProductsCommandValidator()
|
public UpdateProductsCommandValidator()
|
||||||
{
|
{
|
||||||
RuleFor(model => model.Id)
|
RuleFor(model => model.Id)
|
||||||
.NotNull();
|
.NotNull().WithMessage("شناسه محصول الزامی است");
|
||||||
|
|
||||||
RuleFor(model => model.Title)
|
RuleFor(model => model.Title)
|
||||||
.NotEmpty();
|
.NotEmpty().WithMessage("عنوان محصول الزامی است");
|
||||||
RuleFor(model => model.Description)
|
|
||||||
.NotEmpty();
|
|
||||||
RuleFor(model => model.ShortInfomation)
|
|
||||||
.NotEmpty();
|
|
||||||
RuleFor(model => model.FullInformation)
|
|
||||||
.NotEmpty();
|
|
||||||
RuleFor(model => model.Price)
|
RuleFor(model => model.Price)
|
||||||
.NotNull();
|
.GreaterThanOrEqualTo(0).WithMessage("قیمت نمیتواند منفی باشد");
|
||||||
|
|
||||||
RuleFor(model => model.Discount)
|
RuleFor(model => model.Discount)
|
||||||
.NotNull();
|
.InclusiveBetween(0, 100).WithMessage("تخفیف باید بین 0 تا 100 باشد");
|
||||||
RuleFor(model => model.Rate)
|
|
||||||
.NotNull();
|
|
||||||
RuleFor(model => model.ImagePath)
|
|
||||||
.NotEmpty();
|
|
||||||
RuleFor(model => model.ThumbnailPath)
|
|
||||||
.NotEmpty();
|
|
||||||
RuleFor(model => model.SaleCount)
|
|
||||||
.NotNull();
|
|
||||||
RuleFor(model => model.ViewCount)
|
|
||||||
.NotNull();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||||
{
|
{
|
||||||
var result = await ValidateAsync(ValidationContext<UpdateProductsCommand>.CreateWithOptions((UpdateProductsCommand)model, x => x.IncludeProperties(propertyName)));
|
var result = await ValidateAsync(
|
||||||
|
ValidationContext<UpdateProductsCommand>.CreateWithOptions(
|
||||||
|
(UpdateProductsCommand)model, x => x.IncludeProperties(propertyName)));
|
||||||
if (result.IsValid)
|
if (result.IsValid)
|
||||||
return Array.Empty<string>();
|
return Array.Empty<string>();
|
||||||
return result.Errors.Select(e => e.ErrorMessage);
|
return result.Errors.Select(e => e.ErrorMessage);
|
||||||
|
|||||||
-22
@@ -1,22 +0,0 @@
|
|||||||
using CMSMicroservice.Domain.Events;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.ProductsCQ.EventHandlers;
|
|
||||||
|
|
||||||
public class CreateNewProductsEventHandler : INotificationHandler<CreateNewProductsEvent>
|
|
||||||
{
|
|
||||||
private readonly ILogger<
|
|
||||||
CreateNewProductsEventHandler> _logger;
|
|
||||||
|
|
||||||
public CreateNewProductsEventHandler(ILogger<CreateNewProductsEventHandler> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Handle(CreateNewProductsEvent notification, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Domain Event: {DomainEvent}", notification.GetType().Name);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-22
@@ -1,22 +0,0 @@
|
|||||||
using CMSMicroservice.Domain.Events;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.ProductsCQ.EventHandlers;
|
|
||||||
|
|
||||||
public class DeleteProductsEventHandler : INotificationHandler<DeleteProductsEvent>
|
|
||||||
{
|
|
||||||
private readonly ILogger<
|
|
||||||
DeleteProductsEventHandler> _logger;
|
|
||||||
|
|
||||||
public DeleteProductsEventHandler(ILogger<DeleteProductsEventHandler> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Handle(DeleteProductsEvent notification, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Domain Event: {DomainEvent}", notification.GetType().Name);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-22
@@ -1,22 +0,0 @@
|
|||||||
using CMSMicroservice.Domain.Events;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.ProductsCQ.EventHandlers;
|
|
||||||
|
|
||||||
public class UpdateProductsEventHandler : INotificationHandler<UpdateProductsEvent>
|
|
||||||
{
|
|
||||||
private readonly ILogger<
|
|
||||||
UpdateProductsEventHandler> _logger;
|
|
||||||
|
|
||||||
public UpdateProductsEventHandler(ILogger<UpdateProductsEventHandler> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Handle(UpdateProductsEvent notification, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Domain Event: {DomainEvent}", notification.GetType().Name);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-41
@@ -1,41 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Queries.GetAllProductsByFilter;
|
|
||||||
public record GetAllProductsByFilterQuery : IRequest<GetAllProductsByFilterResponseDto>
|
|
||||||
{
|
|
||||||
//موقعیت صفحه بندی
|
|
||||||
public PaginationState? PaginationState { get; init; }
|
|
||||||
//مرتب سازی بر اساس
|
|
||||||
public string? SortBy { get; init; }
|
|
||||||
//فیلتر
|
|
||||||
public GetAllProductsByFilterFilter? Filter { get; init; }
|
|
||||||
|
|
||||||
}public class GetAllProductsByFilterFilter
|
|
||||||
{
|
|
||||||
//
|
|
||||||
public long? Id { get; set; }
|
|
||||||
//
|
|
||||||
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 long? CategoryId { 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; }
|
|
||||||
}
|
|
||||||
-67
@@ -1,67 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Queries.GetAllProductsByFilter;
|
|
||||||
public class GetAllProductsByFilterQueryHandler : IRequestHandler<GetAllProductsByFilterQuery, GetAllProductsByFilterResponseDto>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
|
|
||||||
public GetAllProductsByFilterQueryHandler(IApplicationDbContext context)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<GetAllProductsByFilterResponseDto> Handle(GetAllProductsByFilterQuery request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var query = _context.Products
|
|
||||||
.ApplyOrder(sortBy: request.SortBy)
|
|
||||||
.AsNoTracking()
|
|
||||||
.AsQueryable();
|
|
||||||
if (request.Filter is not null)
|
|
||||||
{
|
|
||||||
query = query
|
|
||||||
.Where(x => request.Filter.Id == null || x.Id == request.Filter.Id)
|
|
||||||
.Where(x => request.Filter.Title == null || x.Title.Contains(request.Filter.Title))
|
|
||||||
.Where(x => request.Filter.Description == null || x.Description.Contains(request.Filter.Description))
|
|
||||||
.Where(x => request.Filter.ShortInfomation == null || x.ShortInfomation.Contains(request.Filter.ShortInfomation))
|
|
||||||
.Where(x => request.Filter.FullInformation == null || x.FullInformation.Contains(request.Filter.FullInformation))
|
|
||||||
.Where(x => request.Filter.Price == null || x.Price == request.Filter.Price)
|
|
||||||
.Where(x => request.Filter.Discount == null || x.Discount == request.Filter.Discount)
|
|
||||||
.Where(x => request.Filter.Rate == null || x.Rate == request.Filter.Rate)
|
|
||||||
.Where(x => request.Filter.CategoryId == null || x.ProductCategories.Any(pc => pc.CategoryId == request.Filter.CategoryId))
|
|
||||||
.Where(x => request.Filter.ImagePath == null || x.ImagePath.Contains(request.Filter.ImagePath))
|
|
||||||
.Where(x => request.Filter.ThumbnailPath == null || x.ThumbnailPath.Contains(request.Filter.ThumbnailPath))
|
|
||||||
.Where(x => request.Filter.SaleCount == null || x.SaleCount == request.Filter.SaleCount)
|
|
||||||
.Where(x => request.Filter.ViewCount == null || x.ViewCount == request.Filter.ViewCount)
|
|
||||||
.Where(x => request.Filter.RemainingCount == null || x.RemainingCount == request.Filter.RemainingCount)
|
|
||||||
;
|
|
||||||
}
|
|
||||||
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
|
|
||||||
|
|
||||||
var models = await query
|
|
||||||
.PaginatedListAsync(paginationState: request.PaginationState)
|
|
||||||
.Select(x => new GetAllProductsByFilterResponseModel
|
|
||||||
{
|
|
||||||
Id = x.Id,
|
|
||||||
Title = x.Title,
|
|
||||||
Description = x.Description,
|
|
||||||
ShortInfomation = x.ShortInfomation,
|
|
||||||
FullInformation = x.FullInformation,
|
|
||||||
Price = x.Price,
|
|
||||||
Discount = x.Discount,
|
|
||||||
Rate = x.Rate,
|
|
||||||
ImagePath = x.ImagePath,
|
|
||||||
ThumbnailPath = x.ThumbnailPath,
|
|
||||||
SaleCount = x.SaleCount,
|
|
||||||
ViewCount = x.ViewCount,
|
|
||||||
RemainingCount = x.RemainingCount,
|
|
||||||
CategoryIds = x.ProductCategories
|
|
||||||
.Select(pc => pc.CategoryId)
|
|
||||||
.ToList()
|
|
||||||
})
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
return new GetAllProductsByFilterResponseDto
|
|
||||||
{
|
|
||||||
MetaData = meta,
|
|
||||||
Models = models
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-14
@@ -1,14 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Queries.GetAllProductsByFilter;
|
|
||||||
public class GetAllProductsByFilterQueryValidator : AbstractValidator<GetAllProductsByFilterQuery>
|
|
||||||
{
|
|
||||||
public GetAllProductsByFilterQueryValidator()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
|
||||||
{
|
|
||||||
var result = await ValidateAsync(ValidationContext<GetAllProductsByFilterQuery>.CreateWithOptions((GetAllProductsByFilterQuery)model, x => x.IncludeProperties(propertyName)));
|
|
||||||
if (result.IsValid)
|
|
||||||
return Array.Empty<string>();
|
|
||||||
return result.Errors.Select(e => e.ErrorMessage);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
-41
@@ -1,41 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Queries.GetAllProductsByFilter;
|
|
||||||
public class GetAllProductsByFilterResponseDto
|
|
||||||
{
|
|
||||||
//متادیتا
|
|
||||||
public MetaData MetaData { get; set; }
|
|
||||||
//مدل خروجی
|
|
||||||
public List<GetAllProductsByFilterResponseModel>? Models { get; set; }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public class GetAllProductsByFilterResponseModel
|
|
||||||
{
|
|
||||||
//
|
|
||||||
public long Id { get; set; }
|
|
||||||
//
|
|
||||||
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; }
|
|
||||||
// لیست شناسه دستهبندیهای محصول
|
|
||||||
public List<long> CategoryIds { get; set; } = new();
|
|
||||||
}
|
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
namespace CMSMicroservice.Application.ProductsCQ.Queries.GetCustomerProducts;
|
||||||
|
|
||||||
|
public class GetCustomerProductsQuery : IRequest<GetCustomerProductsResponseDto>
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
}
|
||||||
+91
@@ -0,0 +1,91 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Entities;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.ProductsCQ.Queries.GetCustomerProducts;
|
||||||
|
|
||||||
|
public class GetCustomerProductsQueryHandler : IRequestHandler<GetCustomerProductsQuery, GetCustomerProductsResponseDto>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public GetCustomerProductsQueryHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GetCustomerProductsResponseDto> Handle(GetCustomerProductsQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var product = await _context.Products
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.Id == request.Id)
|
||||||
|
.Include(x => x.ProductGalleries)
|
||||||
|
.ThenInclude(pg => pg.ProductImage)
|
||||||
|
.Include(x => x.ProductCategories)
|
||||||
|
.ThenInclude(pc => pc.Category)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (product == null)
|
||||||
|
throw new NotFoundException(nameof(Product), request.Id);
|
||||||
|
|
||||||
|
var response = new GetCustomerProductsResponseDto
|
||||||
|
{
|
||||||
|
Id = product.Id,
|
||||||
|
Title = product.Title,
|
||||||
|
Description = product.Description,
|
||||||
|
ShortInfomation = product.ShortInfomation,
|
||||||
|
FullInformation = product.FullInformation,
|
||||||
|
Price = product.Price,
|
||||||
|
Discount = product.Discount,
|
||||||
|
Rate = product.Rate,
|
||||||
|
ImagePath = product.ImagePath,
|
||||||
|
ThumbnailPath = product.ThumbnailPath,
|
||||||
|
SaleCount = product.SaleCount,
|
||||||
|
ViewCount = product.ViewCount,
|
||||||
|
RemainingCount = product.RemainingCount,
|
||||||
|
Gallery = product.ProductGalleries?.Select(pg => new ProductGalleryModel
|
||||||
|
{
|
||||||
|
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() ?? new List<ProductGalleryModel>(),
|
||||||
|
Categories = product.ProductCategories?.Select(pc => new ProductCategoryModel
|
||||||
|
{
|
||||||
|
CategoryId = pc.CategoryId,
|
||||||
|
Title = pc.Category?.Title ?? string.Empty,
|
||||||
|
Path = BuildCategoryPath(pc.Category)
|
||||||
|
}).ToList() ?? new List<ProductCategoryModel>()
|
||||||
|
};
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<CategoryNodeModel> BuildCategoryPath(Category? category)
|
||||||
|
{
|
||||||
|
var path = new List<CategoryNodeModel>();
|
||||||
|
|
||||||
|
while (category != null)
|
||||||
|
{
|
||||||
|
path.Insert(0, new CategoryNodeModel
|
||||||
|
{
|
||||||
|
Id = category.Id,
|
||||||
|
Title = category.Title,
|
||||||
|
ParentId = category.ParentId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Move to parent
|
||||||
|
if (category.ParentId.HasValue)
|
||||||
|
{
|
||||||
|
category = _context.Categories
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefault(c => c.Id == category.ParentId.Value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
category = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
namespace CMSMicroservice.Application.ProductsCQ.Queries.GetCustomerProducts;
|
||||||
|
|
||||||
|
public class GetCustomerProductsResponseDto
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
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; }
|
||||||
|
public List<ProductGalleryModel> Gallery { get; set; }
|
||||||
|
public List<ProductCategoryModel> Categories { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ProductGalleryModel
|
||||||
|
{
|
||||||
|
public long ProductGalleryId { get; set; }
|
||||||
|
public long ProductImageId { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string ImagePath { get; set; }
|
||||||
|
public string ImageThumbnailPath { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ProductCategoryModel
|
||||||
|
{
|
||||||
|
public long CategoryId { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public List<CategoryNodeModel> Path { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CategoryNodeModel
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public long? ParentId { get; set; }
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Models;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.ProductsCQ.Queries.GetCustomerProductsByFilter;
|
||||||
|
|
||||||
|
public class GetCustomerProductsByFilterQuery : IRequest<GetCustomerProductsByFilterResponseDto>
|
||||||
|
{
|
||||||
|
public PaginationState? PaginationState { get; set; }
|
||||||
|
public string? SortBy { get; set; }
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
public long? Id { get; set; }
|
||||||
|
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; }
|
||||||
|
public List<long>? CategoryIds { get; set; }
|
||||||
|
}
|
||||||
+145
@@ -0,0 +1,145 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Extensions;
|
||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Application.Common.Models;
|
||||||
|
using CMSMicroservice.Domain.Entities;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.ProductsCQ.Queries.GetCustomerProductsByFilter;
|
||||||
|
|
||||||
|
public class GetCustomerProductsByFilterQueryHandler : IRequestHandler<GetCustomerProductsByFilterQuery, GetCustomerProductsByFilterResponseDto>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public GetCustomerProductsByFilterQueryHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GetCustomerProductsByFilterResponseDto> Handle(GetCustomerProductsByFilterQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var query = _context.Products
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(x => x.ProductCategories)
|
||||||
|
.ThenInclude(pc => pc.Category)
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
if (request.Id.HasValue)
|
||||||
|
query = query.Where(x => x.Id == request.Id.Value);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(request.Title))
|
||||||
|
query = query.Where(x => x.Title.Contains(request.Title));
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(request.Description))
|
||||||
|
query = query.Where(x => x.Description.Contains(request.Description));
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(request.ShortInfomation))
|
||||||
|
query = query.Where(x => x.ShortInfomation.Contains(request.ShortInfomation));
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(request.FullInformation))
|
||||||
|
query = query.Where(x => x.FullInformation.Contains(request.FullInformation));
|
||||||
|
|
||||||
|
if (request.Price.HasValue)
|
||||||
|
query = query.Where(x => x.Price == request.Price.Value);
|
||||||
|
|
||||||
|
if (request.Discount.HasValue)
|
||||||
|
query = query.Where(x => x.Discount == request.Discount.Value);
|
||||||
|
|
||||||
|
if (request.Rate.HasValue)
|
||||||
|
query = query.Where(x => x.Rate == request.Rate.Value);
|
||||||
|
|
||||||
|
if (request.SaleCount.HasValue)
|
||||||
|
query = query.Where(x => x.SaleCount == request.SaleCount.Value);
|
||||||
|
|
||||||
|
if (request.ViewCount.HasValue)
|
||||||
|
query = query.Where(x => x.ViewCount == request.ViewCount.Value);
|
||||||
|
|
||||||
|
if (request.RemainingCount.HasValue)
|
||||||
|
query = query.Where(x => x.RemainingCount == request.RemainingCount.Value);
|
||||||
|
|
||||||
|
if (request.CategoryIds != null && request.CategoryIds.Any())
|
||||||
|
query = query.Where(x => x.ProductCategories.Any(pc => request.CategoryIds.Contains(pc.CategoryId)));
|
||||||
|
|
||||||
|
// Apply sorting
|
||||||
|
if (!string.IsNullOrEmpty(request.SortBy))
|
||||||
|
query = query.ApplyOrder(request.SortBy);
|
||||||
|
else
|
||||||
|
query = query.OrderByDescending(x => x.Created);
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
var totalCount = await query.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
var paginationState = request.PaginationState ?? new PaginationState { PageNumber = 1, PageSize = 10 };
|
||||||
|
var products = await query
|
||||||
|
.Skip((paginationState.PageNumber - 1) * paginationState.PageSize)
|
||||||
|
.Take(paginationState.PageSize)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var metaData = new MetaData
|
||||||
|
{
|
||||||
|
TotalCount = totalCount,
|
||||||
|
CurrentPage = paginationState.PageNumber,
|
||||||
|
PageSize = paginationState.PageSize,
|
||||||
|
TotalPage = (int)Math.Ceiling((double)totalCount / paginationState.PageSize),
|
||||||
|
HasPrevious = paginationState.PageNumber > 1,
|
||||||
|
HasNext = paginationState.PageNumber < (int)Math.Ceiling((double)totalCount / paginationState.PageSize)
|
||||||
|
};
|
||||||
|
|
||||||
|
var models = products.Select(p => new CustomerProductModel
|
||||||
|
{
|
||||||
|
Id = p.Id,
|
||||||
|
Title = p.Title,
|
||||||
|
Description = p.Description,
|
||||||
|
ShortInfomation = p.ShortInfomation,
|
||||||
|
FullInformation = p.FullInformation,
|
||||||
|
Price = p.Price,
|
||||||
|
Discount = p.Discount,
|
||||||
|
Rate = p.Rate,
|
||||||
|
ImagePath = p.ImagePath,
|
||||||
|
ThumbnailPath = p.ThumbnailPath,
|
||||||
|
SaleCount = p.SaleCount,
|
||||||
|
ViewCount = p.ViewCount,
|
||||||
|
RemainingCount = p.RemainingCount,
|
||||||
|
Categories = p.ProductCategories?.Select(pc => new ProductCategoryPathModel
|
||||||
|
{
|
||||||
|
CategoryId = pc.CategoryId,
|
||||||
|
Title = pc.Category?.Title ?? string.Empty,
|
||||||
|
Path = BuildCategoryPath(pc.Category)
|
||||||
|
}).ToList() ?? new List<ProductCategoryPathModel>()
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
return new GetCustomerProductsByFilterResponseDto
|
||||||
|
{
|
||||||
|
MetaData = metaData,
|
||||||
|
Models = models
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<CategoryNodeItemModel> BuildCategoryPath(Category? category)
|
||||||
|
{
|
||||||
|
var path = new List<CategoryNodeItemModel>();
|
||||||
|
|
||||||
|
while (category != null)
|
||||||
|
{
|
||||||
|
path.Insert(0, new CategoryNodeItemModel
|
||||||
|
{
|
||||||
|
Id = category.Id,
|
||||||
|
Title = category.Title,
|
||||||
|
ParentId = category.ParentId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Move to parent
|
||||||
|
if (category.ParentId.HasValue)
|
||||||
|
{
|
||||||
|
category = _context.Categories
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefault(c => c.Id == category.ParentId.Value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
category = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Models;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.ProductsCQ.Queries.GetCustomerProductsByFilter;
|
||||||
|
|
||||||
|
public class GetCustomerProductsByFilterResponseDto
|
||||||
|
{
|
||||||
|
public MetaData MetaData { get; set; }
|
||||||
|
public List<CustomerProductModel> Models { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CustomerProductModel
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
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; }
|
||||||
|
public List<ProductCategoryPathModel> Categories { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ProductCategoryPathModel
|
||||||
|
{
|
||||||
|
public long CategoryId { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public List<CategoryNodeItemModel> Path { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CategoryNodeItemModel
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public long? ParentId { get; set; }
|
||||||
|
}
|
||||||
-54
@@ -1,54 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Queries.GetLowStockProducts;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// دریافت محصولات کم موجودی
|
|
||||||
/// </summary>
|
|
||||||
public record GetLowStockProductsQuery : IRequest<GetLowStockProductsResponseDto>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// آستانه موجودی (پیشفرض: 10)
|
|
||||||
/// </summary>
|
|
||||||
public int Threshold { get; init; } = 10;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// شماره صفحه (پیشفرض: 1)
|
|
||||||
/// </summary>
|
|
||||||
public int PageIndex { get; init; } = 1;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// تعداد در هر صفحه (پیشفرض: 20)
|
|
||||||
/// </summary>
|
|
||||||
public int PageSize { get; init; } = 20;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// فقط محصولات انحصاری باشگاه (اختیاری)
|
|
||||||
/// </summary>
|
|
||||||
public bool? IsClubExclusive { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// پاسخ لیست محصولات کم موجودی
|
|
||||||
/// </summary>
|
|
||||||
public class GetLowStockProductsResponseDto
|
|
||||||
{
|
|
||||||
public MetaData MetaData { get; set; } = new();
|
|
||||||
public List<LowStockProductDto> Products { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// اطلاعات محصول کم موجودی
|
|
||||||
/// </summary>
|
|
||||||
public class LowStockProductDto
|
|
||||||
{
|
|
||||||
public long Id { get; set; }
|
|
||||||
public string Title { get; set; } = string.Empty;
|
|
||||||
public long Price { get; set; }
|
|
||||||
public int Discount { get; set; }
|
|
||||||
public int RemainingCount { get; set; }
|
|
||||||
public int SaleCount { get; set; }
|
|
||||||
public bool IsClubExclusive { get; set; }
|
|
||||||
public string ImagePath { get; set; } = string.Empty;
|
|
||||||
public string ThumbnailPath { get; set; } = string.Empty;
|
|
||||||
public DateTime Created { get; set; }
|
|
||||||
public DateTime? LastModified { get; set; }
|
|
||||||
}
|
|
||||||
-75
@@ -1,75 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Queries.GetLowStockProducts;
|
|
||||||
|
|
||||||
public class GetLowStockProductsQueryHandler : IRequestHandler<GetLowStockProductsQuery, GetLowStockProductsResponseDto>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
private readonly ILogger<GetLowStockProductsQueryHandler> _logger;
|
|
||||||
|
|
||||||
public GetLowStockProductsQueryHandler(
|
|
||||||
IApplicationDbContext context,
|
|
||||||
ILogger<GetLowStockProductsQueryHandler> logger)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<GetLowStockProductsResponseDto> Handle(GetLowStockProductsQuery request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// Query اصلی: محصولاتی که موجودی کمتر یا مساوی آستانه دارند
|
|
||||||
var query = _context.Products
|
|
||||||
.Where(p => p.RemainingCount <= request.Threshold);
|
|
||||||
|
|
||||||
// فیلتر محصولات انحصاری باشگاه (اگر مشخص شده باشد)
|
|
||||||
if (request.IsClubExclusive.HasValue)
|
|
||||||
{
|
|
||||||
query = query.Where(p => p.IsClubExclusive == request.IsClubExclusive.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// مرتبسازی بر اساس موجودی (کمترین موجودی اول)
|
|
||||||
query = query.OrderBy(p => p.RemainingCount)
|
|
||||||
.ThenByDescending(p => p.SaleCount); // محصولات پرفروش اولویت بیشتری دارند
|
|
||||||
|
|
||||||
// شمارش کل
|
|
||||||
var totalCount = await query.CountAsync(cancellationToken);
|
|
||||||
|
|
||||||
// Pagination
|
|
||||||
var products = await query
|
|
||||||
.Skip((request.PageIndex - 1) * request.PageSize)
|
|
||||||
.Take(request.PageSize)
|
|
||||||
.Select(p => new LowStockProductDto
|
|
||||||
{
|
|
||||||
Id = p.Id,
|
|
||||||
Title = p.Title,
|
|
||||||
Price = p.Price,
|
|
||||||
Discount = p.Discount,
|
|
||||||
RemainingCount = p.RemainingCount,
|
|
||||||
SaleCount = p.SaleCount,
|
|
||||||
IsClubExclusive = p.IsClubExclusive,
|
|
||||||
ImagePath = p.ImagePath,
|
|
||||||
ThumbnailPath = p.ThumbnailPath,
|
|
||||||
Created = p.Created,
|
|
||||||
LastModified = p.LastModified
|
|
||||||
})
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Found {Count} low stock products (threshold: {Threshold}, page: {Page})",
|
|
||||||
totalCount, request.Threshold, request.PageIndex);
|
|
||||||
|
|
||||||
var totalPages = (int)Math.Ceiling(totalCount / (double)request.PageSize);
|
|
||||||
|
|
||||||
return new GetLowStockProductsResponseDto
|
|
||||||
{
|
|
||||||
MetaData = new MetaData
|
|
||||||
{
|
|
||||||
CurrentPage = request.PageIndex,
|
|
||||||
TotalPage = totalPages,
|
|
||||||
PageSize = request.PageSize,
|
|
||||||
TotalCount = totalCount,
|
|
||||||
HasNext = request.PageIndex < totalPages,
|
|
||||||
HasPrevious = request.PageIndex > 1
|
|
||||||
},
|
|
||||||
Products = products
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-16
@@ -1,16 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Queries.GetLowStockProducts;
|
|
||||||
|
|
||||||
public class GetLowStockProductsQueryValidator : AbstractValidator<GetLowStockProductsQuery>
|
|
||||||
{
|
|
||||||
public GetLowStockProductsQueryValidator()
|
|
||||||
{
|
|
||||||
RuleFor(x => x.Threshold)
|
|
||||||
.GreaterThanOrEqualTo(0).WithMessage("آستانه موجودی نمیتواند منفی باشد");
|
|
||||||
|
|
||||||
RuleFor(x => x.PageIndex)
|
|
||||||
.GreaterThan(0).WithMessage("شماره صفحه باید بزرگتر از 0 باشد");
|
|
||||||
|
|
||||||
RuleFor(x => x.PageSize)
|
|
||||||
.InclusiveBetween(1, 100).WithMessage("تعداد در هر صفحه باید بین 1 تا 100 باشد");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+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;
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Queries.GetProducts;
|
|
||||||
public record GetProductsQuery : IRequest<GetProductsResponseDto>
|
|
||||||
{
|
|
||||||
//
|
|
||||||
public long Id { get; init; }
|
|
||||||
|
|
||||||
}
|
|
||||||
-40
@@ -1,40 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Queries.GetProducts;
|
|
||||||
public class GetProductsQueryHandler : IRequestHandler<GetProductsQuery, GetProductsResponseDto>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
|
|
||||||
public GetProductsQueryHandler(IApplicationDbContext context)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<GetProductsResponseDto> Handle(GetProductsQuery request,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var response = await _context.Products
|
|
||||||
.AsNoTracking()
|
|
||||||
.Where(x => x.Id == request.Id)
|
|
||||||
.Select(x => new GetProductsResponseDto
|
|
||||||
{
|
|
||||||
Id = x.Id,
|
|
||||||
Title = x.Title,
|
|
||||||
Description = x.Description,
|
|
||||||
ShortInfomation = x.ShortInfomation,
|
|
||||||
FullInformation = x.FullInformation,
|
|
||||||
Price = x.Price,
|
|
||||||
Discount = x.Discount,
|
|
||||||
Rate = x.Rate,
|
|
||||||
ImagePath = x.ImagePath,
|
|
||||||
ThumbnailPath = x.ThumbnailPath,
|
|
||||||
SaleCount = x.SaleCount,
|
|
||||||
ViewCount = x.ViewCount,
|
|
||||||
RemainingCount = x.RemainingCount,
|
|
||||||
CategoryIds = x.ProductCategories
|
|
||||||
.Select(pc => pc.CategoryId)
|
|
||||||
.ToList()
|
|
||||||
})
|
|
||||||
.FirstOrDefaultAsync(cancellationToken);
|
|
||||||
|
|
||||||
return response ?? throw new NotFoundException(nameof(Product), request.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-16
@@ -1,16 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Queries.GetProducts;
|
|
||||||
public class GetProductsQueryValidator : AbstractValidator<GetProductsQuery>
|
|
||||||
{
|
|
||||||
public GetProductsQueryValidator()
|
|
||||||
{
|
|
||||||
RuleFor(model => model.Id)
|
|
||||||
.NotNull();
|
|
||||||
}
|
|
||||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
|
||||||
{
|
|
||||||
var result = await ValidateAsync(ValidationContext<GetProductsQuery>.CreateWithOptions((GetProductsQuery)model, x => x.IncludeProperties(propertyName)));
|
|
||||||
if (result.IsValid)
|
|
||||||
return Array.Empty<string>();
|
|
||||||
return result.Errors.Select(e => e.ErrorMessage);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
-33
@@ -1,33 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Queries.GetProducts;
|
|
||||||
public class GetProductsResponseDto
|
|
||||||
{
|
|
||||||
//
|
|
||||||
public long Id { get; set; }
|
|
||||||
//
|
|
||||||
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; }
|
|
||||||
// لیست شناسه دستهبندیهای محصول
|
|
||||||
public List<long> CategoryIds { get; set; } = new();
|
|
||||||
|
|
||||||
}
|
|
||||||
-16
@@ -1,16 +0,0 @@
|
|||||||
using CMSMicroservice.Application.Common.Models;
|
|
||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.ProductsCQ.Queries.GetProductsByCategory;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// کوئری دریافت محصولات بر اساس دستهبندی
|
|
||||||
/// </summary>
|
|
||||||
public class GetProductsByCategoryQuery : IRequest<GetProductsByCategoryResponseDto>
|
|
||||||
{
|
|
||||||
public long CategoryId { get; set; }
|
|
||||||
public int PageNumber { get; set; } = 1;
|
|
||||||
public int PageSize { get; set; } = 20;
|
|
||||||
public bool OnlyActive { get; set; } = true;
|
|
||||||
public bool OnlyInStock { get; set; } = false;
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user