Compare commits
2 Commits
96daf899c7
...
794dd01ac0
| Author | SHA1 | Date | |
|---|---|---|---|
| 794dd01ac0 | |||
| 658d076bdf |
@@ -0,0 +1,179 @@
|
|||||||
|
# FrontOffice.BFF to CMS Migration Progress
|
||||||
|
|
||||||
|
## Migration Overview
|
||||||
|
مهاجرت سرویسهای FrontOffice.BFF به CMS Microservice با معماری Clean Architecture و gRPC.
|
||||||
|
|
||||||
|
## ✅ Completed Services
|
||||||
|
|
||||||
|
### 1. Categories Service
|
||||||
|
- **Status**: ✅ Complete
|
||||||
|
- **Proto Definition**: `categories.proto`
|
||||||
|
- **Service Implementation**: `CategoryService.cs`
|
||||||
|
- **Methods Migrated**:
|
||||||
|
- Admin Methods:
|
||||||
|
- `AddNewCategory` - افزودن دستهبندی جدید
|
||||||
|
- `UpdateCategory` - بروزرسانی دستهبندی
|
||||||
|
- `DeleteCategory` - حذف دستهبندی
|
||||||
|
- `GetCategory` - دریافت یک دستهبندی
|
||||||
|
- `GetAllCategoriesByFilter` - دریافت لیست دستهبندیها
|
||||||
|
- Customer Methods:
|
||||||
|
- `GetActiveCategoriesForCustomer` - دریافت دستهبندیهای فعال برای مشتری
|
||||||
|
|
||||||
|
### 2. City Service
|
||||||
|
- **Status**: ✅ Complete
|
||||||
|
- **Proto Definition**: `city.proto`
|
||||||
|
- **Service Implementation**: `CityService.cs`
|
||||||
|
- **Methods Migrated**:
|
||||||
|
- Admin Methods:
|
||||||
|
- `AddNewCity` - افزودن شهر جدید
|
||||||
|
- `UpdateCity` - بروزرسانی شهر
|
||||||
|
- `DeleteCity` - حذف شهر
|
||||||
|
- `GetCity` - دریافت یک شهر
|
||||||
|
- `GetAllCitiesByFilter` - دریافت لیست شهرها
|
||||||
|
- Customer Methods:
|
||||||
|
- `GetActiveCitiesForCustomer` - دریافت شهرهای فعال برای مشتری
|
||||||
|
|
||||||
|
### 3. UserCarts Service
|
||||||
|
- **Status**: ✅ Complete
|
||||||
|
- **Proto Definition**: `usercarts.proto`
|
||||||
|
- **Service Implementation**: `UserCartsService.cs`
|
||||||
|
- **Methods Migrated**:
|
||||||
|
- Admin Methods:
|
||||||
|
- `AddNewUserCart` - افزودن سبد خرید جدید
|
||||||
|
- `UpdateUserCart` - بروزرسانی سبد خرید
|
||||||
|
- `DeleteUserCart` - حذف سبد خرید
|
||||||
|
- `GetUserCart` - دریافت سبد خرید (Admin)
|
||||||
|
- `GetAllUserCartsByFilter` - دریافت لیست سبدهای خرید
|
||||||
|
- Customer Methods:
|
||||||
|
- `AddNewUserCartForCustomer` - افزودن محصول به سبد (Customer)
|
||||||
|
- `UpdateUserCartForCustomer` - بروزرسانی تعداد محصول در سبد
|
||||||
|
- `RemoveUserCartForCustomer` - حذف محصول از سبد
|
||||||
|
- `GetCustomerCart` - دریافت سبد خرید مشتری
|
||||||
|
|
||||||
|
## 🛠️ Technical Implementation Details
|
||||||
|
|
||||||
|
### gRPC HTTP Annotations
|
||||||
|
تمام سرویسها با HTTP annotations تعریف شدهاند:
|
||||||
|
- Admin endpoints: `/ServiceName` pattern
|
||||||
|
- Customer endpoints: `/Customer/Action` pattern
|
||||||
|
|
||||||
|
### Clean Architecture Structure
|
||||||
|
```
|
||||||
|
CMSMicroservice.Domain/ # Core business entities
|
||||||
|
CMSMicroservice.Application/ # Business logic & CQRS
|
||||||
|
CMSMicroservice.Infrastructure/ # Data access & external services
|
||||||
|
CMSMicroservice.WebApi/ # gRPC services & controllers
|
||||||
|
CMSMicroservice.Protobuf/ # Protocol buffer definitions
|
||||||
|
```
|
||||||
|
|
||||||
|
### Swagger Integration
|
||||||
|
- Multiple Swagger documents: cms, admin, customer, unified
|
||||||
|
- gRPC HTTP transcoding enabled
|
||||||
|
- Custom CSS styling applied
|
||||||
|
- Conflict resolution implemented
|
||||||
|
|
||||||
|
## 🔧 Issues Resolved
|
||||||
|
|
||||||
|
### 1. Swagger Conflict Resolution
|
||||||
|
**Problem**:
|
||||||
|
```
|
||||||
|
Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorException:
|
||||||
|
Conflicting method/path combination "GET GetUserCart"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Root Cause**:
|
||||||
|
- دو method با operation ID یکسان: `GetUserCart` و `GetUserCartForCustomer`
|
||||||
|
- Swagger از method name برای operation ID استفاده میکند
|
||||||
|
|
||||||
|
**Solutions Attempted**:
|
||||||
|
1. ❌ `CustomOperationIds` - ineffective
|
||||||
|
2. ❌ `ResolveConflictingActions` - incomplete resolution
|
||||||
|
3. ✅ **Method Renaming** - successful
|
||||||
|
|
||||||
|
**Final Solution**:
|
||||||
|
```protobuf
|
||||||
|
// Before (conflicting):
|
||||||
|
rpc GetUserCartForCustomer(GetUserCartForCustomerRequest) returns (GetUserCartForCustomerResponse)
|
||||||
|
|
||||||
|
// After (resolved):
|
||||||
|
rpc GetCustomerCart(GetUserCartForCustomerRequest) returns (GetUserCartForCustomerResponse)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Application Layer Dependencies
|
||||||
|
**Problem**: Build errors در Application layer
|
||||||
|
**Solution**: پاکسازی dependencies و rebuild پروژه
|
||||||
|
|
||||||
|
## 📊 Migration Status Summary
|
||||||
|
|
||||||
|
| Service | Proto ✅ | Implementation ✅ | Build ✅ | Swagger ✅ |
|
||||||
|
|---------|----------|-------------------|----------|------------|
|
||||||
|
| Categories | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| City | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| UserCarts | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
|
||||||
|
## 🎯 Next Steps
|
||||||
|
1. **Service Integration Testing** - تست عملکرد سرویسهای migrate شده
|
||||||
|
2. **Business Logic Implementation** - پیادهسازی منطق کسبوکار واقعی
|
||||||
|
3. **Database Integration** - اتصال به لایه دیتا
|
||||||
|
4. **Continue Migration** - ادامه migration سایر سرویسها
|
||||||
|
|
||||||
|
## 🏗️ Technical Architecture
|
||||||
|
|
||||||
|
### gRPC Service Pattern
|
||||||
|
```csharp
|
||||||
|
public class ServiceName : ServiceContract.ServiceContractBase
|
||||||
|
{
|
||||||
|
private readonly IDispatchRequestToCQRS _dispatcher;
|
||||||
|
|
||||||
|
// Customer Methods Section
|
||||||
|
#region Customer Methods
|
||||||
|
public override async Task<Response> CustomerMethod(Request request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
// Implementation
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
// Admin Methods Section
|
||||||
|
#region Admin Methods
|
||||||
|
public override async Task<Response> AdminMethod(Request request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
// Implementation
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Proto File Structure
|
||||||
|
```protobuf
|
||||||
|
syntax = "proto3";
|
||||||
|
import "google/api/annotations.proto";
|
||||||
|
|
||||||
|
service ServiceContract {
|
||||||
|
// ============= Admin Methods =============
|
||||||
|
rpc AdminMethod(Request) returns (Response) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
post: "/AdminEndpoint"
|
||||||
|
body: "*"
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============= Customer Methods =============
|
||||||
|
rpc CustomerMethod(Request) returns (Response) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
get: "/Customer/Endpoint"
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📈 Performance & Quality
|
||||||
|
- ✅ All services compile successfully
|
||||||
|
- ✅ Swagger documentation accessible
|
||||||
|
- ✅ gRPC HTTP transcoding working
|
||||||
|
- ✅ Clean separation of Admin/Customer concerns
|
||||||
|
- ✅ Consistent naming conventions applied
|
||||||
|
|
||||||
|
---
|
||||||
|
**Last Updated**: January 30, 2026
|
||||||
|
**Migration Phase**: Foundation Services Complete
|
||||||
|
**Next Milestone**: Business Logic Implementation
|
||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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))
|
|
||||||
;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+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
|
||||||
|
}
|
||||||
-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("مقدار موجودی نامعتبر است");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-29
@@ -1,29 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.CreateNewProducts;
|
|
||||||
public record CreateNewProductsCommand : IRequest<CreateNewProductsResponseDto>
|
|
||||||
{
|
|
||||||
//
|
|
||||||
public string Title { get; init; }
|
|
||||||
//
|
|
||||||
public string Description { get; init; }
|
|
||||||
//
|
|
||||||
public string ShortInfomation { get; init; }
|
|
||||||
//
|
|
||||||
public string FullInformation { get; init; }
|
|
||||||
//
|
|
||||||
public long Price { get; init; }
|
|
||||||
//
|
|
||||||
public int Discount { get; init; }
|
|
||||||
//
|
|
||||||
public int Rate { get; init; }
|
|
||||||
//
|
|
||||||
public string ImagePath { get; init; }
|
|
||||||
//
|
|
||||||
public string ThumbnailPath { get; init; }
|
|
||||||
//
|
|
||||||
public int SaleCount { get; init; }
|
|
||||||
//
|
|
||||||
public int ViewCount { get; init; }
|
|
||||||
// لیست شناسه دستهبندیهای محصول
|
|
||||||
public ICollection<long>? CategoryIds { get; init; }
|
|
||||||
|
|
||||||
}
|
|
||||||
-59
@@ -1,59 +0,0 @@
|
|||||||
using CMSMicroservice.Application.Common.Interfaces;
|
|
||||||
using CMSMicroservice.Domain.Entities;
|
|
||||||
using CMSMicroservice.Domain.Enums;
|
|
||||||
using CMSMicroservice.Domain.Events;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.CreateNewProducts;
|
|
||||||
|
|
||||||
public class CreateNewProductsCommandHandler : IRequestHandler<CreateNewProductsCommand, CreateNewProductsResponseDto>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
private readonly IInventoryService _inventoryService;
|
|
||||||
|
|
||||||
public CreateNewProductsCommandHandler(
|
|
||||||
IApplicationDbContext context,
|
|
||||||
IInventoryService inventoryService)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
_inventoryService = inventoryService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<CreateNewProductsResponseDto> Handle(CreateNewProductsCommand request,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var entity = request.Adapt<Product>();
|
|
||||||
await _context.Products.AddAsync(entity, cancellationToken);
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
|
||||||
|
|
||||||
// ایجاد رکورد موجودی در سیستم انبارداری با موجودی اولیه صفر
|
|
||||||
await _inventoryService.InitializeInventoryAsync(
|
|
||||||
entity.Id,
|
|
||||||
ProductType.RegularProduct,
|
|
||||||
0, // موجودی اولیه صفر - باید از طریق Inventory اضافه شود
|
|
||||||
ct: cancellationToken);
|
|
||||||
|
|
||||||
// ثبت دستهبندیهای محصول (در صورت ارسال)
|
|
||||||
if (request.CategoryIds is { Count: > 0 })
|
|
||||||
{
|
|
||||||
var distinctCategoryIds = request.CategoryIds
|
|
||||||
.Where(id => id > 0)
|
|
||||||
.Distinct()
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
foreach (var categoryId in distinctCategoryIds)
|
|
||||||
{
|
|
||||||
var rel = new ProductCategory
|
|
||||||
{
|
|
||||||
ProductId = entity.Id,
|
|
||||||
CategoryId = categoryId
|
|
||||||
};
|
|
||||||
await _context.ProductCategories.AddAsync(rel, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
entity.AddDomainEvent(new CreateNewProductsEvent(entity));
|
|
||||||
return entity.Adapt<CreateNewProductsResponseDto>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-36
@@ -1,36 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.CreateNewProducts;
|
|
||||||
public class CreateNewProductsCommandValidator : AbstractValidator<CreateNewProductsCommand>
|
|
||||||
{
|
|
||||||
public CreateNewProductsCommandValidator()
|
|
||||||
{
|
|
||||||
RuleFor(model => model.Title)
|
|
||||||
.NotEmpty();
|
|
||||||
RuleFor(model => model.Description)
|
|
||||||
.NotEmpty();
|
|
||||||
RuleFor(model => model.ShortInfomation)
|
|
||||||
.NotEmpty();
|
|
||||||
RuleFor(model => model.FullInformation)
|
|
||||||
.NotEmpty();
|
|
||||||
RuleFor(model => model.Price)
|
|
||||||
.NotNull();
|
|
||||||
RuleFor(model => model.Discount)
|
|
||||||
.NotNull();
|
|
||||||
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) =>
|
|
||||||
{
|
|
||||||
var result = await ValidateAsync(ValidationContext<CreateNewProductsCommand>.CreateWithOptions((CreateNewProductsCommand)model, x => x.IncludeProperties(propertyName)));
|
|
||||||
if (result.IsValid)
|
|
||||||
return Array.Empty<string>();
|
|
||||||
return result.Errors.Select(e => e.ErrorMessage);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
-7
@@ -1,7 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.CreateNewProducts;
|
|
||||||
public class CreateNewProductsResponseDto
|
|
||||||
{
|
|
||||||
//
|
|
||||||
public long Id { get; set; }
|
|
||||||
|
|
||||||
}
|
|
||||||
-7
@@ -1,7 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.DeleteProducts;
|
|
||||||
public record DeleteProductsCommand : IRequest<Unit>
|
|
||||||
{
|
|
||||||
//
|
|
||||||
public long Id { get; init; }
|
|
||||||
|
|
||||||
}
|
|
||||||
-22
@@ -1,22 +0,0 @@
|
|||||||
using CMSMicroservice.Domain.Events;
|
|
||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.DeleteProducts;
|
|
||||||
public class DeleteProductsCommandHandler : IRequestHandler<DeleteProductsCommand, Unit>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
|
|
||||||
public DeleteProductsCommandHandler(IApplicationDbContext context)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Unit> Handle(DeleteProductsCommand request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var entity = await _context.Products
|
|
||||||
.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken) ?? throw new NotFoundException(nameof(Product), request.Id);
|
|
||||||
entity.IsDeleted = true;
|
|
||||||
_context.Products.Update(entity);
|
|
||||||
entity.AddDomainEvent(new DeleteProductsEvent(entity));
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
-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();
|
|
||||||
}
|
|
||||||
-31
@@ -1,31 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.UpdateProducts;
|
|
||||||
public record UpdateProductsCommand : IRequest<Unit>
|
|
||||||
{
|
|
||||||
//
|
|
||||||
public long Id { get; init; }
|
|
||||||
//
|
|
||||||
public string Title { get; init; }
|
|
||||||
//
|
|
||||||
public string Description { get; init; }
|
|
||||||
//
|
|
||||||
public string ShortInfomation { get; init; }
|
|
||||||
//
|
|
||||||
public string FullInformation { get; init; }
|
|
||||||
//
|
|
||||||
public long Price { get; init; }
|
|
||||||
//
|
|
||||||
public int Discount { get; init; }
|
|
||||||
//
|
|
||||||
public int Rate { get; init; }
|
|
||||||
//
|
|
||||||
public string ImagePath { get; init; }
|
|
||||||
//
|
|
||||||
public string ThumbnailPath { get; init; }
|
|
||||||
//
|
|
||||||
public int SaleCount { get; init; }
|
|
||||||
//
|
|
||||||
public int ViewCount { get; init; }
|
|
||||||
// لیست شناسه دستهبندیهای محصول
|
|
||||||
public ICollection<long>? CategoryIds { get; init; }
|
|
||||||
|
|
||||||
}
|
|
||||||
-64
@@ -1,64 +0,0 @@
|
|||||||
using CMSMicroservice.Application.Common.Exceptions;
|
|
||||||
using CMSMicroservice.Application.Common.Interfaces;
|
|
||||||
using CMSMicroservice.Domain.Entities;
|
|
||||||
using CMSMicroservice.Domain.Events;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.UpdateProducts;
|
|
||||||
public class UpdateProductsCommandHandler : IRequestHandler<UpdateProductsCommand, Unit>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
|
|
||||||
public UpdateProductsCommandHandler(IApplicationDbContext context)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Unit> Handle(UpdateProductsCommand request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var entity = await _context.Products
|
|
||||||
.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken)
|
|
||||||
?? throw new NotFoundException(nameof(Product), request.Id);
|
|
||||||
|
|
||||||
request.Adapt(entity);
|
|
||||||
_context.Products.Update(entity);
|
|
||||||
|
|
||||||
// بهروزرسانی دستهبندیهای محصول در صورت ارسال CategoryIds
|
|
||||||
if (request.CategoryIds is not null)
|
|
||||||
{
|
|
||||||
var targetIds = (request.CategoryIds ?? Array.Empty<long>())
|
|
||||||
.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
|
|
||||||
{
|
|
||||||
ProductId = entity.Id,
|
|
||||||
CategoryId = categoryId
|
|
||||||
};
|
|
||||||
await _context.ProductCategories.AddAsync(rel, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toRemove.Count > 0)
|
|
||||||
{
|
|
||||||
_context.ProductCategories.RemoveRange(toRemove);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
entity.AddDomainEvent(new UpdateProductsEvent(entity));
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
|
||||||
return Unit.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-38
@@ -1,38 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.ProductsCQ.Commands.UpdateProducts;
|
|
||||||
public class UpdateProductsCommandValidator : AbstractValidator<UpdateProductsCommand>
|
|
||||||
{
|
|
||||||
public UpdateProductsCommandValidator()
|
|
||||||
{
|
|
||||||
RuleFor(model => model.Id)
|
|
||||||
.NotNull();
|
|
||||||
RuleFor(model => model.Title)
|
|
||||||
.NotEmpty();
|
|
||||||
RuleFor(model => model.Description)
|
|
||||||
.NotEmpty();
|
|
||||||
RuleFor(model => model.ShortInfomation)
|
|
||||||
.NotEmpty();
|
|
||||||
RuleFor(model => model.FullInformation)
|
|
||||||
.NotEmpty();
|
|
||||||
RuleFor(model => model.Price)
|
|
||||||
.NotNull();
|
|
||||||
RuleFor(model => model.Discount)
|
|
||||||
.NotNull();
|
|
||||||
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) =>
|
|
||||||
{
|
|
||||||
var result = await ValidateAsync(ValidationContext<UpdateProductsCommand>.CreateWithOptions((UpdateProductsCommand)model, x => x.IncludeProperties(propertyName)));
|
|
||||||
if (result.IsValid)
|
|
||||||
return Array.Empty<string>();
|
|
||||||
return result.Errors.Select(e => e.ErrorMessage);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
-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; }
|
|
||||||
}
|
|
||||||
+51
-49
@@ -1,5 +1,14 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using Mapster;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.ProductsCQ.Queries.GetAllProductsByFilter;
|
namespace CMSMicroservice.Application.ProductsCQ.Queries.GetAllProductsByFilter;
|
||||||
public class GetAllProductsByFilterQueryHandler : IRequestHandler<GetAllProductsByFilterQuery, GetAllProductsByFilterResponseDto>
|
|
||||||
|
public class
|
||||||
|
GetAllProductsByFilterQueryHandler : IRequestHandler<GetAllProductsByFilterQuery, GetAllProductsByFilterResponseDto>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
@@ -8,60 +17,53 @@ public class GetAllProductsByFilterQueryHandler : IRequestHandler<GetAllProducts
|
|||||||
_context = context;
|
_context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GetAllProductsByFilterResponseDto> Handle(GetAllProductsByFilterQuery request, CancellationToken cancellationToken)
|
public async Task<GetAllProductsByFilterResponseDto> Handle(GetAllProductsByFilterQuery request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var query = _context.Products
|
var grpcRequest = new CmsProductsProtos.GetAllProductsByFilterRequest
|
||||||
.ApplyOrder(sortBy: request.SortBy)
|
|
||||||
.AsNoTracking()
|
|
||||||
.AsQueryable();
|
|
||||||
if (request.Filter is not null)
|
|
||||||
{
|
{
|
||||||
query = query
|
PaginationState = request.PaginationState is { } pagination
|
||||||
.Where(x => request.Filter.Id == null || x.Id == request.Filter.Id)
|
? new CmsPaginationState
|
||||||
.Where(x => request.Filter.Title == null || x.Title.Contains(request.Filter.Title))
|
{
|
||||||
.Where(x => request.Filter.Description == null || x.Description.Contains(request.Filter.Description))
|
PageNumber = pagination.PageNumber,
|
||||||
.Where(x => request.Filter.ShortInfomation == null || x.ShortInfomation.Contains(request.Filter.ShortInfomation))
|
PageSize = pagination.PageSize
|
||||||
.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);
|
: null,
|
||||||
|
SortBy = request.SortBy,
|
||||||
|
Filter = BuildFilter(request.Filter)
|
||||||
|
};
|
||||||
|
|
||||||
var models = await query
|
var result = await _context.Product.GetAllProductsByFilterAsync(grpcRequest,
|
||||||
.PaginatedListAsync(paginationState: request.PaginationState)
|
cancellationToken: cancellationToken);
|
||||||
.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
|
if (request.Filter?.CategoryId is { } categoryId)
|
||||||
{
|
{
|
||||||
MetaData = meta,
|
var matchingModels = result.Models
|
||||||
Models = models
|
.Where(model => model.CategoryIds.Contains(categoryId))
|
||||||
|
.ToList();
|
||||||
|
result.Models.Clear();
|
||||||
|
result.Models.AddRange(matchingModels);
|
||||||
|
}
|
||||||
|
return result.Adapt<GetAllProductsByFilterResponseDto>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CmsProductsProtos.GetAllProductsByFilterFilter? BuildFilter(GetAllProductsByFilterFilter? filter)
|
||||||
|
{
|
||||||
|
if (filter is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CmsProductsProtos.GetAllProductsByFilterFilter
|
||||||
|
{
|
||||||
|
Id = filter.Id,
|
||||||
|
Title = filter.Title,
|
||||||
|
Description = filter.Description,
|
||||||
|
ShortInfomation = filter.ShortInfomation,
|
||||||
|
FullInformation = filter.FullInformation,
|
||||||
|
Price = filter.Price,
|
||||||
|
Discount = filter.Discount,
|
||||||
|
Rate = filter.Rate
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
-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();
|
|
||||||
}
|
|
||||||
-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 باشد");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
-74
@@ -1,74 +0,0 @@
|
|||||||
using CMSMicroservice.Application.Common.Interfaces;
|
|
||||||
using CMSMicroservice.Application.Common.Models;
|
|
||||||
using MediatR;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.ProductsCQ.Queries.GetProductsByCategory;
|
|
||||||
|
|
||||||
public class GetProductsByCategoryQueryHandler : IRequestHandler<GetProductsByCategoryQuery, GetProductsByCategoryResponseDto>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
private readonly ILogger<GetProductsByCategoryQueryHandler> _logger;
|
|
||||||
|
|
||||||
public GetProductsByCategoryQueryHandler(
|
|
||||||
IApplicationDbContext context,
|
|
||||||
ILogger<GetProductsByCategoryQueryHandler> logger)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<GetProductsByCategoryResponseDto> Handle(GetProductsByCategoryQuery request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var query = _context.Products
|
|
||||||
.Where(x => !x.IsDeleted)
|
|
||||||
.Where(x => x.ProductCategories.Any(pc => pc.CategoryId == request.CategoryId && !pc.IsDeleted));
|
|
||||||
|
|
||||||
if (request.OnlyInStock)
|
|
||||||
{
|
|
||||||
query = query.Where(x => x.RemainingCount > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
var totalCount = await query.CountAsync(cancellationToken);
|
|
||||||
|
|
||||||
var products = await query
|
|
||||||
.OrderByDescending(x => x.Created)
|
|
||||||
.Skip((request.PageNumber - 1) * request.PageSize)
|
|
||||||
.Take(request.PageSize)
|
|
||||||
.Select(x => new ProductListDto
|
|
||||||
{
|
|
||||||
Id = x.Id,
|
|
||||||
Name = x.Title,
|
|
||||||
Description = x.Description,
|
|
||||||
Price = x.Price,
|
|
||||||
Stock = x.RemainingCount,
|
|
||||||
IsActive = !x.IsDeleted,
|
|
||||||
ImageUrl = x.ImagePath,
|
|
||||||
Created = x.Created
|
|
||||||
})
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
var metaData = new MetaData
|
|
||||||
{
|
|
||||||
TotalCount = totalCount,
|
|
||||||
PageSize = request.PageSize,
|
|
||||||
CurrentPage = request.PageNumber,
|
|
||||||
TotalPage = (int)Math.Ceiling(totalCount / (double)request.PageSize),
|
|
||||||
HasNext = request.PageNumber < (int)Math.Ceiling(totalCount / (double)request.PageSize),
|
|
||||||
HasPrevious = request.PageNumber > 1
|
|
||||||
};
|
|
||||||
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Retrieved {Count} products for category {CategoryId}",
|
|
||||||
products.Count,
|
|
||||||
request.CategoryId
|
|
||||||
);
|
|
||||||
|
|
||||||
return new GetProductsByCategoryResponseDto
|
|
||||||
{
|
|
||||||
MetaData = metaData,
|
|
||||||
Products = products
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
using CMSMicroservice.Application.Common.Models;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.ProductsCQ.Queries.GetProductsByCategory;
|
|
||||||
|
|
||||||
public class GetProductsByCategoryResponseDto
|
|
||||||
{
|
|
||||||
public MetaData MetaData { get; set; } = new();
|
|
||||||
public List<ProductListDto> Products { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ProductListDto
|
|
||||||
{
|
|
||||||
public long Id { get; set; }
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
public string Description { get; set; } = string.Empty;
|
|
||||||
public long Price { get; set; }
|
|
||||||
public int Stock { get; set; }
|
|
||||||
public bool IsActive { get; set; }
|
|
||||||
public string? ImageUrl { get; set; }
|
|
||||||
public DateTime Created { get; set; }
|
|
||||||
}
|
|
||||||
-16
@@ -1,16 +0,0 @@
|
|||||||
using CMSMicroservice.Application.Common.Models;
|
|
||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.ProductsCQ.Queries.GetProductsByTag;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// کوئری دریافت محصولات بر اساس تگ
|
|
||||||
/// </summary>
|
|
||||||
public class GetProductsByTagQuery : IRequest<GetProductsByTagResponseDto>
|
|
||||||
{
|
|
||||||
public long TagId { 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;
|
|
||||||
}
|
|
||||||
-75
@@ -1,75 +0,0 @@
|
|||||||
using CMSMicroservice.Application.Common.Interfaces;
|
|
||||||
using CMSMicroservice.Application.Common.Models;
|
|
||||||
using CMSMicroservice.Application.ProductsCQ.Queries.GetProductsByCategory;
|
|
||||||
using MediatR;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.ProductsCQ.Queries.GetProductsByTag;
|
|
||||||
|
|
||||||
public class GetProductsByTagQueryHandler : IRequestHandler<GetProductsByTagQuery, GetProductsByTagResponseDto>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
private readonly ILogger<GetProductsByTagQueryHandler> _logger;
|
|
||||||
|
|
||||||
public GetProductsByTagQueryHandler(
|
|
||||||
IApplicationDbContext context,
|
|
||||||
ILogger<GetProductsByTagQueryHandler> logger)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<GetProductsByTagResponseDto> Handle(GetProductsByTagQuery request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var query = _context.Products
|
|
||||||
.Where(x => !x.IsDeleted)
|
|
||||||
.Where(x => x.ProductTags.Any(pt => pt.TagId == request.TagId && !pt.IsDeleted));
|
|
||||||
|
|
||||||
if (request.OnlyInStock)
|
|
||||||
{
|
|
||||||
query = query.Where(x => x.RemainingCount > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
var totalCount = await query.CountAsync(cancellationToken);
|
|
||||||
|
|
||||||
var products = await query
|
|
||||||
.OrderByDescending(x => x.Created)
|
|
||||||
.Skip((request.PageNumber - 1) * request.PageSize)
|
|
||||||
.Take(request.PageSize)
|
|
||||||
.Select(x => new ProductListDto
|
|
||||||
{
|
|
||||||
Id = x.Id,
|
|
||||||
Name = x.Title,
|
|
||||||
Description = x.Description,
|
|
||||||
Price = x.Price,
|
|
||||||
Stock = x.RemainingCount,
|
|
||||||
IsActive = !x.IsDeleted,
|
|
||||||
ImageUrl = x.ImagePath,
|
|
||||||
Created = x.Created
|
|
||||||
})
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
var metaData = new MetaData
|
|
||||||
{
|
|
||||||
TotalCount = totalCount,
|
|
||||||
PageSize = request.PageSize,
|
|
||||||
CurrentPage = request.PageNumber,
|
|
||||||
TotalPage = (int)Math.Ceiling(totalCount / (double)request.PageSize),
|
|
||||||
HasNext = request.PageNumber < (int)Math.Ceiling(totalCount / (double)request.PageSize),
|
|
||||||
HasPrevious = request.PageNumber > 1
|
|
||||||
};
|
|
||||||
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Retrieved {Count} products for tag {TagId}",
|
|
||||||
products.Count,
|
|
||||||
request.TagId
|
|
||||||
);
|
|
||||||
|
|
||||||
return new GetProductsByTagResponseDto
|
|
||||||
{
|
|
||||||
MetaData = metaData,
|
|
||||||
Products = products
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
using CMSMicroservice.Application.Common.Models;
|
|
||||||
using CMSMicroservice.Application.ProductsCQ.Queries.GetProductsByCategory;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.ProductsCQ.Queries.GetProductsByTag;
|
|
||||||
|
|
||||||
public class GetProductsByTagResponseDto
|
|
||||||
{
|
|
||||||
public MetaData MetaData { get; set; } = new();
|
|
||||||
public List<ProductListDto> Products { get; set; } = new();
|
|
||||||
}
|
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
namespace CMSMicroservice.Application.UserCQ.Commands.AcceptContract;
|
||||||
|
public record AcceptContractCommand : IRequest<AcceptContractResponseDto>
|
||||||
|
{
|
||||||
|
//کد otp
|
||||||
|
public string Code { get; init; }
|
||||||
|
//فایل قرارداد
|
||||||
|
public string ContractHtml { get; init; }
|
||||||
|
//شناسه یکتای امضا
|
||||||
|
public string SignGuid { get; init; }
|
||||||
|
|
||||||
|
}
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using CMSMicroservice.Domain.Entities;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.UserCQ.Commands.AcceptContract;
|
||||||
|
public class AcceptContractCommandHandler : IRequestHandler<AcceptContractCommand, AcceptContractResponseDto>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly ICurrentUserService _currentUserService;
|
||||||
|
|
||||||
|
public AcceptContractCommandHandler(IApplicationDbContext context, ICurrentUserService currentUserService)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_currentUserService = currentUserService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AcceptContractResponseDto> Handle(AcceptContractCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Verify OTP first
|
||||||
|
var otpToken = await _context.OtpTokens
|
||||||
|
.Where(x => x.Mobile == _currentUserService.Username && x.Purpose == "signContract" && !x.IsUsed && x.Code == request.Code)
|
||||||
|
.OrderByDescending(x => x.Id) // Use Id instead of CreatedAt for now
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (otpToken == null || !otpToken.IsValid(request.Code))
|
||||||
|
return new AcceptContractResponseDto { IsSuccess = false, Message = "کد تایید نامعتبر است" };
|
||||||
|
|
||||||
|
var user = await _context.Users
|
||||||
|
.Where(x => x.Mobile == _currentUserService.Username)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
return new AcceptContractResponseDto { IsSuccess = false, Message = "کاربر یافت نشد" };
|
||||||
|
|
||||||
|
// Create user contract
|
||||||
|
var userContract = new UserContract
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
ContractId = 1, // Default contract
|
||||||
|
SignGuid = request.SignGuid,
|
||||||
|
SignedPdfFile = request.ContractHtml
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.UserContracts.Add(userContract);
|
||||||
|
|
||||||
|
// Mark OTP as used
|
||||||
|
otpToken.IsUsed = true;
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
// TODO: Implement JWT token generation
|
||||||
|
return new AcceptContractResponseDto
|
||||||
|
{
|
||||||
|
IsSuccess = true,
|
||||||
|
Message = "قرارداد با موفقیت تایید شد",
|
||||||
|
Token = "TODO_IMPLEMENT_JWT_GENERATION"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
namespace CMSMicroservice.Application.UserCQ.Commands.AcceptContract;
|
||||||
|
public class AcceptContractCommandValidator : AbstractValidator<AcceptContractCommand>
|
||||||
|
{
|
||||||
|
public AcceptContractCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(model => model.Code)
|
||||||
|
.NotEmpty();
|
||||||
|
RuleFor(model => model.ContractHtml)
|
||||||
|
.NotEmpty();
|
||||||
|
RuleFor(model => model.SignGuid)
|
||||||
|
.NotEmpty();
|
||||||
|
}
|
||||||
|
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||||
|
{
|
||||||
|
var result = await ValidateAsync(ValidationContext<AcceptContractCommand>.CreateWithOptions((AcceptContractCommand)model, x => x.IncludeProperties(propertyName)));
|
||||||
|
if (result.IsValid)
|
||||||
|
return Array.Empty<string>();
|
||||||
|
return result.Errors.Select(e => e.ErrorMessage);
|
||||||
|
};
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
namespace CMSMicroservice.Application.UserCQ.Commands.AcceptContract;
|
||||||
|
public class AcceptContractResponseDto
|
||||||
|
{
|
||||||
|
//موفق؟
|
||||||
|
public bool IsSuccess { get; set; }
|
||||||
|
//پیام
|
||||||
|
public string? Message { get; set; }
|
||||||
|
//توکن
|
||||||
|
public string? Token { get; set; }
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
namespace CMSMicroservice.Application.UserCQ.Commands.CreateNewOtpToken;
|
||||||
|
public record CreateNewOtpTokenCommand : IRequest<CreateNewOtpTokenResponseDto>
|
||||||
|
{
|
||||||
|
//موبایل مقصد
|
||||||
|
public string Mobile { get; init; }
|
||||||
|
//مقصود
|
||||||
|
public string Purpose { get; init; }
|
||||||
|
//شناسه امضا
|
||||||
|
public string? SignGuid { get; init; }
|
||||||
|
|
||||||
|
}
|
||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
using System.Text;
|
||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using CMSMicroservice.Domain.Entities;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.UserCQ.Commands.CreateNewOtpToken;
|
||||||
|
|
||||||
|
public class CreateNewOtpTokenCommandHandler : IRequestHandler<CreateNewOtpTokenCommand, CreateNewOtpTokenResponseDto>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly IKavenegarService _kavenegarService;
|
||||||
|
private readonly ICurrentUserService _currentUserService;
|
||||||
|
|
||||||
|
public CreateNewOtpTokenCommandHandler(IApplicationDbContext context, IKavenegarService kavenegarService, ICurrentUserService currentUserService)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_kavenegarService = kavenegarService;
|
||||||
|
_currentUserService = currentUserService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CreateNewOtpTokenResponseDto> Handle(CreateNewOtpTokenCommand request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Generate random 4-digit code
|
||||||
|
var random = new Random();
|
||||||
|
var code = random.Next(1000, 9999).ToString();
|
||||||
|
|
||||||
|
// Invalidate previous unused tokens for this mobile and purpose
|
||||||
|
var existingTokens = await _context.OtpTokens
|
||||||
|
.Where(x => x.Mobile == request.Mobile && x.Purpose == request.Purpose && !x.IsUsed)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
foreach (var token in existingTokens)
|
||||||
|
{
|
||||||
|
token.IsUsed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new OTP token
|
||||||
|
var otpToken = new OtpToken
|
||||||
|
{
|
||||||
|
Mobile = request.Mobile,
|
||||||
|
Purpose = request.Purpose,
|
||||||
|
Code = code,
|
||||||
|
CodeHash = BCrypt.Net.BCrypt.HashPassword(code), // Hash the code for security
|
||||||
|
IsUsed = false,
|
||||||
|
ExpiresAt = DateTime.UtcNow.AddMinutes(5) // 5 minutes expiry
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.OtpTokens.Add(otpToken);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Send SMS
|
||||||
|
var user = await _context.Users
|
||||||
|
.Where(x => x.Mobile == request.Mobile)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
await _kavenegarService.VerifyLookupAsync(request.Mobile, code);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Log error but don't fail the request
|
||||||
|
// TODO: Add proper logging
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CreateNewOtpTokenResponseDto
|
||||||
|
{
|
||||||
|
IsSuccess = true,
|
||||||
|
Message = "کد تایید با موفقیت ارسال شد",
|
||||||
|
ExpiresAt = otpToken.ExpiresAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
namespace CMSMicroservice.Application.UserCQ.Commands.CreateNewOtpToken;
|
||||||
|
public class CreateNewOtpTokenCommandValidator : AbstractValidator<CreateNewOtpTokenCommand>
|
||||||
|
{
|
||||||
|
public CreateNewOtpTokenCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(model => model.Mobile)
|
||||||
|
.NotEmpty();
|
||||||
|
RuleFor(model => model.Purpose)
|
||||||
|
.NotEmpty();
|
||||||
|
}
|
||||||
|
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||||
|
{
|
||||||
|
var result = await ValidateAsync(ValidationContext<CreateNewOtpTokenCommand>.CreateWithOptions((CreateNewOtpTokenCommand)model, x => x.IncludeProperties(propertyName)));
|
||||||
|
if (result.IsValid)
|
||||||
|
return Array.Empty<string>();
|
||||||
|
return result.Errors.Select(e => e.ErrorMessage);
|
||||||
|
};
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
namespace CMSMicroservice.Application.UserCQ.Commands.CreateNewOtpToken;
|
||||||
|
public class CreateNewOtpTokenResponseDto
|
||||||
|
{
|
||||||
|
//موفق؟
|
||||||
|
public bool IsSuccess { get; set; }
|
||||||
|
//پیام
|
||||||
|
public string Message { get; set; }
|
||||||
|
//تلاش باقی مانده
|
||||||
|
public int RemainingAttempts { get; set; }
|
||||||
|
//ثانیه باقی مانده
|
||||||
|
public int RemainingSeconds { get; set; }
|
||||||
|
//زمان انقضاء
|
||||||
|
public DateTime? ExpiresAt { get; set; }
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
namespace CMSMicroservice.Application.UserCQ.Commands.VerifyOtpToken;
|
||||||
|
public record VerifyOtpTokenCommand : IRequest<VerifyOtpTokenResponseDto>
|
||||||
|
{
|
||||||
|
//موبایل مقصد
|
||||||
|
public string Mobile { get; init; }
|
||||||
|
//مقصود
|
||||||
|
public string Purpose { get; init; }
|
||||||
|
//کد
|
||||||
|
public string Code { get; init; }
|
||||||
|
//کد معرف والد
|
||||||
|
public string? ParentReferralCode { get; init; }
|
||||||
|
|
||||||
|
}
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.UserCQ.Commands.VerifyOtpToken;
|
||||||
|
public class VerifyOtpTokenCommandHandler : IRequestHandler<VerifyOtpTokenCommand, VerifyOtpTokenResponseDto>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public VerifyOtpTokenCommandHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<VerifyOtpTokenResponseDto> Handle(VerifyOtpTokenCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var otpToken = await _context.OtpTokens
|
||||||
|
.Where(x => x.Mobile == request.Mobile && x.Purpose == request.Purpose && !x.IsUsed)
|
||||||
|
.OrderByDescending(x => x.Id) // Use Id instead of CreatedAt for now
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (otpToken == null || !otpToken.IsValid(request.Code))
|
||||||
|
return new VerifyOtpTokenResponseDto { IsSuccess = false, Message = "کد تایید نامعتبر است" };
|
||||||
|
|
||||||
|
var user = await _context.Users
|
||||||
|
.Where(x => x.Mobile == request.Mobile)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
return new VerifyOtpTokenResponseDto { IsSuccess = false, Message = "کاربر یافت نشد" };
|
||||||
|
|
||||||
|
// Mark OTP as used
|
||||||
|
otpToken.IsUsed = true;
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
// TODO: Implement JWT token generation
|
||||||
|
return new VerifyOtpTokenResponseDto
|
||||||
|
{
|
||||||
|
IsSuccess = true,
|
||||||
|
Message = "کد تایید با موفقیت تایید شد",
|
||||||
|
Token = "TODO_IMPLEMENT_JWT_GENERATION"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
namespace CMSMicroservice.Application.UserCQ.Commands.VerifyOtpToken;
|
||||||
|
public class VerifyOtpTokenCommandValidator : AbstractValidator<VerifyOtpTokenCommand>
|
||||||
|
{
|
||||||
|
public VerifyOtpTokenCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(model => model.Mobile)
|
||||||
|
.NotEmpty();
|
||||||
|
RuleFor(model => model.Purpose)
|
||||||
|
.NotEmpty();
|
||||||
|
RuleFor(model => model.Code)
|
||||||
|
.NotEmpty();
|
||||||
|
}
|
||||||
|
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||||
|
{
|
||||||
|
var result = await ValidateAsync(ValidationContext<VerifyOtpTokenCommand>.CreateWithOptions((VerifyOtpTokenCommand)model, x => x.IncludeProperties(propertyName)));
|
||||||
|
if (result.IsValid)
|
||||||
|
return Array.Empty<string>();
|
||||||
|
return result.Errors.Select(e => e.ErrorMessage);
|
||||||
|
};
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
namespace CMSMicroservice.Application.UserCQ.Commands.VerifyOtpToken;
|
||||||
|
public class VerifyOtpTokenResponseDto
|
||||||
|
{
|
||||||
|
//موفق؟
|
||||||
|
public bool IsSuccess { get; set; }
|
||||||
|
//پیام
|
||||||
|
public string Message { get; set; }
|
||||||
|
//توکن
|
||||||
|
public string? Token { get; set; }
|
||||||
|
//تلاش باقی مانده
|
||||||
|
public int RemainingAttempts { get; set; }
|
||||||
|
//ثانیه باقی مانده
|
||||||
|
public int RemainingSeconds { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.UserCartsCQ.Commands.ClearCart;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Command برای پاک کردن تمام سبد خرید کاربر
|
|
||||||
/// </summary>
|
|
||||||
public record ClearCartCommand : IRequest<ClearCartResponseDto>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// شناسه کاربر
|
|
||||||
/// </summary>
|
|
||||||
public long UserId { get; init; }
|
|
||||||
}
|
|
||||||
-52
@@ -1,52 +0,0 @@
|
|||||||
using CMSMicroservice.Domain.Events;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.UserCartsCQ.Commands.ClearCart;
|
|
||||||
|
|
||||||
public class ClearCartCommandHandler : IRequestHandler<ClearCartCommand, ClearCartResponseDto>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
|
|
||||||
public ClearCartCommandHandler(IApplicationDbContext context)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ClearCartResponseDto> Handle(ClearCartCommand request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// پیدا کردن تمام آیتمهای سبد خرید کاربر
|
|
||||||
var cartItems = await _context.UserCarts
|
|
||||||
.Where(c => c.UserId == request.UserId)
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
if (!cartItems.Any())
|
|
||||||
{
|
|
||||||
return new ClearCartResponseDto
|
|
||||||
{
|
|
||||||
UserId = request.UserId,
|
|
||||||
RemovedItemsCount = 0,
|
|
||||||
Message = "سبد خرید خالی است"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var itemsCount = cartItems.Count;
|
|
||||||
|
|
||||||
// حذف تمام آیتمها
|
|
||||||
_context.UserCarts.RemoveRange(cartItems);
|
|
||||||
|
|
||||||
// ثبت Event
|
|
||||||
// میتونیم یک Event برای هر آیتم یا یک Event کلی بفرستیم
|
|
||||||
foreach (var item in cartItems)
|
|
||||||
{
|
|
||||||
item.AddDomainEvent(new ClearCartEvent(item));
|
|
||||||
}
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
|
||||||
|
|
||||||
return new ClearCartResponseDto
|
|
||||||
{
|
|
||||||
UserId = request.UserId,
|
|
||||||
RemovedItemsCount = itemsCount,
|
|
||||||
Message = $"{itemsCount} آیتم از سبد خرید حذف شد"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-11
@@ -1,11 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.UserCartsCQ.Commands.ClearCart;
|
|
||||||
|
|
||||||
public class ClearCartCommandValidator : AbstractValidator<ClearCartCommand>
|
|
||||||
{
|
|
||||||
public ClearCartCommandValidator()
|
|
||||||
{
|
|
||||||
RuleFor(v => v.UserId)
|
|
||||||
.GreaterThan(0)
|
|
||||||
.WithMessage("شناسه کاربر باید بزرگتر از صفر باشد");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.UserCartsCQ.Commands.ClearCart;
|
|
||||||
|
|
||||||
public class ClearCartResponseDto
|
|
||||||
{
|
|
||||||
public long UserId { get; set; }
|
|
||||||
public int RemovedItemsCount { get; set; }
|
|
||||||
public string Message { get; set; }
|
|
||||||
}
|
|
||||||
-11
@@ -1,11 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.UserCartsCQ.Commands.CreateNewUserCarts;
|
|
||||||
public record CreateNewUserCartsCommand : IRequest<CreateNewUserCartsResponseDto>
|
|
||||||
{
|
|
||||||
//
|
|
||||||
public long ProductId { get; init; }
|
|
||||||
//
|
|
||||||
public long UserId { get; init; }
|
|
||||||
//
|
|
||||||
public int Count { get; init; }
|
|
||||||
|
|
||||||
}
|
|
||||||
-31
@@ -1,31 +0,0 @@
|
|||||||
using CMSMicroservice.Domain.Events;
|
|
||||||
namespace CMSMicroservice.Application.UserCartsCQ.Commands.CreateNewUserCarts;
|
|
||||||
public class CreateNewUserCartsCommandHandler : IRequestHandler<CreateNewUserCartsCommand, CreateNewUserCartsResponseDto>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
|
|
||||||
public CreateNewUserCartsCommandHandler(IApplicationDbContext context)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<CreateNewUserCartsResponseDto> Handle(CreateNewUserCartsCommand request,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var entity = request.Adapt<UserCart>();
|
|
||||||
var existingUserCart = await _context.UserCarts
|
|
||||||
.FirstOrDefaultAsync(x => x.UserId == entity.UserId && x.ProductId == entity.ProductId && !x.IsDeleted, cancellationToken);
|
|
||||||
if (existingUserCart != null)
|
|
||||||
{
|
|
||||||
existingUserCart.Count += entity.Count;
|
|
||||||
_context.UserCarts.Update(existingUserCart);
|
|
||||||
existingUserCart.AddDomainEvent(new UpdateUserCartsEvent(existingUserCart));
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
|
||||||
return existingUserCart.Adapt<CreateNewUserCartsResponseDto>();
|
|
||||||
}
|
|
||||||
await _context.UserCarts.AddAsync(entity, cancellationToken);
|
|
||||||
entity.AddDomainEvent(new CreateNewUserCartsEvent(entity));
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
|
||||||
return entity.Adapt<CreateNewUserCartsResponseDto>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-20
@@ -1,20 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.UserCartsCQ.Commands.CreateNewUserCarts;
|
|
||||||
public class CreateNewUserCartsCommandValidator : AbstractValidator<CreateNewUserCartsCommand>
|
|
||||||
{
|
|
||||||
public CreateNewUserCartsCommandValidator()
|
|
||||||
{
|
|
||||||
RuleFor(model => model.ProductId)
|
|
||||||
.NotNull();
|
|
||||||
RuleFor(model => model.UserId)
|
|
||||||
.NotNull();
|
|
||||||
RuleFor(model => model.Count)
|
|
||||||
.NotNull();
|
|
||||||
}
|
|
||||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
|
||||||
{
|
|
||||||
var result = await ValidateAsync(ValidationContext<CreateNewUserCartsCommand>.CreateWithOptions((CreateNewUserCartsCommand)model, x => x.IncludeProperties(propertyName)));
|
|
||||||
if (result.IsValid)
|
|
||||||
return Array.Empty<string>();
|
|
||||||
return result.Errors.Select(e => e.ErrorMessage);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
-7
@@ -1,7 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.UserCartsCQ.Commands.CreateNewUserCarts;
|
|
||||||
public class CreateNewUserCartsResponseDto
|
|
||||||
{
|
|
||||||
//
|
|
||||||
public long Id { get; set; }
|
|
||||||
|
|
||||||
}
|
|
||||||
-7
@@ -1,7 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.UserCartsCQ.Commands.DeleteUserCarts;
|
|
||||||
public record DeleteUserCartsCommand : IRequest<Unit>
|
|
||||||
{
|
|
||||||
//
|
|
||||||
public long Id { get; init; }
|
|
||||||
|
|
||||||
}
|
|
||||||
-22
@@ -1,22 +0,0 @@
|
|||||||
using CMSMicroservice.Domain.Events;
|
|
||||||
namespace CMSMicroservice.Application.UserCartsCQ.Commands.DeleteUserCarts;
|
|
||||||
public class DeleteUserCartsCommandHandler : IRequestHandler<DeleteUserCartsCommand, Unit>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
|
|
||||||
public DeleteUserCartsCommandHandler(IApplicationDbContext context)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Unit> Handle(DeleteUserCartsCommand request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var entity = await _context.UserCarts
|
|
||||||
.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken) ?? throw new NotFoundException(nameof(UserCart), request.Id);
|
|
||||||
entity.IsDeleted = true;
|
|
||||||
_context.UserCarts.Update(entity);
|
|
||||||
entity.AddDomainEvent(new DeleteUserCartsEvent(entity));
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
|
||||||
return Unit.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-16
@@ -1,16 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.UserCartsCQ.Commands.DeleteUserCarts;
|
|
||||||
public class DeleteUserCartsCommandValidator : AbstractValidator<DeleteUserCartsCommand>
|
|
||||||
{
|
|
||||||
public DeleteUserCartsCommandValidator()
|
|
||||||
{
|
|
||||||
RuleFor(model => model.Id)
|
|
||||||
.NotNull();
|
|
||||||
}
|
|
||||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
|
||||||
{
|
|
||||||
var result = await ValidateAsync(ValidationContext<DeleteUserCartsCommand>.CreateWithOptions((DeleteUserCartsCommand)model, x => x.IncludeProperties(propertyName)));
|
|
||||||
if (result.IsValid)
|
|
||||||
return Array.Empty<string>();
|
|
||||||
return result.Errors.Select(e => e.ErrorMessage);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.UserCartsCQ.Commands.MergeCart;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Command برای ادغام سبد خرید مهمان با سبد خرید کاربر بعد از ورود
|
|
||||||
/// </summary>
|
|
||||||
public record MergeCartCommand : IRequest<MergeCartResponseDto>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// شناسه کاربر (بعد از Login)
|
|
||||||
/// </summary>
|
|
||||||
public long UserId { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// لیست محصولات سبد مهمان
|
|
||||||
/// </summary>
|
|
||||||
public List<GuestCartItem> GuestCartItems { get; init; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class GuestCartItem
|
|
||||||
{
|
|
||||||
public long ProductId { get; set; }
|
|
||||||
public int Count { get; set; }
|
|
||||||
}
|
|
||||||
-97
@@ -1,97 +0,0 @@
|
|||||||
using CMSMicroservice.Application.Common.Interfaces;
|
|
||||||
using CMSMicroservice.Domain.Entities;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.UserCartsCQ.Commands.MergeCart;
|
|
||||||
|
|
||||||
public class MergeCartCommandHandler : IRequestHandler<MergeCartCommand, MergeCartResponseDto>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
|
|
||||||
public MergeCartCommandHandler(IApplicationDbContext context)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<MergeCartResponseDto> Handle(MergeCartCommand request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// بررسی وجود کاربر
|
|
||||||
var user = await _context.Users
|
|
||||||
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
return new MergeCartResponseDto
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Message = "کاربر یافت نشد"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// دریافت سبد فعلی کاربر
|
|
||||||
var existingCartItems = await _context.UserCarts
|
|
||||||
.Where(c => c.UserId == request.UserId && !c.IsDeleted)
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
int mergedCount = 0;
|
|
||||||
|
|
||||||
// ادغام آیتمهای مهمان با سبد کاربر
|
|
||||||
foreach (var guestItem in request.GuestCartItems)
|
|
||||||
{
|
|
||||||
// بررسی موجود بودن محصول
|
|
||||||
var product = await _context.Products
|
|
||||||
.FirstOrDefaultAsync(p => p.Id == guestItem.ProductId && !p.IsDeleted, cancellationToken);
|
|
||||||
|
|
||||||
if (product == null)
|
|
||||||
continue; // محصول پیدا نشد یا حذف شده
|
|
||||||
|
|
||||||
// بررسی موجودی
|
|
||||||
if (product.RemainingCount < guestItem.Count)
|
|
||||||
continue; // موجودی کافی نیست
|
|
||||||
|
|
||||||
// چک کردن آیا این محصول قبلاً در سبد کاربر هست
|
|
||||||
var existingItem = existingCartItems.FirstOrDefault(c => c.ProductId == guestItem.ProductId);
|
|
||||||
|
|
||||||
if (existingItem != null)
|
|
||||||
{
|
|
||||||
// آیتم موجود است → افزایش تعداد
|
|
||||||
existingItem.Count += guestItem.Count;
|
|
||||||
|
|
||||||
// محدود کردن به موجودی
|
|
||||||
if (existingItem.Count > product.RemainingCount)
|
|
||||||
existingItem.Count = product.RemainingCount;
|
|
||||||
|
|
||||||
_context.UserCarts.Update(existingItem);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// آیتم جدید → اضافه کردن به سبد
|
|
||||||
var newCartItem = new UserCart
|
|
||||||
{
|
|
||||||
UserId = request.UserId,
|
|
||||||
ProductId = guestItem.ProductId,
|
|
||||||
Count = Math.Min(guestItem.Count, product.RemainingCount)
|
|
||||||
};
|
|
||||||
|
|
||||||
await _context.UserCarts.AddAsync(newCartItem, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
mergedCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
|
||||||
|
|
||||||
// محاسبه تعداد کل آیتمهای سبد بعد از ادغام
|
|
||||||
var totalItems = await _context.UserCarts
|
|
||||||
.Where(c => c.UserId == request.UserId && !c.IsDeleted)
|
|
||||||
.CountAsync(cancellationToken);
|
|
||||||
|
|
||||||
return new MergeCartResponseDto
|
|
||||||
{
|
|
||||||
Success = true,
|
|
||||||
Message = $"{mergedCount} محصول با موفقیت به سبد خرید اضافه شد",
|
|
||||||
MergedItemsCount = mergedCount,
|
|
||||||
TotalCartItems = totalItems
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-31
@@ -1,31 +0,0 @@
|
|||||||
using FluentValidation;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.UserCartsCQ.Commands.MergeCart;
|
|
||||||
|
|
||||||
public class MergeCartCommandValidator : AbstractValidator<MergeCartCommand>
|
|
||||||
{
|
|
||||||
public MergeCartCommandValidator()
|
|
||||||
{
|
|
||||||
RuleFor(x => x.UserId)
|
|
||||||
.GreaterThan(0)
|
|
||||||
.WithMessage("شناسه کاربر نامعتبر است");
|
|
||||||
|
|
||||||
RuleFor(x => x.GuestCartItems)
|
|
||||||
.NotNull()
|
|
||||||
.WithMessage("لیست آیتمهای سبد خرید نباید خالی باشد");
|
|
||||||
|
|
||||||
RuleForEach(x => x.GuestCartItems)
|
|
||||||
.ChildRules(item =>
|
|
||||||
{
|
|
||||||
item.RuleFor(i => i.ProductId)
|
|
||||||
.GreaterThan(0)
|
|
||||||
.WithMessage("شناسه محصول نامعتبر است");
|
|
||||||
|
|
||||||
item.RuleFor(i => i.Count)
|
|
||||||
.GreaterThan(0)
|
|
||||||
.WithMessage("تعداد باید بیشتر از صفر باشد")
|
|
||||||
.LessThanOrEqualTo(100)
|
|
||||||
.WithMessage("حداکثر تعداد مجاز 100 عدد است");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.UserCartsCQ.Commands.MergeCart;
|
|
||||||
|
|
||||||
public class MergeCartResponseDto
|
|
||||||
{
|
|
||||||
public bool Success { get; set; }
|
|
||||||
public string Message { get; set; }
|
|
||||||
public int MergedItemsCount { get; set; }
|
|
||||||
public int TotalCartItems { get; set; }
|
|
||||||
}
|
|
||||||
-9
@@ -1,9 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.UserCartsCQ.Commands.UpdateUserCarts;
|
|
||||||
public record UpdateUserCartsCommand : IRequest<Unit>
|
|
||||||
{
|
|
||||||
//
|
|
||||||
public long Id { get; init; }
|
|
||||||
//
|
|
||||||
public int Count { get; init; }
|
|
||||||
|
|
||||||
}
|
|
||||||
-29
@@ -1,29 +0,0 @@
|
|||||||
using CMSMicroservice.Application.UserCartsCQ.Commands.DeleteUserCarts;
|
|
||||||
using CMSMicroservice.Domain.Events;
|
|
||||||
namespace CMSMicroservice.Application.UserCartsCQ.Commands.UpdateUserCarts;
|
|
||||||
public class UpdateUserCartsCommandHandler : IRequestHandler<UpdateUserCartsCommand, Unit>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
private readonly ISender _sender;
|
|
||||||
|
|
||||||
public UpdateUserCartsCommandHandler(IApplicationDbContext context, ISender sender)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
_sender = sender;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Unit> Handle(UpdateUserCartsCommand request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (request.Count<=0)
|
|
||||||
{
|
|
||||||
await _sender.Send(request.Adapt<DeleteUserCartsCommand>(), cancellationToken);
|
|
||||||
}
|
|
||||||
var entity = await _context.UserCarts
|
|
||||||
.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken) ?? throw new NotFoundException(nameof(UserCart), request.Id);
|
|
||||||
request.Adapt(entity);
|
|
||||||
_context.UserCarts.Update(entity);
|
|
||||||
entity.AddDomainEvent(new UpdateUserCartsEvent(entity));
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
|
||||||
return Unit.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-18
@@ -1,18 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.UserCartsCQ.Commands.UpdateUserCarts;
|
|
||||||
public class UpdateUserCartsCommandValidator : AbstractValidator<UpdateUserCartsCommand>
|
|
||||||
{
|
|
||||||
public UpdateUserCartsCommandValidator()
|
|
||||||
{
|
|
||||||
RuleFor(model => model.Id)
|
|
||||||
.NotNull();
|
|
||||||
RuleFor(model => model.Count)
|
|
||||||
.NotNull();
|
|
||||||
}
|
|
||||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
|
||||||
{
|
|
||||||
var result = await ValidateAsync(ValidationContext<UpdateUserCartsCommand>.CreateWithOptions((UpdateUserCartsCommand)model, x => x.IncludeProperties(propertyName)));
|
|
||||||
if (result.IsValid)
|
|
||||||
return Array.Empty<string>();
|
|
||||||
return result.Errors.Select(e => e.ErrorMessage);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
-23
@@ -1,23 +0,0 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using CMSMicroservice.Domain.Events;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.UserCartsCQ.EventHandlers.ClearCartEventHandlers;
|
|
||||||
|
|
||||||
public class ClearCartEventHandler : INotificationHandler<ClearCartEvent>
|
|
||||||
{
|
|
||||||
private readonly ILogger<ClearCartEventHandler> _logger;
|
|
||||||
|
|
||||||
public ClearCartEventHandler(ILogger<ClearCartEventHandler> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Handle(ClearCartEvent notification, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Cart item {CartId} removed for user {UserId}",
|
|
||||||
notification.Item.Id,
|
|
||||||
notification.Item.UserId);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-22
@@ -1,22 +0,0 @@
|
|||||||
using CMSMicroservice.Domain.Events;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.UserCartsCQ.EventHandlers;
|
|
||||||
|
|
||||||
public class CreateNewUserCartsEventHandler : INotificationHandler<CreateNewUserCartsEvent>
|
|
||||||
{
|
|
||||||
private readonly ILogger<
|
|
||||||
CreateNewUserCartsEventHandler> _logger;
|
|
||||||
|
|
||||||
public CreateNewUserCartsEventHandler(ILogger<CreateNewUserCartsEventHandler> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Handle(CreateNewUserCartsEvent 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.UserCartsCQ.EventHandlers;
|
|
||||||
|
|
||||||
public class DeleteUserCartsEventHandler : INotificationHandler<DeleteUserCartsEvent>
|
|
||||||
{
|
|
||||||
private readonly ILogger<
|
|
||||||
DeleteUserCartsEventHandler> _logger;
|
|
||||||
|
|
||||||
public DeleteUserCartsEventHandler(ILogger<DeleteUserCartsEventHandler> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Handle(DeleteUserCartsEvent 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.UserCartsCQ.EventHandlers;
|
|
||||||
|
|
||||||
public class UpdateUserCartsEventHandler : INotificationHandler<UpdateUserCartsEvent>
|
|
||||||
{
|
|
||||||
private readonly ILogger<
|
|
||||||
UpdateUserCartsEventHandler> _logger;
|
|
||||||
|
|
||||||
public UpdateUserCartsEventHandler(ILogger<UpdateUserCartsEventHandler> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Handle(UpdateUserCartsEvent notification, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Domain Event: {DomainEvent}", notification.GetType().Name);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.UserCartsCQ.Queries.GetAllUserCartsByFilter;
|
|
||||||
public record GetAllUserCartsByFilterQuery : IRequest<GetAllUserCartsByFilterResponseDto>
|
|
||||||
{
|
|
||||||
//موقعیت صفحه بندی
|
|
||||||
public PaginationState? PaginationState { get; init; }
|
|
||||||
//مرتب سازی بر اساس
|
|
||||||
public string? SortBy { get; init; }
|
|
||||||
//فیلتر
|
|
||||||
public GetAllUserCartsByFilterFilter? Filter { get; init; }
|
|
||||||
|
|
||||||
}public class GetAllUserCartsByFilterFilter
|
|
||||||
{
|
|
||||||
//
|
|
||||||
public long? Id { get; set; }
|
|
||||||
//
|
|
||||||
public long? ProductId { get; set; }
|
|
||||||
//
|
|
||||||
public long? UserId { get; set; }
|
|
||||||
//
|
|
||||||
public int? Count { get; set; }
|
|
||||||
}
|
|
||||||
-33
@@ -1,33 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.UserCartsCQ.Queries.GetAllUserCartsByFilter;
|
|
||||||
public class GetAllUserCartsByFilterQueryHandler : IRequestHandler<GetAllUserCartsByFilterQuery, GetAllUserCartsByFilterResponseDto>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
|
|
||||||
public GetAllUserCartsByFilterQueryHandler(IApplicationDbContext context)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<GetAllUserCartsByFilterResponseDto> Handle(GetAllUserCartsByFilterQuery request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var query = _context.UserCarts.Include(i=>i.Product)
|
|
||||||
.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.ProductId == null || x.ProductId == request.Filter.ProductId)
|
|
||||||
.Where(x => request.Filter.UserId == null || x.UserId == request.Filter.UserId)
|
|
||||||
.Where(x => request.Filter.Count == null || x.Count == request.Filter.Count)
|
|
||||||
;
|
|
||||||
}
|
|
||||||
return new GetAllUserCartsByFilterResponseDto
|
|
||||||
{
|
|
||||||
MetaData = await query.GetMetaData(request.PaginationState, cancellationToken),
|
|
||||||
Models = await query.PaginatedListAsync(paginationState: request.PaginationState)
|
|
||||||
.ProjectToType<GetAllUserCartsByFilterResponseModel>().ToListAsync(cancellationToken)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-14
@@ -1,14 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.UserCartsCQ.Queries.GetAllUserCartsByFilter;
|
|
||||||
public class GetAllUserCartsByFilterQueryValidator : AbstractValidator<GetAllUserCartsByFilterQuery>
|
|
||||||
{
|
|
||||||
public GetAllUserCartsByFilterQueryValidator()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
|
||||||
{
|
|
||||||
var result = await ValidateAsync(ValidationContext<GetAllUserCartsByFilterQuery>.CreateWithOptions((GetAllUserCartsByFilterQuery)model, x => x.IncludeProperties(propertyName)));
|
|
||||||
if (result.IsValid)
|
|
||||||
return Array.Empty<string>();
|
|
||||||
return result.Errors.Select(e => e.ErrorMessage);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
-31
@@ -1,31 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.UserCartsCQ.Queries.GetAllUserCartsByFilter;
|
|
||||||
public class GetAllUserCartsByFilterResponseDto
|
|
||||||
{
|
|
||||||
//متادیتا
|
|
||||||
public MetaData MetaData { get; set; }
|
|
||||||
//مدل خروجی
|
|
||||||
public List<GetAllUserCartsByFilterResponseModel>? Models { get; set; }
|
|
||||||
|
|
||||||
}public class GetAllUserCartsByFilterResponseModel
|
|
||||||
{
|
|
||||||
//
|
|
||||||
public long Id { get; set; }
|
|
||||||
//
|
|
||||||
public long ProductId { get; set; }
|
|
||||||
//
|
|
||||||
public long UserId { get; set; }
|
|
||||||
//
|
|
||||||
public int Count { get; set; }
|
|
||||||
//
|
|
||||||
public string ProductTitle { get; set; }
|
|
||||||
//
|
|
||||||
public string ProductShortInfomation { get; set; }
|
|
||||||
//
|
|
||||||
public long ProductPrice { get; set; }
|
|
||||||
//
|
|
||||||
public int ProductDiscount { get; set; }
|
|
||||||
//
|
|
||||||
public string ProductThumbnailPath { get; set; }
|
|
||||||
//
|
|
||||||
public DateTime Created { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.UserCartsCQ.Queries.GetUserCarts;
|
|
||||||
public record GetUserCartsQuery : IRequest<GetUserCartsResponseDto>
|
|
||||||
{
|
|
||||||
//
|
|
||||||
public long Id { get; init; }
|
|
||||||
|
|
||||||
}
|
|
||||||
-22
@@ -1,22 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.UserCartsCQ.Queries.GetUserCarts;
|
|
||||||
public class GetUserCartsQueryHandler : IRequestHandler<GetUserCartsQuery, GetUserCartsResponseDto>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
|
|
||||||
public GetUserCartsQueryHandler(IApplicationDbContext context)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<GetUserCartsResponseDto> Handle(GetUserCartsQuery request,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var response = await _context.UserCarts
|
|
||||||
.AsNoTracking()
|
|
||||||
.Where(x => x.Id == request.Id)
|
|
||||||
.ProjectToType<GetUserCartsResponseDto>()
|
|
||||||
.FirstOrDefaultAsync(cancellationToken);
|
|
||||||
|
|
||||||
return response ?? throw new NotFoundException(nameof(UserCart), request.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-16
@@ -1,16 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.UserCartsCQ.Queries.GetUserCarts;
|
|
||||||
public class GetUserCartsQueryValidator : AbstractValidator<GetUserCartsQuery>
|
|
||||||
{
|
|
||||||
public GetUserCartsQueryValidator()
|
|
||||||
{
|
|
||||||
RuleFor(model => model.Id)
|
|
||||||
.NotNull();
|
|
||||||
}
|
|
||||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
|
||||||
{
|
|
||||||
var result = await ValidateAsync(ValidationContext<GetUserCartsQuery>.CreateWithOptions((GetUserCartsQuery)model, x => x.IncludeProperties(propertyName)));
|
|
||||||
if (result.IsValid)
|
|
||||||
return Array.Empty<string>();
|
|
||||||
return result.Errors.Select(e => e.ErrorMessage);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
-13
@@ -1,13 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.UserCartsCQ.Queries.GetUserCarts;
|
|
||||||
public class GetUserCartsResponseDto
|
|
||||||
{
|
|
||||||
//
|
|
||||||
public long Id { get; set; }
|
|
||||||
//
|
|
||||||
public long ProductId { get; set; }
|
|
||||||
//
|
|
||||||
public long UserId { get; set; }
|
|
||||||
//
|
|
||||||
public int Count { get; set; }
|
|
||||||
|
|
||||||
}
|
|
||||||
-39
@@ -1,39 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.UserOrderCQ.Commands.ApplyDiscountToOrder;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// اعمال تخفیف به سفارش
|
|
||||||
/// </summary>
|
|
||||||
public record ApplyDiscountToOrderCommand : IRequest<ApplyDiscountToOrderResponseDto>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// شناسه سفارش
|
|
||||||
/// </summary>
|
|
||||||
public long OrderId { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// مبلغ تخفیف (ریال)
|
|
||||||
/// </summary>
|
|
||||||
public long DiscountAmount { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// دلیل تخفیف
|
|
||||||
/// </summary>
|
|
||||||
public string Reason { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// کد تخفیف (اختیاری)
|
|
||||||
/// </summary>
|
|
||||||
public string? DiscountCode { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// پاسخ اعمال تخفیف
|
|
||||||
/// </summary>
|
|
||||||
public class ApplyDiscountToOrderResponseDto
|
|
||||||
{
|
|
||||||
public bool Success { get; set; }
|
|
||||||
public string Message { get; set; } = string.Empty;
|
|
||||||
public long OriginalAmount { get; set; }
|
|
||||||
public long DiscountAmount { get; set; }
|
|
||||||
public long FinalAmount { get; set; }
|
|
||||||
}
|
|
||||||
-74
@@ -1,74 +0,0 @@
|
|||||||
using CMSMicroservice.Application.Common.Interfaces;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.UserOrderCQ.Commands.ApplyDiscountToOrder;
|
|
||||||
|
|
||||||
public class ApplyDiscountToOrderCommandHandler : IRequestHandler<ApplyDiscountToOrderCommand, ApplyDiscountToOrderResponseDto>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
private readonly ILogger<ApplyDiscountToOrderCommandHandler> _logger;
|
|
||||||
|
|
||||||
public ApplyDiscountToOrderCommandHandler(
|
|
||||||
IApplicationDbContext context,
|
|
||||||
ILogger<ApplyDiscountToOrderCommandHandler> logger)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ApplyDiscountToOrderResponseDto> Handle(ApplyDiscountToOrderCommand request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// TODO: پیادهسازی اعمال تخفیف به سفارش
|
|
||||||
// 1. پیدا کردن سفارش:
|
|
||||||
// - var order = await _context.UserOrders.FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken)
|
|
||||||
// - بررسی null و پرتاب NotFoundException
|
|
||||||
//
|
|
||||||
// 2. بررسی شرایط اعمال تخفیف:
|
|
||||||
// - سفارش نباید Delivered یا Cancelled باشد
|
|
||||||
// - مبلغ تخفیف نباید بیشتر از Amount باشد
|
|
||||||
// - if (order.DeliveryStatus == DeliveryStatus.Delivered || order.DeliveryStatus == DeliveryStatus.Cancelled)
|
|
||||||
// throw new InvalidOperationException("نمیتوان به این سفارش تخفیف اعمال کرد")
|
|
||||||
// - if (request.DiscountAmount > order.Amount)
|
|
||||||
// throw new InvalidOperationException("مبلغ تخفیف نمیتواند بیشتر از مبلغ سفارش باشد")
|
|
||||||
//
|
|
||||||
// 3. محاسبه مبلغ نهایی:
|
|
||||||
// - var originalAmount = order.Amount
|
|
||||||
// - var newDiscountedPrice = order.Amount - request.DiscountAmount
|
|
||||||
// - مطمئن شوید که منفی نشود: newDiscountedPrice = Math.Max(0, newDiscountedPrice)
|
|
||||||
//
|
|
||||||
// 4. بهروزرسانی سفارش:
|
|
||||||
// - order.DiscountedPrice = newDiscountedPrice
|
|
||||||
// - اگر فیلد OrderDiscountAmount وجود دارد، آن را هم بهروز کنید
|
|
||||||
// - order.OrderDiscountAmount = request.DiscountAmount
|
|
||||||
// - اضافه کردن به توضیحات:
|
|
||||||
// order.DeliveryDescription = (order.DeliveryDescription ?? "") +
|
|
||||||
// $"\nتخفیف اعمال شده: {request.DiscountAmount} ریال - دلیل: {request.Reason}"
|
|
||||||
//
|
|
||||||
// 5. ذخیره Log تخفیف (اختیاری - اگر جدول OrderDiscountLog دارید):
|
|
||||||
// - var discountLog = new OrderDiscountLog {
|
|
||||||
// OrderId = order.Id,
|
|
||||||
// DiscountAmount = request.DiscountAmount,
|
|
||||||
// Reason = request.Reason,
|
|
||||||
// DiscountCode = request.DiscountCode,
|
|
||||||
// AppliedAt = DateTime.Now
|
|
||||||
// }
|
|
||||||
// - await _context.OrderDiscountLogs.AddAsync(discountLog, cancellationToken)
|
|
||||||
//
|
|
||||||
// 6. ذخیره و Log:
|
|
||||||
// - await _context.SaveChangesAsync(cancellationToken)
|
|
||||||
// - _logger.LogInformation("Discount {Amount} applied to order {OrderId}: {Reason}",
|
|
||||||
// request.DiscountAmount, request.OrderId, request.Reason)
|
|
||||||
//
|
|
||||||
// 7. برگشت Response:
|
|
||||||
// - return new ApplyDiscountToOrderResponseDto {
|
|
||||||
// Success = true,
|
|
||||||
// Message = "تخفیف با موفقیت اعمال شد",
|
|
||||||
// OriginalAmount = originalAmount,
|
|
||||||
// DiscountAmount = request.DiscountAmount,
|
|
||||||
// FinalAmount = newDiscountedPrice
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// نکته: این تخفیف برای تخفیفات دستی Admin است و جدا از تخفیفهای محصول
|
|
||||||
|
|
||||||
throw new NotImplementedException("ApplyDiscountToOrder needs implementation");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.UserOrderCQ.Commands.ApplyDiscountToOrder;
|
|
||||||
|
|
||||||
public class ApplyDiscountToOrderCommandValidator : AbstractValidator<ApplyDiscountToOrderCommand>
|
|
||||||
{
|
|
||||||
public ApplyDiscountToOrderCommandValidator()
|
|
||||||
{
|
|
||||||
RuleFor(x => x.OrderId)
|
|
||||||
.GreaterThan(0)
|
|
||||||
.WithMessage("شناسه سفارش باید بزرگتر از 0 باشد");
|
|
||||||
|
|
||||||
RuleFor(x => x.DiscountAmount)
|
|
||||||
.GreaterThan(0)
|
|
||||||
.WithMessage("مبلغ تخفیف باید بزرگتر از 0 باشد");
|
|
||||||
|
|
||||||
RuleFor(x => x.Reason)
|
|
||||||
.NotEmpty()
|
|
||||||
.WithMessage("دلیل تخفیف الزامی است")
|
|
||||||
.MaximumLength(500)
|
|
||||||
.WithMessage("دلیل تخفیف نمیتواند بیشتر از 500 کاراکتر باشد");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-24
@@ -1,24 +0,0 @@
|
|||||||
using CMSMicroservice.Domain.Enums;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.UserOrderCQ.Commands.CancelOrder;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Command برای لغو سفارش
|
|
||||||
/// </summary>
|
|
||||||
public record CancelOrderCommand : IRequest<CancelOrderResponseDto>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// شناسه سفارش
|
|
||||||
/// </summary>
|
|
||||||
public long OrderId { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// دلیل لغو سفارش
|
|
||||||
/// </summary>
|
|
||||||
public string CancelReason { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// آیا مبلغ باید بازگردانده شود؟
|
|
||||||
/// </summary>
|
|
||||||
public bool RefundPayment { get; init; }
|
|
||||||
}
|
|
||||||
-93
@@ -1,93 +0,0 @@
|
|||||||
using CMSMicroservice.Application.Common.Interfaces;
|
|
||||||
using CMSMicroservice.Domain.Events;
|
|
||||||
using CMSMicroservice.Domain.Enums;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.UserOrderCQ.Commands.CancelOrder;
|
|
||||||
|
|
||||||
public class CancelOrderCommandHandler : IRequestHandler<CancelOrderCommand, CancelOrderResponseDto>
|
|
||||||
{
|
|
||||||
private readonly IApplicationDbContext _context;
|
|
||||||
private readonly IInventoryService _inventoryService;
|
|
||||||
|
|
||||||
public CancelOrderCommandHandler(
|
|
||||||
IApplicationDbContext context,
|
|
||||||
IInventoryService inventoryService)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
_inventoryService = inventoryService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<CancelOrderResponseDto> Handle(CancelOrderCommand request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// پیدا کردن سفارش با جزئیات
|
|
||||||
var order = await _context.UserOrders
|
|
||||||
.Include(o => o.Transaction)
|
|
||||||
.Include(o => o.FactorDetails)
|
|
||||||
.FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken);
|
|
||||||
|
|
||||||
if (order == null)
|
|
||||||
{
|
|
||||||
throw new NotFoundException(nameof(UserOrder), request.OrderId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// چک کردن که سفارش قابل لغو باشد
|
|
||||||
if (order.DeliveryStatus == DeliveryStatus.Delivered)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("سفارش تحویل داده شده قابل لغو نیست");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (order.DeliveryStatus == DeliveryStatus.Cancelled)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("این سفارش قبلاً لغو شده است");
|
|
||||||
}
|
|
||||||
|
|
||||||
// برگشت موجودی محصولات به انبار
|
|
||||||
foreach (var factorDetail in order.FactorDetails)
|
|
||||||
{
|
|
||||||
await _inventoryService.ProcessReturnAsync(
|
|
||||||
factorDetail.ProductId,
|
|
||||||
ProductType.RegularProduct,
|
|
||||||
factorDetail.Count,
|
|
||||||
order.Id,
|
|
||||||
$"لغو سفارش: {request.CancelReason}",
|
|
||||||
null,
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
// تغییر وضعیت سفارش
|
|
||||||
order.DeliveryStatus = DeliveryStatus.Cancelled;
|
|
||||||
order.DeliveryDescription = $"لغو شده: {request.CancelReason}";
|
|
||||||
|
|
||||||
// اگر درخواست بازگشت پول داریم و پرداخت موفق بوده
|
|
||||||
if (request.RefundPayment &&
|
|
||||||
order.Transaction != null &&
|
|
||||||
order.Transaction.PaymentStatus == PaymentStatus.Success)
|
|
||||||
{
|
|
||||||
// ایجاد تراکنش استرداد
|
|
||||||
var refundTransaction = new Transaction
|
|
||||||
{
|
|
||||||
Amount = -order.Amount,
|
|
||||||
Description = $"بازگشت وجه سفارش {request.OrderId}: {request.CancelReason}",
|
|
||||||
PaymentStatus = PaymentStatus.Success,
|
|
||||||
PaymentDate = DateTime.Now,
|
|
||||||
RefId = $"REFUND-ORDER-{order.Id}",
|
|
||||||
Type = TransactionType.Buy
|
|
||||||
};
|
|
||||||
|
|
||||||
await _context.Transactions.AddAsync(refundTransaction, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ثبت Event
|
|
||||||
order.AddDomainEvent(new CancelOrderEvent(order, request.CancelReason));
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
|
||||||
|
|
||||||
return new CancelOrderResponseDto
|
|
||||||
{
|
|
||||||
OrderId = order.Id,
|
|
||||||
Status = order.DeliveryStatus,
|
|
||||||
Message = "سفارش با موفقیت لغو شد",
|
|
||||||
RefundProcessed = request.RefundPayment && order.Transaction != null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-17
@@ -1,17 +0,0 @@
|
|||||||
namespace CMSMicroservice.Application.UserOrderCQ.Commands.CancelOrder;
|
|
||||||
|
|
||||||
public class CancelOrderCommandValidator : AbstractValidator<CancelOrderCommand>
|
|
||||||
{
|
|
||||||
public CancelOrderCommandValidator()
|
|
||||||
{
|
|
||||||
RuleFor(v => v.OrderId)
|
|
||||||
.GreaterThan(0)
|
|
||||||
.WithMessage("شناسه سفارش باید بزرگتر از صفر باشد");
|
|
||||||
|
|
||||||
RuleFor(v => v.CancelReason)
|
|
||||||
.NotEmpty()
|
|
||||||
.WithMessage("دلیل لغو سفارش الزامی است")
|
|
||||||
.MaximumLength(500)
|
|
||||||
.WithMessage("دلیل لغو نباید بیش از 500 کاراکتر باشد");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-11
@@ -1,11 +0,0 @@
|
|||||||
using CMSMicroservice.Domain.Enums;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.UserOrderCQ.Commands.CancelOrder;
|
|
||||||
|
|
||||||
public class CancelOrderResponseDto
|
|
||||||
{
|
|
||||||
public long OrderId { get; set; }
|
|
||||||
public DeliveryStatus Status { get; set; }
|
|
||||||
public string Message { get; set; }
|
|
||||||
public bool RefundProcessed { get; set; }
|
|
||||||
}
|
|
||||||
-23
@@ -1,23 +0,0 @@
|
|||||||
using CMSMicroservice.Domain.Enums;
|
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.UserOrderCQ.Commands.CreateNewUserOrder;
|
|
||||||
public record CreateNewUserOrderCommand : IRequest<CreateNewUserOrderResponseDto>
|
|
||||||
{
|
|
||||||
//قیمت
|
|
||||||
public long Amount { get; init; }
|
|
||||||
//شناسه پکیج
|
|
||||||
public long PackageId { get; init; }
|
|
||||||
//شناسه تراکنش
|
|
||||||
public long? TransactionId { get; init; }
|
|
||||||
//وضعیت پرداخت
|
|
||||||
public PaymentStatus PaymentStatus { get; init; }
|
|
||||||
//تاریخ پرداخت
|
|
||||||
public DateTime? PaymentDate { get; init; }
|
|
||||||
//شناسه کاربر
|
|
||||||
public long UserId { get; init; }
|
|
||||||
//شناسه آدرس کاربر
|
|
||||||
public long UserAddressId { get; init; }
|
|
||||||
//
|
|
||||||
public PaymentMethod? PaymentMethod { get; init; }
|
|
||||||
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user