Add validators and services for Product Galleries and Product Tags

- Implemented Create, Delete, Get, and Update validators for Product Galleries.
- Added Create, Delete, Get, and Update validators for Product Tags.
- Created service classes for handling Discount Categories, Discount Orders, Discount Products, Discount Shopping Cart, Product Categories, Product Galleries, and Product Tags.
- Each service class integrates with CQRS for command and query handling.
- Established mapping profiles for Product Galleries.
This commit is contained in:
masoodafar-web
2025-12-04 02:40:49 +03:30
parent 40d54d08fc
commit f0f48118e7
436 changed files with 33159 additions and 2005 deletions
@@ -0,0 +1,64 @@
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;
}
@@ -0,0 +1,77 @@
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;
}
}
@@ -0,0 +1,30 @@
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 باشد");
});
}
}
@@ -0,0 +1,80 @@
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;
}
@@ -0,0 +1,88 @@
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;
}
}
@@ -0,0 +1,22 @@
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("مقدار موجودی نامعتبر است");
});
}
}
@@ -13,8 +13,8 @@ public class CreateNewProductsCommandHandler : IRequestHandler<CreateNewProducts
public async Task<CreateNewProductsResponseDto> Handle(CreateNewProductsCommand request,
CancellationToken cancellationToken)
{
var entity = request.Adapt<Products>();
await _context.Productss.AddAsync(entity, cancellationToken);
var entity = request.Adapt<Product>();
await _context.Products.AddAsync(entity, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
// ثبت دسته‌بندی‌های محصول (در صورت ارسال)
@@ -27,12 +27,12 @@ public class CreateNewProductsCommandHandler : IRequestHandler<CreateNewProducts
foreach (var categoryId in distinctCategoryIds)
{
var rel = new PruductCategory
var rel = new ProductCategory
{
ProductId = entity.Id,
CategoryId = categoryId
};
await _context.PruductCategorys.AddAsync(rel, cancellationToken);
await _context.ProductCategories.AddAsync(rel, cancellationToken);
}
await _context.SaveChangesAsync(cancellationToken);
@@ -11,10 +11,10 @@ public class DeleteProductsCommandHandler : IRequestHandler<DeleteProductsComman
public async Task<Unit> Handle(DeleteProductsCommand request, CancellationToken cancellationToken)
{
var entity = await _context.Productss
.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken) ?? throw new NotFoundException(nameof(Products), request.Id);
var entity = await _context.Products
.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken) ?? throw new NotFoundException(nameof(Product), request.Id);
entity.IsDeleted = true;
_context.Productss.Update(entity);
_context.Products.Update(entity);
entity.AddDomainEvent(new DeleteProductsEvent(entity));
await _context.SaveChangesAsync(cancellationToken);
return Unit.Value;
@@ -0,0 +1,49 @@
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;
}
@@ -0,0 +1,82 @@
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;
}
}
@@ -0,0 +1,16 @@
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("موجودی پیش‌فرض نمی‌تواند منفی باشد");
}
}
@@ -0,0 +1,36 @@
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; }
}
@@ -0,0 +1,92 @@
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;
}
}
@@ -0,0 +1,40 @@
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("نمی‌توان همزمان موجودی جدید و افزایش موجودی را مشخص کرد");
}
}
@@ -0,0 +1,10 @@
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();
}
@@ -15,12 +15,12 @@ public class UpdateProductsCommandHandler : IRequestHandler<UpdateProductsComman
public async Task<Unit> Handle(UpdateProductsCommand request, CancellationToken cancellationToken)
{
var entity = await _context.Productss
var entity = await _context.Products
.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken)
?? throw new NotFoundException(nameof(Products), request.Id);
?? throw new NotFoundException(nameof(Product), request.Id);
request.Adapt(entity);
_context.Productss.Update(entity);
_context.Products.Update(entity);
// به‌روزرسانی دسته‌بندی‌های محصول در صورت ارسال CategoryIds
if (request.CategoryIds is not null)
@@ -30,7 +30,7 @@ public class UpdateProductsCommandHandler : IRequestHandler<UpdateProductsComman
.Distinct()
.ToHashSet();
var existingRelations = await _context.PruductCategorys
var existingRelations = await _context.ProductCategories
.Where(x => x.ProductId == entity.Id)
.ToListAsync(cancellationToken);
@@ -43,17 +43,17 @@ public class UpdateProductsCommandHandler : IRequestHandler<UpdateProductsComman
foreach (var categoryId in toAdd)
{
var rel = new PruductCategory
var rel = new ProductCategory
{
ProductId = entity.Id,
CategoryId = categoryId
};
await _context.PruductCategorys.AddAsync(rel, cancellationToken);
await _context.ProductCategories.AddAsync(rel, cancellationToken);
}
if (toRemove.Count > 0)
{
_context.PruductCategorys.RemoveRange(toRemove);
_context.ProductCategories.RemoveRange(toRemove);
}
}
@@ -10,7 +10,7 @@ public class GetAllProductsByFilterQueryHandler : IRequestHandler<GetAllProducts
public async Task<GetAllProductsByFilterResponseDto> Handle(GetAllProductsByFilterQuery request, CancellationToken cancellationToken)
{
var query = _context.Productss
var query = _context.Products
.ApplyOrder(sortBy: request.SortBy)
.AsNoTracking()
.AsQueryable();
@@ -25,7 +25,7 @@ public class GetAllProductsByFilterQueryHandler : IRequestHandler<GetAllProducts
.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.PruductCategorys.Any(pc => pc.CategoryId == request.Filter.CategoryId))
.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)
@@ -52,7 +52,7 @@ public class GetAllProductsByFilterQueryHandler : IRequestHandler<GetAllProducts
SaleCount = x.SaleCount,
ViewCount = x.ViewCount,
RemainingCount = x.RemainingCount,
CategoryIds = x.PruductCategorys
CategoryIds = x.ProductCategories
.Select(pc => pc.CategoryId)
.ToList()
})
@@ -0,0 +1,54 @@
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; }
}
@@ -0,0 +1,70 @@
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);
return new GetLowStockProductsResponseDto
{
MetaData = new MetaData
{
CurrentPage = request.PageIndex,
PageSize = request.PageSize,
TotalCount = totalCount
},
Products = products
};
}
}
@@ -0,0 +1,16 @@
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 باشد");
}
}
@@ -11,7 +11,7 @@ public class GetProductsQueryHandler : IRequestHandler<GetProductsQuery, GetProd
public async Task<GetProductsResponseDto> Handle(GetProductsQuery request,
CancellationToken cancellationToken)
{
var response = await _context.Productss
var response = await _context.Products
.AsNoTracking()
.Where(x => x.Id == request.Id)
.Select(x => new GetProductsResponseDto
@@ -29,12 +29,12 @@ public class GetProductsQueryHandler : IRequestHandler<GetProductsQuery, GetProd
SaleCount = x.SaleCount,
ViewCount = x.ViewCount,
RemainingCount = x.RemainingCount,
CategoryIds = x.PruductCategorys
CategoryIds = x.ProductCategories
.Select(pc => pc.CategoryId)
.ToList()
})
.FirstOrDefaultAsync(cancellationToken);
return response ?? throw new NotFoundException(nameof(Products), request.Id);
return response ?? throw new NotFoundException(nameof(Product), request.Id);
}
}
@@ -0,0 +1,16 @@
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;
}
@@ -0,0 +1,74 @@
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
};
}
}
@@ -0,0 +1,21 @@
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; }
}
@@ -0,0 +1,16 @@
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;
}
@@ -0,0 +1,75 @@
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
};
}
}
@@ -0,0 +1,10 @@
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();
}