diff --git a/Domus.DAL/Domus.DAL.csproj b/Domus.DAL/Domus.DAL.csproj index 770beee..b38ad7f 100644 --- a/Domus.DAL/Domus.DAL.csproj +++ b/Domus.DAL/Domus.DAL.csproj @@ -16,4 +16,9 @@ + + + + + diff --git a/Domus.DAL/Implementations/ProductDetailQuoationRevisionRepository.cs b/Domus.DAL/Implementations/ProductDetailQuoationRevisionRepository.cs new file mode 100644 index 0000000..799d175 --- /dev/null +++ b/Domus.DAL/Implementations/ProductDetailQuoationRevisionRepository.cs @@ -0,0 +1,11 @@ +using Domus.DAL.Interfaces; +using Domus.Domain.Entities; + +namespace Domus.DAL.Implementations; + +public class ProductDetailQuoationRevisionRepository : GenericRepository, IProductDetailQuotationRevisionRepository +{ + public ProductDetailQuoationRevisionRepository(IAppDbContext dbContext) : base(dbContext) + { + } +} \ No newline at end of file diff --git a/Domus.DAL/Implementations/QuotationRevisionRepository.cs b/Domus.DAL/Implementations/QuotationRevisionRepository.cs new file mode 100644 index 0000000..fca428d --- /dev/null +++ b/Domus.DAL/Implementations/QuotationRevisionRepository.cs @@ -0,0 +1,11 @@ +using Domus.DAL.Interfaces; +using Domus.Domain.Entities; + +namespace Domus.DAL.Implementations; + +public class QuotationRevisionRepository : GenericRepository, IQuotationRevisionRepository +{ + public QuotationRevisionRepository(IAppDbContext dbContext) : base(dbContext) + { + } +} \ No newline at end of file diff --git a/Domus.DAL/Interfaces/IProductDetailQuotationRevisionRepository.cs b/Domus.DAL/Interfaces/IProductDetailQuotationRevisionRepository.cs new file mode 100644 index 0000000..4e855af --- /dev/null +++ b/Domus.DAL/Interfaces/IProductDetailQuotationRevisionRepository.cs @@ -0,0 +1,8 @@ +using Domus.Common.Interfaces; +using Domus.Domain.Entities; + +namespace Domus.DAL.Interfaces; + +public interface IProductDetailQuotationRevisionRepository : IGenericRepository, IAutoRegisterable +{ +} diff --git a/Domus.DAL/Interfaces/IQuotationRevisionRepository.cs b/Domus.DAL/Interfaces/IQuotationRevisionRepository.cs new file mode 100644 index 0000000..6955a86 --- /dev/null +++ b/Domus.DAL/Interfaces/IQuotationRevisionRepository.cs @@ -0,0 +1,8 @@ +using Domus.Common.Interfaces; +using Domus.Domain.Entities; + +namespace Domus.DAL.Interfaces; + +public interface IQuotationRevisionRepository : IGenericRepository, IAutoRegisterable +{ +} diff --git a/Domus.Domain/Dtos/Quotations/DtoProductDetailQuotationRevision.cs b/Domus.Domain/Dtos/Quotations/DtoProductDetailQuotationRevision.cs new file mode 100644 index 0000000..c2b4cb1 --- /dev/null +++ b/Domus.Domain/Dtos/Quotations/DtoProductDetailQuotationRevision.cs @@ -0,0 +1,18 @@ +using Domus.Domain.Dtos.Products; + +namespace Domus.Domain.Dtos.Quotations; + +public class DtoProductDetailQuotationRevision +{ + public string ProductName { get; set; } = null!; + + public double Price { get; set; } + + public string MonetaryUnit { get; set; } = null!; + + public double Quantity { get; set; } + + public string QuantityType { get; set; } = null!; + + public DtoProductDetail Detail { get; set; } = null!; +} diff --git a/Domus.Domain/Dtos/Quotations/DtoQuotationFullDetails.cs b/Domus.Domain/Dtos/Quotations/DtoQuotationFullDetails.cs index 24921bf..1eca71a 100644 --- a/Domus.Domain/Dtos/Quotations/DtoQuotationFullDetails.cs +++ b/Domus.Domain/Dtos/Quotations/DtoQuotationFullDetails.cs @@ -1,5 +1,4 @@ using System.Text.Json.Serialization; -using Domus.Domain.Dtos.Products; namespace Domus.Domain.Dtos.Quotations; @@ -18,7 +17,8 @@ public class DtoQuotationFullDetails public DateTime? ExpireAt { get; set; } [JsonPropertyName("products")] - public ICollection ProductDetailQuotations { get; set; } = new List(); + // public ICollection ProductDetailQuotations { get; set; } = new List(); + public ICollection ProductDetailQuotations { get; set; } = new List(); [JsonPropertyName("services")] public ICollection ServiceQuotations { get; set; } = new List(); diff --git a/Domus.Service/AutoMappings/AutoMapperConfiguration.cs b/Domus.Service/AutoMappings/AutoMapperConfiguration.cs index 4420ea3..ddc2107 100644 --- a/Domus.Service/AutoMappings/AutoMapperConfiguration.cs +++ b/Domus.Service/AutoMappings/AutoMapperConfiguration.cs @@ -136,7 +136,7 @@ private static void CreateProductMaps(IMapperConfigurationExpression mapper) mapper.CreateMap() .ForMember(dest => dest.ProductName, opt => opt.MapFrom(src => src.Product.ProductName)); - mapper.CreateMap() + mapper.CreateMap() .ForMember(dest => dest.ProductName, opt => opt.MapFrom(src => src.ProductDetail.Product.ProductName)); @@ -196,16 +196,16 @@ private static void CreateQuotationMaps(IMapperConfigurationExpression mapper) opt => opt.Condition(src => !string.IsNullOrEmpty(src.Status))) .ForMember(dest => dest.ExpireAt, opt => opt.Condition(src => src.ExpireAt != default)) - .ForMember(dest => dest.ProductDetailQuotations, + .ForMember(dest => dest.QuotationRevisions, opt => opt.Ignore()) .ForMember(dest => dest.ServiceQuotations, opt => opt.Ignore()); - mapper.CreateMap() - .ForMember(dest => dest.MonetaryUnit, - opt => opt.Condition(src => !string.IsNullOrEmpty(src.MonetaryUnit))) - .ForMember(dest => dest.QuantityType, - opt => opt.Condition(src => !string.IsNullOrEmpty(src.QuantityType))); + // mapper.CreateMap() + // .ForMember(dest => dest.MonetaryUnit, + // opt => opt.Condition(src => !string.IsNullOrEmpty(src.MonetaryUnit))) + // .ForMember(dest => dest.QuantityType, + // opt => opt.Condition(src => !string.IsNullOrEmpty(src.QuantityType))); mapper.CreateMap(); } diff --git a/Domus.Service/Implementations/QuotationService.cs b/Domus.Service/Implementations/QuotationService.cs index 61a6b91..f79c446 100644 --- a/Domus.Service/Implementations/QuotationService.cs +++ b/Domus.Service/Implementations/QuotationService.cs @@ -3,7 +3,6 @@ using AutoMapper.QueryableExtensions; using Domus.Common.Helpers; using Domus.DAL.Interfaces; -using Domus.Domain.Dtos.Products; using Domus.Domain.Dtos.Quotations; using Domus.Domain.Entities; using Domus.Service.Constants; @@ -15,7 +14,6 @@ using Domus.Service.Models.Requests.Quotations; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; -using UnauthorizedAccessException = System.UnauthorizedAccessException; namespace Domus.Service.Implementations; @@ -26,7 +24,6 @@ public class QuotationService : IQuotationService private readonly IMapper _mapper; private readonly IUserRepository _userRepository; private readonly IProductDetailRepository _productDetailRepository; - private readonly IProductDetailQuotationRepository _productDetailQuotationRepository; private readonly IQuotationNegotiationLogRepository _quotationNegotiationLogRepository; private readonly INegotiationMessageRepository _negotiationMessageRepository; private readonly IServiceRepository _serviceRepository; @@ -34,27 +31,29 @@ public class QuotationService : IQuotationService private readonly UserManager _userManager; private readonly IPackageRepository _packageRepository; private readonly IServiceQuotationRepository _serviceQuotationRepository; + private readonly IQuotationRevisionRepository _quotationRevisionRepository; + private readonly IProductDetailQuotationRevisionRepository _productDetailQuotationRevisionRepository; public QuotationService( - IQuotationRepository quotationRepository, - IUnitOfWork unitOfWork, - IMapper mapper, - IUserRepository userRepository, - IProductDetailRepository productDetailRepository, - IProductDetailQuotationRepository productDetailQuotationRepository, - IServiceRepository serviceRepository, - INegotiationMessageRepository negotiationMessageRepository, - IQuotationNegotiationLogRepository quotationNegotiationLogRepository, - IJwtService jwtService, - UserManager userManager, - IPackageRepository packageRepository, - IServiceQuotationRepository serviceQuotationRepository) + IQuotationRepository quotationRepository, + IUnitOfWork unitOfWork, + IMapper mapper, + IUserRepository userRepository, + IProductDetailRepository productDetailRepository, + IServiceRepository serviceRepository, + INegotiationMessageRepository negotiationMessageRepository, + IQuotationNegotiationLogRepository quotationNegotiationLogRepository, + IJwtService jwtService, + UserManager userManager, + IPackageRepository packageRepository, + IQuotationRevisionRepository quotationRevisionRepository, + IProductDetailQuotationRevisionRepository productDetailQuotationRevisionRepository, + IServiceQuotationRepository serviceQuotationRepository) { _quotationRepository = quotationRepository; _unitOfWork = unitOfWork; _userRepository = userRepository; _productDetailRepository = productDetailRepository; - _productDetailQuotationRepository = productDetailQuotationRepository; _quotationNegotiationLogRepository = quotationNegotiationLogRepository; _jwtService = jwtService; _userManager = userManager; @@ -62,6 +61,8 @@ public QuotationService( _serviceRepository = serviceRepository; _negotiationMessageRepository = negotiationMessageRepository; _serviceQuotationRepository = serviceQuotationRepository; + _quotationRevisionRepository = quotationRevisionRepository; + _productDetailQuotationRevisionRepository = productDetailQuotationRevisionRepository; _mapper = mapper; } @@ -130,26 +131,25 @@ public async Task CreateQuotation(CreateQuotationRequest re if (productDetailEntity == null) throw new ProductDetailNotFoundException(); - var productDetailQuotation = new ProductDetailQuotation + var quotationRevision = new QuotationRevision + { + Quotation = quotation, + Version = 0, + CreatedAt = DateTime.Now + }; + + var productDetailQuotationRevision = new ProductDetailQuotationRevision { ProductDetailId = productDetail.Id, - QuotationId = quotation.Id, + QuotationRevision = quotationRevision, Quantity = Math.Max(productDetail.Quantity, 1), Price = productDetailEntity.DisplayPrice, MonetaryUnit = "USD", QuantityType = "Unit", }; - var productDetailQuotationRevision = new ProductDetailQuotationRevision - { - ProductDetailQuotation = productDetailQuotation, - Price = productDetailQuotation.Price, - Quantity = Math.Max(productDetailQuotation.Quantity, 1), - Version = 0 - }; - - productDetailQuotation.ProductDetailQuotationRevisions.Add(productDetailQuotationRevision); - quotation.ProductDetailQuotations.Add(productDetailQuotation); + quotationRevision.ProductDetailQuotationRevisions.Add(productDetailQuotationRevision); + quotation.QuotationRevisions.Add(quotationRevision); } foreach (var service in request.Services) @@ -268,23 +268,19 @@ public async Task GetQuotationById(Guid id) .ProjectTo(_mapper.ConfigurationProvider) .FirstOrDefault() ?? throw new QuotationNotFoundException(); - var products = (await _productDetailQuotationRepository.GetAllAsync()) - .Include(pdq => pdq.ProductDetailQuotationRevisions) - .Include(pdq => pdq.ProductDetail) + var quotationRevisions = + await _quotationRevisionRepository.FindAsync(r => !r.IsDeleted && r.QuotationId == id); + + var latestVersion = quotationRevisions.Any() ? quotationRevisions.Select(r => r.Version).Max() : 0; + + var products = await (await _productDetailQuotationRevisionRepository.FindAsync(r => r.QuotationRevision.QuotationId == id && r.QuotationRevision.Version == latestVersion)) + .Include(r => r.ProductDetail) .ThenInclude(pd => pd.Product) - .Where(pdq => pdq.QuotationId == quotation.Id) - .ToList() - .Select(pdq => - { - var latestRevision = pdq.ProductDetailQuotationRevisions.MaxBy(r => r.Version); - pdq.Price = latestRevision?.Price ?? pdq.Price; - pdq.Quantity = latestRevision?.Quantity ?? pdq.Quantity; - return pdq; - }); - quotation.ProductDetailQuotations = _mapper.Map>(products); + .ToListAsync(); + + quotation.ProductDetailQuotations = _mapper.Map>(products); - var totalProductPrice = products.Select(pdq => pdq.ProductDetailQuotationRevisions.MaxBy(r => r.Version)) - .Sum(r => (float)(r?.Price ?? 0) * r?.Quantity ?? 0); + var totalProductPrice = products.Sum(r => (float)(r?.Price ?? 0) * r?.Quantity ?? 0); var totalServicePrice = quotation.ServiceQuotations.Sum(s => s.Price); quotation.TotalPrice = totalProductPrice + totalServicePrice; @@ -300,25 +296,7 @@ public async Task SearchQuotations(SearchUsingGetRequest re foreach (var quotation in quotations) { - var products = (await _productDetailQuotationRepository.GetAllAsync()) - .Where(pdq => pdq.QuotationId == quotation.Id) - .Include(pdq => pdq.ProductDetailQuotationRevisions) - .Include(pdq => pdq.ProductDetail) - .ThenInclude(pd => pd.Product) - .AsSplitQuery() - .ToList() - .Select(pdq => - { - var latestRevision = pdq.ProductDetailQuotationRevisions.MaxBy(r => r.Version); - pdq.Price = latestRevision?.Price ?? pdq.Price; - pdq.Quantity = latestRevision?.Quantity ?? pdq.Quantity; - return pdq; - }); - quotation.ProductDetailQuotations = _mapper.Map>(products); - var totalProductPrice = products.Select(pdq => pdq.ProductDetailQuotationRevisions.MaxBy(r => r.Version)) - .Sum(r => (float)(r?.Price ?? 0) * r?.Quantity ?? 0); - var totalServicePrice = quotation.ServiceQuotations.Sum(s => s.Price); - quotation.TotalPrice = totalProductPrice + totalServicePrice; + quotation.TotalPrice = await GetQuotationTotalPrice(quotation.Id); } if (!string.IsNullOrEmpty(request.SearchField)) @@ -343,17 +321,25 @@ public async Task SearchQuotations(SearchUsingGetRequest re public async Task UpdateQuotation(UpdateQuotationRequest request, Guid id) { - if (!await _userRepository.ExistsAsync(u => u.Id == request.CustomerId)) + if (request.CustomerId != default && !await _userRepository.ExistsAsync(u => u.Id == request.CustomerId)) throw new UserNotFoundException("Customer not found"); - if (!await _userRepository.ExistsAsync(u => u.Id == request.StaffId)) + if (request.StaffId != default && !await _userRepository.ExistsAsync(u => u.Id == request.StaffId)) throw new UserNotFoundException("Staff not found"); var quotation = await (await _quotationRepository.FindAsync(q => !q.IsDeleted && q.Id == id)) .Include(q => q.ServiceQuotations) - .Include(q => q.ProductDetailQuotations) + .Include(q => q.QuotationRevisions) .FirstOrDefaultAsync() ?? throw new QuotationNotFoundException(); - + _mapper.Map(request, quotation); + + var newQuotationRevision = new QuotationRevision + { + Quotation = quotation, + Version = quotation.QuotationRevisions.Count, + IsDeleted = false, + CreatedAt = DateTime.Now + }; foreach (var requestService in request.Services) { @@ -368,16 +354,16 @@ public async Task UpdateQuotation(UpdateQuotationRequest re ServiceId = requestService.ServiceId, Price = requestService.Price }; - + quotation.ServiceQuotations.Add(newServiceQuotation); continue; } - + quotation.ServiceQuotations.Remove(serviceQuotation); serviceQuotation.Price = requestService.Price; quotation.ServiceQuotations.Add(serviceQuotation); } - + var excludedServices = new List(quotation.ServiceQuotations.Where(s => !request.Services.Select(rs => rs.ServiceId).Contains(s.ServiceId))); foreach (var excludedService in excludedServices) quotation.ServiceQuotations.Remove(excludedService); @@ -386,79 +372,46 @@ public async Task UpdateQuotation(UpdateQuotationRequest re { if (!await _productDetailRepository.ExistsAsync(x => x.Id == requestProductDetail.ProductDetailId)) throw new ProductDetailNotFoundException(); - var productDetail = await (await _productDetailQuotationRepository.FindAsync(s => s.ProductDetailId == requestProductDetail.ProductDetailId && s.QuotationId == quotation.Id)) - .Include(s => s.ProductDetailQuotationRevisions) - .FirstOrDefaultAsync(); - - if (productDetail == null) + var productDetailInQuotaionRevision = new ProductDetailQuotationRevision { - var newProductDetail = new ProductDetailQuotation - { - ProductDetailId = requestProductDetail.ProductDetailId, - QuotationId = quotation.Id, - Quantity = requestProductDetail.Quantity, - Price = requestProductDetail.Price, - MonetaryUnit = string.IsNullOrEmpty(requestProductDetail.MonetaryUnit) ? "USD" : requestProductDetail.MonetaryUnit, - QuantityType = string.IsNullOrEmpty(requestProductDetail.QuantityType) ? "Unit" : requestProductDetail.QuantityType - }; - - var productDetailQuotationRevision = new ProductDetailQuotationRevision - { - ProductDetailQuotation = newProductDetail, - Price = requestProductDetail.Price, - Quantity = requestProductDetail.Quantity, - Version = 0 - }; - - newProductDetail.ProductDetailQuotationRevisions.Add(productDetailQuotationRevision); - quotation.ProductDetailQuotations.Add(newProductDetail); - continue; - }; - - var newProductDetailQuotationRevision = new ProductDetailQuotationRevision - { - ProductDetailQuotationId = productDetail.Id, Price = requestProductDetail.Price, + MonetaryUnit = string.IsNullOrEmpty(requestProductDetail.MonetaryUnit) ? "USD" : requestProductDetail.MonetaryUnit, Quantity = requestProductDetail.Quantity, - Version = productDetail.ProductDetailQuotationRevisions.Count + QuantityType = string.IsNullOrEmpty(requestProductDetail.QuantityType) ? "Unit" : requestProductDetail.QuantityType, + IsDeleted = false, + ProductDetailId = requestProductDetail.ProductDetailId, + QuotationRevision = newQuotationRevision }; - - productDetail.ProductDetailQuotationRevisions.Add(newProductDetailQuotationRevision); + + newQuotationRevision.ProductDetailQuotationRevisions.Add(productDetailInQuotaionRevision); } - var excludedProductDetails = new List(quotation.ProductDetailQuotations.Where(s => !request.ProductDetailQuotations.Select(pdq => pdq.ProductDetailId).Contains(s.ProductDetailId))); - foreach (var excludedProductDetail in excludedProductDetails) - quotation.ProductDetailQuotations.Remove(excludedProductDetail); - + quotation.QuotationRevisions.Add(newQuotationRevision); quotation.LastUpdatedAt = DateTime.Now; await _quotationRepository.UpdateAsync(quotation); await _unitOfWork.CommitAsync(); - + return new ServiceActionResult(true); } private async Task GetQuotationTotalPrice(Guid quotationId) { - var products = (await _productDetailQuotationRepository.GetAllAsync()) - .Include(pdq => pdq.ProductDetailQuotationRevisions) - .Include(pdq => pdq.ProductDetail) + var quotationRevisions = + await _quotationRevisionRepository.FindAsync(r => !r.IsDeleted && r.QuotationId == quotationId); + + var latestVersion = quotationRevisions.Any() ? quotationRevisions.Select(r => r.Version).Max() : 0; + + var products = await (await _productDetailQuotationRevisionRepository.FindAsync(r => r.QuotationRevision.QuotationId == quotationId && r.QuotationRevision.Version == latestVersion)) + .Include(r => r.ProductDetail) .ThenInclude(pd => pd.Product) - .Where(pdq => pdq.QuotationId == quotationId) - .ToList() - .Select(pdq => - { - var latestRevision = pdq.ProductDetailQuotationRevisions.MaxBy(r => r.Version); - pdq.Price = latestRevision?.Price ?? pdq.Price; - pdq.Quantity = latestRevision?.Quantity ?? pdq.Quantity; - return pdq; - }); - var services = await (await _serviceQuotationRepository.FindAsync(s => s.QuotationId == quotationId)) .ToListAsync(); - - var totalProductPrice = products.Select(pdq => pdq.ProductDetailQuotationRevisions.MaxBy(r => r.Version)) - .Sum(r => (float)(r?.Price ?? r?.ProductDetailQuotation.Price ?? 0) * r?.Quantity ?? r?.ProductDetailQuotation.Quantity ?? 0); - - var totalServicePrice = services.Sum(s => s.Price); + + var totalProductPrice = products.Sum(r => (float)r.Price * r.Quantity); + var totalServicePrice = (await _serviceQuotationRepository.FindAsync(sq => sq.QuotationId == quotationId)) + .ToList() + .Select(sq => sq.Price) + .Sum(); + return totalProductPrice + totalServicePrice; } }