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:
masoodafar-web
2025-12-04 02:41:19 +03:30
parent c9dab944fa
commit 4f400eabc5
92 changed files with 2285 additions and 41 deletions

View File

@@ -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; }
}

View File

@@ -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
};
}
}

View File

@@ -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 کاراکتر باشد");
}
}

View File

@@ -0,0 +1,7 @@
namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.CreateDiscountCategory;
public class CreateDiscountCategoryResponseDto
{
/// <summary>شناسه دسته‌بندی ایجاد شده</summary>
public long CategoryId { get; set; }
}

View File

@@ -0,0 +1,7 @@
namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.DeleteDiscountCategory;
public record DeleteDiscountCategoryCommand : IRequest
{
/// <summary>شناسه دسته‌بندی</summary>
public long CategoryId { get; init; }
}

View File

@@ -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;
}
}

View File

@@ -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("شناسه دسته‌بندی نامعتبر است");
}
}

View File

@@ -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; }
}

View File

@@ -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;
}
}

View File

@@ -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("دسته‌بندی نمی‌تواند والد خودش باشد");
}
}

View File

@@ -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; }
}

View File

@@ -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()
};
}
}

View File

@@ -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();
}