using CMSMicroservice.Protobuf.Protos.Products; using Grpc.Core; using MediatR; using CMSMicroservice.Application.ProductsCQ.Commands.CreateNewProducts; using CMSMicroservice.Application.ProductsCQ.Commands.UpdateProducts; using CMSMicroservice.Application.ProductsCQ.Commands.DeleteProducts; using CMSMicroservice.Application.ProductsCQ.Commands.AddProductImage; using CMSMicroservice.Application.ProductsCQ.Commands.RemoveProductImage; using CMSMicroservice.Application.ProductsCQ.Queries.GetCustomerProducts; using CMSMicroservice.Application.ProductsCQ.Queries.GetCustomerProductsByFilter; using CMSMicroservice.Application.ProductsCQ.Queries.GetProductGallery; using CMSMicroservice.Application.Common.Interfaces; using Mapster; using Microsoft.EntityFrameworkCore; using AppModels = CMSMicroservice.Application.Common.Models; using System.Collections.Generic; using System.Linq; namespace CMSMicroservice.WebApi.Services; public class ProductsService : ProductsContract.ProductsContractBase { private readonly ISender _sender; private readonly IApplicationDbContext _context; public ProductsService(ISender sender, IApplicationDbContext context) { _sender = sender; _context = context; } public override async Task CreateNewProducts(CreateNewProductsRequest request, ServerCallContext context) { var command = new CreateNewProductsCommand { Title = request.Title, Description = request.Description, ShortInfomation = request.ShortInfomation, FullInformation = request.FullInformation, Price = request.Price, Discount = request.Discount, Rate = request.Rate, ImagePath = request.ImagePath, ThumbnailPath = request.ThumbnailPath, SaleCount = request.SaleCount, ViewCount = request.ViewCount, RemainingCount = request.RemainingCount, CategoryIds = request.CategoryIds?.ToList() ?? new List(), ImageFileBytes = request.ImageFile?.File?.ToByteArray(), ImageFileMime = request.ImageFile?.Mime, ImageFileName = request.ImageFile?.FileName, ThumbnailFileBytes = request.ThumbnailFile?.File?.ToByteArray(), ThumbnailFileMime = request.ThumbnailFile?.Mime, ThumbnailFileName = request.ThumbnailFile?.FileName }; var result = await _sender.Send(command, context.CancellationToken); return new CreateNewProductsResponse { Id = result.Id }; } public override async Task UpdateProducts(UpdateProductsRequest request, ServerCallContext context) { var command = new UpdateProductsCommand { Id = request.Id, Title = request.Title, Description = request.Description, ShortInfomation = request.ShortInfomation, FullInformation = request.FullInformation, Price = request.Price, Discount = request.Discount, Rate = request.Rate, ImagePath = request.ImagePath, ThumbnailPath = request.ThumbnailPath, SaleCount = request.SaleCount, ViewCount = request.ViewCount, RemainingCount = request.RemainingCount, CategoryIds = request.CategoryIds?.ToList() ?? new List(), ImageFileBytes = request.ImageFile?.File?.ToByteArray(), ImageFileMime = request.ImageFile?.Mime, ImageFileName = request.ImageFile?.FileName, ThumbnailFileBytes = request.ThumbnailFile?.File?.ToByteArray(), ThumbnailFileMime = request.ThumbnailFile?.Mime, ThumbnailFileName = request.ThumbnailFile?.FileName }; await _sender.Send(command, context.CancellationToken); return new Google.Protobuf.WellKnownTypes.Empty(); } public override async Task DeleteProducts(DeleteProductsRequest request, ServerCallContext context) { var command = new DeleteProductsCommand { Id = request.Id }; await _sender.Send(command, context.CancellationToken); return new Google.Protobuf.WellKnownTypes.Empty(); } public override async Task GetProducts(GetProductsRequest request, ServerCallContext context) { var query = new GetCustomerProductsQuery { Id = request.Id }; var result = await _sender.Send(query, context.CancellationToken); var response = new GetProductsResponse { Id = result.Id, Title = result.Title, Description = result.Description, ShortInfomation = result.ShortInfomation, FullInformation = result.FullInformation, Price = result.Price, Discount = result.Discount, Rate = result.Rate, ImagePath = result.ImagePath, ThumbnailPath = result.ThumbnailPath, SaleCount = result.SaleCount, ViewCount = result.ViewCount, RemainingCount = result.RemainingCount }; if (result.Gallery != null) { foreach (var item in result.Gallery) { response.Gallery.Add(new ProductGalleryItem { ProductGalleryId = item.ProductGalleryId, ProductImageId = item.ProductImageId, Title = item.Title, ImagePath = item.ImagePath, ImageThumbnailPath = item.ImageThumbnailPath }); } } if (result.Categories != null) { foreach (var cat in result.Categories) { var categoryPath = new ProductCategoryPath { CategoryId = cat.CategoryId, Title = cat.Title }; if (cat.Path != null) { foreach (var node in cat.Path) { categoryPath.Path.Add(new CategoryNode { Id = node.Id, Title = node.Title, ParentId = node.ParentId }); } } response.Categories.Add(categoryPath); } } return response; } public override async Task GetAllProductsByFilter(GetAllProductsByFilterRequest request, ServerCallContext context) { // Map to GetCustomerProductsByFilter query (public products API) var query = new GetCustomerProductsByFilterQuery { Id = request.Filter?.Id, Title = request.Filter?.Title ?? string.Empty, Description = request.Filter?.Description ?? string.Empty, ShortInfomation = request.Filter?.ShortInfomation ?? string.Empty, FullInformation = request.Filter?.FullInformation ?? string.Empty, Price = request.Filter?.Price, Discount = request.Filter?.Discount, Rate = request.Filter?.Rate, SaleCount = request.Filter?.SaleCount, ViewCount = request.Filter?.ViewCount, RemainingCount = request.Filter?.RemainingCount, CategoryIds = request.Filter?.CategoryId.HasValue == true ? new List { request.Filter.CategoryId.Value } : new List(), IsActive = request.Filter?.IsActive, SortBy = request.SortBy ?? string.Empty, PaginationState = request.PaginationState != null ? new AppModels.PaginationState { PageNumber = request.PaginationState.PageNumber, PageSize = request.PaginationState.PageSize } : new AppModels.PaginationState { PageNumber = 1, PageSize = 20 } }; var result = await _sender.Send(query, context.CancellationToken); return new GetAllProductsByFilterResponse { MetaData = new CMSMicroservice.Protobuf.Protos.MetaData { CurrentPage = result.MetaData.CurrentPage, TotalPage = result.MetaData.TotalPage, PageSize = result.MetaData.PageSize, TotalCount = result.MetaData.TotalCount, HasPrevious = result.MetaData.HasPrevious, HasNext = result.MetaData.HasNext }, Models = { result.Models.Select(m => new GetAllProductsByFilterResponseModel { Id = m.Id, Title = m.Title, Description = m.Description, ShortInfomation = m.ShortInfomation, FullInformation = m.FullInformation, Price = m.Price, Discount = m.Discount, Rate = m.Rate, ImagePath = m.ImagePath, ThumbnailPath = m.ThumbnailPath, SaleCount = m.SaleCount, ViewCount = m.ViewCount, RemainingCount = m.RemainingCount, CategoryIds = { m.Categories?.Select(c => c.CategoryId) ?? Enumerable.Empty() }, IsActive = m.IsActive }) } }; } public override async Task BulkUpdateProductPrices(BulkUpdateProductPricesRequest request, ServerCallContext context) { var response = new BulkUpdateProductPricesResponse { Total = request.Products.Count }; foreach (var item in request.Products) { try { var product = await _context.Products.FindAsync(new object[] { item.ProductId }, context.CancellationToken); if (product == null) { response.Failed++; response.Errors.Add(new BulkOperationError { ProductId = item.ProductId, ErrorMessage = "محصول یافت نشد" }); continue; } product.Price = item.NewPrice; if (item.NewDiscount != null) product.Discount = item.NewDiscount.Value; if (item.NewClubDiscountPercent != null) product.ClubDiscountPercent = item.NewClubDiscountPercent.Value; response.Succeeded++; } catch (Exception ex) { response.Failed++; response.Errors.Add(new BulkOperationError { ProductId = item.ProductId, ErrorMessage = ex.Message }); } } await _context.SaveChangesAsync(context.CancellationToken); return response; } public override async Task BulkUpdateProductStock(BulkUpdateProductStockRequest request, ServerCallContext context) { var response = new BulkUpdateProductStockResponse { Total = request.Products.Count }; foreach (var item in request.Products) { try { var product = await _context.Products.FindAsync(new object[] { item.ProductId }, context.CancellationToken); if (product == null) { response.Failed++; response.Errors.Add(new BulkOperationError { ProductId = item.ProductId, ErrorMessage = "محصول یافت نشد" }); continue; } product.RemainingCount = request.UpdateType switch { StockUpdateType.Set => item.Quantity, StockUpdateType.Add => product.RemainingCount + item.Quantity, StockUpdateType.Subtract => Math.Max(0, product.RemainingCount - item.Quantity), _ => item.Quantity }; response.Succeeded++; } catch (Exception ex) { response.Failed++; response.Errors.Add(new BulkOperationError { ProductId = item.ProductId, ErrorMessage = ex.Message }); } } await _context.SaveChangesAsync(context.CancellationToken); return response; } public override async Task GetLowStockProducts(GetLowStockProductsRequest request, ServerCallContext context) { var threshold = request.Threshold > 0 ? request.Threshold : 10; var pageIndex = request.PageIndex > 0 ? request.PageIndex : 1; var pageSize = request.PageSize > 0 ? request.PageSize : 20; var query = _context.Products .Where(p => !p.IsDeleted && p.RemainingCount <= threshold); if (request.IsClubExclusive != null) query = query.Where(p => p.IsClubExclusive == request.IsClubExclusive.Value); var totalCount = await query.CountAsync(context.CancellationToken); var products = await query .OrderBy(p => p.RemainingCount) .Skip((pageIndex - 1) * pageSize) .Take(pageSize) .Select(p => new LowStockProduct { Id = p.Id, Title = p.Title, RemainingCount = p.RemainingCount, Price = p.Price, IsClubExclusive = p.IsClubExclusive }) .ToListAsync(context.CancellationToken); return new GetLowStockProductsResponse { MetaData = new CMSMicroservice.Protobuf.Protos.MetaData { CurrentPage = pageIndex, PageSize = pageSize, TotalCount = totalCount, TotalPage = (int)Math.Ceiling(totalCount / (double)pageSize), HasPrevious = pageIndex > 1, HasNext = pageIndex * pageSize < totalCount }, Products = { products } }; } public override async Task ToggleProductStatus(ToggleProductStatusRequest request, ServerCallContext context) { var response = new ToggleProductStatusResponse { Total = request.ProductIds.Count }; foreach (var productId in request.ProductIds) { try { var product = await _context.Products.FindAsync(new object[] { productId }, context.CancellationToken); if (product == null) { response.Failed++; response.Errors.Add(new BulkOperationError { ProductId = productId, ErrorMessage = "محصول یافت نشد" }); continue; } product.IsDeleted = !request.Enable; if (request.Enable && request.DefaultStock > 0 && product.RemainingCount == 0) product.RemainingCount = request.DefaultStock; response.Succeeded++; } catch (Exception ex) { response.Failed++; response.Errors.Add(new BulkOperationError { ProductId = productId, ErrorMessage = ex.Message }); } } await _context.SaveChangesAsync(context.CancellationToken); return response; } // ============= Category-Product DragDrop Methods ============= public override async Task GetProductsForCategory(GetProductsForCategoryRequest request, ServerCallContext context) { var assignedProductIds = await _context.ProductCategories .Where(pc => pc.CategoryId == request.CategoryId && !pc.IsDeleted) .Select(pc => pc.ProductId) .ToListAsync(context.CancellationToken); var allProducts = await _context.Products .Where(p => !p.IsDeleted) .OrderBy(p => p.Title) .Select(p => new CategoryProductItem { Id = p.Id, Title = p.Title, Selected = assignedProductIds.Contains(p.Id) }) .ToListAsync(context.CancellationToken); return new GetProductsForCategoryResponse { Items = { allProducts } }; } public override async Task GetCategories(GetCategoriesRequest request, ServerCallContext context) { var assignedCategoryIds = await _context.ProductCategories .Where(pc => pc.ProductId == request.ProductId && !pc.IsDeleted) .Select(pc => pc.CategoryId) .ToListAsync(context.CancellationToken); var allCategories = await _context.Categories .Where(c => !c.IsDeleted && c.IsActive) .OrderBy(c => c.SortOrder) .Select(c => new CategoryItem { Id = c.Id, Title = c.Title, Selected = assignedCategoryIds.Contains(c.Id) }) .ToListAsync(context.CancellationToken); return new GetCategoriesResponse { Items = { allCategories } }; } public override async Task UpdateProductCategories(UpdateProductCategoriesRequest request, ServerCallContext context) { var existingLinks = await _context.ProductCategories .Where(pc => pc.ProductId == request.ProductId) .ToListAsync(context.CancellationToken); // حذف لینک‌های قبلی foreach (var link in existingLinks) link.IsDeleted = true; // ایجاد لینک‌های جدید foreach (var categoryId in request.CategoryIds) { var existing = existingLinks.FirstOrDefault(l => l.CategoryId == categoryId); if (existing != null) { existing.IsDeleted = false; } else { _context.ProductCategories.Add(new Domain.Entities.ProductCategory { ProductId = request.ProductId, CategoryId = categoryId }); } } await _context.SaveChangesAsync(context.CancellationToken); return new Google.Protobuf.WellKnownTypes.Empty(); } public override async Task UpdateCategoryProducts(UpdateCategoryProductsRequest request, ServerCallContext context) { var existingLinks = await _context.ProductCategories .Where(pc => pc.CategoryId == request.CategoryId) .ToListAsync(context.CancellationToken); // حذف لینک‌های قبلی foreach (var link in existingLinks) link.IsDeleted = true; // ایجاد لینک‌های جدید foreach (var productId in request.ProductIds) { var existing = existingLinks.FirstOrDefault(l => l.ProductId == productId); if (existing != null) { existing.IsDeleted = false; } else { _context.ProductCategories.Add(new Domain.Entities.ProductCategory { ProductId = productId, CategoryId = request.CategoryId }); } } await _context.SaveChangesAsync(context.CancellationToken); return new Google.Protobuf.WellKnownTypes.Empty(); } // ============= Customer-specific Methods ============= public override async Task GetCustomerProducts(GetProductsRequest request, ServerCallContext context) { var query = new GetCustomerProductsQuery { Id = request.Id }; var result = await _sender.Send(query, context.CancellationToken); var response = new GetProductsResponse { Id = result.Id, Title = result.Title, Description = result.Description, ShortInfomation = result.ShortInfomation, FullInformation = result.FullInformation, Price = result.Price, Discount = result.Discount, Rate = result.Rate, ImagePath = result.ImagePath, ThumbnailPath = result.ThumbnailPath, SaleCount = result.SaleCount, ViewCount = result.ViewCount, RemainingCount = result.RemainingCount }; // Add gallery items if (result.Gallery != null) { foreach (var item in result.Gallery) { response.Gallery.Add(new ProductGalleryItem { ProductGalleryId = item.ProductGalleryId, ProductImageId = item.ProductImageId, Title = item.Title, ImagePath = item.ImagePath, ImageThumbnailPath = item.ImageThumbnailPath }); } } // Add categories if (result.Categories != null) { foreach (var cat in result.Categories) { var categoryPath = new ProductCategoryPath { CategoryId = cat.CategoryId, Title = cat.Title }; if (cat.Path != null) { foreach (var node in cat.Path) { categoryPath.Path.Add(new CategoryNode { Id = node.Id, Title = node.Title, ParentId = node.ParentId }); } } response.Categories.Add(categoryPath); } } return response; } public override async Task GetCustomerProductsByFilter(GetAllProductsByFilterRequest request, ServerCallContext context) { var query = new GetCustomerProductsByFilterQuery { PaginationState = request.PaginationState?.Adapt(), SortBy = request.SortBy, Id = request.Filter?.Id, Title = request.Filter?.Title, Description = request.Filter?.Description, ShortInfomation = request.Filter?.ShortInfomation, FullInformation = request.Filter?.FullInformation, Price = request.Filter?.Price, Discount = request.Filter?.Discount, Rate = request.Filter?.Rate, ImagePath = request.Filter?.ImagePath, ThumbnailPath = request.Filter?.ThumbnailPath, SaleCount = request.Filter?.SaleCount, ViewCount = request.Filter?.ViewCount, RemainingCount = request.Filter?.RemainingCount, CategoryIds = request.Filter?.CategoryId != null ? new List { request.Filter.CategoryId.Value } : null }; var result = await _sender.Send(query, context.CancellationToken); var response = new GetCustomerProductsByFilterResponse { MetaData = new CMSMicroservice.Protobuf.Protos.MetaData { CurrentPage = result.MetaData.CurrentPage, TotalPage = result.MetaData.TotalPage, PageSize = result.MetaData.PageSize, TotalCount = result.MetaData.TotalCount, HasPrevious = result.MetaData.HasPrevious, HasNext = result.MetaData.HasNext } }; foreach (var model in result.Models) { var productModel = new GetCustomerProductsByFilterResponseModel { Id = model.Id, Title = model.Title, Description = model.Description, ShortInfomation = model.ShortInfomation, FullInformation = model.FullInformation, Price = model.Price, Discount = model.Discount, Rate = model.Rate, ImagePath = model.ImagePath, ThumbnailPath = model.ThumbnailPath, SaleCount = model.SaleCount, ViewCount = model.ViewCount, RemainingCount = model.RemainingCount }; if (model.Categories != null) { foreach (var cat in model.Categories) { var categoryPath = new ProductCategoryPath { CategoryId = cat.CategoryId, Title = cat.Title }; if (cat.Path != null) { foreach (var node in cat.Path) { categoryPath.Path.Add(new CategoryNode { Id = node.Id, Title = node.Title, ParentId = node.ParentId }); } } productModel.Categories.Add(categoryPath); } } response.Models.Add(productModel); } return response; } // ============= Product Image Management ============= public override async Task AddProductImage(AddProductImageRequest request, ServerCallContext context) { var command = new AddProductImageCommand { ProductId = request.ProductId, Title = request.Title, ImageFileBytes = request.ImageFile?.File?.ToByteArray(), ImageFileMime = request.ImageFile?.Mime, ImageFileName = request.ImageFile?.FileName }; var result = await _sender.Send(command, context.CancellationToken); return new AddProductImageResponse { ProductGalleryId = result.ProductGalleryId, ProductImageId = result.ProductImageId, Title = result.Title, ImagePath = result.ImagePath, ImageThumbnailPath = result.ImageThumbnailPath }; } public override async Task RemoveProductImage(RemoveProductImageRequest request, ServerCallContext context) { var command = new RemoveProductImageCommand { ProductGalleryId = request.ProductGalleryId }; await _sender.Send(command, context.CancellationToken); return new Google.Protobuf.WellKnownTypes.Empty(); } public override async Task GetProductGallery(GetProductGalleryRequest request, ServerCallContext context) { var query = new GetProductGalleryQuery { ProductId = request.ProductId }; var result = await _sender.Send(query, context.CancellationToken); var response = new GetProductGalleryResponse(); foreach (var item in result.Items) { response.Items.Add(new ProductGalleryItem { ProductGalleryId = item.ProductGalleryId, ProductImageId = item.ProductImageId, Title = item.Title, ImagePath = item.ImagePath, ImageThumbnailPath = item.ImageThumbnailPath }); } return response; } }