From 518285531aa27a42bb39bd5e071ad8ec7dd67098 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Fri, 28 Nov 2025 11:00:59 +0330 Subject: [PATCH] Add category service and product category support --- .../GetAllCategories/GetAllCategoriesQuery.cs | 5 + .../GetAllCategoriesQueryHandler.cs | 49 ++++++++++ .../GetAllCategoriesResponseDto.cs | 17 ++++ .../Interfaces/IApplicationContractContext.cs | 4 + .../GetAllProductsByFilterQuery.cs | 2 + .../GetAllProductsByFilterQueryHandler.cs | 54 ++++++++++- .../GetProducts/GetProductsQueryHandler.cs | 94 ++++++++++++++++++- .../GetProducts/GetProductsResponseDto.cs | 17 ++++ .../Services/ApplicationContractContext.cs | 4 + .../FrontOffice.BFF.WebApi.csproj | 1 + .../Services/CategoriesService.cs | 20 ++++ src/FrontOffice.BFF.sln | 7 ++ .../FrontOffice.BFF.Category.Protobuf.csproj | 25 +++++ .../Protos/category.proto | 32 +++++++ .../FrontOffice.BFF.Products.Protobuf.csproj | 2 +- .../Protos/products.proto | 17 ++++ 16 files changed, 345 insertions(+), 5 deletions(-) create mode 100644 src/FrontOffice.BFF.Application/CategoryCQ/Queries/GetAllCategories/GetAllCategoriesQuery.cs create mode 100644 src/FrontOffice.BFF.Application/CategoryCQ/Queries/GetAllCategories/GetAllCategoriesQueryHandler.cs create mode 100644 src/FrontOffice.BFF.Application/CategoryCQ/Queries/GetAllCategories/GetAllCategoriesResponseDto.cs create mode 100644 src/FrontOffice.BFF.WebApi/Services/CategoriesService.cs create mode 100644 src/Protobufs/FrontOffice.BFF.Category.Protobuf/FrontOffice.BFF.Category.Protobuf.csproj create mode 100644 src/Protobufs/FrontOffice.BFF.Category.Protobuf/Protos/category.proto diff --git a/src/FrontOffice.BFF.Application/CategoryCQ/Queries/GetAllCategories/GetAllCategoriesQuery.cs b/src/FrontOffice.BFF.Application/CategoryCQ/Queries/GetAllCategories/GetAllCategoriesQuery.cs new file mode 100644 index 0000000..cc9b04d --- /dev/null +++ b/src/FrontOffice.BFF.Application/CategoryCQ/Queries/GetAllCategories/GetAllCategoriesQuery.cs @@ -0,0 +1,5 @@ +using MediatR; + +namespace FrontOffice.BFF.Application.CategoryCQ.Queries.GetAllCategories; + +public sealed record GetAllCategoriesQuery : IRequest; diff --git a/src/FrontOffice.BFF.Application/CategoryCQ/Queries/GetAllCategories/GetAllCategoriesQueryHandler.cs b/src/FrontOffice.BFF.Application/CategoryCQ/Queries/GetAllCategories/GetAllCategoriesQueryHandler.cs new file mode 100644 index 0000000..1ae1d17 --- /dev/null +++ b/src/FrontOffice.BFF.Application/CategoryCQ/Queries/GetAllCategories/GetAllCategoriesQueryHandler.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CMSMicroservice.Protobuf.Protos.Category; +using FrontOffice.BFF.Application.Common.Interfaces; +using MediatR; + +namespace FrontOffice.BFF.Application.CategoryCQ.Queries.GetAllCategories; + +public class GetAllCategoriesQueryHandler : IRequestHandler +{ + private readonly IApplicationContractContext _context; + + public GetAllCategoriesQueryHandler(IApplicationContractContext context) + { + _context = context; + } + + public async Task Handle(GetAllCategoriesQuery request, + CancellationToken cancellationToken) + { + var response = await _context.Category.GetAllCategoryByFilterAsync(new GetAllCategoryByFilterRequest + { + Filter = new GetAllCategoryByFilterFilter(), + PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState + { + PageNumber = 1, + PageSize = 1000 + } + }, cancellationToken: cancellationToken); + + var categories = response.Models? + .Select(model => new CategoryDto + { + Id = model.Id, + Title = model.Title ?? string.Empty, + ParentId = model.ParentId, + ImagePath = model.ImagePath, + IsActive = model.IsActive + }) + .ToList() ?? new List(); + + return new GetAllCategoriesResponseDto + { + Categories = categories + }; + } +} diff --git a/src/FrontOffice.BFF.Application/CategoryCQ/Queries/GetAllCategories/GetAllCategoriesResponseDto.cs b/src/FrontOffice.BFF.Application/CategoryCQ/Queries/GetAllCategories/GetAllCategoriesResponseDto.cs new file mode 100644 index 0000000..155a6ff --- /dev/null +++ b/src/FrontOffice.BFF.Application/CategoryCQ/Queries/GetAllCategories/GetAllCategoriesResponseDto.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace FrontOffice.BFF.Application.CategoryCQ.Queries.GetAllCategories; + +public class GetAllCategoriesResponseDto +{ + public List Categories { get; set; } = new(); +} + +public class CategoryDto +{ + public long Id { get; set; } + public string Title { get; set; } = string.Empty; + public long? ParentId { get; set; } + public string? ImagePath { get; set; } + public bool IsActive { get; set; } +} diff --git a/src/FrontOffice.BFF.Application/Common/Interfaces/IApplicationContractContext.cs b/src/FrontOffice.BFF.Application/Common/Interfaces/IApplicationContractContext.cs index b9eacbd..9b60c52 100644 --- a/src/FrontOffice.BFF.Application/Common/Interfaces/IApplicationContractContext.cs +++ b/src/FrontOffice.BFF.Application/Common/Interfaces/IApplicationContractContext.cs @@ -1,5 +1,7 @@ +using CMSMicroservice.Protobuf.Protos.Category; using CMSMicroservice.Protobuf.Protos.OtpToken; using CMSMicroservice.Protobuf.Protos.Package; +using CMSMicroservice.Protobuf.Protos.PruductCategory; using CMSMicroservice.Protobuf.Protos.Products; using CMSMicroservice.Protobuf.Protos.ProductGallerys; using CMSMicroservice.Protobuf.Protos.ProductImages; @@ -27,6 +29,8 @@ public interface IApplicationContractContext ProductsContract.ProductsContractClient Product { get; } ProductGallerysContract.ProductGallerysContractClient ProductGallerys { get; } ProductImagesContract.ProductImagesContractClient ProductImages { get; } + CategoryContract.CategoryContractClient Category { get; } + PruductCategoryContract.PruductCategoryContractClient ProductCategory { get; } UserCartsContract.UserCartsContractClient UserCart { get; } UserContract.UserContractClient User { get; } UserContractContract.UserContractContractClient UserContract { get; } diff --git a/src/FrontOffice.BFF.Application/ProductsCQ/Queries/GetAllProductsByFilter/GetAllProductsByFilterQuery.cs b/src/FrontOffice.BFF.Application/ProductsCQ/Queries/GetAllProductsByFilter/GetAllProductsByFilterQuery.cs index c6cc5cb..c34c146 100644 --- a/src/FrontOffice.BFF.Application/ProductsCQ/Queries/GetAllProductsByFilter/GetAllProductsByFilterQuery.cs +++ b/src/FrontOffice.BFF.Application/ProductsCQ/Queries/GetAllProductsByFilter/GetAllProductsByFilterQuery.cs @@ -26,4 +26,6 @@ public record GetAllProductsByFilterQuery : IRequest Handle(GetAllProductsByFilterQuery request, CancellationToken cancellationToken) { - var result = await _context.Product.GetAllProductsByFilterAsync(request.Adapt(), + var grpcRequest = new CmsProductsProtos.GetAllProductsByFilterRequest + { + PaginationState = request.PaginationState is { } pagination + ? new CmsPaginationState + { + PageNumber = pagination.PageNumber, + PageSize = pagination.PageSize + } + : null, + SortBy = request.SortBy, + Filter = BuildFilter(request.Filter) + }; + + var result = await _context.Product.GetAllProductsByFilterAsync(grpcRequest, cancellationToken: cancellationToken); + + if (request.Filter?.CategoryId is { } categoryId) + { + var matchingModels = result.Models + .Where(model => model.CategoryIds.Contains(categoryId)) + .ToList(); + result.Models.Clear(); + result.Models.AddRange(matchingModels); + } return result.Adapt(); } + + 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 + }; + } } \ No newline at end of file diff --git a/src/FrontOffice.BFF.Application/ProductsCQ/Queries/GetProducts/GetProductsQueryHandler.cs b/src/FrontOffice.BFF.Application/ProductsCQ/Queries/GetProducts/GetProductsQueryHandler.cs index e176bb5..2fbe452 100644 --- a/src/FrontOffice.BFF.Application/ProductsCQ/Queries/GetProducts/GetProductsQueryHandler.cs +++ b/src/FrontOffice.BFF.Application/ProductsCQ/Queries/GetProducts/GetProductsQueryHandler.cs @@ -1,6 +1,10 @@ -using CMSMicroservice.Protobuf.Protos.Products; +using System.Collections.Generic; +using System.Linq; +using CMSCategory = CMSMicroservice.Protobuf.Protos.Category; using CMSMicroservice.Protobuf.Protos.ProductGallerys; using CMSMicroservice.Protobuf.Protos.ProductImages; +using CMSMicroservice.Protobuf.Protos.Products; +using CMSPruductCategory = CMSMicroservice.Protobuf.Protos.PruductCategory; namespace FrontOffice.BFF.Application.ProductsCQ.Queries.GetProducts; public class GetProductsQueryHandler : IRequestHandler @@ -57,6 +61,94 @@ public class GetProductsQueryHandler : IRequestHandler c.Id) ?? new Dictionary(); + + var productCategoryResponse = await _context.ProductCategory.GetAllPruductCategoryByFilterAsync( + new CMSPruductCategory.GetAllPruductCategoryByFilterRequest + { + Filter = new CMSPruductCategory.GetAllPruductCategoryByFilterFilter + { + ProductId = productId + }, + PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState + { + PageNumber = 1, + PageSize = 1000 + } + }, cancellationToken: cancellationToken); + + var productCategoryIds = productCategoryResponse?.Models? + .Select(pc => pc.CategoryId) + .Distinct() + .ToList() ?? new List(); + + foreach (var categoryId in productCategoryIds) + { + if (!categoryLookup.TryGetValue(categoryId, out var categoryModel)) + { + continue; + } + + var pathNodes = BuildCategoryPath(categoryModel, categoryLookup); + if (!pathNodes.Any()) + { + continue; + } + + dto.Categories.Add(new ProductCategoryPathDto + { + CategoryId = categoryModel.Id, + Title = categoryModel.Title ?? string.Empty, + Path = pathNodes + }); + } + } + + private static List BuildCategoryPath(CMSCategory.GetAllCategoryByFilterResponseModel category, + Dictionary lookup) + { + var path = new List(); + var visited = new HashSet(); + var current = category; + + while (current != null && visited.Add(current.Id)) + { + path.Add(new CategoryNodeDto + { + Id = current.Id, + Title = current.Title ?? string.Empty, + ParentId = current.ParentId + }); + + if (current.ParentId is long parentId && lookup.TryGetValue(parentId, out var parent)) + { + current = parent; + continue; + } + + break; + } + + path.Reverse(); + return path; + } } diff --git a/src/FrontOffice.BFF.Application/ProductsCQ/Queries/GetProducts/GetProductsResponseDto.cs b/src/FrontOffice.BFF.Application/ProductsCQ/Queries/GetProducts/GetProductsResponseDto.cs index 41bd430..1e8779c 100644 --- a/src/FrontOffice.BFF.Application/ProductsCQ/Queries/GetProducts/GetProductsResponseDto.cs +++ b/src/FrontOffice.BFF.Application/ProductsCQ/Queries/GetProducts/GetProductsResponseDto.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace FrontOffice.BFF.Application.ProductsCQ.Queries.GetProducts; public class GetProductsResponseDto { @@ -29,6 +31,7 @@ public class GetProductsResponseDto public int RemainingCount { get; set; } // public List Gallery { get; set; } = new(); + public List Categories { get; set; } = new(); } @@ -40,4 +43,18 @@ public class ProductGalleryItemDto public string ImagePath { get; set; } = string.Empty; public string ImageThumbnailPath { get; set; } = string.Empty; +} + +public class ProductCategoryPathDto +{ + public long CategoryId { get; set; } + public string Title { get; set; } = string.Empty; + public List Path { get; set; } = new(); +} + +public class CategoryNodeDto +{ + public long Id { get; set; } + public string Title { get; set; } = string.Empty; + public long? ParentId { get; set; } } \ No newline at end of file diff --git a/src/FrontOffice.BFF.Infrastructure/Services/ApplicationContractContext.cs b/src/FrontOffice.BFF.Infrastructure/Services/ApplicationContractContext.cs index c325be8..46e9865 100644 --- a/src/FrontOffice.BFF.Infrastructure/Services/ApplicationContractContext.cs +++ b/src/FrontOffice.BFF.Infrastructure/Services/ApplicationContractContext.cs @@ -1,5 +1,7 @@ +using CMSMicroservice.Protobuf.Protos.Category; using CMSMicroservice.Protobuf.Protos.OtpToken; using CMSMicroservice.Protobuf.Protos.Package; +using CMSMicroservice.Protobuf.Protos.PruductCategory; using CMSMicroservice.Protobuf.Protos.Products; using CMSMicroservice.Protobuf.Protos.ProductGallerys; using CMSMicroservice.Protobuf.Protos.ProductImages; @@ -51,6 +53,8 @@ public class ApplicationContractContext : IApplicationContractContext public ProductsContract.ProductsContractClient Product => GetService(); public ProductGallerysContract.ProductGallerysContractClient ProductGallerys => GetService(); public ProductImagesContract.ProductImagesContractClient ProductImages => GetService(); + public CategoryContract.CategoryContractClient Category => GetService(); + public PruductCategoryContract.PruductCategoryContractClient ProductCategory => GetService(); public UserCartsContract.UserCartsContractClient UserCart => GetService(); public UserContract.UserContractClient User => GetService(); diff --git a/src/FrontOffice.BFF.WebApi/FrontOffice.BFF.WebApi.csproj b/src/FrontOffice.BFF.WebApi/FrontOffice.BFF.WebApi.csproj index 8c14479..4b15dc2 100644 --- a/src/FrontOffice.BFF.WebApi/FrontOffice.BFF.WebApi.csproj +++ b/src/FrontOffice.BFF.WebApi/FrontOffice.BFF.WebApi.csproj @@ -21,6 +21,7 @@ + diff --git a/src/FrontOffice.BFF.WebApi/Services/CategoriesService.cs b/src/FrontOffice.BFF.WebApi/Services/CategoriesService.cs new file mode 100644 index 0000000..4844715 --- /dev/null +++ b/src/FrontOffice.BFF.WebApi/Services/CategoriesService.cs @@ -0,0 +1,20 @@ +using FrontOffice.BFF.Application.CategoryCQ.Queries.GetAllCategories; +using FrontOffice.BFF.Category.Protobuf.Protos.Category; +using FrontOffice.BFF.WebApi.Common.Services; + +namespace FrontOffice.BFF.WebApi.Services; + +public class CategoriesService : CategoryContract.CategoryContractBase +{ + private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS; + + public CategoriesService(IDispatchRequestToCQRS dispatchRequestToCQRS) + { + _dispatchRequestToCQRS = dispatchRequestToCQRS; + } + + public override async Task GetAllCategories(GetCategoriesRequest request, ServerCallContext context) + { + return await _dispatchRequestToCQRS.Handle(request, context); + } +} diff --git a/src/FrontOffice.BFF.sln b/src/FrontOffice.BFF.sln index ffc4c1b..493e8ed 100644 --- a/src/FrontOffice.BFF.sln +++ b/src/FrontOffice.BFF.sln @@ -24,6 +24,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FrontOffice.BFF.Transaction EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontOffice.BFF.Products.Protobuf", "Protobufs\FrontOffice.BFF.Products.Protobuf\FrontOffice.BFF.Products.Protobuf.csproj", "{CB77669F-5B48-4AC6-B20E-A928660E93F8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontOffice.BFF.Category.Protobuf", "Protobufs\FrontOffice.BFF.Category.Protobuf\FrontOffice.BFF.Category.Protobuf.csproj", "{E3F6D1B7-DB78-4F36-BE77-2F9D2D7B5B7C}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontOffice.BFF.ShopingCart.Protobuf", "Protobufs\FrontOffice.BFF.ShopingCart.Protobuf\FrontOffice.BFF.ShopingCart.Protobuf.csproj", "{DC61324B-D389-4A1D-B048-D0AA43A6BBE7}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontOffice.BFF.UserWallet.Protobuf", "Protobufs\FrontOffice.BFF.UserWallet.Protobuf\FrontOffice.BFF.UserWallet.Protobuf.csproj", "{03F99CE9-F952-47B0-B71A-1F4865E52443}" @@ -82,6 +84,10 @@ Global {03F99CE9-F952-47B0-B71A-1F4865E52443}.Debug|Any CPU.Build.0 = Debug|Any CPU {03F99CE9-F952-47B0-B71A-1F4865E52443}.Release|Any CPU.ActiveCfg = Release|Any CPU {03F99CE9-F952-47B0-B71A-1F4865E52443}.Release|Any CPU.Build.0 = Release|Any CPU + {E3F6D1B7-DB78-4F36-BE77-2F9D2D7B5B7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E3F6D1B7-DB78-4F36-BE77-2F9D2D7B5B7C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E3F6D1B7-DB78-4F36-BE77-2F9D2D7B5B7C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E3F6D1B7-DB78-4F36-BE77-2F9D2D7B5B7C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -95,5 +101,6 @@ Global {CB77669F-5B48-4AC6-B20E-A928660E93F8} = {CA9BF4D6-6729-4011-888E-48F5F739B469} {DC61324B-D389-4A1D-B048-D0AA43A6BBE7} = {CA9BF4D6-6729-4011-888E-48F5F739B469} {03F99CE9-F952-47B0-B71A-1F4865E52443} = {CA9BF4D6-6729-4011-888E-48F5F739B469} + {E3F6D1B7-DB78-4F36-BE77-2F9D2D7B5B7C} = {CA9BF4D6-6729-4011-888E-48F5F739B469} EndGlobalSection EndGlobal diff --git a/src/Protobufs/FrontOffice.BFF.Category.Protobuf/FrontOffice.BFF.Category.Protobuf.csproj b/src/Protobufs/FrontOffice.BFF.Category.Protobuf/FrontOffice.BFF.Category.Protobuf.csproj new file mode 100644 index 0000000..23fffd9 --- /dev/null +++ b/src/Protobufs/FrontOffice.BFF.Category.Protobuf/FrontOffice.BFF.Category.Protobuf.csproj @@ -0,0 +1,25 @@ + + + net7.0 + enable + enable + 0.0.1 + Foursat.FrontOffice.BFF.Category.Protobuf + False + False + None + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + diff --git a/src/Protobufs/FrontOffice.BFF.Category.Protobuf/Protos/category.proto b/src/Protobufs/FrontOffice.BFF.Category.Protobuf/Protos/category.proto new file mode 100644 index 0000000..cfa196e --- /dev/null +++ b/src/Protobufs/FrontOffice.BFF.Category.Protobuf/Protos/category.proto @@ -0,0 +1,32 @@ +syntax = "proto3"; + +package category; + +import "google/protobuf/empty.proto"; +import "google/protobuf/wrappers.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; +option csharp_namespace = "FrontOffice.BFF.Category.Protobuf.Protos.Category"; + +service CategoryContract { + rpc GetAllCategories(GetCategoriesRequest) returns (GetCategoriesResponse); +} + +message GetCategoriesRequest { + google.protobuf.Int64Value parent_id = 1; +} + +message GetCategoriesResponse { + repeated CategoryDto categories = 1; +} + +message CategoryDto { + int64 id = 1; + string name = 2; + string title = 3; + google.protobuf.StringValue description = 4; + google.protobuf.StringValue image_path = 5; + google.protobuf.Int64Value parent_id = 6; + bool is_active = 7; + int32 sort_order = 8; +} diff --git a/src/Protobufs/FrontOffice.BFF.Products.Protobuf/FrontOffice.BFF.Products.Protobuf.csproj b/src/Protobufs/FrontOffice.BFF.Products.Protobuf/FrontOffice.BFF.Products.Protobuf.csproj index de6871a..1d3a71a 100644 --- a/src/Protobufs/FrontOffice.BFF.Products.Protobuf/FrontOffice.BFF.Products.Protobuf.csproj +++ b/src/Protobufs/FrontOffice.BFF.Products.Protobuf/FrontOffice.BFF.Products.Protobuf.csproj @@ -3,7 +3,7 @@ net7.0 enable enable - 0.0.12 + 0.0.13 Foursat.FrontOffice.BFF.Products.Protobuf False False diff --git a/src/Protobufs/FrontOffice.BFF.Products.Protobuf/Protos/products.proto b/src/Protobufs/FrontOffice.BFF.Products.Protobuf/Protos/products.proto index 6863411..c63edad 100644 --- a/src/Protobufs/FrontOffice.BFF.Products.Protobuf/Protos/products.proto +++ b/src/Protobufs/FrontOffice.BFF.Products.Protobuf/Protos/products.proto @@ -45,6 +45,7 @@ message GetProductsResponse int32 view_count = 12; int32 remaining_count = 13; repeated ProductGalleryItem gallery = 14; + repeated ProductCategoryPath categories = 15; } message GetAllProductsByFilterRequest { @@ -62,6 +63,7 @@ message GetAllProductsByFilterFilter google.protobuf.Int64Value price = 6; google.protobuf.Int32Value discount = 7; google.protobuf.Int32Value rate = 8; + google.protobuf.Int64Value category_id = 9; } message GetAllProductsByFilterResponse { @@ -83,6 +85,7 @@ message GetAllProductsByFilterResponseModel int32 sale_count = 11; int32 view_count = 12; int32 remaining_count = 13; + repeated ProductCategoryPath categories = 14; } message ProductGalleryItem @@ -94,6 +97,20 @@ message ProductGalleryItem string image_thumbnail_path = 5; } +message ProductCategoryPath +{ + int64 category_id = 1; + string title = 2; + repeated CategoryNode path = 3; +} + +message CategoryNode +{ + int64 id = 1; + string title = 2; + google.protobuf.Int64Value parent_id = 3; +} + message PaginationState { int32 page_number = 1;