feat: Implement Discount Product and Shopping Cart functionalities
- Added UpdateDiscountProductCommandValidator for validating discount product updates. - Created GetDiscountProductByIdQuery and its handler for retrieving discount product details by ID. - Implemented GetDiscountProductsQuery and handler for fetching a list of discount products with filtering options. - Developed AddToCartCommand and handler for adding products to the shopping cart. - Implemented ClearCartCommand and handler for clearing the shopping cart. - Created RemoveFromCartCommand and handler for removing items from the cart. - Added UpdateCartItemCountCommand and handler for updating the quantity of items in the cart. - Developed GetUserCartQuery and handler for retrieving the user's shopping cart details. - Implemented Product Tag functionalities including assigning tags to products, creating, updating, and deleting tags. - Added queries for fetching all tags and products by tag.
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.CreateDiscountCategory;
|
||||
|
||||
public record CreateDiscountCategoryCommand : IRequest<CreateDiscountCategoryResponseDto>
|
||||
{
|
||||
/// <summary>نام دستهبندی (انگلیسی)</summary>
|
||||
public string Name { get; init; }
|
||||
|
||||
/// <summary>عنوان دستهبندی (فارسی)</summary>
|
||||
public string Title { get; init; }
|
||||
|
||||
/// <summary>توضیحات دستهبندی</summary>
|
||||
public string Description { get; init; }
|
||||
|
||||
/// <summary>شناسه دستهبندی والد (null برای دسته اصلی)</summary>
|
||||
public long? ParentCategoryId { get; init; }
|
||||
|
||||
/// <summary>ترتیب نمایش</summary>
|
||||
public int SortOrder { get; init; }
|
||||
|
||||
/// <summary>وضعیت فعال/غیرفعال</summary>
|
||||
public bool IsActive { get; init; }
|
||||
|
||||
/// <summary>فایل تصویر دستهبندی</summary>
|
||||
public DiscountCategoryFileModel ImageFile { get; init; }
|
||||
}
|
||||
|
||||
public class DiscountCategoryFileModel
|
||||
{
|
||||
/// <summary>فایل</summary>
|
||||
public byte[] File { get; set; }
|
||||
|
||||
/// <summary>نام فایل</summary>
|
||||
public string FileName { get; set; }
|
||||
|
||||
/// <summary>نوع فایل</summary>
|
||||
public string Mime { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using CMSMicroservice.Protobuf.Protos.DiscountCategory;
|
||||
using Google.Protobuf;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
|
||||
namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.CreateDiscountCategory;
|
||||
|
||||
public class CreateDiscountCategoryCommandHandler : IRequestHandler<CreateDiscountCategoryCommand, CreateDiscountCategoryResponseDto>
|
||||
{
|
||||
private readonly IApplicationContractContext _context;
|
||||
|
||||
public CreateDiscountCategoryCommandHandler(IApplicationContractContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<CreateDiscountCategoryResponseDto> Handle(CreateDiscountCategoryCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var createRequest = new CreateDiscountCategoryRequest
|
||||
{
|
||||
Name = request.Name,
|
||||
Title = request.Title,
|
||||
Description = request.Description ?? string.Empty,
|
||||
SortOrder = request.SortOrder,
|
||||
IsActive = request.IsActive
|
||||
};
|
||||
|
||||
if (request.ParentCategoryId.HasValue)
|
||||
createRequest.ParentCategoryId = new Int64Value { Value = request.ParentCategoryId.Value };
|
||||
|
||||
// Upload Image
|
||||
if (request.ImageFile != null && request.ImageFile.File != null && request.ImageFile.File.Length > 0)
|
||||
{
|
||||
var imageFileInfo = await _context.FileInfos.CreateNewFileInfoAsync(new()
|
||||
{
|
||||
Directory = "Images/DiscountShop/Categories",
|
||||
IsBase64 = false,
|
||||
MIME = request.ImageFile.Mime,
|
||||
FileName = request.ImageFile.FileName,
|
||||
File = ByteString.CopyFrom(request.ImageFile.File)
|
||||
}, cancellationToken: cancellationToken);
|
||||
|
||||
if (imageFileInfo != null && !string.IsNullOrWhiteSpace(imageFileInfo.File))
|
||||
createRequest.ImagePath = imageFileInfo.File;
|
||||
}
|
||||
|
||||
var response = await _context.DiscountCategories.CreateDiscountCategoryAsync(createRequest, cancellationToken: cancellationToken);
|
||||
|
||||
return new CreateDiscountCategoryResponseDto
|
||||
{
|
||||
CategoryId = response.CategoryId
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.CreateDiscountCategory;
|
||||
|
||||
public class CreateDiscountCategoryCommandValidator : AbstractValidator<CreateDiscountCategoryCommand>
|
||||
{
|
||||
public CreateDiscountCategoryCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Name)
|
||||
.NotEmpty().WithMessage("نام دستهبندی الزامی است")
|
||||
.MaximumLength(100).WithMessage("نام دستهبندی نباید بیشتر از 100 کاراکتر باشد")
|
||||
.Matches("^[a-zA-Z0-9_-]+$").WithMessage("نام دستهبندی فقط باید شامل حروف انگلیسی، اعداد، خط تیره و زیرخط باشد");
|
||||
|
||||
RuleFor(x => x.Title)
|
||||
.NotEmpty().WithMessage("عنوان دستهبندی الزامی است")
|
||||
.MaximumLength(200).WithMessage("عنوان دستهبندی نباید بیشتر از 200 کاراکتر باشد");
|
||||
|
||||
RuleFor(x => x.Description)
|
||||
.MaximumLength(1000).WithMessage("توضیحات نباید بیشتر از 1000 کاراکتر باشد");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.CreateDiscountCategory;
|
||||
|
||||
public class CreateDiscountCategoryResponseDto
|
||||
{
|
||||
/// <summary>شناسه دستهبندی ایجاد شده</summary>
|
||||
public long CategoryId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.DeleteDiscountCategory;
|
||||
|
||||
public record DeleteDiscountCategoryCommand : IRequest
|
||||
{
|
||||
/// <summary>شناسه دستهبندی</summary>
|
||||
public long CategoryId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using CMSMicroservice.Protobuf.Protos.DiscountCategory;
|
||||
|
||||
namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.DeleteDiscountCategory;
|
||||
|
||||
public class DeleteDiscountCategoryCommandHandler : IRequestHandler<DeleteDiscountCategoryCommand>
|
||||
{
|
||||
private readonly IApplicationContractContext _context;
|
||||
|
||||
public DeleteDiscountCategoryCommandHandler(IApplicationContractContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(DeleteDiscountCategoryCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
await _context.DiscountCategories.DeleteDiscountCategoryAsync(
|
||||
new DeleteDiscountCategoryRequest { CategoryId = request.CategoryId },
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.DeleteDiscountCategory;
|
||||
|
||||
public class DeleteDiscountCategoryCommandValidator : AbstractValidator<DeleteDiscountCategoryCommand>
|
||||
{
|
||||
public DeleteDiscountCategoryCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.CategoryId)
|
||||
.GreaterThan(0).WithMessage("شناسه دستهبندی نامعتبر است");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.UpdateDiscountCategory;
|
||||
|
||||
public record UpdateDiscountCategoryCommand : IRequest
|
||||
{
|
||||
/// <summary>شناسه دستهبندی</summary>
|
||||
public long CategoryId { get; init; }
|
||||
|
||||
/// <summary>نام دستهبندی (انگلیسی)</summary>
|
||||
public string Name { get; init; }
|
||||
|
||||
/// <summary>عنوان دستهبندی (فارسی)</summary>
|
||||
public string Title { get; init; }
|
||||
|
||||
/// <summary>توضیحات دستهبندی</summary>
|
||||
public string Description { get; init; }
|
||||
|
||||
/// <summary>شناسه دستهبندی والد (null برای دسته اصلی)</summary>
|
||||
public long? ParentCategoryId { get; init; }
|
||||
|
||||
/// <summary>ترتیب نمایش</summary>
|
||||
public int SortOrder { get; init; }
|
||||
|
||||
/// <summary>وضعیت فعال/غیرفعال</summary>
|
||||
public bool IsActive { get; init; }
|
||||
|
||||
/// <summary>فایل تصویر دستهبندی (اختیاری - برای تغییر تصویر)</summary>
|
||||
public DiscountCategoryFileModel ImageFile { get; init; }
|
||||
}
|
||||
|
||||
public class DiscountCategoryFileModel
|
||||
{
|
||||
/// <summary>فایل</summary>
|
||||
public byte[] File { get; set; }
|
||||
|
||||
/// <summary>نام فایل</summary>
|
||||
public string FileName { get; set; }
|
||||
|
||||
/// <summary>نوع فایل</summary>
|
||||
public string Mime { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using CMSMicroservice.Protobuf.Protos.DiscountCategory;
|
||||
using Google.Protobuf;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
|
||||
namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.UpdateDiscountCategory;
|
||||
|
||||
public class UpdateDiscountCategoryCommandHandler : IRequestHandler<UpdateDiscountCategoryCommand>
|
||||
{
|
||||
private readonly IApplicationContractContext _context;
|
||||
|
||||
public UpdateDiscountCategoryCommandHandler(IApplicationContractContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(UpdateDiscountCategoryCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var updateRequest = new UpdateDiscountCategoryRequest
|
||||
{
|
||||
CategoryId = request.CategoryId,
|
||||
Name = request.Name,
|
||||
Title = request.Title,
|
||||
Description = request.Description ?? string.Empty,
|
||||
SortOrder = request.SortOrder,
|
||||
IsActive = request.IsActive
|
||||
};
|
||||
|
||||
if (request.ParentCategoryId.HasValue)
|
||||
updateRequest.ParentCategoryId = new Int64Value { Value = request.ParentCategoryId.Value };
|
||||
|
||||
// Upload new Image if provided
|
||||
if (request.ImageFile != null && request.ImageFile.File != null && request.ImageFile.File.Length > 0)
|
||||
{
|
||||
var imageFileInfo = await _context.FileInfos.CreateNewFileInfoAsync(new()
|
||||
{
|
||||
Directory = "Images/DiscountShop/Categories",
|
||||
IsBase64 = false,
|
||||
MIME = request.ImageFile.Mime,
|
||||
FileName = request.ImageFile.FileName,
|
||||
File = ByteString.CopyFrom(request.ImageFile.File)
|
||||
}, cancellationToken: cancellationToken);
|
||||
|
||||
if (imageFileInfo != null && !string.IsNullOrWhiteSpace(imageFileInfo.File))
|
||||
updateRequest.ImagePath = imageFileInfo.File;
|
||||
}
|
||||
|
||||
await _context.DiscountCategories.UpdateDiscountCategoryAsync(updateRequest, cancellationToken: cancellationToken);
|
||||
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.UpdateDiscountCategory;
|
||||
|
||||
public class UpdateDiscountCategoryCommandValidator : AbstractValidator<UpdateDiscountCategoryCommand>
|
||||
{
|
||||
public UpdateDiscountCategoryCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.CategoryId)
|
||||
.GreaterThan(0).WithMessage("شناسه دستهبندی نامعتبر است");
|
||||
|
||||
RuleFor(x => x.Name)
|
||||
.NotEmpty().WithMessage("نام دستهبندی الزامی است")
|
||||
.MaximumLength(100).WithMessage("نام دستهبندی نباید بیشتر از 100 کاراکتر باشد")
|
||||
.Matches("^[a-zA-Z0-9_-]+$").WithMessage("نام دستهبندی فقط باید شامل حروف انگلیسی، اعداد، خط تیره و زیرخط باشد");
|
||||
|
||||
RuleFor(x => x.Title)
|
||||
.NotEmpty().WithMessage("عنوان دستهبندی الزامی است")
|
||||
.MaximumLength(200).WithMessage("عنوان دستهبندی نباید بیشتر از 200 کاراکتر باشد");
|
||||
|
||||
RuleFor(x => x.Description)
|
||||
.MaximumLength(1000).WithMessage("توضیحات نباید بیشتر از 1000 کاراکتر باشد");
|
||||
|
||||
RuleFor(x => x.ParentCategoryId)
|
||||
.NotEqual(x => x.CategoryId).When(x => x.ParentCategoryId.HasValue)
|
||||
.WithMessage("دستهبندی نمیتواند والد خودش باشد");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace BackOffice.BFF.Application.DiscountCategoryCQ.Queries.GetDiscountCategories;
|
||||
|
||||
public record GetDiscountCategoriesQuery : IRequest<GetDiscountCategoriesResponseDto>
|
||||
{
|
||||
/// <summary>فیلتر براساس دسته والد (null برای ریشه)</summary>
|
||||
public long? ParentCategoryId { get; init; }
|
||||
|
||||
/// <summary>فقط دستههای فعال</summary>
|
||||
public bool? IsActive { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using CMSMicroservice.Protobuf.Protos.DiscountCategory;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
|
||||
namespace BackOffice.BFF.Application.DiscountCategoryCQ.Queries.GetDiscountCategories;
|
||||
|
||||
public class GetDiscountCategoriesQueryHandler : IRequestHandler<GetDiscountCategoriesQuery, GetDiscountCategoriesResponseDto>
|
||||
{
|
||||
private readonly IApplicationContractContext _context;
|
||||
|
||||
public GetDiscountCategoriesQueryHandler(IApplicationContractContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<GetDiscountCategoriesResponseDto> Handle(GetDiscountCategoriesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var grpcRequest = new GetDiscountCategoriesRequest();
|
||||
|
||||
if (request.ParentCategoryId.HasValue)
|
||||
grpcRequest.ParentCategoryId = new Int64Value { Value = request.ParentCategoryId.Value };
|
||||
|
||||
if (request.IsActive.HasValue)
|
||||
grpcRequest.IsActive = new BoolValue { Value = request.IsActive.Value };
|
||||
|
||||
var response = await _context.DiscountCategories.GetDiscountCategoriesAsync(grpcRequest, cancellationToken: cancellationToken);
|
||||
|
||||
return new GetDiscountCategoriesResponseDto
|
||||
{
|
||||
Categories = response.Categories.Select(MapCategory).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private DiscountCategoryTreeDto MapCategory(DiscountCategoryDto node)
|
||||
{
|
||||
return new DiscountCategoryTreeDto
|
||||
{
|
||||
Id = node.Id,
|
||||
Name = node.Name,
|
||||
Title = node.Title,
|
||||
Description = node.HasDescription ? node.Description.Value : string.Empty,
|
||||
ImagePath = node.HasImagePath ? node.ImagePath.Value : string.Empty,
|
||||
ParentCategoryId = node.HasParentCategoryId ? node.ParentCategoryId.Value : null,
|
||||
SortOrder = node.SortOrder,
|
||||
IsActive = node.IsActive,
|
||||
ProductCount = node.ProductCount,
|
||||
Created = DateTime.UtcNow, // Proto doesn't have created field
|
||||
ChildCategories = node.Children.Select(MapCategory).ToList()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace BackOffice.BFF.Application.DiscountCategoryCQ.Queries.GetDiscountCategories;
|
||||
|
||||
public class GetDiscountCategoriesResponseDto
|
||||
{
|
||||
/// <summary>لیست درختی دستهبندیها</summary>
|
||||
public List<DiscountCategoryTreeDto> Categories { get; set; } = new();
|
||||
}
|
||||
|
||||
public class DiscountCategoryTreeDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string ImagePath { get; set; }
|
||||
public long? ParentCategoryId { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public int ProductCount { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
|
||||
/// <summary>زیردستههای این دستهبندی</summary>
|
||||
public List<DiscountCategoryTreeDto> ChildCategories { get; set; } = new();
|
||||
}
|
||||
Reference in New Issue
Block a user