From 2f4c39de63147dfefba85df3bb406dafb0d4dc99 Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 08:14:40 +0200 Subject: [PATCH 01/33] Pass the customer explicitly when listing brands --- .../Services/Brands/BrandService.cs | 8 ++--- .../ExportImport/BrandImportDataObject.cs | 2 +- .../Catalog/Brands/IBrandService.cs | 3 ++ .../Common/GetSitemapXMLCommandHandler.cs | 2 +- .../Services/Brands/BrandServiceTests.cs | 31 ++++++++++++++----- .../BrandImportDataObjectTests.cs | 6 +--- .../GetSitemapXmlCommandHandlerTests.cs | 6 ++-- .../Controllers/BaseBrandControllerTests.cs | 16 +++++----- .../BaseDiscountControllerTests.cs | 8 ++--- .../Controllers/BaseSearchControllerTests.cs | 20 ++++++++---- .../Controllers/SearchController.cs | 2 +- .../Controllers/BaseBrandController.cs | 4 +-- .../Controllers/BaseDiscountController.cs | 2 +- .../Controllers/BaseSearchController.cs | 8 ++++- .../Controllers/SearchController.cs | 4 +-- .../Controllers/SearchController.cs | 6 ++-- .../Handlers/Catalog/GetBrandAllHandler.cs | 2 +- .../Catalog/GetHomepageBrandsHandler.cs | 2 +- .../Handlers/Catalog/GetMenuHandler.cs | 2 +- .../Handlers/Common/GetSitemapHandler.cs | 2 +- 20 files changed, 82 insertions(+), 54 deletions(-) diff --git a/src/Business/Grand.Business.Catalog/Services/Brands/BrandService.cs b/src/Business/Grand.Business.Catalog/Services/Brands/BrandService.cs index 0065024f06..1dfb747bc9 100644 --- a/src/Business/Grand.Business.Catalog/Services/Brands/BrandService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Brands/BrandService.cs @@ -3,7 +3,6 @@ using Grand.Domain; using Grand.Domain.Catalog; using Grand.Domain.Customers; -using Grand.Infrastructure; using Grand.Infrastructure.Caching; using Grand.Infrastructure.Caching.Constants; using Grand.Infrastructure.Configuration; @@ -24,12 +23,10 @@ public class BrandService : IBrandService /// public BrandService(ICacheBase cacheBase, IRepository brandRepository, - IContextAccessor contextAccessor, IMediator mediator, AccessControlConfig accessControlConfig) { _cacheBase = cacheBase; _brandRepository = brandRepository; - _contextAccessor = contextAccessor; _mediator = mediator; _accessControlConfig = accessControlConfig; } @@ -39,7 +36,6 @@ public BrandService(ICacheBase cacheBase, #region Fields private readonly IRepository _brandRepository; - private readonly IContextAccessor _contextAccessor; private readonly IMediator _mediator; private readonly ICacheBase _cacheBase; private readonly AccessControlConfig _accessControlConfig; @@ -53,12 +49,14 @@ public BrandService(ICacheBase cacheBase, /// /// Brand name /// Store ident + /// Customer whose groups limit the result (not read when showHidden is true) /// Page index /// Page size /// A value that indicates if it should shows hidden records /// Brands public virtual async Task> GetAllBrands(string brandName, string storeId, + Customer customer, int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false) @@ -77,7 +75,7 @@ public virtual async Task> GetAllBrands(string brandName, if (!showHidden && !_accessControlConfig.IgnoreAcl) { //Limited to customer groups rules - var allowedCustomerGroupsIds = _contextAccessor.WorkContext.CurrentCustomer.GetCustomerGroupIds(); + var allowedCustomerGroupsIds = customer.GetCustomerGroupIds(); query = from p in query where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x)) select p; diff --git a/src/Business/Grand.Business.Catalog/Services/ExportImport/BrandImportDataObject.cs b/src/Business/Grand.Business.Catalog/Services/ExportImport/BrandImportDataObject.cs index 62c8f6105d..62c5b1e904 100644 --- a/src/Business/Grand.Business.Catalog/Services/ExportImport/BrandImportDataObject.cs +++ b/src/Business/Grand.Business.Catalog/Services/ExportImport/BrandImportDataObject.cs @@ -148,7 +148,7 @@ private static string Key(BrandDto dto) if (string.IsNullOrEmpty(dto.Name)) return (null, false, null); - var candidates = await _brandService.GetAllBrands(dto.Name, "", showHidden: true); + var candidates = await _brandService.GetAllBrands(dto.Name, "", customer: null, showHidden: true); var (found, error) = ImportRows.SingleByName(candidates, b => b.Name, dto.Name, "brand"); return (found, found != null, error); } diff --git a/src/Business/Grand.Business.Core/Interfaces/Catalog/Brands/IBrandService.cs b/src/Business/Grand.Business.Core/Interfaces/Catalog/Brands/IBrandService.cs index 11e09608f4..940a00ec8c 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Catalog/Brands/IBrandService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Catalog/Brands/IBrandService.cs @@ -1,5 +1,6 @@ using Grand.Domain; using Grand.Domain.Catalog; +using Grand.Domain.Customers; namespace Grand.Business.Core.Interfaces.Catalog.Brands; @@ -13,12 +14,14 @@ public interface IBrandService /// /// Brand name /// Store ident + /// Customer whose groups limit the result (not read when showHidden is true) /// Page index /// Page size /// A value that indicates if it should shows hidden records /// Brands Task> GetAllBrands(string brandName, string storeId, + Customer customer, int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false); diff --git a/src/Business/Grand.Business.Messages/Commands/Handlers/Common/GetSitemapXMLCommandHandler.cs b/src/Business/Grand.Business.Messages/Commands/Handlers/Common/GetSitemapXMLCommandHandler.cs index 746e1751a8..393fccf7d3 100644 --- a/src/Business/Grand.Business.Messages/Commands/Handlers/Common/GetSitemapXMLCommandHandler.cs +++ b/src/Business/Grand.Business.Messages/Commands/Handlers/Common/GetSitemapXMLCommandHandler.cs @@ -259,7 +259,7 @@ private async Task> GetCategoryUrls(string parentCategor private async Task> GetBrandUrls(Language language, Store store) { - var brands = (await _brandService.GetAllBrands(brandName: "", storeId: store.Id, showHidden: true)) + var brands = (await _brandService.GetAllBrands(brandName: "", storeId: store.Id, customer: null, showHidden: true)) .Where(b => b.Published && IsVisibleToGuests(b)); var brandUrls = new List(); var storeLocation = GetStoreLocation(); diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Brands/BrandServiceTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Brands/BrandServiceTests.cs index a648d49e93..96ba3dd7bb 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Brands/BrandServiceTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Brands/BrandServiceTests.cs @@ -3,8 +3,6 @@ using Grand.Data.Tests.MongoDb; using Grand.Domain.Catalog; using Grand.Domain.Customers; -using Grand.Domain.Stores; -using Grand.Infrastructure; using Grand.Infrastructure.Caching; using Grand.Infrastructure.Configuration; using Grand.Infrastructure.Tests.Caching; @@ -21,19 +19,15 @@ public class BrandServiceTests private MemoryCacheBase _cacheBase; private Mock _mediatorMock; private IRepository _repository; - private Mock _workContextMock; [TestInitialize] public void InitializeTests() { _repository = new MongoDBRepositoryTest(); - _workContextMock = new Mock(); - _workContextMock.Setup(c => c.StoreContext.CurrentStore).Returns(() => new Store { Id = "" }); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => new Customer()); _mediatorMock = new Mock(); _cacheBase = new MemoryCacheBase(MemoryCacheTest.Get(), _mediatorMock.Object, new CacheConfig { DefaultCacheTimeMinutes = 1 }); - _brandService = new BrandService(_cacheBase, _repository, _workContextMock.Object, _mediatorMock.Object, + _brandService = new BrandService(_cacheBase, _repository, _mediatorMock.Object, new AccessControlConfig()); } @@ -47,12 +41,33 @@ public async Task GetAllBrandsTest() await _brandService.InsertBrand(new Brand { Published = true }); //Act - var brand = await _brandService.GetAllBrands(brandName: "", storeId: ""); + var brand = await _brandService.GetAllBrands(brandName: "", storeId: "", customer: new Customer()); //Assert Assert.HasCount(3, brand); } + [TestMethod] + public async Task GetAllBrands_FiltersByGroupsOfGivenCustomer() + { + //Arrange + await _repository.InsertAsync(new Brand { Id = "open", Name = "open", Published = true }); + await _repository.InsertAsync(new Brand { + Id = "limited", Name = "limited", Published = true, LimitedToGroups = true, + CustomerGroups = new List { "vip" } + }); + var vipCustomer = new Customer(); + vipCustomer.Groups.Add("vip"); + + //Act + var guest = await _brandService.GetAllBrands("", "", new Customer()); + var vip = await _brandService.GetAllBrands("", "", vipCustomer); + + //Assert + Assert.HasCount(1, guest); + Assert.HasCount(2, vip); + } + [TestMethod] public async Task GetBrandByIdTest() { diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/BrandImportDataObjectTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/BrandImportDataObjectTests.cs index dc3c93e257..5ea53bfc09 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/BrandImportDataObjectTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/BrandImportDataObjectTests.cs @@ -43,7 +43,6 @@ public class BrandImportDataObjectTests private IRepository _repository; private Mock _slugServiceMock; - private Mock _workContextMock; private ISeNameService _seNameService; private SecurityConfig _securityConfig; private Mock> _loggerMock; @@ -59,14 +58,11 @@ public void Init() _brandLayoutServiceMock = new Mock(); _slugServiceMock = new Mock(); _languageServiceMock = new Mock(); - _workContextMock = new Mock(); - _workContextMock.Setup(c => c.StoreContext.CurrentStore).Returns(() => new Store { Id = "" }); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => new Customer()); _mediatorMock = new Mock(); _cacheBase = new MemoryCacheBase(MemoryCacheTest.Get(), _mediatorMock.Object, new CacheConfig { DefaultCacheTimeMinutes = 1 }); - _brandService = new BrandService(_cacheBase, _repository, _workContextMock.Object, _mediatorMock.Object, + _brandService = new BrandService(_cacheBase, _repository, _mediatorMock.Object, new AccessControlConfig()); _seNameService = new SeNameService(_slugServiceMock.Object, _languageServiceMock.Object, new SeoSettings()); _brandImportDataObject = new BrandImportDataObject(_brandService, _pictureServiceMock.Object, diff --git a/src/Tests/Grand.Business.Messages.Tests/Commands/GetSitemapXmlCommandHandlerTests.cs b/src/Tests/Grand.Business.Messages.Tests/Commands/GetSitemapXmlCommandHandlerTests.cs index 7a5cfac00a..7c7b4b6cb6 100644 --- a/src/Tests/Grand.Business.Messages.Tests/Commands/GetSitemapXmlCommandHandlerTests.cs +++ b/src/Tests/Grand.Business.Messages.Tests/Commands/GetSitemapXmlCommandHandlerTests.cs @@ -62,7 +62,7 @@ public void Init() _categoryServiceMock.Setup(x => x.GetAllCategories(null, "", StoreId, 0, int.MaxValue, true)) .ReturnsAsync(new PagedList()); - _brandServiceMock.Setup(x => x.GetAllBrands("", StoreId, 0, int.MaxValue, true)) + _brandServiceMock.Setup(x => x.GetAllBrands("", StoreId, null, 0, int.MaxValue, true)) .ReturnsAsync(new PagedList()); _pageServiceMock.Setup(x => x.GetAllPages(StoreId, true)).ReturnsAsync(new List()); _knowledgebaseServiceMock.Setup(x => x.GetPublicKnowledgebaseArticles(StoreId, @@ -125,7 +125,7 @@ public async Task Handle_WithoutWorkContext_UsesOnlyExplicitlyScopedQueries() //Assert Assert.IsFalse(Locations(xml).Any(string.IsNullOrEmpty), "sitemap contains an empty "); _categoryServiceMock.Verify(x => x.GetAllCategories(null, "", StoreId, 0, int.MaxValue, true), Times.Once); - _brandServiceMock.Verify(x => x.GetAllBrands("", StoreId, 0, int.MaxValue, true), Times.Once); + _brandServiceMock.Verify(x => x.GetAllBrands("", StoreId, null, 0, int.MaxValue, true), Times.Once); _pageServiceMock.Verify(x => x.GetAllPages(StoreId, true), Times.Once); _mediatorMock.Verify(x => x.Send(It.Is(q => q.Customer == null && q.StoreId == StoreId && q.VisibleIndividuallyOnly && @@ -159,7 +159,7 @@ public async Task Handle_ListsOnlyWhatAGuestCanSee() new() { Id = "c4", ParentCategoryId = "c3", SeName = "child-of-hidden-category", Published = true }, new() { Id = "c5", ParentCategoryId = "", SeName = "unpublished-category", Published = false } }, 0, int.MaxValue)); - _brandServiceMock.Setup(x => x.GetAllBrands("", StoreId, 0, int.MaxValue, true)) + _brandServiceMock.Setup(x => x.GetAllBrands("", StoreId, null, 0, int.MaxValue, true)) .ReturnsAsync(new PagedList(new List { new() { Id = "b1", SeName = "open-brand", Published = true }, new() { diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseBrandControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseBrandControllerTests.cs index 24ac187d59..81ebc6bb7f 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseBrandControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseBrandControllerTests.cs @@ -122,7 +122,7 @@ public async Task ListPost_ForcesScopeDefaultStoreIdOntoSearchModel() { _scopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); _brandServiceMock - .Setup(b => b.GetAllBrands(It.IsAny(), "store-1", 0, 10, true)) + .Setup(b => b.GetAllBrands(It.IsAny(), "store-1", null, 0, 10, true)) .ReturnsAsync(new PagedList(new List(), 0, 10)); var model = new BrandListModel { SearchStoreId = "attacker-supplied-store" }; @@ -136,7 +136,7 @@ public async Task ListPost_GlobalScope_LeavesSubmittedSearchStoreIdUntouched() { _scopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); _brandServiceMock - .Setup(b => b.GetAllBrands(It.IsAny(), "admin-submitted-store", 0, 10, true)) + .Setup(b => b.GetAllBrands(It.IsAny(), "admin-submitted-store", null, 0, 10, true)) .ReturnsAsync(new PagedList(new List(), 0, 10)); var model = new BrandListModel { SearchStoreId = "admin-submitted-store" }; @@ -377,7 +377,7 @@ public async Task PicturePopupPost_ValidRequest_CallsUpdatePicture() public async Task ExportXlsx_Success_ReturnsXlsxFile() { _brandServiceMock - .Setup(b => b.GetAllBrands("", "", 0, int.MaxValue, true)) + .Setup(b => b.GetAllBrands("", "", null, 0, int.MaxValue, true)) .ReturnsAsync(new PagedList(new List { new() { Id = "b1" } }, 0, int.MaxValue)); var exportManagerMock = new Mock>(); exportManagerMock.Setup(e => e.Export(It.IsAny>(), It.IsAny())).ReturnsAsync([1, 2, 3]); @@ -394,7 +394,7 @@ public async Task ExportXlsx_StoreScoped_ScopesExportToDefaultStoreId() { _scopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); _brandServiceMock - .Setup(b => b.GetAllBrands("", "store-1", 0, int.MaxValue, true)) + .Setup(b => b.GetAllBrands("", "store-1", null, 0, int.MaxValue, true)) .ReturnsAsync(new PagedList(new List { new() { Id = "b1" } }, 0, int.MaxValue)); var exportManagerMock = new Mock>(); exportManagerMock.Setup(e => e.Export(It.IsAny>(), It.IsAny())).ReturnsAsync([1, 2, 3]); @@ -403,15 +403,15 @@ public async Task ExportXlsx_StoreScoped_ScopesExportToDefaultStoreId() var file = result as FileContentResult; Assert.IsNotNull(file); - _brandServiceMock.Verify(b => b.GetAllBrands("", "store-1", 0, int.MaxValue, true), Times.Once); - _brandServiceMock.Verify(b => b.GetAllBrands("", "", 0, int.MaxValue, true), Times.Never); + _brandServiceMock.Verify(b => b.GetAllBrands("", "store-1", null, 0, int.MaxValue, true), Times.Once); + _brandServiceMock.Verify(b => b.GetAllBrands("", "", null, 0, int.MaxValue, true), Times.Never); } [TestMethod] public async Task ExportXlsx_ExportThrows_ShowsErrorAndRedirectsToList() { _brandServiceMock - .Setup(b => b.GetAllBrands("", "", 0, int.MaxValue, true)) + .Setup(b => b.GetAllBrands("", "", null, 0, int.MaxValue, true)) .ThrowsAsync(new InvalidOperationException("boom")); var exportManagerMock = new Mock>(); @@ -495,7 +495,7 @@ await Assert.ThrowsExactlyAsync(() => public async Task ExportXlsx_CancelledWithoutAbortedRequest_ShowsErrorAndRedirectsToList() { _brandServiceMock - .Setup(b => b.GetAllBrands("", "", 0, int.MaxValue, true)) + .Setup(b => b.GetAllBrands("", "", null, 0, int.MaxValue, true)) .ThrowsAsync(new OperationCanceledException()); var exportManagerMock = new Mock>(); diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseDiscountControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseDiscountControllerTests.cs index 7322533f2e..01188050c3 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseDiscountControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseDiscountControllerTests.cs @@ -531,13 +531,13 @@ public async Task BrandAddPopupList_GlobalScope_PassesEmptyStoreId() { _scope.Setup(x => x.GetDefaultStoreIdAsync()).ReturnsAsync((string?)null); var brandService = new Mock(); - brandService.Setup(x => x.GetAllBrands(null, "", 0, 10, true)) + brandService.Setup(x => x.GetAllBrands(null, "", null, 0, 10, true)) .ReturnsAsync(new PagedList(new List(), 0, 10, 0)); await _sut.BrandAddPopupList(new DataSourceRequest { Page = 1, PageSize = 10 }, new DiscountModel.AddBrandToDiscountModel(), brandService.Object); - brandService.Verify(x => x.GetAllBrands(null, "", 0, 10, true), Times.Once); + brandService.Verify(x => x.GetAllBrands(null, "", null, 0, 10, true), Times.Once); } [TestMethod] @@ -545,13 +545,13 @@ public async Task BrandAddPopupList_StoreScope_PassesScopedStoreId() { _scope.Setup(x => x.GetDefaultStoreIdAsync()).ReturnsAsync("store-a"); var brandService = new Mock(); - brandService.Setup(x => x.GetAllBrands(null, "store-a", 0, 10, true)) + brandService.Setup(x => x.GetAllBrands(null, "store-a", null, 0, 10, true)) .ReturnsAsync(new PagedList(new List(), 0, 10, 0)); await _sut.BrandAddPopupList(new DataSourceRequest { Page = 1, PageSize = 10 }, new DiscountModel.AddBrandToDiscountModel(), brandService.Object); - brandService.Verify(x => x.GetAllBrands(null, "store-a", 0, 10, true), Times.Once); + brandService.Verify(x => x.GetAllBrands(null, "store-a", null, 0, 10, true), Times.Once); } [TestMethod] diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseSearchControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseSearchControllerTests.cs index 56c5f6fa05..c2a3027e51 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseSearchControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseSearchControllerTests.cs @@ -4,6 +4,8 @@ using Grand.Domain; using Grand.Domain.Admin; using Grand.Domain.Catalog; +using Grand.Domain.Customers; +using Grand.Infrastructure; using Grand.Web.AdminShared.Controllers; using Grand.Web.Common.DataSource; using Microsoft.AspNetCore.Mvc; @@ -24,8 +26,9 @@ private class TestSearchController( IBrandService brandService, ICollectionService collectionService, AdminSearchSettings adminSearchSettings, + IContextAccessor contextAccessor, string pickerStoreId) - : BaseSearchController(categoryService, brandService, collectionService, adminSearchSettings) + : BaseSearchController(categoryService, brandService, collectionService, adminSearchSettings, contextAccessor) { protected override string PickerStoreId => pickerStoreId; } @@ -34,6 +37,8 @@ private class TestSearchController( private Mock _brandServiceMock = null!; private Mock _collectionServiceMock = null!; private AdminSearchSettings _settings = null!; + private Customer _customer = null!; + private Mock _contextAccessorMock = null!; [TestInitialize] public void Setup() @@ -41,6 +46,9 @@ public void Setup() _categoryServiceMock = new Mock(); _brandServiceMock = new Mock(); _collectionServiceMock = new Mock(); + _customer = new Customer(); + _contextAccessorMock = new Mock(); + _contextAccessorMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(_customer); _settings = new AdminSearchSettings { CategorySizeLimit = 10, CollectionSizeLimit = 10, @@ -49,7 +57,7 @@ public void Setup() } private TestSearchController CreateController(string pickerStoreId) => - new(_categoryServiceMock.Object, _brandServiceMock.Object, _collectionServiceMock.Object, _settings, pickerStoreId); + new(_categoryServiceMock.Object, _brandServiceMock.Object, _collectionServiceMock.Object, _settings, _contextAccessorMock.Object, pickerStoreId); private static DataSourceRequestFilter EmptyFilter() => new() { Filters = [] }; @@ -135,7 +143,7 @@ public async Task Brand_DefaultPickerStoreId_PassesEmptyStoreIdToService() { var controller = CreateController(""); _brandServiceMock - .Setup(s => s.GetAllBrands(null, "", 0, 10, false)) + .Setup(s => s.GetAllBrands(null, "", _customer, 0, 10, false)) .ReturnsAsync(new PagedList([new Brand { Id = "brand-1", Name = "Acme" }], 0, 10)); var result = await controller.Brand(null, EmptyFilter()); @@ -143,7 +151,7 @@ public async Task Brand_DefaultPickerStoreId_PassesEmptyStoreIdToService() var rows = ReadRows(result); Assert.AreEqual(1, rows.Length); Assert.AreEqual("Acme", rows[0].Name); - _brandServiceMock.Verify(s => s.GetAllBrands(null, "", 0, 10, false), Times.Once); + _brandServiceMock.Verify(s => s.GetAllBrands(null, "", _customer, 0, 10, false), Times.Once); } [TestMethod] @@ -151,11 +159,11 @@ public async Task Brand_OverriddenPickerStoreId_PassesStoreIdToService() { var controller = CreateController("store-1"); _brandServiceMock - .Setup(s => s.GetAllBrands(null, "store-1", 0, 10, false)) + .Setup(s => s.GetAllBrands(null, "store-1", _customer, 0, 10, false)) .ReturnsAsync(new PagedList([], 0, 10)); await controller.Brand(null, EmptyFilter()); - _brandServiceMock.Verify(s => s.GetAllBrands(null, "store-1", 0, 10, false), Times.Once); + _brandServiceMock.Verify(s => s.GetAllBrands(null, "store-1", _customer, 0, 10, false), Times.Once); } } diff --git a/src/Web/Grand.Web.Admin/Controllers/SearchController.cs b/src/Web/Grand.Web.Admin/Controllers/SearchController.cs index b3a4b6b15b..972bf4bf66 100644 --- a/src/Web/Grand.Web.Admin/Controllers/SearchController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/SearchController.cs @@ -50,7 +50,7 @@ public SearchController(IProductService productService, ICategoryService categor AdminSearchSettings adminSearchSettings, ITranslationService translationService, IContextAccessor contextAccessor, IGroupService groupService, IStoreService storeService, IVendorService vendorService) - : base(categoryService, brandService, collectionService, adminSearchSettings) + : base(categoryService, brandService, collectionService, adminSearchSettings, contextAccessor) { _productService = productService; _categoryService = categoryService; diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseBrandController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseBrandController.cs index ec77e7092d..d4855a2660 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseBrandController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseBrandController.cs @@ -75,7 +75,7 @@ public async Task List(DataSourceRequest command, BrandListModel { if (scope.DefaultStoreId is not null) model.SearchStoreId = scope.DefaultStoreId; var brands = await brandService.GetAllBrands(model.SearchBrandName, - model.SearchStoreId, command.Page - 1, command.PageSize, true); + model.SearchStoreId, customer: null, command.Page - 1, command.PageSize, true); var gridModel = new DataSourceResult { Data = brands.Select(x => x.ToModel()), Total = brands.TotalCount @@ -251,7 +251,7 @@ public async Task ExportXlsx([FromServices] IExportManager { try { - var bytes = await exportManager.Export(await brandService.GetAllBrands(brandName: "", storeId: scope.DefaultStoreId ?? "", showHidden: true), + var bytes = await exportManager.Export(await brandService.GetAllBrands(brandName: "", storeId: scope.DefaultStoreId ?? "", customer: null, showHidden: true), HttpContext.RequestAborted); return File(bytes, "text/xls", "brands.xlsx"); } diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseDiscountController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseDiscountController.cs index 10fcac061b..7be4c882a1 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseDiscountController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseDiscountController.cs @@ -551,7 +551,7 @@ public async Task BrandAddPopupList(DataSourceRequest command, DiscountModel.AddBrandToDiscountModel model, [FromServices] IBrandService brandService) { var brands = await brandService.GetAllBrands(model.SearchBrandName, - await scope.GetDefaultStoreIdAsync() ?? "", command.Page - 1, command.PageSize, true); + await scope.GetDefaultStoreIdAsync() ?? "", customer: null, command.Page - 1, command.PageSize, true); return Json(new DataSourceResult { Data = brands.Select(x => x.ToModel()), Total = brands.TotalCount }); } diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseSearchController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseSearchController.cs index 8cf33750aa..44c0fc5679 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseSearchController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseSearchController.cs @@ -2,6 +2,8 @@ using Grand.Business.Core.Interfaces.Catalog.Categories; using Grand.Business.Core.Interfaces.Catalog.Collections; using Grand.Domain.Admin; +using Grand.Domain.Customers; +using Grand.Infrastructure; using Grand.Web.Common.Controllers; using Grand.Web.Common.DataSource; using Grand.Web.Common.Helpers; @@ -13,11 +15,14 @@ public abstract class BaseSearchController( ICategoryService categoryService, IBrandService brandService, ICollectionService collectionService, - AdminSearchSettings adminSearchSettings) + AdminSearchSettings adminSearchSettings, + IContextAccessor contextAccessor) : BaseController { protected virtual string PickerStoreId => ""; + protected Customer CurrentCustomer => contextAccessor.WorkContext.CurrentCustomer; + [HttpGet] public virtual async Task Category(string categoryId, DataSourceRequestFilter model) { @@ -55,6 +60,7 @@ public virtual async Task Brand(string brandId, DataSourceRequest var brands = await brandService.GetAllBrands( model.GetNameFilterValue(), storeId: PickerStoreId, + customer: CurrentCustomer, pageSize: adminSearchSettings.BrandSizeLimit); var gridModel = await DataSourceResultHelper.GetSearchResult(brandId, brands, brand => Task.FromResult(brand.Name)); diff --git a/src/Web/Grand.Web.Store/Controllers/SearchController.cs b/src/Web/Grand.Web.Store/Controllers/SearchController.cs index 47220b73f1..98db238a71 100644 --- a/src/Web/Grand.Web.Store/Controllers/SearchController.cs +++ b/src/Web/Grand.Web.Store/Controllers/SearchController.cs @@ -20,7 +20,7 @@ public class SearchController( ICollectionService collectionService, AdminSearchSettings adminSearchSettings, IContextAccessor contextAccessor) - : BaseSearchController(categoryService, brandService, collectionService, adminSearchSettings) + : BaseSearchController(categoryService, brandService, collectionService, adminSearchSettings, contextAccessor) { - protected override string PickerStoreId => contextAccessor.WorkContext.CurrentCustomer.StaffStoreId; + protected override string PickerStoreId => CurrentCustomer.StaffStoreId; } diff --git a/src/Web/Grand.Web.Vendor/Controllers/SearchController.cs b/src/Web/Grand.Web.Vendor/Controllers/SearchController.cs index 93b12fbe9c..ab9cd4674d 100644 --- a/src/Web/Grand.Web.Vendor/Controllers/SearchController.cs +++ b/src/Web/Grand.Web.Vendor/Controllers/SearchController.cs @@ -2,6 +2,7 @@ using Grand.Business.Core.Interfaces.Catalog.Categories; using Grand.Business.Core.Interfaces.Catalog.Collections; using Grand.Domain.Admin; +using Grand.Infrastructure; using Grand.Web.AdminShared.Controllers; using Grand.Web.Common.Filters; using Grand.Web.Vendor.Extensions; @@ -17,7 +18,8 @@ public class SearchController( ICategoryService categoryService, IBrandService brandService, ICollectionService collectionService, - AdminSearchSettings adminSearchSettings) - : BaseSearchController(categoryService, brandService, collectionService, adminSearchSettings) + AdminSearchSettings adminSearchSettings, + IContextAccessor contextAccessor) + : BaseSearchController(categoryService, brandService, collectionService, adminSearchSettings, contextAccessor) { } diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetBrandAllHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetBrandAllHandler.cs index 1fbe7fdc33..f49caf5232 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetBrandAllHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetBrandAllHandler.cs @@ -47,7 +47,7 @@ private async Task> PrepareBrands(GetBrandAll request, BrandLis request.Command.PageSize = _catalogSettings.MaxCatalogPageSize; var model = new List(); - var brands = await _brandService.GetAllBrands(brandName: "", storeId: request.Store.Id, + var brands = await _brandService.GetAllBrands(brandName: "", storeId: request.Store.Id, customer: request.Customer, pageIndex: request.Command.PageNumber - 1, pageSize: request.Command.PageSize ); diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageBrandsHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageBrandsHandler.cs index 7d253d9d98..03db61ad19 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageBrandsHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageBrandsHandler.cs @@ -41,7 +41,7 @@ public async Task> Handle(GetHomepageBrands request, Cancellat var model = await _cacheBase.GetAsync(brandsCacheKey, async () => { var modelBrands = new List(); - var allBrands = await _brandService.GetAllBrands(brandName: "", storeId: request.Store.Id); + var allBrands = await _brandService.GetAllBrands(brandName: "", storeId: request.Store.Id, customer: request.Customer); foreach (var x in allBrands.Where(x => x.ShowOnHomePage)) { var brandModel = x.ToModel(request.Language); diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetMenuHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetMenuHandler.cs index 43822a64c2..86380ff46b 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetMenuHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetMenuHandler.cs @@ -80,7 +80,7 @@ public async Task Handle(GetMenu request, CancellationToken cancellat request.Language.Id, request.Store.Id); var cachedBrandModel = await _cacheBase.GetAsync(brandCacheKey, async () => - (await _brandService.GetAllBrands(brandName: "", storeId: request.Store.Id)) + (await _brandService.GetAllBrands(brandName: "", storeId: request.Store.Id, customer: request.Customer)) .Where(x => x.IncludeInMenu) .Select(t => new MenuModel.MenuBrandModel { Id = t.Id, diff --git a/src/Web/Grand.Web/Features/Handlers/Common/GetSitemapHandler.cs b/src/Web/Grand.Web/Features/Handlers/Common/GetSitemapHandler.cs index e5307c9054..0422c6076d 100644 --- a/src/Web/Grand.Web/Features/Handlers/Common/GetSitemapHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Common/GetSitemapHandler.cs @@ -85,7 +85,7 @@ public async Task Handle(GetSitemap request, CancellationToken can //collections if (_commonSettings.SitemapIncludeBrands) { - var brands = await _brandService.GetAllBrands(brandName: "", storeId: request.Store.Id); + var brands = await _brandService.GetAllBrands(brandName: "", storeId: request.Store.Id, customer: request.Customer); model.Brands = brands.Select(x => x.ToModel(request.Language)).ToList(); } From 9959df8a7bb8b70e31fba756112f411d88389fee Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 08:19:58 +0200 Subject: [PATCH 02/33] Pass the customer and store explicitly when listing collections --- .../Services/Collections/CollectionService.cs | 16 +++++------ .../CollectionImportDataObject.cs | 2 +- .../Catalog/Collections/ICollectionService.cs | 8 +++++- .../Collections/CollectionServiceTests.cs | 28 +++++++++++++++---- .../CollectionImportDataObjectTests.cs | 6 +--- .../BaseCollectionControllerTests.cs | 4 +-- .../BaseDiscountControllerTests.cs | 8 +++--- .../Controllers/BaseSearchControllerTests.cs | 8 +++--- .../Controllers/SearchController.cs | 2 +- .../Controllers/BaseCollectionController.cs | 4 +-- .../Controllers/BaseDiscountController.cs | 2 +- .../Controllers/BaseSearchController.cs | 1 + .../Services/CollectionViewModelService.cs | 2 +- .../Catalog/GetCollectionAllHandler.cs | 2 +- .../GetCollectionFeaturedProductsHandler.cs | 3 +- .../Catalog/GetCollectionNavigationHandler.cs | 2 +- .../Catalog/GetHomepageCollectionsHandler.cs | 2 +- .../Handlers/Catalog/GetMenuHandler.cs | 2 +- .../Handlers/Catalog/GetSearchHandler.cs | 3 +- 19 files changed, 64 insertions(+), 41 deletions(-) diff --git a/src/Business/Grand.Business.Catalog/Services/Collections/CollectionService.cs b/src/Business/Grand.Business.Catalog/Services/Collections/CollectionService.cs index 88721efdae..acdda4b370 100644 --- a/src/Business/Grand.Business.Catalog/Services/Collections/CollectionService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Collections/CollectionService.cs @@ -4,7 +4,6 @@ using Grand.Domain; using Grand.Domain.Catalog; using Grand.Domain.Customers; -using Grand.Infrastructure; using Grand.Infrastructure.Caching; using Grand.Infrastructure.Caching.Constants; using Grand.Infrastructure.Configuration; @@ -25,13 +24,11 @@ public class CollectionService : ICollectionService /// public CollectionService(ICacheBase cacheBase, IRepository collectionRepository, - IContextAccessor contextAccessor, IMediator mediator, IAclService aclService, AccessControlConfig accessControlConfig) { _cacheBase = cacheBase; _collectionRepository = collectionRepository; - _contextAccessor = contextAccessor; _mediator = mediator; _aclService = aclService; _accessControlConfig = accessControlConfig; @@ -42,7 +39,6 @@ public CollectionService(ICacheBase cacheBase, #region Fields private readonly IRepository _collectionRepository; - private readonly IContextAccessor _contextAccessor; private readonly IMediator _mediator; private readonly ICacheBase _cacheBase; private readonly IAclService _aclService; @@ -57,12 +53,14 @@ public CollectionService(ICacheBase cacheBase, /// /// Collection name /// Store ident + /// Customer whose groups limit the result (not read when showHidden is true) /// Page index /// Page size /// A value that indicates if it should shows hidden records /// Collections public virtual async Task> GetAllCollections(string collectionName, string storeId, + Customer customer, int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false) @@ -81,7 +79,7 @@ public virtual async Task> GetAllCollections(string colle if (!showHidden && !_accessControlConfig.IgnoreAcl) { //Limited to customer groups rules - var allowedCustomerGroupsIds = _contextAccessor.WorkContext.CurrentCustomer.GetCustomerGroupIds(); + var allowedCustomerGroupsIds = customer.GetCustomerGroupIds(); query = from p in query where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x)) select p; @@ -102,9 +100,12 @@ public virtual async Task> GetAllCollections(string colle /// /// Gets all featured products for collections displayed on the home page /// + /// Customer whose groups limit the result (not read when showHidden is true) + /// Store whose limitations apply /// A value that indicates if it should shows hidden records /// Collections - public virtual async Task> GetAllCollectionFeaturedProductsOnHomePage(bool showHidden = false) + public virtual async Task> GetAllCollectionFeaturedProductsOnHomePage(Customer customer, string storeId, + bool showHidden = false) { var query = _collectionRepository.Table.Where(x => x.Published && x.FeaturedProductsOnHomePage) .OrderBy(x => x.DisplayOrder); @@ -112,8 +113,7 @@ public virtual async Task> GetAllCollectionFeaturedProductsOnH var collections = await _collectionRepository.ToListAsync(query); if (!showHidden) collections = collections - .Where(c => _aclService.Authorize(c, _contextAccessor.WorkContext.CurrentCustomer) && - _aclService.Authorize(c, _contextAccessor.StoreContext.CurrentStore.Id)) + .Where(c => _aclService.Authorize(c, customer) && _aclService.Authorize(c, storeId)) .ToList(); return collections; } diff --git a/src/Business/Grand.Business.Catalog/Services/ExportImport/CollectionImportDataObject.cs b/src/Business/Grand.Business.Catalog/Services/ExportImport/CollectionImportDataObject.cs index 13439ab62f..955130bf75 100644 --- a/src/Business/Grand.Business.Catalog/Services/ExportImport/CollectionImportDataObject.cs +++ b/src/Business/Grand.Business.Catalog/Services/ExportImport/CollectionImportDataObject.cs @@ -149,7 +149,7 @@ private static string Key(CollectionDto dto) if (string.IsNullOrEmpty(dto.Name)) return (null, false, null); - var candidates = await _collectionService.GetAllCollections(dto.Name, "", showHidden: true); + var candidates = await _collectionService.GetAllCollections(dto.Name, "", customer: null, showHidden: true); var (found, error) = ImportRows.SingleByName(candidates, c => c.Name, dto.Name, "collection"); return (found, found != null, error); } diff --git a/src/Business/Grand.Business.Core/Interfaces/Catalog/Collections/ICollectionService.cs b/src/Business/Grand.Business.Core/Interfaces/Catalog/Collections/ICollectionService.cs index fa1c18d637..3f43b1ad34 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Catalog/Collections/ICollectionService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Catalog/Collections/ICollectionService.cs @@ -1,5 +1,6 @@ using Grand.Domain; using Grand.Domain.Catalog; +using Grand.Domain.Customers; namespace Grand.Business.Core.Interfaces.Catalog.Collections; @@ -13,12 +14,14 @@ public interface ICollectionService /// /// Collection name /// Store ident + /// Customer whose groups limit the result (not read when showHidden is true) /// Page index /// Page size /// A value that indicates if it should shows hidden records /// Collections Task> GetAllCollections(string collectionName, string storeId, + Customer customer, int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false); @@ -26,9 +29,12 @@ Task> GetAllCollections(string collectionName, /// /// Gets all collections products displayed on the home page /// + /// Customer whose groups limit the result (not read when showHidden is true) + /// Store whose limitations apply /// A value that indicates if it should shows hidden records /// Collections - Task> GetAllCollectionFeaturedProductsOnHomePage(bool showHidden = false); + Task> GetAllCollectionFeaturedProductsOnHomePage(Customer customer, string storeId, + bool showHidden = false); /// /// Gets an existing collection by id diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Collections/CollectionServiceTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Collections/CollectionServiceTests.cs index 352fc287ef..89b11f8025 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Collections/CollectionServiceTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Collections/CollectionServiceTests.cs @@ -3,7 +3,7 @@ using Grand.Business.Core.Interfaces.Common.Security; using Grand.Data; using Grand.Domain.Catalog; -using Grand.Infrastructure; +using Grand.Domain.Customers; using Grand.Infrastructure.Caching; using Grand.Infrastructure.Configuration; using Grand.Infrastructure.Events; @@ -22,22 +22,40 @@ public class CollectionServiceTests private Mock _mediatorMock; private Mock> _repositoryMock; private CatalogSettings _settings; - private Mock _workContextMock; [TestInitialize] public void Init() { _cacheMock = new Mock(); _repositoryMock = new Mock>(); - _workContextMock = new Mock(); _mediatorMock = new Mock(); _aclMock = new Mock(); _settings = new CatalogSettings(); - _collectionService = new CollectionService(_cacheMock.Object, _repositoryMock.Object, _workContextMock.Object - , _mediatorMock.Object, _aclMock.Object, new AccessControlConfig()); + _collectionService = new CollectionService(_cacheMock.Object, _repositoryMock.Object, _mediatorMock.Object, _aclMock.Object, new AccessControlConfig()); } + [TestMethod] + public async Task GetAllCollectionFeaturedProductsOnHomePage_AuthorizesAgainstGivenCustomerAndStore() + { + //Arrange + var collection = new Collection { Id = "a", Published = true, FeaturedProductsOnHomePage = true }; + var customer = new Customer(); + _repositoryMock.Setup(r => r.Table).Returns(new List { collection }.AsQueryable()); + _repositoryMock.Setup(r => r.ToListAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync((IQueryable q, CancellationToken _) => q.ToList()); + _aclMock.Setup(a => a.Authorize(collection, customer)).Returns(true); + _aclMock.Setup(a => a.Authorize(collection, "store1")).Returns(true); + + //Act + var inStore1 = await _collectionService.GetAllCollectionFeaturedProductsOnHomePage(customer, "store1"); + var inStore2 = await _collectionService.GetAllCollectionFeaturedProductsOnHomePage(customer, "store2"); + + //Assert + Assert.HasCount(1, inStore1); + Assert.IsEmpty(inStore2); + } + [TestMethod] public async Task DeleteCollection_ValidArguments_InvokeMethods() { diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/CollectionImportDataObjectTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/CollectionImportDataObjectTests.cs index 4fb9eef77b..abbc677751 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/CollectionImportDataObjectTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/CollectionImportDataObjectTests.cs @@ -44,7 +44,6 @@ public class CollectionImportDataObjectTests private IRepository _repository; private Mock _slugServiceMock; - private Mock _workContextMock; private ISeNameService _seNameService; private SecurityConfig _securityConfig; private Mock> _loggerMock; @@ -60,14 +59,11 @@ public void Init() _collectionLayoutServiceMock = new Mock(); _slugServiceMock = new Mock(); _languageServiceMock = new Mock(); - _workContextMock = new Mock(); - _workContextMock.Setup(c => c.StoreContext.CurrentStore).Returns(() => new Store { Id = "" }); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => new Customer()); _mediatorMock = new Mock(); _cacheBase = new MemoryCacheBase(MemoryCacheTest.Get(), _mediatorMock.Object, new CacheConfig { DefaultCacheTimeMinutes = 1 }); - _collectionService = new CollectionService(_cacheBase, _repository, _workContextMock.Object, + _collectionService = new CollectionService(_cacheBase, _repository, _mediatorMock.Object, new AclService(new AccessControlConfig()), new AccessControlConfig()); _seNameService = new SeNameService(_slugServiceMock.Object, _languageServiceMock.Object, new SeoSettings()); _collectionImportDataObject = new CollectionImportDataObject(_collectionService, _pictureServiceMock.Object, diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCollectionControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCollectionControllerTests.cs index 6aba71c60a..9979a18faa 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCollectionControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCollectionControllerTests.cs @@ -123,7 +123,7 @@ public async Task ListPost_ForcesScopeDefaultStoreIdOntoSearchModel() { _scopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); _collectionServiceMock - .Setup(c => c.GetAllCollections(It.IsAny(), "store-1", 0, 10, true)) + .Setup(c => c.GetAllCollections(It.IsAny(), "store-1", null, 0, 10, true)) .ReturnsAsync(new Grand.Domain.PagedList(new List(), 0, 10)); var model = new CollectionListModel { SearchStoreId = "attacker-supplied-store" }; @@ -137,7 +137,7 @@ public async Task ListPost_GlobalScope_LeavesSubmittedSearchStoreIdUntouched() { _scopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); _collectionServiceMock - .Setup(c => c.GetAllCollections(It.IsAny(), "admin-submitted-store", 0, 10, true)) + .Setup(c => c.GetAllCollections(It.IsAny(), "admin-submitted-store", null, 0, 10, true)) .ReturnsAsync(new Grand.Domain.PagedList(new List(), 0, 10)); var model = new CollectionListModel { SearchStoreId = "admin-submitted-store" }; diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseDiscountControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseDiscountControllerTests.cs index 01188050c3..db45a097d0 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseDiscountControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseDiscountControllerTests.cs @@ -613,13 +613,13 @@ public async Task CollectionAddPopupList_GlobalScope_PassesEmptyStoreId() { _scope.Setup(x => x.GetDefaultStoreIdAsync()).ReturnsAsync((string?)null); var collectionService = new Mock(); - collectionService.Setup(x => x.GetAllCollections(null, "", 0, 10, true)) + collectionService.Setup(x => x.GetAllCollections(null, "", null, 0, 10, true)) .ReturnsAsync(new PagedList(new List(), 0, 10, 0)); await _sut.CollectionAddPopupList(new DataSourceRequest { Page = 1, PageSize = 10 }, new DiscountModel.AddCollectionToDiscountModel(), collectionService.Object); - collectionService.Verify(x => x.GetAllCollections(null, "", 0, 10, true), Times.Once); + collectionService.Verify(x => x.GetAllCollections(null, "", null, 0, 10, true), Times.Once); } [TestMethod] @@ -627,13 +627,13 @@ public async Task CollectionAddPopupList_StoreScope_PassesScopedStoreId() { _scope.Setup(x => x.GetDefaultStoreIdAsync()).ReturnsAsync("store-a"); var collectionService = new Mock(); - collectionService.Setup(x => x.GetAllCollections(null, "store-a", 0, 10, true)) + collectionService.Setup(x => x.GetAllCollections(null, "store-a", null, 0, 10, true)) .ReturnsAsync(new PagedList(new List(), 0, 10, 0)); await _sut.CollectionAddPopupList(new DataSourceRequest { Page = 1, PageSize = 10 }, new DiscountModel.AddCollectionToDiscountModel(), collectionService.Object); - collectionService.Verify(x => x.GetAllCollections(null, "store-a", 0, 10, true), Times.Once); + collectionService.Verify(x => x.GetAllCollections(null, "store-a", null, 0, 10, true), Times.Once); } [TestMethod] diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseSearchControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseSearchControllerTests.cs index c2a3027e51..e6469895cb 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseSearchControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseSearchControllerTests.cs @@ -114,7 +114,7 @@ public async Task Collection_DefaultPickerStoreId_PassesEmptyStoreIdToService() { var controller = CreateController(""); _collectionServiceMock - .Setup(s => s.GetAllCollections(null, "", 0, 10, false)) + .Setup(s => s.GetAllCollections(null, "", _customer, 0, 10, false)) .ReturnsAsync(new PagedList([new Collection { Id = "col-1", Name = "Summer" }], 0, 10)); var result = await controller.Collection(null, EmptyFilter()); @@ -122,7 +122,7 @@ public async Task Collection_DefaultPickerStoreId_PassesEmptyStoreIdToService() var rows = ReadRows(result); Assert.AreEqual(1, rows.Length); Assert.AreEqual("Summer", rows[0].Name); - _collectionServiceMock.Verify(s => s.GetAllCollections(null, "", 0, 10, false), Times.Once); + _collectionServiceMock.Verify(s => s.GetAllCollections(null, "", _customer, 0, 10, false), Times.Once); } [TestMethod] @@ -130,12 +130,12 @@ public async Task Collection_OverriddenPickerStoreId_PassesStoreIdToService() { var controller = CreateController("store-1"); _collectionServiceMock - .Setup(s => s.GetAllCollections(null, "store-1", 0, 10, false)) + .Setup(s => s.GetAllCollections(null, "store-1", _customer, 0, 10, false)) .ReturnsAsync(new PagedList([], 0, 10)); await controller.Collection(null, EmptyFilter()); - _collectionServiceMock.Verify(s => s.GetAllCollections(null, "store-1", 0, 10, false), Times.Once); + _collectionServiceMock.Verify(s => s.GetAllCollections(null, "store-1", _customer, 0, 10, false), Times.Once); } [TestMethod] diff --git a/src/Web/Grand.Web.Admin/Controllers/SearchController.cs b/src/Web/Grand.Web.Admin/Controllers/SearchController.cs index 972bf4bf66..a7a3298ad9 100644 --- a/src/Web/Grand.Web.Admin/Controllers/SearchController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/SearchController.cs @@ -109,7 +109,7 @@ public async Task Index(string searchTerm, FoundMenuItem[] foundM if (result.Count < _adminSearchSettings.MaxSearchResultsCount && _adminSearchSettings.SearchInCollections) { - var collections = await _collectionService.GetAllCollections(searchTerm, storeId: "", + var collections = await _collectionService.GetAllCollections(searchTerm, storeId: "", customer: null, pageSize: _adminSearchSettings.MaxSearchResultsCount - result.Count, showHidden: true); foreach (var collection in collections) result.Add(new Tuple(new diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseCollectionController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseCollectionController.cs index 8846ae9f25..1392dae666 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseCollectionController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseCollectionController.cs @@ -76,7 +76,7 @@ public async Task List(DataSourceRequest command, CollectionListM { if (scope.DefaultStoreId is not null) model.SearchStoreId = scope.DefaultStoreId; var collections = await collectionService.GetAllCollections(model.SearchCollectionName, - model.SearchStoreId, command.Page - 1, command.PageSize, true); + model.SearchStoreId, customer: null, command.Page - 1, command.PageSize, true); var gridModel = new DataSourceResult { Data = collections.Select(x => x.ToModel()), Total = collections.TotalCount @@ -252,7 +252,7 @@ public async Task ExportXlsx([FromServices] IExportManager CollectionAddPopupList(DataSourceRequest comman DiscountModel.AddCollectionToDiscountModel model, [FromServices] ICollectionService collectionService) { var collections = await collectionService.GetAllCollections(model.SearchCollectionName, - await scope.GetDefaultStoreIdAsync() ?? "", command.Page - 1, command.PageSize, true); + await scope.GetDefaultStoreIdAsync() ?? "", customer: null, command.Page - 1, command.PageSize, true); return Json(new DataSourceResult { Data = collections.Select(x => x.ToModel()), Total = collections.TotalCount }); } diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseSearchController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseSearchController.cs index 44c0fc5679..8c45b435c5 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseSearchController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseSearchController.cs @@ -45,6 +45,7 @@ public virtual async Task Collection(string collectionId, DataSou var collections = await collectionService.GetAllCollections( collectionName: model.GetNameFilterValue(), storeId: PickerStoreId, + customer: CurrentCustomer, pageIndex: 0, pageSize: adminSearchSettings.CollectionSizeLimit, showHidden: false diff --git a/src/Web/Grand.Web.AdminShared/Services/CollectionViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CollectionViewModelService.cs index c116f72c96..e476f450f7 100644 --- a/src/Web/Grand.Web.AdminShared/Services/CollectionViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/CollectionViewModelService.cs @@ -181,7 +181,7 @@ public virtual async Task DeleteCollection(Collection collection) //collections model.AvailableCollections.Add(new SelectListItem { Text = _translationService.GetResource("Admin.Common.All"), Value = " " }); - foreach (var m in await _collectionService.GetAllCollections(collectionName: "", storeId: "", + foreach (var m in await _collectionService.GetAllCollections(collectionName: "", storeId: "", customer: null, showHidden: true)) model.AvailableCollections.Add(new SelectListItem { Text = m.Name, Value = m.Id }); diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetCollectionAllHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetCollectionAllHandler.cs index ba4c3912d5..b4e08ceace 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetCollectionAllHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetCollectionAllHandler.cs @@ -48,7 +48,7 @@ private async Task> PrepareCollectionAll(GetCollectionAll request.Command.PageSize = _catalogSettings.MaxCatalogPageSize; var model = new List(); - var collections = await _collectionService.GetAllCollections(collectionName: "", storeId: request.Store.Id, + var collections = await _collectionService.GetAllCollections(collectionName: "", storeId: request.Store.Id, customer: request.Customer, pageIndex: request.Command.PageNumber - 1, pageSize: request.Command.PageSize); diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetCollectionFeaturedProductsHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetCollectionFeaturedProductsHandler.cs index 810727fe60..09a35cd967 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetCollectionFeaturedProductsHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetCollectionFeaturedProductsHandler.cs @@ -57,7 +57,8 @@ public async Task> Handle(GetCollectionFeaturedProducts r var model = await _cacheBase.GetAsync(collectionCacheKey, async () => { var collectionList = new List(); - var collectionmodel = await _collectionService.GetAllCollectionFeaturedProductsOnHomePage(); + var collectionmodel = await _collectionService.GetAllCollectionFeaturedProductsOnHomePage(customer: request.Customer, + storeId: request.Store.Id); foreach (var x in collectionmodel) { var colModel = x.ToModel(request.Language); diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetCollectionNavigationHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetCollectionNavigationHandler.cs index 8aeb585935..05603b31df 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetCollectionNavigationHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetCollectionNavigationHandler.cs @@ -36,7 +36,7 @@ public async Task Handle(GetCollectionNavigation requ var currentCollection = await _collectionService.GetCollectionById(request.CurrentCollectionId); var collections = await _collectionService.GetAllCollections(collectionName: "", pageSize: _catalogSettings.CollectionsBlockItemsToDisplay, - storeId: request.Store.Id); + storeId: request.Store.Id, customer: request.Customer); var model = new CollectionNavigationModel { TotalCollections = collections.TotalCount }; diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageCollectionsHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageCollectionsHandler.cs index 4f5123e794..4a76e200c6 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageCollectionsHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageCollectionsHandler.cs @@ -43,7 +43,7 @@ public async Task> Handle(GetHomepageCollections request, var model = await _cacheBase.GetAsync(collectionsCacheKey, async () => { var modelCollect = new List(); - var allcollections = await _collectionService.GetAllCollections(collectionName: "", storeId: request.Store.Id); + var allcollections = await _collectionService.GetAllCollections(collectionName: "", storeId: request.Store.Id, customer: request.Customer); foreach (var x in allcollections.Where(x => x.ShowOnHomePage)) { var _model = x.ToModel(request.Language); diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetMenuHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetMenuHandler.cs index 86380ff46b..79aec36fb5 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetMenuHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetMenuHandler.cs @@ -95,7 +95,7 @@ public async Task Handle(GetMenu request, CancellationToken cancellat request.Language.Id, request.Store.Id); var cachedCollectionModel = await _cacheBase.GetAsync(collectionCacheKey, async () => - (await _collectionService.GetAllCollections(collectionName: "", storeId: request.Store.Id)) + (await _collectionService.GetAllCollections(collectionName: "", storeId: request.Store.Id, customer: request.Customer)) .Where(x => x.IncludeInMenu) .Select(t => new MenuModel.MenuCollectionModel { Id = t.Id, diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetSearchHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetSearchHandler.cs index 0c2921b6d1..61adf39db7 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetSearchHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetSearchHandler.cs @@ -136,7 +136,8 @@ public async Task Handle(GetSearch request, CancellationToken cance }); } - var collections = await _collectionService.GetAllCollections(collectionName: "", storeId: request.Store.Id, pageSize: 100); + var collections = await _collectionService.GetAllCollections(collectionName: "", storeId: request.Store.Id, customer: request.Customer, + pageSize: 100); if (collections.Any()) { request.Model.AvailableCollections.Add(new SelectListItem { From ff002d80abe7c7fc9e1d1d53993e617240a15271 Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 08:30:27 +0200 Subject: [PATCH 03/33] Pass the customer and store explicitly to category queries --- .../Services/Categories/CategoryService.cs | 97 +++++++++++-------- .../ExportImport/CategoryImportDataObject.cs | 2 +- .../Catalog/Categories/ICategoryService.cs | 35 +++++-- .../Common/GetSitemapXMLCommandHandler.cs | 2 +- .../CategoryServiceCacheKeyTests.cs | 23 ++--- .../Categories/CategoryServiceDbTests.cs | 73 +++++++++++--- .../Categories/CategoryServiceTests.cs | 11 +-- .../Categories/CategoryServiceTreeTests.cs | 19 ++-- .../CategoryImportDataObjectTests.cs | 6 +- .../GetSitemapXmlCommandHandlerTests.cs | 6 +- .../BaseDiscountControllerTests.cs | 8 +- .../Controllers/BaseSearchControllerTests.cs | 8 +- .../SearchControllerAttributeTests.cs | 7 +- .../Controllers/SearchController.cs | 1 + .../Controllers/BaseCategoryController.cs | 2 +- .../Controllers/BaseDiscountController.cs | 3 +- .../Controllers/BaseSearchController.cs | 1 + .../Services/CategoryViewModelService.cs | 1 + .../Services/ProductViewModelService.cs | 5 +- .../Handlers/Catalog/GetCategoryAllHandler.cs | 1 + .../GetCategoryFeaturedProductsHandler.cs | 3 +- .../Handlers/Catalog/GetCategoryHandler.cs | 6 +- .../Catalog/GetCategorySimpleHandler.cs | 6 +- .../Catalog/GetChildCategoryIdsHandler.cs | 3 +- .../Catalog/GetHomepageCategoryHandler.cs | 3 +- .../Handlers/Catalog/GetMenuHandler.cs | 2 +- .../Handlers/Catalog/GetSearchBoxHandler.cs | 3 +- .../Handlers/Catalog/GetSearchHandler.cs | 6 +- .../Handlers/Common/GetSitemapHandler.cs | 3 +- .../Products/GetProductDetailsPageHandler.cs | 4 +- 30 files changed, 216 insertions(+), 134 deletions(-) diff --git a/src/Business/Grand.Business.Catalog/Services/Categories/CategoryService.cs b/src/Business/Grand.Business.Catalog/Services/Categories/CategoryService.cs index 744e3bed10..09b4f45310 100644 --- a/src/Business/Grand.Business.Catalog/Services/Categories/CategoryService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Categories/CategoryService.cs @@ -5,8 +5,6 @@ using Grand.Domain; using Grand.Domain.Catalog; using Grand.Domain.Customers; -using Grand.Domain.Stores; -using Grand.Infrastructure; using Grand.Infrastructure.Caching; using Grand.Infrastructure.Caching.Constants; using Grand.Infrastructure.Configuration; @@ -27,20 +25,17 @@ public class CategoryService : ICategoryService /// /// Cache manager /// Category repository - /// Work context /// Mediator /// ACL service /// public CategoryService(ICacheBase cacheBase, IRepository categoryRepository, - IContextAccessor contextAccessor, IMediator mediator, IAclService aclService, AccessControlConfig accessControlConfig) { _cacheBase = cacheBase; _categoryRepository = categoryRepository; - _contextAccessor = contextAccessor; _mediator = mediator; _aclService = aclService; _accessControlConfig = accessControlConfig; @@ -51,15 +46,11 @@ public CategoryService(ICacheBase cacheBase, #region Fields private readonly IRepository _categoryRepository; - private readonly IContextAccessor _contextAccessor; private readonly IMediator _mediator; private readonly ICacheBase _cacheBase; private readonly IAclService _aclService; private readonly AccessControlConfig _accessControlConfig; - private Customer CurrentCustomer => _contextAccessor.WorkContext.CurrentCustomer; - private Store CurrentStore => _contextAccessor.StoreContext.CurrentStore; - #endregion #region Methods @@ -70,12 +61,13 @@ public CategoryService(ICacheBase cacheBase, /// Parent Id /// Category name /// Store ident + /// Customer whose groups limit the result (not read when showHidden is true) /// Page index /// Page size /// A value that indicates if it should shows hidden records /// Categories public virtual async Task> GetAllCategories(string parentId, string categoryName, - string storeId, + string storeId, Customer customer, int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false) { var query = from c in _categoryRepository.Table @@ -95,7 +87,7 @@ public virtual async Task> GetAllCategories(string parentId if (!showHidden && !_accessControlConfig.IgnoreAcl) { //Limited to customer group (access control list) - var allowedCustomerGroupsIds = CurrentCustomer.GetCustomerGroupIds(); + var allowedCustomerGroupsIds = customer.GetCustomerGroupIds(); query = from p in query where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x)) select p; @@ -117,8 +109,10 @@ public virtual async Task> GetAllCategories(string parentId /// /// Gets categories for menu /// + /// Customer whose groups limit the result + /// Store whose limitations apply /// Categories - public virtual async Task> GetMenuCategories() + public virtual async Task> GetMenuCategories(Customer customer, string storeId) { var query = from c in _categoryRepository.Table select c; @@ -128,12 +122,12 @@ public virtual async Task> GetMenuCategories() switch (_accessControlConfig.IgnoreAcl) { case true when - string.IsNullOrEmpty(CurrentStore.Id) || _accessControlConfig.IgnoreStoreLimitations: + string.IsNullOrEmpty(storeId) || _accessControlConfig.IgnoreStoreLimitations: return await _categoryRepository.ToListAsync(query); case false: { //Limited to customer group (access control list) - var allowedCustomerGroupsIds = CurrentCustomer.GetCustomerGroupIds(); + var allowedCustomerGroupsIds = customer.GetCustomerGroupIds(); query = from p in query where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x)) select p; @@ -141,10 +135,10 @@ public virtual async Task> GetMenuCategories() } } - if (!string.IsNullOrEmpty(CurrentStore.Id) && !_accessControlConfig.IgnoreStoreLimitations) + if (!string.IsNullOrEmpty(storeId) && !_accessControlConfig.IgnoreStoreLimitations) //Limited to stores rule query = from p in query - where !p.LimitedToStores || p.Stores.Contains(CurrentStore.Id) + where !p.LimitedToStores || p.Stores.Contains(storeId) select p; return await _categoryRepository.ToListAsync(query); } @@ -152,14 +146,17 @@ public virtual async Task> GetMenuCategories() /// /// Gets all categories filtered by parent category identifier /// + /// Customer whose groups limit the result and key the cache + /// Store whose limitations apply /// Parent category identifier /// A value that indicates if it should shows hidden records /// A value that indicates if we should load all child levels /// Categories - public virtual async Task> GetAllCategoriesByParentCategoryId(string parentCategoryId = "", + public virtual async Task> GetAllCategoriesByParentCategoryId(Customer customer, string storeId, + string parentCategoryId = "", bool showHidden = false, bool includeAllLevels = false) { - var categories = await GetChildCategories(parentCategoryId, showHidden); + var categories = await GetChildCategories(parentCategoryId, showHidden, customer, storeId); if (!includeAllLevels) return categories; //walk the tree outside the cache: every level resolves through its own cache entry, so no @@ -167,18 +164,19 @@ public virtual async Task> GetAllCategoriesByParentCategoryId(st //once per ancestor var allLevels = new List(categories); foreach (var category in categories) - allLevels.AddRange(await GetAllCategoriesByParentCategoryId(category.Id, showHidden, true)); + allLevels.AddRange(await GetAllCategoriesByParentCategoryId(customer, storeId, category.Id, showHidden, true)); return allLevels; } /// - /// Gets the direct children of a category, filtered by the current customer's groups and store + /// Gets the direct children of a category, filtered by the given customer's groups and store /// - private async Task> GetChildCategories(string parentCategoryId, bool showHidden) + private async Task> GetChildCategories(string parentCategoryId, bool showHidden, Customer customer, + string storeId) { var key = string.Format(CacheKey.CATEGORIES_BY_PARENT_CATEGORY_ID_KEY, parentCategoryId, showHidden, - string.Join(",", CurrentCustomer.GetCustomerGroupIds()), CurrentStore.Id); + string.Join(",", customer.GetCustomerGroupIds()), storeId); return await _cacheBase.GetAsync(key, () => { var query = _categoryRepository.Table.Where(c => c.ParentCategoryId == parentCategoryId); @@ -190,7 +188,7 @@ private async Task> GetChildCategories(string parentCategoryId, if (!_accessControlConfig.IgnoreAcl) { //Limited to customer groups rules - var allowedCustomerGroupsIds = CurrentCustomer.GetCustomerGroupIds(); + var allowedCustomerGroupsIds = customer.GetCustomerGroupIds(); query = from p in query where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x)) select p; @@ -199,7 +197,7 @@ private async Task> GetChildCategories(string parentCategoryId, if (!_accessControlConfig.IgnoreStoreLimitations) //Limited to stores rules query = from p in query - where !p.LimitedToStores || p.Stores.Contains(CurrentStore.Id) + where !p.LimitedToStores || p.Stores.Contains(storeId) select p; } @@ -210,9 +208,12 @@ private async Task> GetChildCategories(string parentCategoryId, /// /// Gets all categories that should be displayed on the home page /// + /// Customer whose groups limit the result (not read when showHidden is true) + /// Store whose limitations apply /// A value that indicates if it should shows hidden records /// Categories - public virtual async Task> GetAllCategoriesDisplayedOnHomePage(bool showHidden = false) + public virtual async Task> GetAllCategoriesDisplayedOnHomePage(Customer customer, string storeId, + bool showHidden = false) { var query = _categoryRepository.Table .Where(x => x.Published && x.ShowOnHomePage) @@ -220,8 +221,8 @@ public virtual async Task> GetAllCategoriesDisplayedOnHomePage(b var categories = await _categoryRepository.ToListAsync(query); if (!showHidden) categories = categories - .Where(c => _aclService.Authorize(c, CurrentCustomer) && - _aclService.Authorize(c, CurrentStore.Id)) + .Where(c => _aclService.Authorize(c, customer) && + _aclService.Authorize(c, storeId)) .ToList(); return categories; @@ -230,9 +231,12 @@ public virtual async Task> GetAllCategoriesDisplayedOnHomePage(b /// /// Gets all featured products from categories that should be displayed on the home page /// + /// Customer whose groups limit the result (not read when showHidden is true) + /// Store whose limitations apply /// A value that indicates if it should shows hidden records /// Categories - public virtual async Task> GetAllCategoriesFeaturedProductsOnHomePage(bool showHidden = false) + public virtual async Task> GetAllCategoriesFeaturedProductsOnHomePage(Customer customer, string storeId, + bool showHidden = false) { var query = _categoryRepository.Table .Where(x => x.Published && x.FeaturedProductsOnHomePage) @@ -241,8 +245,8 @@ public virtual async Task> GetAllCategoriesFeaturedProductsOnHom var categories = await _categoryRepository.ToListAsync(query); if (!showHidden) categories = categories - .Where(c => _aclService.Authorize(c, CurrentCustomer) && - _aclService.Authorize(c, CurrentStore.Id)) + .Where(c => _aclService.Authorize(c, customer) && + _aclService.Authorize(c, storeId)) .ToList(); return categories; } @@ -250,16 +254,18 @@ public virtual async Task> GetAllCategoriesFeaturedProductsOnHom /// /// Gets all categories displayed in the search box /// + /// Customer whose groups limit the result + /// Store whose limitations apply /// Categories - public virtual async Task> GetAllCategoriesSearchBox() + public virtual async Task> GetAllCategoriesSearchBox(Customer customer, string storeId) { var query = _categoryRepository.Table .Where(x => x.Published && x.ShowOnSearchBox) .OrderBy(x => x.SearchBoxDisplayOrder); var categories = (await _categoryRepository.ToListAsync(query)) - .Where(c => _aclService.Authorize(c, CurrentCustomer) && - _aclService.Authorize(c, CurrentStore.Id)) + .Where(c => _aclService.Authorize(c, customer) && + _aclService.Authorize(c, storeId)) .ToList(); return categories; @@ -269,9 +275,12 @@ public virtual async Task> GetAllCategoriesSearchBox() /// Get category breadcrumb /// /// Category + /// Customer whose groups limit the result (not read when showHidden is true) + /// Store whose limitations apply /// A value that indicates if it should shows hidden records /// Category breadcrumb - public virtual async Task> GetCategoryBreadCrumb(Category category, bool showHidden = false) + public virtual async Task> GetCategoryBreadCrumb(Category category, Customer customer, string storeId, + bool showHidden = false) { var result = new List(); @@ -281,8 +290,8 @@ public virtual async Task> GetCategoryBreadCrumb(Category catego while (category != null && //not null (showHidden || category.Published) && //published (showHidden || - _aclService.Authorize(category, CurrentCustomer)) && //limited to customer groups - (showHidden || _aclService.Authorize(category, CurrentStore.Id)) && //limited to store + _aclService.Authorize(category, customer)) && //limited to customer groups + (showHidden || _aclService.Authorize(category, storeId)) && //limited to store !alreadyProcessedCategoryIds.Contains(category.Id)) { result.Add(category); @@ -301,10 +310,12 @@ public virtual async Task> GetCategoryBreadCrumb(Category catego /// /// Category /// All categories + /// Customer whose groups limit the result (not read when showHidden is true) + /// Store whose limitations apply /// A value that indicates if it should shows hidden records /// Category breadcrumb public virtual IList GetCategoryBreadCrumb(Category category, IList allCategories, - bool showHidden = false) + Customer customer, string storeId, bool showHidden = false) { var result = new List(); @@ -314,8 +325,8 @@ public virtual IList GetCategoryBreadCrumb(Category category, IList GetFormattedBreadCrumb(Category category, stri { var result = string.Empty; - var breadcrumb = await GetCategoryBreadCrumb(category, true); + var breadcrumb = await GetCategoryBreadCrumb(category, customer: null, storeId: "", showHidden: true); for (var i = 0; i <= breadcrumb.Count - 1; i++) { var categoryName = breadcrumb[i].GetTranslation(x => x.Name, languageId); @@ -370,7 +381,7 @@ public virtual string GetFormattedBreadCrumb(Category category, { var result = string.Empty; - var breadcrumb = GetCategoryBreadCrumb(category, allCategories, true); + var breadcrumb = GetCategoryBreadCrumb(category, allCategories, customer: null, storeId: "", showHidden: true); for (var i = 0; i <= breadcrumb.Count - 1; i++) { var categoryName = breadcrumb[i].GetTranslation(x => x.Name, languageId); @@ -468,7 +479,9 @@ public virtual async Task DeleteCategory(Category category) ArgumentNullException.ThrowIfNull(category); //reset a "Parent category" property of all child subcategories - var subcategories = await GetAllCategoriesByParentCategoryId(category.Id, true); + var subcategories = await _categoryRepository.ToListAsync(_categoryRepository.Table + .Where(c => c.ParentCategoryId == category.Id) + .OrderBy(c => c.DisplayOrder)); foreach (var subcategory in subcategories) { subcategory.ParentCategoryId = ""; diff --git a/src/Business/Grand.Business.Catalog/Services/ExportImport/CategoryImportDataObject.cs b/src/Business/Grand.Business.Catalog/Services/ExportImport/CategoryImportDataObject.cs index eb54d94e05..ec55123978 100644 --- a/src/Business/Grand.Business.Catalog/Services/ExportImport/CategoryImportDataObject.cs +++ b/src/Business/Grand.Business.Catalog/Services/ExportImport/CategoryImportDataObject.cs @@ -155,7 +155,7 @@ private static string Key(CategoryDto dto) return (null, false, null); var parentId = string.IsNullOrEmpty(dto.ParentCategoryId) ? null : dto.ParentCategoryId; - var candidates = await _categoryService.GetAllCategories(parentId, dto.Name, "", showHidden: true); + var candidates = await _categoryService.GetAllCategories(parentId, dto.Name, "", customer: null, showHidden: true); var (found, error) = ImportRows.SingleByName(candidates, c => c.Name, dto.Name, "category"); return (found, found != null, error); } diff --git a/src/Business/Grand.Business.Core/Interfaces/Catalog/Categories/ICategoryService.cs b/src/Business/Grand.Business.Core/Interfaces/Catalog/Categories/ICategoryService.cs index 5015edb1a3..19b20eb730 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Catalog/Categories/ICategoryService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Catalog/Categories/ICategoryService.cs @@ -1,5 +1,6 @@ using Grand.Domain; using Grand.Domain.Catalog; +using Grand.Domain.Customers; namespace Grand.Business.Core.Interfaces.Catalog.Categories; @@ -11,64 +12,82 @@ public interface ICategoryService /// /// Category name /// Store id + /// Customer whose groups limit the result (not read when showHidden is true) /// Page index /// Page size /// A value that indicates if it should shows hidden records /// Categories - Task> GetAllCategories(string parentId, string categoryName, string storeId, + Task> GetAllCategories(string parentId, string categoryName, string storeId, Customer customer, int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false); /// /// Gets all categories /// + /// Customer whose groups limit the result + /// Store whose limitations apply /// Categories - Task> GetMenuCategories(); + Task> GetMenuCategories(Customer customer, string storeId); /// /// Gets all categories by parent category /// + /// Customer whose groups limit the result and key the cache + /// Store whose limitations apply /// Parent category id /// A value that indicates if it should shows hidden records /// A value indicates if we should load all child levels /// Categories - Task> GetAllCategoriesByParentCategoryId(string parentCategoryId = "", + Task> GetAllCategoriesByParentCategoryId(Customer customer, string storeId, string parentCategoryId = "", bool showHidden = false, bool includeAllLevels = false); /// /// Gets all categories displayed on the home page /// + /// Customer whose groups limit the result (not read when showHidden is true) + /// Store whose limitations apply /// A value that indicates if it should shows hidden records /// Categories - Task> GetAllCategoriesDisplayedOnHomePage(bool showHidden = false); + Task> GetAllCategoriesDisplayedOnHomePage(Customer customer, string storeId, bool showHidden = false); /// /// Gets all categories displayed that should be visible on the home page, in featured products section /// + /// Customer whose groups limit the result (not read when showHidden is true) + /// Store whose limitations apply /// A value that indicates if it should shows hidden records - Task> GetAllCategoriesFeaturedProductsOnHomePage(bool showHidden = false); + Task> GetAllCategoriesFeaturedProductsOnHomePage(Customer customer, string storeId, + bool showHidden = false); /// /// Gets all categories that should be displayed in search box /// + /// Customer whose groups limit the result + /// Store whose limitations apply /// Categories - Task> GetAllCategoriesSearchBox(); + Task> GetAllCategoriesSearchBox(Customer customer, string storeId); /// /// Get category breadcrumb /// /// Category + /// Customer whose groups limit the result (not read when showHidden is true) + /// Store whose limitations apply /// A value that indicates if it should shows hidden records /// Category breadcrumb - Task> GetCategoryBreadCrumb(Category category, bool showHidden = false); + Task> GetCategoryBreadCrumb(Category category, Customer customer, string storeId, + bool showHidden = false); /// /// Get category breadcrumb /// /// Category /// All categories + /// Customer whose groups limit the result (not read when showHidden is true) + /// Store whose limitations apply /// A value that indicates if it should shows hidden records /// Category breadcrumb - IList GetCategoryBreadCrumb(Category category, IList allCategories, bool showHidden = false); + IList GetCategoryBreadCrumb(Category category, IList allCategories, Customer customer, + string storeId, bool showHidden = false); /// /// Get formatted category breadcrumb diff --git a/src/Business/Grand.Business.Messages/Commands/Handlers/Common/GetSitemapXMLCommandHandler.cs b/src/Business/Grand.Business.Messages/Commands/Handlers/Common/GetSitemapXMLCommandHandler.cs index 393fccf7d3..01eb015671 100644 --- a/src/Business/Grand.Business.Messages/Commands/Handlers/Common/GetSitemapXMLCommandHandler.cs +++ b/src/Business/Grand.Business.Messages/Commands/Handlers/Common/GetSitemapXMLCommandHandler.cs @@ -196,7 +196,7 @@ private async Task> GenerateUrls(Language language, Store stor //categories if (_commonSettings.SitemapIncludeCategories) { - var categories = (await _categoryService.GetAllCategories(null, "", store.Id, showHidden: true)) + var categories = (await _categoryService.GetAllCategories(null, "", store.Id, customer: null, showHidden: true)) .Where(c => c.Published && IsVisibleToGuests(c)) .ToLookup(c => c.ParentCategoryId ?? ""); sitemapUrls.AddRange(await GetCategoryUrls("", categories, language)); diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceCacheKeyTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceCacheKeyTests.cs index 8a8ab6ecf7..481cab086d 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceCacheKeyTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceCacheKeyTests.cs @@ -28,20 +28,15 @@ public class CategoryServiceCacheKeyTests private MemoryCacheBase _cacheBase; private IRepository _categoryRepository; private CategoryService _categoryService; - private Customer _currentCustomer; [TestInitialize] public void InitializeTests() { _categoryRepository = new MongoDBRepositoryTest(); - _currentCustomer = new Customer(); - var contextAccessor = new Mock(); - contextAccessor.Setup(c => c.StoreContext.CurrentStore).Returns(() => new Store { Id = "store" }); - contextAccessor.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => _currentCustomer); var mediator = new Mock(); _cacheBase = new MemoryCacheBase(MemoryCacheTest.Get(), mediator.Object, new CacheConfig { DefaultCacheTimeMinutes = 1 }); - _categoryService = new CategoryService(_cacheBase, _categoryRepository, contextAccessor.Object, + _categoryService = new CategoryService(_cacheBase, _categoryRepository, mediator.Object, new AclService(new AccessControlConfig()), new AccessControlConfig()); } @@ -58,11 +53,11 @@ public async Task TwoCustomersOfTheSameGroup_ShareTheCachedTree() { await _categoryService.InsertCategory(new Category { ParentCategoryId = "root", Published = true }); - _currentCustomer = WithGroups("customer-1", "group-a"); - var first = await _categoryService.GetAllCategoriesByParentCategoryId("root"); + var first = await _categoryService.GetAllCategoriesByParentCategoryId(WithGroups("customer-1", "group-a"), + "store", "root"); - _currentCustomer = WithGroups("customer-2", "group-a"); - var second = await _categoryService.GetAllCategoriesByParentCategoryId("root"); + var second = await _categoryService.GetAllCategoriesByParentCategoryId(WithGroups("customer-2", "group-a"), + "store", "root"); Assert.AreSame(first, second, "same groups must resolve to the same cache entry"); } @@ -72,11 +67,11 @@ public async Task CustomersOfDifferentGroups_DoNotShareTheCachedTree() { await _categoryService.InsertCategory(new Category { ParentCategoryId = "root", Published = true }); - _currentCustomer = WithGroups("customer-1", "group-a"); - var first = await _categoryService.GetAllCategoriesByParentCategoryId("root"); + var first = await _categoryService.GetAllCategoriesByParentCategoryId(WithGroups("customer-1", "group-a"), + "store", "root"); - _currentCustomer = WithGroups("customer-2", "group-b"); - var second = await _categoryService.GetAllCategoriesByParentCategoryId("root"); + var second = await _categoryService.GetAllCategoriesByParentCategoryId(WithGroups("customer-2", "group-b"), + "store", "root"); Assert.AreNotSame(first, second, "the ACL filter differs per group, so the entries must differ"); } diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceDbTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceDbTests.cs index 03583a4025..871714fc1f 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceDbTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceDbTests.cs @@ -25,24 +25,67 @@ public class CategoryServiceDbTests private CategoryService _categoryService; private Mock _mediatorMock; private CatalogSettings _settings; - private Mock _workContextMock; [TestInitialize] public void InitializeTests() { _categoryRepository = new MongoDBRepositoryTest(); - _workContextMock = new Mock(); - _workContextMock.Setup(c => c.StoreContext.CurrentStore).Returns(() => new Store { Id = "" }); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => new Customer()); _mediatorMock = new Mock(); _aclServiceMock = new AclService(new AccessControlConfig()); _settings = new CatalogSettings(); _cacheBase = new MemoryCacheBase(MemoryCacheTest.Get(), _mediatorMock.Object, new CacheConfig { DefaultCacheTimeMinutes = 1 }); - _categoryService = new CategoryService(_cacheBase, _categoryRepository, _workContextMock.Object, + _categoryService = new CategoryService(_cacheBase, _categoryRepository, _mediatorMock.Object, _aclServiceMock, new AccessControlConfig()); } + [TestMethod] + public async Task GetMenuCategories_UsesGivenCustomerAndStore() + { + //Arrange + await _categoryRepository.InsertAsync(new Category { Id = "open", Published = true, IncludeInMenu = true }); + await _categoryRepository.InsertAsync(new Category { + Id = "vip", Published = true, IncludeInMenu = true, + LimitedToGroups = true, CustomerGroups = new List { "vip" } + }); + await _categoryRepository.InsertAsync(new Category { + Id = "s2", Published = true, IncludeInMenu = true, + LimitedToStores = true, Stores = new List { "store2" } + }); + var vipCustomer = new Customer(); + vipCustomer.Groups.Add("vip"); + + //Act + var guestStore1 = await _categoryService.GetMenuCategories(new Customer(), "store1"); + var vipStore2 = await _categoryService.GetMenuCategories(vipCustomer, "store2"); + + //Assert + CollectionAssert.AreEquivalent(new[] { "open" }, guestStore1.Select(c => c.Id).ToArray()); + CollectionAssert.AreEquivalent(new[] { "open", "vip", "s2" }, vipStore2.Select(c => c.Id).ToArray()); + } + + [TestMethod] + public async Task GetCategoryBreadCrumb_StopsAtCategoryHiddenFromGivenCustomer() + { + //Arrange + var parent = new Category { + Id = "p", Published = true, LimitedToGroups = true, CustomerGroups = new List { "vip" } + }; + var child = new Category { Id = "c", Published = true, ParentCategoryId = "p" }; + await _categoryRepository.InsertAsync(parent); + await _categoryRepository.InsertAsync(child); + var vipCustomer = new Customer(); + vipCustomer.Groups.Add("vip"); + + //Act + var guest = await _categoryService.GetCategoryBreadCrumb(child, new Customer(), ""); + var vip = await _categoryService.GetCategoryBreadCrumb(child, vipCustomer, ""); + + //Assert + Assert.HasCount(1, guest); + Assert.HasCount(2, vip); + } + [TestMethod] public async Task InsertCategory() { @@ -85,7 +128,7 @@ public async Task GetCategoryBreadCrumb() allCategory.ToList().ForEach(x => _categoryService.InsertCategory(x).GetAwaiter().GetResult()); var category = new Category { Id = "6", ParentCategoryId = "3", Published = true }; await _categoryService.InsertCategory(category); - var result = await _categoryService.GetCategoryBreadCrumb(category); + var result = await _categoryService.GetCategoryBreadCrumb(category, new Customer(), ""); Assert.HasCount(2, result); Assert.IsTrue(result.Any(c => c.Id.Equals("6"))); Assert.IsTrue(result.Any(c => c.Id.Equals("3"))); @@ -97,7 +140,7 @@ public void GetCategoryBreadCrumb_AllCategory() var allCategory = GetMockCategoryList(); allCategory.ToList().ForEach(x => _categoryService.InsertCategory(x).GetAwaiter().GetResult()); var category = new Category { ParentCategoryId = "3" }; - var result = _categoryService.GetCategoryBreadCrumb(category, allCategory); + var result = _categoryService.GetCategoryBreadCrumb(category, allCategory, new Customer(), ""); Assert.IsEmpty(result); } @@ -106,7 +149,7 @@ public async Task GetAllCategories() { var allCategory = GetMockCategoryList(); allCategory.ToList().ForEach(x => _categoryService.InsertCategory(x).GetAwaiter().GetResult()); - var result = await _categoryService.GetAllCategories(parentId: null, categoryName: "", storeId: ""); + var result = await _categoryService.GetAllCategories(parentId: null, categoryName: "", storeId: "", customer: new Customer()); Assert.HasCount(5, result); } @@ -115,7 +158,7 @@ public async Task GetMenuCategories() { var allCategory = GetMockCategoryList(); allCategory.ToList().ForEach(x => _categoryService.InsertCategory(x).GetAwaiter().GetResult()); - var result = await _categoryService.GetMenuCategories(); + var result = await _categoryService.GetMenuCategories(new Customer(), ""); Assert.HasCount(1, result); } @@ -124,7 +167,7 @@ public async Task GetAllCategoriesByParentCategoryId() { var allCategory = GetMockCategoryList(); allCategory.ToList().ForEach(x => _categoryService.InsertCategory(x).GetAwaiter().GetResult()); - var result = await _categoryService.GetAllCategoriesByParentCategoryId("5"); + var result = await _categoryService.GetAllCategoriesByParentCategoryId(new Customer(), "", "5"); Assert.HasCount(1, result); } @@ -133,7 +176,7 @@ public async Task GetAllCategoriesDisplayedOnHomePage() { var allCategory = GetMockCategoryList(); allCategory.ToList().ForEach(x => _categoryService.InsertCategory(x).GetAwaiter().GetResult()); - var result = await _categoryService.GetAllCategoriesDisplayedOnHomePage(); + var result = await _categoryService.GetAllCategoriesDisplayedOnHomePage(new Customer(), ""); Assert.HasCount(2, result); } @@ -142,7 +185,7 @@ public async Task GetAllCategoriesFeaturedProductsOnHomePage() { var allCategory = GetMockCategoryList(); allCategory.ToList().ForEach(x => _categoryService.InsertCategory(x).GetAwaiter().GetResult()); - var result = await _categoryService.GetAllCategoriesFeaturedProductsOnHomePage(); + var result = await _categoryService.GetAllCategoriesFeaturedProductsOnHomePage(new Customer(), ""); Assert.HasCount(2, result); } @@ -151,7 +194,7 @@ public async Task GetAllCategoriesSearchBox() { var allCategory = GetMockCategoryList(); allCategory.ToList().ForEach(x => _categoryService.InsertCategory(x).GetAwaiter().GetResult()); - var result = await _categoryService.GetAllCategoriesSearchBox(); + var result = await _categoryService.GetAllCategoriesSearchBox(new Customer(), ""); Assert.HasCount(2, result); } @@ -180,7 +223,7 @@ public async Task GetCategoryBreadCrumb_ShouldReturnTwoElement() allCategory.ToList().ForEach(x => _categoryService.InsertCategory(x).GetAwaiter().GetResult()); var category = new Category { Id = "6", ParentCategoryId = "3", Published = true }; await _categoryService.InsertCategory(category); - var result = _categoryService.GetCategoryBreadCrumb(category, allCategory); + var result = _categoryService.GetCategoryBreadCrumb(category, allCategory, new Customer(), ""); Assert.HasCount(2, result); Assert.IsTrue(result.Any(c => c.Id.Equals("6"))); Assert.IsTrue(result.Any(c => c.Id.Equals("3"))); @@ -192,7 +235,7 @@ public void GetCategoryBreadCrumb_ShouldReturnThreeElement() var allCategory = GetMockCategoryList(); allCategory.ToList().ForEach(x => _categoryService.InsertCategory(x).GetAwaiter().GetResult()); var category = new Category { Id = "6", ParentCategoryId = "1", Published = true }; - var result = _categoryService.GetCategoryBreadCrumb(category, allCategory); + var result = _categoryService.GetCategoryBreadCrumb(category, allCategory, new Customer(), ""); Assert.HasCount(3, result); Assert.IsTrue(result.Any(c => c.Id.Equals("6"))); Assert.IsTrue(result.Any(c => c.Id.Equals("1"))); diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceTests.cs index 109237f306..c530695d69 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceTests.cs @@ -42,7 +42,6 @@ public void Init() _aclServiceMock = new Mock(); _settings = new CatalogSettings(); _categoryService = new CategoryService(_casheManagerMock.Object, _categoryRepositoryMock.Object, - _workContextMock.Object, _mediatorMock.Object, _aclServiceMock.Object, new AccessControlConfig()); _productCategoryService = new ProductCategoryService(_productRepositoryMock.Object, _casheManagerMock.Object, _workContextMock.Object, _mediatorMock.Object, new AccessControlConfig()); @@ -88,7 +87,7 @@ public void GetCategoryBreadCrumb_ShouldReturnEmptyList() var category = new Category { ParentCategoryId = "3" }; _aclServiceMock.Setup(a => a.Authorize(It.IsAny(), It.IsAny())).Returns(() => true); _aclServiceMock.Setup(a => a.Authorize(It.IsAny(), It.IsAny())).Returns(() => true); - var result = _categoryService.GetCategoryBreadCrumb(category, allCategory); + var result = _categoryService.GetCategoryBreadCrumb(category, allCategory, new Customer { Id = "" }, ""); Assert.IsEmpty(result); } @@ -97,11 +96,9 @@ public void GetCategoryBreadCrumb_ShouldReturnTwoElement() { var allCategory = GetMockCategoryList(); var category = new Category { Id = "6", ParentCategoryId = "3", Published = true }; - _workContextMock.Setup(c => c.StoreContext.CurrentStore).Returns(() => new Store { Id = "" }); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => new Customer { Id = "" }); _aclServiceMock.Setup(a => a.Authorize(It.IsAny(), It.IsAny())).Returns(() => true); _aclServiceMock.Setup(a => a.Authorize(It.IsAny(), It.IsAny())).Returns(() => true); - var result = _categoryService.GetCategoryBreadCrumb(category, allCategory); + var result = _categoryService.GetCategoryBreadCrumb(category, allCategory, new Customer { Id = "" }, ""); Assert.HasCount(2, result); Assert.IsTrue(result.Any(c => c.Id.Equals("6"))); Assert.IsTrue(result.Any(c => c.Id.Equals("3"))); @@ -112,11 +109,9 @@ public void GetCategoryBreadCrumb_ShouldReturnThreeElement() { var allCategory = GetMockCategoryList(); var category = new Category { Id = "6", ParentCategoryId = "1", Published = true }; - _workContextMock.Setup(c => c.StoreContext.CurrentStore).Returns(() => new Store { Id = "" }); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => new Customer { Id = "" }); _aclServiceMock.Setup(a => a.Authorize(It.IsAny(), It.IsAny())).Returns(() => true); _aclServiceMock.Setup(a => a.Authorize(It.IsAny(), It.IsAny())).Returns(() => true); - var result = _categoryService.GetCategoryBreadCrumb(category, allCategory); + var result = _categoryService.GetCategoryBreadCrumb(category, allCategory, new Customer { Id = "" }, ""); Assert.HasCount(3, result); Assert.IsTrue(result.Any(c => c.Id.Equals("6"))); Assert.IsTrue(result.Any(c => c.Id.Equals("1"))); diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceTreeTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceTreeTests.cs index 49c552eaf2..29cb87fc26 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceTreeTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceTreeTests.cs @@ -26,20 +26,19 @@ public class CategoryServiceTreeTests private CategoryService _categoryService; private IRepository _categoryRepository; + private Customer _customer; + [TestInitialize] public void InitializeTests() { _categoryRepository = new MongoDBRepositoryTest(); - //one customer for the whole fixture: a fresh instance per access would give every call its + //one customer for the whole fixture: a fresh instance per call would give every call its //own id, hence its own cache key, and the caching under test would never be exercised - var customer = new Customer { Id = "customer" }; - var contextAccessor = new Mock(); - contextAccessor.Setup(c => c.StoreContext.CurrentStore).Returns(() => new Store { Id = "store" }); - contextAccessor.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => customer); + _customer = new Customer { Id = "customer" }; var mediator = new Mock(); var cacheBase = new MemoryCacheBase(MemoryCacheTest.Get(), mediator.Object, new CacheConfig { DefaultCacheTimeMinutes = 1 }); - _categoryService = new CategoryService(cacheBase, _categoryRepository, contextAccessor.Object, + _categoryService = new CategoryService(cacheBase, _categoryRepository, mediator.Object, new AclService(new AccessControlConfig()), new AccessControlConfig()); } @@ -59,7 +58,7 @@ public async Task AllLevels_KeepsTheWholeLevelBeforeEachSubtree() { await GivenATwoLevelTree(); - var result = await _categoryService.GetAllCategoriesByParentCategoryId("root", includeAllLevels: true); + var result = await _categoryService.GetAllCategoriesByParentCategoryId(_customer, "store", "root", includeAllLevels: true); CollectionAssert.AreEqual(new[] { "a", "b", "a1", "b1" }, result.Select(x => x.Id).ToArray()); } @@ -69,7 +68,7 @@ public async Task WithoutAllLevels_ReturnsOnlyTheDirectChildren() { await GivenATwoLevelTree(); - var result = await _categoryService.GetAllCategoriesByParentCategoryId("root"); + var result = await _categoryService.GetAllCategoriesByParentCategoryId(_customer, "store", "root"); CollectionAssert.AreEqual(new[] { "a", "b" }, result.Select(x => x.Id).ToArray()); } @@ -83,9 +82,9 @@ public async Task AllLevels_HandsBackAListTheCallerOwns() { await GivenATwoLevelTree(); - var first = await _categoryService.GetAllCategoriesByParentCategoryId("root", includeAllLevels: true); + var first = await _categoryService.GetAllCategoriesByParentCategoryId(_customer, "store", "root", includeAllLevels: true); first.Clear(); - var second = await _categoryService.GetAllCategoriesByParentCategoryId("root", includeAllLevels: true); + var second = await _categoryService.GetAllCategoriesByParentCategoryId(_customer, "store", "root", includeAllLevels: true); Assert.HasCount(4, second); } diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/CategoryImportDataObjectTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/CategoryImportDataObjectTests.cs index 43fb3a9929..819a7bf06a 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/CategoryImportDataObjectTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/CategoryImportDataObjectTests.cs @@ -44,7 +44,6 @@ public class CategoryImportDataObjectTests private IRepository _repository; private Mock _slugServiceMock; - private Mock _workContextMock; private ISeNameService _seNameService; private SecurityConfig _securityConfig; private Mock> _loggerMock; @@ -60,14 +59,11 @@ public void Init() _categoryLayoutServiceMock = new Mock(); _slugServiceMock = new Mock(); _languageServiceMock = new Mock(); - _workContextMock = new Mock(); - _workContextMock.Setup(c => c.StoreContext.CurrentStore).Returns(() => new Store { Id = "" }); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => new Customer()); _mediatorMock = new Mock(); _cacheBase = new MemoryCacheBase(MemoryCacheTest.Get(), _mediatorMock.Object, new CacheConfig { DefaultCacheTimeMinutes = 1 }); - _categoryService = new CategoryService(_cacheBase, _repository, _workContextMock.Object, _mediatorMock.Object, + _categoryService = new CategoryService(_cacheBase, _repository, _mediatorMock.Object, new AclService(new AccessControlConfig()), new AccessControlConfig()); _seNameService = new SeNameService(_slugServiceMock.Object, _languageServiceMock.Object, new SeoSettings()); _categoryImportDataObject = new CategoryImportDataObject(_categoryService, _pictureServiceMock.Object, diff --git a/src/Tests/Grand.Business.Messages.Tests/Commands/GetSitemapXmlCommandHandlerTests.cs b/src/Tests/Grand.Business.Messages.Tests/Commands/GetSitemapXmlCommandHandlerTests.cs index 7c7b4b6cb6..4c5d55ca7c 100644 --- a/src/Tests/Grand.Business.Messages.Tests/Commands/GetSitemapXmlCommandHandlerTests.cs +++ b/src/Tests/Grand.Business.Messages.Tests/Commands/GetSitemapXmlCommandHandlerTests.cs @@ -60,7 +60,7 @@ public void Init() groupServiceMock.Setup(x => x.GetCustomerGroupBySystemName(SystemCustomerGroupNames.Guests)) .ReturnsAsync(new CustomerGroup { Id = GuestsGroupId, SystemName = SystemCustomerGroupNames.Guests }); - _categoryServiceMock.Setup(x => x.GetAllCategories(null, "", StoreId, 0, int.MaxValue, true)) + _categoryServiceMock.Setup(x => x.GetAllCategories(null, "", StoreId, null, 0, int.MaxValue, true)) .ReturnsAsync(new PagedList()); _brandServiceMock.Setup(x => x.GetAllBrands("", StoreId, null, 0, int.MaxValue, true)) .ReturnsAsync(new PagedList()); @@ -124,7 +124,7 @@ public async Task Handle_WithoutWorkContext_UsesOnlyExplicitlyScopedQueries() //Assert Assert.IsFalse(Locations(xml).Any(string.IsNullOrEmpty), "sitemap contains an empty "); - _categoryServiceMock.Verify(x => x.GetAllCategories(null, "", StoreId, 0, int.MaxValue, true), Times.Once); + _categoryServiceMock.Verify(x => x.GetAllCategories(null, "", StoreId, null, 0, int.MaxValue, true), Times.Once); _brandServiceMock.Verify(x => x.GetAllBrands("", StoreId, null, 0, int.MaxValue, true), Times.Once); _pageServiceMock.Verify(x => x.GetAllPages(StoreId, true), Times.Once); _mediatorMock.Verify(x => x.Send(It.Is(q => @@ -148,7 +148,7 @@ public async Task Handle_ListsOnlyWhatAGuestCanSee() }, new() { Id = "4", SystemName = "Draft", SeName = "draft-page", Published = false, IncludeInSitemap = true } }); - _categoryServiceMock.Setup(x => x.GetAllCategories(null, "", StoreId, 0, int.MaxValue, true)) + _categoryServiceMock.Setup(x => x.GetAllCategories(null, "", StoreId, null, 0, int.MaxValue, true)) .ReturnsAsync(new PagedList(new List { new() { Id = "c1", ParentCategoryId = "", SeName = "open-category", Published = true }, new() { Id = "c2", ParentCategoryId = "c1", SeName = "child-category", Published = true }, diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseDiscountControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseDiscountControllerTests.cs index db45a097d0..3ed0b458db 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseDiscountControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseDiscountControllerTests.cs @@ -449,13 +449,13 @@ public async Task CategoryAddPopupList_GlobalScope_PassesEmptyStoreId() { _scope.Setup(x => x.GetDefaultStoreIdAsync()).ReturnsAsync((string?)null); var categoryService = new Mock(); - categoryService.Setup(x => x.GetAllCategories(null, null, "", 0, 10, true)) + categoryService.Setup(x => x.GetAllCategories(null, null, "", null, 0, 10, true)) .ReturnsAsync(new PagedList(new List(), 0, 10, 0)); await _sut.CategoryAddPopupList(new DataSourceRequest { Page = 1, PageSize = 10 }, new DiscountModel.AddCategoryToDiscountModel(), categoryService.Object); - categoryService.Verify(x => x.GetAllCategories(null, null, "", 0, 10, true), Times.Once); + categoryService.Verify(x => x.GetAllCategories(null, null, "", null, 0, 10, true), Times.Once); } [TestMethod] @@ -463,13 +463,13 @@ public async Task CategoryAddPopupList_StoreScope_PassesScopedStoreId() { _scope.Setup(x => x.GetDefaultStoreIdAsync()).ReturnsAsync("store-a"); var categoryService = new Mock(); - categoryService.Setup(x => x.GetAllCategories(null, null, "store-a", 0, 10, true)) + categoryService.Setup(x => x.GetAllCategories(null, null, "store-a", null, 0, 10, true)) .ReturnsAsync(new PagedList(new List(), 0, 10, 0)); await _sut.CategoryAddPopupList(new DataSourceRequest { Page = 1, PageSize = 10 }, new DiscountModel.AddCategoryToDiscountModel(), categoryService.Object); - categoryService.Verify(x => x.GetAllCategories(null, null, "store-a", 0, 10, true), Times.Once); + categoryService.Verify(x => x.GetAllCategories(null, null, "store-a", null, 0, 10, true), Times.Once); } [TestMethod] diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseSearchControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseSearchControllerTests.cs index e6469895cb..6ca9a631cb 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseSearchControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseSearchControllerTests.cs @@ -83,7 +83,7 @@ public async Task Category_DefaultPickerStoreId_PassesEmptyStoreIdToService() { var controller = CreateController(""); _categoryServiceMock - .Setup(s => s.GetAllCategories(null, null, "", 0, 10, false)) + .Setup(s => s.GetAllCategories(null, null, "", _customer, 0, 10, false)) .ReturnsAsync(new PagedList([new Category { Id = "cat-1", Name = "Books" }], 0, 10)); _categoryServiceMock.Setup(s => s.GetFormattedBreadCrumb(It.IsAny(), ">>", "")) .ReturnsAsync("Books"); @@ -93,7 +93,7 @@ public async Task Category_DefaultPickerStoreId_PassesEmptyStoreIdToService() var rows = ReadRows(result); Assert.AreEqual(1, rows.Length); Assert.AreEqual("cat-1", rows[0].Id); - _categoryServiceMock.Verify(s => s.GetAllCategories(null, null, "", 0, 10, false), Times.Once); + _categoryServiceMock.Verify(s => s.GetAllCategories(null, null, "", _customer, 0, 10, false), Times.Once); } [TestMethod] @@ -101,12 +101,12 @@ public async Task Category_OverriddenPickerStoreId_PassesStoreIdToService() { var controller = CreateController("store-1"); _categoryServiceMock - .Setup(s => s.GetAllCategories(null, null, "store-1", 0, 10, false)) + .Setup(s => s.GetAllCategories(null, null, "store-1", _customer, 0, 10, false)) .ReturnsAsync(new PagedList([], 0, 10)); await controller.Category(null, EmptyFilter()); - _categoryServiceMock.Verify(s => s.GetAllCategories(null, null, "store-1", 0, 10, false), Times.Once); + _categoryServiceMock.Verify(s => s.GetAllCategories(null, null, "store-1", _customer, 0, 10, false), Times.Once); } [TestMethod] diff --git a/src/Tests/Grand.Web.Store.Tests/Controllers/SearchControllerAttributeTests.cs b/src/Tests/Grand.Web.Store.Tests/Controllers/SearchControllerAttributeTests.cs index 3b18c46ba1..69a0a46146 100644 --- a/src/Tests/Grand.Web.Store.Tests/Controllers/SearchControllerAttributeTests.cs +++ b/src/Tests/Grand.Web.Store.Tests/Controllers/SearchControllerAttributeTests.cs @@ -60,13 +60,14 @@ public void HasAuthorizeMenuAttribute() [TestMethod] public async System.Threading.Tasks.Task PickerStoreId_ReflectsCurrentCustomerStaffStoreId() { + var customer = new Customer { StaffStoreId = "store-42" }; var categoryServiceMock = new Mock(); categoryServiceMock - .Setup(s => s.GetAllCategories(null, null, "store-42", 0, It.IsAny(), false)) + .Setup(s => s.GetAllCategories(null, null, "store-42", customer, 0, It.IsAny(), false)) .ReturnsAsync(new Grand.Domain.PagedList([], 0, 10)); var workContextMock = new Mock(); - workContextMock.Setup(w => w.CurrentCustomer).Returns(new Customer { StaffStoreId = "store-42" }); + workContextMock.Setup(w => w.CurrentCustomer).Returns(customer); var contextAccessorMock = new Mock(); contextAccessorMock.Setup(c => c.WorkContext).Returns(workContextMock.Object); @@ -79,6 +80,6 @@ public async System.Threading.Tasks.Task PickerStoreId_ReflectsCurrentCustomerSt await controller.Category(null, new Grand.Web.Common.DataSource.DataSourceRequestFilter { Filters = [] }); - categoryServiceMock.Verify(s => s.GetAllCategories(null, null, "store-42", 0, 10, false), Times.Once); + categoryServiceMock.Verify(s => s.GetAllCategories(null, null, "store-42", customer, 0, 10, false), Times.Once); } } diff --git a/src/Web/Grand.Web.Admin/Controllers/SearchController.cs b/src/Web/Grand.Web.Admin/Controllers/SearchController.cs index a7a3298ad9..87b6968ceb 100644 --- a/src/Web/Grand.Web.Admin/Controllers/SearchController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/SearchController.cs @@ -96,6 +96,7 @@ public async Task Index(string searchTerm, FoundMenuItem[] foundM if (result.Count < _adminSearchSettings.MaxSearchResultsCount && _adminSearchSettings.SearchInCategories) { var categories = await _categoryService.GetAllCategories(parentId: null, categoryName: searchTerm, storeId: "", + customer: _contextAccessor.WorkContext.CurrentCustomer, pageSize: _adminSearchSettings.MaxSearchResultsCount - result.Count, showHidden: await _groupService.IsAdmin(_contextAccessor.WorkContext.CurrentCustomer)); foreach (var category in categories) diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseCategoryController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseCategoryController.cs index a8c2da8a9b..97a5ef373c 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseCategoryController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseCategoryController.cs @@ -219,7 +219,7 @@ public async Task ExportXlsx([FromServices] IExportManager CategoryAddPopupList(DataSourceRequest command, DiscountModel.AddCategoryToDiscountModel model, [FromServices] ICategoryService categoryService) { var categories = await categoryService.GetAllCategories(parentId: null, categoryName: model.SearchCategoryName, - storeId: await scope.GetDefaultStoreIdAsync() ?? "", pageIndex: command.Page - 1, pageSize: command.PageSize, showHidden: true); + storeId: await scope.GetDefaultStoreIdAsync() ?? "", customer: null, pageIndex: command.Page - 1, + pageSize: command.PageSize, showHidden: true); var items = new List(); foreach (var item in categories) { diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseSearchController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseSearchController.cs index 8c45b435c5..03b74e4b06 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseSearchController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseSearchController.cs @@ -30,6 +30,7 @@ public virtual async Task Category(string categoryId, DataSourceR parentId: null, categoryName: model.GetNameFilterValue(), storeId: PickerStoreId, + customer: CurrentCustomer, pageIndex: 0, pageSize: adminSearchSettings.CategorySizeLimit, showHidden: false diff --git a/src/Web/Grand.Web.AdminShared/Services/CategoryViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CategoryViewModelService.cs index d9f60512cf..260ca7970d 100644 --- a/src/Web/Grand.Web.AdminShared/Services/CategoryViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/CategoryViewModelService.cs @@ -79,6 +79,7 @@ public virtual async Task PrepareCategoryListModel(string sto parentId: null, categoryName: model.SearchCategoryName, storeId: model.SearchStoreId, + customer: null, pageSize: pageSize, pageIndex: pageIndex - 1, showHidden: true); diff --git a/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs index 9734f0709b..0c56ce2ce3 100644 --- a/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs @@ -2599,7 +2599,10 @@ protected virtual async Task UpdatePictureSeoNames(Product product) protected virtual async Task> GetChildCategoryIds(string parentCategoryId) { var categoriesIds = new List(); - var categories = await categoryService.GetAllCategoriesByParentCategoryId(parentCategoryId, true); + var categories = await categoryService.GetAllCategoriesByParentCategoryId( + customer: contextAccessor.WorkContext.CurrentCustomer, + storeId: contextAccessor.StoreContext.CurrentStore.Id, + parentCategoryId: parentCategoryId, showHidden: true); foreach (var category in categories) { categoriesIds.Add(category.Id); diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategoryAllHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategoryAllHandler.cs index 829f448134..9fbe433aed 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategoryAllHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategoryAllHandler.cs @@ -49,6 +49,7 @@ private async Task> PrepareCategories(GetCategoryAll request var model = new List(); var categories = await _categoryService.GetAllCategories(parentId: null, categoryName: "", storeId: request.Store.Id, + customer: request.Customer, pageIndex: request.Command.PageNumber - 1, pageSize: request.Command.PageSize ); diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategoryFeaturedProductsHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategoryFeaturedProductsHandler.cs index 404986e429..c80d555db9 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategoryFeaturedProductsHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategoryFeaturedProductsHandler.cs @@ -56,7 +56,8 @@ public async Task> Handle(GetCategoryFeaturedProducts reque var model = await _cacheBase.GetAsync(categoriesCacheKey, async () => { var catlistmodel = new List(); - foreach (var x in await _categoryService.GetAllCategoriesFeaturedProductsOnHomePage()) + foreach (var x in await _categoryService.GetAllCategoriesFeaturedProductsOnHomePage(customer: request.Customer, + storeId: request.Store.Id)) { var catModel = x.ToModel(request.Language); //prepare picture model diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategoryHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategoryHandler.cs index 88615fd485..4738e46434 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategoryHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategoryHandler.cs @@ -87,7 +87,8 @@ public async Task Handle(GetCategory request, CancellationToken c storeId, languageId); model.CategoryBreadcrumb = await _cacheBase.GetAsync(breadcrumbCacheKey, async () => - (await _categoryService.GetCategoryBreadCrumb(request.Category)) + (await _categoryService.GetCategoryBreadCrumb(request.Category, customer: request.Customer, + storeId: request.Store.Id)) .Select(catBr => new CategoryModel { Id = catBr.Id, Name = catBr.GetTranslation(x => x.Name, languageId), @@ -99,7 +100,8 @@ public async Task Handle(GetCategory request, CancellationToken c //subcategories var subCategories = new List(); - foreach (var x in (await _categoryService.GetAllCategoriesByParentCategoryId(request.Category.Id)).Where(x => + foreach (var x in (await _categoryService.GetAllCategoriesByParentCategoryId(customer: request.Customer, + storeId: request.Store.Id, parentCategoryId: request.Category.Id)).Where(x => !x.HideOnCatalog)) { var subCatModel = new CategoryModel.SubCategoryModel { diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategorySimpleHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategorySimpleHandler.cs index dc5e72f900..7c1d7c8d21 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategorySimpleHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategorySimpleHandler.cs @@ -65,7 +65,8 @@ private async Task> PrepareCategorySimpleModels(GetCat async Task PrepareCategories(string categoryId) { - var parentCategories = await _categoryService.GetAllCategories(categoryId, categoryName: "", storeId: request.Store.Id); + var parentCategories = await _categoryService.GetAllCategories(categoryId, categoryName: "", storeId: request.Store.Id, + customer: request.Customer); if (parentCategories.Any()) { categories.AddRange(parentCategories); @@ -79,7 +80,8 @@ async Task PrepareCategories(string categoryId) if (currentCategory != null) { var currentCategories = - await _categoryService.GetAllCategories(currentCategory.Id, categoryName: "", storeId: request.Store.Id); + await _categoryService.GetAllCategories(currentCategory.Id, categoryName: "", storeId: request.Store.Id, + customer: request.Customer); categories.AddRange(currentCategories); await PrepareCategories(currentCategory.ParentCategoryId); } diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetChildCategoryIdsHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetChildCategoryIdsHandler.cs index 5b50bbc1f1..cf366788a7 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetChildCategoryIdsHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetChildCategoryIdsHandler.cs @@ -32,7 +32,8 @@ private async Task> GetChildCategoryIds(GetChildCategoryIds request return await _cacheBase.GetAsync(cacheKey, async () => { var categoriesIds = new List(); - var categories = await _categoryService.GetAllCategoriesByParentCategoryId(request.ParentCategoryId); + var categories = await _categoryService.GetAllCategoriesByParentCategoryId(customer: request.Customer, + storeId: request.Store.Id, parentCategoryId: request.ParentCategoryId); foreach (var category in categories) { categoriesIds.Add(category.Id); diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageCategoryHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageCategoryHandler.cs index bcea875130..0c5d47c9a7 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageCategoryHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageCategoryHandler.cs @@ -46,7 +46,8 @@ public async Task> Handle(GetHomepageCategory request, Canc var model = await _cacheBase.GetAsync(categoriesCacheKey, async () => { var cat = new List(); - foreach (var x in await _categoryService.GetAllCategoriesDisplayedOnHomePage()) + foreach (var x in await _categoryService.GetAllCategoriesDisplayedOnHomePage(customer: request.Customer, + storeId: request.Store.Id)) { var catModel = x.ToModel(request.Language); //prepare picture model diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetMenuHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetMenuHandler.cs index 79aec36fb5..5e7024cb90 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetMenuHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetMenuHandler.cs @@ -62,7 +62,7 @@ public async Task Handle(GetMenu request, CancellationToken cancellat request.Store.Id, string.Join(",", request.Customer.GetCustomerGroupIds())); var cachedCategoriesModel = await _cacheBase.GetAsync(cacheKey, async () => await PrepareCategorySimpleModels( request.Language, - await _categoryService.GetMenuCategories())); + await _categoryService.GetMenuCategories(customer: request.Customer, storeId: request.Store.Id))); //top menu pages var now = DateTime.UtcNow; diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetSearchBoxHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetSearchBoxHandler.cs index 2a9bd5ad0d..7c0e40555d 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetSearchBoxHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetSearchBoxHandler.cs @@ -42,7 +42,8 @@ public async Task Handle(GetSearchBox request, CancellationToken return await _cacheBase.GetAsync(cacheKey, async () => { - var searchbocategories = await _categoryService.GetAllCategoriesSearchBox(); + var searchbocategories = await _categoryService.GetAllCategoriesSearchBox(customer: request.Customer, + storeId: request.Store.Id); var availableCategories = new List(); if (searchbocategories.Any()) diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetSearchHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetSearchHandler.cs index 61adf39db7..934c40a487 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetSearchHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetSearchHandler.cs @@ -99,12 +99,14 @@ public async Task Handle(GetSearch request, CancellationToken cance { var categoriesModel = new List(); //all categories - var allCategories = await _categoryService.GetAllCategories(parentId: null, categoryName: "", storeId: request.Store.Id, pageSize: 100); + var allCategories = await _categoryService.GetAllCategories(parentId: null, categoryName: "", storeId: request.Store.Id, + customer: request.Customer, pageSize: 100); foreach (var c in allCategories) { //generate full category name (breadcrumb) var categoryBreadcrumb = ""; - var breadcrumb = _categoryService.GetCategoryBreadCrumb(c, allCategories); + var breadcrumb = _categoryService.GetCategoryBreadCrumb(c, allCategories, customer: request.Customer, + storeId: request.Store.Id); for (var i = 0; i <= breadcrumb.Count - 1; i++) { categoryBreadcrumb += breadcrumb[i].GetTranslation(x => x.Name, request.Language.Id); diff --git a/src/Web/Grand.Web/Features/Handlers/Common/GetSitemapHandler.cs b/src/Web/Grand.Web/Features/Handlers/Common/GetSitemapHandler.cs index 0422c6076d..d2ff8074bf 100644 --- a/src/Web/Grand.Web/Features/Handlers/Common/GetSitemapHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Common/GetSitemapHandler.cs @@ -78,7 +78,8 @@ public async Task Handle(GetSitemap request, CancellationToken can //categories if (_commonSettings.SitemapIncludeCategories) { - var categories = await _categoryService.GetAllCategories(parentId: null, categoryName: "", storeId: request.Store.Id); + var categories = await _categoryService.GetAllCategories(parentId: null, categoryName: "", storeId: request.Store.Id, + customer: request.Customer); model.Categories = categories.Select(x => x.ToModel(request.Language)).ToList(); } diff --git a/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsPageHandler.cs b/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsPageHandler.cs index 6c52c308a1..d2c6462293 100644 --- a/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsPageHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsPageHandler.cs @@ -549,7 +549,9 @@ private async Task PrepareVendorBriefInfoModel(Product pro await _categoryService.GetCategoryById(productCategories.MinBy(x => x.DisplayOrder).CategoryId); if (category == null) return breadcrumbModel; - foreach (var catBr in await _categoryService.GetCategoryBreadCrumb(category)) + foreach (var catBr in await _categoryService.GetCategoryBreadCrumb(category, + customer: _contextAccessor.WorkContext.CurrentCustomer, + storeId: _contextAccessor.StoreContext.CurrentStore.Id)) breadcrumbModel.CategoryBreadcrumb.Add(new CategorySimpleModel { Id = catBr.Id, Name = catBr.GetTranslation(x => x.Name, _contextAccessor.WorkContext.WorkingLanguage.Id), From ceec7b9d6cf47b272335bd84e5ffa9343eed2431 Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 08:37:36 +0200 Subject: [PATCH 04/33] Pass the customer and store explicitly to product mapping queries --- .../Categories/ProductCategoryService.cs | 17 ++++----- .../Collections/ProductCollectionService.cs | 11 +++--- .../Categories/IProductCategoryService.cs | 7 ++-- .../Collections/IProductCollectionService.cs | 4 ++- .../Categories/CategoryServiceTests.cs | 4 +-- .../Categories/ProductCategoryServiceTests.cs | 33 +++++++++++------ .../ProductCollectionServiceTests.cs | 35 +++++++++++++------ .../Services/CategoryViewModelService.cs | 8 ++++- .../Services/CollectionViewModelService.cs | 1 + 9 files changed, 76 insertions(+), 44 deletions(-) diff --git a/src/Business/Grand.Business.Catalog/Services/Categories/ProductCategoryService.cs b/src/Business/Grand.Business.Catalog/Services/Categories/ProductCategoryService.cs index e29adae7e2..895b6cb58a 100644 --- a/src/Business/Grand.Business.Catalog/Services/Categories/ProductCategoryService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Categories/ProductCategoryService.cs @@ -4,7 +4,6 @@ using Grand.Domain; using Grand.Domain.Catalog; using Grand.Domain.Customers; -using Grand.Infrastructure; using Grand.Infrastructure.Caching; using Grand.Infrastructure.Caching.Constants; using Grand.Infrastructure.Configuration; @@ -19,17 +18,14 @@ public class ProductCategoryService : IProductCategoryService private readonly ICacheBase _cacheBase; private readonly IMediator _mediator; private readonly IRepository _productRepository; - private readonly IContextAccessor _contextAccessor; public ProductCategoryService( IRepository productRepository, ICacheBase cacheBase, - IContextAccessor contextAccessor, IMediator mediator, AccessControlConfig accessControlConfig) { _productRepository = productRepository; _cacheBase = cacheBase; - _contextAccessor = contextAccessor; _mediator = mediator; _accessControlConfig = accessControlConfig; } @@ -38,19 +34,21 @@ public ProductCategoryService( /// Gets product category mapping collection /// /// Category identifier + /// Customer whose groups limit the result and key the cache + /// Store whose limitations apply /// Page index /// Page size /// A value that indicates if it should shows hidden records /// Product a category mapping collection public virtual async Task> GetProductCategoriesByCategoryId(string categoryId, - int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false) + Customer customer, string storeId, int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false) { if (string.IsNullOrEmpty(categoryId)) return new PagedList(new List(), pageIndex, pageSize); var key = string.Format(CacheKey.PRODUCTCATEGORIES_ALLBYCATEGORYID_KEY, showHidden, categoryId, pageIndex, - pageSize, string.Join(",", _contextAccessor.WorkContext.CurrentCustomer.GetCustomerGroupIds()), - _contextAccessor.StoreContext.CurrentStore.Id); + pageSize, string.Join(",", customer.GetCustomerGroupIds()), + storeId); return await _cacheBase.GetAsync(key, async () => { var query = _productRepository.Table.Where(x => x.ProductCategories.Any(y => y.CategoryId == categoryId)); @@ -60,7 +58,7 @@ public virtual async Task> GetProductCategoriesByCa if (!_accessControlConfig.IgnoreAcl) { //Limited to customer groups - var allowedCustomerGroupsIds = _contextAccessor.WorkContext.CurrentCustomer.GetCustomerGroupIds(); + var allowedCustomerGroupsIds = customer.GetCustomerGroupIds(); query = from p in query where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x)) select p; @@ -69,9 +67,8 @@ public virtual async Task> GetProductCategoriesByCa if (!_accessControlConfig.IgnoreStoreLimitations) { //Limited to stores - var currentStoreId = _contextAccessor.StoreContext.CurrentStore.Id; query = from p in query - where !p.LimitedToStores || p.Stores.Contains(currentStoreId) + where !p.LimitedToStores || p.Stores.Contains(storeId) select p; } } diff --git a/src/Business/Grand.Business.Catalog/Services/Collections/ProductCollectionService.cs b/src/Business/Grand.Business.Catalog/Services/Collections/ProductCollectionService.cs index 3f88c0651a..98d8b4a04f 100644 --- a/src/Business/Grand.Business.Catalog/Services/Collections/ProductCollectionService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Collections/ProductCollectionService.cs @@ -4,7 +4,6 @@ using Grand.Domain; using Grand.Domain.Catalog; using Grand.Domain.Customers; -using Grand.Infrastructure; using Grand.Infrastructure.Caching; using Grand.Infrastructure.Caching.Constants; using Grand.Infrastructure.Configuration; @@ -19,12 +18,10 @@ public class ProductCollectionService : IProductCollectionService public ProductCollectionService(ICacheBase cacheBase, IRepository productRepository, - IContextAccessor contextAccessor, IMediator mediator, AccessControlConfig accessControlConfig) { _cacheBase = cacheBase; _productRepository = productRepository; - _contextAccessor = contextAccessor; _mediator = mediator; _accessControlConfig = accessControlConfig; } @@ -36,16 +33,17 @@ public ProductCollectionService(ICacheBase cacheBase, /// /// Collection id /// Store ident + /// Customer whose groups limit the result and key the cache /// Page index /// Page size /// A value that indicates if it should shows hidden records /// Product collection collection public virtual async Task> GetProductCollectionsByCollectionId(string collectionId, - string storeId, + string storeId, Customer customer, int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false) { var key = string.Format(CacheKey.PRODUCTCOLLECTIONS_ALLBYCOLLECTIONID_KEY, showHidden, collectionId, pageIndex, - pageSize, string.Join(",", _contextAccessor.WorkContext.CurrentCustomer.GetCustomerGroupIds()), storeId); + pageSize, string.Join(",", customer.GetCustomerGroupIds()), storeId); return await _cacheBase.GetAsync(key, async () => { var query = _productRepository.Table.Where(x => @@ -56,7 +54,7 @@ public virtual async Task> GetProductCollectionsB if (!_accessControlConfig.IgnoreAcl) { //ACL (access control list) - var allowedCustomerGroupsIds = _contextAccessor.WorkContext.CurrentCustomer.GetCustomerGroupIds(); + var allowedCustomerGroupsIds = customer.GetCustomerGroupIds(); query = from p in query where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x)) select p; @@ -149,7 +147,6 @@ public virtual async Task DeleteProductCollection(ProductCollection productColle #region Fields private readonly IRepository _productRepository; - private readonly IContextAccessor _contextAccessor; private readonly IMediator _mediator; private readonly ICacheBase _cacheBase; private readonly AccessControlConfig _accessControlConfig; diff --git a/src/Business/Grand.Business.Core/Interfaces/Catalog/Categories/IProductCategoryService.cs b/src/Business/Grand.Business.Core/Interfaces/Catalog/Categories/IProductCategoryService.cs index f0a495d082..5230c4e48a 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Catalog/Categories/IProductCategoryService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Catalog/Categories/IProductCategoryService.cs @@ -1,6 +1,7 @@ using Grand.Business.Core.Utilities.Catalog; using Grand.Domain; using Grand.Domain.Catalog; +using Grand.Domain.Customers; namespace Grand.Business.Core.Interfaces.Catalog.Categories; @@ -10,12 +11,14 @@ public interface IProductCategoryService /// Gets product category mapping collection /// /// Category id + /// Customer whose groups limit the result and key the cache + /// Store whose limitations apply /// Page index /// Page size /// A value that indicates if it should shows hidden records /// Product a category mapping collection - Task> GetProductCategoriesByCategoryId(string categoryId, - int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false); + Task> GetProductCategoriesByCategoryId(string categoryId, Customer customer, + string storeId, int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false); /// /// Inserts a mapping for product category diff --git a/src/Business/Grand.Business.Core/Interfaces/Catalog/Collections/IProductCollectionService.cs b/src/Business/Grand.Business.Core/Interfaces/Catalog/Collections/IProductCollectionService.cs index 2f8ecb7071..4964f9bd51 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Catalog/Collections/IProductCollectionService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Catalog/Collections/IProductCollectionService.cs @@ -1,6 +1,7 @@ using Grand.Business.Core.Utilities.Catalog; using Grand.Domain; using Grand.Domain.Catalog; +using Grand.Domain.Customers; namespace Grand.Business.Core.Interfaces.Catalog.Collections; @@ -11,12 +12,13 @@ public interface IProductCollectionService /// /// Collection id /// Store id + /// Customer whose groups limit the result and key the cache /// Page index /// Page size /// A value that indicates if it should shows hidden records /// Product collection collection Task> GetProductCollectionsByCollectionId(string collectionId, string storeId, - int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false); + Customer customer, int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false); /// diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceTests.cs index c530695d69..35a7df711c 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceTests.cs @@ -27,7 +27,6 @@ public class CategoryServiceTests private ProductCategoryService _productCategoryService; private Mock> _productRepositoryMock; private CatalogSettings _settings; - private Mock _workContextMock; [TestInitialize] public void Init() @@ -37,14 +36,13 @@ public void Init() _casheManagerMock = new Mock(); _categoryRepositoryMock = new Mock>(); _productRepositoryMock = new Mock>(Mock.Of()); - _workContextMock = new Mock(); _mediatorMock = new Mock(); _aclServiceMock = new Mock(); _settings = new CatalogSettings(); _categoryService = new CategoryService(_casheManagerMock.Object, _categoryRepositoryMock.Object, _mediatorMock.Object, _aclServiceMock.Object, new AccessControlConfig()); _productCategoryService = new ProductCategoryService(_productRepositoryMock.Object, _casheManagerMock.Object, - _workContextMock.Object, _mediatorMock.Object, new AccessControlConfig()); + _mediatorMock.Object, new AccessControlConfig()); } [TestMethod] diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/ProductCategoryServiceTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/ProductCategoryServiceTests.cs index 4be5c2083d..43643eebfd 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/ProductCategoryServiceTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/ProductCategoryServiceTests.cs @@ -21,23 +21,36 @@ public class ProductCategoryServiceTests private Mock _mediatorMock; private ProductCategoryService _productCategoryService; private IRepository _repository; - private Mock _workContextMock; [TestInitialize] public void InitializeTests() { _repository = new MongoDBRepositoryTest(); - _workContextMock = new Mock(); - _workContextMock.Setup(c => c.StoreContext.CurrentStore).Returns(() => new Store { Id = "" }); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => new Customer()); _mediatorMock = new Mock(); _cacheBase = new MemoryCacheBase(MemoryCacheTest.Get(), _mediatorMock.Object, new CacheConfig { DefaultCacheTimeMinutes = 1 }); - _productCategoryService = new ProductCategoryService(_repository, _cacheBase, _workContextMock.Object, + _productCategoryService = new ProductCategoryService(_repository, _cacheBase, _mediatorMock.Object, new AccessControlConfig()); } + [TestMethod] + public async Task GetProductCategoriesByCategoryId_FiltersByGivenStore() + { + //Arrange + var product = new Product { LimitedToStores = true, Stores = new List { "store1" } }; + product.ProductCategories.Add(new ProductCategory { CategoryId = "cat" }); + await _repository.InsertAsync(product); + + //Act + var store1 = await _productCategoryService.GetProductCategoriesByCategoryId("cat", new Customer(), "store1"); + var store2 = await _productCategoryService.GetProductCategoriesByCategoryId("cat", new Customer(), "store2"); + + //Assert + Assert.HasCount(1, store1); + Assert.IsEmpty(store2); + } + [TestMethod] public async Task GetProductCategoriesByCategoryIdTest() { @@ -52,8 +65,8 @@ public async Task GetProductCategoriesByCategoryIdTest() await _repository.InsertAsync(p2); //Act - var pc1 = await _productCategoryService.GetProductCategoriesByCategoryId("1"); - var pc2 = await _productCategoryService.GetProductCategoriesByCategoryId("2"); + var pc1 = await _productCategoryService.GetProductCategoriesByCategoryId("1", new Customer(), ""); + var pc2 = await _productCategoryService.GetProductCategoriesByCategoryId("2", new Customer(), ""); //Assert Assert.HasCount(1, pc1); @@ -74,7 +87,7 @@ await _productCategoryService.InsertProductCategory(new ProductCategory { Catego p1.Id); await _productCategoryService.InsertProductCategory(new ProductCategory { CategoryId = "2" }, p1.Id); - var pc1 = await _productCategoryService.GetProductCategoriesByCategoryId("1"); + var pc1 = await _productCategoryService.GetProductCategoriesByCategoryId("1", new Customer(), ""); //Assert Assert.HasCount(1, pc1); @@ -99,7 +112,7 @@ public async Task UpdateProductCategoryTest() pc.DisplayOrder = 5; await _productCategoryService.UpdateProductCategory(pc, p1.Id); - var pc1 = await _productCategoryService.GetProductCategoriesByCategoryId("10"); + var pc1 = await _productCategoryService.GetProductCategoriesByCategoryId("10", new Customer(), ""); //Assert Assert.HasCount(1, pc1); @@ -122,7 +135,7 @@ public async Task DeleteProductCategoryTest() await _productCategoryService.DeleteProductCategory(pc, p1.Id); - var pc1 = await _productCategoryService.GetProductCategoriesByCategoryId("1"); + var pc1 = await _productCategoryService.GetProductCategoriesByCategoryId("1", new Customer(), ""); //Assert Assert.IsEmpty(pc1); diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Collections/ProductCollectionServiceTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Collections/ProductCollectionServiceTests.cs index 0880d34358..c561695f2c 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Collections/ProductCollectionServiceTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Collections/ProductCollectionServiceTests.cs @@ -21,23 +21,38 @@ public class ProductCollectionServiceTests private Mock _mediatorMock; private ProductCollectionService _productCollectionService; private IRepository _repository; - private Mock _workContextMock; [TestInitialize] public void InitializeTests() { _repository = new MongoDBRepositoryTest(); - _workContextMock = new Mock(); - _workContextMock.Setup(c => c.StoreContext.CurrentStore).Returns(() => new Store { Id = "" }); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => new Customer()); _mediatorMock = new Mock(); _cacheBase = new MemoryCacheBase(MemoryCacheTest.Get(), _mediatorMock.Object, new CacheConfig { DefaultCacheTimeMinutes = 1 }); - _productCollectionService = new ProductCollectionService(_cacheBase, _repository, _workContextMock.Object, + _productCollectionService = new ProductCollectionService(_cacheBase, _repository, _mediatorMock.Object, new AccessControlConfig()); } + [TestMethod] + public async Task GetProductCollectionsByCollectionId_FiltersByGroupsOfGivenCustomer() + { + //Arrange + var product = new Product { LimitedToGroups = true, CustomerGroups = new List { "vip" } }; + product.ProductCollections.Add(new ProductCollection { CollectionId = "col" }); + await _repository.InsertAsync(product); + var vipCustomer = new Customer(); + vipCustomer.Groups.Add("vip"); + + //Act + var guest = await _productCollectionService.GetProductCollectionsByCollectionId("col", "", new Customer()); + var vip = await _productCollectionService.GetProductCollectionsByCollectionId("col", "", vipCustomer); + + //Assert + Assert.IsEmpty(guest); + Assert.HasCount(1, vip); + } + [TestMethod] public async Task GetProductCollectionsByCollectionIdTest() { @@ -52,8 +67,8 @@ public async Task GetProductCollectionsByCollectionIdTest() await _repository.InsertAsync(p2); //Act - var pc1 = await _productCollectionService.GetProductCollectionsByCollectionId("1", ""); - var pc2 = await _productCollectionService.GetProductCollectionsByCollectionId("2", ""); + var pc1 = await _productCollectionService.GetProductCollectionsByCollectionId("1", "", new Customer()); + var pc2 = await _productCollectionService.GetProductCollectionsByCollectionId("2", "", new Customer()); //Assert Assert.HasCount(1, pc1); @@ -74,7 +89,7 @@ await _productCollectionService.InsertProductCollection( new ProductCollection { CollectionId = "1", DisplayOrder = 10 }, p1.Id); await _productCollectionService.InsertProductCollection(new ProductCollection { CollectionId = "2" }, p1.Id); - var pc1 = await _productCollectionService.GetProductCollectionsByCollectionId("1", ""); + var pc1 = await _productCollectionService.GetProductCollectionsByCollectionId("1", "", new Customer()); //Assert Assert.HasCount(1, pc1); @@ -99,7 +114,7 @@ public async Task UpdateProductCollectionTest() pc.DisplayOrder = 5; await _productCollectionService.UpdateProductCollection(pc, p1.Id); - var pc1 = await _productCollectionService.GetProductCollectionsByCollectionId("10", ""); + var pc1 = await _productCollectionService.GetProductCollectionsByCollectionId("10", "", new Customer()); //Assert Assert.HasCount(1, pc1); @@ -122,7 +137,7 @@ public async Task DeleteProductCollectionTest() await _productCollectionService.DeleteProductCollection(pc, p1.Id); - var pc1 = await _productCollectionService.GetProductCollectionsByCollectionId("1", ""); + var pc1 = await _productCollectionService.GetProductCollectionsByCollectionId("1", "", new Customer()); //Assert Assert.IsEmpty(pc1); diff --git a/src/Web/Grand.Web.AdminShared/Services/CategoryViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CategoryViewModelService.cs index 260ca7970d..47b8c560a5 100644 --- a/src/Web/Grand.Web.AdminShared/Services/CategoryViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/CategoryViewModelService.cs @@ -8,6 +8,7 @@ using Grand.Business.Core.Interfaces.Storage; using Grand.Domain.Catalog; using Grand.Domain.Discounts; +using Grand.Infrastructure; using Grand.Web.AdminShared.Extensions; using Grand.Web.AdminShared.Extensions.Mapping; using Grand.Web.AdminShared.Interfaces; @@ -32,6 +33,7 @@ public class CategoryViewModelService : ICategoryViewModelService private readonly IVendorService _vendorService; private readonly ISeNameService _seNameService; private readonly IEnumTranslationService _enumTranslationService; + private readonly IContextAccessor _contextAccessor; public CategoryViewModelService( ICategoryService categoryService, @@ -45,7 +47,8 @@ public CategoryViewModelService( IVendorService vendorService, CatalogSettings catalogSettings, ISeNameService seNameService, - IEnumTranslationService enumTranslationService) + IEnumTranslationService enumTranslationService, + IContextAccessor contextAccessor) { _categoryService = categoryService; _productCategoryService = productCategoryService; @@ -59,6 +62,7 @@ public CategoryViewModelService( _catalogSettings = catalogSettings; _seNameService = seNameService; _enumTranslationService = enumTranslationService; + _contextAccessor = contextAccessor; } public virtual async Task PrepareCategoryListModel(string storeId) @@ -199,6 +203,8 @@ public virtual async Task DeleteCategory(Category category) PrepareCategoryProductModel(string categoryId, int pageIndex, int pageSize) { var productCategories = await _productCategoryService.GetProductCategoriesByCategoryId(categoryId, + customer: _contextAccessor.WorkContext.CurrentCustomer, + storeId: _contextAccessor.StoreContext.CurrentStore.Id, pageIndex - 1, pageSize, true); var categoryproducts = new List(); diff --git a/src/Web/Grand.Web.AdminShared/Services/CollectionViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CollectionViewModelService.cs index e476f450f7..b4fa5f27b6 100644 --- a/src/Web/Grand.Web.AdminShared/Services/CollectionViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/CollectionViewModelService.cs @@ -220,6 +220,7 @@ public virtual async { var productCollections = await _productCollectionService.GetProductCollectionsByCollectionId(collectionId, storeId, + customer: _contextAccessor.WorkContext.CurrentCustomer, pageIndex - 1, pageSize, true); var items = new List(); foreach (var x in productCollections) From afc911c945634c4f70fe46ac531cf058626e5e8e Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 08:53:17 +0200 Subject: [PATCH 05/33] Pass the customer and store explicitly to product queries --- .../GetPersonalizedProductsQueryHandler.cs | 3 +- .../GetRecommendedProductsQueryHandler.cs | 3 +- .../Handlers/GetSearchProductsQueryHandler.cs | 7 ++- .../GetSuggestedProductsQueryHandler.cs | 3 +- .../Services/Products/ProductService.cs | 30 +++++++----- .../Products/RecentlyViewedProductsService.cs | 11 +++-- .../Catalog/Products/IProductService.cs | 15 ++++-- .../IRecentlyViewedProductsService.cs | 6 ++- .../Catalog/GetPersonalizedProductsQuery.cs | 3 ++ .../Catalog/GetRecommendedProductsQuery.cs | 2 + .../Catalog/GetSuggestedProductsQuery.cs | 3 ++ .../DiscountRulesHasAllProductsController.cs | 3 +- .../DiscountRulesHasOneProductController.cs | 3 +- ...GetRecommendedProductsQueryHandlerTests.cs | 22 +++++++++ .../GetSearchProductsQueryHandlerTests.cs | 16 +++++++ .../ProductImportDataObjectTests.cs | 7 +-- .../Services/Products/ProductServiceTests.cs | 47 +++++++++++++++---- .../RecentlyViewedProductsServiceTests.cs | 5 +- .../SpecificationAttributeServiceTests.cs | 3 +- .../Controllers/BaseOrderControllerTests.cs | 4 +- .../Controllers/BaseProductControllerTests.cs | 26 +++++----- .../BaseProductReviewControllerTests.cs | 2 + ...seSpecificationAttributeControllerTests.cs | 2 + .../Services/ProductViewModelServiceTests.cs | 4 +- .../Controllers/ProductTagsController.cs | 9 +++- .../Controllers/SearchController.cs | 2 +- .../Controllers/BaseOrderController.cs | 1 + .../BaseOrderManagementController.cs | 2 +- .../Controllers/BaseProductController.cs | 10 ++-- .../BaseProductReviewController.cs | 1 + .../BaseSpecificationAttributeController.cs | 1 + .../Services/ProductViewModelService.cs | 6 ++- .../Extensions/ProductList.cs | 1 + .../Grand.Web/Components/BlogPostProducts.cs | 9 +++- .../Grand.Web/Components/CrossSellProducts.cs | 3 +- .../Components/CustomerRecommendedProducts.cs | 3 +- .../Components/HomePageBestSellers.cs | 3 +- .../Grand.Web/Components/HomePageProducts.cs | 9 +++- .../Components/PersonalizedProducts.cs | 4 +- .../Components/ProductsAlsoPurchased.cs | 3 +- .../Components/RecentlyViewedProductsBlock.cs | 3 +- .../Components/RecommendedProducts.cs | 9 +++- .../Grand.Web/Components/RelatedProducts.cs | 9 +++- .../Grand.Web/Components/SimilarProducts.cs | 9 +++- .../Grand.Web/Components/SuggestedProducts.cs | 4 +- .../Controllers/ProductController.cs | 7 ++- .../Handlers/Common/GetSitemapHandler.cs | 1 + .../Products/GetProductDetailsPageHandler.cs | 3 +- .../Products/GetProductOverviewHandler.cs | 3 +- 49 files changed, 250 insertions(+), 95 deletions(-) diff --git a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetPersonalizedProductsQueryHandler.cs b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetPersonalizedProductsQueryHandler.cs index 8090984005..212cad69fa 100644 --- a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetPersonalizedProductsQueryHandler.cs +++ b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetPersonalizedProductsQueryHandler.cs @@ -38,7 +38,8 @@ orderby cr.DisplayOrder var productIds = query.Take(request.ProductsNumber).ToList(); - var ids = await _productService.GetProductsByIds(productIds.Distinct().ToArray()); + var ids = await _productService.GetProductsByIds(productIds.Distinct().ToArray(), customer: request.Customer, + storeId: request.StoreId); return ids.Where(product => product.Published).ToList(); }); diff --git a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetRecommendedProductsQueryHandler.cs b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetRecommendedProductsQueryHandler.cs index e9b9eb426c..863c1d22ed 100644 --- a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetRecommendedProductsQueryHandler.cs +++ b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetRecommendedProductsQueryHandler.cs @@ -39,7 +39,8 @@ orderby cr.DisplayOrder var productIds = query.ToList(); - var ids = await _productService.GetProductsByIds(productIds.Distinct().ToArray()); + var ids = await _productService.GetProductsByIds(productIds.Distinct().ToArray(), customer: request.Customer, + storeId: request.StoreId); return ids.Where(product => product.Published).ToList(); }); diff --git a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetSearchProductsQueryHandler.cs b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetSearchProductsQueryHandler.cs index ac9f7efd5d..7cda042dfb 100644 --- a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetSearchProductsQueryHandler.cs +++ b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetSearchProductsQueryHandler.cs @@ -34,8 +34,11 @@ public GetSearchProductsQueryHandler( // Clean up request parameters CleanupRequestParameters(request); - // Access control list. Allowed customer groups - var allowedCustomerGroupsIds = request.CustomerGroupIds?.ToArray() ?? request.Customer.GetCustomerGroupIds(); + // Access control list. Allowed customer groups; the customer is read only when the filter applies + var allowedCustomerGroupsIds = request.CustomerGroupIds?.ToArray() + ?? (request.ShowHidden || _accessControlConfig.IgnoreAcl + ? [] + : request.Customer.GetCustomerGroupIds()); // Build base query var query = _productRepository.Table.AsQueryable(); diff --git a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetSuggestedProductsQueryHandler.cs b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetSuggestedProductsQueryHandler.cs index 3aa1bebb5f..7fae7c1575 100644 --- a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetSuggestedProductsQueryHandler.cs +++ b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetSuggestedProductsQueryHandler.cs @@ -33,7 +33,8 @@ orderby cr.DisplayOrder var productIds = query.Take(request.ProductsNumber).ToList(); - var ids = await _productService.GetProductsByIds(productIds.Distinct().ToArray()); + var ids = await _productService.GetProductsByIds(productIds.Distinct().ToArray(), customer: request.Customer, + storeId: request.StoreId); return ids.Where(product => product.Published).ToList(); }); diff --git a/src/Business/Grand.Business.Catalog/Services/Products/ProductService.cs b/src/Business/Grand.Business.Catalog/Services/Products/ProductService.cs index 71b5a49915..806b6d1bb8 100644 --- a/src/Business/Grand.Business.Catalog/Services/Products/ProductService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Products/ProductService.cs @@ -7,7 +7,6 @@ using Grand.Domain.Catalog; using Grand.Domain.Customers; using Grand.Domain.Orders; -using Grand.Infrastructure; using Grand.Infrastructure.Caching; using Grand.Infrastructure.Caching.Constants; using Grand.Infrastructure.Extensions; @@ -28,14 +27,12 @@ public class ProductService : IProductService /// public ProductService(ICacheBase cacheBase, IRepository productRepository, - IContextAccessor contextAccessor, IMediator mediator, IAclService aclService ) { _cacheBase = cacheBase; _productRepository = productRepository; - _contextAccessor = contextAccessor; _mediator = mediator; _aclService = aclService; } @@ -46,7 +43,6 @@ IAclService aclService private readonly IRepository _productRepository; private readonly ICacheBase _cacheBase; - private readonly IContextAccessor _contextAccessor; private readonly IAclService _aclService; private readonly IMediator _mediator; @@ -119,9 +115,12 @@ public virtual async Task GetProductByIdIncludeArch(string productId) /// Get products by identifiers /// /// Product identifiers + /// Customer whose groups limit the result (not read when showHidden is true) + /// Store whose limitations apply /// Show hidden /// Products - public virtual async Task> GetProductsByIds(string[] productIds, bool showHidden = false) + public virtual async Task> GetProductsByIds(string[] productIds, Customer customer, string storeId, + bool showHidden = false) { if (productIds == null || productIds.Length == 0) return new List(); @@ -130,8 +129,8 @@ public virtual async Task> GetProductsByIds(string[] productIds, foreach (var id in productIds) { var product = await GetProductById(id); - if (product != null && (showHidden || (_aclService.Authorize(product, _contextAccessor.WorkContext.CurrentCustomer) && - _aclService.Authorize(product, _contextAccessor.StoreContext.CurrentStore.Id) && + if (product != null && (showHidden || (_aclService.Authorize(product, customer) && + _aclService.Authorize(product, storeId) && product.IsAvailable()))) products.Add(product); } @@ -451,6 +450,7 @@ public virtual int GetCategoryProductNumber(Customer customer, IList cat /// /// Search products /// + /// Customer whose groups limit the search /// /// A value indicating whether we should load the specification /// attribute option identifiers applied to loaded products (all pages) @@ -495,6 +495,7 @@ public virtual int GetCategoryProductNumber(Customer customer, IList cat /// Products public virtual async Task<(IPagedList products, IList filterableSpecificationAttributeOptionIds)> SearchProducts( + Customer customer, bool loadFilterableSpecificationAttributeOptionIds = false, int pageIndex = 0, int pageSize = int.MaxValue, @@ -524,7 +525,7 @@ public virtual int GetCategoryProductNumber(Customer customer, IList cat bool? overridePublished = null) { var model = await _mediator.Send(new GetSearchProductsQuery { - Customer = _contextAccessor.WorkContext.CurrentCustomer, + Customer = customer, LoadFilterableSpecificationAttributeOptionIds = loadFilterableSpecificationAttributeOptionIds, PageIndex = pageIndex, PageSize = pageSize, @@ -584,11 +585,12 @@ public virtual async Task> GetProductsByProductAttributeId(s /// Gets associated products /// /// Parent product identifier (used with grouped products) + /// Customer whose groups limit the result (not read when showHidden is true) /// Store identifier; "" to load all records /// Vendor identifier; "" to load all records /// Show hidden /// Products - public virtual async Task> GetAssociatedProducts(string parentGroupedProductId, + public virtual async Task> GetAssociatedProducts(string parentGroupedProductId, Customer customer, string storeId = "", string vendorId = "", bool showHidden = false) { var query = from p in _productRepository.Table @@ -613,7 +615,7 @@ public virtual async Task> GetAssociatedProducts(string parentGro //ACL mapping if (!showHidden) - products = products.Where(x => _aclService.Authorize(x, _contextAccessor.WorkContext.CurrentCustomer)).ToList(); + products = products.Where(x => _aclService.Authorize(x, customer)).ToList(); //Store acl if (!showHidden && !string.IsNullOrEmpty(storeId)) products = products.Where(x => _aclService.Authorize(x, storeId)).ToList(); @@ -863,9 +865,11 @@ public virtual async Task DeleteCrossSellProduct(CrossSellProduct crossSellProdu /// /// Shopping cart /// Number of products to return + /// Customer whose groups limit the result + /// Store whose limitations apply /// Cross-sells public virtual async Task> GetCrossSellProductsByShoppingCart(IList cart, - int numberOfProducts) + int numberOfProducts, Customer customer, string storeId) { var result = new List(); @@ -895,8 +899,8 @@ public virtual async Task> GetCrossSellProductsByShoppingCart(ILi var productToAdd = await GetProductById(crossSell); //validate product if (productToAdd is not { Published: true } - || !_aclService.Authorize(productToAdd, _contextAccessor.WorkContext.CurrentCustomer) || - !_aclService.Authorize(productToAdd, _contextAccessor.StoreContext.CurrentStore.Id) + || !_aclService.Authorize(productToAdd, customer) || + !_aclService.Authorize(productToAdd, storeId) || !productToAdd.IsAvailable()) continue; diff --git a/src/Business/Grand.Business.Catalog/Services/Products/RecentlyViewedProductsService.cs b/src/Business/Grand.Business.Catalog/Services/Products/RecentlyViewedProductsService.cs index 8b7f73b9a6..d79d38ee4a 100644 --- a/src/Business/Grand.Business.Catalog/Services/Products/RecentlyViewedProductsService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Products/RecentlyViewedProductsService.cs @@ -1,6 +1,7 @@ using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Data; using Grand.Domain.Catalog; +using Grand.Domain.Customers; using Grand.Infrastructure.Caching; using Grand.Infrastructure.Caching.Constants; @@ -81,13 +82,15 @@ orderby p.CreatedOnUtc descending /// /// Gets a "recently viewed products" list /// - /// Customer ident + /// Customer whose list is loaded and whose groups limit it + /// Store whose limitations apply /// Number of products to load /// "recently viewed products" list - public virtual async Task> GetRecentlyViewedProducts(string customerId, int number) + public virtual async Task> GetRecentlyViewedProducts(Customer customer, string storeId, int number) { - var productIds = await GetRecentlyViewedProductsIds(customerId, number); - return (await _productService.GetProductsByIds(productIds.ToArray())).Where(product => product.Published) + var productIds = await GetRecentlyViewedProductsIds(customer.Id, number); + return (await _productService.GetProductsByIds(productIds.ToArray(), customer, storeId)) + .Where(product => product.Published) .ToList(); } diff --git a/src/Business/Grand.Business.Core/Interfaces/Catalog/Products/IProductService.cs b/src/Business/Grand.Business.Core/Interfaces/Catalog/Products/IProductService.cs index d76f1c8f14..6edd4aebde 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Catalog/Products/IProductService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Catalog/Products/IProductService.cs @@ -44,9 +44,12 @@ public interface IProductService /// Gets products by identifier /// /// Product identifiers + /// Customer whose groups limit the result (not read when showHidden is true) + /// Store whose limitations apply /// Show hidden /// Products - Task> GetProductsByIds(string[] productIds, bool showHidden = false); + Task> GetProductsByIds(string[] productIds, Customer customer, string storeId, + bool showHidden = false); /// /// Gets products by discount @@ -115,6 +118,7 @@ int GetCategoryProductNumber(Customer customer, IList categoryIds = null /// /// Search products /// + /// Customer whose groups limit the search /// /// A value indicating whether we should load the specification /// attribute option identifiers applied to loaded products (all pages) @@ -158,6 +162,7 @@ int GetCategoryProductNumber(Customer customer, IList categoryIds = null /// /// Products Task<(IPagedList products, IList filterableSpecificationAttributeOptionIds)> SearchProducts( + Customer customer, bool loadFilterableSpecificationAttributeOptionIds = false, int pageIndex = 0, int pageSize = int.MaxValue, @@ -201,11 +206,12 @@ Task> GetProductsByProductAttributeId(string productAttribut /// Gets associated products /// /// Parent product identifier (used with grouped products) + /// Customer whose groups limit the result (not read when showHidden is true) /// Store identifier; "" to load all records /// Vendor identifier; "" to load all records /// A value indicating whether to show hidden records /// Products - Task> GetAssociatedProducts(string parentGroupedProductId, + Task> GetAssociatedProducts(string parentGroupedProductId, Customer customer, string storeId = "", string vendorId = "", bool showHidden = false); /// @@ -289,8 +295,11 @@ Task> GetAssociatedProducts(string parentGroupedProductId, /// /// Shopping cart /// Number of products to return + /// Customer whose groups limit the result + /// Store whose limitations apply /// Cross-sells - Task> GetCrossSellProductsByShoppingCart(IList cart, int numberOfProducts); + Task> GetCrossSellProductsByShoppingCart(IList cart, int numberOfProducts, + Customer customer, string storeId); #endregion diff --git a/src/Business/Grand.Business.Core/Interfaces/Catalog/Products/IRecentlyViewedProductsService.cs b/src/Business/Grand.Business.Core/Interfaces/Catalog/Products/IRecentlyViewedProductsService.cs index 7510508cc7..7d3285aa8a 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Catalog/Products/IRecentlyViewedProductsService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Catalog/Products/IRecentlyViewedProductsService.cs @@ -1,4 +1,5 @@ using Grand.Domain.Catalog; +using Grand.Domain.Customers; namespace Grand.Business.Core.Interfaces.Catalog.Products; @@ -10,10 +11,11 @@ public interface IRecentlyViewedProductsService /// /// Gets a "recently viewed products" list /// - /// Customer ident + /// Customer whose list is loaded and whose groups limit it + /// Store whose limitations apply /// Number of products to load /// "recently viewed products" list - Task> GetRecentlyViewedProducts(string customerId, int number); + Task> GetRecentlyViewedProducts(Customer customer, string storeId, int number); /// /// Adds a product to a recently viewed products list diff --git a/src/Business/Grand.Business.Core/Queries/Catalog/GetPersonalizedProductsQuery.cs b/src/Business/Grand.Business.Core/Queries/Catalog/GetPersonalizedProductsQuery.cs index 4d01c5bd65..e117841659 100644 --- a/src/Business/Grand.Business.Core/Queries/Catalog/GetPersonalizedProductsQuery.cs +++ b/src/Business/Grand.Business.Core/Queries/Catalog/GetPersonalizedProductsQuery.cs @@ -1,4 +1,5 @@ using Grand.Domain.Catalog; +using Grand.Domain.Customers; using Grand.Mediator; namespace Grand.Business.Core.Queries.Catalog; @@ -7,4 +8,6 @@ public class GetPersonalizedProductsQuery : IRequest> { public string CustomerId { get; set; } public int ProductsNumber { get; set; } + public Customer Customer { get; set; } + public string StoreId { get; set; } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Core/Queries/Catalog/GetRecommendedProductsQuery.cs b/src/Business/Grand.Business.Core/Queries/Catalog/GetRecommendedProductsQuery.cs index 3439150c70..c16dd9edf6 100644 --- a/src/Business/Grand.Business.Core/Queries/Catalog/GetRecommendedProductsQuery.cs +++ b/src/Business/Grand.Business.Core/Queries/Catalog/GetRecommendedProductsQuery.cs @@ -1,4 +1,5 @@ using Grand.Domain.Catalog; +using Grand.Domain.Customers; using Grand.Mediator; namespace Grand.Business.Core.Queries.Catalog; @@ -7,4 +8,5 @@ public class GetRecommendedProductsQuery : IRequest> { public string[] CustomerGroupIds { get; set; } public string StoreId { get; set; } + public Customer Customer { get; set; } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Core/Queries/Catalog/GetSuggestedProductsQuery.cs b/src/Business/Grand.Business.Core/Queries/Catalog/GetSuggestedProductsQuery.cs index 9cfc0382a1..88bed64b98 100644 --- a/src/Business/Grand.Business.Core/Queries/Catalog/GetSuggestedProductsQuery.cs +++ b/src/Business/Grand.Business.Core/Queries/Catalog/GetSuggestedProductsQuery.cs @@ -1,4 +1,5 @@ using Grand.Domain.Catalog; +using Grand.Domain.Customers; using Grand.Mediator; namespace Grand.Business.Core.Queries.Catalog; @@ -7,4 +8,6 @@ public class GetSuggestedProductsQuery : IRequest> { public string[] CustomerTagIds { get; set; } public int ProductsNumber { get; set; } + public Customer Customer { get; set; } + public string StoreId { get; set; } } \ No newline at end of file diff --git a/src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesHasAllProductsController.cs b/src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesHasAllProductsController.cs index 16c754fec8..189053cf85 100644 --- a/src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesHasAllProductsController.cs +++ b/src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesHasAllProductsController.cs @@ -174,6 +174,7 @@ public async Task ProductAddPopupList(DataSourceRequest command, searchCategoryIds.Add(model.SearchCategoryId); var products = (await _productService.SearchProducts( + customer: null, categoryIds: searchCategoryIds, collectionId: model.SearchCollectionId, storeId: model.SearchStoreId, @@ -229,7 +230,7 @@ public async Task LoadProductFriendlyNames(string productIds) ids.Add(str2); } - var products = await _productService.GetProductsByIds(ids.ToArray(), true); + var products = await _productService.GetProductsByIds(ids.ToArray(), customer: null, storeId: "", showHidden: true); for (var i = 0; i <= products.Count - 1; i++) { result += products[i].Name; diff --git a/src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesHasOneProductController.cs b/src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesHasOneProductController.cs index b214e08ad7..83dbb85c04 100644 --- a/src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesHasOneProductController.cs +++ b/src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesHasOneProductController.cs @@ -186,6 +186,7 @@ public async Task ProductAddPopupList(DataSourceRequest command, searchCategoryIds.Add(model.SearchCategoryId); var products = (await _productService.SearchProducts( + customer: null, categoryIds: searchCategoryIds, collectionId: model.SearchCollectionId, storeId: model.SearchStoreId, @@ -223,7 +224,7 @@ public async Task LoadProductFriendlyNames(string productIds) .Select(x => x.Split(':')[0].Trim()) .ToList(); - var products = await _productService.GetProductsByIds(ids.ToArray(), true); + var products = await _productService.GetProductsByIds(ids.ToArray(), customer: null, storeId: "", showHidden: true); result = string.Join(", ", products.Select(p => p.Name)); return new JsonResult(new { Text = result }); diff --git a/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetRecommendedProductsQueryHandlerTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetRecommendedProductsQueryHandlerTests.cs index 42d2efc3e5..e55a818a9a 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetRecommendedProductsQueryHandlerTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetRecommendedProductsQueryHandlerTests.cs @@ -41,4 +41,26 @@ public async Task HandleTest() _casheManagerMock.Verify(c => c.GetAsync(It.IsAny(), It.IsAny>>>()), Times.Once); } + + [TestMethod] + public async Task Handle_AuthorizesProductsAgainstCustomerAndStoreOfQuery() + { + //Arrange + var customer = new Customer(); + _casheManagerMock.Setup(c => c.GetAsync(It.IsAny(), It.IsAny>>>())) + .Returns((string _, Func>> acquire) => acquire()); + _customerGroupProductRepositoryMock.Setup(r => r.Table) + .Returns(new List { new() { CustomerGroupId = "1", ProductId = "p1" } } + .AsQueryable()); + _productServiceMock.Setup(p => p.GetProductsByIds(It.IsAny(), customer, "1", false)) + .ReturnsAsync(new List()); + + //Act + await handler.Handle(new GetRecommendedProductsQuery { + CustomerGroupIds = ["1"], StoreId = "1", Customer = customer + }, CancellationToken.None); + + //Assert + _productServiceMock.Verify(p => p.GetProductsByIds(It.IsAny(), customer, "1", false), Times.Once); + } } \ No newline at end of file diff --git a/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetSearchProductsQueryHandlerTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetSearchProductsQueryHandlerTests.cs index c2f20dff86..f16d663a46 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetSearchProductsQueryHandlerTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetSearchProductsQueryHandlerTests.cs @@ -61,4 +61,20 @@ await _repository.InsertAsync(new Product { //Assert CollectionAssert.AreEquivalent(new[] { "open", "guests" }, result.products.Select(p => p.Id).ToList()); } + + [TestMethod] + public async Task Handle_WhenShowHiddenWithoutCustomer_DoesNotFilterByGroups() + { + //Arrange + await _repository.InsertAsync(new Product { Id = "open", Published = true, VisibleIndividually = true }); + await _repository.InsertAsync(new Product { + Id = "admins", Published = true, VisibleIndividually = true, LimitedToGroups = true, + CustomerGroups = ["admins-group"] + }); + var searchProductsQuery = new GetSearchProductsQuery { ShowHidden = true }; + //Act + var result = await handler.Handle(searchProductsQuery, CancellationToken.None); + //Assert + CollectionAssert.AreEquivalent(new[] { "open", "admins" }, result.products.Select(p => p.Id).ToList()); + } } \ No newline at end of file diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/ProductImportDataObjectTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/ProductImportDataObjectTests.cs index 1b092edb23..c50526dcbc 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/ProductImportDataObjectTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/ProductImportDataObjectTests.cs @@ -66,7 +66,6 @@ public class ProductImportDataObjectTests private Mock _slugServiceMock; private Mock _taxServiceMock; private Mock _warehouseServiceMock; - private Mock _workContextMock; private ISeNameService _seNameService; private SecurityConfig _securityConfig; private TaxSettings _taxSettings; @@ -100,14 +99,10 @@ public void Init() _productCategoryServiceMock = new Mock(); _productCollectionServiceMock = new Mock(); - _workContextMock = new Mock(); - _workContextMock.Setup(c => c.StoreContext.CurrentStore).Returns(() => new Store { Id = "" }); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => new Customer()); - _mediatorMock = new Mock(); _cacheBase = new MemoryCacheBase(MemoryCacheTest.Get(), _mediatorMock.Object, new CacheConfig { DefaultCacheTimeMinutes = 1 }); - _productService = new ProductService(_cacheBase, _repository, _workContextMock.Object, _mediatorMock.Object, + _productService = new ProductService(_cacheBase, _repository, _mediatorMock.Object, new AclService(new AccessControlConfig())); _seNameService = new SeNameService(_slugServiceMock.Object, _languageServiceMock.Object, new SeoSettings()); _productImportDataObject = new ProductImportDataObject diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Products/ProductServiceTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Products/ProductServiceTests.cs index c134491da4..f82fc75f7f 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Products/ProductServiceTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Products/ProductServiceTests.cs @@ -4,6 +4,7 @@ using Grand.Business.Core.Queries.Catalog; using Grand.Data; using Grand.Data.Tests.MongoDb; +using Grand.Domain; using Grand.Domain.Catalog; using Grand.Domain.Customers; using Grand.Domain.Orders; @@ -26,15 +27,11 @@ public class ProductServiceTests private Mock _mediatorMock; private IRepository _productRepository; private ProductService _productService; - private Mock _workContextMock; [TestInitialize] public void InitializeTests() { _productRepository = new MongoDBRepositoryTest(); - _workContextMock = new Mock(); - _workContextMock.Setup(c => c.StoreContext.CurrentStore).Returns(() => new Store { Id = "" }); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => new Customer()); _mediatorMock = new Mock(); _mediatorMock.Setup(x => x.Send(It.IsAny(), default)) @@ -43,10 +40,44 @@ public void InitializeTests() _aclServiceMock = new AclService(new AccessControlConfig()); _cacheBase = new MemoryCacheBase(MemoryCacheTest.Get(), _mediatorMock.Object, new CacheConfig { DefaultCacheTimeMinutes = 1 }); - _productService = new ProductService(_cacheBase, _productRepository, _workContextMock.Object, + _productService = new ProductService(_cacheBase, _productRepository, _mediatorMock.Object, _aclServiceMock); } + [TestMethod] + public async Task GetProductsByIds_AuthorizesAgainstGivenCustomerAndStore() + { + //Arrange + await _productRepository.InsertAsync(new Product { + Id = "p1", Published = true, LimitedToStores = true, Stores = new List { "store1" } + }); + + //Act + var store1 = await _productService.GetProductsByIds(["p1"], new Customer(), "store1"); + var store2 = await _productService.GetProductsByIds(["p1"], new Customer(), "store2"); + + //Assert + Assert.HasCount(1, store1); + Assert.IsEmpty(store2); + } + + [TestMethod] + public async Task SearchProducts_SendsGivenCustomerInQuery() + { + //Arrange + var customer = new Customer { Id = "c1" }; + _mediatorMock.Setup(m => m.Send(It.IsAny(), It.IsAny())) + .ReturnsAsync((new PagedList(new List(), 0, 1), (IList)new List())); + + //Act + await _productService.SearchProducts(customer, storeId: "s1"); + + //Assert + _mediatorMock.Verify(m => m.Send( + It.Is(q => q.Customer == customer && q.StoreId == "s1"), + It.IsAny()), Times.Once); + } + private async Task insertSampleProducts() { await _productRepository.InsertAsync(new Product @@ -140,7 +171,7 @@ public async Task GetProductsByIdsTest() { await insertSampleProducts(); //Act - var result = await _productService.GetProductsByIds(["1"]); + var result = await _productService.GetProductsByIds(["1"], new Customer(), ""); //Assert Assert.IsNotEmpty(result); @@ -257,7 +288,7 @@ public async Task GetAssociatedProductsTest() //Arrange await insertSampleProducts(); //Act - var result = await _productService.GetAssociatedProducts("1"); + var result = await _productService.GetAssociatedProducts("1", new Customer()); //Assert Assert.IsNotEmpty(result); @@ -558,7 +589,7 @@ await _productService.InsertCrossSellProduct(new CrossSellProduct { var result = await _productService.GetCrossSellProductsByShoppingCart( - new List { new() { ProductId = product.Id } }, 2); + new List { new() { ProductId = product.Id } }, 2, new Customer(), ""); //Assert Assert.IsNotNull(result); diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Products/RecentlyViewedProductsServiceTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Products/RecentlyViewedProductsServiceTests.cs index 89e1febfe9..b9afeb9577 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Products/RecentlyViewedProductsServiceTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Products/RecentlyViewedProductsServiceTests.cs @@ -3,6 +3,7 @@ using Grand.Data; using Grand.Data.Tests.MongoDb; using Grand.Domain.Catalog; +using Grand.Domain.Customers; using Grand.Infrastructure.Caching; using Grand.Infrastructure.Configuration; using Grand.Infrastructure.Tests.Caching; @@ -27,7 +28,7 @@ public void Init() _repository = new MongoDBRepositoryTest(); _mediatorMock = new Mock(); _productServiceMock = new Mock(); - _productServiceMock.Setup(a => a.GetProductsByIds(It.IsAny(), false)).Returns(() => + _productServiceMock.Setup(a => a.GetProductsByIds(It.IsAny(), It.IsAny(), It.IsAny(), false)).Returns(() => Task.FromResult((IList)new List { new() { Id = "1", Published = true }, new() { Id = "2", Published = true }, new() { Id = "3", Published = true } @@ -50,7 +51,7 @@ public async Task GetRecentlyViewedProductsTest() await _service.AddProductToRecentlyViewedList("2", "1"); //Act - var result = await _service.GetRecentlyViewedProducts("1", 10); + var result = await _service.GetRecentlyViewedProducts(new Customer { Id = "1" }, "", 10); //Assert Assert.HasCount(3, result); diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Products/SpecificationAttributeServiceTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Products/SpecificationAttributeServiceTests.cs index 7d9ef088f8..9a5a50f06e 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Products/SpecificationAttributeServiceTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Products/SpecificationAttributeServiceTests.cs @@ -3,6 +3,7 @@ using Grand.Data; using Grand.Data.Tests.MongoDb; using Grand.Domain.Catalog; +using Grand.Domain.Customers; using Grand.Infrastructure.Caching; using Grand.Infrastructure.Configuration; using Grand.Infrastructure.Tests.Caching; @@ -30,7 +31,7 @@ public void Init() _repositoryProduct = new MongoDBRepositoryTest(); _mediatorMock = new Mock(); _productServiceMock = new Mock(); - _productServiceMock.Setup(a => a.GetProductsByIds(It.IsAny(), false)).Returns(() => + _productServiceMock.Setup(a => a.GetProductsByIds(It.IsAny(), It.IsAny(), It.IsAny(), false)).Returns(() => Task.FromResult((IList)new List { new() { Id = "1", Published = true }, new() { Id = "2", Published = true }, new() { Id = "3", Published = true } diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseOrderControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseOrderControllerTests.cs index a84197dbb3..7f4658b67a 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseOrderControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseOrderControllerTests.cs @@ -264,7 +264,7 @@ public async Task ProductSearchAutoComplete_VendorScope_ForcesVendorIdIntoSearch new Grand.Domain.PagedList(new List(), 0, 15); productServiceMock .Setup(p => p.SearchProducts( - It.IsAny(), It.IsAny(), 15, It.IsAny>(), It.IsAny(), It.IsAny(), + null, It.IsAny(), It.IsAny(), 15, It.IsAny>(), It.IsAny(), It.IsAny(), null, "vendor-A", It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), "abc", It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), @@ -275,7 +275,7 @@ public async Task ProductSearchAutoComplete_VendorScope_ForcesVendorIdIntoSearch await _controller.ProductSearchAutoComplete("abc", productServiceMock.Object); productServiceMock.Verify(p => p.SearchProducts( - It.IsAny(), It.IsAny(), 15, It.IsAny>(), It.IsAny(), It.IsAny(), + null, It.IsAny(), It.IsAny(), 15, It.IsAny>(), It.IsAny(), It.IsAny(), null, "vendor-A", It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), "abc", It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductControllerTests.cs index 73df439b8d..c471342970 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductControllerTests.cs @@ -411,7 +411,7 @@ public async Task DeleteSelected_FiltersOutProductsScopeDenies() // bulk-delete endpoint (any staff could delete any product id in the system). var owned = new Product { Id = "owned" }; var foreign = new Product { Id = "foreign" }; - _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "owned", "foreign" }, true)) + _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "owned", "foreign" }, null, "", true)) .ReturnsAsync(new List { owned, foreign }); _scopeMock.Setup(s => s.HasAccess(owned)).ReturnsAsync(true); _scopeMock.Setup(s => s.HasAccess(foreign)).ReturnsAsync(false); @@ -426,7 +426,7 @@ public async Task DeleteSelected_FiltersOutProductsScopeDenies() public async Task DeleteSelected_AllProductsScopeDenies_DoesNotCallDeleteSelected() { var foreign = new Product { Id = "foreign" }; - _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "foreign" }, true)) + _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "foreign" }, null, "", true)) .ReturnsAsync(new List { foreign }); _scopeMock.Setup(s => s.HasAccess(foreign)).ReturnsAsync(false); @@ -522,7 +522,7 @@ public async Task LoadProductFriendlyNames_EmptyInput_ReturnsEmptyText() var json = result as JsonResult; Assert.IsNotNull(json); Assert.AreEqual("", GetTextProperty(json.Value)); - _productServiceMock.Verify(s => s.GetProductsByIds(It.IsAny(), It.IsAny()), Times.Never); + _productServiceMock.Verify(s => s.GetProductsByIds(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [TestMethod] @@ -530,7 +530,7 @@ public async Task LoadProductFriendlyNames_ScopeDeniesAccess_SkipsProduct() { var allowed = new Product { Id = "p1", Name = "Allowed" }; var denied = new Product { Id = "p2", Name = "Denied" }; - _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "p1", "p2" }, true)) + _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "p1", "p2" }, null, "", true)) .ReturnsAsync(new List { allowed, denied }); _scopeMock.Setup(s => s.HasAccess(allowed)).ReturnsAsync(true); _scopeMock.Setup(s => s.HasAccess(denied)).ReturnsAsync(false); @@ -551,7 +551,7 @@ public async Task LoadProductFriendlyNames_ScopeGrantsAccess_IncludesAllProducts { var p1 = new Product { Id = "p1", Name = "First" }; var p2 = new Product { Id = "p2", Name = "Second" }; - _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "p1", "p2" }, true)) + _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "p1", "p2" }, null, "", true)) .ReturnsAsync(new List { p1, p2 }); _scopeMock.Setup(s => s.HasAccess(It.IsAny())).ReturnsAsync(true); @@ -1979,7 +1979,7 @@ public async Task AssociatedProductList_ScopeGrantsAccess_ReturnsGrid() var product = new Product { Id = "p1" }; _productServiceMock.Setup(p => p.GetProductById("p1", false)).ReturnsAsync(product); _scopeMock.Setup(s => s.CanView(product)).ReturnsAsync(true); - _productServiceMock.Setup(p => p.GetAssociatedProducts("p1", "", "", true)) + _productServiceMock.Setup(p => p.GetAssociatedProducts("p1", null, "", "", true)) .ReturnsAsync(new List { new() { Id = "a1", Name = "Assoc", DisplayOrder = 1 } }); var result = await _controller.AssociatedProductList(new Grand.Web.Common.DataSource.DataSourceRequest(), "p1"); @@ -2920,7 +2920,7 @@ public async Task ExportExcelSelected_NullSelectedIds_ExportsEmptyList() var file = result as FileContentResult; Assert.IsNotNull(file); - _productServiceMock.Verify(p => p.GetProductsByIds(It.IsAny(), true), Times.Never); + _productServiceMock.Verify(p => p.GetProductsByIds(It.IsAny(), null, "", true), Times.Never); } [TestMethod] @@ -2931,7 +2931,7 @@ public async Task ExportExcelSelected_FiltersOutProductsScopeDenies() // Admin (where the original had no check at all) gets the same protection. var owned = new Product { Id = "owned" }; var foreign = new Product { Id = "foreign" }; - _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "owned", "foreign" }, true)) + _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "owned", "foreign" }, null, "", true)) .ReturnsAsync([owned, foreign]); _scopeMock.Setup(s => s.HasAccess(owned)).ReturnsAsync(true); _scopeMock.Setup(s => s.HasAccess(foreign)).ReturnsAsync(false); @@ -3133,7 +3133,7 @@ public async Task BulkEditUpdate_FiltersOutProductsScopeDenies() // that per-item gate uniformly. var owned = new Product { Id = "owned" }; var foreign = new Product { Id = "foreign" }; - _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "owned", "foreign" }, true)) + _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "owned", "foreign" }, null, "", true)) .ReturnsAsync(new List { owned, foreign }); _scopeMock.Setup(s => s.HasAccess(owned)).ReturnsAsync(true); _scopeMock.Setup(s => s.HasAccess(foreign)).ReturnsAsync(false); @@ -3152,7 +3152,7 @@ public async Task BulkEditUpdate_FiltersOutProductsScopeDenies() public async Task BulkEditUpdate_AllProductsScopeDenies_DoesNotCallUpdateBulkEdit() { var foreign = new Product { Id = "foreign" }; - _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "foreign" }, true)) + _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "foreign" }, null, "", true)) .ReturnsAsync(new List { foreign }); _scopeMock.Setup(s => s.HasAccess(foreign)).ReturnsAsync(false); @@ -3177,7 +3177,7 @@ public async Task BulkEditDelete_FiltersOutProductsScopeDenies() { var owned = new Product { Id = "owned" }; var foreign = new Product { Id = "foreign" }; - _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "owned", "foreign" }, true)) + _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "owned", "foreign" }, null, "", true)) .ReturnsAsync(new List { owned, foreign }); _scopeMock.Setup(s => s.HasAccess(owned)).ReturnsAsync(true); _scopeMock.Setup(s => s.HasAccess(foreign)).ReturnsAsync(false); @@ -3195,7 +3195,7 @@ await _controller.BulkEditDelete(new List { public async Task BulkEditDelete_AllProductsScopeDenies_DoesNotCallDeleteBulkEdit() { var foreign = new Product { Id = "foreign" }; - _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "foreign" }, true)) + _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "foreign" }, null, "", true)) .ReturnsAsync(new List { foreign }); _scopeMock.Setup(s => s.HasAccess(foreign)).ReturnsAsync(false); @@ -3211,7 +3211,7 @@ public async Task BulkEditUpdate_ProductsWithEmptyIds_AreIgnoredWithoutServiceCa var result = await _controller.BulkEditUpdate(new List { new() { Id = "" } }); Assert.IsInstanceOfType(result, typeof(JsonResult)); - _productServiceMock.Verify(p => p.GetProductsByIds(It.IsAny(), It.IsAny()), Times.Never); + _productServiceMock.Verify(p => p.GetProductsByIds(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); _productViewModelServiceMock.Verify( s => s.UpdateBulkEdit(It.IsAny>()), Times.Never); } diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductReviewControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductReviewControllerTests.cs index 30338f4f87..9188be0c91 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductReviewControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductReviewControllerTests.cs @@ -320,6 +320,7 @@ public async Task ProductSearchAutoComplete_TermTooShort_ReturnsEmptyContent() Assert.IsNotNull(result); Assert.AreEqual("", result.Content); productService.Verify(s => s.SearchProducts( + customer: null, loadFilterableSpecificationAttributeOptionIds: It.IsAny(), pageIndex: It.IsAny(), pageSize: It.IsAny(), @@ -355,6 +356,7 @@ public async Task ProductSearchAutoComplete_Store_PassesDefaultStoreId() _scope.Setup(s => s.DefaultStoreId).Returns("store-1"); var productService = new Mock(); productService.Setup(s => s.SearchProducts( + customer: null, loadFilterableSpecificationAttributeOptionIds: false, pageIndex: 0, pageSize: 15, diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseSpecificationAttributeControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseSpecificationAttributeControllerTests.cs index 4ead67a87f..4934097165 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseSpecificationAttributeControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseSpecificationAttributeControllerTests.cs @@ -84,6 +84,7 @@ public async Task UsedByProducts_PassesScopeStoreIdToSearchProducts() _service.Setup(x => x.GetSpecificationAttributeById("s1")).ReturnsAsync(spec); _scope.Setup(x => x.CanView(spec)).ReturnsAsync(true); _productService.Setup(x => x.SearchProducts( + customer: null, loadFilterableSpecificationAttributeOptionIds: false, pageIndex: 0, pageSize: 10, @@ -116,6 +117,7 @@ public async Task UsedByProducts_PassesScopeStoreIdToSearchProducts() await _sut.UsedByProducts(new DataSourceRequest { Page = 1, PageSize = 10 }, "s1"); _productService.Verify(x => x.SearchProducts( + customer: null, loadFilterableSpecificationAttributeOptionIds: false, pageIndex: 0, pageSize: 10, diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/ProductViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/ProductViewModelServiceTests.cs index 0b58f2b6a8..0194fc9ad8 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Services/ProductViewModelServiceTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Services/ProductViewModelServiceTests.cs @@ -306,7 +306,7 @@ public async Task PrepareTierPriceModel_GlobalScope_IncludesStoreDropdown() private void SetupSearchProducts() { _productServiceMock.Setup(p => p.SearchProducts( - It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), + null, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), @@ -318,7 +318,7 @@ private void SetupSearchProducts() private void VerifySearchProductsCalledWithVendorId(string expectedVendorId) { _productServiceMock.Verify(p => p.SearchProducts( - It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), + null, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), expectedVendorId, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), diff --git a/src/Web/Grand.Web.Admin/Controllers/ProductTagsController.cs b/src/Web/Grand.Web.Admin/Controllers/ProductTagsController.cs index 5deff039cd..aba1076cf4 100644 --- a/src/Web/Grand.Web.Admin/Controllers/ProductTagsController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/ProductTagsController.cs @@ -4,6 +4,7 @@ using Grand.Domain.Permissions; using Grand.Domain.Catalog; using Grand.Domain.Seo; +using Grand.Infrastructure; using Grand.Web.AdminShared.Models.Catalog; using Grand.Web.Common.DataSource; using Grand.Web.Common.Extensions; @@ -21,19 +22,22 @@ public class ProductTagsController : BaseAdminController private readonly SeoSettings _seoSettings; private readonly ITranslationService _translationService; + private readonly IContextAccessor _contextAccessor; public ProductTagsController( IProductTagService productTagService, IProductService productService, ILanguageService languageService, ITranslationService translationService, - SeoSettings seoSettings) + SeoSettings seoSettings, + IContextAccessor contextAccessor) { _productTagService = productTagService; _productService = productService; _languageService = languageService; _translationService = translationService; _seoSettings = seoSettings; + _contextAccessor = contextAccessor; } public IActionResult Index() @@ -76,7 +80,8 @@ public async Task Products(string tagId, DataSourceRequest comman { var tag = await _productTagService.GetProductTagById(tagId); - var products = (await _productService.SearchProducts(pageIndex: command.Page - 1, pageSize: command.PageSize, + var products = (await _productService.SearchProducts(customer: _contextAccessor.WorkContext.CurrentCustomer, + pageIndex: command.Page - 1, pageSize: command.PageSize, productTag: tag.Name, orderBy: ProductSortingEnum.NameAsc)).products; var gridModel = new DataSourceResult { Data = products.Select(x => new { diff --git a/src/Web/Grand.Web.Admin/Controllers/SearchController.cs b/src/Web/Grand.Web.Admin/Controllers/SearchController.cs index 87b6968ceb..5e190cf7c7 100644 --- a/src/Web/Grand.Web.Admin/Controllers/SearchController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/SearchController.cs @@ -82,7 +82,7 @@ public async Task Index(string searchTerm, FoundMenuItem[] foundM { if (result.Count < _adminSearchSettings.MaxSearchResultsCount && _adminSearchSettings.SearchInProducts) { - var products = (await _productService.SearchProducts(keywords: searchTerm, + var products = (await _productService.SearchProducts(customer: null, keywords: searchTerm, pageSize: _adminSearchSettings.MaxSearchResultsCount - result.Count, showHidden: true)).products; foreach (var product in products) result.Add(new Tuple(new diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseOrderController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseOrderController.cs index d198b2a633..7fd21a1e8f 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseOrderController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseOrderController.cs @@ -181,6 +181,7 @@ public async Task ProductSearchAutoComplete(string term, const int productNumber = 15; var products = (await productService.SearchProducts( + customer: null, storeId: scope.DefaultStoreId, vendorId: scope.DefaultVendorId, keywords: term, diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseOrderManagementController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseOrderManagementController.cs index 7ac2a74d8c..9912928daa 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseOrderManagementController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseOrderManagementController.cs @@ -497,7 +497,7 @@ public async Task AddProductToOrder( if (!string.IsNullOrEmpty(model.SearchCategoryId)) categoryIds.Add(model.SearchCategoryId); var gridModel = new Grand.Web.Common.DataSource.DataSourceResult(); - var products = (await productService.SearchProducts(categoryIds: categoryIds, + var products = (await productService.SearchProducts(customer: null, categoryIds: categoryIds, storeId: Scope.DefaultStoreId, brandId: model.SearchBrandId, collectionId: model.SearchCollectionId, diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductController.cs index 0f156277ab..30fdee37af 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductController.cs @@ -225,7 +225,7 @@ public async Task DeleteSelected(ICollection selectedIds) // unscoped bulk-delete endpoint (any store staff could delete any product id in the system, // bypassing AccessToEntityByStore entirely) purely because MVC routes actions regardless of // whether a host's views ever link to them. - var products = await productService.GetProductsByIds(selectedIds.ToArray(), true); + var products = await productService.GetProductsByIds(selectedIds.ToArray(), customer: null, storeId: "", showHidden: true); var allowedIds = new List(); foreach (var product in products) if (await scope.HasAccess(product)) @@ -309,7 +309,7 @@ public async Task LoadProductFriendlyNames(string productIds) .Select(x => x.Trim()) .ToList(); - var products = await productService.GetProductsByIds(ids.ToArray(), true); + var products = await productService.GetProductsByIds(ids.ToArray(), customer: null, storeId: "", showHidden: true); for (var i = 0; i <= products.Count - 1; i++) { // Filters the friendly-name list, not a hard deny of the whole action: matches Store's @@ -1223,7 +1223,7 @@ public async Task AssociatedProductList(DataSourceRequest command // associated-product set, the ones they themselves own. That vendor filter is unconditional in // GetAssociatedProducts regardless of showHidden, unlike its storeId parameter (which only applies // when showHidden is false - moot here since all three hosts pass showHidden: true). - var associatedProducts = await productService.GetAssociatedProducts(productId, + var associatedProducts = await productService.GetAssociatedProducts(productId, customer: null, vendorId: AssociatedProductVendorId, showHidden: true); var associatedProductsModel = associatedProducts .Select(x => new ProductModel.AssociatedProductModel { @@ -1835,7 +1835,7 @@ public async Task ExportExcelSelected(string selectedIds, .Split([','], StringSplitOptions.RemoveEmptyEntries) .Select(x => x) .ToArray(); - products.AddRange(await productService.GetProductsByIds(ids, true)); + products.AddRange(await productService.GetProductsByIds(ids, customer: null, storeId: "", showHidden: true)); } // Unlike ExportExcelAll, selectedIds is caller-supplied and not derived from a scoped search - @@ -2011,7 +2011,7 @@ private async Task> FilterBulkEditProductsByAccess( .ToDictionary(g => g.Key, g => g.First()); if (byId.Count == 0) return []; - var loadedProducts = await productService.GetProductsByIds(byId.Keys.ToArray(), true); + var loadedProducts = await productService.GetProductsByIds(byId.Keys.ToArray(), customer: null, storeId: "", showHidden: true); var validProducts = new List(); foreach (var product in loadedProducts) if (await scope.HasAccess(product)) diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductReviewController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductReviewController.cs index 4e51a76645..11b33e7769 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductReviewController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductReviewController.cs @@ -144,6 +144,7 @@ public async Task ProductSearchAutoComplete(string term, const int productNumber = 15; var products = (await productService.SearchProducts( + customer: null, storeId: scope.DefaultStoreId ?? "", keywords: term, pageSize: productNumber, diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseSpecificationAttributeController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseSpecificationAttributeController.cs index db1ed3a130..ca95cd5855 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseSpecificationAttributeController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseSpecificationAttributeController.cs @@ -43,6 +43,7 @@ public async Task UsedByProducts(DataSourceRequest command, strin if (optionIds.Count > 0) { var products = (await productService.SearchProducts( + customer: null, storeId: scope.DefaultStoreId ?? "", specificationOptions: optionIds, pageIndex: command.Page - 1, diff --git a/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs index 0c56ce2ce3..a9f109cf2f 100644 --- a/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs @@ -623,6 +623,7 @@ public virtual async Task PrepareProductListModel() // of any vendor filter on the model. Admin/Store have no DefaultVendorId, so this falls back to // model.SearchVendorId unchanged for them. var products = (await productService.SearchProducts( + customer: null, categoryIds: categoryIds, brandId: model.SearchBrandId, collectionId: model.SearchCollectionId, @@ -718,6 +719,7 @@ public virtual async Task> PrepareProducts(ProductListModel model // vendorId: same override as PrepareProductsModel above - mirrors Vendor's original PrepareProducts, // which always passed vendorId: CurrentVendor.Id regardless of the search model. var products = (await productService.SearchProducts( + customer: null, categoryIds: categoryIds, brandId: model.SearchBrandId, collectionId: model.SearchCollectionId, @@ -876,7 +878,7 @@ public virtual async Task DeleteProduct(Product product) public virtual async Task DeleteSelected(IEnumerable selectedIds) { var products = new List(); - products.AddRange(await productService.GetProductsByIds(selectedIds.ToArray(), true)); + products.AddRange(await productService.GetProductsByIds(selectedIds.ToArray(), customer: null, storeId: "", showHidden: true)); for (var i = 0; i < products.Count; i++) { await DeleteProduct(products[i]); @@ -1320,7 +1322,7 @@ public virtual async Task PrepareBulkEditListModel() // has no SearchVendorId field for a caller to (mis)supply in the first place, so there is nothing // to fall back to for Admin/Store, whose scope.DefaultVendorId is null and who intentionally see // every vendor's products in bulk-edit. - var products = (await productService.SearchProducts(categoryIds: searchCategoryIds, + var products = (await productService.SearchProducts(customer: null, categoryIds: searchCategoryIds, brandId: model.SearchBrandId, collectionId: model.SearchCollectionId, storeId: model.SearchStoreId, diff --git a/src/Web/Grand.Web.Common/Extensions/ProductList.cs b/src/Web/Grand.Web.Common/Extensions/ProductList.cs index 89fa152502..72c796d7aa 100644 --- a/src/Web/Grand.Web.Common/Extensions/ProductList.cs +++ b/src/Web/Grand.Web.Common/Extensions/ProductList.cs @@ -16,6 +16,7 @@ public static async Task> PrepareProductList(this IProductSe searchCategoryIds.Add(searchCategoryId); var products = (await productService.SearchProducts( + customer: null, categoryIds: searchCategoryIds, brandId: searchBrandId, collectionId: searchCollectionId, diff --git a/src/Web/Grand.Web/Components/BlogPostProducts.cs b/src/Web/Grand.Web/Components/BlogPostProducts.cs index f27d511666..eaec4f048b 100644 --- a/src/Web/Grand.Web/Components/BlogPostProducts.cs +++ b/src/Web/Grand.Web/Components/BlogPostProducts.cs @@ -1,6 +1,7 @@ using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Cms; using Grand.Domain.Catalog; +using Grand.Infrastructure; using Grand.Web.Common.Components; using Grand.Web.Features.Models.Products; using Grand.Mediator; @@ -16,12 +17,14 @@ public BlogPostProductsViewComponent( IProductService productService, IMediator mediator, IBlogService blogService, - CatalogSettings catalogSettings) + CatalogSettings catalogSettings, + IContextAccessor contextAccessor) { _productService = productService; _mediator = mediator; _blogService = blogService; _catalogSettings = catalogSettings; + _contextAccessor = contextAccessor; } #endregion @@ -34,7 +37,8 @@ public async Task InvokeAsync(string blogPostId, int? prod if (!blogproducts.Any()) return Content(""); - var products = await _productService.GetProductsByIds(blogproducts.Select(x => x.ProductId).ToArray()); + var products = await _productService.GetProductsByIds(blogproducts.Select(x => x.ProductId).ToArray(), _contextAccessor.WorkContext.CurrentCustomer, + _contextAccessor.StoreContext.CurrentStore.Id); if (!products.Any()) return Content(""); @@ -59,6 +63,7 @@ public async Task InvokeAsync(string blogPostId, int? prod private readonly IBlogService _blogService; private readonly CatalogSettings _catalogSettings; + private readonly IContextAccessor _contextAccessor; #endregion } \ No newline at end of file diff --git a/src/Web/Grand.Web/Components/CrossSellProducts.cs b/src/Web/Grand.Web/Components/CrossSellProducts.cs index 60944daa6e..f71cbb99fa 100644 --- a/src/Web/Grand.Web/Components/CrossSellProducts.cs +++ b/src/Web/Grand.Web/Components/CrossSellProducts.cs @@ -42,7 +42,8 @@ public async Task InvokeAsync(int? productThumbPictureSize .ToList(); var products = - await _productService.GetCrossSellProductsByShoppingCart(cart, _shoppingCartSettings.CrossSellsNumber); + await _productService.GetCrossSellProductsByShoppingCart(cart, _shoppingCartSettings.CrossSellsNumber, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id); if (!products.Any()) return Content(""); diff --git a/src/Web/Grand.Web/Components/CustomerRecommendedProducts.cs b/src/Web/Grand.Web/Components/CustomerRecommendedProducts.cs index 845b420ccd..371b6e1bdb 100644 --- a/src/Web/Grand.Web/Components/CustomerRecommendedProducts.cs +++ b/src/Web/Grand.Web/Components/CustomerRecommendedProducts.cs @@ -34,7 +34,8 @@ public async Task InvokeAsync(int? productThumbPictureSize var products = await _mediator.Send(new GetRecommendedProductsQuery { CustomerGroupIds = _contextAccessor.WorkContext.CurrentCustomer.GetCustomerGroupIds(), - StoreId = _contextAccessor.StoreContext.CurrentStore.Id + StoreId = _contextAccessor.StoreContext.CurrentStore.Id, + Customer = _contextAccessor.WorkContext.CurrentCustomer }); if (!products.Any()) diff --git a/src/Web/Grand.Web/Components/HomePageBestSellers.cs b/src/Web/Grand.Web/Components/HomePageBestSellers.cs index aa370725eb..bf9de0371f 100644 --- a/src/Web/Grand.Web/Components/HomePageBestSellers.cs +++ b/src/Web/Grand.Web/Components/HomePageBestSellers.cs @@ -66,7 +66,8 @@ await _orderReportService.BestSellersReport( } //load products - var products = await _productService.GetProductsByIds(productIds.ToArray()); + var products = await _productService.GetProductsByIds(productIds.ToArray(), _contextAccessor.WorkContext.CurrentCustomer, + _contextAccessor.StoreContext.CurrentStore.Id); if (!products.Any()) return Content(""); diff --git a/src/Web/Grand.Web/Components/HomePageProducts.cs b/src/Web/Grand.Web/Components/HomePageProducts.cs index 8dc02c7590..bef60089f5 100644 --- a/src/Web/Grand.Web/Components/HomePageProducts.cs +++ b/src/Web/Grand.Web/Components/HomePageProducts.cs @@ -1,5 +1,6 @@ using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Domain.Catalog; +using Grand.Infrastructure; using Grand.Infrastructure.Caching; using Grand.Web.Common.Components; using Grand.Web.Events.Cache; @@ -17,12 +18,14 @@ public HomePageProductsViewComponent( IProductService productService, IMediator mediator, ICacheBase cacheBase, - CatalogSettings catalogSettings) + CatalogSettings catalogSettings, + IContextAccessor contextAccessor) { _productService = productService; _mediator = mediator; _cacheBase = cacheBase; _catalogSettings = catalogSettings; + _contextAccessor = contextAccessor; } #endregion @@ -34,7 +37,8 @@ public async Task InvokeAsync(int? productThumbPictureSize var productsIds = await _cacheBase.GetAsync(CacheKeyConst.HOMEPAGE_PRODUCTS_MODEL_KEY, async () => await _productService.GetAllProductsDisplayedOnHomePage()); - var products = await _productService.GetProductsByIds(productsIds.ToArray()); + var products = await _productService.GetProductsByIds(productsIds.ToArray(), _contextAccessor.WorkContext.CurrentCustomer, + _contextAccessor.StoreContext.CurrentStore.Id); if (!products.Any()) return Content(""); @@ -58,6 +62,7 @@ public async Task InvokeAsync(int? productThumbPictureSize private readonly IMediator _mediator; private readonly ICacheBase _cacheBase; private readonly CatalogSettings _catalogSettings; + private readonly IContextAccessor _contextAccessor; #endregion } \ No newline at end of file diff --git a/src/Web/Grand.Web/Components/PersonalizedProducts.cs b/src/Web/Grand.Web/Components/PersonalizedProducts.cs index ad01ad3ac2..12a86813e3 100644 --- a/src/Web/Grand.Web/Components/PersonalizedProducts.cs +++ b/src/Web/Grand.Web/Components/PersonalizedProducts.cs @@ -32,7 +32,9 @@ public async Task InvokeAsync(int? productThumbPictureSize return Content(""); var products = await _mediator.Send(new GetPersonalizedProductsQuery { - CustomerId = _contextAccessor.WorkContext.CurrentCustomer.Id, ProductsNumber = _catalogSettings.PersonalizedProductsNumber + CustomerId = _contextAccessor.WorkContext.CurrentCustomer.Id, ProductsNumber = _catalogSettings.PersonalizedProductsNumber, + Customer = _contextAccessor.WorkContext.CurrentCustomer, + StoreId = _contextAccessor.StoreContext.CurrentStore.Id }); if (!products.Any()) diff --git a/src/Web/Grand.Web/Components/ProductsAlsoPurchased.cs b/src/Web/Grand.Web/Components/ProductsAlsoPurchased.cs index 4d2fd13e22..75115dbbd1 100644 --- a/src/Web/Grand.Web/Components/ProductsAlsoPurchased.cs +++ b/src/Web/Grand.Web/Components/ProductsAlsoPurchased.cs @@ -51,7 +51,8 @@ public async Task InvokeAsync(string productId, int? produ ); //load products - var products = await _productService.GetProductsByIds(productIds); + var products = await _productService.GetProductsByIds(productIds, _contextAccessor.WorkContext.CurrentCustomer, + _contextAccessor.StoreContext.CurrentStore.Id); if (!products.Any()) return Content(""); diff --git a/src/Web/Grand.Web/Components/RecentlyViewedProductsBlock.cs b/src/Web/Grand.Web/Components/RecentlyViewedProductsBlock.cs index ecec6506e7..c55267f340 100644 --- a/src/Web/Grand.Web/Components/RecentlyViewedProductsBlock.cs +++ b/src/Web/Grand.Web/Components/RecentlyViewedProductsBlock.cs @@ -34,7 +34,8 @@ public async Task InvokeAsync(int? productThumbPictureSize return Content(""); var preparePictureModel = productThumbPictureSize.HasValue; - var products = await _recentlyViewedProductsService.GetRecentlyViewedProducts(_contextAccessor.WorkContext.CurrentCustomer.Id, + var products = await _recentlyViewedProductsService.GetRecentlyViewedProducts(_contextAccessor.WorkContext.CurrentCustomer, + _contextAccessor.StoreContext.CurrentStore.Id, _catalogSettings.RecentlyViewedProductsNumber); if (!products.Any()) diff --git a/src/Web/Grand.Web/Components/RecommendedProducts.cs b/src/Web/Grand.Web/Components/RecommendedProducts.cs index 9c5a32639e..1565e922d5 100644 --- a/src/Web/Grand.Web/Components/RecommendedProducts.cs +++ b/src/Web/Grand.Web/Components/RecommendedProducts.cs @@ -1,5 +1,6 @@ using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Domain.Catalog; +using Grand.Infrastructure; using Grand.Web.Common.Components; using Grand.Web.Features.Models.Products; using Grand.Mediator; @@ -14,11 +15,13 @@ public class RecommendedProductsViewComponent : BaseViewComponent public RecommendedProductsViewComponent( IProductService productService, IMediator mediator, - CatalogSettings catalogSettings) + CatalogSettings catalogSettings, + IContextAccessor contextAccessor) { _productService = productService; _mediator = mediator; _catalogSettings = catalogSettings; + _contextAccessor = contextAccessor; } #endregion @@ -31,7 +34,8 @@ public async Task InvokeAsync(string productId, int? produ if (!productIds.Any()) return Content(""); - var products = await _productService.GetProductsByIds(productIds); + var products = await _productService.GetProductsByIds(productIds, _contextAccessor.WorkContext.CurrentCustomer, + _contextAccessor.StoreContext.CurrentStore.Id); if (!products.Any()) return Content(""); @@ -55,6 +59,7 @@ public async Task InvokeAsync(string productId, int? produ private readonly IMediator _mediator; private readonly CatalogSettings _catalogSettings; + private readonly IContextAccessor _contextAccessor; #endregion } \ No newline at end of file diff --git a/src/Web/Grand.Web/Components/RelatedProducts.cs b/src/Web/Grand.Web/Components/RelatedProducts.cs index 139ecd33f9..22ee273035 100644 --- a/src/Web/Grand.Web/Components/RelatedProducts.cs +++ b/src/Web/Grand.Web/Components/RelatedProducts.cs @@ -1,5 +1,6 @@ using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Domain.Catalog; +using Grand.Infrastructure; using Grand.Web.Common.Components; using Grand.Web.Features.Models.Products; using Grand.Mediator; @@ -14,11 +15,13 @@ public class RelatedProductsViewComponent : BaseViewComponent public RelatedProductsViewComponent( IProductService productService, IMediator mediator, - CatalogSettings catalogSettings) + CatalogSettings catalogSettings, + IContextAccessor contextAccessor) { _productService = productService; _mediator = mediator; _catalogSettings = catalogSettings; + _contextAccessor = contextAccessor; } #endregion @@ -31,7 +34,8 @@ public async Task InvokeAsync(string productId, int? produ .Select(x => x.ProductId2).ToArray(); //load products - var products = await _productService.GetProductsByIds(productIds); + var products = await _productService.GetProductsByIds(productIds, _contextAccessor.WorkContext.CurrentCustomer, + _contextAccessor.StoreContext.CurrentStore.Id); var model = await _mediator.Send(new GetProductOverview { PreparePictureModel = true, @@ -52,6 +56,7 @@ public async Task InvokeAsync(string productId, int? produ private readonly IMediator _mediator; private readonly CatalogSettings _catalogSettings; + private readonly IContextAccessor _contextAccessor; #endregion } \ No newline at end of file diff --git a/src/Web/Grand.Web/Components/SimilarProducts.cs b/src/Web/Grand.Web/Components/SimilarProducts.cs index 75f013a01d..f83729fd6d 100644 --- a/src/Web/Grand.Web/Components/SimilarProducts.cs +++ b/src/Web/Grand.Web/Components/SimilarProducts.cs @@ -1,5 +1,6 @@ using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Domain.Catalog; +using Grand.Infrastructure; using Grand.Web.Common.Components; using Grand.Web.Features.Models.Products; using Grand.Mediator; @@ -14,11 +15,13 @@ public class SimilarProductsViewComponent : BaseViewComponent public SimilarProductsViewComponent( IProductService productService, IMediator mediator, - CatalogSettings catalogSettings) + CatalogSettings catalogSettings, + IContextAccessor contextAccessor) { _productService = productService; _mediator = mediator; _catalogSettings = catalogSettings; + _contextAccessor = contextAccessor; } #endregion @@ -32,7 +35,8 @@ public async Task InvokeAsync(string productId, int? produ if (!productIds.Any()) return Content(""); - var products = await _productService.GetProductsByIds(productIds); + var products = await _productService.GetProductsByIds(productIds, _contextAccessor.WorkContext.CurrentCustomer, + _contextAccessor.StoreContext.CurrentStore.Id); if (!products.Any()) return Content(""); @@ -56,6 +60,7 @@ public async Task InvokeAsync(string productId, int? produ private readonly IMediator _mediator; private readonly CatalogSettings _catalogSettings; + private readonly IContextAccessor _contextAccessor; #endregion } \ No newline at end of file diff --git a/src/Web/Grand.Web/Components/SuggestedProducts.cs b/src/Web/Grand.Web/Components/SuggestedProducts.cs index 0953f7de9e..7a6c13ee70 100644 --- a/src/Web/Grand.Web/Components/SuggestedProducts.cs +++ b/src/Web/Grand.Web/Components/SuggestedProducts.cs @@ -33,7 +33,9 @@ public async Task InvokeAsync(int? productThumbPictureSize var products = await _mediator.Send(new GetSuggestedProductsQuery { CustomerTagIds = _contextAccessor.WorkContext.CurrentCustomer.CustomerTags.ToArray(), - ProductsNumber = _catalogSettings.SuggestedProductsNumber + ProductsNumber = _catalogSettings.SuggestedProductsNumber, + Customer = _contextAccessor.WorkContext.CurrentCustomer, + StoreId = _contextAccessor.StoreContext.CurrentStore.Id }); if (!products.Any()) diff --git a/src/Web/Grand.Web/Controllers/ProductController.cs b/src/Web/Grand.Web/Controllers/ProductController.cs index 4f7ac0c1be..db072f839a 100644 --- a/src/Web/Grand.Web/Controllers/ProductController.cs +++ b/src/Web/Grand.Web/Controllers/ProductController.cs @@ -72,7 +72,8 @@ public virtual async Task RecentlyViewedProducts() if (!_catalogSettings.RecentlyViewedProductsEnabled) return Content(""); - var products = await _recentlyViewedProductsService.GetRecentlyViewedProducts(_contextAccessor.WorkContext.CurrentCustomer.Id, + var products = await _recentlyViewedProductsService.GetRecentlyViewedProducts(_contextAccessor.WorkContext.CurrentCustomer, + _contextAccessor.StoreContext.CurrentStore.Id, _catalogSettings.RecentlyViewedProductsNumber); //prepare model @@ -94,7 +95,8 @@ public virtual async Task RelatedProducts(string productId, int? .Select(x => x.ProductId2).ToArray(); //load products - var products = await _productService.GetProductsByIds(productIds); + var products = await _productService.GetProductsByIds(productIds, _contextAccessor.WorkContext.CurrentCustomer, + _contextAccessor.StoreContext.CurrentStore.Id); var model = await _mediator.Send(new GetProductOverview { PreparePictureModel = true, @@ -119,6 +121,7 @@ public virtual async Task NewProducts() return Content(""); var products = (await _productService.SearchProducts( + customer: _contextAccessor.WorkContext.CurrentCustomer, storeId: _contextAccessor.StoreContext.CurrentStore.Id, visibleIndividuallyOnly: true, markedAsNewOnly: true, diff --git a/src/Web/Grand.Web/Features/Handlers/Common/GetSitemapHandler.cs b/src/Web/Grand.Web/Features/Handlers/Common/GetSitemapHandler.cs index d2ff8074bf..b108f24818 100644 --- a/src/Web/Grand.Web/Features/Handlers/Common/GetSitemapHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Common/GetSitemapHandler.cs @@ -95,6 +95,7 @@ public async Task Handle(GetSitemap request, CancellationToken can { //limit product to 200 until paging is supported on this page var products = (await _productService.SearchProducts( + customer: request.Customer, storeId: request.Store.Id, visibleIndividuallyOnly: true, pageSize: 200)).products; diff --git a/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsPageHandler.cs b/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsPageHandler.cs index d2c6462293..c678397a0b 100644 --- a/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsPageHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsPageHandler.cs @@ -312,7 +312,8 @@ or ManageInventoryMethod.ManageStockByAttributes && if (!isAssociatedProduct) { var associatedProducts = - await _productService.GetAssociatedProducts(product.Id, _contextAccessor.StoreContext.CurrentStore.Id); + await _productService.GetAssociatedProducts(product.Id, _contextAccessor.WorkContext.CurrentCustomer, + _contextAccessor.StoreContext.CurrentStore.Id); foreach (var associatedProduct in associatedProducts) model.AssociatedProducts.Add( await PrepareProductDetailsModel(store, associatedProduct, null, true)); diff --git a/src/Web/Grand.Web/Features/Handlers/Products/GetProductOverviewHandler.cs b/src/Web/Grand.Web/Features/Handlers/Products/GetProductOverviewHandler.cs index 293045c3ff..71c21d0ab5 100644 --- a/src/Web/Grand.Web/Features/Handlers/Products/GetProductOverviewHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Products/GetProductOverviewHandler.cs @@ -225,7 +225,8 @@ private async Task PrepareProductOverviewModel(Product pro #region Grouped product var associatedProducts = - await _productService.GetAssociatedProducts(product.Id, _contextAccessor.StoreContext.CurrentStore.Id); + await _productService.GetAssociatedProducts(product.Id, _contextAccessor.WorkContext.CurrentCustomer, + _contextAccessor.StoreContext.CurrentStore.Id); //add to cart button (ignore "DisableBuyButton" property for grouped products) priceModel.DisableBuyButton = !enableShoppingCart || !displayPrices; From 32fdc93389900fa5bc2af67142b0d466aa1278fc Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 09:07:55 +0200 Subject: [PATCH 06/33] Reject recommendation queries without a customer The handlers cache the ACL-filtered result under keys shared by many customers. A query without a customer would authorize nothing and cache the empty list for all of them. Also pins DeleteCategory detaching its children and restores a negative assertion to any customer and store. --- .../GetPersonalizedProductsQueryHandler.cs | 3 +++ .../GetRecommendedProductsQueryHandler.cs | 3 +++ .../GetSuggestedProductsQueryHandler.cs | 3 +++ .../GetPersonalizedProductsQueryHandlerTests.cs | 12 +++++++++++- .../GetRecommendedProductsQueryHandlerTests.cs | 13 ++++++++++++- .../GetSuggestedProductsQueryHandlerTests.cs | 13 ++++++++++++- .../Categories/CategoryServiceDbTests.cs | 17 +++++++++++++++++ .../Controllers/BaseProductControllerTests.cs | 2 +- 8 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetPersonalizedProductsQueryHandler.cs b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetPersonalizedProductsQueryHandler.cs index 212cad69fa..27d8c3f604 100644 --- a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetPersonalizedProductsQueryHandler.cs +++ b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetPersonalizedProductsQueryHandler.cs @@ -28,6 +28,9 @@ public GetPersonalizedProductsQueryHandler( public async Task> Handle(GetPersonalizedProductsQuery request, CancellationToken cancellationToken) { + //the result is cached per key shared by many customers, so a query without one must not fill it + ArgumentNullException.ThrowIfNull(request.Customer); + return await _cacheBase.GetAsync(string.Format(CacheKey.PRODUCTS_CUSTOMER_PERSONAL_KEY, request.CustomerId), async () => { diff --git a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetRecommendedProductsQueryHandler.cs b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetRecommendedProductsQueryHandler.cs index 863c1d22ed..8c27a87b75 100644 --- a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetRecommendedProductsQueryHandler.cs +++ b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetRecommendedProductsQueryHandler.cs @@ -28,6 +28,9 @@ public GetRecommendedProductsQueryHandler( public async Task> Handle(GetRecommendedProductsQuery request, CancellationToken cancellationToken) { + //the result is cached per key shared by many customers, so a query without one must not fill it + ArgumentNullException.ThrowIfNull(request.Customer); + return await _cacheBase.GetAsync( string.Format(CacheKey.PRODUCTS_CUSTOMER_GROUP, string.Join(",", request.CustomerGroupIds), request.StoreId), async () => diff --git a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetSuggestedProductsQueryHandler.cs b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetSuggestedProductsQueryHandler.cs index 7fae7c1575..6f9d24b9e3 100644 --- a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetSuggestedProductsQueryHandler.cs +++ b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetSuggestedProductsQueryHandler.cs @@ -23,6 +23,9 @@ public GetSuggestedProductsQueryHandler( public async Task> Handle(GetSuggestedProductsQuery request, CancellationToken cancellationToken) { + //the result is cached per key shared by many customers, so a query without one must not fill it + ArgumentNullException.ThrowIfNull(request.Customer); + return await _cacheBase.GetAsync( string.Format(CacheKey.PRODUCTS_CUSTOMER_TAG, string.Join(",", request.CustomerTagIds)), async () => { diff --git a/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetPersonalizedProductsQueryHandlerTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetPersonalizedProductsQueryHandlerTests.cs index 3443522058..a5a1017b6a 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetPersonalizedProductsQueryHandlerTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetPersonalizedProductsQueryHandlerTests.cs @@ -33,8 +33,18 @@ public void Init() [TestMethod] public async Task HandleTest() { - await handler.Handle(new GetPersonalizedProductsQuery(), CancellationToken.None); + await handler.Handle(new GetPersonalizedProductsQuery { Customer = new Customer() }, CancellationToken.None); _casheManagerMock.Verify(c => c.GetAsync(It.IsAny(), It.IsAny>>>()), Times.Once); } + + [TestMethod] + public async Task Handle_WithoutCustomer_ThrowsBeforeCaching() + { + await Assert.ThrowsExactlyAsync(() => + handler.Handle(new GetPersonalizedProductsQuery { CustomerId = "1", ProductsNumber = 1 }, + CancellationToken.None)); + _casheManagerMock.Verify(c => c.GetAsync(It.IsAny(), It.IsAny>>>()), + Times.Never); + } } \ No newline at end of file diff --git a/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetRecommendedProductsQueryHandlerTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetRecommendedProductsQueryHandlerTests.cs index e55a818a9a..a293452132 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetRecommendedProductsQueryHandlerTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetRecommendedProductsQueryHandlerTests.cs @@ -35,13 +35,24 @@ public async Task HandleTest() { var getRecommendedProductsQuery = new GetRecommendedProductsQuery { CustomerGroupIds = ["1"], - StoreId = "1" + StoreId = "1", + Customer = new Customer() }; await handler.Handle(getRecommendedProductsQuery, CancellationToken.None); _casheManagerMock.Verify(c => c.GetAsync(It.IsAny(), It.IsAny>>>()), Times.Once); } + [TestMethod] + public async Task Handle_WithoutCustomer_ThrowsBeforeCaching() + { + await Assert.ThrowsExactlyAsync(() => + handler.Handle(new GetRecommendedProductsQuery { CustomerGroupIds = ["1"], StoreId = "1" }, + CancellationToken.None)); + _casheManagerMock.Verify(c => c.GetAsync(It.IsAny(), It.IsAny>>>()), + Times.Never); + } + [TestMethod] public async Task Handle_AuthorizesProductsAgainstCustomerAndStoreOfQuery() { diff --git a/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetSuggestedProductsQueryHandlerTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetSuggestedProductsQueryHandlerTests.cs index 6d76a5202b..f1cf682a51 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetSuggestedProductsQueryHandlerTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetSuggestedProductsQueryHandlerTests.cs @@ -35,10 +35,21 @@ public async Task HandleTest() { var suggestedProductsQuery = new GetSuggestedProductsQuery { CustomerTagIds = ["1"], - ProductsNumber = 10 + ProductsNumber = 10, + Customer = new Customer() }; await handler.Handle(suggestedProductsQuery, CancellationToken.None); _casheManagerMock.Verify(c => c.GetAsync(It.IsAny(), It.IsAny>>>()), Times.Once); } + + [TestMethod] + public async Task Handle_WithoutCustomer_ThrowsBeforeCaching() + { + await Assert.ThrowsExactlyAsync(() => + handler.Handle(new GetSuggestedProductsQuery { CustomerTagIds = ["1"], ProductsNumber = 1 }, + CancellationToken.None)); + _casheManagerMock.Verify(c => c.GetAsync(It.IsAny(), It.IsAny>>>()), + Times.Never); + } } \ No newline at end of file diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceDbTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceDbTests.cs index 871714fc1f..1047839931 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceDbTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceDbTests.cs @@ -108,6 +108,23 @@ public async Task UpdateCategory() Assert.IsNotNull(_categoryRepository.Table.FirstOrDefault(x => x.Name == "test2")); } + [TestMethod] + public async Task DeleteCategory_DetachesItsChildCategories() + { + //Arrange + var parent = new Category { Id = "p", Published = true }; + await _categoryService.InsertCategory(parent); + await _categoryService.InsertCategory(new Category { Id = "c1", ParentCategoryId = "p" }); + await _categoryService.InsertCategory(new Category { Id = "c2", ParentCategoryId = "p", Published = true }); + await _categoryService.InsertCategory(new Category { Id = "other", ParentCategoryId = "x" }); + //Act + await _categoryService.DeleteCategory(parent); + //Assert + Assert.AreEqual("", _categoryRepository.Table.First(x => x.Id == "c1").ParentCategoryId); + Assert.AreEqual("", _categoryRepository.Table.First(x => x.Id == "c2").ParentCategoryId); + Assert.AreEqual("x", _categoryRepository.Table.First(x => x.Id == "other").ParentCategoryId); + } + [TestMethod] public async Task DeleteCategory() { diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductControllerTests.cs index c471342970..d66e54e95a 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductControllerTests.cs @@ -2920,7 +2920,7 @@ public async Task ExportExcelSelected_NullSelectedIds_ExportsEmptyList() var file = result as FileContentResult; Assert.IsNotNull(file); - _productServiceMock.Verify(p => p.GetProductsByIds(It.IsAny(), null, "", true), Times.Never); + _productServiceMock.Verify(p => p.GetProductsByIds(It.IsAny(), It.IsAny(), It.IsAny(), true), Times.Never); } [TestMethod] From 3ed5514ddf6b3d2825adc5fab44236c42f369190 Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 12:24:08 +0200 Subject: [PATCH 07/33] Add a calculation context for checkout calculations --- .../Utilities/Checkout/CalculationContext.cs | 18 +++++++++ .../CalculationContextExtensionsTests.cs | 37 +++++++++++++++++++ .../CalculationContextExtensions.cs | 20 ++++++++++ 3 files changed, 75 insertions(+) create mode 100644 src/Business/Grand.Business.Core/Utilities/Checkout/CalculationContext.cs create mode 100644 src/Tests/Grand.Web.Common.Tests/Extensions/CalculationContextExtensionsTests.cs create mode 100644 src/Web/Grand.Web.Common/Extensions/CalculationContextExtensions.cs diff --git a/src/Business/Grand.Business.Core/Utilities/Checkout/CalculationContext.cs b/src/Business/Grand.Business.Core/Utilities/Checkout/CalculationContext.cs new file mode 100644 index 0000000000..07db7de6a2 --- /dev/null +++ b/src/Business/Grand.Business.Core/Utilities/Checkout/CalculationContext.cs @@ -0,0 +1,18 @@ +using Grand.Domain.Customers; +using Grand.Domain.Directory; +using Grand.Domain.Stores; +using Grand.Domain.Tax; + +namespace Grand.Business.Core.Utilities.Checkout; + +/// +/// Who and where a checkout calculation is for: the customer whose cart, discounts and loyalty points apply, +/// the store, the currency amounts are rounded and converted in, and whether prices include tax +/// +public sealed record CalculationContext(Customer Customer, Store Store, Currency Currency, TaxDisplayType TaxDisplayType) +{ + /// + /// Gets a value indicating whether prices are calculated including tax + /// + public bool IncludingTax => TaxDisplayType == TaxDisplayType.IncludingTax; +} diff --git a/src/Tests/Grand.Web.Common.Tests/Extensions/CalculationContextExtensionsTests.cs b/src/Tests/Grand.Web.Common.Tests/Extensions/CalculationContextExtensionsTests.cs new file mode 100644 index 0000000000..8720a69837 --- /dev/null +++ b/src/Tests/Grand.Web.Common.Tests/Extensions/CalculationContextExtensionsTests.cs @@ -0,0 +1,37 @@ +using Grand.Domain.Customers; +using Grand.Domain.Directory; +using Grand.Domain.Stores; +using Grand.Domain.Tax; +using Grand.Infrastructure; +using Grand.Web.Common.Extensions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Common.Tests.Extensions; + +[TestClass] +public class CalculationContextExtensionsTests +{ + [TestMethod] + public void CalculationContext_TakesCustomerStoreCurrencyAndTaxDisplayOfTheRequest() + { + //Arrange + var customer = new Customer(); + var store = new Store { Id = "s1" }; + var currency = new Currency { CurrencyCode = "EUR" }; + var accessor = new Mock(); + accessor.Setup(a => a.WorkContext.CurrentCustomer).Returns(customer); + accessor.Setup(a => a.WorkContext.WorkingCurrency).Returns(currency); + accessor.Setup(a => a.WorkContext.TaxDisplayType).Returns(TaxDisplayType.IncludingTax); + accessor.Setup(a => a.StoreContext.CurrentStore).Returns(store); + + //Act + var context = accessor.Object.CalculationContext(); + + //Assert + Assert.AreSame(customer, context.Customer); + Assert.AreSame(store, context.Store); + Assert.AreSame(currency, context.Currency); + Assert.IsTrue(context.IncludingTax); + } +} diff --git a/src/Web/Grand.Web.Common/Extensions/CalculationContextExtensions.cs b/src/Web/Grand.Web.Common/Extensions/CalculationContextExtensions.cs new file mode 100644 index 0000000000..bad1405815 --- /dev/null +++ b/src/Web/Grand.Web.Common/Extensions/CalculationContextExtensions.cs @@ -0,0 +1,20 @@ +using Grand.Business.Core.Utilities.Checkout; +using Grand.Infrastructure; + +namespace Grand.Web.Common.Extensions; + +/// +/// Builds the calculation context of the current request +/// +public static class CalculationContextExtensions +{ + /// + /// Gets the calculation context of the current request: its customer, store, working currency and tax display + /// + public static CalculationContext CalculationContext(this IContextAccessor contextAccessor) + { + return new CalculationContext(contextAccessor.WorkContext.CurrentCustomer, + contextAccessor.StoreContext.CurrentStore, contextAccessor.WorkContext.WorkingCurrency, + contextAccessor.WorkContext.TaxDisplayType); + } +} From 5fe233412418c277dd24adb8a303396b32f157d9 Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 12:32:58 +0200 Subject: [PATCH 08/33] Format order email tax rates in the order currency Every other amount in the order email is formatted in the currency the order was placed in; the tax rates used whatever currency the request that sent the email was working in, which for background sending is not the customer's. --- .../Messages/GetOrderTokensCommandHandler.cs | 2 +- .../GetOrderTokensCommandHandlerTests.cs | 65 +++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 src/Tests/Grand.Business.Messages.Tests/Commands/GetOrderTokensCommandHandlerTests.cs diff --git a/src/Business/Grand.Business.Messages/Commands/Handlers/Messages/GetOrderTokensCommandHandler.cs b/src/Business/Grand.Business.Messages/Commands/Handlers/Messages/GetOrderTokensCommandHandler.cs index 2642d7bab6..fefe67d106 100644 --- a/src/Business/Grand.Business.Messages/Commands/Handlers/Messages/GetOrderTokensCommandHandler.cs +++ b/src/Business/Grand.Business.Messages/Commands/Handlers/Messages/GetOrderTokensCommandHandler.cs @@ -136,7 +136,7 @@ public async Task Handle(GetOrderTokensCommand request, Cancellatio foreach (var item in request.Order.OrderTaxes) { var taxRate = _priceFormatter.FormatTaxRate(item.Percent); - var taxValue = _priceFormatter.FormatPrice(item.Amount); + var taxValue = _priceFormatter.FormatPrice(item.Amount, currency); if (string.IsNullOrEmpty(taxRate)) taxRate = item.Percent.ToString(CultureInfo.InvariantCulture); diff --git a/src/Tests/Grand.Business.Messages.Tests/Commands/GetOrderTokensCommandHandlerTests.cs b/src/Tests/Grand.Business.Messages.Tests/Commands/GetOrderTokensCommandHandlerTests.cs new file mode 100644 index 0000000000..08816ea40c --- /dev/null +++ b/src/Tests/Grand.Business.Messages.Tests/Commands/GetOrderTokensCommandHandlerTests.cs @@ -0,0 +1,65 @@ +using Grand.Business.Core.Commands.Messages.Tokens; +using Grand.Business.Core.Interfaces.Catalog.Prices; +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Interfaces.Checkout.GiftVouchers; +using Grand.Business.Core.Interfaces.Checkout.Payments; +using Grand.Business.Core.Interfaces.Common.Addresses; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Business.Messages.Commands.Handlers.Messages; +using Grand.Domain.Customers; +using Grand.Domain.Directory; +using Grand.Domain.Localization; +using Grand.Domain.Orders; +using Grand.Domain.Stores; +using Grand.Domain.Tax; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Business.Messages.Tests.Commands; + +[TestClass] +public class GetOrderTokensCommandHandlerTests +{ + private readonly Currency _orderCurrency = new() { CurrencyCode = "EUR" }; + private GetOrderTokensCommandHandler _handler; + private Mock _priceFormatterMock; + + [TestInitialize] + public void Init() + { + var languageService = new Mock(); + languageService.Setup(x => x.GetLanguageById(It.IsAny())).ReturnsAsync(new Language { Id = "1" }); + var currencyService = new Mock(); + currencyService.Setup(x => x.GetCurrencyByCode("EUR")).ReturnsAsync(_orderCurrency); + var giftVoucherService = new Mock(); + giftVoucherService.Setup(x => x.GetAllGiftVoucherUsageHistory(It.IsAny())) + .ReturnsAsync(new List()); + _priceFormatterMock = new Mock(); + _priceFormatterMock.Setup(x => x.FormatTaxRate(It.IsAny())).Returns("23"); + _priceFormatterMock.Setup(x => x.FormatPrice(It.IsAny(), It.IsAny())) + .Returns((double price, Currency currency) => $"{currency?.CurrencyCode}{price}"); + + _handler = new GetOrderTokensCommandHandler(languageService.Object, currencyService.Object, + new Mock().Object, new Mock().Object, _priceFormatterMock.Object, + new Mock().Object, new Mock().Object, + new Mock().Object, giftVoucherService.Object, new TaxSettings()); + } + + [TestMethod] + public async Task Handle_FormatsTaxRatesInTheOrderCurrency() + { + //Arrange + var order = new Order { CustomerCurrencyCode = "EUR", CustomerLanguageId = "1", OrderTax = 10 }; + order.OrderTaxes.Add(new OrderTax { Percent = 23, Amount = 10 }); + + //Act + var result = await _handler.Handle(new GetOrderTokensCommand { + Order = order, Customer = new Customer(), Store = new Store(), Host = new DomainHost { Url = "https://localhost/" } + }, CancellationToken.None); + + //Assert + Assert.AreEqual("EUR10", result.TaxRates["23"]); + } +} From 1ce5574997734b164736a523d3899662fd51a1ab Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 12:32:59 +0200 Subject: [PATCH 09/33] Format prices in an explicitly given currency --- .../Services/Prices/PriceFormatter.cs | 26 ------------------- .../CheckoutAttributeFormatter.cs | 3 ++- .../Catalog/Prices/IPriceFormatter.cs | 7 ----- .../Services/Prices/PriceFormatterTests.cs | 2 +- .../Controllers/ShoppingCartController.cs | 14 ++++++---- .../Services/CustomerViewModelService.cs | 4 +-- .../MerchandiseReturnViewModelService.cs | 4 +-- .../Services/ProductViewModelService.cs | 2 +- .../MerchandiseReturnController.cs | 3 ++- .../Catalog/GetFormatBasePriceHandler.cs | 2 +- .../Catalog/GetSearchAutoCompleteHandler.cs | 4 +-- .../Checkout/GetShippingMethodHandler.cs | 2 +- .../GetMerchandiseReturnDetailsHandler.cs | 2 +- .../Orders/GetMerchandiseReturnsHandler.cs | 2 +- ...GetProductDetailsAttributeChangeHandler.cs | 2 +- .../Products/GetProductDetailsPageHandler.cs | 22 ++++++++-------- .../ShoppingCart/GetAddToCartHandler.cs | 8 +++--- .../GetMiniShoppingCartHandler.cs | 2 +- .../ShoppingCart/GetMiniWishlistHandler.cs | 2 +- .../ShoppingCart/GetShoppingCartHandler.cs | 12 ++++----- .../ShoppingCart/GetWishlistHandler.cs | 6 ++--- .../Models/Orders/GetMerchandiseReturns.cs | 2 ++ 22 files changed, 54 insertions(+), 79 deletions(-) diff --git a/src/Business/Grand.Business.Catalog/Services/Prices/PriceFormatter.cs b/src/Business/Grand.Business.Catalog/Services/Prices/PriceFormatter.cs index 0bd8c531b9..aa8e381421 100644 --- a/src/Business/Grand.Business.Catalog/Services/Prices/PriceFormatter.cs +++ b/src/Business/Grand.Business.Catalog/Services/Prices/PriceFormatter.cs @@ -1,6 +1,5 @@ using Grand.Business.Core.Interfaces.Catalog.Prices; using Grand.Domain.Directory; -using Grand.Infrastructure; using System.Globalization; namespace Grand.Business.Catalog.Services.Prices; @@ -10,21 +9,6 @@ namespace Grand.Business.Catalog.Services.Prices; /// public class PriceFormatter : IPriceFormatter { - #region Fields - - private readonly IContextAccessor _contextAccessor; - - #endregion - - #region Constructors - - public PriceFormatter(IContextAccessor contextAccessor) - { - _contextAccessor = contextAccessor; - } - - #endregion - #region Utilities /// @@ -66,16 +50,6 @@ protected virtual string GetCurrencyString(double amount, Currency targetCurrenc #region Methods - /// - /// Formats the price - /// - /// Price - /// Price - public virtual string FormatPrice(double price) - { - return FormatPrice(price, _contextAccessor.WorkContext.WorkingCurrency); - } - /// /// Formats the price /// diff --git a/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeFormatter.cs b/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeFormatter.cs index 28387414cd..6e7708ed12 100644 --- a/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeFormatter.cs +++ b/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeFormatter.cs @@ -138,7 +138,8 @@ await _currencyService.ConvertFromPrimaryStoreCurrency(priceAdjustmentBase, _contextAccessor.WorkContext.WorkingCurrency); if (priceAdjustmentBase > 0) { - var priceAdjustmentStr = _priceFormatter.FormatPrice(priceAdjustment); + var priceAdjustmentStr = _priceFormatter.FormatPrice(priceAdjustment, + _contextAccessor.WorkContext.WorkingCurrency); formattedAttribute += $" [+{priceAdjustmentStr}]"; } } diff --git a/src/Business/Grand.Business.Core/Interfaces/Catalog/Prices/IPriceFormatter.cs b/src/Business/Grand.Business.Core/Interfaces/Catalog/Prices/IPriceFormatter.cs index be5a8c06bf..de287f0899 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Catalog/Prices/IPriceFormatter.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Catalog/Prices/IPriceFormatter.cs @@ -4,13 +4,6 @@ namespace Grand.Business.Core.Interfaces.Catalog.Prices; public interface IPriceFormatter { - /// - /// Formats price - /// - /// Price - /// Price - string FormatPrice(double price); - /// /// Formats price /// diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Prices/PriceFormatterTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Prices/PriceFormatterTests.cs index 3e4199f545..cde9b0d9e7 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Prices/PriceFormatterTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Prices/PriceFormatterTests.cs @@ -99,7 +99,7 @@ public void TestInitialize() _translationService = tempLocalizationService.Object; } - _priceFormatter = new PriceFormatter(tempWorkContext.Object); + _priceFormatter = new PriceFormatter(); } [TestMethod] diff --git a/src/Web/Grand.Web.Admin/Controllers/ShoppingCartController.cs b/src/Web/Grand.Web.Admin/Controllers/ShoppingCartController.cs index 2c63fcb0c4..4839cbf2b6 100644 --- a/src/Web/Grand.Web.Admin/Controllers/ShoppingCartController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/ShoppingCartController.cs @@ -7,6 +7,7 @@ using Grand.Business.Core.Interfaces.Customers; using Grand.Domain.Permissions; using Grand.Domain.Orders; +using Grand.Infrastructure; using Grand.Web.AdminShared.Models.ShoppingCart; using Grand.Web.Common.DataSource; using Grand.Web.Common.Security.Authorization; @@ -27,7 +28,8 @@ public ShoppingCartController(ICustomerService customerService, IPricingService priceCalculationService, ITranslationService translationService, IProductAttributeFormatter productAttributeFormatter, - IProductService productService) + IProductService productService, + IContextAccessor contextAccessor) { _customerService = customerService; _dateTimeService = dateTimeService; @@ -38,6 +40,7 @@ public ShoppingCartController(ICustomerService customerService, _translationService = translationService; _productAttributeFormatter = productAttributeFormatter; _productService = productService; + _contextAccessor = contextAccessor; } #endregion @@ -53,6 +56,7 @@ public ShoppingCartController(ICustomerService customerService, private readonly ITranslationService _translationService; private readonly IProductAttributeFormatter _productAttributeFormatter; private readonly IProductService _productService; + private readonly IContextAccessor _contextAccessor; #endregion @@ -108,9 +112,9 @@ public async Task GetCartDetails(string customerId) ProductName = product.Name, AttributeInfo = await _productAttributeFormatter.FormatAttributes(product, sci.Attributes, customer), UnitPrice = _priceFormatter.FormatPrice((await _taxService.GetProductPrice(product, - (await _pricingService.GetUnitPrice(sci, product)).unitprice)).productprice), + (await _pricingService.GetUnitPrice(sci, product)).unitprice)).productprice, _contextAccessor.WorkContext.WorkingCurrency), Total = _priceFormatter.FormatPrice((await _taxService.GetProductPrice(product, - (await _pricingService.GetSubTotal(sci, product)).subTotal)).productprice), + (await _pricingService.GetSubTotal(sci, product)).subTotal)).productprice, _contextAccessor.WorkContext.WorkingCurrency), UpdatedOn = sci.UpdatedOnUtc.HasValue ? _dateTimeService.ConvertToUserTime(sci.UpdatedOnUtc.Value, DateTimeKind.Utc) : _dateTimeService.ConvertToUserTime(sci.CreatedOnUtc, DateTimeKind.Utc) @@ -169,9 +173,9 @@ public async Task GetWishlistDetails(string customerId) ProductName = product.Name, AttributeInfo = await _productAttributeFormatter.FormatAttributes(product, sci.Attributes, customer), UnitPrice = _priceFormatter.FormatPrice((await _taxService.GetProductPrice(product, - (await _pricingService.GetUnitPrice(sci, product)).unitprice)).productprice), + (await _pricingService.GetUnitPrice(sci, product)).unitprice)).productprice, _contextAccessor.WorkContext.WorkingCurrency), Total = _priceFormatter.FormatPrice((await _taxService.GetProductPrice(product, - (await _pricingService.GetSubTotal(sci, product)).subTotal)).productprice), + (await _pricingService.GetSubTotal(sci, product)).subTotal)).productprice, _contextAccessor.WorkContext.WorkingCurrency), UpdatedOn = sci.UpdatedOnUtc.HasValue ? _dateTimeService.ConvertToUserTime(sci.UpdatedOnUtc.Value, DateTimeKind.Utc) : _dateTimeService.ConvertToUserTime(sci.CreatedOnUtc, DateTimeKind.Utc) diff --git a/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs index 6f39b69a61..da471da101 100644 --- a/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs @@ -870,10 +870,10 @@ public virtual async Task> PrepareShoppingCartItemM Quantity = sci.Quantity, ProductName = product.Name, AttributeInfo = await productAttributeFormatter.FormatAttributes(product, sci.Attributes), - UnitPrice = priceFormatter.FormatPrice(price), + UnitPrice = priceFormatter.FormatPrice(price, _contextAccessor.WorkContext.WorkingCurrency), UnitPriceValue = price, Total = priceFormatter.FormatPrice((await taxService.GetProductPrice(product, - (await priceCalculationService.GetSubTotal(sci, product)).subTotal)).productprice), + (await priceCalculationService.GetSubTotal(sci, product)).subTotal)).productprice, _contextAccessor.WorkContext.WorkingCurrency), UpdatedOn = sci.UpdatedOnUtc.HasValue ? _dateTimeService.ConvertToUserTime(sci.UpdatedOnUtc.Value, DateTimeKind.Utc) : _dateTimeService.ConvertToUserTime(sci.CreatedOnUtc, DateTimeKind.Utc) diff --git a/src/Web/Grand.Web.AdminShared/Services/MerchandiseReturnViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/MerchandiseReturnViewModelService.cs index 351deae882..9d9bf62e10 100644 --- a/src/Web/Grand.Web.AdminShared/Services/MerchandiseReturnViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/MerchandiseReturnViewModelService.cs @@ -58,7 +58,7 @@ public virtual async Task PrepareMerchandiseReturnModel( unitPriceInclTaxInCustomerCurrency += orderItem.UnitPriceInclTax * item.Quantity; } - model.Total = priceFormatter.FormatPrice(unitPriceInclTaxInCustomerCurrency); + model.Total = priceFormatter.FormatPrice(unitPriceInclTaxInCustomerCurrency, contextAccessor.WorkContext.WorkingCurrency); model.Quantity = merchandiseReturn.MerchandiseReturnItems.Sum(x => x.Quantity); model.Id = merchandiseReturn.Id; model.OrderId = order.Id; @@ -230,7 +230,7 @@ public virtual async Task produ bidsModel.Add(new ProductModel.BidModel { BidId = x.Id, ProductId = x.ProductId, - Amount = priceFormatter.FormatPrice(x.Amount), + Amount = priceFormatter.FormatPrice(x.Amount, contextAccessor.WorkContext.WorkingCurrency), Date = dateTimeService.ConvertToUserTime(x.Date, DateTimeKind.Utc), CustomerId = x.CustomerId, Email = (await customerService.GetCustomerById(x.CustomerId))?.Email, diff --git a/src/Web/Grand.Web/Controllers/MerchandiseReturnController.cs b/src/Web/Grand.Web/Controllers/MerchandiseReturnController.cs index 3d57264db4..38200dc471 100644 --- a/src/Web/Grand.Web/Controllers/MerchandiseReturnController.cs +++ b/src/Web/Grand.Web/Controllers/MerchandiseReturnController.cs @@ -113,7 +113,8 @@ public virtual async Task CustomerMerchandiseReturns() var model = await _mediator.Send(new GetMerchandiseReturns { Customer = _contextAccessor.WorkContext.CurrentCustomer, Store = _contextAccessor.StoreContext.CurrentStore, - Language = _contextAccessor.WorkContext.WorkingLanguage + Language = _contextAccessor.WorkContext.WorkingLanguage, + Currency = _contextAccessor.WorkContext.WorkingCurrency }); return View(model); diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetFormatBasePriceHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetFormatBasePriceHandler.cs index 255e386ab1..49f0580509 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetFormatBasePriceHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetFormatBasePriceHandler.cs @@ -49,7 +49,7 @@ public async Task Handle(GetFormatBasePrice request, CancellationToken c //do not round. otherwise, it can cause issues await _measureService.ConvertWeight(productAmount, productUnit, referenceUnit, false) * referenceAmount; - var basePriceStr = _priceFormatter.FormatPrice(basePrice); + var basePriceStr = _priceFormatter.FormatPrice(basePrice, request.Currency); var result = string.Format(_translationService.GetResource("Products.BasePrice"), basePriceStr, referenceAmount.ToString("G29"), referenceUnit.Name); diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetSearchAutoCompleteHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetSearchAutoCompleteHandler.cs index a58f1b49cf..0be55d7b02 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetSearchAutoCompleteHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetSearchAutoCompleteHandler.cs @@ -251,8 +251,8 @@ public async Task> Handle(GetSearchAutoComplete r request.Currency, includeDiscounts: true); var finalPriceWithDiscount = (await _taxService.GetProductPrice(product, appliedPrice.finalPrice)).productprice; - var price = _priceFormatter.FormatPrice(finalPriceWithoutDiscount); - var priceWithDiscount = _priceFormatter.FormatPrice(finalPriceWithDiscount); + var price = _priceFormatter.FormatPrice(finalPriceWithoutDiscount, request.Currency); + var priceWithDiscount = _priceFormatter.FormatPrice(finalPriceWithDiscount, request.Currency); return (price, priceWithDiscount); } diff --git a/src/Web/Grand.Web/Features/Handlers/Checkout/GetShippingMethodHandler.cs b/src/Web/Grand.Web/Features/Handlers/Checkout/GetShippingMethodHandler.cs index c890deeae2..6294ad5ee2 100644 --- a/src/Web/Grand.Web/Features/Handlers/Checkout/GetShippingMethodHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Checkout/GetShippingMethodHandler.cs @@ -65,7 +65,7 @@ await _customerService.UpdateUserField(request.Customer, shippingOption.Rate, request.Cart)).shippingRate; var rateBase = (await _taxService.GetShippingPrice(shippingTotal, request.Customer, request.Store)).shippingPrice; - soModel.Fee = _priceFormatter.FormatPrice(rateBase); + soModel.Fee = _priceFormatter.FormatPrice(rateBase, request.Currency); model.ShippingMethods.Add(soModel); } diff --git a/src/Web/Grand.Web/Features/Handlers/Orders/GetMerchandiseReturnDetailsHandler.cs b/src/Web/Grand.Web/Features/Handlers/Orders/GetMerchandiseReturnDetailsHandler.cs index 9847684149..7892df35c0 100644 --- a/src/Web/Grand.Web/Features/Handlers/Orders/GetMerchandiseReturnDetailsHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Orders/GetMerchandiseReturnDetailsHandler.cs @@ -77,7 +77,7 @@ public async Task Handle(GetMerchandiseReturnDeta orderItem.UnitPriceInclTax : //excluding tax - orderItem.UnitPriceExclTax); + orderItem.UnitPriceExclTax, _contextAccessor.WorkContext.WorkingCurrency); model.MerchandiseReturnItems.Add(new MerchandiseReturnDetailsModel.MerchandiseReturnItemModel { OrderItemId = item.OrderItemId, diff --git a/src/Web/Grand.Web/Features/Handlers/Orders/GetMerchandiseReturnsHandler.cs b/src/Web/Grand.Web/Features/Handlers/Orders/GetMerchandiseReturnsHandler.cs index 1c2fc7941d..59d8d5897a 100644 --- a/src/Web/Grand.Web/Features/Handlers/Orders/GetMerchandiseReturnsHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Orders/GetMerchandiseReturnsHandler.cs @@ -73,7 +73,7 @@ public async Task Handle(GetMerchandiseReturns MerchandiseReturnStatus = _enumTranslationService.GetTranslationEnum(merchandiseReturn.MerchandiseReturnStatus), CreatedOn = _dateTimeService.ConvertToUserTime(merchandiseReturn.CreatedOnUtc, DateTimeKind.Utc), ProductsCount = merchandiseReturn.MerchandiseReturnItems.Sum(x => x.Quantity), - ReturnTotal = _priceFormatter.FormatPrice(total) + ReturnTotal = _priceFormatter.FormatPrice(total, request.Currency) }; model.Items.Add(itemModel); diff --git a/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsAttributeChangeHandler.cs b/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsAttributeChangeHandler.cs index e5cc106dfa..e7b8bda960 100644 --- a/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsAttributeChangeHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsAttributeChangeHandler.cs @@ -101,7 +101,7 @@ await _mediator.Send( var finalPrice = unitprice.unitprice; var productprice = await _taxService.GetProductPrice(request.Product, finalPrice); var finalPriceWithDiscount = productprice.productprice; - model.Price = _priceFormatter.FormatPrice(finalPriceWithDiscount); + model.Price = _priceFormatter.FormatPrice(finalPriceWithDiscount, request.Currency); } //stock diff --git a/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsPageHandler.cs b/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsPageHandler.cs index c678397a0b..433218e4e3 100644 --- a/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsPageHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsPageHandler.cs @@ -456,7 +456,7 @@ private async Task PrepareStandardProperties(Product produc if (model.AdditionalShippingCharge > 0) model.AdditionalShippingChargeStr = _priceFormatter.FormatPrice( (await _taxService.GetShippingPrice(model.AdditionalShippingCharge, _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)) - .shippingPrice); + .shippingPrice, _contextAccessor.WorkContext.WorkingCurrency); //ask question us on the product if (model.AskQuestionOnProduct) @@ -696,9 +696,9 @@ await _currencyService.ConvertFromPrimaryStoreCurrency(oldPriceBase, _contextAccessor.WorkContext.WorkingCurrency); if (finalPriceWithoutDiscount != oldPrice && oldPrice > 0) - model.OldPrice = _priceFormatter.FormatPrice(oldPrice); + model.OldPrice = _priceFormatter.FormatPrice(oldPrice, _contextAccessor.WorkContext.WorkingCurrency); - model.Price = _priceFormatter.FormatPrice(finalPriceWithoutDiscount); + model.Price = _priceFormatter.FormatPrice(finalPriceWithoutDiscount, _contextAccessor.WorkContext.WorkingCurrency); if (appliedPrice.appliedDiscounts.Any()) model.AppliedDiscounts = appliedPrice.appliedDiscounts; if (appliedPrice.preferredTierPrice != null) @@ -709,11 +709,11 @@ await _currencyService.ConvertFromPrimaryStoreCurrency(oldPriceBase, var catalogPrice = await _currencyService.ConvertFromPrimaryStoreCurrency(product.CatalogPrice, _contextAccessor.WorkContext.WorkingCurrency); - model.CatalogPrice = _priceFormatter.FormatPrice(catalogPrice); + model.CatalogPrice = _priceFormatter.FormatPrice(catalogPrice, _contextAccessor.WorkContext.WorkingCurrency); } if (finalPriceWithoutDiscount != finalPriceWithDiscount) - model.PriceWithDiscount = _priceFormatter.FormatPrice(finalPriceWithDiscount); + model.PriceWithDiscount = _priceFormatter.FormatPrice(finalPriceWithDiscount, _contextAccessor.WorkContext.WorkingCurrency); model.PriceValue = finalPriceWithDiscount; @@ -731,7 +731,7 @@ await _currencyService.ConvertFromPrimaryStoreCurrency(product.CatalogPrice, if (product.ProductTypeId == ProductType.Reservation) { model.IsReservation = true; - var priceStr = _priceFormatter.FormatPrice(finalPriceWithDiscount); + var priceStr = _priceFormatter.FormatPrice(finalPriceWithDiscount, _contextAccessor.WorkContext.WorkingCurrency); model.ReservationPrice = string.Format(_translationService.GetResource(product.ResourceReservationProductPeriod()), priceStr, product.Interval); @@ -743,13 +743,13 @@ await _currencyService.ConvertFromPrimaryStoreCurrency(product.CatalogPrice, var highestBid = await _currencyService.ConvertFromPrimaryStoreCurrency(product.HighestBid, _contextAccessor.WorkContext.WorkingCurrency); - model.HighestBid = _priceFormatter.FormatPrice(highestBid); + model.HighestBid = _priceFormatter.FormatPrice(highestBid, _contextAccessor.WorkContext.WorkingCurrency); model.HighestBidValue = highestBid; model.DisableBuyButton = product.DisableBuyButton; var startPrice = await _currencyService.ConvertFromPrimaryStoreCurrency(product.StartPrice, _contextAccessor.WorkContext.WorkingCurrency); - model.StartPrice = _priceFormatter.FormatPrice(startPrice); + model.StartPrice = _priceFormatter.FormatPrice(startPrice, _contextAccessor.WorkContext.WorkingCurrency); model.StartPriceValue = startPrice; } } @@ -832,8 +832,8 @@ await _currencyService.ConvertFromPrimaryStoreCurrency(product.MaxEnteredPrice, : minimumCustomerEnteredPrice; model.CustomerEnteredPriceRange = string.Format( _translationService.GetResource("Products.EnterProductPrice.Range"), - _priceFormatter.FormatPrice(minimumCustomerEnteredPrice), - _priceFormatter.FormatPrice(maximumCustomerEnteredPrice)); + _priceFormatter.FormatPrice(minimumCustomerEnteredPrice, _contextAccessor.WorkContext.WorkingCurrency), + _priceFormatter.FormatPrice(maximumCustomerEnteredPrice, _contextAccessor.WorkContext.WorkingCurrency)); return model; } @@ -1165,7 +1165,7 @@ private async Task PrepareProductReservation(ProductDetailsModel model, Product var productprice = await _taxService.GetProductPrice(p1, (await _pricingService.GetFinalPrice(p1, _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, _contextAccessor.WorkContext.WorkingCurrency, includeDiscounts: true)).finalPrice); - bundleProduct.Price = _priceFormatter.FormatPrice(productprice.productprice); + bundleProduct.Price = _priceFormatter.FormatPrice(productprice.productprice, _contextAccessor.WorkContext.WorkingCurrency); bundleProduct.PriceValue = productprice.productprice; } diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs index 0a1c5afaa5..939874217e 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs @@ -113,11 +113,11 @@ await _productAttributeFormatter.FormatAttributes(request.Product, request.Attri var productprices = await _taxService.GetProductPrice(request.Product, (await _pricingService.GetUnitPrice(request.ShoppingCartItem, request.Product)).unitprice); model.Price = !request.CustomerEnteredPrice.HasValue - ? _priceFormatter.FormatPrice(productprices.productprice) - : _priceFormatter.FormatPrice(request.CustomerEnteredPrice.Value); + ? _priceFormatter.FormatPrice(productprices.productprice, request.Currency) + : _priceFormatter.FormatPrice(request.CustomerEnteredPrice.Value, request.Currency); model.DecimalPrice = request.CustomerEnteredPrice ?? productprices.productprice; model.TotalPrice = - _priceFormatter.FormatPrice(productprices.productprice * request.ShoppingCartItem.Quantity); + _priceFormatter.FormatPrice(productprices.productprice * request.ShoppingCartItem.Quantity, request.Currency); } //picture @@ -167,7 +167,7 @@ await _productAttributeFormatter.FormatAttributes(request.Product, request.Attri case ShoppingCartType.Auctions: model.IsAuction = true; model.HighestBidValue = request.Product.HighestBid; - model.HighestBid = _priceFormatter.FormatPrice(request.Product.HighestBid); + model.HighestBid = _priceFormatter.FormatPrice(request.Product.HighestBid, request.Currency); model.EndTime = request.Product.AvailableEndDateTimeUtc; break; } diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs index 58b959a18b..62b11005b1 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs @@ -189,7 +189,7 @@ public async Task Handle(GetMiniShoppingCart request, { var productprices = await _taxService.GetProductPrice(product, (await _pricingService.GetUnitPrice(sci, product)).unitprice); - cartItemModel.UnitPrice = _priceFormatter.FormatPrice(productprices.productprice); + cartItemModel.UnitPrice = _priceFormatter.FormatPrice(productprices.productprice, request.Currency); } //picture diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniWishlistHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniWishlistHandler.cs index 1d14f97d02..8d77ef2053 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniWishlistHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniWishlistHandler.cs @@ -94,7 +94,7 @@ public async Task Handle(GetMiniWishlist request, Cancellatio var productprice = await _taxService.GetProductPrice(product, (await _pricingService.GetUnitPrice(sci, product)).unitprice); - cartItemModel.UnitPrice = _priceFormatter.FormatPrice(productprice.productprice); + cartItemModel.UnitPrice = _priceFormatter.FormatPrice(productprice.productprice, request.Currency); cartItemModel.UnitPriceValue = productprice.productprice; cartItemModel.TaxRate = productprice.taxRate; } diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs index d8b6582dba..0b54ae2b60 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs @@ -251,10 +251,10 @@ await _checkoutAttributeService.GetAllCheckoutAttributes(request.Store.Id, switch (priceAdjustmentBase) { case > 0: - attributeValueModel.PriceAdjustment = "+" + _priceFormatter.FormatPrice(priceAdjustment); + attributeValueModel.PriceAdjustment = "+" + _priceFormatter.FormatPrice(priceAdjustment, request.Currency); break; case < 0: - attributeValueModel.PriceAdjustment = "-" + _priceFormatter.FormatPrice(-priceAdjustment); + attributeValueModel.PriceAdjustment = "-" + _priceFormatter.FormatPrice(-priceAdjustment, request.Currency); break; } } @@ -449,9 +449,9 @@ private async Task PrepareCartItems(ShoppingCartModel model, GetShoppingCart req (await _pricingService.GetUnitPrice(sci, product, false)).unitprice)).productprice; cartItemModel.UnitPriceWithoutDiscount = - _priceFormatter.FormatPrice(cartItemModel.UnitPriceWithoutDiscountValue); + _priceFormatter.FormatPrice(cartItemModel.UnitPriceWithoutDiscountValue, request.Currency); cartItemModel.UnitPriceValue = productprices.productprice; - cartItemModel.UnitPrice = _priceFormatter.FormatPrice(productprices.productprice); + cartItemModel.UnitPrice = _priceFormatter.FormatPrice(productprices.productprice, request.Currency); if (appliedDiscounts.Any()) { if (appliedDiscounts.Any(x => x.MaximumDiscountedQuantity.HasValue)) @@ -466,7 +466,7 @@ private async Task PrepareCartItems(ShoppingCartModel model, GetShoppingCart req var shoppingCartItemDiscountBase = subtotal.discountAmount; var shoppingCartItemSubTotalWithDiscount = (await _taxService.GetProductPrice(product, subtotal.subTotal)).productprice; - cartItemModel.SubTotal = _priceFormatter.FormatPrice(shoppingCartItemSubTotalWithDiscount); + cartItemModel.SubTotal = _priceFormatter.FormatPrice(shoppingCartItemSubTotalWithDiscount, request.Currency); cartItemModel.SubTotalValue = shoppingCartItemSubTotalWithDiscount; //display an applied discount amount @@ -475,7 +475,7 @@ private async Task PrepareCartItems(ShoppingCartModel model, GetShoppingCart req shoppingCartItemDiscountBase = (await _taxService.GetProductPrice(product, shoppingCartItemDiscountBase)).productprice; if (shoppingCartItemDiscountBase > 0) - cartItemModel.Discount = _priceFormatter.FormatPrice(shoppingCartItemDiscountBase); + cartItemModel.Discount = _priceFormatter.FormatPrice(shoppingCartItemDiscountBase, request.Currency); } } diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs index ebb9de49f5..d1d0985302 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs @@ -143,7 +143,7 @@ public async Task Handle(GetWishlist request, CancellationToken c { var productprice = await _taxService.GetProductPrice(product, (await _pricingService.GetUnitPrice(sci, product)).unitprice); - cartItemModel.UnitPrice = _priceFormatter.FormatPrice(productprice.productprice); + cartItemModel.UnitPrice = _priceFormatter.FormatPrice(productprice.productprice, request.Currency); } //subtotal, discount @@ -157,7 +157,7 @@ public async Task Handle(GetWishlist request, CancellationToken c var subtotal = await _pricingService.GetSubTotal(sci, product); var shoppingCartItemDiscountBase = subtotal.discountAmount; var productprices = await _taxService.GetProductPrice(product, subtotal.subTotal); - cartItemModel.SubTotal = _priceFormatter.FormatPrice(productprices.productprice); + cartItemModel.SubTotal = _priceFormatter.FormatPrice(productprices.productprice, request.Currency); //display an applied discount amount if (shoppingCartItemDiscountBase > 0) @@ -165,7 +165,7 @@ public async Task Handle(GetWishlist request, CancellationToken c shoppingCartItemDiscountBase = (await _taxService.GetProductPrice(product, shoppingCartItemDiscountBase)).productprice; if (shoppingCartItemDiscountBase > 0) - cartItemModel.Discount = _priceFormatter.FormatPrice(shoppingCartItemDiscountBase); + cartItemModel.Discount = _priceFormatter.FormatPrice(shoppingCartItemDiscountBase, request.Currency); } } diff --git a/src/Web/Grand.Web/Features/Models/Orders/GetMerchandiseReturns.cs b/src/Web/Grand.Web/Features/Models/Orders/GetMerchandiseReturns.cs index 4860b1f9f7..76d7251041 100644 --- a/src/Web/Grand.Web/Features/Models/Orders/GetMerchandiseReturns.cs +++ b/src/Web/Grand.Web/Features/Models/Orders/GetMerchandiseReturns.cs @@ -1,4 +1,5 @@ using Grand.Domain.Customers; +using Grand.Domain.Directory; using Grand.Domain.Localization; using Grand.Domain.Stores; using Grand.Web.Models.Orders; @@ -11,4 +12,5 @@ public class GetMerchandiseReturns : IRequest public Customer Customer { get; set; } public Domain.Stores.Store Store { get; set; } public Language Language { get; set; } + public Currency Currency { get; set; } } \ No newline at end of file From 52b5c0da7ede8a154d7a6a6c482bfd94144accc1 Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 14:45:19 +0200 Subject: [PATCH 10/33] Pass the customer explicitly to checkout attribute queries CheckoutAttributeService no longer reads the current customer for its group filter; callers pass the customer whose groups apply. The admin condition picker has no request customer, so it now loads attributes without the group filter, like the attribute list on the same screen. Before, it hid attributes limited to groups the staff member was not in. --- .../CheckoutAttributeService.cs | 10 ++- ...ShoppingCartCheckoutAttributesValidator.cs | 2 +- .../ICheckoutAttributeService.cs | 4 +- .../CheckoutAttributeParserTests.cs | 4 +- .../CheckoutAttributeServiceTests.cs | 51 +++++++++++++-- .../Validators/ShoppingCartValidatorsTests.cs | 4 +- .../CheckoutAttributeViewModelServiceTests.cs | 62 +++++++++++++++++++ .../CheckoutAttributeViewModelService.cs | 4 +- .../SaveCheckoutAttributesCommandHandler.cs | 2 +- .../Controllers/ShoppingCartController.cs | 2 +- .../GetMiniShoppingCartHandler.cs | 2 +- .../ShoppingCart/GetShoppingCartHandler.cs | 2 +- 12 files changed, 126 insertions(+), 23 deletions(-) create mode 100644 src/Tests/Grand.Web.Admin.Tests/Services/CheckoutAttributeViewModelServiceTests.cs diff --git a/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeService.cs b/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeService.cs index 6bc68c5850..f3c70183de 100644 --- a/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeService.cs +++ b/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeService.cs @@ -2,7 +2,6 @@ using Grand.Data; using Grand.Domain.Customers; using Grand.Domain.Orders; -using Grand.Infrastructure; using Grand.Infrastructure.Caching; using Grand.Infrastructure.Caching.Constants; using Grand.Infrastructure.Configuration; @@ -25,12 +24,11 @@ public CheckoutAttributeService( ICacheBase cacheBase, IRepository checkoutAttributeRepository, IMediator mediator, - IContextAccessor contextAccessor, AccessControlConfig accessControlConfig) + AccessControlConfig accessControlConfig) { _cacheBase = cacheBase; _checkoutAttributeRepository = checkoutAttributeRepository; _mediator = mediator; - _contextAccessor = contextAccessor; _accessControlConfig = accessControlConfig; } @@ -41,7 +39,6 @@ public CheckoutAttributeService( private readonly IRepository _checkoutAttributeRepository; private readonly IMediator _mediator; private readonly ICacheBase _cacheBase; - private readonly IContextAccessor _contextAccessor; private readonly AccessControlConfig _accessControlConfig; #endregion @@ -52,10 +49,11 @@ public CheckoutAttributeService( /// Gets all checkout attributes /// /// Store identifier + /// Customer whose groups limit the result (not read when ignoreAcl is true) /// A value indicating whether we should exclude shippable attributes /// /// Checkout attributes - public virtual async Task> GetAllCheckoutAttributes(string storeId = "", + public virtual async Task> GetAllCheckoutAttributes(string storeId, Customer customer, bool excludeShippableAttributes = false, bool ignoreAcl = false) { var key = string.Format(CacheKey.CHECKOUTATTRIBUTES_ALL_KEY, storeId, excludeShippableAttributes, ignoreAcl); @@ -71,7 +69,7 @@ public virtual async Task> GetAllCheckoutAttributes(str { if (!ignoreAcl && !_accessControlConfig.IgnoreAcl) { - var allowedCustomerGroupsIds = _contextAccessor.WorkContext.CurrentCustomer.GetCustomerGroupIds(); + var allowedCustomerGroupsIds = customer.GetCustomerGroupIds(); query = from p in query where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x)) select p; diff --git a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartCheckoutAttributesValidator.cs b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartCheckoutAttributesValidator.cs index 24ef123b73..fa358049b0 100644 --- a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartCheckoutAttributesValidator.cs +++ b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartCheckoutAttributesValidator.cs @@ -27,7 +27,7 @@ public ShoppingCartCheckoutAttributesValidator(ITranslationService translationSe //existing checkout attributes var attributes2 = - await checkoutAttributeService.GetAllCheckoutAttributes(value.Store.Id, + await checkoutAttributeService.GetAllCheckoutAttributes(value.Store.Id, value.Customer, !value.ShoppingCarts.RequiresShipping()); foreach (var a2 in attributes2) { diff --git a/src/Business/Grand.Business.Core/Interfaces/Checkout/CheckoutAttributes/ICheckoutAttributeService.cs b/src/Business/Grand.Business.Core/Interfaces/Checkout/CheckoutAttributes/ICheckoutAttributeService.cs index a6897ba1a9..4fcb731ee8 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Checkout/CheckoutAttributes/ICheckoutAttributeService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Checkout/CheckoutAttributes/ICheckoutAttributeService.cs @@ -1,3 +1,4 @@ +using Grand.Domain.Customers; using Grand.Domain.Orders; namespace Grand.Business.Core.Interfaces.Checkout.CheckoutAttributes; @@ -13,10 +14,11 @@ public interface ICheckoutAttributeService /// Gets all checkout attributes /// /// Store identifier + /// Customer whose groups limit the result (not read when ignoreAcl is true) /// A value indicating whether we should exclude shippable attributes /// /// Checkout attributes - Task> GetAllCheckoutAttributes(string storeId = "", + Task> GetAllCheckoutAttributes(string storeId, Customer customer, bool excludeShippableAttributes = false, bool ignoreAcl = false); /// diff --git a/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeParserTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeParserTests.cs index 5c5131eec3..4366aafc1d 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeParserTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeParserTests.cs @@ -23,7 +23,6 @@ public class CheckoutAttributeParserTests private IRepository _checkoutAttributeRepo; private ICheckoutAttributeService _checkoutAttributeService; private IMediator _eventPublisher; - private Mock _workContextMock; private CheckoutAttribute ca1, ca2, ca3; private Mock cacheManager; @@ -119,10 +118,9 @@ public void TestInitialize() } cacheManager = new Mock(); - _workContextMock = new Mock(); _checkoutAttributeService = new CheckoutAttributeService(cacheManager.Object, _checkoutAttributeRepo, - _eventPublisher, _workContextMock.Object, new AccessControlConfig()); + _eventPublisher, new AccessControlConfig()); _checkoutAttributeParser = new CheckoutAttributeParser(_checkoutAttributeService); diff --git a/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeServiceTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeServiceTests.cs index 71eff8af83..22236a572d 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeServiceTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeServiceTests.cs @@ -1,8 +1,9 @@ using Grand.Business.Checkout.Services.CheckoutAttributes; using Grand.Business.Core.Interfaces.Checkout.CheckoutAttributes; using Grand.Data; +using Grand.Data.Tests.MongoDb; +using Grand.Domain.Customers; using Grand.Domain.Orders; -using Grand.Infrastructure; using Grand.Infrastructure.Caching; using Grand.Infrastructure.Configuration; using Grand.Infrastructure.Events; @@ -19,7 +20,6 @@ public class CheckoutAttributeServiceTests private Mock _mediatorMock; private Mock> _repositoryMock; private ICheckoutAttributeService _service; - private Mock _workContextMock; [TestInitialize] public void Init() @@ -27,9 +27,8 @@ public void Init() _cacheMock = new Mock(); _repositoryMock = new Mock>(); _mediatorMock = new Mock(); - _workContextMock = new Mock(); _service = new CheckoutAttributeService(_cacheMock.Object, _repositoryMock.Object, _mediatorMock.Object, - _workContextMock.Object, new AccessControlConfig()); + new AccessControlConfig()); } [TestMethod] @@ -76,4 +75,48 @@ public void DeleteCheckoutAttribute_NullArgument_ThrowException() { Assert.ThrowsExactlyAsync(async () => await _service.DeleteCheckoutAttribute(null)); } + + [TestMethod] + public async Task GetAllCheckoutAttributes_FiltersByTheGroupsOfTheGivenCustomer() + { + //Arrange + var repository = new MongoDBRepositoryTest(); + await repository.InsertAsync(new CheckoutAttribute { Name = "members only", LimitedToGroups = true, CustomerGroups = ["members"] }); + await repository.InsertAsync(new CheckoutAttribute { Name = "everyone" }); + var cache = new Mock(); + cache.Setup(c => c.GetAsync(It.IsAny(), It.IsAny>>>())) + .Returns((string _, Func>> acquire) => acquire()); + var service = new CheckoutAttributeService(cache.Object, repository, _mediatorMock.Object, new AccessControlConfig()); + var guest = new Customer(); + guest.Groups.Add("guests"); + + //Act + var result = await service.GetAllCheckoutAttributes("", guest); + + //Assert + Assert.AreEqual(1, result.Count); + Assert.AreEqual("everyone", result[0].Name); + } + + [TestMethod] + [Ignore("fix in next commit")] + public async Task GetAllCheckoutAttributes_CacheKeyDependsOnTheCustomerGroups() + { + //Arrange + var keys = new List(); + _cacheMock.Setup(c => c.GetAsync(It.IsAny(), It.IsAny>>>())) + .Callback((string key, Func>> _) => keys.Add(key)) + .ReturnsAsync(new List()); + var member = new Customer(); + member.Groups.Add("members"); + var guest = new Customer(); + guest.Groups.Add("guests"); + + //Act + await _service.GetAllCheckoutAttributes("store", member); + await _service.GetAllCheckoutAttributes("store", guest); + + //Assert + Assert.AreNotEqual(keys[0], keys[1]); + } } \ No newline at end of file diff --git a/src/Tests/Grand.Business.Checkout.Tests/Validators/ShoppingCartValidatorsTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Validators/ShoppingCartValidatorsTests.cs index eed2d7d6c0..a5f476fedc 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Validators/ShoppingCartValidatorsTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Validators/ShoppingCartValidatorsTests.cs @@ -123,7 +123,7 @@ public async Task ShoppingCartCheckoutAttributesValidator_Success() } })); - _checkoutAttributeServiceMock.Setup(x => x.GetAllCheckoutAttributes(It.IsAny(), true, false)) + _checkoutAttributeServiceMock.Setup(x => x.GetAllCheckoutAttributes(It.IsAny(), It.IsAny(), true, false)) .Returns(() => Task.FromResult((IList)new List { new() { Id = "1", Name = "test", IsRequired = true, AttributeControlTypeId = AttributeControlType.TextBox @@ -162,7 +162,7 @@ public async Task ShoppingCartCheckoutAttributesValidator_Fail() } })); - _checkoutAttributeServiceMock.Setup(x => x.GetAllCheckoutAttributes(It.IsAny(), true, false)) + _checkoutAttributeServiceMock.Setup(x => x.GetAllCheckoutAttributes(It.IsAny(), It.IsAny(), true, false)) .Returns(() => Task.FromResult((IList)new List { new() { Id = "1", Name = "test", IsRequired = true, AttributeControlTypeId = AttributeControlType.TextBox, diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/CheckoutAttributeViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/CheckoutAttributeViewModelServiceTests.cs new file mode 100644 index 0000000000..c37b2643a4 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Services/CheckoutAttributeViewModelServiceTests.cs @@ -0,0 +1,62 @@ +using Grand.Business.Core.Interfaces.Catalog.Directory; +using Grand.Business.Core.Interfaces.Catalog.Tax; +using Grand.Business.Core.Interfaces.Checkout.CheckoutAttributes; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Domain.Catalog; +using Grand.Domain.Common; +using Grand.Domain.Customers; +using Grand.Domain.Directory; +using Grand.Domain.Orders; +using Grand.Web.AdminShared.Models.Orders; +using Grand.Web.AdminShared.Services; +using Grand.Web.Common.Localization; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Assert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; + +namespace Grand.Web.Admin.Tests.Services; + +[TestClass] +public class CheckoutAttributeViewModelServiceTests +{ + private Mock _checkoutAttributeServiceMock; + private CheckoutAttributeViewModelService _service; + + [TestInitialize] + public void Setup() + { + _checkoutAttributeServiceMock = new Mock(); + var parserMock = new Mock(); + parserMock.Setup(x => x.ParseCheckoutAttributes(It.IsAny>())) + .ReturnsAsync(new List()); + parserMock.Setup(x => x.ParseCheckoutAttributeValues(It.IsAny>())) + .ReturnsAsync(new List()); + + _service = new CheckoutAttributeViewModelService(_checkoutAttributeServiceMock.Object, parserMock.Object, + new Mock().Object, new Mock().Object, + new Mock().Object, new CurrencySettings(), new Mock().Object, + new MeasureSettings(), new Mock().Object); + } + + [TestMethod] + public async Task PrepareConditionAttributes_OffersAttributesLimitedToCustomerGroups() + { + //Arrange + var limited = new CheckoutAttribute { + Id = "limited", Name = "Members gift", LimitedToGroups = true, CustomerGroups = ["members"], + AttributeControlTypeId = AttributeControlType.DropdownList + }; + _checkoutAttributeServiceMock + .Setup(x => x.GetAllCheckoutAttributes(It.IsAny(), It.IsAny(), false, true)) + .ReturnsAsync(new List { limited }); + var model = new CheckoutAttributeModel(); + + //Act + await _service.PrepareConditionAttributes(model, new CheckoutAttribute { Id = "edited" }); + + //Assert + Assert.AreEqual(1, model.ConditionModel.ConditionAttributes.Count); + Assert.AreEqual("limited", model.ConditionModel.ConditionAttributes[0].Id); + } +} diff --git a/src/Web/Grand.Web.AdminShared/Services/CheckoutAttributeViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CheckoutAttributeViewModelService.cs index da4b224bb4..e92b6d5238 100644 --- a/src/Web/Grand.Web.AdminShared/Services/CheckoutAttributeViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/CheckoutAttributeViewModelService.cs @@ -29,7 +29,7 @@ public class CheckoutAttributeViewModelService( { public virtual async Task> PrepareCheckoutAttributeListModel(string storeId = "") { - var checkoutAttributes = await checkoutAttributeService.GetAllCheckoutAttributes(storeId: storeId, ignoreAcl: true); + var checkoutAttributes = await checkoutAttributeService.GetAllCheckoutAttributes(storeId: storeId, customer: null, ignoreAcl: true); return checkoutAttributes.Select((Func)(x => { var attributeModel = x.ToModel(); @@ -170,7 +170,7 @@ public virtual async Task PrepareConditionAttributes(CheckoutAttributeModel mode model.ConditionModel = new ConditionModel { EnableCondition = checkoutAttribute.ConditionAttribute.Any(), SelectedAttributeId = selectedAttribute != null ? selectedAttribute.Id : "", - ConditionAttributes = (await checkoutAttributeService.GetAllCheckoutAttributes(ignoreAcl: false)) + ConditionAttributes = (await checkoutAttributeService.GetAllCheckoutAttributes(storeId: "", customer: null, ignoreAcl: true)) //ignore this attribute and non-combinable attributes .Where(x => x.Id != checkoutAttribute.Id && x.CanBeUsedAsCondition()) .Select(x => diff --git a/src/Web/Grand.Web/Commands/Handler/ShoppingCart/SaveCheckoutAttributesCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/ShoppingCart/SaveCheckoutAttributesCommandHandler.cs index 0a01f349a3..9e040f4117 100644 --- a/src/Web/Grand.Web/Commands/Handler/ShoppingCart/SaveCheckoutAttributesCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/ShoppingCart/SaveCheckoutAttributesCommandHandler.cs @@ -32,7 +32,7 @@ public async Task> Handle(SaveCheckoutAttributesCommand r var customAttributes = new List(); var checkoutAttributes = - await _checkoutAttributeService.GetAllCheckoutAttributes(request.Store.Id, + await _checkoutAttributeService.GetAllCheckoutAttributes(request.Store.Id, request.Customer, !request.Cart.RequiresShipping()); foreach (var attribute in checkoutAttributes) switch (attribute.AttributeControlTypeId) diff --git a/src/Web/Grand.Web/Controllers/ShoppingCartController.cs b/src/Web/Grand.Web/Controllers/ShoppingCartController.cs index 923eeb86c7..acf6bf4c95 100644 --- a/src/Web/Grand.Web/Controllers/ShoppingCartController.cs +++ b/src/Web/Grand.Web/Controllers/ShoppingCartController.cs @@ -141,7 +141,7 @@ public virtual async Task CheckoutAttributeChange(CheckoutAttribu var disabledAttributeIds = new List(); var attributes = await _checkoutAttributeService.GetAllCheckoutAttributes(_contextAccessor.StoreContext.CurrentStore.Id, - !cart.RequiresShipping()); + _contextAccessor.WorkContext.CurrentCustomer, !cart.RequiresShipping()); foreach (var attribute in attributes) { var conditionMet = await checkoutAttributeParser.IsConditionMet(attribute, checkoutAttributes); diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs index 8e9241c196..f109276d0a 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs @@ -122,7 +122,7 @@ public async Task Handle(GetMiniShoppingCart request, var requiresShipping = cart.RequiresShipping(); var checkoutAttributesExist = - (await _checkoutAttributeService.GetAllCheckoutAttributes(request.Store.Id, !requiresShipping)) + (await _checkoutAttributeService.GetAllCheckoutAttributes(request.Store.Id, request.Customer, !requiresShipping)) .Any(); var minOrderSubtotalAmountOk = await _mediator.Send(new ValidateMinShoppingCartSubtotalAmountCommand { diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs index b96244455a..d42391aa85 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs @@ -218,7 +218,7 @@ private async Task PrepareCheckoutAttributes(ShoppingCartModel model, GetShoppin #region Checkout attributes var checkoutAttributes = - await _checkoutAttributeService.GetAllCheckoutAttributes(request.Store.Id, + await _checkoutAttributeService.GetAllCheckoutAttributes(request.Store.Id, request.Customer, !request.Cart.RequiresShipping()); foreach (var attribute in checkoutAttributes) { From 892d86515665881fd85bcbcfa50dffcd489e155b Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 12:42:46 +0200 Subject: [PATCH 11/33] Pass tax display, customer and store explicitly to tax calculations --- .../Products/ProductAttributeFormatter.cs | 5 +- .../Services/Tax/TaxService.cs | 89 ++----------------- .../CheckoutAttributeFormatter.cs | 5 +- .../Interfaces/Catalog/Tax/ITaxService.cs | 58 +----------- .../Services/Tax/TaxServiceTests.cs | 15 +++- .../GetProductStructuredDataHandlerTests.cs | 10 ++- .../Controllers/ShoppingCartController.cs | 21 ++++- .../Services/CustomerViewModelService.cs | 9 +- .../Catalog/GetSearchAutoCompleteHandler.cs | 18 ++-- .../Checkout/GetPaymentMethodHandler.cs | 10 ++- .../Checkout/GetShippingAddressHandler.cs | 10 ++- .../Checkout/GetShippingMethodHandler.cs | 10 ++- ...GetProductDetailsAttributeChangeHandler.cs | 11 ++- .../Products/GetProductDetailsPageHandler.cs | 73 ++++++++++----- .../GetProductStructuredDataHandler.cs | 10 ++- .../ShoppingCart/GetAddToCartHandler.cs | 6 +- .../GetEstimateShippingResultHandler.cs | 13 ++- .../GetMiniShoppingCartHandler.cs | 3 +- .../ShoppingCart/GetMiniWishlistHandler.cs | 11 ++- .../ShoppingCart/GetOrderTotalsHandler.cs | 3 +- .../ShoppingCart/GetShoppingCartHandler.cs | 39 +++++--- .../ShoppingCart/GetWishlistHandler.cs | 22 +++-- 22 files changed, 239 insertions(+), 212 deletions(-) diff --git a/src/Business/Grand.Business.Catalog/Services/Products/ProductAttributeFormatter.cs b/src/Business/Grand.Business.Catalog/Services/Products/ProductAttributeFormatter.cs index 7360dbd0dd..11e085520a 100644 --- a/src/Business/Grand.Business.Catalog/Services/Products/ProductAttributeFormatter.cs +++ b/src/Business/Grand.Business.Catalog/Services/Products/ProductAttributeFormatter.cs @@ -6,6 +6,7 @@ using Grand.Domain.Common; using Grand.Domain.Customers; using Grand.Domain.Orders; +using Grand.Domain.Tax; using Grand.Infrastructure; using Grand.SharedKernel.Extensions; using System.Net; @@ -235,7 +236,9 @@ private async Task PrepareFormattedAttribute(Product product, var attributeValuePriceAdjustment = await _pricingService.GetProductAttributeValuePriceAdjustment(attributeValue); var (priceAdjustmentBase, _) = await _taxService.GetProductPrice(product, - attributeValuePriceAdjustment, _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore); + attributeValuePriceAdjustment, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore); switch (priceAdjustmentBase) { case > 0: diff --git a/src/Business/Grand.Business.Catalog/Services/Tax/TaxService.cs b/src/Business/Grand.Business.Catalog/Services/Tax/TaxService.cs index e1110c8890..e3ee25ff42 100644 --- a/src/Business/Grand.Business.Catalog/Services/Tax/TaxService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Tax/TaxService.cs @@ -8,7 +8,6 @@ using Grand.Domain.Orders; using Grand.Domain.Stores; using Grand.Domain.Tax; -using Grand.Infrastructure; using Grand.Infrastructure.Extensions; using Microsoft.Extensions.Logging; @@ -22,7 +21,6 @@ public class TaxService : ITaxService #region Ctor public TaxService( - IContextAccessor contextAccessor, IGroupService groupService, ICountryService countryService, IEnumerable taxProviders, @@ -32,7 +30,6 @@ public TaxService( CustomerSettings customerSettings, AddressSettings addressSettings) { - _contextAccessor = contextAccessor; _groupService = groupService; _taxSettings = taxSettings; _taxProviderSettings = taxProviderSettings; @@ -47,7 +44,6 @@ public TaxService( #region Fields - private readonly IContextAccessor _contextAccessor; private readonly IGroupService _groupService; private readonly ICountryService _countryService; private readonly IEnumerable _taxProviders; @@ -193,7 +189,7 @@ protected virtual double CalculatePrice(double price, double percent, bool incre var isTaxable = true; //active tax provider - var activeTaxProvider = LoadActiveTaxProvider(); + var activeTaxProvider = LoadActiveTaxProvider(customer, store?.Id); if (activeTaxProvider == null) return (taxRate, true); @@ -235,11 +231,13 @@ await IsVatExempt(calculateTaxRequest.Address, calculateTaxRequest.Customer)) /// /// Load active tax provider /// + /// Customer whose groups limit the fallback providers + /// Store whose providers are the fallback /// Active tax provider - public virtual ITaxProvider LoadActiveTaxProvider() + public virtual ITaxProvider LoadActiveTaxProvider(Customer customer, string storeId) { var taxProvider = LoadTaxProviderBySystemName(_taxProviderSettings.ActiveTaxProviderSystemName) ?? - LoadAllTaxProviders(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id) + LoadAllTaxProviders(customer, storeId) .FirstOrDefault(); return taxProvider; } @@ -270,31 +268,6 @@ public virtual IList LoadAllTaxProviders(Customer customer = null, } - /// - /// Gets price - /// - /// Product - /// Price - /// Price - public virtual async Task<(double productprice, double taxRate)> GetProductPrice(Product product, double price) - { - return await GetProductPrice(product, price, _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore); - } - - /// - /// Gets price - /// - /// Product - /// Price - /// Customer - /// Store - /// Price - public virtual async Task<(double productprice, double taxRate)> GetProductPrice(Product product, double price, Customer customer, Store store) - { - var includingTax = _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax; - return await GetProductPrice(product, price, includingTax, customer, store); - } - /// /// Gets price /// @@ -461,18 +434,6 @@ bool priceIncludesTax return productPrice; } - /// - /// Gets shipping price - /// - /// Price - /// Customer - /// Price - public virtual async Task<(double shippingPrice, double taxRate)> GetShippingPrice(double price, Customer customer, Store store) - { - var includingTax = _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax; - return await GetShippingPrice(price, includingTax, customer, store); - } - /// /// Gets shipping price /// @@ -492,19 +453,6 @@ bool priceIncludesTax return (prices.productprice, prices.taxRate); } - /// - /// Gets payment method additional handling fee - /// - /// Price - /// Customer - /// Store - /// Price - public virtual async Task<(double paymentPrice, double taxRate)> GetPaymentMethodAdditionalFee(double price, Customer customer, Store store) - { - var includingTax = _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax; - return await GetPaymentMethodAdditionalFee(price, includingTax, customer, store); - } - /// /// Gets payment method additional handling fee /// @@ -522,33 +470,6 @@ bool priceIncludesTax return (prices.productprice, prices.taxRate); } - /// - /// Gets checkout attribute value price - /// - /// Checkout attribute - /// Checkout attribute value - /// Price - public virtual async Task<(double checkoutPrice, double taxRate)> GetCheckoutAttributePrice(CheckoutAttribute ca, CheckoutAttributeValue cav) - { - var customer = _contextAccessor.WorkContext.CurrentCustomer; - var store = _contextAccessor.StoreContext.CurrentStore; - return await GetCheckoutAttributePrice(ca, cav, customer, store); - } - - /// - /// Gets checkout attribute value price - /// - /// Checkout attribute - /// Checkout attribute value - /// Customer - /// Store - /// Price - public virtual async Task<(double checkoutPrice, double taxRate)> GetCheckoutAttributePrice(CheckoutAttribute ca, CheckoutAttributeValue cav, Customer customer, Store store) - { - var includingTax = _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax; - return await GetCheckoutAttributePrice(ca, cav, includingTax, customer, store); - } - /// /// Gets checkout attribute value price /// diff --git a/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeFormatter.cs b/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeFormatter.cs index 6e7708ed12..8e8ba47e63 100644 --- a/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeFormatter.cs +++ b/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeFormatter.cs @@ -8,6 +8,7 @@ using Grand.Domain.Customers; using Grand.Domain.Orders; using Grand.Domain.Stores; +using Grand.Domain.Tax; using Grand.Infrastructure; using Grand.SharedKernel.Extensions; using System.Net; @@ -131,7 +132,9 @@ public virtual async Task FormatAttributes(IList custom if (renderPrices) { var priceAdjustmentBase = - (await _taxService.GetCheckoutAttributePrice(attribute, attributeValue, customer, store)) + (await _taxService.GetCheckoutAttributePrice(attribute, attributeValue, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + customer, store)) .checkoutPrice; var priceAdjustment = await _currencyService.ConvertFromPrimaryStoreCurrency(priceAdjustmentBase, diff --git a/src/Business/Grand.Business.Core/Interfaces/Catalog/Tax/ITaxService.cs b/src/Business/Grand.Business.Core/Interfaces/Catalog/Tax/ITaxService.cs index 9aa86712cd..f95373069f 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Catalog/Tax/ITaxService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Catalog/Tax/ITaxService.cs @@ -15,8 +15,10 @@ public interface ITaxService /// /// Load active tax provider /// + /// Customer whose groups limit the fallback providers + /// Store whose providers are the fallback /// Active tax provider - ITaxProvider LoadActiveTaxProvider(); + ITaxProvider LoadActiveTaxProvider(Customer customer, string storeId); /// /// Load tax provider by system name @@ -34,24 +36,6 @@ public interface ITaxService IList LoadAllTaxProviders(Customer customer = null, string storeId = ""); - /// - /// Gets price - /// - /// Product - /// Price - /// Price - Task<(double productprice, double taxRate)> GetProductPrice(Product product, double price); - - /// - /// Gets price - /// - /// Product - /// Price - /// Customer - /// Store - /// Price - Task<(double productprice, double taxRate)> GetProductPrice(Product product, double price, Customer customer, Store store); - /// /// Gets price /// @@ -103,15 +87,6 @@ Task GetTaxProductPrice( bool priceIncludesTax ); - /// - /// Gets shipping price - /// - /// Price - /// Customer - /// Store - /// Price - Task<(double shippingPrice, double taxRate)> GetShippingPrice(double price, Customer customer, Store store); - /// /// Gets shipping price /// @@ -122,15 +97,6 @@ bool priceIncludesTax /// Price Task<(double shippingPrice, double taxRate)> GetShippingPrice(double price, bool includingTax, Customer customer, Store store); - /// - /// Gets payment method additional handling fee - /// - /// Price - /// Customer - /// Store - /// Price - Task<(double paymentPrice, double taxRate)> GetPaymentMethodAdditionalFee(double price, Customer customer, Store store); - /// /// Gets payment method additional handling fee /// @@ -141,24 +107,6 @@ bool priceIncludesTax /// Price Task<(double paymentPrice, double taxRate)> GetPaymentMethodAdditionalFee(double price, bool includingTax, Customer customer, Store store); - /// - /// Gets checkout attribute value price - /// - /// Checkout attribute - /// Checkout attribute value - /// Price - Task<(double checkoutPrice, double taxRate)> GetCheckoutAttributePrice(CheckoutAttribute ca, CheckoutAttributeValue cav); - - /// - /// Gets checkout attribute value price - /// - /// Checkout attribute - /// Checkout attribute value - /// Customer - /// Store - /// Price - Task<(double checkoutPrice, double taxRate)> GetCheckoutAttributePrice(CheckoutAttribute ca, CheckoutAttributeValue cav, Customer customer, Store store); - /// /// Gets checkout attribute value price /// diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Tax/TaxServiceTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Tax/TaxServiceTests.cs index dfb1bc42dd..4d4d2bf5b9 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Tax/TaxServiceTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Tax/TaxServiceTests.cs @@ -50,13 +50,26 @@ public void TestInitialize() _loggerMock = new Mock>(); var providers = new List(); providers.Add(new FixedRateTestTaxProvider()); - _taxService = new TaxService(_workContext, _groupServiceMock.Object, + _taxService = new TaxService(_groupServiceMock.Object, _countryService, providers, _loggerMock.Object, _taxSettings, _taxProviderSettings, _customerSettings, _addressSettings); _vatService = new VatService(_taxSettings); } + [TestMethod] + public void LoadActiveTaxProvider_FallsBackToProvidersOfGivenCustomerAndStore() + { + //Arrange + _taxProviderSettings.ActiveTaxProviderSystemName = "not-installed"; + + //Act + var provider = _taxService.LoadActiveTaxProvider(new Customer(), "store1"); + + //Assert + Assert.AreEqual("test-provider", provider?.SystemName); + } + [TestMethod] public async Task Can_check_taxExempt_product() { diff --git a/src/Tests/Grand.Web.Tests/Features/Handlers/Products/GetProductStructuredDataHandlerTests.cs b/src/Tests/Grand.Web.Tests/Features/Handlers/Products/GetProductStructuredDataHandlerTests.cs index f743d10397..d01c6c63d7 100644 --- a/src/Tests/Grand.Web.Tests/Features/Handlers/Products/GetProductStructuredDataHandlerTests.cs +++ b/src/Tests/Grand.Web.Tests/Features/Handlers/Products/GetProductStructuredDataHandlerTests.cs @@ -6,6 +6,8 @@ using Grand.Domain.Directory; using Grand.Domain.Orders; using Grand.Domain.Shipping; +using Grand.Domain.Tax; +using Grand.Infrastructure; using Grand.Web.Features.Handlers.Products; using Grand.Web.Features.Models.Products; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -44,13 +46,15 @@ public void Init() _pricingServiceMock = new Mock(); _taxServiceMock = new Mock(); //shipping taxed at 10%, product prices passed through - _taxServiceMock.Setup(x => x.GetShippingPrice(It.IsAny(), _customer, _store)) - .ReturnsAsync((double price, Customer _, Domain.Stores.Store _) => (price * 1.1, 10)); + _taxServiceMock.Setup(x => x.GetShippingPrice(It.IsAny(), false, _customer, _store)) + .ReturnsAsync((double price, bool _, Customer _, Domain.Stores.Store _) => (price * 1.1, 10)); _taxServiceMock.Setup(x => x.GetProductPrice(It.IsAny(), It.IsAny(), It.IsAny(), _customer, _store)) .ReturnsAsync((Product _, double price, bool _, Customer _, Domain.Stores.Store _) => (price, 0)); + var contextAccessor = new Mock(); + contextAccessor.Setup(c => c.WorkContext.TaxDisplayType).Returns(TaxDisplayType.ExcludingTax); _handler = new GetProductStructuredDataHandler(_countryServiceMock.Object, _currencyServiceMock.Object, - _pricingServiceMock.Object, _taxServiceMock.Object, _orderSettings, _shippingSettings); + _pricingServiceMock.Object, _taxServiceMock.Object, _orderSettings, _shippingSettings, contextAccessor.Object); } private Task Handle(Product product) => diff --git a/src/Web/Grand.Web.Admin/Controllers/ShoppingCartController.cs b/src/Web/Grand.Web.Admin/Controllers/ShoppingCartController.cs index 4839cbf2b6..ca49d1d3a1 100644 --- a/src/Web/Grand.Web.Admin/Controllers/ShoppingCartController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/ShoppingCartController.cs @@ -7,6 +7,7 @@ using Grand.Business.Core.Interfaces.Customers; using Grand.Domain.Permissions; using Grand.Domain.Orders; +using Grand.Domain.Tax; using Grand.Infrastructure; using Grand.Web.AdminShared.Models.ShoppingCart; using Grand.Web.Common.DataSource; @@ -112,9 +113,15 @@ public async Task GetCartDetails(string customerId) ProductName = product.Name, AttributeInfo = await _productAttributeFormatter.FormatAttributes(product, sci.Attributes, customer), UnitPrice = _priceFormatter.FormatPrice((await _taxService.GetProductPrice(product, - (await _pricingService.GetUnitPrice(sci, product)).unitprice)).productprice, _contextAccessor.WorkContext.WorkingCurrency), + (await _pricingService.GetUnitPrice(sci, product)).unitprice, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice, + _contextAccessor.WorkContext.WorkingCurrency), Total = _priceFormatter.FormatPrice((await _taxService.GetProductPrice(product, - (await _pricingService.GetSubTotal(sci, product)).subTotal)).productprice, _contextAccessor.WorkContext.WorkingCurrency), + (await _pricingService.GetSubTotal(sci, product)).subTotal, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice, + _contextAccessor.WorkContext.WorkingCurrency), UpdatedOn = sci.UpdatedOnUtc.HasValue ? _dateTimeService.ConvertToUserTime(sci.UpdatedOnUtc.Value, DateTimeKind.Utc) : _dateTimeService.ConvertToUserTime(sci.CreatedOnUtc, DateTimeKind.Utc) @@ -173,9 +180,15 @@ public async Task GetWishlistDetails(string customerId) ProductName = product.Name, AttributeInfo = await _productAttributeFormatter.FormatAttributes(product, sci.Attributes, customer), UnitPrice = _priceFormatter.FormatPrice((await _taxService.GetProductPrice(product, - (await _pricingService.GetUnitPrice(sci, product)).unitprice)).productprice, _contextAccessor.WorkContext.WorkingCurrency), + (await _pricingService.GetUnitPrice(sci, product)).unitprice, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice, + _contextAccessor.WorkContext.WorkingCurrency), Total = _priceFormatter.FormatPrice((await _taxService.GetProductPrice(product, - (await _pricingService.GetSubTotal(sci, product)).subTotal)).productprice, _contextAccessor.WorkContext.WorkingCurrency), + (await _pricingService.GetSubTotal(sci, product)).subTotal, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice, + _contextAccessor.WorkContext.WorkingCurrency), UpdatedOn = sci.UpdatedOnUtc.HasValue ? _dateTimeService.ConvertToUserTime(sci.UpdatedOnUtc.Value, DateTimeKind.Utc) : _dateTimeService.ConvertToUserTime(sci.CreatedOnUtc, DateTimeKind.Utc) diff --git a/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs index da471da101..29eed72c21 100644 --- a/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs @@ -862,7 +862,9 @@ public virtual async Task> PrepareShoppingCartItemM if (product != null) { var price = (await taxService.GetProductPrice(product, - (await priceCalculationService.GetUnitPrice(sci, product)).unitprice)).productprice; + (await priceCalculationService.GetUnitPrice(sci, product)).unitprice, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice; var sciModel = new ShoppingCartItemModel { Id = sci.Id, Store = store != null ? store.Shortcut : "Unknown", @@ -873,7 +875,10 @@ public virtual async Task> PrepareShoppingCartItemM UnitPrice = priceFormatter.FormatPrice(price, _contextAccessor.WorkContext.WorkingCurrency), UnitPriceValue = price, Total = priceFormatter.FormatPrice((await taxService.GetProductPrice(product, - (await priceCalculationService.GetSubTotal(sci, product)).subTotal)).productprice, _contextAccessor.WorkContext.WorkingCurrency), + (await priceCalculationService.GetSubTotal(sci, product)).subTotal, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice, + _contextAccessor.WorkContext.WorkingCurrency), UpdatedOn = sci.UpdatedOnUtc.HasValue ? _dateTimeService.ConvertToUserTime(sci.UpdatedOnUtc.Value, DateTimeKind.Utc) : _dateTimeService.ConvertToUserTime(sci.CreatedOnUtc, DateTimeKind.Utc) diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetSearchAutoCompleteHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetSearchAutoCompleteHandler.cs index 0be55d7b02..d003a5b403 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetSearchAutoCompleteHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetSearchAutoCompleteHandler.cs @@ -13,6 +13,7 @@ using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Media; +using Grand.Domain.Tax; using Grand.Infrastructure; using Grand.Infrastructure.Configuration; using Grand.Web.Features.Models.Catalog; @@ -114,7 +115,8 @@ public async Task> Handle(GetSearchAutoComplete r var storeurl = _contextAccessor.StoreContext.CurrentHost.Url.TrimEnd('/'); var displayPrices = - await _permissionService.Authorize(StandardPermission.DisplayPrices, _contextAccessor.WorkContext.CurrentCustomer); + await _permissionService.Authorize(StandardPermission.DisplayPrices, + _contextAccessor.WorkContext.CurrentCustomer); foreach (var item in products) { @@ -163,7 +165,8 @@ public async Task> Handle(GetSearchAutoComplete r if (brand is not { Published: true }) continue; var allow = true; - if (!_accessControlConfig.IgnoreAcl && !_aclService.Authorize(brand, _contextAccessor.WorkContext.CurrentCustomer)) + if (!_accessControlConfig.IgnoreAcl && !_aclService.Authorize(brand, + _contextAccessor.WorkContext.CurrentCustomer)) allow = false; if (!_accessControlConfig.IgnoreStoreLimitations && !_aclService.Authorize(brand, storeId)) @@ -187,7 +190,8 @@ public async Task> Handle(GetSearchAutoComplete r var category = await _categoryService.GetCategoryById(item); if (category is not { Published: true }) continue; var allow = true; - if (!_accessControlConfig.IgnoreAcl && !_aclService.Authorize(category, _contextAccessor.WorkContext.CurrentCustomer)) + if (!_accessControlConfig.IgnoreAcl && !_aclService.Authorize(category, + _contextAccessor.WorkContext.CurrentCustomer)) allow = false; if (!_accessControlConfig.IgnoreStoreLimitations && !_aclService.Authorize(category, storeId)) allow = false; @@ -245,11 +249,15 @@ public async Task> Handle(GetSearchAutoComplete r var finalPriceWithoutDiscount = (await _taxService.GetProductPrice(product, (await _pricingService.GetFinalPrice(product, request.Customer, request.Store, request.Currency, - includeDiscounts: false)).finalPrice)).productprice; + includeDiscounts: false)).finalPrice, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice; var appliedPrice = await _pricingService.GetFinalPrice(product, request.Customer, request.Store, request.Currency, includeDiscounts: true); - var finalPriceWithDiscount = (await _taxService.GetProductPrice(product, appliedPrice.finalPrice)).productprice; + var finalPriceWithDiscount = (await _taxService.GetProductPrice(product, appliedPrice.finalPrice, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice; var price = _priceFormatter.FormatPrice(finalPriceWithoutDiscount, request.Currency); var priceWithDiscount = _priceFormatter.FormatPrice(finalPriceWithDiscount, request.Currency); diff --git a/src/Web/Grand.Web/Features/Handlers/Checkout/GetPaymentMethodHandler.cs b/src/Web/Grand.Web/Features/Handlers/Checkout/GetPaymentMethodHandler.cs index 37ae97c59b..86fb5df89f 100644 --- a/src/Web/Grand.Web/Features/Handlers/Checkout/GetPaymentMethodHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Checkout/GetPaymentMethodHandler.cs @@ -8,6 +8,8 @@ using Grand.Domain.Customers; using Grand.Domain.Orders; using Grand.Domain.Payments; +using Grand.Domain.Tax; +using Grand.Infrastructure; using Grand.Web.Features.Models.Checkout; using Grand.Web.Models.Checkout; using Grand.Mediator; @@ -16,6 +18,7 @@ namespace Grand.Web.Features.Handlers.Checkout; public class GetPaymentMethodHandler : IRequestHandler { + private readonly IContextAccessor _contextAccessor; private readonly ICurrencyService _currencyService; private readonly ILoyaltyPointsService _loyaltyPointsService; @@ -33,8 +36,10 @@ public GetPaymentMethodHandler(ILoyaltyPointsService loyaltyPointsService, IPaymentService paymentService, ITaxService taxService, LoyaltyPointsSettings loyaltyPointsSettings, - PaymentSettings paymentSettings) + PaymentSettings paymentSettings, + IContextAccessor contextAccessor) { + _contextAccessor = contextAccessor; _loyaltyPointsService = loyaltyPointsService; _orderTotalCalculationService = orderTotalCalculationService; _currencyService = currencyService; @@ -89,7 +94,8 @@ await _orderTotalCalculationService.ConvertLoyaltyPointsToAmount(loyaltyPointsBa //payment method additional fee var paymentMethodAdditionalFee = await _paymentService.GetAdditionalHandlingFee(request.Cart, pm.SystemName); - var rate = (await _taxService.GetPaymentMethodAdditionalFee(paymentMethodAdditionalFee, request.Customer, request.Store)) + var rate = (await _taxService.GetPaymentMethodAdditionalFee(paymentMethodAdditionalFee, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, request.Customer, request.Store)) .paymentPrice; if (rate > 0) pmModel.Fee = _priceFormatter.FormatPrice(rate, request.Currency); diff --git a/src/Web/Grand.Web/Features/Handlers/Checkout/GetShippingAddressHandler.cs b/src/Web/Grand.Web/Features/Handlers/Checkout/GetShippingAddressHandler.cs index 9f7bb660a8..fa3a98f1c4 100644 --- a/src/Web/Grand.Web/Features/Handlers/Checkout/GetShippingAddressHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Checkout/GetShippingAddressHandler.cs @@ -6,6 +6,8 @@ using Grand.Business.Core.Interfaces.Common.Security; using Grand.Domain.Common; using Grand.Domain.Shipping; +using Grand.Domain.Tax; +using Grand.Infrastructure; using Grand.Web.Features.Models.Checkout; using Grand.Web.Features.Models.Common; using Grand.Web.Models.Checkout; @@ -15,6 +17,7 @@ namespace Grand.Web.Features.Handlers.Checkout; public class GetShippingAddressHandler : IRequestHandler { + private readonly IContextAccessor _contextAccessor; private readonly IAclService _aclService; private readonly AddressSettings _addressSettings; private readonly ICountryService _countryService; @@ -38,8 +41,10 @@ public GetShippingAddressHandler( IAclService aclService, IMediator mediator, ShippingSettings shippingSettings, - AddressSettings addressSettings) + AddressSettings addressSettings, + IContextAccessor contextAccessor) { + _contextAccessor = contextAccessor; _shippingService = shippingService; _pickupPointService = pickupPointService; _taxService = taxService; @@ -101,7 +106,8 @@ private async Task PreparePickupPoints(CheckoutShippingAddressModel model, GetSh }; if (pickupPoint.PickupFee > 0) { - var amount = (await _taxService.GetShippingPrice(pickupPoint.PickupFee, request.Customer, request.Store)) + var amount = (await _taxService.GetShippingPrice(pickupPoint.PickupFee, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, request.Customer, request.Store)) .shippingPrice; amount = await _currencyService.ConvertFromPrimaryStoreCurrency(amount, request.Currency); pickupPointModel.PickupFee = _priceFormatter.FormatPrice(amount, request.Currency); diff --git a/src/Web/Grand.Web/Features/Handlers/Checkout/GetShippingMethodHandler.cs b/src/Web/Grand.Web/Features/Handlers/Checkout/GetShippingMethodHandler.cs index 6294ad5ee2..f4242a22db 100644 --- a/src/Web/Grand.Web/Features/Handlers/Checkout/GetShippingMethodHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Checkout/GetShippingMethodHandler.cs @@ -6,6 +6,8 @@ using Grand.Domain.Common; using Grand.Domain.Customers; using Grand.Domain.Shipping; +using Grand.Domain.Tax; +using Grand.Infrastructure; using Grand.Web.Features.Models.Checkout; using Grand.Web.Models.Checkout; using Grand.Mediator; @@ -14,6 +16,7 @@ namespace Grand.Web.Features.Handlers.Checkout; public class GetShippingMethodHandler : IRequestHandler { + private readonly IContextAccessor _contextAccessor; private readonly IOrderCalculationService _orderTotalCalculationService; private readonly IPriceFormatter _priceFormatter; private readonly IShippingService _shippingService; @@ -24,8 +27,10 @@ public GetShippingMethodHandler(IShippingService shippingService, ICustomerService customerService, IOrderCalculationService orderTotalCalculationService, ITaxService taxService, - IPriceFormatter priceFormatter) + IPriceFormatter priceFormatter, + IContextAccessor contextAccessor) { + _contextAccessor = contextAccessor; _shippingService = shippingService; _customerService = customerService; _orderTotalCalculationService = orderTotalCalculationService; @@ -64,7 +69,8 @@ await _customerService.UpdateUserField(request.Customer, var shippingTotal = (await _orderTotalCalculationService.AdjustShippingRate( shippingOption.Rate, request.Cart)).shippingRate; - var rateBase = (await _taxService.GetShippingPrice(shippingTotal, request.Customer, request.Store)).shippingPrice; + var rateBase = (await _taxService.GetShippingPrice(shippingTotal, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, request.Customer, request.Store)).shippingPrice; soModel.Fee = _priceFormatter.FormatPrice(rateBase, request.Currency); model.ShippingMethods.Add(soModel); diff --git a/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsAttributeChangeHandler.cs b/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsAttributeChangeHandler.cs index e7b8bda960..7b1886c7ec 100644 --- a/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsAttributeChangeHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsAttributeChangeHandler.cs @@ -9,6 +9,8 @@ using Grand.Domain.Common; using Grand.Domain.Media; using Grand.Domain.Orders; +using Grand.Domain.Tax; +using Grand.Infrastructure; using Grand.Web.Extensions; using Grand.Web.Features.Models.Products; using Grand.Web.Features.Models.ShoppingCart; @@ -21,6 +23,7 @@ namespace Grand.Web.Features.Handlers.Products; public class GetProductDetailsAttributeChangeHandler : IRequestHandler { + private readonly IContextAccessor _contextAccessor; private readonly MediaSettings _mediaSettings; private readonly IMediator _mediator; private readonly IOutOfStockSubscriptionService _outOfStockSubscriptionService; @@ -44,8 +47,10 @@ public GetProductDetailsAttributeChangeHandler( ITranslationService translationService, IPictureService pictureService, ShoppingCartSettings shoppingCartSettings, - MediaSettings mediaSettings) + MediaSettings mediaSettings, + IContextAccessor contextAccessor) { + _contextAccessor = contextAccessor; _mediator = mediator; _stockQuantityService = stockQuantityService; _permissionService = permissionService; @@ -99,7 +104,9 @@ await _mediator.Send( true); var finalPrice = unitprice.unitprice; - var productprice = await _taxService.GetProductPrice(request.Product, finalPrice); + var productprice = await _taxService.GetProductPrice(request.Product, finalPrice, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore); var finalPriceWithDiscount = productprice.productprice; model.Price = _priceFormatter.FormatPrice(finalPriceWithDiscount, request.Currency); } diff --git a/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsPageHandler.cs b/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsPageHandler.cs index 433218e4e3..18b89c911c 100644 --- a/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsPageHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsPageHandler.cs @@ -19,6 +19,7 @@ using Grand.Domain.Orders; using Grand.Domain.Permissions; using Grand.Domain.Seo; +using Grand.Domain.Tax; using Grand.Domain.Vendors; using Grand.Infrastructure; using Grand.Infrastructure.Caching; @@ -312,7 +313,8 @@ or ManageInventoryMethod.ManageStockByAttributes && if (!isAssociatedProduct) { var associatedProducts = - await _productService.GetAssociatedProducts(product.Id, _contextAccessor.WorkContext.CurrentCustomer, + await _productService.GetAssociatedProducts(product.Id, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id); foreach (var associatedProduct in associatedProducts) model.AssociatedProducts.Add( @@ -455,7 +457,9 @@ private async Task PrepareStandardProperties(Product produc //additional shipping charge if (model.AdditionalShippingCharge > 0) model.AdditionalShippingChargeStr = _priceFormatter.FormatPrice( - (await _taxService.GetShippingPrice(model.AdditionalShippingCharge, _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)) + (await _taxService.GetShippingPrice(model.AdditionalShippingCharge, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)) .shippingPrice, _contextAccessor.WorkContext.WorkingCurrency); //ask question us on the product @@ -680,25 +684,35 @@ await _pictureService.GetPictureUrl(defaultPicture.PictureId, 0, !isAssociatedPr { if (!product.CallForPrice) { - var oldproductprice = await _taxService.GetProductPrice(product, product.OldPrice); + var oldproductprice = await _taxService.GetProductPrice(product, product.OldPrice, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore); var oldPriceBase = oldproductprice.productprice; var finalPriceWithoutDiscount = (await _taxService.GetProductPrice(product, (await _pricingService.GetFinalPrice(product, _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingCurrency, includeDiscounts: false)).finalPrice)).productprice; - - var appliedPrice = await _pricingService.GetFinalPrice(product, _contextAccessor.WorkContext.CurrentCustomer, - _contextAccessor.StoreContext.CurrentStore, _contextAccessor.WorkContext.WorkingCurrency, includeDiscounts: true); + _contextAccessor.WorkContext.WorkingCurrency, includeDiscounts: false)).finalPrice, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice; + + var appliedPrice = await _pricingService.GetFinalPrice(product, + _contextAccessor.WorkContext.CurrentCustomer, + _contextAccessor.StoreContext.CurrentStore, + _contextAccessor.WorkContext.WorkingCurrency, includeDiscounts: true); var finalPriceWithDiscount = - (await _taxService.GetProductPrice(product, appliedPrice.finalPrice)).productprice; + (await _taxService.GetProductPrice(product, appliedPrice.finalPrice, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice; var oldPrice = await _currencyService.ConvertFromPrimaryStoreCurrency(oldPriceBase, _contextAccessor.WorkContext.WorkingCurrency); if (finalPriceWithoutDiscount != oldPrice && oldPrice > 0) - model.OldPrice = _priceFormatter.FormatPrice(oldPrice, _contextAccessor.WorkContext.WorkingCurrency); + model.OldPrice = _priceFormatter.FormatPrice(oldPrice, + _contextAccessor.WorkContext.WorkingCurrency); - model.Price = _priceFormatter.FormatPrice(finalPriceWithoutDiscount, _contextAccessor.WorkContext.WorkingCurrency); + model.Price = _priceFormatter.FormatPrice(finalPriceWithoutDiscount, + _contextAccessor.WorkContext.WorkingCurrency); if (appliedPrice.appliedDiscounts.Any()) model.AppliedDiscounts = appliedPrice.appliedDiscounts; if (appliedPrice.preferredTierPrice != null) @@ -709,11 +723,13 @@ await _currencyService.ConvertFromPrimaryStoreCurrency(oldPriceBase, var catalogPrice = await _currencyService.ConvertFromPrimaryStoreCurrency(product.CatalogPrice, _contextAccessor.WorkContext.WorkingCurrency); - model.CatalogPrice = _priceFormatter.FormatPrice(catalogPrice, _contextAccessor.WorkContext.WorkingCurrency); + model.CatalogPrice = _priceFormatter.FormatPrice(catalogPrice, + _contextAccessor.WorkContext.WorkingCurrency); } if (finalPriceWithoutDiscount != finalPriceWithDiscount) - model.PriceWithDiscount = _priceFormatter.FormatPrice(finalPriceWithDiscount, _contextAccessor.WorkContext.WorkingCurrency); + model.PriceWithDiscount = _priceFormatter.FormatPrice(finalPriceWithDiscount, + _contextAccessor.WorkContext.WorkingCurrency); model.PriceValue = finalPriceWithDiscount; @@ -731,7 +747,8 @@ await _currencyService.ConvertFromPrimaryStoreCurrency(product.CatalogPrice, if (product.ProductTypeId == ProductType.Reservation) { model.IsReservation = true; - var priceStr = _priceFormatter.FormatPrice(finalPriceWithDiscount, _contextAccessor.WorkContext.WorkingCurrency); + var priceStr = _priceFormatter.FormatPrice(finalPriceWithDiscount, + _contextAccessor.WorkContext.WorkingCurrency); model.ReservationPrice = string.Format(_translationService.GetResource(product.ResourceReservationProductPeriod()), priceStr, product.Interval); @@ -743,13 +760,15 @@ await _currencyService.ConvertFromPrimaryStoreCurrency(product.CatalogPrice, var highestBid = await _currencyService.ConvertFromPrimaryStoreCurrency(product.HighestBid, _contextAccessor.WorkContext.WorkingCurrency); - model.HighestBid = _priceFormatter.FormatPrice(highestBid, _contextAccessor.WorkContext.WorkingCurrency); + model.HighestBid = _priceFormatter.FormatPrice(highestBid, + _contextAccessor.WorkContext.WorkingCurrency); model.HighestBidValue = highestBid; model.DisableBuyButton = product.DisableBuyButton; var startPrice = await _currencyService.ConvertFromPrimaryStoreCurrency(product.StartPrice, _contextAccessor.WorkContext.WorkingCurrency); - model.StartPrice = _priceFormatter.FormatPrice(startPrice, _contextAccessor.WorkContext.WorkingCurrency); + model.StartPrice = _priceFormatter.FormatPrice(startPrice, + _contextAccessor.WorkContext.WorkingCurrency); model.StartPriceValue = startPrice; } } @@ -943,7 +962,9 @@ private ProductDetailsModel.GiftVoucherModel PrepareGiftVoucherModel(Product pro var attributeValuePriceAdjustment = await _pricingService.GetProductAttributeValuePriceAdjustment(attributeValue); var productprice = - await _taxService.GetProductPrice(product, attributeValuePriceAdjustment); + await _taxService.GetProductPrice(product, attributeValuePriceAdjustment, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore); switch (productprice.productprice) { case > 0: @@ -1084,10 +1105,14 @@ await _pictureService.GetPictureUrl(attributeValue.ImageSquaresPictureId), { var tier = new ProductDetailsModel.TierPriceModel(); var priceBase = await _taxService.GetProductPrice(product, (await _pricingService.GetFinalPrice(product, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, _contextAccessor.WorkContext.WorkingCurrency, - 0, _catalogSettings.DisplayTierPricesWithDiscounts, tierPrice.Quantity)).finalPrice); + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + _contextAccessor.WorkContext.WorkingCurrency, + 0, _catalogSettings.DisplayTierPricesWithDiscounts, tierPrice.Quantity)).finalPrice, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore); tier.Quantity = tierPrice.Quantity; - tier.Price = _priceFormatter.FormatPrice(priceBase.productprice, _contextAccessor.WorkContext.WorkingCurrency); + tier.Price = _priceFormatter.FormatPrice(priceBase.productprice, + _contextAccessor.WorkContext.WorkingCurrency); model.Add(tier); } @@ -1163,9 +1188,13 @@ private async Task PrepareProductReservation(ProductDetailsModel model, Product if (displayPrices) { var productprice = await _taxService.GetProductPrice(p1, - (await _pricingService.GetFinalPrice(p1, _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingCurrency, includeDiscounts: true)).finalPrice); - bundleProduct.Price = _priceFormatter.FormatPrice(productprice.productprice, _contextAccessor.WorkContext.WorkingCurrency); + (await _pricingService.GetFinalPrice(p1, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + _contextAccessor.WorkContext.WorkingCurrency, includeDiscounts: true)).finalPrice, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore); + bundleProduct.Price = _priceFormatter.FormatPrice(productprice.productprice, + _contextAccessor.WorkContext.WorkingCurrency); bundleProduct.PriceValue = productprice.productprice; } diff --git a/src/Web/Grand.Web/Features/Handlers/Products/GetProductStructuredDataHandler.cs b/src/Web/Grand.Web/Features/Handlers/Products/GetProductStructuredDataHandler.cs index bec3cb3d69..73f3271e66 100644 --- a/src/Web/Grand.Web/Features/Handlers/Products/GetProductStructuredDataHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Products/GetProductStructuredDataHandler.cs @@ -4,6 +4,8 @@ using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Domain.Orders; using Grand.Domain.Shipping; +using Grand.Domain.Tax; +using Grand.Infrastructure; using Grand.Web.Features.Models.Products; using Grand.Web.Models.Catalog; using Grand.Mediator; @@ -12,6 +14,7 @@ namespace Grand.Web.Features.Handlers.Products; public class GetProductStructuredDataHandler : IRequestHandler { + private readonly IContextAccessor _contextAccessor; private readonly ICountryService _countryService; private readonly ICurrencyService _currencyService; private readonly OrderSettings _orderSettings; @@ -25,8 +28,10 @@ public GetProductStructuredDataHandler( IPricingService pricingService, ITaxService taxService, OrderSettings orderSettings, - ShippingSettings shippingSettings) + ShippingSettings shippingSettings, + IContextAccessor contextAccessor) { + _contextAccessor = contextAccessor; _countryService = countryService; _currencyService = currencyService; _pricingService = pricingService; @@ -99,7 +104,8 @@ public async Task Handle(GetProductStructuredData re //convert theirs, plus the product's own surcharge, taxed as shipping var rate = await _currencyService.ConvertFromPrimaryStoreCurrency( _shippingSettings.DefaultShippingRate.Value + product.AdditionalShippingCharge, request.Currency); - rate = (await _taxService.GetShippingPrice(rate, request.Customer, request.Store)).shippingPrice; + rate = (await _taxService.GetShippingPrice(rate, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, request.Customer, request.Store)).shippingPrice; return RoundingHelper.RoundPrice(rate, request.Currency); } diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs index 939874217e..8a7fa2c3ab 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs @@ -111,13 +111,15 @@ await _productAttributeFormatter.FormatAttributes(request.Product, request.Attri else { var productprices = await _taxService.GetProductPrice(request.Product, - (await _pricingService.GetUnitPrice(request.ShoppingCartItem, request.Product)).unitprice); + (await _pricingService.GetUnitPrice(request.ShoppingCartItem, request.Product)).unitprice, + request.TaxDisplayType == TaxDisplayType.IncludingTax, request.Customer, request.Store); model.Price = !request.CustomerEnteredPrice.HasValue ? _priceFormatter.FormatPrice(productprices.productprice, request.Currency) : _priceFormatter.FormatPrice(request.CustomerEnteredPrice.Value, request.Currency); model.DecimalPrice = request.CustomerEnteredPrice ?? productprices.productprice; model.TotalPrice = - _priceFormatter.FormatPrice(productprices.productprice * request.ShoppingCartItem.Quantity, request.Currency); + _priceFormatter.FormatPrice(productprices.productprice * request.ShoppingCartItem.Quantity, + request.Currency); } //picture diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetEstimateShippingResultHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetEstimateShippingResultHandler.cs index 7d6d78446b..436463334e 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetEstimateShippingResultHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetEstimateShippingResultHandler.cs @@ -6,6 +6,8 @@ using Grand.Domain.Common; using Grand.Domain.Orders; using Grand.Domain.Shipping; +using Grand.Domain.Tax; +using Grand.Infrastructure; using Grand.Web.Features.Models.ShoppingCart; using Grand.Web.Models.ShoppingCart; using Grand.Mediator; @@ -14,6 +16,7 @@ namespace Grand.Web.Features.Handlers.ShoppingCart; public class GetEstimateShippingResultHandler : IRequestHandler { + private readonly IContextAccessor _contextAccessor; private readonly IOrderCalculationService _orderTotalCalculationService; private readonly IPickupPointService _pickupPointService; private readonly IPriceFormatter _priceFormatter; @@ -30,8 +33,10 @@ public GetEstimateShippingResultHandler( ITaxService taxService, ITranslationService translationService, IPriceFormatter priceFormatter, - ShippingSettings shippingSettings) + ShippingSettings shippingSettings, + IContextAccessor contextAccessor) { + _contextAccessor = contextAccessor; _shippingService = shippingService; _pickupPointService = pickupPointService; _orderTotalCalculationService = orderTotalCalculationService; @@ -76,7 +81,8 @@ public async Task Handle(GetEstimateShippingResult request.Cart); var shippingTotal = total.shippingRate; - var rate = (await _taxService.GetShippingPrice(shippingTotal, request.Customer, request.Store)).shippingPrice; + var rate = (await _taxService.GetShippingPrice(shippingTotal, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, request.Customer, request.Store)).shippingPrice; soModel.Price = _priceFormatter.FormatPrice(rate, request.Currency); model.ShippingOptions.Add(soModel); } @@ -92,7 +98,8 @@ public async Task Handle(GetEstimateShippingResult }; var shippingTotal = pickupPoints.Max(x => x.PickupFee); - var rate = (await _taxService.GetShippingPrice(shippingTotal, request.Customer, request.Store)).shippingPrice; + var rate = (await _taxService.GetShippingPrice(shippingTotal, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, request.Customer, request.Store)).shippingPrice; soModel.Price = _priceFormatter.FormatPrice(rate, request.Currency); model.ShippingOptions.Add(soModel); } diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs index 62b11005b1..a41c0baf97 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs @@ -188,7 +188,8 @@ public async Task Handle(GetMiniShoppingCart request, else { var productprices = await _taxService.GetProductPrice(product, - (await _pricingService.GetUnitPrice(sci, product)).unitprice); + (await _pricingService.GetUnitPrice(sci, product)).unitprice, + request.TaxDisplayType == TaxDisplayType.IncludingTax, request.Customer, request.Store); cartItemModel.UnitPrice = _priceFormatter.FormatPrice(productprices.productprice, request.Currency); } diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniWishlistHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniWishlistHandler.cs index 8d77ef2053..a65af8bb8f 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniWishlistHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniWishlistHandler.cs @@ -8,6 +8,8 @@ using Grand.Domain.Common; using Grand.Domain.Media; using Grand.Domain.Orders; +using Grand.Domain.Tax; +using Grand.Infrastructure; using Grand.Web.Extensions; using Grand.Web.Features.Models.ShoppingCart; using Grand.Web.Models.Media; @@ -18,6 +20,7 @@ namespace Grand.Web.Features.Handlers.ShoppingCart; public class GetMiniWishlistHandler : IRequestHandler { + private readonly IContextAccessor _contextAccessor; private readonly LinkGenerator _linkGenerator; private readonly MediaSettings _mediaSettings; private readonly IPictureService _pictureService; @@ -39,8 +42,10 @@ public GetMiniWishlistHandler( IPictureService pictureService, LinkGenerator linkGenerator, ShoppingCartSettings shoppingCartSettings, - MediaSettings mediaSettings) + MediaSettings mediaSettings, + IContextAccessor contextAccessor) { + _contextAccessor = contextAccessor; _productService = productService; _productAttributeFormatter = productAttributeFormatter; _translationService = translationService; @@ -92,7 +97,9 @@ public async Task Handle(GetMiniWishlist request, Cancellatio else { var productprice = await _taxService.GetProductPrice(product, - (await _pricingService.GetUnitPrice(sci, product)).unitprice); + (await _pricingService.GetUnitPrice(sci, product)).unitprice, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore); cartItemModel.UnitPrice = _priceFormatter.FormatPrice(productprice.productprice, request.Currency); cartItemModel.UnitPriceValue = productprice.productprice; diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetOrderTotalsHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetOrderTotalsHandler.cs index 077054a8e5..b8ccf8687a 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetOrderTotalsHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetOrderTotalsHandler.cs @@ -120,7 +120,8 @@ private async Task PreparePayment(OrderTotalsModel model, GetOrderTotals request var paymentMethodAdditionalFee = await _paymentService.GetAdditionalHandlingFee(request.Cart, paymentMethodSystemName); var paymentMethodAdditionalFeeWithTaxBase = - (await _taxService.GetPaymentMethodAdditionalFee(paymentMethodAdditionalFee, request.Customer, request.Store)) + (await _taxService.GetPaymentMethodAdditionalFee(paymentMethodAdditionalFee, + request.TaxDisplayType == TaxDisplayType.IncludingTax, request.Customer, request.Store)) .paymentPrice; if (paymentMethodAdditionalFeeWithTaxBase > 0) { diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs index 0b54ae2b60..0192f21f4c 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs @@ -19,7 +19,9 @@ using Grand.Domain.Customers; using Grand.Domain.Media; using Grand.Domain.Orders; +using Grand.Domain.Tax; using Grand.Infrastructure.Configuration; +using Grand.Infrastructure; using Grand.Web.Common.Localization; using Grand.Web.Extensions; using Grand.Web.Features.Models.ShoppingCart; @@ -32,6 +34,7 @@ namespace Grand.Web.Features.Handlers.ShoppingCart; public class GetShoppingCartHandler : IRequestHandler { + private readonly IContextAccessor _contextAccessor; private readonly CatalogSettings _catalogSettings; private readonly ICheckoutAttributeFormatter _checkoutAttributeFormatter; private readonly ICheckoutAttributeParser _checkoutAttributeParser; @@ -90,8 +93,10 @@ public GetShoppingCartHandler( ShoppingCartSettings shoppingCartSettings, CatalogSettings catalogSettings, CommonSettings commonSettings, - IEnumTranslationService enumTranslationService) + IEnumTranslationService enumTranslationService, + IContextAccessor contextAccessor) { + _contextAccessor = contextAccessor; _productService = productService; _pictureService = pictureService; _translationService = translationService; @@ -244,17 +249,21 @@ await _checkoutAttributeService.GetAllCheckoutAttributes(request.Store.Id, //display price if allowed if (!await _permissionService.Authorize(StandardPermission.DisplayPrices)) continue; - var priceAdjustmentBase = (await _taxService.GetCheckoutAttributePrice(attribute, attributeValue)) + var priceAdjustmentBase = (await _taxService.GetCheckoutAttributePrice(attribute, attributeValue, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)) .checkoutPrice; var priceAdjustment = await _currencyService.ConvertFromPrimaryStoreCurrency(priceAdjustmentBase, request.Currency); switch (priceAdjustmentBase) { case > 0: - attributeValueModel.PriceAdjustment = "+" + _priceFormatter.FormatPrice(priceAdjustment, request.Currency); + attributeValueModel.PriceAdjustment = "+" + _priceFormatter.FormatPrice(priceAdjustment, + request.Currency); break; case < 0: - attributeValueModel.PriceAdjustment = "-" + _priceFormatter.FormatPrice(-priceAdjustment, request.Currency); + attributeValueModel.PriceAdjustment = "-" + _priceFormatter.FormatPrice(-priceAdjustment, + request.Currency); break; } } @@ -442,11 +451,15 @@ private async Task PrepareCartItems(ShoppingCartModel model, GetShoppingCart req { var unitprices = await _pricingService.GetUnitPrice(sci, product); var appliedDiscounts = unitprices.appliedDiscounts; - var productprices = await _taxService.GetProductPrice(product, unitprices.unitprice); + var productprices = await _taxService.GetProductPrice(product, unitprices.unitprice, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore); cartItemModel.UnitPriceWithoutDiscountValue = (await _taxService.GetProductPrice(product, - (await _pricingService.GetUnitPrice(sci, product, false)).unitprice)).productprice; + (await _pricingService.GetUnitPrice(sci, product, false)).unitprice, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice; cartItemModel.UnitPriceWithoutDiscount = _priceFormatter.FormatPrice(cartItemModel.UnitPriceWithoutDiscountValue, request.Currency); @@ -465,17 +478,23 @@ private async Task PrepareCartItems(ShoppingCartModel model, GetShoppingCart req var subtotal = await _pricingService.GetSubTotal(sci, product); var shoppingCartItemDiscountBase = subtotal.discountAmount; var shoppingCartItemSubTotalWithDiscount = - (await _taxService.GetProductPrice(product, subtotal.subTotal)).productprice; - cartItemModel.SubTotal = _priceFormatter.FormatPrice(shoppingCartItemSubTotalWithDiscount, request.Currency); + (await _taxService.GetProductPrice(product, subtotal.subTotal, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice; + cartItemModel.SubTotal = _priceFormatter.FormatPrice(shoppingCartItemSubTotalWithDiscount, + request.Currency); cartItemModel.SubTotalValue = shoppingCartItemSubTotalWithDiscount; //display an applied discount amount if (shoppingCartItemDiscountBase > 0) { shoppingCartItemDiscountBase = - (await _taxService.GetProductPrice(product, shoppingCartItemDiscountBase)).productprice; + (await _taxService.GetProductPrice(product, shoppingCartItemDiscountBase, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice; if (shoppingCartItemDiscountBase > 0) - cartItemModel.Discount = _priceFormatter.FormatPrice(shoppingCartItemDiscountBase, request.Currency); + cartItemModel.Discount = _priceFormatter.FormatPrice(shoppingCartItemDiscountBase, + request.Currency); } } diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs index d1d0985302..436bab48ba 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs @@ -13,6 +13,8 @@ using Grand.Domain.Customers; using Grand.Domain.Media; using Grand.Domain.Orders; +using Grand.Domain.Tax; +using Grand.Infrastructure; using Grand.Web.Common.Localization; using Grand.Web.Extensions; using Grand.Web.Features.Models.ShoppingCart; @@ -25,6 +27,7 @@ namespace Grand.Web.Features.Handlers.ShoppingCart; public class GetWishlistHandler : IRequestHandler { + private readonly IContextAccessor _contextAccessor; private readonly CatalogSettings _catalogSettings; private readonly LinkGenerator _linkGenerator; private readonly MediaSettings _mediaSettings; @@ -53,8 +56,10 @@ public GetWishlistHandler( ShoppingCartSettings shoppingCartSettings, CatalogSettings catalogSettings, MediaSettings mediaSettings, - IEnumTranslationService enumTranslationService) + IEnumTranslationService enumTranslationService, + IContextAccessor contextAccessor) { + _contextAccessor = contextAccessor; _permissionService = permissionService; _productService = productService; _productAttributeFormatter = productAttributeFormatter; @@ -142,7 +147,9 @@ public async Task Handle(GetWishlist request, CancellationToken c else { var productprice = await _taxService.GetProductPrice(product, - (await _pricingService.GetUnitPrice(sci, product)).unitprice); + (await _pricingService.GetUnitPrice(sci, product)).unitprice, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore); cartItemModel.UnitPrice = _priceFormatter.FormatPrice(productprice.productprice, request.Currency); } @@ -156,16 +163,21 @@ public async Task Handle(GetWishlist request, CancellationToken c //sub total var subtotal = await _pricingService.GetSubTotal(sci, product); var shoppingCartItemDiscountBase = subtotal.discountAmount; - var productprices = await _taxService.GetProductPrice(product, subtotal.subTotal); + var productprices = await _taxService.GetProductPrice(product, subtotal.subTotal, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore); cartItemModel.SubTotal = _priceFormatter.FormatPrice(productprices.productprice, request.Currency); //display an applied discount amount if (shoppingCartItemDiscountBase > 0) { shoppingCartItemDiscountBase = - (await _taxService.GetProductPrice(product, shoppingCartItemDiscountBase)).productprice; + (await _taxService.GetProductPrice(product, shoppingCartItemDiscountBase, + _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice; if (shoppingCartItemDiscountBase > 0) - cartItemModel.Discount = _priceFormatter.FormatPrice(shoppingCartItemDiscountBase, request.Currency); + cartItemModel.Discount = _priceFormatter.FormatPrice(shoppingCartItemDiscountBase, + request.Currency); } } From 5e047bbe97c0610c3b5b5be551cbc8a166e1f2b3 Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 14:48:46 +0200 Subject: [PATCH 12/33] Cache checkout attributes per customer group set The all-attributes cache key held the store, the shippable flag and the ACL flag, but not the groups the ACL filter used. The first customer's filtered list was then served to every customer of the store, so group-limited attributes leaked to or hid from the wrong customers. The key now includes the group ids, like the catalog keys. --- .../Services/CheckoutAttributes/CheckoutAttributeService.cs | 4 +++- .../Grand.Infrastructure/Caching/Constants/OrdersCacheKeys.cs | 3 ++- .../CheckoutAttributes/CheckoutAttributeServiceTests.cs | 1 - 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeService.cs b/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeService.cs index f3c70183de..a8771d8bc7 100644 --- a/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeService.cs +++ b/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeService.cs @@ -56,7 +56,9 @@ public CheckoutAttributeService( public virtual async Task> GetAllCheckoutAttributes(string storeId, Customer customer, bool excludeShippableAttributes = false, bool ignoreAcl = false) { - var key = string.Format(CacheKey.CHECKOUTATTRIBUTES_ALL_KEY, storeId, excludeShippableAttributes, ignoreAcl); + var applyAcl = !ignoreAcl && !_accessControlConfig.IgnoreAcl; + var key = string.Format(CacheKey.CHECKOUTATTRIBUTES_ALL_KEY, storeId, excludeShippableAttributes, ignoreAcl, + applyAcl ? string.Join(",", customer.GetCustomerGroupIds()) : string.Empty); return await _cacheBase.GetAsync(key, async () => { var query = from p in _checkoutAttributeRepository.Table diff --git a/src/Core/Grand.Infrastructure/Caching/Constants/OrdersCacheKeys.cs b/src/Core/Grand.Infrastructure/Caching/Constants/OrdersCacheKeys.cs index d1560a18ae..62fdacac9d 100644 --- a/src/Core/Grand.Infrastructure/Caching/Constants/OrdersCacheKeys.cs +++ b/src/Core/Grand.Infrastructure/Caching/Constants/OrdersCacheKeys.cs @@ -20,8 +20,9 @@ public static partial class CacheKey /// {0} : store ID /// {1} : >A value indicating whether we should exclude shippable attributes /// {2} : ignore ACL? + /// {3} : customer group ids (empty when ACL is ignored) /// - public static string CHECKOUTATTRIBUTES_ALL_KEY => "Grand.checkoutattribute.all-{0}-{1}-{2}"; + public static string CHECKOUTATTRIBUTES_ALL_KEY => "Grand.checkoutattribute.all-{0}-{1}-{2}-{3}"; /// /// Key for caching diff --git a/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeServiceTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeServiceTests.cs index 22236a572d..434206c953 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeServiceTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeServiceTests.cs @@ -99,7 +99,6 @@ public async Task GetAllCheckoutAttributes_FiltersByTheGroupsOfTheGivenCustomer( } [TestMethod] - [Ignore("fix in next commit")] public async Task GetAllCheckoutAttributes_CacheKeyDependsOnTheCustomerGroups() { //Arrange From 669ce7bbe849ca3311630f8c7abc742e5ca0ecf5 Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 12:50:01 +0200 Subject: [PATCH 13/33] Pass customer, store and currency explicitly to cart item pricing --- .../Services/Prices/PricingService.cs | 50 +++++++++++-------- .../Products/ProductAttributeFormatter.cs | 4 +- .../Orders/PlaceOrderCommandHandler.cs | 14 ++++-- .../Orders/ShoppingCartSubTotalService.cs | 22 +++++--- .../Catalog/Prices/IPricingService.cs | 19 +++++-- .../ByWeightShippingProvider.cs | 7 ++- ...ppingCartItemsSubTotalQueryHandlerTests.cs | 4 +- .../Services/Prices/PriceServiceTests.cs | 48 ++++++++++++++++-- .../Controllers/ShoppingCartController.cs | 16 ++++-- .../Services/CustomerViewModelService.cs | 8 ++- .../Products/GetProductDetailsPageHandler.cs | 4 +- .../ShoppingCart/GetAddToCartHandler.cs | 3 +- .../GetMiniShoppingCartHandler.cs | 3 +- .../ShoppingCart/GetMiniWishlistHandler.cs | 4 +- .../ShoppingCart/GetShoppingCartHandler.cs | 12 +++-- .../ShoppingCart/GetWishlistHandler.cs | 8 ++- 16 files changed, 166 insertions(+), 60 deletions(-) diff --git a/src/Business/Grand.Business.Catalog/Services/Prices/PricingService.cs b/src/Business/Grand.Business.Catalog/Services/Prices/PricingService.cs index 6a538ec2b5..c49feb9910 100644 --- a/src/Business/Grand.Business.Catalog/Services/Prices/PricingService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Prices/PricingService.cs @@ -11,7 +11,6 @@ using Grand.Domain.Directory; using Grand.Domain.Orders; using Grand.Domain.Stores; -using Grand.Infrastructure; using Grand.Mediator; using System.Globalization; @@ -25,7 +24,6 @@ public class PricingService : IPricingService #region Ctor public PricingService( - IContextAccessor contextAccessor, IProductService productService, IMediator mediator, ICurrencyService currencyService, @@ -33,7 +31,6 @@ public PricingService( ShoppingCartSettings shoppingCartSettings, CatalogSettings catalogSettings) { - _contextAccessor = contextAccessor; _productService = productService; _mediator = mediator; _currencyService = currencyService; @@ -58,7 +55,6 @@ protected class ProductPrice #region Fields - private readonly IContextAccessor _contextAccessor; private readonly IProductService _productService; private readonly IMediator _mediator; private readonly ICurrencyService _currencyService; @@ -210,18 +206,21 @@ await _currencyService.ConvertToPrimaryStoreCurrency(price, currency)) /// /// The shopping cart item /// Product + /// Customer the price is for + /// Store the price is for + /// Currency the price is converted to /// Include discounts or not for price /// Shopping cart unit price (one item) public virtual async Task<(double unitprice, double discountAmount, List appliedDiscounts)> - GetUnitPrice(ShoppingCartItem shoppingCartItem, - Product product, bool includeDiscounts = true) + GetUnitPrice(ShoppingCartItem shoppingCartItem, Product product, Customer customer, Store store, + Currency currency, bool includeDiscounts = true) { ArgumentNullException.ThrowIfNull(shoppingCartItem); return await GetUnitPrice(product, - _contextAccessor.WorkContext.CurrentCustomer, - _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingCurrency, + customer, + store, + currency, shoppingCartItem.ShoppingCartTypeId, shoppingCartItem.Quantity, shoppingCartItem.Attributes, @@ -303,7 +302,8 @@ await _currencyService.ConvertFromPrimaryStoreCurrency(combination.OverriddenPri var attributeValues = product.ParseProductAttributeValues(attributes); if (attributeValues != null) foreach (var attributeValue in attributeValues) - attributesTotalPrice += await GetProductAttributeValuePriceAdjustment(attributeValue); + attributesTotalPrice += + await GetProductAttributeValuePriceAdjustment(attributeValue, customer, store, currency); if (product.ProductTypeId == ProductType.BundledProduct) foreach (var item in product.BundleProducts) { @@ -312,7 +312,8 @@ await _currencyService.ConvertFromPrimaryStoreCurrency(combination.OverriddenPri if (bundledProductsAttributeValues == null) continue; foreach (var attributeValue in bundledProductsAttributeValues) attributesTotalPrice += item.Quantity * - await GetProductAttributeValuePriceAdjustment(attributeValue); + await GetProductAttributeValuePriceAdjustment(attributeValue, + customer, store, currency); } } @@ -355,17 +356,20 @@ await _currencyService.ConvertFromPrimaryStoreCurrency(combination.OverriddenPri /// /// The shopping cart item /// Product + /// Customer the price is for + /// Store the price is for + /// Currency the price is converted to /// Include discounts or not for price /// Shopping cart item sub total public virtual async Task<(double subTotal, double discountAmount, List appliedDiscounts)> - GetSubTotal(ShoppingCartItem shoppingCartItem, Product product, - bool includeDiscounts = true) + GetSubTotal(ShoppingCartItem shoppingCartItem, Product product, Customer customer, Store store, + Currency currency, bool includeDiscounts = true) { ArgumentNullException.ThrowIfNull(shoppingCartItem); double subTotal = 0; //unit price - var getUnitPrice = await GetUnitPrice(shoppingCartItem, product, includeDiscounts); + var getUnitPrice = await GetUnitPrice(shoppingCartItem, product, customer, store, currency, includeDiscounts); var unitPrice = getUnitPrice.unitprice; var discountAmount = getUnitPrice.discountAmount; var appliedDiscounts = getUnitPrice.appliedDiscounts; @@ -385,7 +389,8 @@ await _currencyService.ConvertFromPrimaryStoreCurrency(combination.OverriddenPri discountAmount *= discountedQuantity; var notDiscountedQuantity = shoppingCartItem.Quantity - discountedQuantity; - var notDiscountedUnitPrice = await GetUnitPrice(shoppingCartItem, product, false); + var notDiscountedUnitPrice = + await GetUnitPrice(shoppingCartItem, product, customer, store, currency, false); var notDiscountedSubTotal = notDiscountedUnitPrice.unitprice * notDiscountedQuantity; subTotal = discountedSubTotal + notDiscountedSubTotal; @@ -445,8 +450,12 @@ public virtual async Task GetProductCost(Product product, IList /// Product attribute value + /// Customer the price is for + /// Store the price is for + /// Currency the price is converted to /// Price adjustment - public virtual async Task GetProductAttributeValuePriceAdjustment(ProductAttributeValue value) + public virtual async Task GetProductAttributeValuePriceAdjustment(ProductAttributeValue value, + Customer customer, Store store, Currency currency) { ArgumentNullException.ThrowIfNull(value); @@ -459,8 +468,7 @@ public virtual async Task GetProductAttributeValuePriceAdjustment(Produc adjustment = value.PriceAdjustment; if (adjustment > 0) adjustment = - await _currencyService.ConvertFromPrimaryStoreCurrency(adjustment, - _contextAccessor.WorkContext.WorkingCurrency); + await _currencyService.ConvertFromPrimaryStoreCurrency(adjustment, currency); } break; case AttributeValueType.AssociatedToProduct: @@ -469,9 +477,9 @@ await _currencyService.ConvertFromPrimaryStoreCurrency(adjustment, var associatedProduct = await _productService.GetProductById(value.AssociatedProductId); if (associatedProduct != null) adjustment = (await GetFinalPrice(associatedProduct, - _contextAccessor.WorkContext.CurrentCustomer, - _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingCurrency, + customer, + store, + currency, value.PriceAdjustment)).finalPrice * value.Quantity; } break; diff --git a/src/Business/Grand.Business.Catalog/Services/Products/ProductAttributeFormatter.cs b/src/Business/Grand.Business.Catalog/Services/Products/ProductAttributeFormatter.cs index 11e085520a..71c673b5bd 100644 --- a/src/Business/Grand.Business.Catalog/Services/Products/ProductAttributeFormatter.cs +++ b/src/Business/Grand.Business.Catalog/Services/Products/ProductAttributeFormatter.cs @@ -234,7 +234,9 @@ private async Task PrepareFormattedAttribute(Product product, if (renderPrices) { var attributeValuePriceAdjustment = - await _pricingService.GetProductAttributeValuePriceAdjustment(attributeValue); + await _pricingService.GetProductAttributeValuePriceAdjustment(attributeValue, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + _contextAccessor.WorkContext.WorkingCurrency); var (priceAdjustmentBase, _) = await _taxService.GetProductPrice(product, attributeValuePriceAdjustment, _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs index 60803f6efc..045f8d246d 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs @@ -617,10 +617,16 @@ protected virtual async Task PreparePlaceOrderDetails() protected virtual async Task PrepareOrderItem(ShoppingCartItem sc, Product product, PlaceOrderContainer details) { - var scUnitPrice = (await _pricingService.GetUnitPrice(sc, product)).unitprice; - var scUnitPriceWithoutDisc = (await _pricingService.GetUnitPrice(sc, product, false)).unitprice; - - var (scSubTotal, discountAmount, scDiscounts) = await _pricingService.GetSubTotal(sc, product); + var scUnitPrice = (await _pricingService.GetUnitPrice(sc, product, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + _contextAccessor.WorkContext.WorkingCurrency)).unitprice; + var scUnitPriceWithoutDisc = (await _pricingService.GetUnitPrice(sc, product, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + _contextAccessor.WorkContext.WorkingCurrency, false)).unitprice; + + var (scSubTotal, discountAmount, scDiscounts) = await _pricingService.GetSubTotal(sc, product, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + _contextAccessor.WorkContext.WorkingCurrency); var prices = await _taxService.GetTaxProductPrice(product, details.Customer, details.Store, scUnitPrice, diff --git a/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartSubTotalService.cs b/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartSubTotalService.cs index 9ad776e112..cc2acfbe64 100644 --- a/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartSubTotalService.cs +++ b/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartSubTotalService.cs @@ -148,7 +148,9 @@ public ShoppingCartSubTotalService(IContextAccessor contextAccessor, if (product == null) continue; - var subtotal = await _pricingService.GetSubTotal(shoppingCartItem, product); + var subtotal = await _pricingService.GetSubTotal(shoppingCartItem, product, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + _contextAccessor.WorkContext.WorkingCurrency); var sciSubTotal = subtotal.subTotal; var pricesExcl = await _taxService.GetProductPrice(product, sciSubTotal, false, customer, store); @@ -177,9 +179,11 @@ public ShoppingCartSubTotalService(IContextAccessor contextAccessor, { var checkoutAttributePriceExclTax = await _taxService.GetCheckoutAttributePrice(attributeValue.ca, attributeValue.cav, false, customer, store); - var caExclTax = await _currencyService.ConvertFromPrimaryStoreCurrency(checkoutAttributePriceExclTax.checkoutPrice, _contextAccessor.WorkContext.WorkingCurrency); + var caExclTax = await _currencyService.ConvertFromPrimaryStoreCurrency(checkoutAttributePriceExclTax.checkoutPrice, + _contextAccessor.WorkContext.WorkingCurrency); var (checkoutPrice, taxRate) = await _taxService.GetCheckoutAttributePrice(attributeValue.ca, attributeValue.cav, true, customer, store); - var caInclTax = await _currencyService.ConvertFromPrimaryStoreCurrency(checkoutPrice, _contextAccessor.WorkContext.WorkingCurrency); + var caInclTax = await _currencyService.ConvertFromPrimaryStoreCurrency(checkoutPrice, + _contextAccessor.WorkContext.WorkingCurrency); subTotalExclTaxWithoutDiscount += caExclTax; subTotalInclTaxWithoutDiscount += caInclTax; @@ -201,7 +205,8 @@ public ShoppingCartSubTotalService(IContextAccessor contextAccessor, subTotalWithoutDiscount = 0; if (_shoppingCartSettings.RoundPrices) - subTotalWithoutDiscount = RoundingHelper.RoundPrice(subTotalWithoutDiscount, _contextAccessor.WorkContext.WorkingCurrency); + subTotalWithoutDiscount = RoundingHelper.RoundPrice(subTotalWithoutDiscount, + _contextAccessor.WorkContext.WorkingCurrency); //We calculate discount amount on order subtotal excl tax (discount first) //calculate discount amount ('Applied to order subtotal' discount) @@ -242,8 +247,10 @@ public ShoppingCartSubTotalService(IContextAccessor contextAccessor, if (_shoppingCartSettings.RoundPrices) { - discountAmountInclTax = RoundingHelper.RoundPrice(discountAmountInclTax, _contextAccessor.WorkContext.WorkingCurrency); - discountAmountExclTax = RoundingHelper.RoundPrice(discountAmountExclTax, _contextAccessor.WorkContext.WorkingCurrency); + discountAmountInclTax = RoundingHelper.RoundPrice(discountAmountInclTax, + _contextAccessor.WorkContext.WorkingCurrency); + discountAmountExclTax = RoundingHelper.RoundPrice(discountAmountExclTax, + _contextAccessor.WorkContext.WorkingCurrency); } if (includingTax) @@ -261,7 +268,8 @@ public ShoppingCartSubTotalService(IContextAccessor contextAccessor, subTotalWithDiscount = 0; if (_shoppingCartSettings.RoundPrices) - subTotalWithDiscount = RoundingHelper.RoundPrice(subTotalWithDiscount, _contextAccessor.WorkContext.WorkingCurrency); + subTotalWithDiscount = RoundingHelper.RoundPrice(subTotalWithDiscount, + _contextAccessor.WorkContext.WorkingCurrency); return (discountAmount, appliedDiscounts, subTotalWithoutDiscount, subTotalWithDiscount, taxRates); } diff --git a/src/Business/Grand.Business.Core/Interfaces/Catalog/Prices/IPricingService.cs b/src/Business/Grand.Business.Core/Interfaces/Catalog/Prices/IPricingService.cs index 580a1caa9a..d95aacf1d7 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Catalog/Prices/IPricingService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Catalog/Prices/IPricingService.cs @@ -64,12 +64,14 @@ public interface IPricingService /// /// The shopping cart item /// Product + /// Customer the price is for + /// Store the price is for + /// Currency the price is converted to /// A value indicating whether include discounts or not for price computation /// Shopping cart unit price (one item) Task<(double unitprice, double discountAmount, List appliedDiscounts)> GetUnitPrice( - ShoppingCartItem shoppingCartItem, - Product product, bool includeDiscounts = true); - + ShoppingCartItem shoppingCartItem, Product product, Customer customer, Store store, Currency currency, + bool includeDiscounts = true); /// /// Gets the shopping cart unit price (one item) @@ -102,10 +104,13 @@ public interface IPricingService /// /// The shopping cart item /// Product + /// Customer the price is for + /// Store the price is for + /// Currency the price is converted to /// A value indicating whether include discounts or not for price computation /// Shopping cart item sub total Task<(double subTotal, double discountAmount, List appliedDiscounts)> GetSubTotal( - ShoppingCartItem shoppingCartItem, Product product, + ShoppingCartItem shoppingCartItem, Product product, Customer customer, Store store, Currency currency, bool includeDiscounts = true); /// @@ -121,6 +126,10 @@ public interface IPricingService /// Get a price adjustment of a product attribute value /// /// Product attribute value + /// Customer the price is for + /// Store the price is for + /// Currency the price is converted to /// Price adjustment - Task GetProductAttributeValuePriceAdjustment(ProductAttributeValue value); + Task GetProductAttributeValuePriceAdjustment(ProductAttributeValue value, Customer customer, Store store, + Currency currency); } \ No newline at end of file diff --git a/src/Plugins/Shipping.ByWeight/ByWeightShippingProvider.cs b/src/Plugins/Shipping.ByWeight/ByWeightShippingProvider.cs index 5a0f8568c2..420e75f66d 100644 --- a/src/Plugins/Shipping.ByWeight/ByWeightShippingProvider.cs +++ b/src/Plugins/Shipping.ByWeight/ByWeightShippingProvider.cs @@ -220,12 +220,15 @@ public async Task GetShippingOptions(GetShippingOptio var product = await _productService.GetProductById(packageItem.ShoppingCartItem.ProductId); if (product != null) - subTotal += (await _pricingService.GetSubTotal(packageItem.ShoppingCartItem, product)).subTotal; + subTotal += (await _pricingService.GetSubTotal(packageItem.ShoppingCartItem, product, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + _contextAccessor.WorkContext.WorkingCurrency)).subTotal; } var weight = await GetTotalWeight(getShippingOptionRequest); - var shippingMethods = await _shippingMethodService.GetAllShippingMethods(countryId, _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id); + var shippingMethods = await _shippingMethodService.GetAllShippingMethods(countryId, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id); foreach (var shippingMethod in shippingMethods) { double? rate = null; diff --git a/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetShoppingCartItemsSubTotalQueryHandlerTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetShoppingCartItemsSubTotalQueryHandlerTests.cs index 5bda515fef..6818f8cae2 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetShoppingCartItemsSubTotalQueryHandlerTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetShoppingCartItemsSubTotalQueryHandlerTests.cs @@ -48,7 +48,9 @@ public async Task Handle_PricesItemsForRequestCustomerInPrimaryCurrency_SkipsMis }, CancellationToken.None); Assert.AreEqual(60, subTotal); - pricingService.Verify(p => p.GetSubTotal(It.IsAny(), It.IsAny(), It.IsAny()), + pricingService.Verify(p => p.GetSubTotal(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny()), Times.Never); } } diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Prices/PriceServiceTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Prices/PriceServiceTests.cs index 279f027061..668309248c 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Prices/PriceServiceTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Prices/PriceServiceTests.cs @@ -110,7 +110,6 @@ public void TestInitialize() tempDiscountApplicationService = new Mock(); _pricingService = new PricingService( - _workContext, _productService, _eventPublisher, _currencyService, @@ -291,10 +290,50 @@ public async Task Can_get_shopping_cart_item_unitPrice() tempDiscountServiceMock .Setup(x => x.GetActiveDiscountsByContext(DiscountType.AssignedToAllProducts, "1", _currency.CurrencyCode)) .ReturnsAsync(new List()); - var pp = (await _pricingService.GetUnitPrice(shoppingCartItem, product001)).unitprice; + var pp = (await _pricingService.GetUnitPrice(shoppingCartItem, product001, customer001, _store, _currency)) + .unitprice; Assert.AreEqual(49.99, pp); } + [TestMethod] + public async Task GetSubTotal_PricesTheItemForTheGivenCustomerStoreAndCurrency() + { + //Arrange - nothing is set on the ambient context: the price must come from the arguments alone + var product = new Product { Id = "explicit", Name = "explicit", Price = 10, Published = true }; + product.ProductPrices.Add(new ProductPrice { CurrencyCode = "USD", Price = 10 }); + var customer = new Customer { Id = "c-explicit" }; + var item = new ShoppingCartItem { ProductId = product.Id, Quantity = 3 }; + tempDiscountApplicationService.Setup(x => x.GetDiscountAmount(It.IsAny(), customer, _store, + _currency, It.IsAny())) + .ReturnsAsync((new List(), 0)); + + //Act + var (subTotal, _, _) = await _pricingService.GetSubTotal(item, product, customer, _store, _currency); + + //Assert + Assert.AreEqual(30, subTotal); + } + + [TestMethod] + public async Task GetProductAttributeValuePriceAdjustment_ConvertsToTheGivenCurrency() + { + //Arrange - a working currency at twice the primary one, so the conversion shows + var eur = new Currency { Id = "2", CurrencyCode = "EUR", Rate = 2, Published = true, + MidpointRoundId = MidpointRounding.ToEven, RoundingTypeId = RoundingType.Rounding001 }; + var value = new ProductAttributeValue { PriceAdjustment = 10, AttributeValueTypeId = AttributeValueType.Simple }; + var currencyService = new Mock(); + currencyService.Setup(x => x.ConvertFromPrimaryStoreCurrency(10, eur)).ReturnsAsync(20); + var pricingService = new PricingService(_productService, _eventPublisher, currencyService.Object, + tempDiscountApplicationService.Object, _shoppingCartSettings, _catalogSettings); + + //Act + var adjustment = + await pricingService.GetProductAttributeValuePriceAdjustment(value, new Customer(), _store, eur); + + //Assert + Assert.AreEqual(20, adjustment); + } + [TestMethod] public async Task Can_get_shopping_cart_item_subTotal() { @@ -327,7 +366,8 @@ public async Task Can_get_shopping_cart_item_subTotal() tempDiscountServiceMock .Setup(x => x.GetActiveDiscountsByContext(DiscountType.AssignedToAllProducts, "1", _currency.CurrencyCode)) .ReturnsAsync(new List()); - var subtotal = (await _pricingService.GetSubTotal(shoppingCartItem, product001)).subTotal; + var subtotal = (await _pricingService.GetSubTotal(shoppingCartItem, product001, customer001, _store, _currency)) + .subTotal; Assert.AreEqual(110.22, subtotal); } @@ -350,7 +390,7 @@ public async Task GetSubTotal_DiscountedQuantityCap_OnlyAPositiveValueLimits(int It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync((new List { new() { DiscountId = "d1", MaximumDiscountedQuantity = cap } }, 100)); - var (subTotal, _, _) = await _pricingService.GetSubTotal(item, product); + var (subTotal, _, _) = await _pricingService.GetSubTotal(item, product, customer, _store, _currency); Assert.AreEqual(expected, subTotal); } diff --git a/src/Web/Grand.Web.Admin/Controllers/ShoppingCartController.cs b/src/Web/Grand.Web.Admin/Controllers/ShoppingCartController.cs index ca49d1d3a1..149fd63149 100644 --- a/src/Web/Grand.Web.Admin/Controllers/ShoppingCartController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/ShoppingCartController.cs @@ -113,12 +113,16 @@ public async Task GetCartDetails(string customerId) ProductName = product.Name, AttributeInfo = await _productAttributeFormatter.FormatAttributes(product, sci.Attributes, customer), UnitPrice = _priceFormatter.FormatPrice((await _taxService.GetProductPrice(product, - (await _pricingService.GetUnitPrice(sci, product)).unitprice, + (await _pricingService.GetUnitPrice(sci, product, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + _contextAccessor.WorkContext.WorkingCurrency)).unitprice, _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice, _contextAccessor.WorkContext.WorkingCurrency), Total = _priceFormatter.FormatPrice((await _taxService.GetProductPrice(product, - (await _pricingService.GetSubTotal(sci, product)).subTotal, + (await _pricingService.GetSubTotal(sci, product, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + _contextAccessor.WorkContext.WorkingCurrency)).subTotal, _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice, _contextAccessor.WorkContext.WorkingCurrency), @@ -180,12 +184,16 @@ public async Task GetWishlistDetails(string customerId) ProductName = product.Name, AttributeInfo = await _productAttributeFormatter.FormatAttributes(product, sci.Attributes, customer), UnitPrice = _priceFormatter.FormatPrice((await _taxService.GetProductPrice(product, - (await _pricingService.GetUnitPrice(sci, product)).unitprice, + (await _pricingService.GetUnitPrice(sci, product, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + _contextAccessor.WorkContext.WorkingCurrency)).unitprice, _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice, _contextAccessor.WorkContext.WorkingCurrency), Total = _priceFormatter.FormatPrice((await _taxService.GetProductPrice(product, - (await _pricingService.GetSubTotal(sci, product)).subTotal, + (await _pricingService.GetSubTotal(sci, product, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + _contextAccessor.WorkContext.WorkingCurrency)).subTotal, _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice, _contextAccessor.WorkContext.WorkingCurrency), diff --git a/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs index 29eed72c21..80d15a4d18 100644 --- a/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs @@ -862,7 +862,9 @@ public virtual async Task> PrepareShoppingCartItemM if (product != null) { var price = (await taxService.GetProductPrice(product, - (await priceCalculationService.GetUnitPrice(sci, product)).unitprice, + (await priceCalculationService.GetUnitPrice(sci, product, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + _contextAccessor.WorkContext.WorkingCurrency)).unitprice, _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice; var sciModel = new ShoppingCartItemModel { @@ -875,7 +877,9 @@ public virtual async Task> PrepareShoppingCartItemM UnitPrice = priceFormatter.FormatPrice(price, _contextAccessor.WorkContext.WorkingCurrency), UnitPriceValue = price, Total = priceFormatter.FormatPrice((await taxService.GetProductPrice(product, - (await priceCalculationService.GetSubTotal(sci, product)).subTotal, + (await priceCalculationService.GetSubTotal(sci, product, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + _contextAccessor.WorkContext.WorkingCurrency)).subTotal, _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice, _contextAccessor.WorkContext.WorkingCurrency), diff --git a/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsPageHandler.cs b/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsPageHandler.cs index 18b89c911c..efd85086c4 100644 --- a/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsPageHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsPageHandler.cs @@ -960,7 +960,9 @@ private ProductDetailsModel.GiftVoucherModel PrepareGiftVoucherModel(Product pro if (displayPrices) { var attributeValuePriceAdjustment = - await _pricingService.GetProductAttributeValuePriceAdjustment(attributeValue); + await _pricingService.GetProductAttributeValuePriceAdjustment(attributeValue, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + _contextAccessor.WorkContext.WorkingCurrency); var productprice = await _taxService.GetProductPrice(product, attributeValuePriceAdjustment, _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs index 8a7fa2c3ab..0fd54dbffb 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs @@ -111,7 +111,8 @@ await _productAttributeFormatter.FormatAttributes(request.Product, request.Attri else { var productprices = await _taxService.GetProductPrice(request.Product, - (await _pricingService.GetUnitPrice(request.ShoppingCartItem, request.Product)).unitprice, + (await _pricingService.GetUnitPrice(request.ShoppingCartItem, request.Product, request.Customer, request.Store, + request.Currency)).unitprice, request.TaxDisplayType == TaxDisplayType.IncludingTax, request.Customer, request.Store); model.Price = !request.CustomerEnteredPrice.HasValue ? _priceFormatter.FormatPrice(productprices.productprice, request.Currency) diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs index a41c0baf97..ae62b5b19f 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs @@ -188,7 +188,8 @@ public async Task Handle(GetMiniShoppingCart request, else { var productprices = await _taxService.GetProductPrice(product, - (await _pricingService.GetUnitPrice(sci, product)).unitprice, + (await _pricingService.GetUnitPrice(sci, product, request.Customer, request.Store, + request.Currency)).unitprice, request.TaxDisplayType == TaxDisplayType.IncludingTax, request.Customer, request.Store); cartItemModel.UnitPrice = _priceFormatter.FormatPrice(productprices.productprice, request.Currency); } diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniWishlistHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniWishlistHandler.cs index a65af8bb8f..39f4fb03ed 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniWishlistHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniWishlistHandler.cs @@ -97,7 +97,9 @@ public async Task Handle(GetMiniWishlist request, Cancellatio else { var productprice = await _taxService.GetProductPrice(product, - (await _pricingService.GetUnitPrice(sci, product)).unitprice, + (await _pricingService.GetUnitPrice(sci, product, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + _contextAccessor.WorkContext.WorkingCurrency)).unitprice, _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore); diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs index 0192f21f4c..528fc49828 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs @@ -449,7 +449,9 @@ private async Task PrepareCartItems(ShoppingCartModel model, GetShoppingCart req } else { - var unitprices = await _pricingService.GetUnitPrice(sci, product); + var unitprices = await _pricingService.GetUnitPrice(sci, product, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + _contextAccessor.WorkContext.WorkingCurrency); var appliedDiscounts = unitprices.appliedDiscounts; var productprices = await _taxService.GetProductPrice(product, unitprices.unitprice, _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, @@ -457,7 +459,9 @@ private async Task PrepareCartItems(ShoppingCartModel model, GetShoppingCart req cartItemModel.UnitPriceWithoutDiscountValue = (await _taxService.GetProductPrice(product, - (await _pricingService.GetUnitPrice(sci, product, false)).unitprice, + (await _pricingService.GetUnitPrice(sci, product, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + _contextAccessor.WorkContext.WorkingCurrency, false)).unitprice, _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice; @@ -475,7 +479,9 @@ private async Task PrepareCartItems(ShoppingCartModel model, GetShoppingCart req } //sub total - var subtotal = await _pricingService.GetSubTotal(sci, product); + var subtotal = await _pricingService.GetSubTotal(sci, product, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + _contextAccessor.WorkContext.WorkingCurrency); var shoppingCartItemDiscountBase = subtotal.discountAmount; var shoppingCartItemSubTotalWithDiscount = (await _taxService.GetProductPrice(product, subtotal.subTotal, diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs index 436bab48ba..f6f43be4cb 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs @@ -147,7 +147,9 @@ public async Task Handle(GetWishlist request, CancellationToken c else { var productprice = await _taxService.GetProductPrice(product, - (await _pricingService.GetUnitPrice(sci, product)).unitprice, + (await _pricingService.GetUnitPrice(sci, product, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + _contextAccessor.WorkContext.WorkingCurrency)).unitprice, _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore); cartItemModel.UnitPrice = _priceFormatter.FormatPrice(productprice.productprice, request.Currency); @@ -161,7 +163,9 @@ public async Task Handle(GetWishlist request, CancellationToken c else { //sub total - var subtotal = await _pricingService.GetSubTotal(sci, product); + var subtotal = await _pricingService.GetSubTotal(sci, product, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + _contextAccessor.WorkContext.WorkingCurrency); var shoppingCartItemDiscountBase = subtotal.discountAmount; var productprices = await _taxService.GetProductPrice(product, subtotal.subTotal, _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, From 1e947c9358e8940e1f8fe28fd430fa30d3989f13 Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 14:58:39 +0200 Subject: [PATCH 14/33] Pass context, language and host to the checkout attribute formatter CheckoutAttributeFormatter no longer reads the working language, currency, tax display or request host. Callers pass a CalculationContext, the language and the store host; the place-order handler and the cart page still hand over their current request values until their own steps. --- .../Orders/PlaceOrderCommandHandler.cs | 3 +- .../CheckoutAttributeFormatter.cs | 39 +++---- .../ICheckoutAttributeFormatter.cs | 13 ++- .../CheckoutAttributeFormatterTests.cs | 106 ++++++++++++++++++ .../Controllers/ShoppingCartController.cs | 3 +- .../ShoppingCart/GetShoppingCartHandler.cs | 5 +- 6 files changed, 140 insertions(+), 29 deletions(-) create mode 100644 src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeFormatterTests.cs diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs index 15584bdef1..d0e2f189f0 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs @@ -441,7 +441,8 @@ protected virtual async Task PreparePlaceOrderDetails() details.Customer.GetUserFieldFromEntity>(SystemCustomerFieldNames.CheckoutAttributes, _contextAccessor.StoreContext.CurrentStore.Id); details.CheckoutAttributeDescription = - await _checkoutAttributeFormatter.FormatAttributes(details.CheckoutAttributes, details.Customer, details.Store); + await _checkoutAttributeFormatter.FormatAttributes(details.CheckoutAttributes, AmbientCalculationContext, + _contextAccessor.WorkContext.WorkingLanguage, _contextAccessor.StoreContext.CurrentHost); //load and validate customer shopping cart details.Cart = details.Customer.ShoppingCartItems diff --git a/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeFormatter.cs b/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeFormatter.cs index 8e8ba47e63..bbc29f8081 100644 --- a/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeFormatter.cs +++ b/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeFormatter.cs @@ -3,13 +3,12 @@ using Grand.Business.Core.Interfaces.Catalog.Tax; using Grand.Business.Core.Interfaces.Checkout.CheckoutAttributes; using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Utilities.Checkout; using Grand.Domain.Catalog; using Grand.Domain.Common; -using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Orders; using Grand.Domain.Stores; -using Grand.Domain.Tax; -using Grand.Infrastructure; using Grand.SharedKernel.Extensions; using System.Net; @@ -24,15 +23,12 @@ public class CheckoutAttributeFormatter : ICheckoutAttributeFormatter private readonly ICurrencyService _currencyService; private readonly IPriceFormatter _priceFormatter; private readonly ITaxService _taxService; - private readonly IContextAccessor _contextAccessor; - public CheckoutAttributeFormatter(IContextAccessor contextAccessor, - ICheckoutAttributeParser checkoutAttributeParser, + public CheckoutAttributeFormatter(ICheckoutAttributeParser checkoutAttributeParser, ICurrencyService currencyService, ITaxService taxService, IPriceFormatter priceFormatter) { - _contextAccessor = contextAccessor; _checkoutAttributeParser = checkoutAttributeParser; _currencyService = currencyService; _taxService = taxService; @@ -43,16 +39,18 @@ public CheckoutAttributeFormatter(IContextAccessor contextAccessor, /// Formats attributes /// /// Attributes - /// Customer - /// Store + /// Customer, store, currency and tax display the prices are rendered for + /// Language of the attribute names + /// Store address used for file download links (read only when allowHyperlinks is true) /// Separator /// A value indicating whether to encode (HTML) values /// A value indicating whether to render prices /// A value indicating whether to HTML hyperlink tags could be rendered (if required) /// Attributes public virtual async Task FormatAttributes(IList customAttributes, - Customer customer, - Store store, + CalculationContext context, + Language language, + DomainHost host, string separator = "
", bool htmlEncode = true, bool renderPrices = true, @@ -80,7 +78,7 @@ public virtual async Task FormatAttributes(IList custom case AttributeControlType.MultilineTextbox: { //multiline text box - var attributeName = attribute.GetTranslation(a => a.Name, _contextAccessor.WorkContext.WorkingLanguage.Id); + var attributeName = attribute.GetTranslation(a => a.Name, language.Id); //encode (if required) if (htmlEncode) attributeName = WebUtility.HtmlEncode(attributeName); @@ -95,14 +93,14 @@ public virtual async Task FormatAttributes(IList custom { var attributeText = string.Empty; var attributeName = - attribute.GetTranslation(a => a.Name, _contextAccessor.WorkContext.WorkingLanguage.Id); + attribute.GetTranslation(a => a.Name, language.Id); if (allowHyperlinks) { //hyperlinks are allowed var downloadLink = - $"{_contextAccessor.StoreContext.CurrentHost.Url.TrimEnd('/')}/download/getfileupload/?downloadId={downloadGuid}"; + $"{host.Url.TrimEnd('/')}/download/getfileupload/?downloadId={downloadGuid}"; attributeText = - $"{attribute.GetTranslation(a => a.TextPrompt, _contextAccessor.WorkContext.WorkingLanguage.Id)}"; + $"{attribute.GetTranslation(a => a.TextPrompt, language.Id)}"; } formattedAttribute = $"{attributeName}: {attributeText}"; @@ -114,7 +112,7 @@ public virtual async Task FormatAttributes(IList custom { //other attributes (text box, datepicker) formattedAttribute = - $"{attribute.GetTranslation(a => a.Name, _contextAccessor.WorkContext.WorkingLanguage.Id)}: {valueStr}"; + $"{attribute.GetTranslation(a => a.Name, language.Id)}: {valueStr}"; //encode (if required) if (htmlEncode) formattedAttribute = WebUtility.HtmlEncode(formattedAttribute); @@ -128,21 +126,20 @@ public virtual async Task FormatAttributes(IList custom if (attributeValue != null) { formattedAttribute = - $"{attribute.GetTranslation(a => a.Name, _contextAccessor.WorkContext.WorkingLanguage.Id)}: {attributeValue.GetTranslation(a => a.Name, _contextAccessor.WorkContext.WorkingLanguage.Id)}"; + $"{attribute.GetTranslation(a => a.Name, language.Id)}: {attributeValue.GetTranslation(a => a.Name, language.Id)}"; if (renderPrices) { var priceAdjustmentBase = (await _taxService.GetCheckoutAttributePrice(attribute, attributeValue, - _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, - customer, store)) + context.IncludingTax, context.Customer, context.Store)) .checkoutPrice; var priceAdjustment = await _currencyService.ConvertFromPrimaryStoreCurrency(priceAdjustmentBase, - _contextAccessor.WorkContext.WorkingCurrency); + context.Currency); if (priceAdjustmentBase > 0) { var priceAdjustmentStr = _priceFormatter.FormatPrice(priceAdjustment, - _contextAccessor.WorkContext.WorkingCurrency); + context.Currency); formattedAttribute += $" [+{priceAdjustmentStr}]"; } } diff --git a/src/Business/Grand.Business.Core/Interfaces/Checkout/CheckoutAttributes/ICheckoutAttributeFormatter.cs b/src/Business/Grand.Business.Core/Interfaces/Checkout/CheckoutAttributes/ICheckoutAttributeFormatter.cs index ef50d2e43c..b16bdab2c9 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Checkout/CheckoutAttributes/ICheckoutAttributeFormatter.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Checkout/CheckoutAttributes/ICheckoutAttributeFormatter.cs @@ -1,5 +1,6 @@ +using Grand.Business.Core.Utilities.Checkout; using Grand.Domain.Common; -using Grand.Domain.Customers; +using Grand.Domain.Localization; using Grand.Domain.Stores; namespace Grand.Business.Core.Interfaces.Checkout.CheckoutAttributes; @@ -13,16 +14,18 @@ public interface ICheckoutAttributeFormatter /// Formats attributes /// /// Attributes - /// Customer + /// Customer, store, currency and tax display the prices are rendered for + /// Language of the attribute names + /// Store address used for file download links (read only when allowHyperlinks is true) /// Separator /// A value indicating whether to encode (HTML) values /// A value indicating whether to render prices /// A value indicating whether to HTML hyperlink tags could be rendered (if required) - /// Store /// Attributes Task FormatAttributes(IList customAttributes, - Customer customer, - Store store, + CalculationContext context, + Language language, + DomainHost host, string separator = "
", bool htmlEncode = true, bool renderPrices = true, diff --git a/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeFormatterTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeFormatterTests.cs new file mode 100644 index 0000000000..5c3dbc8415 --- /dev/null +++ b/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeFormatterTests.cs @@ -0,0 +1,106 @@ +using Grand.Business.Checkout.Services.CheckoutAttributes; +using Grand.Business.Core.Interfaces.Catalog.Prices; +using Grand.Business.Core.Interfaces.Catalog.Tax; +using Grand.Business.Core.Interfaces.Checkout.CheckoutAttributes; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Utilities.Checkout; +using Grand.Domain.Catalog; +using Grand.Domain.Common; +using Grand.Domain.Customers; +using Grand.Domain.Directory; +using Grand.Domain.Localization; +using Grand.Domain.Orders; +using Grand.Domain.Stores; +using Grand.Domain.Tax; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Business.Checkout.Tests.Services.CheckoutAttributes; + +[TestClass] +public class CheckoutAttributeFormatterTests +{ + private readonly Customer _customer = new(); + private readonly Currency _eur = new() { CurrencyCode = "EUR" }; + private readonly DomainHost _host = new() { Url = "https://shop.example/" }; + private readonly Language _language = new() { Id = "pl" }; + private readonly Store _store = new() { Id = "store" }; + private CheckoutAttributeFormatter _formatter; + private Mock _parserMock; + private Mock _taxServiceMock; + + [TestInitialize] + public void Init() + { + _parserMock = new Mock(); + _taxServiceMock = new Mock(); + var currencyService = new Mock(); + currencyService.Setup(x => x.ConvertFromPrimaryStoreCurrency(12.3, _eur)).ReturnsAsync(11.69); + var priceFormatter = new Mock(); + priceFormatter.Setup(x => x.FormatPrice(11.69, _eur)).Returns("€11.69"); + _formatter = new CheckoutAttributeFormatter(_parserMock.Object, currencyService.Object, + _taxServiceMock.Object, priceFormatter.Object); + } + + private CalculationContext Context(TaxDisplayType taxDisplayType) => + new(_customer, _store, _eur, taxDisplayType); + + [TestMethod] + public async Task FormatAttributes_PricesTheValueInTheContextCurrencyAndTaxDisplay() + { + //Arrange + var value = new CheckoutAttributeValue { Id = "yes", Name = "Yes", PriceAdjustment = 10 }; + var attribute = new CheckoutAttribute { + Id = "wrap", Name = "Gift wrapping", AttributeControlTypeId = AttributeControlType.DropdownList, + CheckoutAttributeValues = { value } + }; + var selected = new List { new() { Key = "wrap", Value = "yes" } }; + _parserMock.Setup(x => x.ParseCheckoutAttributes(selected)).ReturnsAsync(new List { attribute }); + _taxServiceMock.Setup(x => x.GetCheckoutAttributePrice(attribute, value, true, _customer, _store)) + .ReturnsAsync((12.3, 23)); + + //Act + var result = await _formatter.FormatAttributes(selected, Context(TaxDisplayType.IncludingTax), _language, _host, + htmlEncode: false); + + //Assert + Assert.AreEqual("Gift wrapping: Yes [+€11.69]", result); + } + + [TestMethod] + public async Task FormatAttributes_TranslatesNamesIntoTheGivenLanguage() + { + //Arrange + var attribute = new CheckoutAttribute { + Id = "msg", Name = "Gift message", AttributeControlTypeId = AttributeControlType.TextBox, + Locales = { new TranslationEntity { LanguageId = "pl", LocaleKey = "Name", LocaleValue = "Wiadomość" } } + }; + var selected = new List { new() { Key = "msg", Value = "Hi" } }; + _parserMock.Setup(x => x.ParseCheckoutAttributes(selected)).ReturnsAsync(new List { attribute }); + + //Act + var result = await _formatter.FormatAttributes(selected, Context(TaxDisplayType.ExcludingTax), _language, _host, + htmlEncode: false); + + //Assert + Assert.AreEqual("Wiadomość: Hi", result); + } + + [TestMethod] + public async Task FormatAttributes_BuildsFileLinksOnTheGivenHost() + { + //Arrange + var downloadGuid = Guid.NewGuid(); + var attribute = new CheckoutAttribute { + Id = "file", Name = "Logo", TextPrompt = "Download", AttributeControlTypeId = AttributeControlType.FileUpload + }; + var selected = new List { new() { Key = "file", Value = downloadGuid.ToString() } }; + _parserMock.Setup(x => x.ParseCheckoutAttributes(selected)).ReturnsAsync(new List { attribute }); + + //Act + var result = await _formatter.FormatAttributes(selected, Context(TaxDisplayType.ExcludingTax), _language, _host); + + //Assert + StringAssert.Contains(result, $"https://shop.example/download/getfileupload/?downloadId={downloadGuid}"); + } +} diff --git a/src/Web/Grand.Web/Controllers/ShoppingCartController.cs b/src/Web/Grand.Web/Controllers/ShoppingCartController.cs index acf6bf4c95..ef9d40d5d1 100644 --- a/src/Web/Grand.Web/Controllers/ShoppingCartController.cs +++ b/src/Web/Grand.Web/Controllers/ShoppingCartController.cs @@ -168,7 +168,8 @@ await _checkoutAttributeService.GetAllCheckoutAttributes(_contextAccessor.StoreC disabledattributeids = disabledAttributeIds.ToArray(), model = orderTotals, checkoutattributeinfo = - await checkoutAttributeFormatter.FormatAttributes(checkoutAttributes, _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore) + await checkoutAttributeFormatter.FormatAttributes(checkoutAttributes, _contextAccessor.CalculationContext(), + _contextAccessor.WorkContext.WorkingLanguage, _contextAccessor.StoreContext.CurrentHost) }); } diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs index d42391aa85..ca99143293 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs @@ -160,7 +160,10 @@ private async Task PrepareSimpleProperties(ShoppingCartModel model, GetShoppingC request.Customer.GetUserFieldFromEntity>(SystemCustomerFieldNames.CheckoutAttributes, request.Store.Id); model.CheckoutAttributeInfo = - await _checkoutAttributeFormatter.FormatAttributes(checkoutAttributes, request.Customer, request.Store); + await _checkoutAttributeFormatter.FormatAttributes(checkoutAttributes, + new CalculationContext(request.Customer, request.Store, _contextAccessor.WorkContext.WorkingCurrency, + _contextAccessor.WorkContext.TaxDisplayType), + _contextAccessor.WorkContext.WorkingLanguage, _contextAccessor.StoreContext.CurrentHost); if (!request.Cart.Where(x => x.ShoppingCartTypeId is ShoppingCartType.ShoppingCart or ShoppingCartType.Auctions) .ToList().Any()) { From d1d5651d97e6bac4887da2e3fa191cd9876871a5 Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 13:01:23 +0200 Subject: [PATCH 15/33] Pass a calculation context to the checkout calculations --- .../Orders/PlaceOrderCommandHandler.cs | 17 +- ...hoppingCartSubtotalAmountCommandHandler.cs | 9 +- .../Orders/OrderCalculationService.cs | 147 +++++++++--------- .../Orders/ShoppingCartSubTotalService.cs | 39 +++-- .../Services/Orders/ShoppingCartValidator.cs | 6 +- .../ShoppingCartTotalAmountValidator.cs | 8 +- ...ateMinShoppingCartSubtotalAmountCommand.cs | 4 +- .../Orders/IOrderCalculationService.cs | 27 ++-- .../Orders/IShoppingCartSubTotalService.cs | 3 +- .../BrainTreePaymentProvider.cs | 4 +- .../Controllers/PaymentBrainTreeController.cs | 5 +- .../CashOnDeliveryPaymentProvider.cs | 4 +- ...ngCartSubtotalAmountCommandHandlerTests.cs | 7 +- .../Orders/OrderCalculationServiceTests.cs | 67 ++++++-- .../Payments/BrainTreePaymentProviderTests.cs | 3 +- .../CashOnDeliveryPaymentProviderTests.cs | 6 +- .../GetIsPaymentWorkflowRequiredHandler.cs | 9 +- .../Checkout/GetPaymentMethodHandler.cs | 6 +- .../Checkout/GetShippingMethodHandler.cs | 3 +- .../Orders/GetCustomerLoyaltyPointsHandler.cs | 13 +- .../ShoppingCart/GetAddToCartHandler.cs | 9 +- .../GetEstimateShippingResultHandler.cs | 3 +- .../GetMiniShoppingCartHandler.cs | 10 +- .../ShoppingCart/GetOrderTotalsHandler.cs | 17 +- .../ShoppingCart/GetShoppingCartHandler.cs | 2 + 25 files changed, 269 insertions(+), 159 deletions(-) diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs index 045f8d246d..15584bdef1 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs @@ -377,6 +377,11 @@ private async Task GetShoppingCartItemWeight(ShoppingCartItem shoppingCa return weight; } + //TODO M1 PR 3: build the calculation context from PlaceOrderContainer instead of the ambient context + private CalculationContext AmbientCalculationContext => + new(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + _contextAccessor.WorkContext.WorkingCurrency, _contextAccessor.WorkContext.TaxDisplayType); + protected virtual async Task PreparePlaceOrderDetails() { var details = new PlaceOrderContainer { @@ -487,7 +492,7 @@ protected virtual async Task PreparePlaceOrderDetails() //sub total //sub total (incl tax) - var shoppingCartSubTotal = await _orderTotalCalculationService.GetShoppingCartSubTotal(details.Cart, true); + var shoppingCartSubTotal = await _orderTotalCalculationService.GetShoppingCartSubTotal(details.Cart, AmbientCalculationContext, true); var orderSubTotalDiscountAmount = shoppingCartSubTotal.discountAmount; var orderSubTotalAppliedDiscounts = shoppingCartSubTotal.appliedDiscounts; var subTotalWithoutDiscountBase = shoppingCartSubTotal.subTotalWithoutDiscount; @@ -500,7 +505,7 @@ protected virtual async Task PreparePlaceOrderDetails() details.AppliedDiscounts.Add(disc); //sub total (excl tax) - shoppingCartSubTotal = await _orderTotalCalculationService.GetShoppingCartSubTotal(details.Cart, false); + shoppingCartSubTotal = await _orderTotalCalculationService.GetShoppingCartSubTotal(details.Cart, AmbientCalculationContext, false); orderSubTotalDiscountAmount = shoppingCartSubTotal.discountAmount; subTotalWithoutDiscountBase = shoppingCartSubTotal.subTotalWithoutDiscount; @@ -555,9 +560,9 @@ protected virtual async Task PreparePlaceOrderDetails() //shipping total var (orderShippingTotalInclTax, _, shippingTotalDiscounts) = - await _orderTotalCalculationService.GetShoppingCartShippingTotal(details.Cart, true); + await _orderTotalCalculationService.GetShoppingCartShippingTotal(details.Cart, AmbientCalculationContext, true); var orderShippingTotalExclTax = - (await _orderTotalCalculationService.GetShoppingCartShippingTotal(details.Cart, false)) + (await _orderTotalCalculationService.GetShoppingCartShippingTotal(details.Cart, AmbientCalculationContext, false)) .shoppingCartShippingTotal; if (!orderShippingTotalInclTax.HasValue || !orderShippingTotalExclTax.HasValue) throw new GrandException("Shipping total couldn't be calculated"); @@ -584,7 +589,7 @@ protected virtual async Task PreparePlaceOrderDetails() //tax total //tax amount - var (taxTotal, taxRates) = await _orderTotalCalculationService.GetTaxTotal(details.Cart); + var (taxTotal, taxRates) = await _orderTotalCalculationService.GetTaxTotal(details.Cart, AmbientCalculationContext); details.OrderTaxTotal = taxTotal; //tax rates @@ -596,7 +601,7 @@ protected virtual async Task PreparePlaceOrderDetails() //order total (and applied discounts, gift vouchers, loyalty points) var (orderTotal, orderDiscountAmount, orderAppliedDiscounts, appliedGiftVouchers, redeemedLoyaltyPoints, - redeemedLoyaltyPointsAmount) = await _orderTotalCalculationService.GetShoppingCartTotal(details.Cart); + redeemedLoyaltyPointsAmount) = await _orderTotalCalculationService.GetShoppingCartTotal(details.Cart, AmbientCalculationContext); if (!orderTotal.HasValue) throw new GrandException("Order total couldn't be calculated"); diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ValidateMinShoppingCartSubtotalAmountCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ValidateMinShoppingCartSubtotalAmountCommandHandler.cs index eef41cfe94..1160a540de 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ValidateMinShoppingCartSubtotalAmountCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ValidateMinShoppingCartSubtotalAmountCommandHandler.cs @@ -1,5 +1,6 @@ using Grand.Business.Core.Commands.Checkout.Orders; using Grand.Business.Core.Interfaces.Checkout.Orders; +using Grand.Business.Core.Utilities.Checkout; using Grand.Domain.Orders; using Grand.Mediator; @@ -25,11 +26,13 @@ public async Task Handle(ValidateMinShoppingCartSubtotalAmountCommand requ { ArgumentNullException.ThrowIfNull(request.Cart); ArgumentNullException.ThrowIfNull(request.Customer); + ArgumentNullException.ThrowIfNull(request.Context); - return await ValidateMinOrderSubtotalAmount(request.Cart); + return await ValidateMinOrderSubtotalAmount(request.Cart, request.Context); } - protected virtual async Task ValidateMinOrderSubtotalAmount(IList cart) + protected virtual async Task ValidateMinOrderSubtotalAmount(IList cart, + CalculationContext context) { ArgumentNullException.ThrowIfNull(cart); @@ -41,7 +44,7 @@ protected virtual async Task ValidateMinOrderSubtotalAmount(IList /// Ctor /// - /// Work context /// Shopping cart subtotal service /// Tax service /// Shipping service @@ -48,8 +46,7 @@ public class OrderCalculationService : IOrderCalculationService /// Shipping settings /// Shopping cart settings /// Catalog settings - public OrderCalculationService(IContextAccessor contextAccessor, - IShoppingCartSubTotalService shoppingCartSubTotalService, + public OrderCalculationService(IShoppingCartSubTotalService shoppingCartSubTotalService, ITaxService taxService, IShippingService shippingService, IPaymentService paymentService, @@ -65,7 +62,6 @@ public OrderCalculationService(IContextAccessor contextAccessor, ShoppingCartSettings shoppingCartSettings, CatalogSettings catalogSettings) { - _contextAccessor = contextAccessor; _shoppingCartSubTotalService = shoppingCartSubTotalService; _taxService = taxService; _shippingService = shippingService; @@ -87,7 +83,6 @@ public OrderCalculationService(IContextAccessor contextAccessor, #region Fields - private readonly IContextAccessor _contextAccessor; private readonly IShoppingCartSubTotalService _shoppingCartSubTotalService; private readonly ITaxService _taxService; private readonly IShippingService _shippingService; @@ -125,7 +120,7 @@ public OrderCalculationService(IContextAccessor contextAccessor, return (shippingDiscountAmount, appliedDiscounts); var allDiscounts = await _discountService.GetActiveDiscountsByContext(DiscountType.AssignedToShipping, - _contextAccessor.StoreContext.CurrentStore.Id, currency.CurrencyCode); + store.Id, currency.CurrencyCode); var allowedDiscounts = new List(); if (allDiscounts != null) foreach (var discount in allDiscounts) @@ -144,7 +139,7 @@ public OrderCalculationService(IContextAccessor contextAccessor, } var (appliedDiscount, discountAmount) = await _discountService.GetPreferredDiscount(allowedDiscounts, - customer, _contextAccessor.WorkContext.WorkingCurrency, shippingTotal); + customer, currency, shippingTotal); appliedDiscounts = appliedDiscount; shippingDiscountAmount = discountAmount; @@ -153,7 +148,7 @@ public OrderCalculationService(IContextAccessor contextAccessor, if (_shoppingCartSettings.RoundPrices) shippingDiscountAmount = - RoundingHelper.RoundPrice(shippingDiscountAmount, _contextAccessor.WorkContext.WorkingCurrency); + RoundingHelper.RoundPrice(shippingDiscountAmount, currency); return (shippingDiscountAmount, appliedDiscounts); } @@ -175,7 +170,7 @@ public OrderCalculationService(IContextAccessor contextAccessor, return (discountAmount, appliedDiscounts); var allDiscounts = await _discountService.GetActiveDiscountsByContext(DiscountType.AssignedToOrderTotal, - _contextAccessor.StoreContext.CurrentStore.Id, currency.CurrencyCode); + store.Id, currency.CurrencyCode); var allowedDiscounts = new List(); if (allDiscounts != null) foreach (var discount in allDiscounts) @@ -194,7 +189,7 @@ public OrderCalculationService(IContextAccessor contextAccessor, } var preferredDiscount = await _discountService.GetPreferredDiscount(allowedDiscounts, customer, - _contextAccessor.WorkContext.WorkingCurrency, orderTotal); + currency, orderTotal); appliedDiscounts = preferredDiscount.appliedDiscount; discountAmount = preferredDiscount.discountAmount; @@ -202,7 +197,7 @@ public OrderCalculationService(IContextAccessor contextAccessor, discountAmount = 0; if (_shoppingCartSettings.RoundPrices) - discountAmount = RoundingHelper.RoundPrice(discountAmount, _contextAccessor.WorkContext.WorkingCurrency); + discountAmount = RoundingHelper.RoundPrice(discountAmount, currency); return (discountAmount, appliedDiscounts); } @@ -239,6 +234,7 @@ private async Task> GetActiveGiftVouchers(Customer customer, /// Gets shopping cart subtotal /// /// Cart + /// Customer, store, currency and tax display the calculation is for /// A value indicating whether calculated price should include tax /// /// Not virtual: payment providers read the subtotal from , @@ -247,9 +243,9 @@ private async Task> GetActiveGiftVouchers(Customer customer, /// public Task<(double discountAmount, List appliedDiscounts, double subTotalWithoutDiscount, double subTotalWithDiscount, SortedDictionary taxRates)> - GetShoppingCartSubTotal(IList cart, bool includingTax) + GetShoppingCartSubTotal(IList cart, CalculationContext context, bool includingTax) { - return _shoppingCartSubTotalService.GetShoppingCartSubTotal(cart, includingTax); + return _shoppingCartSubTotalService.GetShoppingCartSubTotal(cart, context, includingTax); } @@ -257,10 +253,12 @@ private async Task> GetActiveGiftVouchers(Customer customer, /// Gets shopping cart additional shipping charge /// /// Cart + /// Customer, store, currency and tax display the calculation is for /// Additional shipping charge - public virtual async Task GetShoppingCartAdditionalShippingCharge(IList cart) + public virtual async Task GetShoppingCartAdditionalShippingCharge(IList cart, + CalculationContext context) { - var isFreeShipping = await IsFreeShipping(cart); + var isFreeShipping = await IsFreeShipping(cart, context); if (isFreeShipping) return 0; @@ -274,10 +272,11 @@ public virtual async Task GetShoppingCartAdditionalShippingCharge(IList< /// Gets a value indicating whether shipping is free /// /// Cart + /// Customer, store, currency and tax display the calculation is for /// A value indicating whether shipping is free - public virtual async Task IsFreeShipping(IList cart) + public virtual async Task IsFreeShipping(IList cart, CalculationContext context) { - var customer = _contextAccessor.WorkContext.CurrentCustomer; + var customer = context.Customer; if (customer != null) { //check whether customer has a free shipping @@ -302,7 +301,7 @@ public virtual async Task IsFreeShipping(IList cart) if (!_shippingSettings.FreeShippingOverXEnabled) return false; //check whether we have subtotal enough to have free shipping var (_, _, _, subTotalWithDiscount, _) = - await GetShoppingCartSubTotal(cart, _shippingSettings.FreeShippingOverXIncludingTax); + await GetShoppingCartSubTotal(cart, context, _shippingSettings.FreeShippingOverXIncludingTax); return subTotalWithDiscount > _shippingSettings.FreeShippingOverXValue; //otherwise, return false @@ -313,25 +312,26 @@ public virtual async Task IsFreeShipping(IList cart) /// /// Shipping rate to adjust /// Cart + /// Customer, store, currency and tax display the calculation is for /// Adjusted shipping rate public virtual async Task<(double shippingRate, List appliedDiscounts)> AdjustShippingRate( - double shippingRate, IList cart) + double shippingRate, IList cart, CalculationContext context) { var appliedDiscounts = new List(); //free shipping - if (await IsFreeShipping(cart)) + if (await IsFreeShipping(cart, context)) return (0, appliedDiscounts); //additional shipping charges - var additionalShippingCharge = await GetShoppingCartAdditionalShippingCharge(cart); + var additionalShippingCharge = await GetShoppingCartAdditionalShippingCharge(cart, context); var adjustedRate = shippingRate + additionalShippingCharge; //discount var (discountAmount, applyDiscounts) = await GetShippingDiscount( - _contextAccessor.WorkContext.CurrentCustomer, - _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingCurrency, adjustedRate); + context.Customer, + context.Store, + context.Currency, adjustedRate); appliedDiscounts = applyDiscounts; adjustedRate -= discountAmount; @@ -340,7 +340,7 @@ public virtual async Task IsFreeShipping(IList cart) adjustedRate = 0; if (_shoppingCartSettings.RoundPrices) - adjustedRate = RoundingHelper.RoundPrice(adjustedRate, _contextAccessor.WorkContext.WorkingCurrency); + adjustedRate = RoundingHelper.RoundPrice(adjustedRate, context.Currency); return (adjustedRate, appliedDiscounts); } @@ -349,13 +349,14 @@ public virtual async Task IsFreeShipping(IList cart) /// Gets shopping cart shipping total /// /// Cart + /// Customer, store, currency and tax display the calculation is for /// Shipping total public virtual async Task<(double? shoppingCartShippingTotal, double taxRate, List appliedDiscounts)> - GetShoppingCartShippingTotal(IList cart) + GetShoppingCartShippingTotal(IList cart, CalculationContext context) { - var includingTax = _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax; - return await GetShoppingCartShippingTotal(cart, includingTax); + var includingTax = context.IncludingTax; + return await GetShoppingCartShippingTotal(cart, context, includingTax); } @@ -363,21 +364,22 @@ public virtual async /// Gets shopping cart shipping total /// /// Cart + /// Customer, store, currency and tax display the calculation is for /// A value indicating whether calculated price should include tax /// Shipping total public virtual async Task<(double? shoppingCartShippingTotal, double taxRate, List appliedDiscounts)> - GetShoppingCartShippingTotal(IList cart, bool includingTax) + GetShoppingCartShippingTotal(IList cart, CalculationContext context, bool includingTax) { double? shippingTotal = null; var appliedDiscounts = new List(); double taxRate = 0; - var customer = _contextAccessor.WorkContext.CurrentCustomer; - var store = _contextAccessor.StoreContext.CurrentStore; + var customer = context.Customer; + var store = context.Store; var currency = await _currencyService.GetPrimaryExchangeRateCurrency(); - var isFreeShipping = await IsFreeShipping(cart); + var isFreeShipping = await IsFreeShipping(cart, context); if (isFreeShipping) return (0, taxRate, appliedDiscounts); @@ -385,12 +387,12 @@ public virtual async if (customer != null) shippingOption = customer.GetUserFieldFromEntity(SystemCustomerFieldNames.SelectedShippingOption, - _contextAccessor.StoreContext.CurrentStore.Id); + context.Store.Id); if (shippingOption != null) { var rate = shippingOption.Rate; - var adjustShippingRate = await AdjustShippingRate(rate, cart); + var adjustShippingRate = await AdjustShippingRate(rate, cart, context); shippingTotal = adjustShippingRate.shippingRate; appliedDiscounts = adjustShippingRate.appliedDiscounts; } @@ -402,8 +404,8 @@ public virtual async shippingAddress = customer.ShippingAddress; var shippingRateMethods = - await _shippingService.LoadActiveShippingRateCalculationProviders(_contextAccessor.WorkContext.CurrentCustomer, - _contextAccessor.StoreContext.CurrentStore.Id, cart); + await _shippingService.LoadActiveShippingRateCalculationProviders(context.Customer, + context.Store.Id, cart); if (!shippingRateMethods.Any() && !_shippingSettings.AllowPickUpInStore) throw new GrandException("Shipping rate method could not be loaded"); @@ -414,7 +416,7 @@ await _shippingService.LoadActiveShippingRateCalculationProviders(_contextAccess var shippingOptionRequest = await _shippingService.CreateShippingOptionRequests(customer, cart, shippingAddress, - _contextAccessor.StoreContext.CurrentStore); + context.Store); double? fixedRate = null; //calculate fixed rates for each request-package @@ -428,7 +430,7 @@ await _shippingService.LoadActiveShippingRateCalculationProviders(_contextAccess if (fixedRate.HasValue) { //adjust shipping rate - var adjustShippingRate = await AdjustShippingRate(fixedRate.Value, cart); + var adjustShippingRate = await AdjustShippingRate(fixedRate.Value, cart, context); shippingTotal = adjustShippingRate.shippingRate; appliedDiscounts = adjustShippingRate.appliedDiscounts; } @@ -463,31 +465,32 @@ await _shippingService.LoadActiveShippingRateCalculationProviders(_contextAccess /// Gets tax /// /// Shopping cart + /// Customer, store, currency and tax display the calculation is for /// /// A value indicating whether we should use payment method additional fee when /// calculating tax /// /// Tax total public virtual async Task<(double taxtotal, SortedDictionary taxRates)> GetTaxTotal( - IList cart, bool usePaymentMethodAdditionalFee = true) + IList cart, CalculationContext context, bool usePaymentMethodAdditionalFee = true) { ArgumentNullException.ThrowIfNull(cart); var taxRates = new SortedDictionary(); - var customer = _contextAccessor.WorkContext.CurrentCustomer; - var store = _contextAccessor.StoreContext.CurrentStore; + var customer = context.Customer; + var store = context.Store; var paymentMethodSystemName = ""; if (customer != null) paymentMethodSystemName = customer.GetUserFieldFromEntity( SystemCustomerFieldNames.SelectedPaymentMethod, - _contextAccessor.StoreContext.CurrentStore.Id); + context.Store.Id); //order sub total (items + checkout attributes) double subTotalTaxTotal = 0; - var shoppingCartSubTotal = await GetShoppingCartSubTotal(cart, false); + var shoppingCartSubTotal = await GetShoppingCartSubTotal(cart, context, false); var orderSubTotalTaxRates = shoppingCartSubTotal.taxRates; foreach (var (taxRate, taxValue) in orderSubTotalTaxRates) @@ -505,10 +508,10 @@ await _shippingService.LoadActiveShippingRateCalculationProviders(_contextAccess double shippingTax = 0; if (_taxSettings.ShippingIsTaxable) { - var shippingTotalExcl = await GetShoppingCartShippingTotal(cart, false); + var shippingTotalExcl = await GetShoppingCartShippingTotal(cart, context, false); var shippingExclTax = shippingTotalExcl.shoppingCartShippingTotal; - var (shippingInclTax, taxRate, _) = await GetShoppingCartShippingTotal(cart, true); + var (shippingInclTax, taxRate, _) = await GetShoppingCartShippingTotal(cart, context, true); if (shippingExclTax.HasValue && shippingInclTax.HasValue) { @@ -567,7 +570,7 @@ await _shippingService.LoadActiveShippingRateCalculationProviders(_contextAccess taxTotal = 0; //round tax if (_shoppingCartSettings.RoundPrices) - taxTotal = RoundingHelper.RoundPrice(taxTotal, _contextAccessor.WorkContext.WorkingCurrency); + taxTotal = RoundingHelper.RoundPrice(taxTotal, context.Currency); return (taxTotal, taxRates); } @@ -577,28 +580,29 @@ await _shippingService.LoadActiveShippingRateCalculationProviders(_contextAccess /// Gets shopping cart total /// /// Cart + /// Customer, store, currency and tax display the calculation is for /// Use Loyalty Points /// A value indicating whether we should use payment method additional fee /// Shopping cart total, discount amount, applied discounts, gift vouchers, loyalty point/amount public virtual async Task<(double? shoppingCartTotal, double discountAmount, List appliedDiscounts, List appliedGiftVouchers, int redeemedLoyaltyPoints, double redeemedLoyaltyPointsAmount)> - GetShoppingCartTotal(IList cart, bool? useLoyaltyPoints = null, + GetShoppingCartTotal(IList cart, CalculationContext context, bool? useLoyaltyPoints = null, bool usePaymentMethodAdditionalFee = true) { var redeemedLoyaltyPoints = 0; double redeemedLoyaltyPointsAmount = 0; - var paymentMethodSystemName = _contextAccessor.WorkContext.CurrentCustomer.GetUserFieldFromEntity( + var paymentMethodSystemName = context.Customer.GetUserFieldFromEntity( SystemCustomerFieldNames.SelectedPaymentMethod, - _contextAccessor.StoreContext.CurrentStore.Id); + context.Store.Id); //subtotal without tax - var subTotal = await GetShoppingCartSubTotal(cart, false); + var subTotal = await GetShoppingCartSubTotal(cart, context, false); var subTotalWithDiscountBase = subTotal.subTotalWithDiscount; //shipping without tax - var shippingTotal = await GetShoppingCartShippingTotal(cart, false); + var shippingTotal = await GetShoppingCartShippingTotal(cart, context, false); var shoppingCartShipping = shippingTotal.shoppingCartShippingTotal; //payment method additional fee without tax @@ -609,12 +613,12 @@ await _shippingService.LoadActiveShippingRateCalculationProviders(_contextAccess await _paymentService.GetAdditionalHandlingFee(cart, paymentMethodSystemName); paymentMethodAdditionalFeeWithoutTax = (await _taxService.GetPaymentMethodAdditionalFee(paymentMethodAdditionalFee, false, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)) + context.Customer, context.Store)) .paymentPrice; } //tax - var shoppingCartTax = (await GetTaxTotal(cart, usePaymentMethodAdditionalFee)).taxtotal; + var shoppingCartTax = (await GetTaxTotal(cart, context, usePaymentMethodAdditionalFee)).taxtotal; //order total double resultTemp = 0; @@ -624,12 +628,12 @@ await _shippingService.LoadActiveShippingRateCalculationProviders(_contextAccess resultTemp += paymentMethodAdditionalFeeWithoutTax; resultTemp += shoppingCartTax; if (_shoppingCartSettings.RoundPrices) - resultTemp = RoundingHelper.RoundPrice(resultTemp, _contextAccessor.WorkContext.WorkingCurrency); + resultTemp = RoundingHelper.RoundPrice(resultTemp, context.Currency); #region Order total discount - var totalDiscount = await GetOrderTotalDiscount(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingCurrency, resultTemp); + var totalDiscount = await GetOrderTotalDiscount(context.Customer, context.Store, + context.Currency, resultTemp); var discountAmount = totalDiscount.orderTotalDiscount; var appliedDiscounts = totalDiscount.appliedDiscounts; @@ -643,7 +647,7 @@ await _shippingService.LoadActiveShippingRateCalculationProviders(_contextAccess if (resultTemp < 0) resultTemp = 0; if (_shoppingCartSettings.RoundPrices) - resultTemp = RoundingHelper.RoundPrice(resultTemp, _contextAccessor.WorkContext.WorkingCurrency); + resultTemp = RoundingHelper.RoundPrice(resultTemp, context.Currency); #endregion @@ -652,8 +656,8 @@ await _shippingService.LoadActiveShippingRateCalculationProviders(_contextAccess var appliedGiftVouchers = new List(); //we don't apply gift vouchers for recurring products var giftVouchers = - await GetActiveGiftVouchers(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.WorkContext.WorkingCurrency, - _contextAccessor.StoreContext.CurrentStore); + await GetActiveGiftVouchers(context.Customer, context.Currency, + context.Store); if (giftVouchers != null) foreach (var gc in giftVouchers) if (resultTemp > 0) @@ -676,7 +680,7 @@ await GetActiveGiftVouchers(_contextAccessor.WorkContext.CurrentCustomer, _conte if (resultTemp < 0) resultTemp = 0; if (_shoppingCartSettings.RoundPrices) - resultTemp = RoundingHelper.RoundPrice(resultTemp, _contextAccessor.WorkContext.WorkingCurrency); + resultTemp = RoundingHelper.RoundPrice(resultTemp, context.Currency); if (!shoppingCartShipping.HasValue) //we have errors @@ -689,18 +693,18 @@ await GetActiveGiftVouchers(_contextAccessor.WorkContext.CurrentCustomer, _conte if (_loyaltyPointsSettings.Enabled) { - useLoyaltyPoints ??= _contextAccessor.WorkContext.CurrentCustomer.GetUserFieldFromEntity( + useLoyaltyPoints ??= context.Customer.GetUserFieldFromEntity( SystemCustomerFieldNames.UseLoyaltyPointsDuringCheckout, - _contextAccessor.StoreContext.CurrentStore.Id); + context.Store.Id); if (useLoyaltyPoints.Value) { var loyaltyPointsBalance = - await _loyaltyPointsService.GetLoyaltyPointsBalance(_contextAccessor.WorkContext.CurrentCustomer.Id, - _contextAccessor.StoreContext.CurrentStore.Id); + await _loyaltyPointsService.GetLoyaltyPointsBalance(context.Customer.Id, + context.Store.Id); if (CheckMinimumLoyaltyPointsToUseRequirement(loyaltyPointsBalance)) { - var loyaltyPointsBalanceAmount = await ConvertLoyaltyPointsToAmount(loyaltyPointsBalance); + var loyaltyPointsBalanceAmount = await ConvertLoyaltyPointsToAmount(loyaltyPointsBalance, context.Currency); if (orderTotal > 0) { if (orderTotal > loyaltyPointsBalanceAmount) @@ -708,14 +712,14 @@ await _loyaltyPointsService.GetLoyaltyPointsBalance(_contextAccessor.WorkContext redeemedLoyaltyPoints = loyaltyPointsBalance; redeemedLoyaltyPointsAmount = await _currencyService.ConvertFromPrimaryStoreCurrency(loyaltyPointsBalanceAmount, - _contextAccessor.WorkContext.WorkingCurrency); + context.Currency); } else { redeemedLoyaltyPointsAmount = orderTotal; redeemedLoyaltyPoints = ConvertAmountToLoyaltyPoints( await _currencyService.ConvertToPrimaryStoreCurrency(redeemedLoyaltyPointsAmount, - _contextAccessor.WorkContext.WorkingCurrency)); + context.Currency)); } } } @@ -726,7 +730,7 @@ await _currencyService.ConvertToPrimaryStoreCurrency(redeemedLoyaltyPointsAmount orderTotal -= redeemedLoyaltyPointsAmount; if (_shoppingCartSettings.RoundPrices) - orderTotal = RoundingHelper.RoundPrice(orderTotal, _contextAccessor.WorkContext.WorkingCurrency); + orderTotal = RoundingHelper.RoundPrice(orderTotal, context.Currency); return (orderTotal, discountAmount, appliedDiscounts, appliedGiftVouchers, redeemedLoyaltyPoints, redeemedLoyaltyPointsAmount); @@ -736,14 +740,15 @@ await _currencyService.ConvertToPrimaryStoreCurrency(redeemedLoyaltyPointsAmount /// Converts existing loyalty points to amount /// /// Loyalty points + /// Currency the amount is rounded in /// Converted value - public virtual async Task ConvertLoyaltyPointsToAmount(int loyaltyPoints) + public virtual async Task ConvertLoyaltyPointsToAmount(int loyaltyPoints, Currency currency) { if (loyaltyPoints <= 0) return 0; var result = loyaltyPoints * _loyaltyPointsSettings.ExchangeRate; - if (_shoppingCartSettings.RoundPrices) result = RoundingHelper.RoundPrice(result, _contextAccessor.WorkContext.WorkingCurrency); + if (_shoppingCartSettings.RoundPrices) result = RoundingHelper.RoundPrice(result, currency); return await Task.FromResult(result); } diff --git a/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartSubTotalService.cs b/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartSubTotalService.cs index cc2acfbe64..3849d91c87 100644 --- a/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartSubTotalService.cs +++ b/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartSubTotalService.cs @@ -7,6 +7,7 @@ using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Utilities.Catalog; +using Grand.Business.Core.Utilities.Checkout; using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Customers; @@ -14,7 +15,6 @@ using Grand.Domain.Discounts; using Grand.Domain.Orders; using Grand.Domain.Stores; -using Grand.Infrastructure; namespace Grand.Business.Checkout.Services.Orders; @@ -25,8 +25,7 @@ public class ShoppingCartSubTotalService : IShoppingCartSubTotalService { #region Ctor - public ShoppingCartSubTotalService(IContextAccessor contextAccessor, - IPricingService pricingService, + public ShoppingCartSubTotalService(IPricingService pricingService, ITaxService taxService, ICheckoutAttributeParser checkoutAttributeParser, IDiscountService discountService, @@ -36,7 +35,6 @@ public ShoppingCartSubTotalService(IContextAccessor contextAccessor, ShoppingCartSettings shoppingCartSettings, CatalogSettings catalogSettings) { - _contextAccessor = contextAccessor; _pricingService = pricingService; _taxService = taxService; _checkoutAttributeParser = checkoutAttributeParser; @@ -52,7 +50,6 @@ public ShoppingCartSubTotalService(IContextAccessor contextAccessor, #region Fields - private readonly IContextAccessor _contextAccessor; private readonly IPricingService _pricingService; private readonly ITaxService _taxService; private readonly ICheckoutAttributeParser _checkoutAttributeParser; @@ -84,7 +81,7 @@ public ShoppingCartSubTotalService(IContextAccessor contextAccessor, return (discountAmount, appliedDiscounts); var allDiscounts = await _discountService.GetActiveDiscountsByContext(DiscountType.AssignedToOrderSubTotal, - _contextAccessor.StoreContext.CurrentStore.Id, currency.CurrencyCode); + store.Id, currency.CurrencyCode); var allowedDiscounts = new List(); if (allDiscounts != null) foreach (var discount in allDiscounts) @@ -103,7 +100,7 @@ public ShoppingCartSubTotalService(IContextAccessor contextAccessor, } var preferredDiscounts = await _discountService.GetPreferredDiscount(allowedDiscounts, customer, - _contextAccessor.WorkContext.WorkingCurrency, orderSubTotal); + currency, orderSubTotal); appliedDiscounts = preferredDiscounts.appliedDiscount; discountAmount = preferredDiscounts.discountAmount; @@ -121,10 +118,11 @@ public ShoppingCartSubTotalService(IContextAccessor contextAccessor, /// Gets shopping cart subtotal /// /// Cart + /// Customer, store, currency and tax display the calculation is for /// A value indicating whether calculated price should include tax public virtual async Task<(double discountAmount, List appliedDiscounts, double subTotalWithoutDiscount, double subTotalWithDiscount, SortedDictionary taxRates)> - GetShoppingCartSubTotal(IList cart, bool includingTax) + GetShoppingCartSubTotal(IList cart, CalculationContext context, bool includingTax) { double discountAmount = 0; var appliedDiscounts = new List(); @@ -136,8 +134,8 @@ public ShoppingCartSubTotalService(IContextAccessor contextAccessor, return (discountAmount, appliedDiscounts, subTotalWithoutDiscount, subTotalWithDiscount, taxRates); //get the customer - var customer = _contextAccessor.WorkContext.CurrentCustomer; - var store = _contextAccessor.StoreContext.CurrentStore; + var customer = context.Customer; + var store = context.Store; //sub totals double subTotalExclTaxWithoutDiscount = 0; @@ -149,8 +147,7 @@ public ShoppingCartSubTotalService(IContextAccessor contextAccessor, continue; var subtotal = await _pricingService.GetSubTotal(shoppingCartItem, product, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingCurrency); + customer, store, context.Currency); var sciSubTotal = subtotal.subTotal; var pricesExcl = await _taxService.GetProductPrice(product, sciSubTotal, false, customer, store); @@ -180,10 +177,10 @@ public ShoppingCartSubTotalService(IContextAccessor contextAccessor, var checkoutAttributePriceExclTax = await _taxService.GetCheckoutAttributePrice(attributeValue.ca, attributeValue.cav, false, customer, store); var caExclTax = await _currencyService.ConvertFromPrimaryStoreCurrency(checkoutAttributePriceExclTax.checkoutPrice, - _contextAccessor.WorkContext.WorkingCurrency); + context.Currency); var (checkoutPrice, taxRate) = await _taxService.GetCheckoutAttributePrice(attributeValue.ca, attributeValue.cav, true, customer, store); var caInclTax = await _currencyService.ConvertFromPrimaryStoreCurrency(checkoutPrice, - _contextAccessor.WorkContext.WorkingCurrency); + context.Currency); subTotalExclTaxWithoutDiscount += caExclTax; subTotalInclTaxWithoutDiscount += caInclTax; @@ -206,12 +203,12 @@ public ShoppingCartSubTotalService(IContextAccessor contextAccessor, if (_shoppingCartSettings.RoundPrices) subTotalWithoutDiscount = RoundingHelper.RoundPrice(subTotalWithoutDiscount, - _contextAccessor.WorkContext.WorkingCurrency); + context.Currency); //We calculate discount amount on order subtotal excl tax (discount first) //calculate discount amount ('Applied to order subtotal' discount) - var orderSubtotalDiscount = await GetOrderSubtotalDiscount(customer, _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingCurrency, subTotalExclTaxWithoutDiscount); + var orderSubtotalDiscount = await GetOrderSubtotalDiscount(customer, store, context.Currency, + subTotalExclTaxWithoutDiscount); var discountAmountExclTax = orderSubtotalDiscount.ordersubtotaldiscount; appliedDiscounts = orderSubtotalDiscount.appliedDiscounts; @@ -237,7 +234,7 @@ public ShoppingCartSubTotalService(IContextAccessor contextAccessor, discountAmountInclTax += discountTax; taxValue = taxRates[taxRate] - discountTax; if (_shoppingCartSettings.RoundPrices) - taxValue = RoundingHelper.RoundPrice(taxValue, _contextAccessor.WorkContext.WorkingCurrency); + taxValue = RoundingHelper.RoundPrice(taxValue, context.Currency); taxRates[taxRate] = taxValue; } @@ -248,9 +245,9 @@ public ShoppingCartSubTotalService(IContextAccessor contextAccessor, if (_shoppingCartSettings.RoundPrices) { discountAmountInclTax = RoundingHelper.RoundPrice(discountAmountInclTax, - _contextAccessor.WorkContext.WorkingCurrency); + context.Currency); discountAmountExclTax = RoundingHelper.RoundPrice(discountAmountExclTax, - _contextAccessor.WorkContext.WorkingCurrency); + context.Currency); } if (includingTax) @@ -269,7 +266,7 @@ public ShoppingCartSubTotalService(IContextAccessor contextAccessor, if (_shoppingCartSettings.RoundPrices) subTotalWithDiscount = RoundingHelper.RoundPrice(subTotalWithDiscount, - _contextAccessor.WorkContext.WorkingCurrency); + context.Currency); return (discountAmount, appliedDiscounts, subTotalWithoutDiscount, subTotalWithDiscount, taxRates); } diff --git a/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartValidator.cs b/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartValidator.cs index 92bc00b3a8..4a692b3677 100644 --- a/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartValidator.cs +++ b/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartValidator.cs @@ -151,7 +151,11 @@ public virtual async Task> GetShoppingCartWarnings(IList().ValidateAsync( new ShoppingCartTotalAmountValidatorRecord(_contextAccessor.WorkContext.CurrentCustomer, - _contextAccessor.WorkContext.WorkingCurrency, shoppingCart)); + _contextAccessor.WorkContext.WorkingCurrency, shoppingCart, + //TODO M1 PR 3: the cart validator still reads the ambient context + new CalculationContext(_contextAccessor.WorkContext.CurrentCustomer, + _contextAccessor.StoreContext.CurrentStore, _contextAccessor.WorkContext.WorkingCurrency, + _contextAccessor.WorkContext.TaxDisplayType))); if (!resultCheckoutAttributes.IsValid) warnings.AddRange(resultCheckoutAttributes.Errors.Select(x => x.ErrorMessage)); } diff --git a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartTotalAmountValidator.cs b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartTotalAmountValidator.cs index 87823a2112..102150feb8 100644 --- a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartTotalAmountValidator.cs +++ b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartTotalAmountValidator.cs @@ -3,6 +3,7 @@ using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Utilities.Checkout; using Grand.Domain.Customers; using Grand.Domain.Directory; using Grand.Domain.Orders; @@ -12,7 +13,8 @@ namespace Grand.Business.Checkout.Validators; public record ShoppingCartTotalAmountValidatorRecord( Customer Customer, Currency Currency, - IList Cart); + IList Cart, + CalculationContext Context); public class ShoppingCartTotalAmountValidator : AbstractValidator { @@ -38,7 +40,7 @@ public ShoppingCartTotalAmountValidator( !(orderSettings.MinOrderTotalAmount > 0))) return; var shoppingCartTotalBase = - (await orderTotalCalculationService.GetShoppingCartTotal(value.Cart)).shoppingCartTotal; + (await orderTotalCalculationService.GetShoppingCartTotal(value.Cart, value.Context)).shoppingCartTotal; if (shoppingCartTotalBase.HasValue && (shoppingCartTotalBase.Value < minOrderAmount || shoppingCartTotalBase.Value > maxOrderAmount)) @@ -48,7 +50,7 @@ public ShoppingCartTotalAmountValidator( //subtotal var (_, _, subTotalWithoutDiscount, _, _) = - await orderTotalCalculationService.GetShoppingCartSubTotal(value.Cart, false); + await orderTotalCalculationService.GetShoppingCartSubTotal(value.Cart, value.Context, false); if (subTotalWithoutDiscount < orderSettings.MinOrderSubtotalAmount) { var minOrderSubtotalAmount = diff --git a/src/Business/Grand.Business.Core/Commands/Checkout/Orders/ValidateMinShoppingCartSubtotalAmountCommand.cs b/src/Business/Grand.Business.Core/Commands/Checkout/Orders/ValidateMinShoppingCartSubtotalAmountCommand.cs index 0a17cbd40e..7709bb371d 100644 --- a/src/Business/Grand.Business.Core/Commands/Checkout/Orders/ValidateMinShoppingCartSubtotalAmountCommand.cs +++ b/src/Business/Grand.Business.Core/Commands/Checkout/Orders/ValidateMinShoppingCartSubtotalAmountCommand.cs @@ -1,4 +1,5 @@ -using Grand.Domain.Customers; +using Grand.Business.Core.Utilities.Checkout; +using Grand.Domain.Customers; using Grand.Domain.Orders; using Grand.Mediator; @@ -8,4 +9,5 @@ public class ValidateMinShoppingCartSubtotalAmountCommand : IRequest { public Customer Customer { get; set; } public IList Cart { get; set; } + public CalculationContext Context { get; set; } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IOrderCalculationService.cs b/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IOrderCalculationService.cs index ffaa3d0a59..2de6b1df7f 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IOrderCalculationService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IOrderCalculationService.cs @@ -1,5 +1,6 @@ using Grand.Business.Core.Utilities.Catalog; using Grand.Business.Core.Utilities.Checkout; +using Grand.Domain.Directory; using Grand.Domain.Orders; namespace Grand.Business.Core.Interfaces.Checkout.Orders; @@ -13,10 +14,11 @@ public interface IOrderCalculationService /// Gets shopping cart subtotal /// /// Cart + /// Customer, store, currency and tax display the calculation is for /// A value indicating whether calculated price should include tax Task<(double discountAmount, List appliedDiscounts, double subTotalWithoutDiscount, double subTotalWithDiscount, SortedDictionary taxRates)> - GetShoppingCartSubTotal(IList cart, + GetShoppingCartSubTotal(IList cart, CalculationContext context, bool includingTax); /// @@ -24,58 +26,65 @@ public interface IOrderCalculationService /// /// Shipping rate to adjust /// Cart + /// Customer, store, currency and tax display the calculation is for /// Adjusted shipping rate Task<(double shippingRate, List appliedDiscounts)> AdjustShippingRate(double shippingRate, - IList cart); + IList cart, CalculationContext context); /// /// Gets shopping cart additional shipping charge /// /// Cart + /// Customer, store, currency and tax display the calculation is for /// Additional shipping charge - Task GetShoppingCartAdditionalShippingCharge(IList cart); + Task GetShoppingCartAdditionalShippingCharge(IList cart, CalculationContext context); /// /// Gets a value indicating whether shipping is free /// /// Cart + /// Customer, store, currency and tax display the calculation is for /// A value indicating whether shipping is free - Task IsFreeShipping(IList cart); + Task IsFreeShipping(IList cart, CalculationContext context); /// /// Gets shopping cart shipping total /// /// Cart + /// Customer, store, currency and tax display the calculation is for /// Shipping total Task<(double? shoppingCartShippingTotal, double taxRate, List appliedDiscounts)> - GetShoppingCartShippingTotal(IList cart); + GetShoppingCartShippingTotal(IList cart, CalculationContext context); /// /// Gets shopping cart shipping total /// /// Cart + /// Customer, store, currency and tax display the calculation is for /// A value indicating whether calculated price should include tax /// Shipping total Task<(double? shoppingCartShippingTotal, double taxRate, List appliedDiscounts)> - GetShoppingCartShippingTotal(IList cart, bool includingTax); + GetShoppingCartShippingTotal(IList cart, CalculationContext context, bool includingTax); /// /// Gets tax /// /// Shopping cart + /// Customer, store, currency and tax display the calculation is for /// /// A value indicating whether we should use payment method additional fee when /// calculating tax /// /// Tax total Task<(double taxtotal, SortedDictionary taxRates)> GetTaxTotal(IList cart, - bool usePaymentMethodAdditionalFee = true); + CalculationContext context, bool usePaymentMethodAdditionalFee = true); /// /// Gets shopping cart total /// /// Cart + /// Customer, store, currency and tax display the calculation is for /// /// A value indicating loyalty points should be used; null to detect current choice of the /// customer @@ -88,7 +97,7 @@ public interface IOrderCalculationService Task<(double? shoppingCartTotal, double discountAmount, List appliedDiscounts, List appliedGiftVouchers, int redeemedLoyaltyPoints, double redeemedLoyaltyPointsAmount)> - GetShoppingCartTotal(IList cart, bool? useLoyaltyPoints = null, + GetShoppingCartTotal(IList cart, CalculationContext context, bool? useLoyaltyPoints = null, bool usePaymentMethodAdditionalFee = true); /// @@ -96,7 +105,7 @@ public interface IOrderCalculationService /// /// Loyalty points /// Converted value - Task ConvertLoyaltyPointsToAmount(int loyaltyPoints); + Task ConvertLoyaltyPointsToAmount(int loyaltyPoints, Currency currency); /// /// Converts an amount to loyalty points diff --git a/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartSubTotalService.cs b/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartSubTotalService.cs index 35cd4534b7..ddcda21540 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartSubTotalService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartSubTotalService.cs @@ -1,4 +1,5 @@ using Grand.Business.Core.Utilities.Catalog; +using Grand.Business.Core.Utilities.Checkout; using Grand.Domain.Orders; namespace Grand.Business.Core.Interfaces.Checkout.Orders; @@ -17,5 +18,5 @@ public interface IShoppingCartSubTotalService /// A value indicating whether calculated price should include tax Task<(double discountAmount, List appliedDiscounts, double subTotalWithoutDiscount, double subTotalWithDiscount, SortedDictionary taxRates)> - GetShoppingCartSubTotal(IList cart, bool includingTax); + GetShoppingCartSubTotal(IList cart, CalculationContext context, bool includingTax); } diff --git a/src/Plugins/Payments.BrainTree/BrainTreePaymentProvider.cs b/src/Plugins/Payments.BrainTree/BrainTreePaymentProvider.cs index fc6eeddbe7..d744eaa64e 100644 --- a/src/Plugins/Payments.BrainTree/BrainTreePaymentProvider.cs +++ b/src/Plugins/Payments.BrainTree/BrainTreePaymentProvider.cs @@ -235,7 +235,9 @@ public async Task GetAdditionalHandlingFee(IList cart) if (_brainTreePaymentSettings.AdditionalFeePercentage) { //percentage of the subtotal, which is already in the working currency - var subtotal = await _shoppingCartSubTotalService.GetShoppingCartSubTotal(cart, true); + var subtotal = await _shoppingCartSubTotalService.GetShoppingCartSubTotal(cart, + new CalculationContext(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + _contextAccessor.WorkContext.WorkingCurrency, _contextAccessor.WorkContext.TaxDisplayType), true); return (float)subtotal.subTotalWithDiscount * (float)_brainTreePaymentSettings.AdditionalFee / 100f; } diff --git a/src/Plugins/Payments.BrainTree/Controllers/PaymentBrainTreeController.cs b/src/Plugins/Payments.BrainTree/Controllers/PaymentBrainTreeController.cs index 83a9291c59..d55e33e5fd 100644 --- a/src/Plugins/Payments.BrainTree/Controllers/PaymentBrainTreeController.cs +++ b/src/Plugins/Payments.BrainTree/Controllers/PaymentBrainTreeController.cs @@ -1,6 +1,7 @@ using Braintree; using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Utilities.Checkout; using Grand.Domain.Orders; using Grand.Infrastructure; using Grand.Web.Common.Controllers; @@ -60,7 +61,9 @@ public async Task PaymentInfo() }; ViewBag.ClientToken = await gateway.ClientToken.GenerateAsync(); - ViewBag.OrderTotal = (await _orderTotalCalculationService.GetShoppingCartTotal(cart)).shoppingCartTotal; + ViewBag.OrderTotal = (await _orderTotalCalculationService.GetShoppingCartTotal(cart, + new CalculationContext(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + _contextAccessor.WorkContext.WorkingCurrency, _contextAccessor.WorkContext.TaxDisplayType))).shoppingCartTotal; return View("PaymentInfo_3DS", model); } diff --git a/src/Plugins/Payments.CashOnDelivery/CashOnDeliveryPaymentProvider.cs b/src/Plugins/Payments.CashOnDelivery/CashOnDeliveryPaymentProvider.cs index 7e8cde3bbe..69e432e8df 100644 --- a/src/Plugins/Payments.CashOnDelivery/CashOnDeliveryPaymentProvider.cs +++ b/src/Plugins/Payments.CashOnDelivery/CashOnDeliveryPaymentProvider.cs @@ -96,7 +96,9 @@ public async Task GetAdditionalHandlingFee(IList cart) if (_cashOnDeliveryPaymentSettings.AdditionalFeePercentage) { //percentage of the subtotal, which is already in the working currency - var subtotal = await _shoppingCartSubTotalService.GetShoppingCartSubTotal(cart, true); + var subtotal = await _shoppingCartSubTotalService.GetShoppingCartSubTotal(cart, + new CalculationContext(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + _contextAccessor.WorkContext.WorkingCurrency, _contextAccessor.WorkContext.TaxDisplayType), true); return (float)subtotal.subTotalWithDiscount * (float)_cashOnDeliveryPaymentSettings.AdditionalFee / 100f; } diff --git a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/ValidateMinShoppingCartSubtotalAmountCommandHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/ValidateMinShoppingCartSubtotalAmountCommandHandlerTests.cs index af4fcde676..843c54ac64 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/ValidateMinShoppingCartSubtotalAmountCommandHandlerTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/ValidateMinShoppingCartSubtotalAmountCommandHandlerTests.cs @@ -31,11 +31,14 @@ public async Task HandleTest() { var command = new ValidateMinShoppingCartSubtotalAmountCommand { Customer = new Customer(), - Cart = new List { new() } + Cart = new List { new() }, + Context = new Grand.Business.Core.Utilities.Checkout.CalculationContext(new Customer(), + new Grand.Domain.Stores.Store(), new Grand.Domain.Directory.Currency(), + Grand.Domain.Tax.TaxDisplayType.ExcludingTax) }; _orderTotalCalculationServiceMock - .Setup(x => x.GetShoppingCartSubTotal(It.IsAny>(), false)).Returns(() => + .Setup(x => x.GetShoppingCartSubTotal(It.IsAny>(), It.IsAny(), false)).Returns(() => Task.FromResult<(double discountAmount, List appliedDiscounts, double subTotalWithoutDiscount, double subTotalWithDiscount, SortedDictionary taxRates)>(( 20, null, 100, 100, null))); diff --git a/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/OrderCalculationServiceTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/OrderCalculationServiceTests.cs index 243772127a..4d706fe170 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/OrderCalculationServiceTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/OrderCalculationServiceTests.cs @@ -9,6 +9,7 @@ using Grand.Business.Core.Interfaces.Checkout.Payments; using Grand.Business.Core.Interfaces.Checkout.Shipping; using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Utilities.Checkout; using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Customers; @@ -46,12 +47,11 @@ public class OrderCalculationServiceTests private Mock _taxServiceMock; private TaxSettings _taxSettings; - private Mock _workContextMock; + private CalculationContext _context; [TestInitialize] public void Init() { - _workContextMock = new Mock(); _pricingServiceMock = new Mock(); _taxServiceMock = new Mock(); _shippingServiceMock = new Mock(); @@ -69,19 +69,17 @@ public void Init() _shoppingCartSettings = new ShoppingCartSettings(); _catalogSettings = new CatalogSettings(); _discountValidationService = new Mock(); - _workContextMock.Setup(c => c.StoreContext.CurrentStore).Returns(() => new Store { Id = "", Name = "test store" }); var customer = new Customer(); customer.Groups.Add("1"); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => customer); - _workContextMock.Setup(c => c.WorkContext.WorkingCurrency).Returns(() => new Currency()); - _workContextMock.Setup(c => c.WorkContext.TaxDisplayType).Returns(() => TaxDisplayType.ExcludingTax); + _context = new CalculationContext(customer, new Store { Id = "", Name = "test store" }, new Currency(), + TaxDisplayType.ExcludingTax); - var shoppingCartSubTotalService = new ShoppingCartSubTotalService(_workContextMock.Object, + var shoppingCartSubTotalService = new ShoppingCartSubTotalService( _pricingServiceMock.Object, _taxServiceMock.Object, _checkoutAttributeParserMock.Object, _discountServiceMock.Object, _discountValidationService.Object, _productServiceMock.Object, _currencyServiceMock.Object, _shoppingCartSettings, _catalogSettings); - _service = new OrderCalculationService(_workContextMock.Object, shoppingCartSubTotalService, + _service = new OrderCalculationService(shoppingCartSubTotalService, _taxServiceMock.Object, _shippingServiceMock.Object, _paymentServiceMock.Object, _discountServiceMock.Object, _giftVoucherServiceMock.Object, @@ -90,6 +88,43 @@ public void Init() _taxSettings, _loyaltyPointsSettings, _shippingSettings, _shoppingCartSettings, _catalogSettings); } + [TestMethod] + public async Task IsFreeShipping_UsesTheCustomerOfTheContext() + { + //Arrange + var customer = new Customer { FreeShipping = true }; + var context = new CalculationContext(customer, new Store { Id = "s1" }, new Currency(), + TaxDisplayType.ExcludingTax); + + //Act + var free = await _service.IsFreeShipping(new List { new() { IsShipEnabled = true } }, + context); + + //Assert + Assert.IsTrue(free); + } + + [TestMethod] + public async Task GetShoppingCartSubTotal_PricesItemsForTheContextCustomerStoreAndCurrency() + { + //Arrange + var product = new Product { Id = "1", Published = true, Price = 10 }; + _productServiceMock.Setup(a => a.GetProductById(It.IsAny(), false)).ReturnsAsync(product); + _checkoutAttributeParserMock.Setup(a => a.ParseCheckoutAttributeValue(It.IsAny>())) + .ReturnsAsync(new List<(CheckoutAttribute ca, CheckoutAttributeValue cav)>()); + var customer = new Customer(); + var store = new Store { Id = "s1" }; + var currency = new Currency { CurrencyCode = "EUR" }; + var context = new CalculationContext(customer, store, currency, TaxDisplayType.ExcludingTax); + + //Act + await _service.GetShoppingCartSubTotal(new List { new() { Quantity = 1 } }, context, false); + + //Assert + _pricingServiceMock.Verify(p => p.GetSubTotal(It.IsAny(), product, customer, store, + currency, true), Times.Once); + } + [TestMethod] public async Task GetShoppingCartSubTotalTest() { @@ -106,7 +141,7 @@ public async Task GetShoppingCartSubTotalTest() .Returns(() => Task.FromResult((IList)new List())); var shoppingCartItem = new List { new() { Quantity = 1 } }; - var result = await _service.GetShoppingCartSubTotal(shoppingCartItem, false); + var result = await _service.GetShoppingCartSubTotal(shoppingCartItem, _context, false); Assert.IsTrue(result.subTotalWithDiscount >= 0); } @@ -122,7 +157,7 @@ public async Task GetShoppingCartAdditionalShippingChargeTest() var shoppingCartItem = new List { new() { Quantity = 1, IsShipEnabled = true, AdditionalShippingChargeProduct = 10 } }; //Act - var result = await _service.GetShoppingCartAdditionalShippingCharge(shoppingCartItem); + var result = await _service.GetShoppingCartAdditionalShippingCharge(shoppingCartItem, _context); //Assert Assert.AreEqual(10, result); } @@ -139,7 +174,7 @@ public async Task IsFreeShippingTest() new() { Quantity = 1, IsShipEnabled = true, AdditionalShippingChargeProduct = 10, IsFreeShipping = true } }; //Act - var result = await _service.GetShoppingCartAdditionalShippingCharge(shoppingCartItem); + var result = await _service.GetShoppingCartAdditionalShippingCharge(shoppingCartItem, _context); //Assert Assert.AreEqual(0, result); } @@ -155,7 +190,7 @@ public async Task AdjustShippingRateTest() var shoppingCartItem = new List { new() { Quantity = 1, IsShipEnabled = true, AdditionalShippingChargeProduct = 10 } }; //Act - var result = await _service.AdjustShippingRate(10, shoppingCartItem); + var result = await _service.AdjustShippingRate(10, shoppingCartItem, _context); //Assert Assert.AreEqual(20, result.shippingRate); @@ -184,7 +219,7 @@ public async Task GetShoppingCartShippingTotalTest() new() { Quantity = 1, IsShipEnabled = true, AdditionalShippingChargeProduct = 10, IsFreeShipping = true } }; //Act - var result = await _service.GetShoppingCartShippingTotal(shoppingCartItem); + var result = await _service.GetShoppingCartShippingTotal(shoppingCartItem, _context); //Assert Assert.AreEqual(0, result.shoppingCartShippingTotal); @@ -214,7 +249,7 @@ public async Task GetTaxTotalTest() new() { Quantity = 1, IsShipEnabled = true, AdditionalShippingChargeProduct = 10, IsFreeShipping = true } }; //Act - var result = await _service.GetTaxTotal(shoppingCartItem, false); + var result = await _service.GetTaxTotal(shoppingCartItem, _context, false); //Assert Assert.AreEqual(0, result.taxtotal); } @@ -243,7 +278,7 @@ public async Task GetShoppingCartTotalTest() new() { Quantity = 1, IsShipEnabled = true, AdditionalShippingChargeProduct = 10, IsFreeShipping = true } }; //Act - var result = await _service.GetShoppingCartTotal(shoppingCartItem, false, false); + var result = await _service.GetShoppingCartTotal(shoppingCartItem, _context, false, false); //Assert Assert.AreEqual(0, result.shoppingCartTotal); } @@ -254,7 +289,7 @@ public async Task ConvertLoyaltyPointsToAmountTest() //Arrange _loyaltyPointsSettings.ExchangeRate = 1; //Act - var result = await _service.ConvertLoyaltyPointsToAmount(10); + var result = await _service.ConvertLoyaltyPointsToAmount(10, _context.Currency); //Assert Assert.AreEqual(10, result); } diff --git a/src/Tests/Grand.Modules.Tests/Plugins/Payments/BrainTreePaymentProviderTests.cs b/src/Tests/Grand.Modules.Tests/Plugins/Payments/BrainTreePaymentProviderTests.cs index f3ea2c7ba2..be98bd1fdc 100644 --- a/src/Tests/Grand.Modules.Tests/Plugins/Payments/BrainTreePaymentProviderTests.cs +++ b/src/Tests/Grand.Modules.Tests/Plugins/Payments/BrainTreePaymentProviderTests.cs @@ -32,6 +32,7 @@ public void Init() _currencyService = new Mock(); var contextAccessor = new Mock(); contextAccessor.Setup(c => c.WorkContext.WorkingCurrency).Returns(new Currency()); + contextAccessor.Setup(c => c.StoreContext.CurrentStore).Returns(new Grand.Domain.Stores.Store()); var services = new ServiceCollection(); services.AddSingleton(_settings); @@ -61,7 +62,7 @@ public async Task GetAdditionalHandlingFee_Percentage_WorksWithoutHttpContext() _settings.AdditionalFee = 2; _settings.AdditionalFeePercentage = true; var cart = new List { new() }; - _subTotalService.Setup(s => s.GetShoppingCartSubTotal(cart, true)) + _subTotalService.Setup(s => s.GetShoppingCartSubTotal(cart, It.IsAny(), true)) .ReturnsAsync((0d, new List(), 500d, 500d, new SortedDictionary())); var provider = _scope.ServiceProvider.GetRequiredService(); diff --git a/src/Tests/Grand.Modules.Tests/Plugins/Payments/CashOnDeliveryPaymentProviderTests.cs b/src/Tests/Grand.Modules.Tests/Plugins/Payments/CashOnDeliveryPaymentProviderTests.cs index 7d2b57ca78..8edd9bca68 100644 --- a/src/Tests/Grand.Modules.Tests/Plugins/Payments/CashOnDeliveryPaymentProviderTests.cs +++ b/src/Tests/Grand.Modules.Tests/Plugins/Payments/CashOnDeliveryPaymentProviderTests.cs @@ -35,6 +35,7 @@ public void Init() _paymentTransactionService = new Mock(); var contextAccessor = new Mock(); contextAccessor.Setup(c => c.WorkContext.WorkingCurrency).Returns(_workingCurrency); + contextAccessor.Setup(c => c.StoreContext.CurrentStore).Returns(new Grand.Domain.Stores.Store()); //no IHttpContextAccessor: the provider has to work outside a request var services = new ServiceCollection(); @@ -71,7 +72,7 @@ public async Task GetAdditionalHandlingFee_Percentage_IsShareOfSubtotalWithoutSe _settings.AdditionalFee = 10; _settings.AdditionalFeePercentage = true; var cart = new List { new() }; - _subTotalService.Setup(s => s.GetShoppingCartSubTotal(cart, true)) + _subTotalService.Setup(s => s.GetShoppingCartSubTotal(cart, It.IsAny(), true)) .ReturnsAsync((0d, new List(), 200d, 180d, new SortedDictionary())); var fee = await Provider.GetAdditionalHandlingFee(cart); @@ -91,7 +92,8 @@ public async Task GetAdditionalHandlingFee_FixedValue_IsConvertedToWorkingCurren var fee = await Provider.GetAdditionalHandlingFee(new List()); Assert.AreEqual(20, fee); - _subTotalService.Verify(s => s.GetShoppingCartSubTotal(It.IsAny>(), It.IsAny()), + _subTotalService.Verify(s => s.GetShoppingCartSubTotal(It.IsAny>(), It.IsAny(), + It.IsAny()), Times.Never); } diff --git a/src/Web/Grand.Web/Features/Handlers/Checkout/GetIsPaymentWorkflowRequiredHandler.cs b/src/Web/Grand.Web/Features/Handlers/Checkout/GetIsPaymentWorkflowRequiredHandler.cs index 45a14863a0..ece3c146ef 100644 --- a/src/Web/Grand.Web/Features/Handlers/Checkout/GetIsPaymentWorkflowRequiredHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Checkout/GetIsPaymentWorkflowRequiredHandler.cs @@ -1,5 +1,7 @@ using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Domain.Payments; +using Grand.Infrastructure; +using Grand.Web.Common.Extensions; using Grand.Web.Features.Models.Checkout; using Grand.Mediator; @@ -7,12 +9,15 @@ namespace Grand.Web.Features.Handlers.Checkout; public class GetIsPaymentWorkflowRequiredHandler : IRequestHandler { + private readonly IContextAccessor _contextAccessor; private readonly IOrderCalculationService _orderTotalCalculationService; private readonly PaymentSettings _paymentSettings; public GetIsPaymentWorkflowRequiredHandler(IOrderCalculationService orderTotalCalculationService, - PaymentSettings paymentSettings) + PaymentSettings paymentSettings, + IContextAccessor contextAccessor) { + _contextAccessor = contextAccessor; _orderTotalCalculationService = orderTotalCalculationService; _paymentSettings = paymentSettings; } @@ -22,7 +27,7 @@ public async Task Handle(GetIsPaymentWorkflowRequired request, Cancellatio var result = true; //check whether order total equals zero var shoppingCartTotalBase = - (await _orderTotalCalculationService.GetShoppingCartTotal(request.Cart, request.UseLoyaltyPoints)) + (await _orderTotalCalculationService.GetShoppingCartTotal(request.Cart, _contextAccessor.CalculationContext(), request.UseLoyaltyPoints)) .shoppingCartTotal; if (shoppingCartTotalBase is 0 && !_paymentSettings.ShowPaymentIfCartIsZero) result = false; diff --git a/src/Web/Grand.Web/Features/Handlers/Checkout/GetPaymentMethodHandler.cs b/src/Web/Grand.Web/Features/Handlers/Checkout/GetPaymentMethodHandler.cs index 86fb5df89f..6197e15994 100644 --- a/src/Web/Grand.Web/Features/Handlers/Checkout/GetPaymentMethodHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Checkout/GetPaymentMethodHandler.cs @@ -10,6 +10,7 @@ using Grand.Domain.Payments; using Grand.Domain.Tax; using Grand.Infrastructure; +using Grand.Web.Common.Extensions; using Grand.Web.Features.Models.Checkout; using Grand.Web.Models.Checkout; using Grand.Mediator; @@ -60,7 +61,8 @@ public async Task Handle(GetPaymentMethod request, C var loyaltyPointsBalance = await _loyaltyPointsService.GetLoyaltyPointsBalance(request.Customer.Id, request.Store.Id); var loyaltyPointsAmount = await _currencyService.ConvertFromPrimaryStoreCurrency( - await _orderTotalCalculationService.ConvertLoyaltyPointsToAmount(loyaltyPointsBalance), + await _orderTotalCalculationService.ConvertLoyaltyPointsToAmount(loyaltyPointsBalance, + _contextAccessor.WorkContext.WorkingCurrency), request.Currency); if (loyaltyPointsAmount > 0 && _orderTotalCalculationService.CheckMinimumLoyaltyPointsToUseRequirement(loyaltyPointsBalance)) @@ -69,7 +71,7 @@ await _orderTotalCalculationService.ConvertLoyaltyPointsToAmount(loyaltyPointsBa model.LoyaltyPointsAmount = _priceFormatter.FormatPrice(loyaltyPointsAmount, request.Currency); model.LoyaltyPointsBalance = loyaltyPointsBalance; var shoppingCartTotalBase = - (await _orderTotalCalculationService.GetShoppingCartTotal(request.Cart, true)).shoppingCartTotal; + (await _orderTotalCalculationService.GetShoppingCartTotal(request.Cart, _contextAccessor.CalculationContext(), true)).shoppingCartTotal; model.LoyaltyPointsEnoughToPayForOrder = shoppingCartTotalBase is 0; } } diff --git a/src/Web/Grand.Web/Features/Handlers/Checkout/GetShippingMethodHandler.cs b/src/Web/Grand.Web/Features/Handlers/Checkout/GetShippingMethodHandler.cs index f4242a22db..83d2b0e79b 100644 --- a/src/Web/Grand.Web/Features/Handlers/Checkout/GetShippingMethodHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Checkout/GetShippingMethodHandler.cs @@ -8,6 +8,7 @@ using Grand.Domain.Shipping; using Grand.Domain.Tax; using Grand.Infrastructure; +using Grand.Web.Common.Extensions; using Grand.Web.Features.Models.Checkout; using Grand.Web.Models.Checkout; using Grand.Mediator; @@ -67,7 +68,7 @@ await _customerService.UpdateUserField(request.Customer, //adjust rate var shippingTotal = (await _orderTotalCalculationService.AdjustShippingRate( - shippingOption.Rate, request.Cart)).shippingRate; + shippingOption.Rate, request.Cart, _contextAccessor.CalculationContext())).shippingRate; var rateBase = (await _taxService.GetShippingPrice(shippingTotal, _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, request.Customer, request.Store)).shippingPrice; diff --git a/src/Web/Grand.Web/Features/Handlers/Orders/GetCustomerLoyaltyPointsHandler.cs b/src/Web/Grand.Web/Features/Handlers/Orders/GetCustomerLoyaltyPointsHandler.cs index d560672d40..1b8beb252b 100644 --- a/src/Web/Grand.Web/Features/Handlers/Orders/GetCustomerLoyaltyPointsHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Orders/GetCustomerLoyaltyPointsHandler.cs @@ -2,6 +2,8 @@ using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Domain.Orders; +using Grand.Infrastructure; +using Grand.Web.Common.Extensions; using Grand.Web.Features.Models.Orders; using Grand.Web.Models.Orders; using Grand.Mediator; @@ -10,6 +12,7 @@ namespace Grand.Web.Features.Handlers.Orders; public class GetCustomerLoyaltyPointsHandler : IRequestHandler { + private readonly IContextAccessor _contextAccessor; private readonly ICurrencyService _currencyService; private readonly IDateTimeService _dateTimeService; private readonly ILoyaltyPointsService _loyaltyPointsService; @@ -20,8 +23,10 @@ public class GetCustomerLoyaltyPointsHandler : IRequestHandler Handle(GetCustomerLoyaltyPoints re var loyaltyPointsBalance = await _loyaltyPointsService.GetLoyaltyPointsBalance(request.Customer.Id, request.Store.Id); var loyaltyPointsAmountBase = - await _orderTotalCalculationService.ConvertLoyaltyPointsToAmount(loyaltyPointsBalance); + await _orderTotalCalculationService.ConvertLoyaltyPointsToAmount(loyaltyPointsBalance, + _contextAccessor.WorkContext.WorkingCurrency); var loyaltyPointsAmount = await _currencyService.ConvertFromPrimaryStoreCurrency(loyaltyPointsAmountBase, request.Currency); model.LoyaltyPointsBalance = loyaltyPointsBalance; @@ -53,7 +59,8 @@ public async Task Handle(GetCustomerLoyaltyPoints re //minimum amount/balance var minimumLoyaltyPointsBalance = _loyaltyPointsSettings.MinimumLoyaltyPointsToUse; var minimumLoyaltyPointsAmountBase = - await _orderTotalCalculationService.ConvertLoyaltyPointsToAmount(minimumLoyaltyPointsBalance); + await _orderTotalCalculationService.ConvertLoyaltyPointsToAmount(minimumLoyaltyPointsBalance, + _contextAccessor.WorkContext.WorkingCurrency); var minimumLoyaltyPointsAmount = await _currencyService.ConvertFromPrimaryStoreCurrency(minimumLoyaltyPointsAmountBase, request.Currency); model.MinimumLoyaltyPointsBalance = minimumLoyaltyPointsBalance; diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs index 0fd54dbffb..936987903e 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs @@ -9,6 +9,8 @@ using Grand.Domain.Media; using Grand.Domain.Orders; using Grand.Domain.Tax; +using Grand.Infrastructure; +using Grand.Web.Common.Extensions; using Grand.Web.Extensions; using Grand.Web.Features.Models.ShoppingCart; using Grand.Web.Models.Media; @@ -19,6 +21,7 @@ namespace Grand.Web.Features.Handlers.ShoppingCart; public class GetAddToCartHandler : IRequestHandler { + private readonly IContextAccessor _contextAccessor; private readonly IAuctionService _auctionService; private readonly MediaSettings _mediaSettings; private readonly IOrderCalculationService _orderTotalCalculationService; @@ -46,8 +49,10 @@ public GetAddToCartHandler( IAuctionService auctionService, ShoppingCartSettings shoppingCartSettings, TaxSettings taxSettings, - MediaSettings mediaSettings) + MediaSettings mediaSettings, + IContextAccessor contextAccessor) { + _contextAccessor = contextAccessor; _productAttributeFormatter = productAttributeFormatter; _translationService = translationService; _taxService = taxService; @@ -156,7 +161,7 @@ await _productAttributeFormatter.FormatAttributes(request.Product, request.Attri var subTotalIncludingTax = request.TaxDisplayType == TaxDisplayType.IncludingTax && !_taxSettings.ForceTaxExclusionFromOrderSubtotal; var shoppingCartSubTotal = - await _orderTotalCalculationService.GetShoppingCartSubTotal(cart, subTotalIncludingTax); + await _orderTotalCalculationService.GetShoppingCartSubTotal(cart, _contextAccessor.CalculationContext(), subTotalIncludingTax); model.SubTotalIncludingTax = subTotalIncludingTax; model.SubTotal = _priceFormatter.FormatPrice(shoppingCartSubTotal.subTotalWithoutDiscount, request.Currency); diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetEstimateShippingResultHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetEstimateShippingResultHandler.cs index 436463334e..142d90cc20 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetEstimateShippingResultHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetEstimateShippingResultHandler.cs @@ -8,6 +8,7 @@ using Grand.Domain.Shipping; using Grand.Domain.Tax; using Grand.Infrastructure; +using Grand.Web.Common.Extensions; using Grand.Web.Features.Models.ShoppingCart; using Grand.Web.Models.ShoppingCart; using Grand.Mediator; @@ -78,7 +79,7 @@ public async Task Handle(GetEstimateShippingResult //calculate discounted and taxed rate var total = await _orderTotalCalculationService.AdjustShippingRate(shippingOption.Rate, - request.Cart); + request.Cart, _contextAccessor.CalculationContext()); var shippingTotal = total.shippingRate; var rate = (await _taxService.GetShippingPrice(shippingTotal, diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs index ae62b5b19f..8e9241c196 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs @@ -14,6 +14,8 @@ using Grand.Domain.Orders; using Grand.Domain.Tax; using Grand.Infrastructure.Configuration; +using Grand.Infrastructure; +using Grand.Web.Common.Extensions; using Grand.Web.Extensions; using Grand.Web.Features.Models.ShoppingCart; using Grand.Web.Models.Media; @@ -24,6 +26,7 @@ namespace Grand.Web.Features.Handlers.ShoppingCart; public class GetMiniShoppingCartHandler : IRequestHandler { + private readonly IContextAccessor _contextAccessor; private readonly ICheckoutAttributeService _checkoutAttributeService; private readonly IGroupService _groupService; private readonly LinkGenerator _linkGenerator; @@ -61,8 +64,10 @@ public GetMiniShoppingCartHandler( OrderSettings orderSettings, OidcLoginConfig oidcLoginConfig, TaxSettings taxSettings, - MediaSettings mediaSettings) + MediaSettings mediaSettings, + IContextAccessor contextAccessor) { + _contextAccessor = contextAccessor; _shoppingCartService = shoppingCartService; _orderTotalCalculationService = orderTotalCalculationService; _priceFormatter = priceFormatter; @@ -110,7 +115,7 @@ public async Task Handle(GetMiniShoppingCart request, var subTotalIncludingTax = request.TaxDisplayType == TaxDisplayType.IncludingTax && !_taxSettings.ForceTaxExclusionFromOrderSubtotal; var shoppingCartSubTotal = - await _orderTotalCalculationService.GetShoppingCartSubTotal(cart, subTotalIncludingTax); + await _orderTotalCalculationService.GetShoppingCartSubTotal(cart, _contextAccessor.CalculationContext(), subTotalIncludingTax); model.SubTotalIncludingTax = subTotalIncludingTax; model.SubTotal = _priceFormatter.FormatPrice(shoppingCartSubTotal.subTotalWithoutDiscount, request.Currency); @@ -121,6 +126,7 @@ public async Task Handle(GetMiniShoppingCart request, .Any(); var minOrderSubtotalAmountOk = await _mediator.Send(new ValidateMinShoppingCartSubtotalAmountCommand { + Context = _contextAccessor.CalculationContext(), Customer = request.Customer, Cart = cart.Where (x => x.ShoppingCartTypeId is ShoppingCartType.ShoppingCart or ShoppingCartType.Auctions).ToList() diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetOrderTotalsHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetOrderTotalsHandler.cs index b8ccf8687a..5553de3dc6 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetOrderTotalsHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetOrderTotalsHandler.cs @@ -10,6 +10,8 @@ using Grand.Domain.Orders; using Grand.Domain.Shipping; using Grand.Domain.Tax; +using Grand.Infrastructure; +using Grand.Web.Common.Extensions; using Grand.Web.Features.Models.ShoppingCart; using Grand.Web.Models.ShoppingCart; using Grand.Mediator; @@ -18,6 +20,7 @@ namespace Grand.Web.Features.Handlers.ShoppingCart; public class GetOrderTotalsHandler : IRequestHandler { + private readonly IContextAccessor _contextAccessor; private readonly ICurrencyService _currencyService; private readonly LoyaltyPointsSettings _loyaltyPointsSettings; private readonly IMediator _mediator; @@ -36,8 +39,10 @@ public GetOrderTotalsHandler( ITaxService taxService, IMediator mediator, TaxSettings taxSettings, - LoyaltyPointsSettings loyaltyPointsSettings) + LoyaltyPointsSettings loyaltyPointsSettings, + IContextAccessor contextAccessor) { + _contextAccessor = contextAccessor; _orderTotalCalculationService = orderTotalCalculationService; _currencyService = currencyService; _priceFormatter = priceFormatter; @@ -78,7 +83,7 @@ private async Task PrepareSubtotal(OrderTotalsModel model, GetOrderTotals reques var subTotalIncludingTax = request.TaxDisplayType == TaxDisplayType.IncludingTax && !_taxSettings.ForceTaxExclusionFromOrderSubtotal; var shoppingCartSubTotal = - await _orderTotalCalculationService.GetShoppingCartSubTotal(request.Cart, subTotalIncludingTax); + await _orderTotalCalculationService.GetShoppingCartSubTotal(request.Cart, _contextAccessor.CalculationContext(), subTotalIncludingTax); model.SubTotalIncludingTax = subTotalIncludingTax; model.SubTotal = _priceFormatter.FormatPrice(shoppingCartSubTotal.subTotalWithoutDiscount, request.Currency); model.SubTotalValue = shoppingCartSubTotal.subTotalWithoutDiscount; @@ -97,7 +102,7 @@ private async Task PrepareShippingInfo(OrderTotalsModel model, GetOrderTotals re if (model.RequiresShipping) { var shoppingCartShippingBase = - (await _orderTotalCalculationService.GetShoppingCartShippingTotal(request.Cart)) + (await _orderTotalCalculationService.GetShoppingCartShippingTotal(request.Cart, _contextAccessor.CalculationContext())) .shoppingCartShippingTotal; if (shoppingCartShippingBase.HasValue) { @@ -142,7 +147,7 @@ private async Task PrepareTax(OrderTotalsModel model, GetOrderTotals request) } else { - var taxtotal = await _orderTotalCalculationService.GetTaxTotal(request.Cart); + var taxtotal = await _orderTotalCalculationService.GetTaxTotal(request.Cart, _contextAccessor.CalculationContext()); var taxRates = taxtotal.taxRates; if (taxtotal.taxtotal == 0 && _taxSettings.HideZeroTax) @@ -170,7 +175,7 @@ private async Task PrepareTax(OrderTotalsModel model, GetOrderTotals request) private async Task PrepareTotal(OrderTotalsModel model, GetOrderTotals request) { - var carttotal = await _orderTotalCalculationService.GetShoppingCartTotal(request.Cart); + var carttotal = await _orderTotalCalculationService.GetShoppingCartTotal(request.Cart, _contextAccessor.CalculationContext()); var shoppingCartTotalBase = carttotal.shoppingCartTotal; var orderTotalDiscountAmountBase = carttotal.discountAmount; var appliedGiftVouchers = carttotal.appliedGiftVouchers; @@ -209,7 +214,7 @@ private async Task PrepareTotal(OrderTotalsModel model, GetOrderTotals request) shoppingCartTotalBase.HasValue) { var shippingBaseInclTax = model.RequiresShipping - ? (await _orderTotalCalculationService.GetShoppingCartShippingTotal(request.Cart, true)) + ? (await _orderTotalCalculationService.GetShoppingCartShippingTotal(request.Cart, _contextAccessor.CalculationContext(), true)) .shoppingCartShippingTotal : 0; var earnLoyaltyPoints = shoppingCartTotalBase.Value - shippingBaseInclTax ?? 0; diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs index 528fc49828..b96244455a 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs @@ -22,6 +22,7 @@ using Grand.Domain.Tax; using Grand.Infrastructure.Configuration; using Grand.Infrastructure; +using Grand.Web.Common.Extensions; using Grand.Web.Common.Localization; using Grand.Web.Extensions; using Grand.Web.Features.Models.ShoppingCart; @@ -168,6 +169,7 @@ private async Task PrepareSimpleProperties(ShoppingCartModel model, GetShoppingC else { var minOrderSubtotalAmountOk = await _mediator.Send(new ValidateMinShoppingCartSubtotalAmountCommand { + Context = _contextAccessor.CalculationContext(), Customer = request.Customer, Cart = request.Cart.Where(x => x.ShoppingCartTypeId is ShoppingCartType.ShoppingCart or ShoppingCartType.Auctions).ToList() From 3ac776bcf7b044152e69acba5951a2983db12d16 Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 15:06:42 +0200 Subject: [PATCH 16/33] Validate the cart for an explicitly given context ShoppingCartValidator no longer reads the current customer, store or currency. GetShoppingCartWarnings takes a CalculationContext; CheckCommonWarnings and GetRequiredProductWarnings use the customer and store id they already received. The required-product record carries the store id and the common-warnings record drops its unused store. --- .../Orders/PlaceOrderCommandHandler.cs | 3 +- .../Services/Orders/ShoppingCartValidator.cs | 29 +++---- .../ShoppingCartCommonWarningsValidator.cs | 2 - .../ShoppingCartRequiredProductValidator.cs | 5 +- .../Checkout/Orders/IShoppingCartValidator.cs | 4 +- .../Orders/ShoppingCartValidatorTests.cs | 87 +++++++++++++++++++ .../Validators/ShoppingCartValidatorsTests.cs | 6 +- .../Controllers/CheckoutController.cs | 7 +- .../Controllers/ShoppingCartController.cs | 3 +- .../ShoppingCart/GetShoppingCartHandler.cs | 2 +- .../ShoppingCart/GetWishlistHandler.cs | 4 +- 11 files changed, 117 insertions(+), 35 deletions(-) create mode 100644 src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartValidatorTests.cs diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs index d0e2f189f0..b597c9759d 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs @@ -455,7 +455,8 @@ await _checkoutAttributeFormatter.FormatAttributes(details.CheckoutAttributes, A //validate the entire shopping cart var warnings = - await _shoppingCartValidator.GetShoppingCartWarnings(details.Cart, details.CheckoutAttributes, true, true); + await _shoppingCartValidator.GetShoppingCartWarnings(details.Cart, details.CheckoutAttributes, AmbientCalculationContext, + true, true); if (warnings.Any()) throw new GrandException(string.Join(", ", warnings)); //validate individual cart items diff --git a/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartValidator.cs b/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartValidator.cs index 4a692b3677..2551150e8d 100644 --- a/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartValidator.cs +++ b/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartValidator.cs @@ -6,7 +6,6 @@ using Grand.Domain.Common; using Grand.Domain.Customers; using Grand.Domain.Orders; -using Grand.Infrastructure; using Grand.Infrastructure.Validators; using Grand.Mediator; @@ -17,14 +16,10 @@ public class ShoppingCartValidator : IShoppingCartValidator private readonly IMediator _mediator; private readonly IValidatorFactory _validatorFactory; - private readonly IContextAccessor _contextAccessor; - public ShoppingCartValidator( - IContextAccessor contextAccessor, IMediator mediator, IValidatorFactory validatorFactory) { - _contextAccessor = contextAccessor; _mediator = mediator; _validatorFactory = validatorFactory; } @@ -122,14 +117,16 @@ public virtual async Task> GetReservationProductWarnings(Customer } public virtual async Task> GetShoppingCartWarnings(IList shoppingCart, - IList checkoutAttributes, bool validateCheckoutAttributes, bool validateAmount) + IList checkoutAttributes, CalculationContext context, bool validateCheckoutAttributes, + bool validateAmount) { + ArgumentNullException.ThrowIfNull(context); + var warnings = new List(); checkoutAttributes ??= new List(); var result = await _validatorFactory.GetValidator() - .ValidateAsync(new ShoppingCartWarningsValidatorRecord(_contextAccessor.WorkContext.CurrentCustomer, - _contextAccessor.StoreContext.CurrentStore, shoppingCart)); + .ValidateAsync(new ShoppingCartWarningsValidatorRecord(context.Customer, context.Store, shoppingCart)); if (!result.IsValid) warnings.AddRange(result.Errors.Select(x => x.ErrorMessage)); @@ -138,8 +135,7 @@ public virtual async Task> GetShoppingCartWarnings(IList().ValidateAsync( - new ShoppingCartCheckoutAttributesValidatorRecord(_contextAccessor.WorkContext.CurrentCustomer, - _contextAccessor.StoreContext.CurrentStore, + new ShoppingCartCheckoutAttributesValidatorRecord(context.Customer, context.Store, shoppingCart, checkoutAttributes)); if (!resultCheckoutAttributes.IsValid) warnings.AddRange(resultCheckoutAttributes.Errors.Select(x => x.ErrorMessage)); @@ -150,12 +146,8 @@ public virtual async Task> GetShoppingCartWarnings(IList().ValidateAsync( - new ShoppingCartTotalAmountValidatorRecord(_contextAccessor.WorkContext.CurrentCustomer, - _contextAccessor.WorkContext.WorkingCurrency, shoppingCart, - //TODO M1 PR 3: the cart validator still reads the ambient context - new CalculationContext(_contextAccessor.WorkContext.CurrentCustomer, - _contextAccessor.StoreContext.CurrentStore, _contextAccessor.WorkContext.WorkingCurrency, - _contextAccessor.WorkContext.TaxDisplayType))); + new ShoppingCartTotalAmountValidatorRecord(context.Customer, context.Currency, shoppingCart, + context)); if (!resultCheckoutAttributes.IsValid) warnings.AddRange(resultCheckoutAttributes.Errors.Select(x => x.ErrorMessage)); } @@ -174,7 +166,7 @@ public async Task> CheckCommonWarnings(Customer customer, IList(); var result = await _validatorFactory.GetValidator().ValidateAsync( - new ShoppingCartCommonWarningsValidatorRecord(customer, _contextAccessor.StoreContext.CurrentStore, currentCart, + new ShoppingCartCommonWarningsValidatorRecord(customer, currentCart, product, shoppingCartType, rentalStartDate, rentalEndDate, quantity, reservationId)); if (!result.IsValid) warnings.AddRange(result.Errors.Select(x => x.ErrorMessage)); @@ -232,8 +224,7 @@ public virtual async Task> GetRequiredProductWarnings(Customer cus if (!product.RequireOtherProducts) return warnings; var result = await _validatorFactory.GetValidator() - .ValidateAsync(new ShoppingCartRequiredProductValidatorRecord(_contextAccessor.WorkContext.CurrentCustomer, - _contextAccessor.StoreContext.CurrentStore, product, shoppingCartItem)); + .ValidateAsync(new ShoppingCartRequiredProductValidatorRecord(customer, storeId, product, shoppingCartItem)); if (!result.IsValid) warnings.AddRange(result.Errors.Select(x => x.ErrorMessage)); return warnings; diff --git a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartCommonWarningsValidator.cs b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartCommonWarningsValidator.cs index 4f1a5b1f4e..43bd391384 100644 --- a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartCommonWarningsValidator.cs +++ b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartCommonWarningsValidator.cs @@ -5,13 +5,11 @@ using Grand.Domain.Customers; using Grand.Domain.Orders; using Grand.Domain.Permissions; -using Grand.Domain.Stores; namespace Grand.Business.Checkout.Validators; public record ShoppingCartCommonWarningsValidatorRecord( Customer Customer, - Store Store, IList ShoppingCarts, Product Product, ShoppingCartType ShoppingCartType, diff --git a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartRequiredProductValidator.cs b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartRequiredProductValidator.cs index 2010053bb3..cc455c493b 100644 --- a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartRequiredProductValidator.cs +++ b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartRequiredProductValidator.cs @@ -4,13 +4,12 @@ using Grand.Domain.Catalog; using Grand.Domain.Customers; using Grand.Domain.Orders; -using Grand.Domain.Stores; namespace Grand.Business.Checkout.Validators; public record ShoppingCartRequiredProductValidatorRecord( Customer Customer, - Store Store, + string StoreId, Product Product, ShoppingCartItem ShoppingCartItem); @@ -23,7 +22,7 @@ public ShoppingCartRequiredProductValidator(ITranslationService translationServi { var cart = value.Customer.ShoppingCartItems .Where(sci => sci.ShoppingCartTypeId == value.ShoppingCartItem.ShoppingCartTypeId) - .LimitPerStore(shoppingCartSettings.SharedCartBetweenStores, value.Store.Id) + .LimitPerStore(shoppingCartSettings.SharedCartBetweenStores, value.StoreId) .ToList(); var requiredProducts = new List(); diff --git a/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartValidator.cs b/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartValidator.cs index 8edaf3c768..c80fb60fa5 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartValidator.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartValidator.cs @@ -64,11 +64,13 @@ Task> GetShoppingCartItemWarnings(Customer customer, ShoppingCartI /// /// Shopping cart /// Checkout attributes + /// Customer, store, currency and tax display the cart is validated for /// A value indicating whether to validate checkout attributes /// A value indicating whether to validate subtotal/total attributes /// Warnings Task> GetShoppingCartWarnings(IList shoppingCart, - IList checkoutAttributes, bool validateCheckoutAttributes, bool validateAmount); + IList checkoutAttributes, CalculationContext context, bool validateCheckoutAttributes, + bool validateAmount); /// /// Validates shopping cart item for reservation products diff --git a/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartValidatorTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartValidatorTests.cs new file mode 100644 index 0000000000..670936ce4e --- /dev/null +++ b/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartValidatorTests.cs @@ -0,0 +1,87 @@ +using FluentValidation; +using FluentValidation.Results; +using Grand.Business.Checkout.Services.Orders; +using Grand.Business.Checkout.Validators; +using Grand.Business.Core.Utilities.Checkout; +using Grand.Domain.Catalog; +using Grand.Domain.Common; +using Grand.Domain.Customers; +using Grand.Domain.Directory; +using Grand.Domain.Orders; +using Grand.Domain.Stores; +using Grand.Domain.Tax; +using Grand.Infrastructure.Validators; +using Grand.Mediator; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using IValidatorFactory = Grand.Infrastructure.Validators.IValidatorFactory; + +namespace Grand.Business.Checkout.Tests.Services.Orders; + +[TestClass] +public class ShoppingCartValidatorTests +{ + private readonly List _records = []; + private ShoppingCartValidator _validator; + private Mock _validatorFactoryMock; + + [TestInitialize] + public void Init() + { + _validatorFactoryMock = new Mock(); + Capture(); + Capture(); + Capture(); + Capture(); + _validator = new ShoppingCartValidator(new Mock().Object, _validatorFactoryMock.Object); + } + + private void Capture() + { + var validator = new Mock>(); + validator.Setup(x => x.ValidateAsync(It.IsAny(), It.IsAny())) + .Callback((T record, CancellationToken _) => _records.Add(record)) + .ReturnsAsync(new ValidationResult()); + _validatorFactoryMock.Setup(x => x.GetValidator()).Returns(validator.Object); + } + + [TestMethod] + public async Task GetShoppingCartWarnings_ValidatesForTheContextCustomerAndStore() + { + //Arrange + var context = new CalculationContext(new Customer(), new Store { Id = "store" }, + new Currency { CurrencyCode = "EUR" }, TaxDisplayType.IncludingTax); + var cart = new List(); + + //Act + await _validator.GetShoppingCartWarnings(cart, new List(), context, true, true); + + //Assert + var warnings = _records.OfType().Single(); + Assert.AreSame(context.Customer, warnings.Customer); + Assert.AreSame(context.Store, warnings.Store); + var attributes = _records.OfType().Single(); + Assert.AreSame(context.Customer, attributes.Customer); + Assert.AreSame(context.Store, attributes.Store); + var amount = _records.OfType().Single(); + Assert.AreSame(context.Customer, amount.Customer); + Assert.AreSame(context.Currency, amount.Currency); + Assert.AreSame(context, amount.Context); + } + + [TestMethod] + public async Task GetRequiredProductWarnings_UsesTheGivenCustomerAndStore() + { + //Arrange + var customer = new Customer(); + var product = new Product { RequireOtherProducts = true }; + + //Act + await _validator.GetRequiredProductWarnings(customer, new ShoppingCartItem(), product, "store-b"); + + //Assert + var record = _records.OfType().Single(); + Assert.AreSame(customer, record.Customer); + Assert.AreEqual("store-b", record.StoreId); + } +} diff --git a/src/Tests/Grand.Business.Checkout.Tests/Validators/ShoppingCartValidatorsTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Validators/ShoppingCartValidatorsTests.cs index a5f476fedc..6e993b70a3 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Validators/ShoppingCartValidatorsTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Validators/ShoppingCartValidatorsTests.cs @@ -337,7 +337,7 @@ public async Task ShoppingCartRequiredProductValidator_Success() var result = await shoppingCartRequiredProductValidator.ValidateAsync( new ShoppingCartRequiredProductValidatorRecord( customer, - new Store { Id = "1" }, + "1", product, new ShoppingCartItem { ProductId = "1", Quantity = 1, ShoppingCartTypeId = ShoppingCartType.ShoppingCart, StoreId = "1" } @@ -375,7 +375,7 @@ public async Task ShoppingCartRequiredProductValidator_Fail() var result = await shoppingCartRequiredProductValidator.ValidateAsync( new ShoppingCartRequiredProductValidatorRecord( customer, - new Store { Id = "1" }, + "1", product, new ShoppingCartItem { ProductId = "2", Quantity = 1, ShoppingCartTypeId = ShoppingCartType.ShoppingCart, StoreId = "1" } @@ -567,7 +567,6 @@ public async Task ShoppingCartCommonWarningsValidator_Success() var result = await shoppingCartCommonWarningsValidator.ValidateAsync( new ShoppingCartCommonWarningsValidatorRecord( customer, - new Store { Id = "1" }, new List { new() { ProductId = "1", Quantity = 1, ShoppingCartTypeId = ShoppingCartType.ShoppingCart, StoreId = "1" @@ -603,7 +602,6 @@ public async Task ShoppingCartCommonWarningsValidator_Fail() var result = await shoppingCartCommonWarningsValidator.ValidateAsync( new ShoppingCartCommonWarningsValidatorRecord( customer, - new Store { Id = "1" }, new List { new() { ProductId = "1", Quantity = 1, ShoppingCartTypeId = ShoppingCartType.ShoppingCart, StoreId = "1" diff --git a/src/Web/Grand.Web/Controllers/CheckoutController.cs b/src/Web/Grand.Web/Controllers/CheckoutController.cs index 971499d6c7..97dcba53c8 100644 --- a/src/Web/Grand.Web/Controllers/CheckoutController.cs +++ b/src/Web/Grand.Web/Controllers/CheckoutController.cs @@ -19,6 +19,7 @@ using Grand.Infrastructure.Extensions; using Grand.SharedKernel.Attributes; using Grand.Web.Common.Controllers; +using Grand.Web.Common.Extensions; using Grand.Web.Common.Filters; using Grand.Web.Extensions; using Grand.Web.Features.Models.Checkout; @@ -101,7 +102,8 @@ public virtual async Task Index() //validation (cart) var checkoutAttributes = customer.GetUserFieldFromEntity>(SystemCustomerFieldNames.CheckoutAttributes, _contextAccessor.StoreContext.CurrentStore.Id); - var scWarnings = await _shoppingCartValidator.GetShoppingCartWarnings(cart, checkoutAttributes, true, true); + var scWarnings = await _shoppingCartValidator.GetShoppingCartWarnings(cart, checkoutAttributes, + _contextAccessor.CalculationContext(), true, true); if (scWarnings.Any()) return RedirectToRoute("ShoppingCart", new { checkoutAttributes = true }); @@ -141,7 +143,8 @@ public virtual async Task Start() var checkoutAttributes = _contextAccessor.WorkContext.CurrentCustomer .GetUserFieldFromEntity>(SystemCustomerFieldNames.CheckoutAttributes, _contextAccessor.StoreContext.CurrentStore.Id); - var cartWarnings = await _shoppingCartValidator.GetShoppingCartWarnings(cart, checkoutAttributes, true, true); + var cartWarnings = await _shoppingCartValidator.GetShoppingCartWarnings(cart, checkoutAttributes, + _contextAccessor.CalculationContext(), true, true); if (cartWarnings.Any()) return RedirectToRoute("ShoppingCart", new { checkoutAttributes = true }); diff --git a/src/Web/Grand.Web/Controllers/ShoppingCartController.cs b/src/Web/Grand.Web/Controllers/ShoppingCartController.cs index ef9d40d5d1..7b6dccb6dd 100644 --- a/src/Web/Grand.Web/Controllers/ShoppingCartController.cs +++ b/src/Web/Grand.Web/Controllers/ShoppingCartController.cs @@ -490,7 +490,8 @@ public virtual async Task StartCheckout(CheckoutAttributeSelected SystemCustomerFieldNames.CheckoutAttributes, _contextAccessor.StoreContext.CurrentStore.Id); var checkoutAttributeWarnings = - await _shoppingCartValidator.GetShoppingCartWarnings(cart, checkoutAttributes, true, true); + await _shoppingCartValidator.GetShoppingCartWarnings(cart, checkoutAttributes, + _contextAccessor.CalculationContext(), true, true); if (checkoutAttributeWarnings.Any()) return RedirectToRoute("ShoppingCart", new { checkoutAttributes = true }); //everything is OK diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs index ca99143293..729c7a3c36 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs @@ -208,7 +208,7 @@ await _currencyService.ConvertFromPrimaryStoreCurrency(_orderSettings.MinOrderSu model.GiftVoucherBox.Display = _shoppingCartSettings.ShowGiftVoucherBox; //cart warnings - var cartWarnings = await _shoppingCartValidator.GetShoppingCartWarnings(request.Cart, checkoutAttributes, + var cartWarnings = await _shoppingCartValidator.GetShoppingCartWarnings(request.Cart, checkoutAttributes, _contextAccessor.CalculationContext(), request.ValidateCheckoutAttributes, true); foreach (var warning in cartWarnings) model.Warnings.Add(warning); diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs index f6f43be4cb..9bf902eead 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs @@ -15,6 +15,7 @@ using Grand.Domain.Orders; using Grand.Domain.Tax; using Grand.Infrastructure; +using Grand.Web.Common.Extensions; using Grand.Web.Common.Localization; using Grand.Web.Extensions; using Grand.Web.Features.Models.ShoppingCart; @@ -95,7 +96,8 @@ public async Task Handle(GetWishlist request, CancellationToken c model.ShowSku = _catalogSettings.ShowSkuOnProductDetailsPage; //cart warnings - var cartWarnings = await _shoppingCartValidator.GetShoppingCartWarnings(request.Cart, null, false, false); + var cartWarnings = await _shoppingCartValidator.GetShoppingCartWarnings(request.Cart, null, + _contextAccessor.CalculationContext(), false, false); foreach (var warning in cartWarnings) model.Warnings.Add(warning); From 4f3c4b22d066500cf50f66596b8a010ebb92ba47 Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 15:19:30 +0200 Subject: [PATCH 17/33] Pass the customer and store explicitly to cart reads and cart merge ShoppingCartService.GetShoppingCart takes the customer whose cart it reads, and MigrateShoppingCart the store whose selected checkout attributes move along. CustomerLoggedInEvent carries the customer the request was signed in as before the login and the request's store; every publisher reads them where it publishes, so the cart merge sees the same values as before. --- .../Services/ExternalAuthenticationService.cs | 3 +- .../CustomerLoggedInEventHandler.cs | 8 ++- .../Services/Orders/ShoppingCartService.cs | 19 +++---- .../Events/Customers/CustomerLoggedInEvent.cs | 14 ++++- .../Checkout/Orders/IShoppingCartService.cs | 7 ++- .../Controllers/OidcLoginController.cs | 3 +- .../Controllers/PaymentBrainTreeController.cs | 2 +- .../CustomerLoggedInEventHandlerTests.cs | 27 +++++++--- .../Orders/ShoppingCartServiceTests.cs | 54 ++++++++++++------- ...ustomerLoggedInNotificationHandlerTests.cs | 2 +- ...ccountControllerTwoFactorReturnUrlTests.cs | 2 + .../Controllers/BaseLoginController.cs | 3 +- src/Web/Grand.Web/Components/EmailWishlist.cs | 2 +- .../Grand.Web/Components/EstimateShipping.cs | 2 +- .../Controllers/AccountController.cs | 3 +- .../Controllers/ActionCartController.cs | 10 ++-- .../Controllers/CheckoutController.cs | 16 +++--- .../Controllers/ProductController.cs | 2 +- .../Controllers/ShoppingCartController.cs | 34 ++++++------ .../Controllers/WishlistController.cs | 6 +-- .../ShoppingCart/GetAddToCartHandler.cs | 2 +- .../GetMiniShoppingCartHandler.cs | 2 +- .../ShoppingCart/UpdateQuantityValidator.cs | 2 +- .../Shared/Partials/ShoppingCartLinks.cshtml | 4 +- 24 files changed, 138 insertions(+), 91 deletions(-) diff --git a/src/Business/Grand.Business.Authentication/Services/ExternalAuthenticationService.cs b/src/Business/Grand.Business.Authentication/Services/ExternalAuthenticationService.cs index 0a08767243..9c06bc0d7e 100644 --- a/src/Business/Grand.Business.Authentication/Services/ExternalAuthenticationService.cs +++ b/src/Business/Grand.Business.Authentication/Services/ExternalAuthenticationService.cs @@ -169,7 +169,8 @@ _customerSettings.UserRegistrationType is UserRegistrationType.Standard protected virtual async Task LoginUser(Customer user, string returnUrl) { //raise event - await _mediator.Publish(new CustomerLoggedInEvent(user)); + await _mediator.Publish(new CustomerLoggedInEvent(user, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id)); //authenticate await _authenticationService.SignIn(user, false); diff --git a/src/Business/Grand.Business.Checkout/Events/ShoppingCart/CustomerLoggedInEventHandler.cs b/src/Business/Grand.Business.Checkout/Events/ShoppingCart/CustomerLoggedInEventHandler.cs index ffcb5d67c9..22e9efb41c 100644 --- a/src/Business/Grand.Business.Checkout/Events/ShoppingCart/CustomerLoggedInEventHandler.cs +++ b/src/Business/Grand.Business.Checkout/Events/ShoppingCart/CustomerLoggedInEventHandler.cs @@ -1,6 +1,5 @@ using Grand.Business.Core.Events.Customers; using Grand.Business.Core.Interfaces.Checkout.Orders; -using Grand.Infrastructure; using Grand.Mediator; namespace Grand.Business.Checkout.Events.ShoppingCart; @@ -8,17 +7,16 @@ namespace Grand.Business.Checkout.Events.ShoppingCart; public class CustomerLoggedInEventHandler : INotificationHandler { private readonly IShoppingCartService _shoppingCartService; - private readonly IContextAccessor _contextAccessor; - public CustomerLoggedInEventHandler(IShoppingCartService shoppingCartService, IContextAccessor contextAccessor) + public CustomerLoggedInEventHandler(IShoppingCartService shoppingCartService) { _shoppingCartService = shoppingCartService; - _contextAccessor = contextAccessor; } public async Task Handle(CustomerLoggedInEvent notification, CancellationToken cancellationToken) { //migrate shopping cart - await _shoppingCartService.MigrateShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, notification.Customer, true); + await _shoppingCartService.MigrateShoppingCart(notification.PreviousCustomer, notification.Customer, + notification.StoreId, true); } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartService.cs b/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartService.cs index 068209b507..59e0292409 100644 --- a/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartService.cs +++ b/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartService.cs @@ -8,7 +8,6 @@ using Grand.Domain.Common; using Grand.Domain.Customers; using Grand.Domain.Orders; -using Grand.Infrastructure; using Grand.Infrastructure.Extensions; using Grand.Mediator; @@ -22,14 +21,12 @@ public class ShoppingCartService : IShoppingCartService #region Ctor public ShoppingCartService( - IContextAccessor contextAccessor, IProductService productService, ICustomerService customerService, IMediator mediator, IShoppingCartValidator shoppingCartValidator, ShoppingCartSettings shoppingCartSettings) { - _contextAccessor = contextAccessor; _productService = productService; _customerService = customerService; _mediator = mediator; @@ -41,7 +38,6 @@ public ShoppingCartService( #region Fields - private readonly IContextAccessor _contextAccessor; private readonly IProductService _productService; private readonly ICustomerService _customerService; private readonly IMediator _mediator; @@ -55,14 +51,17 @@ public ShoppingCartService( /// /// Gets shopping cart /// + /// Customer whose cart is read /// Store identifier; pass null to load all records /// Shopping cart type; pass null to load all records /// Shopping Cart - public virtual async Task> GetShoppingCart(string storeId = null, + public virtual async Task> GetShoppingCart(Customer customer, string storeId = null, params ShoppingCartType[] shoppingCartType) { + ArgumentNullException.ThrowIfNull(customer); + var model = new List(); - var cart = _contextAccessor.WorkContext.CurrentCustomer.ShoppingCartItems.ToList(); + var cart = customer.ShoppingCartItems.ToList(); if (!string.IsNullOrEmpty(storeId)) cart = cart.LimitPerStore(_shoppingCartSettings.SharedCartBetweenStores, storeId).ToList(); @@ -393,11 +392,13 @@ public virtual async Task DeleteShoppingCartItem(Customer customer, ShoppingCart /// /// From customer /// To customer + /// Store whose selected checkout attributes move along /// /// A value indicating whether to coupon codes (discount and gift voucher) should be also /// re-applied /// - public virtual async Task MigrateShoppingCart(Customer fromCustomer, Customer toCustomer, bool includeCouponCodes) + public virtual async Task MigrateShoppingCart(Customer fromCustomer, Customer toCustomer, string storeId, + bool includeCouponCodes) { ArgumentNullException.ThrowIfNull(fromCustomer); ArgumentNullException.ThrowIfNull(toCustomer); @@ -430,9 +431,9 @@ await AddToCart(toCustomer, sci.ProductId, sci.ShoppingCartTypeId, sci.StoreId, } //move selected checkout attributes - var checkoutAttributes = fromCustomer.GetUserFieldFromEntity>(SystemCustomerFieldNames.CheckoutAttributes, _contextAccessor.StoreContext.CurrentStore.Id); + var checkoutAttributes = fromCustomer.GetUserFieldFromEntity>(SystemCustomerFieldNames.CheckoutAttributes, storeId); await _customerService.UpdateUserField(toCustomer, SystemCustomerFieldNames.CheckoutAttributes, checkoutAttributes, - _contextAccessor.StoreContext.CurrentStore.Id); + storeId); } #endregion diff --git a/src/Business/Grand.Business.Core/Events/Customers/CustomerLoggedInEvent.cs b/src/Business/Grand.Business.Core/Events/Customers/CustomerLoggedInEvent.cs index adeda2f2ef..7b05608c7d 100644 --- a/src/Business/Grand.Business.Core/Events/Customers/CustomerLoggedInEvent.cs +++ b/src/Business/Grand.Business.Core/Events/Customers/CustomerLoggedInEvent.cs @@ -8,9 +8,11 @@ namespace Grand.Business.Core.Events.Customers; /// public class CustomerLoggedInEvent : INotification { - public CustomerLoggedInEvent(Customer customer) + public CustomerLoggedInEvent(Customer customer, Customer previousCustomer, string storeId) { Customer = customer; + PreviousCustomer = previousCustomer; + StoreId = storeId; } /// @@ -20,4 +22,14 @@ public Customer Customer { get; private set; } + + /// + /// Customer the request was signed in as before the login (usually the guest) + /// + public Customer PreviousCustomer { get; } + + /// + /// Store of the request the customer logged in through + /// + public string StoreId { get; } } \ No newline at end of file diff --git a/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartService.cs b/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartService.cs index ae62d61c9c..cd67b2b767 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IShoppingCartService.cs @@ -13,10 +13,12 @@ public interface IShoppingCartService /// /// Gets shopping cart /// + /// Customer whose cart is read /// Store identifier; pass null to load all records /// Shopping cart type; pass null to load all records /// Shopping Cart - Task> GetShoppingCart(string storeId = null, params ShoppingCartType[] shoppingCartType); + Task> GetShoppingCart(Customer customer, string storeId = null, + params ShoppingCartType[] shoppingCartType); /// /// Finds a shopping cart item in the cart @@ -107,9 +109,10 @@ Task DeleteShoppingCartItem(Customer customer, ShoppingCartItem shoppingCartItem /// /// From customer /// To customer + /// Store whose selected checkout attributes move along /// /// A value indicating whether to coupon codes (discount and gift voucher) should be also /// re-applied /// - Task MigrateShoppingCart(Customer fromCustomer, Customer toCustomer, bool includeCouponCodes); + Task MigrateShoppingCart(Customer fromCustomer, Customer toCustomer, string storeId, bool includeCouponCodes); } \ No newline at end of file diff --git a/src/Plugins/Authentication.OpenIdConnect/Controllers/OidcLoginController.cs b/src/Plugins/Authentication.OpenIdConnect/Controllers/OidcLoginController.cs index 338642a0a7..658376972a 100644 --- a/src/Plugins/Authentication.OpenIdConnect/Controllers/OidcLoginController.cs +++ b/src/Plugins/Authentication.OpenIdConnect/Controllers/OidcLoginController.cs @@ -144,7 +144,8 @@ public async Task Callback(string returnUrl) return RedirectToRoute("OidcLoginFailed", new { session = true }); //the identity provider is the sign-in: its multi-factor policy applies, not the store's two-factor code - await _mediator.Publish(new CustomerLoggedInEvent(result.Customer)); + await _mediator.Publish(new CustomerLoggedInEvent(result.Customer, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id)); await _authenticationService.SignIn(result.Customer, false, sessionId); return RedirectLocal(returnUrl); } diff --git a/src/Plugins/Payments.BrainTree/Controllers/PaymentBrainTreeController.cs b/src/Plugins/Payments.BrainTree/Controllers/PaymentBrainTreeController.cs index d55e33e5fd..368ad2e18b 100644 --- a/src/Plugins/Payments.BrainTree/Controllers/PaymentBrainTreeController.cs +++ b/src/Plugins/Payments.BrainTree/Controllers/PaymentBrainTreeController.cs @@ -41,7 +41,7 @@ public async Task PaymentInfo() if (_brainTreePaymentSettings.Use3DS) { - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.ShoppingCart); if (!cart.Any()) diff --git a/src/Tests/Grand.Business.Checkout.Tests/Events/ShoppingCart/CustomerLoggedInEventHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Events/ShoppingCart/CustomerLoggedInEventHandlerTests.cs index 1fd906e096..483434ff51 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Events/ShoppingCart/CustomerLoggedInEventHandlerTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Events/ShoppingCart/CustomerLoggedInEventHandlerTests.cs @@ -2,7 +2,6 @@ using Grand.Business.Core.Events.Customers; using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Domain.Customers; -using Grand.Infrastructure; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -13,28 +12,40 @@ public class CustomerLoggedInEventHandlerTests { private CustomerLoggedInEventHandler _customerLoggedInEventHandler; private Mock _shoppingCartServiceMock; - private Mock _workContextMock; [TestInitialize] public void Init() { _shoppingCartServiceMock = new Mock(); - _workContextMock = new Mock(); - _workContextMock.Setup(s => s.WorkContext.CurrentCustomer).Returns(new Customer()); - _customerLoggedInEventHandler = - new CustomerLoggedInEventHandler(_shoppingCartServiceMock.Object, _workContextMock.Object); + _customerLoggedInEventHandler = new CustomerLoggedInEventHandler(_shoppingCartServiceMock.Object); } [TestMethod] public async Task HandleTest() { //Arrange - var notification = new CustomerLoggedInEvent(new Customer()); + var notification = new CustomerLoggedInEvent(new Customer(), new Customer(), ""); //Act await _customerLoggedInEventHandler.Handle(notification, CancellationToken.None); //Assert - _shoppingCartServiceMock.Verify(c => c.MigrateShoppingCart(It.IsAny(), It.IsAny(), true), + _shoppingCartServiceMock.Verify(c => c.MigrateShoppingCart(It.IsAny(), It.IsAny(), + It.IsAny(), true), Times.Once); } + + [TestMethod] + public async Task Handle_MigratesFromThePreviousCustomerInTheEventStore() + { + //Arrange + var guest = new Customer(); + var customer = new Customer(); + + //Act + await _customerLoggedInEventHandler.Handle(new CustomerLoggedInEvent(customer, guest, "store-b"), + CancellationToken.None); + + //Assert + _shoppingCartServiceMock.Verify(c => c.MigrateShoppingCart(guest, customer, "store-b", true), Times.Once); + } } \ No newline at end of file diff --git a/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartServiceTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartServiceTests.cs index 46ede9fa40..bbccb4d2a4 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartServiceTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartServiceTests.cs @@ -26,26 +26,17 @@ public class ShoppingCartServiceTests private ShoppingCartService _shoppingCartService; private ShoppingCartSettings _shoppingCartSettings; private Mock _shoppingCartValidatorMock; - private Mock _workContextMock; [TestInitialize] public void Init() { - _workContextMock = new Mock(); _productServiceMock = new Mock(); _shoppingCartSettings = new ShoppingCartSettings(); _customerServiceMock = new Mock(); _mediatorMock = new Mock(); _shoppingCartValidatorMock = new Mock(); - _workContextMock.Setup(c => c.StoreContext.CurrentStore).Returns(() => new Store { Id = "", Name = "test store" }); - var customer = new Customer(); - customer.Groups.Add("1"); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => customer); - _workContextMock.Setup(c => c.WorkContext.WorkingCurrency).Returns(() => new Currency()); - _workContextMock.Setup(c => c.WorkContext.TaxDisplayType).Returns(() => TaxDisplayType.ExcludingTax); - - _shoppingCartService = new ShoppingCartService(_workContextMock.Object, _productServiceMock.Object, + _shoppingCartService = new ShoppingCartService(_productServiceMock.Object, _customerServiceMock.Object, _mediatorMock.Object, _shoppingCartValidatorMock.Object, _shoppingCartSettings); } @@ -57,13 +48,12 @@ public async Task GetShoppingCartTest() customer.Groups.Add("1"); customer.ShoppingCartItems.Add(new ShoppingCartItem { ShoppingCartTypeId = ShoppingCartType.ShoppingCart, StoreId = "" }); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => customer); _productServiceMock.Setup(a => a.GetProductById(It.IsAny(), false)).Returns(() => Task.FromResult(new Product { Id = "1", Published = true, Price = 10 })); //Act - var result = await _shoppingCartService.GetShoppingCart(); + var result = await _shoppingCartService.GetShoppingCart(customer); //Assert Assert.IsNotEmpty(result); } @@ -78,7 +68,6 @@ public async Task FindShoppingCartItemTest() { ShoppingCartTypeId = ShoppingCartType.ShoppingCart, StoreId = "", ProductId = "1" }); customer.ShoppingCartItems.Add(new ShoppingCartItem { ShoppingCartTypeId = ShoppingCartType.ShoppingCart, StoreId = "", ProductId = "2" }); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => customer); _productServiceMock.Setup(a => a.GetProductById(It.IsAny(), false)).Returns(() => Task.FromResult(new Product { Id = "1", Published = true, Price = 10 })); @@ -97,7 +86,6 @@ public async Task AddToCartTest_Success() //Arrange var customer = new Customer(); customer.Groups.Add("1"); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => customer); _productServiceMock.Setup(a => a.GetProductById(It.IsAny(), false)).Returns(() => Task.FromResult(new Product { Id = "1", Published = true, Price = 10 })); @@ -122,7 +110,6 @@ public async Task AddToCartTest_Warning() //Arrange var customer = new Customer(); customer.Groups.Add("1"); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => customer); _productServiceMock.Setup(a => a.GetProductById(It.IsAny(), false)).Returns(() => Task.FromResult(new Product { Id = "1", Published = true, Price = 10 })); @@ -147,7 +134,6 @@ public async Task UpdateShoppingCartItemTest() //Arrange var customer = new Customer(); customer.Groups.Add("1"); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => customer); _productServiceMock.Setup(a => a.GetProductById(It.IsAny(), false)).Returns(() => Task.FromResult(new Product { Id = "1", Published = true, Price = 10 })); @@ -174,7 +160,6 @@ public async Task DeleteShoppingCartItemTest() //Arrange var customer = new Customer(); customer.Groups.Add("1"); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => customer); _productServiceMock.Setup(a => a.GetProductById(It.IsAny(), false)).Returns(() => Task.FromResult(new Product { Id = "1", Published = true, Price = 10 })); @@ -200,7 +185,6 @@ public async Task MigrateShoppingCartTest() //Arrange var customer = new Customer(); customer.Groups.Add("1"); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => customer); _productServiceMock.Setup(a => a.GetProductById(It.IsAny(), false)).Returns(() => Task.FromResult(new Product { Id = "1", Published = true, Price = 10 })); @@ -215,9 +199,41 @@ public async Task MigrateShoppingCartTest() var item = await _shoppingCartService.AddToCart(customer, "2", ShoppingCartType.ShoppingCart, ""); var customer2 = new Customer(); //Act - await _shoppingCartService.MigrateShoppingCart(customer, customer2, false); + await _shoppingCartService.MigrateShoppingCart(customer, customer2, "", false); //Assert Assert.IsNotEmpty(customer2.ShoppingCartItems); Assert.IsEmpty(customer.ShoppingCartItems); } + + [TestMethod] + public async Task GetShoppingCart_ReturnsTheItemsOfTheGivenCustomer() + { + //Arrange + var customer = new Customer(); + customer.ShoppingCartItems.Add(new ShoppingCartItem + { ProductId = "1", ShoppingCartTypeId = ShoppingCartType.Wishlist, StoreId = "store" }); + _productServiceMock.Setup(a => a.GetProductById("1", false)).ReturnsAsync(new Product { Id = "1" }); + + //Act + var result = await _shoppingCartService.GetShoppingCart(customer, "store", ShoppingCartType.Wishlist); + + //Assert + Assert.HasCount(1, result); + Assert.AreSame(customer.ShoppingCartItems.First(), result[0]); + } + + [TestMethod] + public async Task MigrateShoppingCart_MovesCheckoutAttributesOfTheGivenStore() + { + //Arrange + var fromCustomer = new Customer(); + var toCustomer = new Customer(); + + //Act + await _shoppingCartService.MigrateShoppingCart(fromCustomer, toCustomer, "store-b", false); + + //Assert + _customerServiceMock.Verify(x => x.UpdateUserField(toCustomer, SystemCustomerFieldNames.CheckoutAttributes, + It.IsAny>(), "store-b"), Times.Once); + } } \ No newline at end of file diff --git a/src/Tests/Grand.Business.Customers.Tests/Handler/CustomerLoggedInNotificationHandlerTests.cs b/src/Tests/Grand.Business.Customers.Tests/Handler/CustomerLoggedInNotificationHandlerTests.cs index 9df29753d4..879f426e88 100644 --- a/src/Tests/Grand.Business.Customers.Tests/Handler/CustomerLoggedInNotificationHandlerTests.cs +++ b/src/Tests/Grand.Business.Customers.Tests/Handler/CustomerLoggedInNotificationHandlerTests.cs @@ -29,7 +29,7 @@ public async Task HandleTest() CannotLoginUntilDateUtc = DateTime.Now }; //Act - await _handler.Handle(new CustomerLoggedInEvent(customer), CancellationToken.None); + await _handler.Handle(new CustomerLoggedInEvent(customer, new Customer(), ""), CancellationToken.None); //Assert Assert.AreEqual(0, customer.FailedLoginAttempts); diff --git a/src/Tests/Grand.Web.Tests/Controllers/AccountControllerTwoFactorReturnUrlTests.cs b/src/Tests/Grand.Web.Tests/Controllers/AccountControllerTwoFactorReturnUrlTests.cs index 9cec195e40..3073dd5c97 100644 --- a/src/Tests/Grand.Web.Tests/Controllers/AccountControllerTwoFactorReturnUrlTests.cs +++ b/src/Tests/Grand.Web.Tests/Controllers/AccountControllerTwoFactorReturnUrlTests.cs @@ -49,6 +49,8 @@ public void Init() _customers = new Mock(); _customers.Setup(x => x.GetCustomerByEmail("a@b.c", It.IsAny())).ReturnsAsync(_customer); var context = new Mock(); + context.Setup(x => x.StoreContext.CurrentStore).Returns(new Domain.Stores.Store { Id = "store" }); + context.Setup(x => x.WorkContext.CurrentCustomer).Returns(new Customer()); var settings = new CustomerSettings { TwoFactorAuthenticationEnabled = true }; _controller = new AccountController(new Mock().Object, new Mock().Object, context.Object, _customers.Object, new Mock().Object, _manager.Object, new Mock().Object, diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseLoginController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseLoginController.cs index aab0f6e03c..6dbde89931 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseLoginController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseLoginController.cs @@ -219,7 +219,8 @@ protected async Task SignInAction(Customer customer, bool createP await _authenticationService.SignIn(customer, createPersistent); //raise event - await _mediator.Publish(new CustomerLoggedInEvent(customer)); + await _mediator.Publish(new CustomerLoggedInEvent(customer, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id)); return RedirectToRoute($"{GetCurrentArea()}Index"); } diff --git a/src/Web/Grand.Web/Components/EmailWishlist.cs b/src/Web/Grand.Web/Components/EmailWishlist.cs index 3faad0e9ef..51c2479324 100644 --- a/src/Web/Grand.Web/Components/EmailWishlist.cs +++ b/src/Web/Grand.Web/Components/EmailWishlist.cs @@ -39,7 +39,7 @@ public async Task InvokeAsync() !_shoppingCartSettings.EmailWishlistEnabled) return Content(""); - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist); + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist); if (!cart.Any()) return Content(""); diff --git a/src/Web/Grand.Web/Components/EstimateShipping.cs b/src/Web/Grand.Web/Components/EstimateShipping.cs index a718f516e2..83b59cf1fd 100644 --- a/src/Web/Grand.Web/Components/EstimateShipping.cs +++ b/src/Web/Grand.Web/Components/EstimateShipping.cs @@ -24,7 +24,7 @@ public EstimateShippingViewComponent(IMediator mediator, IShoppingCartService sh public async Task InvokeAsync() { - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.ShoppingCart); var model = await _mediator.Send(new GetEstimateShipping { diff --git a/src/Web/Grand.Web/Controllers/AccountController.cs b/src/Web/Grand.Web/Controllers/AccountController.cs index 8738e8d078..101fa48163 100644 --- a/src/Web/Grand.Web/Controllers/AccountController.cs +++ b/src/Web/Grand.Web/Controllers/AccountController.cs @@ -357,7 +357,8 @@ private async Task SignInAction(Customer customer, bool createPer string returnUrl = null) { //raise event - await _mediator.Publish(new CustomerLoggedInEvent(customer)); + await _mediator.Publish(new CustomerLoggedInEvent(customer, + _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id)); //sign in new customer await _authenticationService.SignIn(customer, createPersistentCookie); diff --git a/src/Web/Grand.Web/Controllers/ActionCartController.cs b/src/Web/Grand.Web/Controllers/ActionCartController.cs index 0ecb50ebc4..f40980bd54 100644 --- a/src/Web/Grand.Web/Controllers/ActionCartController.cs +++ b/src/Web/Grand.Web/Controllers/ActionCartController.cs @@ -182,7 +182,7 @@ public virtual async Task> AddProductCatalog(Pr var warehouseId = GetWarehouse(product); - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, model.ShoppingCartTypeId); + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, model.ShoppingCartTypeId); if (model.ShoppingCartTypeId != ShoppingCartType.Wishlist) { @@ -255,7 +255,7 @@ await _shoppingCartService.FindShoppingCartItem(cart, model.ShoppingCartTypeId, }); //display notification message and update appropriate blocks - var qty = (await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var qty = (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist)).Sum(x => x.Quantity); var updatetopwishlistsectionhtml = string.Format(_translationService.GetResource("Wishlist.HeaderQuantity"), qty); @@ -289,7 +289,7 @@ await _shoppingCartService.FindShoppingCartItem(cart, model.ShoppingCartTypeId, var updatetopcartsectionhtml = string.Format( _translationService.GetResource("ShoppingCart.HeaderQuantity"), - (await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, shoppingCartTypes.ToArray())) .Sum(x => x.Quantity)); @@ -451,7 +451,7 @@ public virtual async Task> AddProductDetails(Pr }); //display notification message and update appropriate blocks - var qty = (await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var qty = (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist)).Sum(x => x.Quantity); var updatetopwishlistsectionhtml = string.Format(_translationService.GetResource("Wishlist.HeaderQuantity"), qty); @@ -485,7 +485,7 @@ public virtual async Task> AddProductDetails(Pr var updatetopcartsectionhtml = string.Format( _translationService.GetResource("ShoppingCart.HeaderQuantity"), - (await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, shoppingCartTypes.ToArray())) .Sum(x => x.Quantity)); diff --git a/src/Web/Grand.Web/Controllers/CheckoutController.cs b/src/Web/Grand.Web/Controllers/CheckoutController.cs index 97dcba53c8..748ca3ef46 100644 --- a/src/Web/Grand.Web/Controllers/CheckoutController.cs +++ b/src/Web/Grand.Web/Controllers/CheckoutController.cs @@ -88,7 +88,7 @@ public virtual async Task Index() { var customer = _contextAccessor.WorkContext.CurrentCustomer; - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); if (!cart.Any()) @@ -125,7 +125,7 @@ await _shoppingCartValidator.GetShoppingCartItemWarnings(customer, sci, product, public virtual async Task Start() { //validation - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); if (!cart.Any()) @@ -191,7 +191,7 @@ public virtual async Task SaveBilling( try { //validation - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); await CartValidate(cart); @@ -289,7 +289,7 @@ public virtual async Task SaveShipping( try { //validation - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); await CartValidate(cart); @@ -465,7 +465,7 @@ public virtual async Task SaveShippingMethod(CheckoutShippingMeth var customer = _contextAccessor.WorkContext.CurrentCustomer; var store = _contextAccessor.StoreContext.CurrentStore; - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); await CartValidate(cart); @@ -550,7 +550,7 @@ public virtual async Task SavePaymentMethod(CheckoutPaymentMethod try { //validation - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); await CartValidate(cart); @@ -620,7 +620,7 @@ public virtual async Task SavePaymentInfo(IDictionary ConfirmOrder() try { //validation - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); await CartValidate(cart); diff --git a/src/Web/Grand.Web/Controllers/ProductController.cs b/src/Web/Grand.Web/Controllers/ProductController.cs index db072f839a..0cdddc650b 100644 --- a/src/Web/Grand.Web/Controllers/ProductController.cs +++ b/src/Web/Grand.Web/Controllers/ProductController.cs @@ -241,7 +241,7 @@ public virtual async Task GetDatesForMonth(string productId, int if (!string.IsNullOrEmpty(parameter)) query = query.Where(x => x.Parameter == parameter); var reservations = query.ToList(); - var inCart = (await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id)) + var inCart = (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id)) .Where(x => !string.IsNullOrEmpty(x.ReservationId)).ToList(); foreach (var cartItem in inCart) { diff --git a/src/Web/Grand.Web/Controllers/ShoppingCartController.cs b/src/Web/Grand.Web/Controllers/ShoppingCartController.cs index 7b6dccb6dd..7328aa6f95 100644 --- a/src/Web/Grand.Web/Controllers/ShoppingCartController.cs +++ b/src/Web/Grand.Web/Controllers/ShoppingCartController.cs @@ -127,7 +127,7 @@ public virtual async Task CheckoutAttributeChange(CheckoutAttribu [FromServices] ICheckoutAttributeParser checkoutAttributeParser, [FromServices] ICheckoutAttributeFormatter checkoutAttributeFormatter) { - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); var checkoutAttributes = await _mediator.Send(new SaveCheckoutAttributesCommand { @@ -254,7 +254,7 @@ public virtual async Task> Cart(bool checkoutAtt if (!await _permissionService.Authorize(StandardPermission.EnableShoppingCart)) return RedirectToRoute("HomePage"); - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); var model = await _mediator.Send(new GetShoppingCart { Cart = cart, ValidateCheckoutAttributes = checkoutAttributes, @@ -271,7 +271,7 @@ public virtual async Task> Cart(bool checkoutAtt [DenySystemAccount] public async Task> CartSummary() { - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); var model = await _mediator.Send(new GetShoppingCart { @@ -291,7 +291,7 @@ public async Task> CartSummary() [DenySystemAccount] public async Task> CartTotal() { - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); var model = await _mediator.Send(new GetOrderTotals { @@ -316,7 +316,7 @@ public virtual async Task UpdateQuantity(UpdateQuantityModel mode warnings = string.Join(',', ModelState.Values.SelectMany(x => x.Errors.Select(x => x.ErrorMessage))) }); - var cart = (await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes())) + var cart = (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes())) .FirstOrDefault(x => x.Id == model.ShoppingCartId); var warnings = new List(); @@ -330,7 +330,7 @@ public virtual async Task UpdateQuantity(UpdateQuantityModel mode } var cartModel = await _mediator.Send(new GetShoppingCart { - Cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()), + Cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()), Customer = _contextAccessor.WorkContext.CurrentCustomer, Currency = _contextAccessor.WorkContext.WorkingCurrency, Language = _contextAccessor.WorkContext.WorkingLanguage, @@ -354,7 +354,7 @@ public virtual async Task ClearCart() if (!await _permissionService.Authorize(StandardPermission.EnableShoppingCart)) return RedirectToRoute("HomePage"); - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); foreach (var sci in cart) await _shoppingCartService.DeleteShoppingCartItem(_contextAccessor.WorkContext.CurrentCustomer, sci, @@ -374,7 +374,7 @@ public virtual async Task DeleteCartItem(DeleteCartItemModel mode if (_shoppingCartSettings.AllowOnHoldCart) shoppingCartTypes.Add(ShoppingCartType.OnHoldCart); - var item = (await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var item = (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, shoppingCartTypes.ToArray())) .FirstOrDefault(sci => sci.Id == model.Id); @@ -396,7 +396,7 @@ await _shoppingCartService.DeleteShoppingCartItem(_contextAccessor.WorkContext.C sidebarshoppingcartmodel = miniShoppingCartmodel }); - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); var shoppingcartmodel = await _mediator.Send(new GetShoppingCart { Cart = cart, Customer = _contextAccessor.WorkContext.CurrentCustomer, @@ -428,7 +428,7 @@ public virtual async Task ChangeTypeCartItem(ChangeTypeCartItemMo if (_shoppingCartSettings.AllowOnHoldCart) shoppingCartTypes.Add(ShoppingCartType.OnHoldCart); - var item = (await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var item = (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, shoppingCartTypes.ToArray())) .FirstOrDefault(sci => sci.Id == model.Id); @@ -446,7 +446,7 @@ public virtual async Task ChangeTypeCartItem(ChangeTypeCartItemMo Store = _contextAccessor.StoreContext.CurrentStore }); - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); var shoppingcartmodel = await _mediator.Send(new GetShoppingCart { Cart = cart, Customer = _contextAccessor.WorkContext.CurrentCustomer, @@ -473,7 +473,7 @@ public virtual IActionResult ContinueShopping() [DenySystemAccount] public virtual async Task StartCheckout(CheckoutAttributeSelectedModel model) { - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); List checkoutAttributes; //parse and save checkout attributes @@ -506,7 +506,7 @@ await _shoppingCartValidator.GetShoppingCartWarnings(cart, checkoutAttributes, [HttpPost] public virtual async Task ApplyDiscountCoupon(DiscountCouponModel model) { - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); var message = string.Empty; var isApplied = false; @@ -549,7 +549,7 @@ await _customerService.UpdateUserField(_contextAccessor.WorkContext.CurrentCusto [HttpPost] public virtual async Task ApplyGiftVoucher(GiftVoucherCouponModel model) { - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); var message = string.Empty; var isApplied = false; @@ -592,7 +592,7 @@ await _customerService.UpdateUserField(_contextAccessor.WorkContext.CurrentCusto [HttpPost] public virtual async Task GetEstimateShipping(EstimateShippingModel model) { - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.ShoppingCart, ShoppingCartType.Auctions); var result = await _mediator.Send(new GetEstimateShippingResult { @@ -630,7 +630,7 @@ await _customerService.UpdateUserField(_contextAccessor.WorkContext.CurrentCusto } } - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); var model = await _mediator.Send(new GetShoppingCart { Cart = cart, @@ -665,7 +665,7 @@ await _customerService.UpdateUserField(_contextAccessor.WorkContext.CurrentCusto } } - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes()); var model = await _mediator.Send(new GetShoppingCart { Cart = cart, diff --git a/src/Web/Grand.Web/Controllers/WishlistController.cs b/src/Web/Grand.Web/Controllers/WishlistController.cs index 5afe8ecc0f..c6adf55a12 100644 --- a/src/Web/Grand.Web/Controllers/WishlistController.cs +++ b/src/Web/Grand.Web/Controllers/WishlistController.cs @@ -119,7 +119,7 @@ public virtual async Task UpdateQuantity(UpdateQuantityModel mode if (ModelState.IsValid) { var cart = - (await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist)) + (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist)) .FirstOrDefault(x => x.Id == model.ShoppingCartId); if (cart != null) { @@ -141,7 +141,7 @@ public virtual async Task UpdateQuantity(UpdateQuantityModel mode success = !warnings.Any(), warnings = string.Join(", ", warnings), totalproducts = - (await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist)) + (await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist)) .Sum(x => x.Quantity) }); } @@ -216,7 +216,7 @@ public virtual async Task EmailWishlist(WishlistEmailAFriendModel !_shoppingCartSettings.EmailWishlistEnabled) return Content(""); - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist); + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist); if (!cart.Any()) return Content(""); diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs index 936987903e..536b30f433 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs @@ -136,7 +136,7 @@ await _productAttributeFormatter.FormatAttributes(request.Product, request.Attri model.Picture = await PrepareCartItemPicture(request); } - var cart = await _shoppingCartService.GetShoppingCart(request.Store.Id, request.CartType); + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, request.Store.Id, request.CartType); if (request.CartType != ShoppingCartType.Auctions) { diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs index f109276d0a..d1e90bc3c6 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs @@ -107,7 +107,7 @@ public async Task Handle(GetMiniShoppingCart request, if (_shoppingCartSettings.AllowOnHoldCart) shoppingCartTypes.Add(ShoppingCartType.OnHoldCart); - var cart = await _shoppingCartService.GetShoppingCart(request.Store.Id, shoppingCartTypes.ToArray()); + var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, request.Store.Id, shoppingCartTypes.ToArray()); model.TotalProducts = cart.Sum(x => x.Quantity); if (!cart.Any()) return model; diff --git a/src/Web/Grand.Web/Validators/ShoppingCart/UpdateQuantityValidator.cs b/src/Web/Grand.Web/Validators/ShoppingCart/UpdateQuantityValidator.cs index 5d0c48e401..8fc61cc9f3 100644 --- a/src/Web/Grand.Web/Validators/ShoppingCart/UpdateQuantityValidator.cs +++ b/src/Web/Grand.Web/Validators/ShoppingCart/UpdateQuantityValidator.cs @@ -36,7 +36,7 @@ public UpdateQuantityValidator( } } - var cart = (await shoppingCartService.GetShoppingCart(contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes())) + var cart = (await shoppingCartService.GetShoppingCart(contextAccessor.WorkContext.CurrentCustomer, contextAccessor.StoreContext.CurrentStore.Id, PrepareCartTypes())) .FirstOrDefault(z => z.Id == x.ShoppingCartId); if (cart == null) context.AddFailure("Shopping cart item not found"); }); diff --git a/src/Web/Grand.Web/Views/Shared/Partials/ShoppingCartLinks.cshtml b/src/Web/Grand.Web/Views/Shared/Partials/ShoppingCartLinks.cshtml index 1216b00608..68e2824aba 100644 --- a/src/Web/Grand.Web/Views/Shared/Partials/ShoppingCartLinks.cshtml +++ b/src/Web/Grand.Web/Views/Shared/Partials/ShoppingCartLinks.cshtml @@ -21,13 +21,13 @@ var shoppingCartItems = contextAccessor.WorkContext.CurrentCustomer.ShoppingCartItems.Any() - ? (await shoppingCartService.GetShoppingCart(contextAccessor.StoreContext.CurrentStore.Id, shoppingCartTypes.ToArray())) + ? (await shoppingCartService.GetShoppingCart(contextAccessor.WorkContext.CurrentCustomer, contextAccessor.StoreContext.CurrentStore.Id, shoppingCartTypes.ToArray())) .Sum(x => x.Quantity) : 0; var wishlistItems = contextAccessor.WorkContext.CurrentCustomer.ShoppingCartItems.Any() - ? (await shoppingCartService.GetShoppingCart(contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist)) + ? (await shoppingCartService.GetShoppingCart(contextAccessor.WorkContext.CurrentCustomer, contextAccessor.StoreContext.CurrentStore.Id, ShoppingCartType.Wishlist)) .Sum(x => x.Quantity) : 0; } From 2dba4d6bbda4af9369f1cc2a6eb1a0319361188a Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 15:25:11 +0200 Subject: [PATCH 18/33] Take the place order context from the command PlaceOrderCommand carries the calculation context, the fallback language, the store host and the impersonating staff member; the checkout page fills them from its request. PlaceOrderCommandHandler no longer reads WorkContext or StoreContext, and OrderNotificationCommand carries the customer and the impersonating staff member instead of the whole work context. --- .../Orders/OrderNotificationCommandHandler.cs | 10 +- .../Orders/PlaceOrderCommandHandler.cs | 86 +++++++++-------- .../Checkout/Orders/PlaceOrderCommand.cs | 26 ++++- .../Common/OrderNotificationCommand.cs | 15 ++- .../Utilities/Checkout/PlaceOrderContainer.cs | 5 + .../OrderNotificationCommandHandlerTests.cs | 63 +++++++++++++ .../Orders/PlaceOrderCommandHandlerTests.cs | 94 +++++++++++++++++++ .../Controllers/CheckoutController.cs | 7 +- 8 files changed, 252 insertions(+), 54 deletions(-) create mode 100644 src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/OrderNotificationCommandHandlerTests.cs create mode 100644 src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/PlaceOrderCommandHandlerTests.cs diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/OrderNotificationCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/OrderNotificationCommandHandler.cs index 66eb921eeb..4013d2b751 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/OrderNotificationCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/OrderNotificationCommandHandler.cs @@ -43,11 +43,11 @@ public async Task Handle(OrderNotificationCommand request, CancellationToken can try { - if (request.WorkContext.OriginalCustomerIfImpersonated != null) + if (request.OriginalCustomerIfImpersonated != null) //this order is placed by a store administrator impersonating a customer await _orderService.InsertOrderNote(new OrderNote { Note = - $"Order placed by a store owner ('{request.WorkContext.OriginalCustomerIfImpersonated.Email}'. ID = {request.WorkContext.OriginalCustomerIfImpersonated.Id}) impersonating the customer.", + $"Order placed by a store owner ('{request.OriginalCustomerIfImpersonated.Email}'. ID = {request.OriginalCustomerIfImpersonated.Id}) impersonating the customer.", DisplayToCustomer = false, OrderId = request.Order.Id }); @@ -59,7 +59,7 @@ await _orderService.InsertOrderNote(new OrderNote { }); //send email notifications - await _messageProviderService.SendOrderPlacedStoreOwnerMessage(request.Order, request.WorkContext.CurrentCustomer, + await _messageProviderService.SendOrderPlacedStoreOwnerMessage(request.Order, request.Customer, _languageSettings.DefaultAdminLanguageId); string orderPlacedAttachmentFilePath = string.Empty, orderPlacedAttachmentFileName = string.Empty; @@ -87,13 +87,13 @@ await _pdfService.SaveOrderToBinary(request.Order, request.Order.CustomerLanguag _logger.LogError(ex, "Error - order placed attachment file {OrderOrderNumber}", request.Order.OrderNumber); } - await _messageProviderService.SendOrderPlacedCustomerMessage(request.Order, request.WorkContext.CurrentCustomer, request.Order.CustomerLanguageId, orderPlacedAttachmentFilePath, orderPlacedAttachmentFileName, orderPlacedAttachments); + await _messageProviderService.SendOrderPlacedCustomerMessage(request.Order, request.Customer, request.Order.CustomerLanguageId, orderPlacedAttachmentFilePath, orderPlacedAttachmentFileName, orderPlacedAttachments); if (request.Order.OrderItems.Any(x => !string.IsNullOrEmpty(x.VendorId))) { var vendors = await _mediator.Send(new GetVendorsInOrderQuery { Order = request.Order }); foreach (var vendor in vendors) - await _messageProviderService.SendOrderPlacedVendorMessage(request.Order, request.WorkContext.CurrentCustomer, vendor, + await _messageProviderService.SendOrderPlacedVendorMessage(request.Order, request.Customer, vendor, _languageSettings.DefaultAdminLanguageId); } } diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs index b597c9759d..c9a2803099 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs @@ -25,7 +25,6 @@ using Grand.Domain.Shipping; using Grand.Domain.Tax; using Grand.Infrastructure.Configuration; -using Grand.Infrastructure; using Grand.SharedKernel; using Grand.SharedKernel.Extensions; using Grand.Mediator; @@ -67,7 +66,6 @@ public class PlaceOrderCommandHandler : IRequestHandler Handle(PlaceOrderCommand command, Cancellati var result = new PlaceOrderResult(); try { + ArgumentNullException.ThrowIfNull(command.Context); + //prepare order details - var details = await PreparePlaceOrderDetails(); + var details = await PreparePlaceOrderDetails(command); //event notification await _mediator.PlaceOrderDetailsEvent(result, details); @@ -181,7 +179,11 @@ await UpdatePaymentTransaction(processPayment.paymentTransaction, result.PlacedO #region Events & notes - await _mediator.Send(new OrderNotificationCommand { Order = result.PlacedOrder, WorkContext = _contextAccessor.WorkContext }, cancellationToken); + await _mediator.Send(new OrderNotificationCommand { + Order = result.PlacedOrder, + Customer = details.Customer, + OriginalCustomerIfImpersonated = command.OriginalCustomerIfImpersonated + }, cancellationToken); //check order status await _mediator.Send(new CheckOrderStatusCommand { Order = result.PlacedOrder }, cancellationToken); @@ -270,7 +272,7 @@ private async Task PreparePaymentTransaction(PlaceOrderConta var paymentTransaction = new PaymentTransaction(); var paymentTransactionId = details.Customer.GetUserFieldFromEntity(SystemCustomerFieldNames.PaymentTransaction, - _contextAccessor.StoreContext.CurrentStore.Id); + details.Store.Id); if (!string.IsNullOrEmpty(paymentTransactionId)) { paymentTransaction = await _paymentTransactionService.GetById(paymentTransactionId); @@ -288,7 +290,7 @@ private async Task PreparePaymentTransaction(PlaceOrderConta paymentTransaction.OrderGuid = paymentTransaction.OrderGuid == Guid.Empty ? Guid.NewGuid() : paymentTransaction.OrderGuid; paymentTransaction.PaymentMethodSystemName = details.PaymentMethodSystemName; - paymentTransaction.StoreId = _contextAccessor.StoreContext.CurrentStore.Id; + paymentTransaction.StoreId = details.Store.Id; paymentTransaction.CustomerId = details.Customer.Id; paymentTransaction.TransactionAmount = details.OrderTotal; paymentTransaction.CurrencyRate = details.CurrencyRate; @@ -377,16 +379,12 @@ private async Task GetShoppingCartItemWeight(ShoppingCartItem shoppingCa return weight; } - //TODO M1 PR 3: build the calculation context from PlaceOrderContainer instead of the ambient context - private CalculationContext AmbientCalculationContext => - new(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingCurrency, _contextAccessor.WorkContext.TaxDisplayType); - - protected virtual async Task PreparePlaceOrderDetails() + protected virtual async Task PreparePlaceOrderDetails(PlaceOrderCommand command) { var details = new PlaceOrderContainer { - Customer = _contextAccessor.WorkContext.CurrentCustomer, - Store = _contextAccessor.StoreContext.CurrentStore, + Context = command.Context, + Customer = command.Context.Customer, + Store = command.Context.Store }; if (details.Customer == null) throw new ArgumentException("Customer is not set"); @@ -413,8 +411,8 @@ protected virtual async Task PreparePlaceOrderDetails() //customer currency var currencyTmp = await _currencyService.GetCurrencyById( details.Customer.GetUserFieldFromEntity(SystemCustomerFieldNames.CurrencyId, - _contextAccessor.StoreContext.CurrentStore.Id)); - var customerCurrency = currencyTmp is { Published: true } ? currencyTmp : _contextAccessor.WorkContext.WorkingCurrency; + details.Store.Id)); + var customerCurrency = currencyTmp is { Published: true } ? currencyTmp : details.Context.Currency; details.Currency = customerCurrency; var primaryStoreCurrency = await _currencyService.GetPrimaryStoreCurrency(); details.CurrencyRate = Math.Round(customerCurrency.Rate / primaryStoreCurrency.Rate, 6); @@ -423,10 +421,10 @@ protected virtual async Task PreparePlaceOrderDetails() //customer language details.Language = await _languageService.GetLanguageById( details.Customer.GetUserFieldFromEntity(SystemCustomerFieldNames.LanguageId, - _contextAccessor.StoreContext.CurrentStore.Id)); + details.Store.Id)); if (details.Language is not { Published: true }) - details.Language = _contextAccessor.WorkContext.WorkingLanguage; + details.Language = command.Language; details.BillingAddress = details.Customer.BillingAddress; if (!string.IsNullOrEmpty(details.BillingAddress.CountryId)) @@ -439,15 +437,15 @@ protected virtual async Task PreparePlaceOrderDetails() //checkout attributes details.CheckoutAttributes = details.Customer.GetUserFieldFromEntity>(SystemCustomerFieldNames.CheckoutAttributes, - _contextAccessor.StoreContext.CurrentStore.Id); + details.Store.Id); details.CheckoutAttributeDescription = - await _checkoutAttributeFormatter.FormatAttributes(details.CheckoutAttributes, AmbientCalculationContext, - _contextAccessor.WorkContext.WorkingLanguage, _contextAccessor.StoreContext.CurrentHost); + await _checkoutAttributeFormatter.FormatAttributes(details.CheckoutAttributes, details.Context, + command.Language, command.Host); //load and validate customer shopping cart details.Cart = details.Customer.ShoppingCartItems .Where(sci => sci.ShoppingCartTypeId is ShoppingCartType.ShoppingCart or ShoppingCartType.Auctions) - .LimitPerStore(_shoppingCartSettings.SharedCartBetweenStores, _contextAccessor.StoreContext.CurrentStore.Id) + .LimitPerStore(_shoppingCartSettings.SharedCartBetweenStores, details.Store.Id) .ToList(); if (!details.Cart.Any()) @@ -455,7 +453,7 @@ await _checkoutAttributeFormatter.FormatAttributes(details.CheckoutAttributes, A //validate the entire shopping cart var warnings = - await _shoppingCartValidator.GetShoppingCartWarnings(details.Cart, details.CheckoutAttributes, AmbientCalculationContext, + await _shoppingCartValidator.GetShoppingCartWarnings(details.Cart, details.CheckoutAttributes, details.Context, true, true); if (warnings.Any()) throw new GrandException(string.Join(", ", warnings)); @@ -488,13 +486,13 @@ await _shoppingCartValidator.GetShoppingCartWarnings(details.Cart, details.Check if (_taxSettings.AllowCustomersToSelectTaxDisplayType) details.TaxDisplayType = (TaxDisplayType)details.Customer.GetUserFieldFromEntity(SystemCustomerFieldNames.TaxDisplayTypeId, - _contextAccessor.StoreContext.CurrentStore.Id); + details.Store.Id); else details.TaxDisplayType = _taxSettings.TaxDisplayType; //sub total //sub total (incl tax) - var shoppingCartSubTotal = await _orderTotalCalculationService.GetShoppingCartSubTotal(details.Cart, AmbientCalculationContext, true); + var shoppingCartSubTotal = await _orderTotalCalculationService.GetShoppingCartSubTotal(details.Cart, details.Context, true); var orderSubTotalDiscountAmount = shoppingCartSubTotal.discountAmount; var orderSubTotalAppliedDiscounts = shoppingCartSubTotal.appliedDiscounts; var subTotalWithoutDiscountBase = shoppingCartSubTotal.subTotalWithoutDiscount; @@ -507,7 +505,7 @@ await _shoppingCartValidator.GetShoppingCartWarnings(details.Cart, details.Check details.AppliedDiscounts.Add(disc); //sub total (excl tax) - shoppingCartSubTotal = await _orderTotalCalculationService.GetShoppingCartSubTotal(details.Cart, AmbientCalculationContext, false); + shoppingCartSubTotal = await _orderTotalCalculationService.GetShoppingCartSubTotal(details.Cart, details.Context, false); orderSubTotalDiscountAmount = shoppingCartSubTotal.discountAmount; subTotalWithoutDiscountBase = shoppingCartSubTotal.subTotalWithoutDiscount; @@ -521,7 +519,7 @@ await _shoppingCartValidator.GetShoppingCartWarnings(details.Cart, details.Check { var pickupPoint = details.Customer.GetUserFieldFromEntity(SystemCustomerFieldNames.SelectedPickupPoint, - _contextAccessor.StoreContext.CurrentStore.Id); + details.Store.Id); if (_shippingSettings.AllowPickUpInStore && pickupPoint != null) { details.PickUpInStore = true; @@ -547,7 +545,7 @@ await _shoppingCartValidator.GetShoppingCartWarnings(details.Cart, details.Check var shippingOption = details.Customer.GetUserFieldFromEntity(SystemCustomerFieldNames.SelectedShippingOption, - _contextAccessor.StoreContext.CurrentStore.Id); + details.Store.Id); if (shippingOption != null) { details.ShippingMethodName = shippingOption.Name; @@ -562,9 +560,9 @@ await _shoppingCartValidator.GetShoppingCartWarnings(details.Cart, details.Check //shipping total var (orderShippingTotalInclTax, _, shippingTotalDiscounts) = - await _orderTotalCalculationService.GetShoppingCartShippingTotal(details.Cart, AmbientCalculationContext, true); + await _orderTotalCalculationService.GetShoppingCartShippingTotal(details.Cart, details.Context, true); var orderShippingTotalExclTax = - (await _orderTotalCalculationService.GetShoppingCartShippingTotal(details.Cart, AmbientCalculationContext, false)) + (await _orderTotalCalculationService.GetShoppingCartShippingTotal(details.Cart, details.Context, false)) .shoppingCartShippingTotal; if (!orderShippingTotalInclTax.HasValue || !orderShippingTotalExclTax.HasValue) throw new GrandException("Shipping total couldn't be calculated"); @@ -577,8 +575,8 @@ await _shoppingCartValidator.GetShoppingCartWarnings(details.Cart, details.Check //payment var paymentMethodSystemName = - _contextAccessor.WorkContext.CurrentCustomer.GetUserFieldFromEntity(SystemCustomerFieldNames.SelectedPaymentMethod, - _contextAccessor.StoreContext.CurrentStore.Id); + details.Customer.GetUserFieldFromEntity(SystemCustomerFieldNames.SelectedPaymentMethod, + details.Store.Id); details.PaymentMethodSystemName = paymentMethodSystemName; var paymentAdditionalFee = await _paymentService.GetAdditionalHandlingFee(details.Cart, paymentMethodSystemName); @@ -591,7 +589,7 @@ await _shoppingCartValidator.GetShoppingCartWarnings(details.Cart, details.Check //tax total //tax amount - var (taxTotal, taxRates) = await _orderTotalCalculationService.GetTaxTotal(details.Cart, AmbientCalculationContext); + var (taxTotal, taxRates) = await _orderTotalCalculationService.GetTaxTotal(details.Cart, details.Context); details.OrderTaxTotal = taxTotal; //tax rates @@ -603,7 +601,7 @@ await _shoppingCartValidator.GetShoppingCartWarnings(details.Cart, details.Check //order total (and applied discounts, gift vouchers, loyalty points) var (orderTotal, orderDiscountAmount, orderAppliedDiscounts, appliedGiftVouchers, redeemedLoyaltyPoints, - redeemedLoyaltyPointsAmount) = await _orderTotalCalculationService.GetShoppingCartTotal(details.Cart, AmbientCalculationContext); + redeemedLoyaltyPointsAmount) = await _orderTotalCalculationService.GetShoppingCartTotal(details.Cart, details.Context); if (!orderTotal.HasValue) throw new GrandException("Order total couldn't be calculated"); @@ -625,15 +623,15 @@ protected virtual async Task PrepareOrderItem(ShoppingCartItem sc, Pr PlaceOrderContainer details) { var scUnitPrice = (await _pricingService.GetUnitPrice(sc, product, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingCurrency)).unitprice; + details.Customer, details.Store, + details.Context.Currency)).unitprice; var scUnitPriceWithoutDisc = (await _pricingService.GetUnitPrice(sc, product, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingCurrency, false)).unitprice; + details.Customer, details.Store, + details.Context.Currency, false)).unitprice; var (scSubTotal, discountAmount, scDiscounts) = await _pricingService.GetSubTotal(sc, product, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingCurrency); + details.Customer, details.Store, + details.Context.Currency); var prices = await _taxService.GetTaxProductPrice(product, details.Customer, details.Store, scUnitPrice, @@ -655,7 +653,7 @@ protected virtual async Task PrepareOrderItem(ShoppingCartItem sc, Pr var itemWeight = await GetShoppingCartItemWeight(sc); var warehouseId = !string.IsNullOrEmpty(sc.WarehouseId) ? sc.WarehouseId - : _contextAccessor.StoreContext.CurrentStore.DefaultWarehouseId; + : details.Store.DefaultWarehouseId; if (!product.UseMultipleWarehouses && string.IsNullOrEmpty(warehouseId)) if (!string.IsNullOrEmpty(product.WarehouseId)) warehouseId = product.WarehouseId; @@ -752,7 +750,7 @@ protected virtual async Task GenerateGiftVoucher(PlaceOrderContainer details, Sh SenderEmail = giftVoucherSenderEmail, Message = giftVoucherMessage, IsRecipientNotified = false, - StoreId = _orderSettings.GiftVouchers_Assign_StoreId ? _contextAccessor.StoreContext.CurrentStore.Id : string.Empty + StoreId = _orderSettings.GiftVouchers_Assign_StoreId ? details.Store.Id : string.Empty }; await _giftVoucherService.InsertGiftVoucher(gc); } diff --git a/src/Business/Grand.Business.Core/Commands/Checkout/Orders/PlaceOrderCommand.cs b/src/Business/Grand.Business.Core/Commands/Checkout/Orders/PlaceOrderCommand.cs index 057eaf52d2..4a789305b6 100644 --- a/src/Business/Grand.Business.Core/Commands/Checkout/Orders/PlaceOrderCommand.cs +++ b/src/Business/Grand.Business.Core/Commands/Checkout/Orders/PlaceOrderCommand.cs @@ -1,6 +1,30 @@ using Grand.Business.Core.Utilities.Checkout; +using Grand.Domain.Customers; +using Grand.Domain.Localization; +using Grand.Domain.Stores; using Grand.Mediator; namespace Grand.Business.Core.Commands.Checkout.Orders; -public class PlaceOrderCommand : IRequest; \ No newline at end of file +public class PlaceOrderCommand : IRequest +{ + /// + /// Customer, store, working currency and tax display the order is calculated for (required) + /// + public CalculationContext Context { get; set; } + + /// + /// Language used when the customer has no published language of their own + /// + public Language Language { get; set; } + + /// + /// Store address used for file download links in the checkout attribute description + /// + public DomainHost Host { get; set; } + + /// + /// Staff member who places the order while impersonating the customer, if any + /// + public Customer OriginalCustomerIfImpersonated { get; set; } +} diff --git a/src/Business/Grand.Business.Core/Commands/Messages/Common/OrderNotificationCommand.cs b/src/Business/Grand.Business.Core/Commands/Messages/Common/OrderNotificationCommand.cs index c079e614bc..e451bac685 100644 --- a/src/Business/Grand.Business.Core/Commands/Messages/Common/OrderNotificationCommand.cs +++ b/src/Business/Grand.Business.Core/Commands/Messages/Common/OrderNotificationCommand.cs @@ -1,5 +1,5 @@ -using Grand.Domain.Orders; -using Grand.Infrastructure; +using Grand.Domain.Customers; +using Grand.Domain.Orders; using Grand.Mediator; namespace Grand.Business.Core.Commands.Messages.Common; @@ -7,5 +7,14 @@ namespace Grand.Business.Core.Commands.Messages.Common; public class OrderNotificationCommand : IRequest { public Order Order { get; set; } - public IWorkContext WorkContext { get; set; } + + /// + /// Customer the order was placed for + /// + public Customer Customer { get; set; } + + /// + /// Staff member who placed the order while impersonating the customer, if any + /// + public Customer OriginalCustomerIfImpersonated { get; set; } } diff --git a/src/Business/Grand.Business.Core/Utilities/Checkout/PlaceOrderContainer.cs b/src/Business/Grand.Business.Core/Utilities/Checkout/PlaceOrderContainer.cs index 5c6849cbfc..eb6bff6aa8 100644 --- a/src/Business/Grand.Business.Core/Utilities/Checkout/PlaceOrderContainer.cs +++ b/src/Business/Grand.Business.Core/Utilities/Checkout/PlaceOrderContainer.cs @@ -13,6 +13,11 @@ namespace Grand.Business.Core.Utilities.Checkout; public class PlaceOrderContainer { + /// + /// Customer, store, working currency and tax display the order totals are calculated for + /// + public CalculationContext Context { get; set; } + public Customer Customer { get; set; } public Store Store { get; set; } public Language Language { get; set; } diff --git a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/OrderNotificationCommandHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/OrderNotificationCommandHandlerTests.cs new file mode 100644 index 0000000000..fc0213c050 --- /dev/null +++ b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/OrderNotificationCommandHandlerTests.cs @@ -0,0 +1,63 @@ +using Grand.Business.Checkout.Commands.Handlers.Orders; +using Grand.Business.Core.Commands.Messages.Common; +using Grand.Business.Core.Interfaces.Checkout.Orders; +using Grand.Business.Core.Interfaces.Common.Pdf; +using Grand.Business.Core.Interfaces.Messages; +using Grand.Domain.Customers; +using Grand.Domain.Localization; +using Grand.Domain.Orders; +using Grand.Mediator; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Business.Checkout.Tests.Commands.Handlers.Orders; + +[TestClass] +public class OrderNotificationCommandHandlerTests +{ + private OrderNotificationCommandHandler _handler; + private Mock _messageProviderServiceMock; + private Mock _orderServiceMock; + + [TestInitialize] + public void Init() + { + _orderServiceMock = new Mock(); + _messageProviderServiceMock = new Mock(); + _handler = new OrderNotificationCommandHandler(_orderServiceMock.Object, _messageProviderServiceMock.Object, + new OrderSettings(), new LanguageSettings(), new Mock().Object, + new Mock>().Object, new Mock().Object); + } + + [TestMethod] + public async Task Handle_AddsImpersonationNoteForTheOriginalCustomer() + { + //Arrange + var staff = new Customer { Email = "staff@shop.example" }; + + //Act + await _handler.Handle(new OrderNotificationCommand { + Order = new Order { Id = "order" }, Customer = new Customer(), OriginalCustomerIfImpersonated = staff + }, CancellationToken.None); + + //Assert + _orderServiceMock.Verify(x => x.InsertOrderNote(It.Is(n => + n.OrderId == "order" && n.Note.Contains("staff@shop.example") && n.Note.Contains("impersonating"))), Times.Once); + } + + [TestMethod] + public async Task Handle_SendsTheCustomerMessageForTheCommandCustomer() + { + //Arrange + var customer = new Customer(); + var order = new Order { Id = "order", CustomerLanguageId = "en" }; + + //Act + await _handler.Handle(new OrderNotificationCommand { Order = order, Customer = customer }, CancellationToken.None); + + //Assert + _messageProviderServiceMock.Verify(x => x.SendOrderPlacedCustomerMessage(order, customer, "en", + It.IsAny(), It.IsAny(), It.IsAny>()), Times.Once); + } +} diff --git a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/PlaceOrderCommandHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/PlaceOrderCommandHandlerTests.cs new file mode 100644 index 0000000000..01da9d3266 --- /dev/null +++ b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/PlaceOrderCommandHandlerTests.cs @@ -0,0 +1,94 @@ +using Grand.Business.Checkout.Commands.Handlers.Orders; +using Grand.Business.Core.Commands.Checkout.Orders; +using Grand.Business.Core.Interfaces.Catalog.Discounts; +using Grand.Business.Core.Interfaces.Catalog.Prices; +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Interfaces.Catalog.Tax; +using Grand.Business.Core.Interfaces.Checkout.CheckoutAttributes; +using Grand.Business.Core.Interfaces.Checkout.GiftVouchers; +using Grand.Business.Core.Interfaces.Checkout.Orders; +using Grand.Business.Core.Interfaces.Checkout.Payments; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Business.Core.Interfaces.Messages; +using Grand.Business.Core.Utilities.Checkout; +using Grand.Domain.Common; +using Grand.Domain.Customers; +using Grand.Domain.Directory; +using Grand.Domain.Localization; +using Grand.Domain.Orders; +using Grand.Domain.Payments; +using Grand.Domain.Shipping; +using Grand.Domain.Stores; +using Grand.Domain.Tax; +using Grand.Infrastructure.Configuration; +using Grand.Mediator; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Business.Checkout.Tests.Commands.Handlers.Orders; + +[TestClass] +public class PlaceOrderCommandHandlerTests +{ + private Mock _checkoutAttributeFormatterMock; + private PlaceOrderCommandHandler _handler; + + [TestInitialize] + public void Init() + { + _checkoutAttributeFormatterMock = new Mock(); + var currencyService = new Mock(); + currencyService.Setup(x => x.GetPrimaryStoreCurrency()).ReturnsAsync(new Currency { CurrencyCode = "USD", Rate = 1 }); + + _handler = new PlaceOrderCommandHandler(new Mock().Object, new Mock().Object, + new Mock().Object, new Mock().Object, + new Mock().Object, new Mock().Object, + new Mock>().Object, new Mock().Object, + new Mock().Object, new Mock().Object, + new Mock().Object, _checkoutAttributeFormatterMock.Object, + new Mock().Object, new Mock().Object, new Mock().Object, + new Mock().Object, new Mock().Object, + new Mock().Object, new Mock().Object, currencyService.Object, + new Mock().Object, new Mock().Object, + new Mock().Object, new Mock().Object, + new Mock().Object, new Mock().Object, new ShippingSettings(), + new ShoppingCartSettings(), new PaymentSettings(), new OrderSettings(), new OidcLoginConfig(), + new TaxSettings()); + } + + [TestMethod] + public async Task Handle_WithoutContext_ReturnsError() + { + //Act + var result = await _handler.Handle(new PlaceOrderCommand(), CancellationToken.None); + + //Assert + Assert.IsNotEmpty(result.Errors); + Assert.IsNull(result.PlacedOrder); + } + + [TestMethod] + public async Task Handle_PreparesDetailsFromTheCommand() + { + //Arrange + var customer = new Customer { BillingAddress = new Address { Email = "buyer@shop.example" } }; + var command = new PlaceOrderCommand { + Context = new CalculationContext(customer, new Store { Id = "store" }, new Currency { CurrencyCode = "EUR", Rate = 0.95 }, + TaxDisplayType.IncludingTax), + Language = new Language { Id = "pl" }, + Host = new DomainHost { Url = "https://shop.example/" } + }; + + //Act + var result = await _handler.Handle(command, CancellationToken.None); + + //Assert + _checkoutAttributeFormatterMock.Verify(x => x.FormatAttributes(It.IsAny>(), command.Context, + command.Language, command.Host, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + Assert.IsTrue(result.Errors.Any(e => e.Contains("Cart is empty"))); + } +} diff --git a/src/Web/Grand.Web/Controllers/CheckoutController.cs b/src/Web/Grand.Web/Controllers/CheckoutController.cs index 748ca3ef46..d864159562 100644 --- a/src/Web/Grand.Web/Controllers/CheckoutController.cs +++ b/src/Web/Grand.Web/Controllers/CheckoutController.cs @@ -720,7 +720,12 @@ public virtual async Task ConfirmOrder() error = 1, message = _translationService.GetResource("Checkout.MinOrderPlacementInterval") }); - var placeOrderResult = await _mediator.Send(new PlaceOrderCommand()); + var placeOrderResult = await _mediator.Send(new PlaceOrderCommand { + Context = _contextAccessor.CalculationContext(), + Language = _contextAccessor.WorkContext.WorkingLanguage, + Host = _contextAccessor.StoreContext.CurrentHost, + OriginalCustomerIfImpersonated = _contextAccessor.WorkContext.OriginalCustomerIfImpersonated + }); if (placeOrderResult.Success) { var paymentMethod = From 15f6bf072079a4c641f209bd33e105aa5e358687 Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 15:28:57 +0200 Subject: [PATCH 19/33] Build the cart page context from its request GetShoppingCartHandler priced, taxed and validated the cart with the current request's values although its request carries the same customer, store, currency, tax display and language. It now uses the request; every sender fills it from the current request, so the page is unchanged. Only the store host for file links still comes from the request context. --- .../ShoppingCart/GetShoppingCartHandler.cs | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs index 729c7a3c36..4885f25503 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs @@ -145,6 +145,9 @@ public async Task Handle(GetShoppingCart request, Cancellatio return model; } + private static CalculationContext CalculationContextOf(GetShoppingCart request) => + new(request.Customer, request.Store, request.Currency, request.TaxDisplayType); + private async Task PrepareSimpleProperties(ShoppingCartModel model, GetShoppingCart request) { #region Simple properties @@ -160,10 +163,8 @@ private async Task PrepareSimpleProperties(ShoppingCartModel model, GetShoppingC request.Customer.GetUserFieldFromEntity>(SystemCustomerFieldNames.CheckoutAttributes, request.Store.Id); model.CheckoutAttributeInfo = - await _checkoutAttributeFormatter.FormatAttributes(checkoutAttributes, - new CalculationContext(request.Customer, request.Store, _contextAccessor.WorkContext.WorkingCurrency, - _contextAccessor.WorkContext.TaxDisplayType), - _contextAccessor.WorkContext.WorkingLanguage, _contextAccessor.StoreContext.CurrentHost); + await _checkoutAttributeFormatter.FormatAttributes(checkoutAttributes, CalculationContextOf(request), + request.Language, _contextAccessor.StoreContext.CurrentHost); if (!request.Cart.Where(x => x.ShoppingCartTypeId is ShoppingCartType.ShoppingCart or ShoppingCartType.Auctions) .ToList().Any()) { @@ -172,7 +173,7 @@ await _checkoutAttributeFormatter.FormatAttributes(checkoutAttributes, else { var minOrderSubtotalAmountOk = await _mediator.Send(new ValidateMinShoppingCartSubtotalAmountCommand { - Context = _contextAccessor.CalculationContext(), + Context = CalculationContextOf(request), Customer = request.Customer, Cart = request.Cart.Where(x => x.ShoppingCartTypeId is ShoppingCartType.ShoppingCart or ShoppingCartType.Auctions).ToList() @@ -197,8 +198,7 @@ await _currencyService.ConvertFromPrimaryStoreCurrency(_orderSettings.MinOrderSu { var discount = await _discountService.GetDiscountByCouponCode(couponCode); if (discount is { RequiresCouponCode: true } && - (await _discountValidationService.ValidateDiscount(discount, request.Customer, request.Store, - request.Currency)).IsValid) + (await _discountValidationService.ValidateDiscount(discount, request.Customer, request.Store, request.Currency)).IsValid) model.DiscountBox.AppliedDiscountsWithCodes.Add(new ShoppingCartModel.DiscountBoxModel.DiscountInfoModel { Id = discount.Id, CouponCode = couponCode @@ -208,7 +208,8 @@ await _currencyService.ConvertFromPrimaryStoreCurrency(_orderSettings.MinOrderSu model.GiftVoucherBox.Display = _shoppingCartSettings.ShowGiftVoucherBox; //cart warnings - var cartWarnings = await _shoppingCartValidator.GetShoppingCartWarnings(request.Cart, checkoutAttributes, _contextAccessor.CalculationContext(), + var cartWarnings = await _shoppingCartValidator.GetShoppingCartWarnings(request.Cart, checkoutAttributes, + CalculationContextOf(request), request.ValidateCheckoutAttributes, true); foreach (var warning in cartWarnings) model.Warnings.Add(warning); @@ -255,8 +256,8 @@ await _checkoutAttributeService.GetAllCheckoutAttributes(request.Store.Id, reque //display price if allowed if (!await _permissionService.Authorize(StandardPermission.DisplayPrices)) continue; var priceAdjustmentBase = (await _taxService.GetCheckoutAttributePrice(attribute, attributeValue, - _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)) + request.TaxDisplayType == TaxDisplayType.IncludingTax, + request.Customer, request.Store)) .checkoutPrice; var priceAdjustment = await _currencyService.ConvertFromPrimaryStoreCurrency(priceAdjustmentBase, request.Currency); @@ -455,20 +456,18 @@ private async Task PrepareCartItems(ShoppingCartModel model, GetShoppingCart req else { var unitprices = await _pricingService.GetUnitPrice(sci, product, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingCurrency); + request.Customer, request.Store, request.Currency); var appliedDiscounts = unitprices.appliedDiscounts; var productprices = await _taxService.GetProductPrice(product, unitprices.unitprice, - _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore); + request.TaxDisplayType == TaxDisplayType.IncludingTax, + request.Customer, request.Store); cartItemModel.UnitPriceWithoutDiscountValue = (await _taxService.GetProductPrice(product, (await _pricingService.GetUnitPrice(sci, product, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingCurrency, false)).unitprice, - _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice; + request.Customer, request.Store, request.Currency, false)).unitprice, + request.TaxDisplayType == TaxDisplayType.IncludingTax, + request.Customer, request.Store)).productprice; cartItemModel.UnitPriceWithoutDiscount = _priceFormatter.FormatPrice(cartItemModel.UnitPriceWithoutDiscountValue, request.Currency); @@ -485,13 +484,12 @@ private async Task PrepareCartItems(ShoppingCartModel model, GetShoppingCart req //sub total var subtotal = await _pricingService.GetSubTotal(sci, product, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingCurrency); + request.Customer, request.Store, request.Currency); var shoppingCartItemDiscountBase = subtotal.discountAmount; var shoppingCartItemSubTotalWithDiscount = (await _taxService.GetProductPrice(product, subtotal.subTotal, - _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice; + request.TaxDisplayType == TaxDisplayType.IncludingTax, + request.Customer, request.Store)).productprice; cartItemModel.SubTotal = _priceFormatter.FormatPrice(shoppingCartItemSubTotalWithDiscount, request.Currency); cartItemModel.SubTotalValue = shoppingCartItemSubTotalWithDiscount; @@ -501,8 +499,8 @@ private async Task PrepareCartItems(ShoppingCartModel model, GetShoppingCart req { shoppingCartItemDiscountBase = (await _taxService.GetProductPrice(product, shoppingCartItemDiscountBase, - _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice; + request.TaxDisplayType == TaxDisplayType.IncludingTax, + request.Customer, request.Store)).productprice; if (shoppingCartItemDiscountBase > 0) cartItemModel.Discount = _priceFormatter.FormatPrice(shoppingCartItemDiscountBase, request.Currency); From f6dd5ba193e625b0ccf85f088c2474bb68fc61eb Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 15:51:19 +0200 Subject: [PATCH 20/33] Keep required products when merging the cart at login The required-product check now looks at the cart of the customer it is given instead of the request's customer. During the login merge that is the account being filled, which does not hold the guest's remaining items yet, so a product whose required product came later in the guest cart was skipped and then deleted with the guest cart. The merge moves the whole cart together and skips that check, which matches the old outcome where the check saw the guest's full cart. --- .../CheckoutAttributeService.cs | 2 +- .../Services/Orders/ShoppingCartService.cs | 5 ++-- .../ICheckoutAttributeService.cs | 2 +- .../Orders/ShoppingCartServiceTests.cs | 27 +++++++++++++++++++ 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeService.cs b/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeService.cs index a8771d8bc7..27929ba88a 100644 --- a/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeService.cs +++ b/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeService.cs @@ -49,7 +49,7 @@ public CheckoutAttributeService( /// Gets all checkout attributes /// /// Store identifier - /// Customer whose groups limit the result (not read when ignoreAcl is true) + /// Customer whose groups limit the result (not read when ignoreAcl or AccessControlConfig.IgnoreAcl is set) /// A value indicating whether we should exclude shippable attributes /// /// Checkout attributes diff --git a/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartService.cs b/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartService.cs index 59e0292409..273cd2b68d 100644 --- a/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartService.cs +++ b/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartService.cs @@ -406,14 +406,15 @@ public virtual async Task MigrateShoppingCart(Customer fromCustomer, Customer to if (fromCustomer.Id == toCustomer.Id) return; //the same customer - //shopping cart items + //shopping cart items; the whole cart moves together, so a product's required products + //may not be in the target cart yet when the product itself is added var fromCart = fromCustomer.ShoppingCartItems.ToList(); foreach (var sci in fromCart) await AddToCart(toCustomer, sci.ProductId, sci.ShoppingCartTypeId, sci.StoreId, sci.WarehouseId, sci.Attributes, sci.EnteredPrice, sci.RentalStartDateUtc, sci.RentalEndDateUtc, sci.Quantity, false, sci.ReservationId, sci.Parameter, sci.Duration, - new ShoppingCartValidatorOptions()); + new ShoppingCartValidatorOptions { GetRequiredProductWarnings = false }); foreach (var sci in fromCart) await DeleteShoppingCartItem(fromCustomer, sci); //copy discount and gift voucher coupon codes diff --git a/src/Business/Grand.Business.Core/Interfaces/Checkout/CheckoutAttributes/ICheckoutAttributeService.cs b/src/Business/Grand.Business.Core/Interfaces/Checkout/CheckoutAttributes/ICheckoutAttributeService.cs index 4fcb731ee8..23c4814b2d 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Checkout/CheckoutAttributes/ICheckoutAttributeService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Checkout/CheckoutAttributes/ICheckoutAttributeService.cs @@ -14,7 +14,7 @@ public interface ICheckoutAttributeService /// Gets all checkout attributes /// /// Store identifier - /// Customer whose groups limit the result (not read when ignoreAcl is true) + /// Customer whose groups limit the result (not read when ignoreAcl or AccessControlConfig.IgnoreAcl is set) /// A value indicating whether we should exclude shippable attributes /// /// Checkout attributes diff --git a/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartServiceTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartServiceTests.cs index bbccb4d2a4..8376c7708c 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartServiceTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartServiceTests.cs @@ -222,6 +222,33 @@ public async Task GetShoppingCart_ReturnsTheItemsOfTheGivenCustomer() Assert.AreSame(customer.ShoppingCartItems.First(), result[0]); } + [TestMethod] + public async Task MigrateShoppingCart_DoesNotRequireRequiredProductsInTheTargetCartYet() + { + //Arrange + var guest = new Customer(); + guest.ShoppingCartItems.Add(new ShoppingCartItem + { ProductId = "A", ShoppingCartTypeId = ShoppingCartType.ShoppingCart, StoreId = "", Quantity = 1 }); + var customer = new Customer(); + _productServiceMock.Setup(a => a.GetProductById(It.IsAny(), false)) + .ReturnsAsync(new Product { Id = "A", Published = true }); + _shoppingCartValidatorMock.Setup(x => x.CheckCommonWarnings(It.IsAny(), + It.IsAny>(), It.IsAny(), It.IsAny(), null, null, + It.IsAny(), It.IsAny())) + .ReturnsAsync(new List()); + _shoppingCartValidatorMock.Setup(x => x.GetShoppingCartItemWarnings(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new List()); + + //Act + await _shoppingCartService.MigrateShoppingCart(guest, customer, "", false); + + //Assert + //the guest's whole cart moves together, so a required product may still be on its way to the target cart + _shoppingCartValidatorMock.Verify(x => x.GetShoppingCartItemWarnings(customer, It.IsAny(), + It.IsAny(), It.Is(o => !o.GetRequiredProductWarnings)), Times.Once); + } + [TestMethod] public async Task MigrateShoppingCart_MovesCheckoutAttributesOfTheGivenStore() { From 43a1ec61a472676b8062ccef350c2155abe060ca Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 16:36:10 +0200 Subject: [PATCH 21/33] Derive recommended products customer groups from the query customer --- .../Handlers/GetRecommendedProductsQueryHandler.cs | 6 ++++-- .../Queries/Catalog/GetRecommendedProductsQuery.cs | 1 - .../GetRecommendedProductsQueryHandlerTests.cs | 13 ++++++++----- .../Components/CustomerRecommendedProducts.cs | 2 -- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetRecommendedProductsQueryHandler.cs b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetRecommendedProductsQueryHandler.cs index 8c27a87b75..7a42935889 100644 --- a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetRecommendedProductsQueryHandler.cs +++ b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetRecommendedProductsQueryHandler.cs @@ -31,12 +31,14 @@ public async Task> Handle(GetRecommendedProductsQuery request, Ca //the result is cached per key shared by many customers, so a query without one must not fill it ArgumentNullException.ThrowIfNull(request.Customer); + var customerGroupIds = request.Customer.GetCustomerGroupIds(); + return await _cacheBase.GetAsync( - string.Format(CacheKey.PRODUCTS_CUSTOMER_GROUP, string.Join(",", request.CustomerGroupIds), + string.Format(CacheKey.PRODUCTS_CUSTOMER_GROUP, string.Join(",", customerGroupIds), request.StoreId), async () => { var query = from cr in _customerGroupProductRepository.Table - where request.CustomerGroupIds.Contains(cr.CustomerGroupId) + where customerGroupIds.Contains(cr.CustomerGroupId) orderby cr.DisplayOrder select cr.ProductId; diff --git a/src/Business/Grand.Business.Core/Queries/Catalog/GetRecommendedProductsQuery.cs b/src/Business/Grand.Business.Core/Queries/Catalog/GetRecommendedProductsQuery.cs index c16dd9edf6..1fb839a05c 100644 --- a/src/Business/Grand.Business.Core/Queries/Catalog/GetRecommendedProductsQuery.cs +++ b/src/Business/Grand.Business.Core/Queries/Catalog/GetRecommendedProductsQuery.cs @@ -6,7 +6,6 @@ namespace Grand.Business.Core.Queries.Catalog; public class GetRecommendedProductsQuery : IRequest> { - public string[] CustomerGroupIds { get; set; } public string StoreId { get; set; } public Customer Customer { get; set; } } \ No newline at end of file diff --git a/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetRecommendedProductsQueryHandlerTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetRecommendedProductsQueryHandlerTests.cs index a293452132..c08ee133fc 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetRecommendedProductsQueryHandlerTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetRecommendedProductsQueryHandlerTests.cs @@ -5,6 +5,7 @@ using Grand.Domain.Catalog; using Grand.Domain.Customers; using Grand.Infrastructure.Caching; +using Grand.Infrastructure.Caching.Constants; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -34,7 +35,6 @@ public void Init() public async Task HandleTest() { var getRecommendedProductsQuery = new GetRecommendedProductsQuery { - CustomerGroupIds = ["1"], StoreId = "1", Customer = new Customer() }; @@ -47,7 +47,7 @@ public async Task HandleTest() public async Task Handle_WithoutCustomer_ThrowsBeforeCaching() { await Assert.ThrowsExactlyAsync(() => - handler.Handle(new GetRecommendedProductsQuery { CustomerGroupIds = ["1"], StoreId = "1" }, + handler.Handle(new GetRecommendedProductsQuery { StoreId = "1" }, CancellationToken.None)); _casheManagerMock.Verify(c => c.GetAsync(It.IsAny(), It.IsAny>>>()), Times.Never); @@ -58,20 +58,23 @@ public async Task Handle_AuthorizesProductsAgainstCustomerAndStoreOfQuery() { //Arrange var customer = new Customer(); + customer.Groups.Add("1"); _casheManagerMock.Setup(c => c.GetAsync(It.IsAny(), It.IsAny>>>())) .Returns((string _, Func>> acquire) => acquire()); _customerGroupProductRepositoryMock.Setup(r => r.Table) .Returns(new List { new() { CustomerGroupId = "1", ProductId = "p1" } } .AsQueryable()); - _productServiceMock.Setup(p => p.GetProductsByIds(It.IsAny(), customer, "1", false)) + _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "p1" }, customer, "1", false)) .ReturnsAsync(new List()); //Act await handler.Handle(new GetRecommendedProductsQuery { - CustomerGroupIds = ["1"], StoreId = "1", Customer = customer + StoreId = "1", Customer = customer }, CancellationToken.None); //Assert - _productServiceMock.Verify(p => p.GetProductsByIds(It.IsAny(), customer, "1", false), Times.Once); + _casheManagerMock.Verify(c => c.GetAsync(string.Format(CacheKey.PRODUCTS_CUSTOMER_GROUP, "1", "1"), It.IsAny>>>()), + Times.Once); + _productServiceMock.Verify(p => p.GetProductsByIds(new[] { "p1" }, customer, "1", false), Times.Once); } } \ No newline at end of file diff --git a/src/Web/Grand.Web/Components/CustomerRecommendedProducts.cs b/src/Web/Grand.Web/Components/CustomerRecommendedProducts.cs index 371b6e1bdb..27b5b85540 100644 --- a/src/Web/Grand.Web/Components/CustomerRecommendedProducts.cs +++ b/src/Web/Grand.Web/Components/CustomerRecommendedProducts.cs @@ -1,6 +1,5 @@ using Grand.Business.Core.Queries.Catalog; using Grand.Domain.Catalog; -using Grand.Domain.Customers; using Grand.Infrastructure; using Grand.Web.Common.Components; using Grand.Web.Features.Models.Products; @@ -33,7 +32,6 @@ public async Task InvokeAsync(int? productThumbPictureSize return Content(""); var products = await _mediator.Send(new GetRecommendedProductsQuery { - CustomerGroupIds = _contextAccessor.WorkContext.CurrentCustomer.GetCustomerGroupIds(), StoreId = _contextAccessor.StoreContext.CurrentStore.Id, Customer = _contextAccessor.WorkContext.CurrentCustomer }); From 10d8f731c96730656e81ad39e21f9c6309de51b9 Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 16:36:10 +0200 Subject: [PATCH 22/33] Derive recommended products customer groups from the query customer --- .../Handlers/GetRecommendedProductsQueryHandler.cs | 6 ++++-- .../Queries/Catalog/GetRecommendedProductsQuery.cs | 1 - .../GetRecommendedProductsQueryHandlerTests.cs | 13 ++++++++----- .../Components/CustomerRecommendedProducts.cs | 2 -- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetRecommendedProductsQueryHandler.cs b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetRecommendedProductsQueryHandler.cs index 8c27a87b75..7a42935889 100644 --- a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetRecommendedProductsQueryHandler.cs +++ b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetRecommendedProductsQueryHandler.cs @@ -31,12 +31,14 @@ public async Task> Handle(GetRecommendedProductsQuery request, Ca //the result is cached per key shared by many customers, so a query without one must not fill it ArgumentNullException.ThrowIfNull(request.Customer); + var customerGroupIds = request.Customer.GetCustomerGroupIds(); + return await _cacheBase.GetAsync( - string.Format(CacheKey.PRODUCTS_CUSTOMER_GROUP, string.Join(",", request.CustomerGroupIds), + string.Format(CacheKey.PRODUCTS_CUSTOMER_GROUP, string.Join(",", customerGroupIds), request.StoreId), async () => { var query = from cr in _customerGroupProductRepository.Table - where request.CustomerGroupIds.Contains(cr.CustomerGroupId) + where customerGroupIds.Contains(cr.CustomerGroupId) orderby cr.DisplayOrder select cr.ProductId; diff --git a/src/Business/Grand.Business.Core/Queries/Catalog/GetRecommendedProductsQuery.cs b/src/Business/Grand.Business.Core/Queries/Catalog/GetRecommendedProductsQuery.cs index c16dd9edf6..1fb839a05c 100644 --- a/src/Business/Grand.Business.Core/Queries/Catalog/GetRecommendedProductsQuery.cs +++ b/src/Business/Grand.Business.Core/Queries/Catalog/GetRecommendedProductsQuery.cs @@ -6,7 +6,6 @@ namespace Grand.Business.Core.Queries.Catalog; public class GetRecommendedProductsQuery : IRequest> { - public string[] CustomerGroupIds { get; set; } public string StoreId { get; set; } public Customer Customer { get; set; } } \ No newline at end of file diff --git a/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetRecommendedProductsQueryHandlerTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetRecommendedProductsQueryHandlerTests.cs index a293452132..c08ee133fc 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetRecommendedProductsQueryHandlerTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetRecommendedProductsQueryHandlerTests.cs @@ -5,6 +5,7 @@ using Grand.Domain.Catalog; using Grand.Domain.Customers; using Grand.Infrastructure.Caching; +using Grand.Infrastructure.Caching.Constants; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -34,7 +35,6 @@ public void Init() public async Task HandleTest() { var getRecommendedProductsQuery = new GetRecommendedProductsQuery { - CustomerGroupIds = ["1"], StoreId = "1", Customer = new Customer() }; @@ -47,7 +47,7 @@ public async Task HandleTest() public async Task Handle_WithoutCustomer_ThrowsBeforeCaching() { await Assert.ThrowsExactlyAsync(() => - handler.Handle(new GetRecommendedProductsQuery { CustomerGroupIds = ["1"], StoreId = "1" }, + handler.Handle(new GetRecommendedProductsQuery { StoreId = "1" }, CancellationToken.None)); _casheManagerMock.Verify(c => c.GetAsync(It.IsAny(), It.IsAny>>>()), Times.Never); @@ -58,20 +58,23 @@ public async Task Handle_AuthorizesProductsAgainstCustomerAndStoreOfQuery() { //Arrange var customer = new Customer(); + customer.Groups.Add("1"); _casheManagerMock.Setup(c => c.GetAsync(It.IsAny(), It.IsAny>>>())) .Returns((string _, Func>> acquire) => acquire()); _customerGroupProductRepositoryMock.Setup(r => r.Table) .Returns(new List { new() { CustomerGroupId = "1", ProductId = "p1" } } .AsQueryable()); - _productServiceMock.Setup(p => p.GetProductsByIds(It.IsAny(), customer, "1", false)) + _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "p1" }, customer, "1", false)) .ReturnsAsync(new List()); //Act await handler.Handle(new GetRecommendedProductsQuery { - CustomerGroupIds = ["1"], StoreId = "1", Customer = customer + StoreId = "1", Customer = customer }, CancellationToken.None); //Assert - _productServiceMock.Verify(p => p.GetProductsByIds(It.IsAny(), customer, "1", false), Times.Once); + _casheManagerMock.Verify(c => c.GetAsync(string.Format(CacheKey.PRODUCTS_CUSTOMER_GROUP, "1", "1"), It.IsAny>>>()), + Times.Once); + _productServiceMock.Verify(p => p.GetProductsByIds(new[] { "p1" }, customer, "1", false), Times.Once); } } \ No newline at end of file diff --git a/src/Web/Grand.Web/Components/CustomerRecommendedProducts.cs b/src/Web/Grand.Web/Components/CustomerRecommendedProducts.cs index 371b6e1bdb..27b5b85540 100644 --- a/src/Web/Grand.Web/Components/CustomerRecommendedProducts.cs +++ b/src/Web/Grand.Web/Components/CustomerRecommendedProducts.cs @@ -1,6 +1,5 @@ using Grand.Business.Core.Queries.Catalog; using Grand.Domain.Catalog; -using Grand.Domain.Customers; using Grand.Infrastructure; using Grand.Web.Common.Components; using Grand.Web.Features.Models.Products; @@ -33,7 +32,6 @@ public async Task InvokeAsync(int? productThumbPictureSize return Content(""); var products = await _mediator.Send(new GetRecommendedProductsQuery { - CustomerGroupIds = _contextAccessor.WorkContext.CurrentCustomer.GetCustomerGroupIds(), StoreId = _contextAccessor.StoreContext.CurrentStore.Id, Customer = _contextAccessor.WorkContext.CurrentCustomer }); From 08983cec90332bcf2f3c62b0d196db4e791e1252 Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 18:02:29 +0200 Subject: [PATCH 23/33] Take customer details from the query customer and stop reading the customer for hidden records GetPersonalizedProductsQuery and GetSuggestedProductsQuery read the customer id and tags from Customer instead of separate fields that could disagree with it. The product mapping and child category lookups no longer read the customer or store when showHidden is set, so admin callers pass null. Methods that authorize through IAclService throw on a missing customer instead of returning nothing. --- .../GetPersonalizedProductsQueryHandler.cs | 4 +-- .../GetSuggestedProductsQueryHandler.cs | 6 ++-- .../Services/Categories/CategoryService.cs | 20 +++++++++++-- .../Categories/ProductCategoryService.cs | 6 ++-- .../Services/Collections/CollectionService.cs | 3 ++ .../Collections/ProductCollectionService.cs | 4 +-- .../Services/Products/ProductService.cs | 9 ++++++ .../Catalog/Categories/ICategoryService.cs | 2 +- .../Categories/IProductCategoryService.cs | 2 +- .../Collections/IProductCollectionService.cs | 2 +- .../Catalog/GetPersonalizedProductsQuery.cs | 1 - .../Catalog/GetSuggestedProductsQuery.cs | 1 - ...etPersonalizedProductsQueryHandlerTests.cs | 27 ++++++++++++++++- .../GetSuggestedProductsQueryHandlerTests.cs | 29 +++++++++++++++++-- .../Categories/CategoryServiceDbTests.cs | 24 +++++++++++++++ .../Categories/ProductCategoryServiceTests.cs | 19 ++++++++++++ .../Services/Products/ProductServiceTests.cs | 7 +++++ .../Services/CategoryViewModelService.cs | 10 ++----- .../Services/CollectionViewModelService.cs | 2 +- .../Services/ProductViewModelService.cs | 4 +-- .../Components/PersonalizedProducts.cs | 2 +- .../Grand.Web/Components/SuggestedProducts.cs | 1 - 22 files changed, 153 insertions(+), 32 deletions(-) diff --git a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetPersonalizedProductsQueryHandler.cs b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetPersonalizedProductsQueryHandler.cs index 27d8c3f604..d0b6e6637f 100644 --- a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetPersonalizedProductsQueryHandler.cs +++ b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetPersonalizedProductsQueryHandler.cs @@ -31,11 +31,11 @@ public async Task> Handle(GetPersonalizedProductsQuery request, C //the result is cached per key shared by many customers, so a query without one must not fill it ArgumentNullException.ThrowIfNull(request.Customer); - return await _cacheBase.GetAsync(string.Format(CacheKey.PRODUCTS_CUSTOMER_PERSONAL_KEY, request.CustomerId), + return await _cacheBase.GetAsync(string.Format(CacheKey.PRODUCTS_CUSTOMER_PERSONAL_KEY, request.Customer.Id), async () => { var query = from cr in _customerProductRepository.Table - where cr.CustomerId == request.CustomerId + where cr.CustomerId == request.Customer.Id orderby cr.DisplayOrder select cr.ProductId; diff --git a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetSuggestedProductsQueryHandler.cs b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetSuggestedProductsQueryHandler.cs index 6f9d24b9e3..095dbdcd6c 100644 --- a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetSuggestedProductsQueryHandler.cs +++ b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetSuggestedProductsQueryHandler.cs @@ -26,11 +26,13 @@ public async Task> Handle(GetSuggestedProductsQuery request, Canc //the result is cached per key shared by many customers, so a query without one must not fill it ArgumentNullException.ThrowIfNull(request.Customer); + var customerTagIds = request.Customer.CustomerTags.ToArray(); + return await _cacheBase.GetAsync( - string.Format(CacheKey.PRODUCTS_CUSTOMER_TAG, string.Join(",", request.CustomerTagIds)), async () => + string.Format(CacheKey.PRODUCTS_CUSTOMER_TAG, string.Join(",", customerTagIds)), async () => { var query = from cr in _customerTagProductRepository.Table - where request.CustomerTagIds.Contains(cr.CustomerTagId) + where customerTagIds.Contains(cr.CustomerTagId) orderby cr.DisplayOrder select cr.ProductId; diff --git a/src/Business/Grand.Business.Catalog/Services/Categories/CategoryService.cs b/src/Business/Grand.Business.Catalog/Services/Categories/CategoryService.cs index 09b4f45310..f09236181a 100644 --- a/src/Business/Grand.Business.Catalog/Services/Categories/CategoryService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Categories/CategoryService.cs @@ -146,7 +146,7 @@ public virtual async Task> GetMenuCategories(Customer customer, /// /// Gets all categories filtered by parent category identifier /// - /// Customer whose groups limit the result and key the cache + /// Customer whose groups limit the result (not read when showHidden is true) /// Store whose limitations apply /// Parent category identifier /// A value that indicates if it should shows hidden records @@ -175,8 +175,9 @@ public virtual async Task> GetAllCategoriesByParentCategoryId(Cu private async Task> GetChildCategories(string parentCategoryId, bool showHidden, Customer customer, string storeId) { + //hidden records are not filtered by customer or store, so the key does not depend on them var key = string.Format(CacheKey.CATEGORIES_BY_PARENT_CATEGORY_ID_KEY, parentCategoryId, showHidden, - string.Join(",", customer.GetCustomerGroupIds()), storeId); + showHidden ? "" : string.Join(",", customer.GetCustomerGroupIds()), showHidden ? "" : storeId); return await _cacheBase.GetAsync(key, () => { var query = _categoryRepository.Table.Where(c => c.ParentCategoryId == parentCategoryId); @@ -215,6 +216,9 @@ private async Task> GetChildCategories(string parentCategoryId, public virtual async Task> GetAllCategoriesDisplayedOnHomePage(Customer customer, string storeId, bool showHidden = false) { + //without a customer the ACL check rejects every record, so fail instead of returning nothing + if (!showHidden) ArgumentNullException.ThrowIfNull(customer); + var query = _categoryRepository.Table .Where(x => x.Published && x.ShowOnHomePage) .OrderBy(x => x.DisplayOrder); @@ -238,6 +242,9 @@ public virtual async Task> GetAllCategoriesDisplayedOnHomePage(C public virtual async Task> GetAllCategoriesFeaturedProductsOnHomePage(Customer customer, string storeId, bool showHidden = false) { + //without a customer the ACL check rejects every record, so fail instead of returning nothing + if (!showHidden) ArgumentNullException.ThrowIfNull(customer); + var query = _categoryRepository.Table .Where(x => x.Published && x.FeaturedProductsOnHomePage) .OrderBy(x => x.DisplayOrder); @@ -259,6 +266,9 @@ public virtual async Task> GetAllCategoriesFeaturedProductsOnHom /// Categories public virtual async Task> GetAllCategoriesSearchBox(Customer customer, string storeId) { + //without a customer the ACL check rejects every record, so fail instead of returning nothing + ArgumentNullException.ThrowIfNull(customer); + var query = _categoryRepository.Table .Where(x => x.Published && x.ShowOnSearchBox) .OrderBy(x => x.SearchBoxDisplayOrder); @@ -282,6 +292,9 @@ public virtual async Task> GetAllCategoriesSearchBox(Customer cu public virtual async Task> GetCategoryBreadCrumb(Category category, Customer customer, string storeId, bool showHidden = false) { + //without a customer the ACL check rejects every record, so fail instead of returning nothing + if (!showHidden) ArgumentNullException.ThrowIfNull(customer); + var result = new List(); //used to avoid circular references @@ -317,6 +330,9 @@ public virtual async Task> GetCategoryBreadCrumb(Category catego public virtual IList GetCategoryBreadCrumb(Category category, IList allCategories, Customer customer, string storeId, bool showHidden = false) { + //without a customer the ACL check rejects every record, so fail instead of returning nothing + if (!showHidden) ArgumentNullException.ThrowIfNull(customer); + var result = new List(); //used to avoid circular references diff --git a/src/Business/Grand.Business.Catalog/Services/Categories/ProductCategoryService.cs b/src/Business/Grand.Business.Catalog/Services/Categories/ProductCategoryService.cs index 895b6cb58a..48ae7e1204 100644 --- a/src/Business/Grand.Business.Catalog/Services/Categories/ProductCategoryService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Categories/ProductCategoryService.cs @@ -34,7 +34,7 @@ public ProductCategoryService( /// Gets product category mapping collection /// /// Category identifier - /// Customer whose groups limit the result and key the cache + /// Customer whose groups limit the result (not read when showHidden is true) /// Store whose limitations apply /// Page index /// Page size @@ -47,8 +47,8 @@ public virtual async Task> GetProductCategoriesByCa return new PagedList(new List(), pageIndex, pageSize); var key = string.Format(CacheKey.PRODUCTCATEGORIES_ALLBYCATEGORYID_KEY, showHidden, categoryId, pageIndex, - pageSize, string.Join(",", customer.GetCustomerGroupIds()), - storeId); + pageSize, showHidden ? "" : string.Join(",", customer.GetCustomerGroupIds()), + showHidden ? "" : storeId); return await _cacheBase.GetAsync(key, async () => { var query = _productRepository.Table.Where(x => x.ProductCategories.Any(y => y.CategoryId == categoryId)); diff --git a/src/Business/Grand.Business.Catalog/Services/Collections/CollectionService.cs b/src/Business/Grand.Business.Catalog/Services/Collections/CollectionService.cs index acdda4b370..d5ac64bf89 100644 --- a/src/Business/Grand.Business.Catalog/Services/Collections/CollectionService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Collections/CollectionService.cs @@ -107,6 +107,9 @@ public virtual async Task> GetAllCollections(string colle public virtual async Task> GetAllCollectionFeaturedProductsOnHomePage(Customer customer, string storeId, bool showHidden = false) { + //without a customer the ACL check rejects every record, so fail instead of returning nothing + if (!showHidden) ArgumentNullException.ThrowIfNull(customer); + var query = _collectionRepository.Table.Where(x => x.Published && x.FeaturedProductsOnHomePage) .OrderBy(x => x.DisplayOrder); diff --git a/src/Business/Grand.Business.Catalog/Services/Collections/ProductCollectionService.cs b/src/Business/Grand.Business.Catalog/Services/Collections/ProductCollectionService.cs index 98d8b4a04f..8386ab440b 100644 --- a/src/Business/Grand.Business.Catalog/Services/Collections/ProductCollectionService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Collections/ProductCollectionService.cs @@ -33,7 +33,7 @@ public ProductCollectionService(ICacheBase cacheBase, /// /// Collection id /// Store ident - /// Customer whose groups limit the result and key the cache + /// Customer whose groups limit the result (not read when showHidden is true) /// Page index /// Page size /// A value that indicates if it should shows hidden records @@ -43,7 +43,7 @@ public virtual async Task> GetProductCollectionsB int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false) { var key = string.Format(CacheKey.PRODUCTCOLLECTIONS_ALLBYCOLLECTIONID_KEY, showHidden, collectionId, pageIndex, - pageSize, string.Join(",", customer.GetCustomerGroupIds()), storeId); + pageSize, showHidden ? "" : string.Join(",", customer.GetCustomerGroupIds()), showHidden ? "" : storeId); return await _cacheBase.GetAsync(key, async () => { var query = _productRepository.Table.Where(x => diff --git a/src/Business/Grand.Business.Catalog/Services/Products/ProductService.cs b/src/Business/Grand.Business.Catalog/Services/Products/ProductService.cs index 806b6d1bb8..a775eef454 100644 --- a/src/Business/Grand.Business.Catalog/Services/Products/ProductService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Products/ProductService.cs @@ -125,6 +125,9 @@ public virtual async Task> GetProductsByIds(string[] productIds, if (productIds == null || productIds.Length == 0) return new List(); + //without a customer the ACL check rejects every record, so fail instead of returning nothing + if (!showHidden) ArgumentNullException.ThrowIfNull(customer); + var products = new List(); foreach (var id in productIds) { @@ -593,6 +596,9 @@ public virtual async Task> GetProductsByProductAttributeId(s public virtual async Task> GetAssociatedProducts(string parentGroupedProductId, Customer customer, string storeId = "", string vendorId = "", bool showHidden = false) { + //without a customer the ACL check rejects every record, so fail instead of returning nothing + if (!showHidden) ArgumentNullException.ThrowIfNull(customer); + var query = from p in _productRepository.Table select p; @@ -871,6 +877,9 @@ public virtual async Task DeleteCrossSellProduct(CrossSellProduct crossSellProdu public virtual async Task> GetCrossSellProductsByShoppingCart(IList cart, int numberOfProducts, Customer customer, string storeId) { + //without a customer the ACL check rejects every record, so fail instead of returning nothing + ArgumentNullException.ThrowIfNull(customer); + var result = new List(); if (numberOfProducts == 0) diff --git a/src/Business/Grand.Business.Core/Interfaces/Catalog/Categories/ICategoryService.cs b/src/Business/Grand.Business.Core/Interfaces/Catalog/Categories/ICategoryService.cs index 19b20eb730..d9453d3bb2 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Catalog/Categories/ICategoryService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Catalog/Categories/ICategoryService.cs @@ -31,7 +31,7 @@ Task> GetAllCategories(string parentId, string categoryName /// /// Gets all categories by parent category /// - /// Customer whose groups limit the result and key the cache + /// Customer whose groups limit the result (not read when showHidden is true) /// Store whose limitations apply /// Parent category id /// A value that indicates if it should shows hidden records diff --git a/src/Business/Grand.Business.Core/Interfaces/Catalog/Categories/IProductCategoryService.cs b/src/Business/Grand.Business.Core/Interfaces/Catalog/Categories/IProductCategoryService.cs index 5230c4e48a..e5d1db8781 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Catalog/Categories/IProductCategoryService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Catalog/Categories/IProductCategoryService.cs @@ -11,7 +11,7 @@ public interface IProductCategoryService /// Gets product category mapping collection /// /// Category id - /// Customer whose groups limit the result and key the cache + /// Customer whose groups limit the result (not read when showHidden is true) /// Store whose limitations apply /// Page index /// Page size diff --git a/src/Business/Grand.Business.Core/Interfaces/Catalog/Collections/IProductCollectionService.cs b/src/Business/Grand.Business.Core/Interfaces/Catalog/Collections/IProductCollectionService.cs index 4964f9bd51..bb14935380 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Catalog/Collections/IProductCollectionService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Catalog/Collections/IProductCollectionService.cs @@ -12,7 +12,7 @@ public interface IProductCollectionService /// /// Collection id /// Store id - /// Customer whose groups limit the result and key the cache + /// Customer whose groups limit the result (not read when showHidden is true) /// Page index /// Page size /// A value that indicates if it should shows hidden records diff --git a/src/Business/Grand.Business.Core/Queries/Catalog/GetPersonalizedProductsQuery.cs b/src/Business/Grand.Business.Core/Queries/Catalog/GetPersonalizedProductsQuery.cs index e117841659..2cf03864ca 100644 --- a/src/Business/Grand.Business.Core/Queries/Catalog/GetPersonalizedProductsQuery.cs +++ b/src/Business/Grand.Business.Core/Queries/Catalog/GetPersonalizedProductsQuery.cs @@ -6,7 +6,6 @@ namespace Grand.Business.Core.Queries.Catalog; public class GetPersonalizedProductsQuery : IRequest> { - public string CustomerId { get; set; } public int ProductsNumber { get; set; } public Customer Customer { get; set; } public string StoreId { get; set; } diff --git a/src/Business/Grand.Business.Core/Queries/Catalog/GetSuggestedProductsQuery.cs b/src/Business/Grand.Business.Core/Queries/Catalog/GetSuggestedProductsQuery.cs index 88bed64b98..a0eb80aaf5 100644 --- a/src/Business/Grand.Business.Core/Queries/Catalog/GetSuggestedProductsQuery.cs +++ b/src/Business/Grand.Business.Core/Queries/Catalog/GetSuggestedProductsQuery.cs @@ -6,7 +6,6 @@ namespace Grand.Business.Core.Queries.Catalog; public class GetSuggestedProductsQuery : IRequest> { - public string[] CustomerTagIds { get; set; } public int ProductsNumber { get; set; } public Customer Customer { get; set; } public string StoreId { get; set; } diff --git a/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetPersonalizedProductsQueryHandlerTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetPersonalizedProductsQueryHandlerTests.cs index a5a1017b6a..7908e58409 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetPersonalizedProductsQueryHandlerTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetPersonalizedProductsQueryHandlerTests.cs @@ -5,6 +5,7 @@ using Grand.Domain.Catalog; using Grand.Domain.Customers; using Grand.Infrastructure.Caching; +using Grand.Infrastructure.Caching.Constants; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -42,9 +43,33 @@ public async Task HandleTest() public async Task Handle_WithoutCustomer_ThrowsBeforeCaching() { await Assert.ThrowsExactlyAsync(() => - handler.Handle(new GetPersonalizedProductsQuery { CustomerId = "1", ProductsNumber = 1 }, + handler.Handle(new GetPersonalizedProductsQuery { ProductsNumber = 1 }, CancellationToken.None)); _casheManagerMock.Verify(c => c.GetAsync(It.IsAny(), It.IsAny>>>()), Times.Never); } + + [TestMethod] + public async Task Handle_KeysAndLoadsByIdOfQueryCustomer() + { + //Arrange + var customer = new Customer { Id = "c1" }; + _casheManagerMock.Setup(c => c.GetAsync(It.IsAny(), It.IsAny>>>())) + .Returns((string _, Func>> acquire) => acquire()); + _customerProductRepositoryMock.Setup(r => r.Table) + .Returns(new List { + new() { CustomerId = "c1", ProductId = "p1" }, new() { CustomerId = "c2", ProductId = "p2" } + }.AsQueryable()); + _productServiceMock.Setup(p => p.GetProductsByIds(It.IsAny(), customer, "s1", false)) + .ReturnsAsync(new List()); + + //Act + await handler.Handle(new GetPersonalizedProductsQuery { Customer = customer, StoreId = "s1", ProductsNumber = 10 }, + CancellationToken.None); + + //Assert + _casheManagerMock.Verify(c => c.GetAsync(string.Format(CacheKey.PRODUCTS_CUSTOMER_PERSONAL_KEY, "c1"), + It.IsAny>>>()), Times.Once); + _productServiceMock.Verify(p => p.GetProductsByIds(new[] { "p1" }, customer, "s1", false), Times.Once); + } } \ No newline at end of file diff --git a/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetSuggestedProductsQueryHandlerTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetSuggestedProductsQueryHandlerTests.cs index f1cf682a51..fb8bba39d3 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetSuggestedProductsQueryHandlerTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetSuggestedProductsQueryHandlerTests.cs @@ -5,6 +5,7 @@ using Grand.Domain.Catalog; using Grand.Domain.Customers; using Grand.Infrastructure.Caching; +using Grand.Infrastructure.Caching.Constants; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -34,7 +35,6 @@ public void Init() public async Task HandleTest() { var suggestedProductsQuery = new GetSuggestedProductsQuery { - CustomerTagIds = ["1"], ProductsNumber = 10, Customer = new Customer() }; @@ -47,9 +47,34 @@ public async Task HandleTest() public async Task Handle_WithoutCustomer_ThrowsBeforeCaching() { await Assert.ThrowsExactlyAsync(() => - handler.Handle(new GetSuggestedProductsQuery { CustomerTagIds = ["1"], ProductsNumber = 1 }, + handler.Handle(new GetSuggestedProductsQuery { ProductsNumber = 1 }, CancellationToken.None)); _casheManagerMock.Verify(c => c.GetAsync(It.IsAny(), It.IsAny>>>()), Times.Never); } + + [TestMethod] + public async Task Handle_KeysAndFiltersByTagsOfQueryCustomer() + { + //Arrange + var customer = new Customer(); + customer.CustomerTags.Add("t1"); + _casheManagerMock.Setup(c => c.GetAsync(It.IsAny(), It.IsAny>>>())) + .Returns((string _, Func>> acquire) => acquire()); + _customerGroupProductRepositoryMock.Setup(r => r.Table) + .Returns(new List { + new() { CustomerTagId = "t1", ProductId = "p1" }, new() { CustomerTagId = "t2", ProductId = "p2" } + }.AsQueryable()); + _productServiceMock.Setup(p => p.GetProductsByIds(It.IsAny(), customer, "s1", false)) + .ReturnsAsync(new List()); + + //Act + await handler.Handle(new GetSuggestedProductsQuery { Customer = customer, StoreId = "s1", ProductsNumber = 10 }, + CancellationToken.None); + + //Assert + _casheManagerMock.Verify(c => c.GetAsync(string.Format(CacheKey.PRODUCTS_CUSTOMER_TAG, "t1"), + It.IsAny>>>()), Times.Once); + _productServiceMock.Verify(p => p.GetProductsByIds(new[] { "p1" }, customer, "s1", false), Times.Once); + } } \ No newline at end of file diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceDbTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceDbTests.cs index 1047839931..2848acaa52 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceDbTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceDbTests.cs @@ -39,6 +39,30 @@ public void InitializeTests() _mediatorMock.Object, _aclServiceMock, new AccessControlConfig()); } + [TestMethod] + public async Task GetAllCategoriesByParentCategoryId_ShowHiddenWithoutCustomer_ReturnsHiddenChildren() + { + //Arrange + await _categoryRepository.InsertAsync(new Category { + Id = "child", ParentCategoryId = "parent", Published = false, + LimitedToGroups = true, CustomerGroups = new List { "vip" } + }); + + //Act + var children = await _categoryService.GetAllCategoriesByParentCategoryId(customer: null, storeId: "", + parentCategoryId: "parent", showHidden: true); + + //Assert + Assert.HasCount(1, children); + } + + [TestMethod] + public async Task GetAllCategoriesSearchBox_WithoutCustomer_Throws() + { + await Assert.ThrowsExactlyAsync(() => + _categoryService.GetAllCategoriesSearchBox(null, "store1")); + } + [TestMethod] public async Task GetMenuCategories_UsesGivenCustomerAndStore() { diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/ProductCategoryServiceTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/ProductCategoryServiceTests.cs index 43643eebfd..bd917628c1 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/ProductCategoryServiceTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/ProductCategoryServiceTests.cs @@ -51,6 +51,25 @@ public async Task GetProductCategoriesByCategoryId_FiltersByGivenStore() Assert.IsEmpty(store2); } + [TestMethod] + public async Task GetProductCategoriesByCategoryId_ShowHiddenWithoutCustomer_ReturnsLimitedProducts() + { + //Arrange + var product = new Product { + LimitedToStores = true, Stores = new List { "store1" }, + LimitedToGroups = true, CustomerGroups = new List { "vip" } + }; + product.ProductCategories.Add(new ProductCategory { CategoryId = "cat" }); + await _repository.InsertAsync(product); + + //Act + var mappings = await _productCategoryService.GetProductCategoriesByCategoryId("cat", customer: null, storeId: "", + showHidden: true); + + //Assert + Assert.HasCount(1, mappings); + } + [TestMethod] public async Task GetProductCategoriesByCategoryIdTest() { diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Products/ProductServiceTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Products/ProductServiceTests.cs index f82fc75f7f..a15652363c 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Products/ProductServiceTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Products/ProductServiceTests.cs @@ -61,6 +61,13 @@ await _productRepository.InsertAsync(new Product { Assert.IsEmpty(store2); } + [TestMethod] + public async Task GetProductsByIds_WithoutCustomer_Throws() + { + await Assert.ThrowsExactlyAsync(() => + _productService.GetProductsByIds(["p1"], null, "store1")); + } + [TestMethod] public async Task SearchProducts_SendsGivenCustomerInQuery() { diff --git a/src/Web/Grand.Web.AdminShared/Services/CategoryViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CategoryViewModelService.cs index 47b8c560a5..558f484194 100644 --- a/src/Web/Grand.Web.AdminShared/Services/CategoryViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/CategoryViewModelService.cs @@ -8,7 +8,6 @@ using Grand.Business.Core.Interfaces.Storage; using Grand.Domain.Catalog; using Grand.Domain.Discounts; -using Grand.Infrastructure; using Grand.Web.AdminShared.Extensions; using Grand.Web.AdminShared.Extensions.Mapping; using Grand.Web.AdminShared.Interfaces; @@ -33,7 +32,6 @@ public class CategoryViewModelService : ICategoryViewModelService private readonly IVendorService _vendorService; private readonly ISeNameService _seNameService; private readonly IEnumTranslationService _enumTranslationService; - private readonly IContextAccessor _contextAccessor; public CategoryViewModelService( ICategoryService categoryService, @@ -47,8 +45,7 @@ public CategoryViewModelService( IVendorService vendorService, CatalogSettings catalogSettings, ISeNameService seNameService, - IEnumTranslationService enumTranslationService, - IContextAccessor contextAccessor) + IEnumTranslationService enumTranslationService) { _categoryService = categoryService; _productCategoryService = productCategoryService; @@ -62,7 +59,6 @@ public CategoryViewModelService( _catalogSettings = catalogSettings; _seNameService = seNameService; _enumTranslationService = enumTranslationService; - _contextAccessor = contextAccessor; } public virtual async Task PrepareCategoryListModel(string storeId) @@ -203,8 +199,8 @@ public virtual async Task DeleteCategory(Category category) PrepareCategoryProductModel(string categoryId, int pageIndex, int pageSize) { var productCategories = await _productCategoryService.GetProductCategoriesByCategoryId(categoryId, - customer: _contextAccessor.WorkContext.CurrentCustomer, - storeId: _contextAccessor.StoreContext.CurrentStore.Id, + customer: null, + storeId: "", pageIndex - 1, pageSize, true); var categoryproducts = new List(); diff --git a/src/Web/Grand.Web.AdminShared/Services/CollectionViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CollectionViewModelService.cs index b4fa5f27b6..38bbcaf387 100644 --- a/src/Web/Grand.Web.AdminShared/Services/CollectionViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/CollectionViewModelService.cs @@ -220,7 +220,7 @@ public virtual async { var productCollections = await _productCollectionService.GetProductCollectionsByCollectionId(collectionId, storeId, - customer: _contextAccessor.WorkContext.CurrentCustomer, + customer: null, pageIndex - 1, pageSize, true); var items = new List(); foreach (var x in productCollections) diff --git a/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs index a9f109cf2f..e4cbc47e4c 100644 --- a/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs @@ -2602,9 +2602,7 @@ protected virtual async Task> GetChildCategoryIds(string parentCate { var categoriesIds = new List(); var categories = await categoryService.GetAllCategoriesByParentCategoryId( - customer: contextAccessor.WorkContext.CurrentCustomer, - storeId: contextAccessor.StoreContext.CurrentStore.Id, - parentCategoryId: parentCategoryId, showHidden: true); + customer: null, storeId: "", parentCategoryId: parentCategoryId, showHidden: true); foreach (var category in categories) { categoriesIds.Add(category.Id); diff --git a/src/Web/Grand.Web/Components/PersonalizedProducts.cs b/src/Web/Grand.Web/Components/PersonalizedProducts.cs index 12a86813e3..5d5fa9f396 100644 --- a/src/Web/Grand.Web/Components/PersonalizedProducts.cs +++ b/src/Web/Grand.Web/Components/PersonalizedProducts.cs @@ -32,7 +32,7 @@ public async Task InvokeAsync(int? productThumbPictureSize return Content(""); var products = await _mediator.Send(new GetPersonalizedProductsQuery { - CustomerId = _contextAccessor.WorkContext.CurrentCustomer.Id, ProductsNumber = _catalogSettings.PersonalizedProductsNumber, + ProductsNumber = _catalogSettings.PersonalizedProductsNumber, Customer = _contextAccessor.WorkContext.CurrentCustomer, StoreId = _contextAccessor.StoreContext.CurrentStore.Id }); diff --git a/src/Web/Grand.Web/Components/SuggestedProducts.cs b/src/Web/Grand.Web/Components/SuggestedProducts.cs index 7a6c13ee70..e1db22c840 100644 --- a/src/Web/Grand.Web/Components/SuggestedProducts.cs +++ b/src/Web/Grand.Web/Components/SuggestedProducts.cs @@ -32,7 +32,6 @@ public async Task InvokeAsync(int? productThumbPictureSize return Content(""); var products = await _mediator.Send(new GetSuggestedProductsQuery { - CustomerTagIds = _contextAccessor.WorkContext.CurrentCustomer.CustomerTags.ToArray(), ProductsNumber = _catalogSettings.SuggestedProductsNumber, Customer = _contextAccessor.WorkContext.CurrentCustomer, StoreId = _contextAccessor.StoreContext.CurrentStore.Id From 87916a3e01f2dff31ed35cee78f8feb8032496ac Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 18:14:09 +0200 Subject: [PATCH 24/33] Price a customer's cart in the admin panel for that customer The admin cart and wishlist details, the customer edit page's cart tab and the attribute price adjustments priced another customer's items with the logged-in admin's groups, discounts and tax. They now use the customer whose cart it is and the store the item was added in. --- .../Products/ProductAttributeFormatter.cs | 15 ++++++++------- .../Controllers/ShoppingCartController.cs | 18 ++++++++++-------- .../Services/CustomerViewModelService.cs | 11 ++++++----- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/Business/Grand.Business.Catalog/Services/Products/ProductAttributeFormatter.cs b/src/Business/Grand.Business.Catalog/Services/Products/ProductAttributeFormatter.cs index 71c673b5bd..3aa63362ab 100644 --- a/src/Business/Grand.Business.Catalog/Services/Products/ProductAttributeFormatter.cs +++ b/src/Business/Grand.Business.Catalog/Services/Products/ProductAttributeFormatter.cs @@ -86,8 +86,8 @@ public virtual async Task FormatAttributes(Product product, IList FormatAttributes(Product product, IList x.Name, langId)) : p1.GetTranslation(x => x.Name, langId))}"); - var formattedAttribute = await PrepareFormattedAttribute(p1, customAttributes, langId, separator, - htmlEncode, + var formattedAttribute = await PrepareFormattedAttribute(p1, customAttributes, customer, langId, + separator, htmlEncode, renderPrices, allowHyperlinks, showInAdmin); if (formattedAttribute.Length > 0) { @@ -151,10 +151,11 @@ public virtual async Task FormatAttributes(Product product, IList PrepareFormattedAttribute(Product product, - IList customAttributes, string langId, + IList customAttributes, Customer customer, string langId, string separator, bool htmlEncode, bool renderPrices, bool allowHyperlinks, bool showInAdmin) { + var priceCustomer = customer ?? _contextAccessor.WorkContext.CurrentCustomer; var result = new StringBuilder(); var attributes = product.ParseProductAttributeMappings(customAttributes); for (var i = 0; i < attributes.Count; i++) @@ -235,12 +236,12 @@ private async Task PrepareFormattedAttribute(Product product, { var attributeValuePriceAdjustment = await _pricingService.GetProductAttributeValuePriceAdjustment(attributeValue, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + priceCustomer, _contextAccessor.StoreContext.CurrentStore, _contextAccessor.WorkContext.WorkingCurrency); var (priceAdjustmentBase, _) = await _taxService.GetProductPrice(product, attributeValuePriceAdjustment, _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore); + priceCustomer, _contextAccessor.StoreContext.CurrentStore); switch (priceAdjustmentBase) { case > 0: diff --git a/src/Web/Grand.Web.Admin/Controllers/ShoppingCartController.cs b/src/Web/Grand.Web.Admin/Controllers/ShoppingCartController.cs index 149fd63149..573f7193e7 100644 --- a/src/Web/Grand.Web.Admin/Controllers/ShoppingCartController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/ShoppingCartController.cs @@ -104,6 +104,7 @@ public async Task GetCartDetails(string customerId) foreach (var sci in cart) { var store = await _storeService.GetStoreById(sci.StoreId); + var priceStore = store ?? _contextAccessor.StoreContext.CurrentStore; var product = await _productService.GetProductById(sci.ProductId); var sciModel = new ShoppingCartItemModel { Id = sci.Id, @@ -114,17 +115,17 @@ public async Task GetCartDetails(string customerId) AttributeInfo = await _productAttributeFormatter.FormatAttributes(product, sci.Attributes, customer), UnitPrice = _priceFormatter.FormatPrice((await _taxService.GetProductPrice(product, (await _pricingService.GetUnitPrice(sci, product, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + customer, priceStore, _contextAccessor.WorkContext.WorkingCurrency)).unitprice, _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice, + customer, priceStore)).productprice, _contextAccessor.WorkContext.WorkingCurrency), Total = _priceFormatter.FormatPrice((await _taxService.GetProductPrice(product, (await _pricingService.GetSubTotal(sci, product, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + customer, priceStore, _contextAccessor.WorkContext.WorkingCurrency)).subTotal, _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice, + customer, priceStore)).productprice, _contextAccessor.WorkContext.WorkingCurrency), UpdatedOn = sci.UpdatedOnUtc.HasValue ? _dateTimeService.ConvertToUserTime(sci.UpdatedOnUtc.Value, DateTimeKind.Utc) @@ -175,6 +176,7 @@ public async Task GetWishlistDetails(string customerId) foreach (var sci in cart) { var store = await _storeService.GetStoreById(sci.StoreId); + var priceStore = store ?? _contextAccessor.StoreContext.CurrentStore; var product = await _productService.GetProductById(sci.ProductId); var sciModel = new ShoppingCartItemModel { Id = sci.Id, @@ -185,17 +187,17 @@ public async Task GetWishlistDetails(string customerId) AttributeInfo = await _productAttributeFormatter.FormatAttributes(product, sci.Attributes, customer), UnitPrice = _priceFormatter.FormatPrice((await _taxService.GetProductPrice(product, (await _pricingService.GetUnitPrice(sci, product, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + customer, priceStore, _contextAccessor.WorkContext.WorkingCurrency)).unitprice, _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice, + customer, priceStore)).productprice, _contextAccessor.WorkContext.WorkingCurrency), Total = _priceFormatter.FormatPrice((await _taxService.GetProductPrice(product, (await _pricingService.GetSubTotal(sci, product, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + customer, priceStore, _contextAccessor.WorkContext.WorkingCurrency)).subTotal, _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice, + customer, priceStore)).productprice, _contextAccessor.WorkContext.WorkingCurrency), UpdatedOn = sci.UpdatedOnUtc.HasValue ? _dateTimeService.ConvertToUserTime(sci.UpdatedOnUtc.Value, DateTimeKind.Utc) diff --git a/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs index 80d15a4d18..4aab4bf264 100644 --- a/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs @@ -858,30 +858,31 @@ public virtual async Task> PrepareShoppingCartItemM foreach (var sci in cart) { var store = await _storeService.GetStoreById(sci.StoreId); + var priceStore = store ?? _contextAccessor.StoreContext.CurrentStore; var product = await _productService.GetProductById(sci.ProductId); if (product != null) { var price = (await taxService.GetProductPrice(product, (await priceCalculationService.GetUnitPrice(sci, product, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + customer, priceStore, _contextAccessor.WorkContext.WorkingCurrency)).unitprice, _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice; + customer, priceStore)).productprice; var sciModel = new ShoppingCartItemModel { Id = sci.Id, Store = store != null ? store.Shortcut : "Unknown", ProductId = sci.ProductId, Quantity = sci.Quantity, ProductName = product.Name, - AttributeInfo = await productAttributeFormatter.FormatAttributes(product, sci.Attributes), + AttributeInfo = await productAttributeFormatter.FormatAttributes(product, sci.Attributes, customer), UnitPrice = priceFormatter.FormatPrice(price, _contextAccessor.WorkContext.WorkingCurrency), UnitPriceValue = price, Total = priceFormatter.FormatPrice((await taxService.GetProductPrice(product, (await priceCalculationService.GetSubTotal(sci, product, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, + customer, priceStore, _contextAccessor.WorkContext.WorkingCurrency)).subTotal, _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, - _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore)).productprice, + customer, priceStore)).productprice, _contextAccessor.WorkContext.WorkingCurrency), UpdatedOn = sci.UpdatedOnUtc.HasValue ? _dateTimeService.ConvertToUserTime(sci.UpdatedOnUtc.Value, DateTimeKind.Utc) From 81c3eed5fae0d32f405912ab96eda1b1c3b430e1 Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 18:14:19 +0200 Subject: [PATCH 25/33] Round loyalty points amounts in the primary store currency ConvertLoyaltyPointsToAmount returns an amount in the primary store currency, but rounded it in the working currency, so a zero-decimal working currency rounded 4.95 USD to 5 before conversion. The currency parameter added for that rounding is dropped again; the amount is rounded in the primary store currency. --- .../Services/Orders/OrderCalculationService.cs | 11 ++++++----- .../Checkout/Orders/IOrderCalculationService.cs | 5 ++--- .../Orders/OrderCalculationServiceTests.cs | 16 +++++++++++++++- .../Handlers/Checkout/GetPaymentMethodHandler.cs | 3 +-- .../Orders/GetCustomerLoyaltyPointsHandler.cs | 13 +++---------- 5 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/Business/Grand.Business.Checkout/Services/Orders/OrderCalculationService.cs b/src/Business/Grand.Business.Checkout/Services/Orders/OrderCalculationService.cs index 12d503028a..a3be2f49d6 100644 --- a/src/Business/Grand.Business.Checkout/Services/Orders/OrderCalculationService.cs +++ b/src/Business/Grand.Business.Checkout/Services/Orders/OrderCalculationService.cs @@ -704,7 +704,7 @@ await _loyaltyPointsService.GetLoyaltyPointsBalance(context.Customer.Id, context.Store.Id); if (CheckMinimumLoyaltyPointsToUseRequirement(loyaltyPointsBalance)) { - var loyaltyPointsBalanceAmount = await ConvertLoyaltyPointsToAmount(loyaltyPointsBalance, context.Currency); + var loyaltyPointsBalanceAmount = await ConvertLoyaltyPointsToAmount(loyaltyPointsBalance); if (orderTotal > 0) { if (orderTotal > loyaltyPointsBalanceAmount) @@ -740,17 +740,18 @@ await _currencyService.ConvertToPrimaryStoreCurrency(redeemedLoyaltyPointsAmount /// Converts existing loyalty points to amount /// /// Loyalty points - /// Currency the amount is rounded in /// Converted value - public virtual async Task ConvertLoyaltyPointsToAmount(int loyaltyPoints, Currency currency) + public virtual async Task ConvertLoyaltyPointsToAmount(int loyaltyPoints) { if (loyaltyPoints <= 0) return 0; + //the amount is in the primary store currency, so it is rounded in that currency var result = loyaltyPoints * _loyaltyPointsSettings.ExchangeRate; - if (_shoppingCartSettings.RoundPrices) result = RoundingHelper.RoundPrice(result, currency); + if (_shoppingCartSettings.RoundPrices) + result = RoundingHelper.RoundPrice(result, await _currencyService.GetPrimaryStoreCurrency()); - return await Task.FromResult(result); + return result; } /// diff --git a/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IOrderCalculationService.cs b/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IOrderCalculationService.cs index 2de6b1df7f..6f43c76239 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IOrderCalculationService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Checkout/Orders/IOrderCalculationService.cs @@ -1,6 +1,5 @@ using Grand.Business.Core.Utilities.Catalog; using Grand.Business.Core.Utilities.Checkout; -using Grand.Domain.Directory; using Grand.Domain.Orders; namespace Grand.Business.Core.Interfaces.Checkout.Orders; @@ -104,8 +103,8 @@ public interface IOrderCalculationService /// Converts existing loyalty points to amount /// /// Loyalty points - /// Converted value - Task ConvertLoyaltyPointsToAmount(int loyaltyPoints, Currency currency); + /// Converted value in the primary store currency + Task ConvertLoyaltyPointsToAmount(int loyaltyPoints); /// /// Converts an amount to loyalty points diff --git a/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/OrderCalculationServiceTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/OrderCalculationServiceTests.cs index 4d706fe170..1f1ccb9d29 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/OrderCalculationServiceTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/OrderCalculationServiceTests.cs @@ -289,11 +289,25 @@ public async Task ConvertLoyaltyPointsToAmountTest() //Arrange _loyaltyPointsSettings.ExchangeRate = 1; //Act - var result = await _service.ConvertLoyaltyPointsToAmount(10, _context.Currency); + var result = await _service.ConvertLoyaltyPointsToAmount(10); //Assert Assert.AreEqual(10, result); } + [TestMethod] + public async Task ConvertLoyaltyPointsToAmount_RoundsInThePrimaryStoreCurrency() + { + //Arrange + _loyaltyPointsSettings.ExchangeRate = 0.33; + _shoppingCartSettings.RoundPrices = true; + _currencyServiceMock.Setup(x => x.GetPrimaryStoreCurrency()) + .ReturnsAsync(new Currency { CurrencyCode = "USD", NumberDecimal = 2 }); + //Act + var result = await _service.ConvertLoyaltyPointsToAmount(15); + //Assert + Assert.AreEqual(4.95, result, 0.0001); + } + [TestMethod] public void ConvertAmountToLoyaltyPointsTest() { diff --git a/src/Web/Grand.Web/Features/Handlers/Checkout/GetPaymentMethodHandler.cs b/src/Web/Grand.Web/Features/Handlers/Checkout/GetPaymentMethodHandler.cs index 6197e15994..e07f9fc3c8 100644 --- a/src/Web/Grand.Web/Features/Handlers/Checkout/GetPaymentMethodHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Checkout/GetPaymentMethodHandler.cs @@ -61,8 +61,7 @@ public async Task Handle(GetPaymentMethod request, C var loyaltyPointsBalance = await _loyaltyPointsService.GetLoyaltyPointsBalance(request.Customer.Id, request.Store.Id); var loyaltyPointsAmount = await _currencyService.ConvertFromPrimaryStoreCurrency( - await _orderTotalCalculationService.ConvertLoyaltyPointsToAmount(loyaltyPointsBalance, - _contextAccessor.WorkContext.WorkingCurrency), + await _orderTotalCalculationService.ConvertLoyaltyPointsToAmount(loyaltyPointsBalance), request.Currency); if (loyaltyPointsAmount > 0 && _orderTotalCalculationService.CheckMinimumLoyaltyPointsToUseRequirement(loyaltyPointsBalance)) diff --git a/src/Web/Grand.Web/Features/Handlers/Orders/GetCustomerLoyaltyPointsHandler.cs b/src/Web/Grand.Web/Features/Handlers/Orders/GetCustomerLoyaltyPointsHandler.cs index 1b8beb252b..d560672d40 100644 --- a/src/Web/Grand.Web/Features/Handlers/Orders/GetCustomerLoyaltyPointsHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Orders/GetCustomerLoyaltyPointsHandler.cs @@ -2,8 +2,6 @@ using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Domain.Orders; -using Grand.Infrastructure; -using Grand.Web.Common.Extensions; using Grand.Web.Features.Models.Orders; using Grand.Web.Models.Orders; using Grand.Mediator; @@ -12,7 +10,6 @@ namespace Grand.Web.Features.Handlers.Orders; public class GetCustomerLoyaltyPointsHandler : IRequestHandler { - private readonly IContextAccessor _contextAccessor; private readonly ICurrencyService _currencyService; private readonly IDateTimeService _dateTimeService; private readonly ILoyaltyPointsService _loyaltyPointsService; @@ -23,10 +20,8 @@ public class GetCustomerLoyaltyPointsHandler : IRequestHandler Handle(GetCustomerLoyaltyPoints re var loyaltyPointsBalance = await _loyaltyPointsService.GetLoyaltyPointsBalance(request.Customer.Id, request.Store.Id); var loyaltyPointsAmountBase = - await _orderTotalCalculationService.ConvertLoyaltyPointsToAmount(loyaltyPointsBalance, - _contextAccessor.WorkContext.WorkingCurrency); + await _orderTotalCalculationService.ConvertLoyaltyPointsToAmount(loyaltyPointsBalance); var loyaltyPointsAmount = await _currencyService.ConvertFromPrimaryStoreCurrency(loyaltyPointsAmountBase, request.Currency); model.LoyaltyPointsBalance = loyaltyPointsBalance; @@ -59,8 +53,7 @@ await _orderTotalCalculationService.ConvertLoyaltyPointsToAmount(loyaltyPointsBa //minimum amount/balance var minimumLoyaltyPointsBalance = _loyaltyPointsSettings.MinimumLoyaltyPointsToUse; var minimumLoyaltyPointsAmountBase = - await _orderTotalCalculationService.ConvertLoyaltyPointsToAmount(minimumLoyaltyPointsBalance, - _contextAccessor.WorkContext.WorkingCurrency); + await _orderTotalCalculationService.ConvertLoyaltyPointsToAmount(minimumLoyaltyPointsBalance); var minimumLoyaltyPointsAmount = await _currencyService.ConvertFromPrimaryStoreCurrency(minimumLoyaltyPointsAmountBase, request.Currency); model.MinimumLoyaltyPointsBalance = minimumLoyaltyPointsBalance; From 6d38be651e00845f608a9f1e55abab8106b0ab42 Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 18:14:20 +0200 Subject: [PATCH 26/33] Build calculation contexts from the request and drop duplicated state The cart, mini cart, order totals and payment method handlers priced their lines from the request but their totals from the ambient context. They now build one context from the request fields, which no longer needs the context accessor. GetPaymentMethod carries the tax display type like the other cart requests. ShoppingCartTotalAmountValidatorRecord and ValidateMinShoppingCartSubtotalAmountCommand carried a customer and a currency next to a context holding the same; they now read both from the context. --- ...hoppingCartSubtotalAmountCommandHandler.cs | 1 - .../Services/Orders/ShoppingCartValidator.cs | 3 +- .../ShoppingCartTotalAmountValidator.cs | 10 ++--- ...ateMinShoppingCartSubtotalAmountCommand.cs | 2 - ...ngCartSubtotalAmountCommandHandlerTests.cs | 1 - .../Controllers/CheckoutController.cs | 3 +- .../Checkout/GetPaymentMethodHandler.cs | 14 +++---- .../ShoppingCart/GetAddToCartHandler.cs | 12 +++--- .../GetMiniShoppingCartHandler.cs | 16 ++++---- .../ShoppingCart/GetOrderTotalsHandler.cs | 37 +++++++++---------- .../ShoppingCart/GetShoppingCartHandler.cs | 1 - .../Models/Checkout/GetPaymentMethod.cs | 2 + 12 files changed, 44 insertions(+), 58 deletions(-) diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ValidateMinShoppingCartSubtotalAmountCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ValidateMinShoppingCartSubtotalAmountCommandHandler.cs index 1160a540de..775b77add1 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ValidateMinShoppingCartSubtotalAmountCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/ValidateMinShoppingCartSubtotalAmountCommandHandler.cs @@ -25,7 +25,6 @@ public async Task Handle(ValidateMinShoppingCartSubtotalAmountCommand requ CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request.Cart); - ArgumentNullException.ThrowIfNull(request.Customer); ArgumentNullException.ThrowIfNull(request.Context); return await ValidateMinOrderSubtotalAmount(request.Cart, request.Context); diff --git a/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartValidator.cs b/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartValidator.cs index 4a692b3677..2094e071f6 100644 --- a/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartValidator.cs +++ b/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartValidator.cs @@ -150,8 +150,7 @@ public virtual async Task> GetShoppingCartWarnings(IList().ValidateAsync( - new ShoppingCartTotalAmountValidatorRecord(_contextAccessor.WorkContext.CurrentCustomer, - _contextAccessor.WorkContext.WorkingCurrency, shoppingCart, + new ShoppingCartTotalAmountValidatorRecord(shoppingCart, //TODO M1 PR 3: the cart validator still reads the ambient context new CalculationContext(_contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, _contextAccessor.WorkContext.WorkingCurrency, diff --git a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartTotalAmountValidator.cs b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartTotalAmountValidator.cs index 102150feb8..ae3966a703 100644 --- a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartTotalAmountValidator.cs +++ b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartTotalAmountValidator.cs @@ -4,15 +4,11 @@ using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Utilities.Checkout; -using Grand.Domain.Customers; -using Grand.Domain.Directory; using Grand.Domain.Orders; namespace Grand.Business.Checkout.Validators; public record ShoppingCartTotalAmountValidatorRecord( - Customer Customer, - Currency Currency, IList Cart, CalculationContext Context); @@ -28,7 +24,7 @@ public ShoppingCartTotalAmountValidator( { RuleFor(x => x).CustomAsync(async (value, context, _) => { - var customerGroups = await groupService.GetAllByIds(value.Customer.Groups.ToArray()); + var customerGroups = await groupService.GetAllByIds(value.Context.Customer.Groups.ToArray()); var minRoles = customerGroups.OrderBy(x => x.MinOrderAmount).FirstOrDefault(x => x.MinOrderAmount.HasValue); var minOrderAmount = minRoles?.MinOrderAmount ?? 0; @@ -55,9 +51,9 @@ public ShoppingCartTotalAmountValidator( { var minOrderSubtotalAmount = await currencyService.ConvertFromPrimaryStoreCurrency(orderSettings.MinOrderSubtotalAmount, - value.Currency); + value.Context.Currency); context.AddFailure(string.Format(translationService.GetResource("Checkout.MinOrderSubtotalAmount"), - priceFormatter.FormatPrice(minOrderSubtotalAmount, value.Currency))); + priceFormatter.FormatPrice(minOrderSubtotalAmount, value.Context.Currency))); } }); } diff --git a/src/Business/Grand.Business.Core/Commands/Checkout/Orders/ValidateMinShoppingCartSubtotalAmountCommand.cs b/src/Business/Grand.Business.Core/Commands/Checkout/Orders/ValidateMinShoppingCartSubtotalAmountCommand.cs index 7709bb371d..1f69ec782e 100644 --- a/src/Business/Grand.Business.Core/Commands/Checkout/Orders/ValidateMinShoppingCartSubtotalAmountCommand.cs +++ b/src/Business/Grand.Business.Core/Commands/Checkout/Orders/ValidateMinShoppingCartSubtotalAmountCommand.cs @@ -1,5 +1,4 @@ using Grand.Business.Core.Utilities.Checkout; -using Grand.Domain.Customers; using Grand.Domain.Orders; using Grand.Mediator; @@ -7,7 +6,6 @@ namespace Grand.Business.Core.Commands.Checkout.Orders; public class ValidateMinShoppingCartSubtotalAmountCommand : IRequest { - public Customer Customer { get; set; } public IList Cart { get; set; } public CalculationContext Context { get; set; } } \ No newline at end of file diff --git a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/ValidateMinShoppingCartSubtotalAmountCommandHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/ValidateMinShoppingCartSubtotalAmountCommandHandlerTests.cs index 843c54ac64..4ca0cc2fcb 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/ValidateMinShoppingCartSubtotalAmountCommandHandlerTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/ValidateMinShoppingCartSubtotalAmountCommandHandlerTests.cs @@ -30,7 +30,6 @@ public void Init() public async Task HandleTest() { var command = new ValidateMinShoppingCartSubtotalAmountCommand { - Customer = new Customer(), Cart = new List { new() }, Context = new Grand.Business.Core.Utilities.Checkout.CalculationContext(new Customer(), new Grand.Domain.Stores.Store(), new Grand.Domain.Directory.Currency(), diff --git a/src/Web/Grand.Web/Controllers/CheckoutController.cs b/src/Web/Grand.Web/Controllers/CheckoutController.cs index 971499d6c7..3ba659f5bd 100644 --- a/src/Web/Grand.Web/Controllers/CheckoutController.cs +++ b/src/Web/Grand.Web/Controllers/CheckoutController.cs @@ -938,7 +938,8 @@ private async Task LoadStepAfterShippingMethod(IList { - private readonly IContextAccessor _contextAccessor; private readonly ICurrencyService _currencyService; private readonly ILoyaltyPointsService _loyaltyPointsService; @@ -37,10 +35,8 @@ public GetPaymentMethodHandler(ILoyaltyPointsService loyaltyPointsService, IPaymentService paymentService, ITaxService taxService, LoyaltyPointsSettings loyaltyPointsSettings, - PaymentSettings paymentSettings, - IContextAccessor contextAccessor) + PaymentSettings paymentSettings) { - _contextAccessor = contextAccessor; _loyaltyPointsService = loyaltyPointsService; _orderTotalCalculationService = orderTotalCalculationService; _currencyService = currencyService; @@ -70,7 +66,9 @@ await _orderTotalCalculationService.ConvertLoyaltyPointsToAmount(loyaltyPointsBa model.LoyaltyPointsAmount = _priceFormatter.FormatPrice(loyaltyPointsAmount, request.Currency); model.LoyaltyPointsBalance = loyaltyPointsBalance; var shoppingCartTotalBase = - (await _orderTotalCalculationService.GetShoppingCartTotal(request.Cart, _contextAccessor.CalculationContext(), true)).shoppingCartTotal; + (await _orderTotalCalculationService.GetShoppingCartTotal(request.Cart, + new CalculationContext(request.Customer, request.Store, request.Currency, + request.TaxDisplayType), true)).shoppingCartTotal; model.LoyaltyPointsEnoughToPayForOrder = shoppingCartTotalBase is 0; } } @@ -96,7 +94,7 @@ await _orderTotalCalculationService.ConvertLoyaltyPointsToAmount(loyaltyPointsBa var paymentMethodAdditionalFee = await _paymentService.GetAdditionalHandlingFee(request.Cart, pm.SystemName); var rate = (await _taxService.GetPaymentMethodAdditionalFee(paymentMethodAdditionalFee, - _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, request.Customer, request.Store)) + request.TaxDisplayType == TaxDisplayType.IncludingTax, request.Customer, request.Store)) .paymentPrice; if (rate > 0) pmModel.Fee = _priceFormatter.FormatPrice(rate, request.Currency); diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs index 936987903e..72395fb36c 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs @@ -5,12 +5,11 @@ using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Checkout; using Grand.Domain.Catalog; using Grand.Domain.Media; using Grand.Domain.Orders; using Grand.Domain.Tax; -using Grand.Infrastructure; -using Grand.Web.Common.Extensions; using Grand.Web.Extensions; using Grand.Web.Features.Models.ShoppingCart; using Grand.Web.Models.Media; @@ -21,7 +20,6 @@ namespace Grand.Web.Features.Handlers.ShoppingCart; public class GetAddToCartHandler : IRequestHandler { - private readonly IContextAccessor _contextAccessor; private readonly IAuctionService _auctionService; private readonly MediaSettings _mediaSettings; private readonly IOrderCalculationService _orderTotalCalculationService; @@ -49,10 +47,8 @@ public GetAddToCartHandler( IAuctionService auctionService, ShoppingCartSettings shoppingCartSettings, TaxSettings taxSettings, - MediaSettings mediaSettings, - IContextAccessor contextAccessor) + MediaSettings mediaSettings) { - _contextAccessor = contextAccessor; _productAttributeFormatter = productAttributeFormatter; _translationService = translationService; _taxService = taxService; @@ -161,7 +157,9 @@ await _productAttributeFormatter.FormatAttributes(request.Product, request.Attri var subTotalIncludingTax = request.TaxDisplayType == TaxDisplayType.IncludingTax && !_taxSettings.ForceTaxExclusionFromOrderSubtotal; var shoppingCartSubTotal = - await _orderTotalCalculationService.GetShoppingCartSubTotal(cart, _contextAccessor.CalculationContext(), subTotalIncludingTax); + await _orderTotalCalculationService.GetShoppingCartSubTotal(cart, + new CalculationContext(request.Customer, request.Store, request.Currency, + request.TaxDisplayType), subTotalIncludingTax); model.SubTotalIncludingTax = subTotalIncludingTax; model.SubTotal = _priceFormatter.FormatPrice(shoppingCartSubTotal.subTotalWithoutDiscount, request.Currency); diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs index 8e9241c196..99431262da 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs @@ -8,14 +8,13 @@ using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Checkout; using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Media; using Grand.Domain.Orders; using Grand.Domain.Tax; using Grand.Infrastructure.Configuration; -using Grand.Infrastructure; -using Grand.Web.Common.Extensions; using Grand.Web.Extensions; using Grand.Web.Features.Models.ShoppingCart; using Grand.Web.Models.Media; @@ -26,7 +25,6 @@ namespace Grand.Web.Features.Handlers.ShoppingCart; public class GetMiniShoppingCartHandler : IRequestHandler { - private readonly IContextAccessor _contextAccessor; private readonly ICheckoutAttributeService _checkoutAttributeService; private readonly IGroupService _groupService; private readonly LinkGenerator _linkGenerator; @@ -64,10 +62,8 @@ public GetMiniShoppingCartHandler( OrderSettings orderSettings, OidcLoginConfig oidcLoginConfig, TaxSettings taxSettings, - MediaSettings mediaSettings, - IContextAccessor contextAccessor) + MediaSettings mediaSettings) { - _contextAccessor = contextAccessor; _shoppingCartService = shoppingCartService; _orderTotalCalculationService = orderTotalCalculationService; _priceFormatter = priceFormatter; @@ -111,11 +107,14 @@ public async Task Handle(GetMiniShoppingCart request, model.TotalProducts = cart.Sum(x => x.Quantity); if (!cart.Any()) return model; + var context = new CalculationContext(request.Customer, request.Store, request.Currency, + request.TaxDisplayType); + //subtotal var subTotalIncludingTax = request.TaxDisplayType == TaxDisplayType.IncludingTax && !_taxSettings.ForceTaxExclusionFromOrderSubtotal; var shoppingCartSubTotal = - await _orderTotalCalculationService.GetShoppingCartSubTotal(cart, _contextAccessor.CalculationContext(), subTotalIncludingTax); + await _orderTotalCalculationService.GetShoppingCartSubTotal(cart, context, subTotalIncludingTax); model.SubTotalIncludingTax = subTotalIncludingTax; model.SubTotal = _priceFormatter.FormatPrice(shoppingCartSubTotal.subTotalWithoutDiscount, request.Currency); @@ -126,8 +125,7 @@ public async Task Handle(GetMiniShoppingCart request, .Any(); var minOrderSubtotalAmountOk = await _mediator.Send(new ValidateMinShoppingCartSubtotalAmountCommand { - Context = _contextAccessor.CalculationContext(), - Customer = request.Customer, + Context = context, Cart = cart.Where (x => x.ShoppingCartTypeId is ShoppingCartType.ShoppingCart or ShoppingCartType.Auctions).ToList() }, cancellationToken); diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetOrderTotalsHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetOrderTotalsHandler.cs index 5553de3dc6..5b820989c5 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetOrderTotalsHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetOrderTotalsHandler.cs @@ -10,8 +10,6 @@ using Grand.Domain.Orders; using Grand.Domain.Shipping; using Grand.Domain.Tax; -using Grand.Infrastructure; -using Grand.Web.Common.Extensions; using Grand.Web.Features.Models.ShoppingCart; using Grand.Web.Models.ShoppingCart; using Grand.Mediator; @@ -20,7 +18,6 @@ namespace Grand.Web.Features.Handlers.ShoppingCart; public class GetOrderTotalsHandler : IRequestHandler { - private readonly IContextAccessor _contextAccessor; private readonly ICurrencyService _currencyService; private readonly LoyaltyPointsSettings _loyaltyPointsSettings; private readonly IMediator _mediator; @@ -39,10 +36,8 @@ public GetOrderTotalsHandler( ITaxService taxService, IMediator mediator, TaxSettings taxSettings, - LoyaltyPointsSettings loyaltyPointsSettings, - IContextAccessor contextAccessor) + LoyaltyPointsSettings loyaltyPointsSettings) { - _contextAccessor = contextAccessor; _orderTotalCalculationService = orderTotalCalculationService; _currencyService = currencyService; _priceFormatter = priceFormatter; @@ -61,29 +56,32 @@ public async Task Handle(GetOrderTotals request, CancellationT if (!request.Cart.Any()) return model; + var context = new CalculationContext(request.Customer, request.Store, request.Currency, + request.TaxDisplayType); + //subtotal - await PrepareSubtotal(model, request); + await PrepareSubtotal(model, request, context); //shipping info - await PrepareShippingInfo(model, request); + await PrepareShippingInfo(model, request, context); //payment method fee await PreparePayment(model, request); //tax - await PrepareTax(model, request); + await PrepareTax(model, request, context); //total - await PrepareTotal(model, request); + await PrepareTotal(model, request, context); return model; } - private async Task PrepareSubtotal(OrderTotalsModel model, GetOrderTotals request) + private async Task PrepareSubtotal(OrderTotalsModel model, GetOrderTotals request, CalculationContext context) { var subTotalIncludingTax = request.TaxDisplayType == TaxDisplayType.IncludingTax && !_taxSettings.ForceTaxExclusionFromOrderSubtotal; var shoppingCartSubTotal = - await _orderTotalCalculationService.GetShoppingCartSubTotal(request.Cart, _contextAccessor.CalculationContext(), subTotalIncludingTax); + await _orderTotalCalculationService.GetShoppingCartSubTotal(request.Cart, context, subTotalIncludingTax); model.SubTotalIncludingTax = subTotalIncludingTax; model.SubTotal = _priceFormatter.FormatPrice(shoppingCartSubTotal.subTotalWithoutDiscount, request.Currency); model.SubTotalValue = shoppingCartSubTotal.subTotalWithoutDiscount; @@ -96,13 +94,14 @@ private async Task PrepareSubtotal(OrderTotalsModel model, GetOrderTotals reques } } - private async Task PrepareShippingInfo(OrderTotalsModel model, GetOrderTotals request) + private async Task PrepareShippingInfo(OrderTotalsModel model, GetOrderTotals request, + CalculationContext context) { model.RequiresShipping = request.Cart.RequiresShipping(); if (model.RequiresShipping) { var shoppingCartShippingBase = - (await _orderTotalCalculationService.GetShoppingCartShippingTotal(request.Cart, _contextAccessor.CalculationContext())) + (await _orderTotalCalculationService.GetShoppingCartShippingTotal(request.Cart, context)) .shoppingCartShippingTotal; if (shoppingCartShippingBase.HasValue) { @@ -136,7 +135,7 @@ private async Task PreparePayment(OrderTotalsModel model, GetOrderTotals request } } - private async Task PrepareTax(OrderTotalsModel model, GetOrderTotals request) + private async Task PrepareTax(OrderTotalsModel model, GetOrderTotals request, CalculationContext context) { bool displayTax; bool displayTaxRates; @@ -147,7 +146,7 @@ private async Task PrepareTax(OrderTotalsModel model, GetOrderTotals request) } else { - var taxtotal = await _orderTotalCalculationService.GetTaxTotal(request.Cart, _contextAccessor.CalculationContext()); + var taxtotal = await _orderTotalCalculationService.GetTaxTotal(request.Cart, context); var taxRates = taxtotal.taxRates; if (taxtotal.taxtotal == 0 && _taxSettings.HideZeroTax) @@ -173,9 +172,9 @@ private async Task PrepareTax(OrderTotalsModel model, GetOrderTotals request) model.DisplayTax = displayTax; } - private async Task PrepareTotal(OrderTotalsModel model, GetOrderTotals request) + private async Task PrepareTotal(OrderTotalsModel model, GetOrderTotals request, CalculationContext context) { - var carttotal = await _orderTotalCalculationService.GetShoppingCartTotal(request.Cart, _contextAccessor.CalculationContext()); + var carttotal = await _orderTotalCalculationService.GetShoppingCartTotal(request.Cart, context); var shoppingCartTotalBase = carttotal.shoppingCartTotal; var orderTotalDiscountAmountBase = carttotal.discountAmount; var appliedGiftVouchers = carttotal.appliedGiftVouchers; @@ -214,7 +213,7 @@ private async Task PrepareTotal(OrderTotalsModel model, GetOrderTotals request) shoppingCartTotalBase.HasValue) { var shippingBaseInclTax = model.RequiresShipping - ? (await _orderTotalCalculationService.GetShoppingCartShippingTotal(request.Cart, _contextAccessor.CalculationContext(), true)) + ? (await _orderTotalCalculationService.GetShoppingCartShippingTotal(request.Cart, context, true)) .shoppingCartShippingTotal : 0; var earnLoyaltyPoints = shoppingCartTotalBase.Value - shippingBaseInclTax ?? 0; diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs index b96244455a..37f3c6f485 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs @@ -170,7 +170,6 @@ private async Task PrepareSimpleProperties(ShoppingCartModel model, GetShoppingC { var minOrderSubtotalAmountOk = await _mediator.Send(new ValidateMinShoppingCartSubtotalAmountCommand { Context = _contextAccessor.CalculationContext(), - Customer = request.Customer, Cart = request.Cart.Where(x => x.ShoppingCartTypeId is ShoppingCartType.ShoppingCart or ShoppingCartType.Auctions).ToList() }); diff --git a/src/Web/Grand.Web/Features/Models/Checkout/GetPaymentMethod.cs b/src/Web/Grand.Web/Features/Models/Checkout/GetPaymentMethod.cs index 0db29fdae4..b5d1fcda9c 100644 --- a/src/Web/Grand.Web/Features/Models/Checkout/GetPaymentMethod.cs +++ b/src/Web/Grand.Web/Features/Models/Checkout/GetPaymentMethod.cs @@ -3,6 +3,7 @@ using Grand.Domain.Localization; using Grand.Domain.Orders; using Grand.Domain.Stores; +using Grand.Domain.Tax; using Grand.Web.Models.Checkout; using Grand.Mediator; @@ -13,6 +14,7 @@ public class GetPaymentMethod : IRequest public Customer Customer { get; set; } public Domain.Stores.Store Store { get; set; } public Currency Currency { get; set; } + public TaxDisplayType TaxDisplayType { get; set; } public Language Language { get; set; } public IList Cart { get; set; } public string FilterByCountryId { get; set; } From 4660ba89675328ff7eaea55ad493a775daea9e8d Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 18:19:08 +0200 Subject: [PATCH 27/33] Require the language and host on PlaceOrderCommand Both are dereferenced while the order is prepared: the language when the customer has no published language of their own, the host for file links in the checkout attribute description. A sender that left them out got a NullReferenceException instead of an argument error. --- .../Orders/PlaceOrderCommandHandler.cs | 2 ++ .../Checkout/Orders/PlaceOrderCommand.cs | 4 +-- .../Orders/PlaceOrderCommandHandlerTests.cs | 34 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs index c9a2803099..6633308e16 100644 --- a/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs +++ b/src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs @@ -141,6 +141,8 @@ public async Task Handle(PlaceOrderCommand command, Cancellati try { ArgumentNullException.ThrowIfNull(command.Context); + ArgumentNullException.ThrowIfNull(command.Language); + ArgumentNullException.ThrowIfNull(command.Host); //prepare order details var details = await PreparePlaceOrderDetails(command); diff --git a/src/Business/Grand.Business.Core/Commands/Checkout/Orders/PlaceOrderCommand.cs b/src/Business/Grand.Business.Core/Commands/Checkout/Orders/PlaceOrderCommand.cs index 4a789305b6..f372553d64 100644 --- a/src/Business/Grand.Business.Core/Commands/Checkout/Orders/PlaceOrderCommand.cs +++ b/src/Business/Grand.Business.Core/Commands/Checkout/Orders/PlaceOrderCommand.cs @@ -14,12 +14,12 @@ public class PlaceOrderCommand : IRequest public CalculationContext Context { get; set; } /// - /// Language used when the customer has no published language of their own + /// Language used when the customer has no published language of their own (required) /// public Language Language { get; set; } /// - /// Store address used for file download links in the checkout attribute description + /// Store address used for file download links in the checkout attribute description (required) /// public DomainHost Host { get; set; } diff --git a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/PlaceOrderCommandHandlerTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/PlaceOrderCommandHandlerTests.cs index 01da9d3266..3a9600a974 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/PlaceOrderCommandHandlerTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Commands/Handlers/Orders/PlaceOrderCommandHandlerTests.cs @@ -70,6 +70,40 @@ public async Task Handle_WithoutContext_ReturnsError() Assert.IsNull(result.PlacedOrder); } + [TestMethod] + public async Task Handle_WithoutLanguage_ReturnsError() + { + //Arrange + var command = new PlaceOrderCommand { + Context = new CalculationContext(new Customer(), new Store(), new Currency(), TaxDisplayType.IncludingTax), + Host = new DomainHost { Url = "https://shop.example/" } + }; + + //Act + var result = await _handler.Handle(command, CancellationToken.None); + + //Assert + Assert.IsTrue(result.Errors.Any(e => e.Contains(nameof(PlaceOrderCommand.Language)))); + Assert.IsNull(result.PlacedOrder); + } + + [TestMethod] + public async Task Handle_WithoutHost_ReturnsError() + { + //Arrange + var command = new PlaceOrderCommand { + Context = new CalculationContext(new Customer(), new Store(), new Currency(), TaxDisplayType.IncludingTax), + Language = new Language { Id = "pl" } + }; + + //Act + var result = await _handler.Handle(command, CancellationToken.None); + + //Assert + Assert.IsTrue(result.Errors.Any(e => e.Contains(nameof(PlaceOrderCommand.Host)))); + Assert.IsNull(result.PlacedOrder); + } + [TestMethod] public async Task Handle_PreparesDetailsFromTheCommand() { From 636de0ce777068368b654391297bbc8243d694dc Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 18:19:08 +0200 Subject: [PATCH 28/33] Require a customer when checkout attributes apply ACL The customer is optional only when ACL is ignored. Without the guard a null customer failed inside the cache key with a NullReferenceException. --- .../CheckoutAttributes/CheckoutAttributeService.cs | 3 +++ .../CheckoutAttributes/CheckoutAttributeServiceTests.cs | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeService.cs b/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeService.cs index 27929ba88a..8190329e68 100644 --- a/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeService.cs +++ b/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeService.cs @@ -57,6 +57,9 @@ public virtual async Task> GetAllCheckoutAttributes(str bool excludeShippableAttributes = false, bool ignoreAcl = false) { var applyAcl = !ignoreAcl && !_accessControlConfig.IgnoreAcl; + if (applyAcl) + ArgumentNullException.ThrowIfNull(customer); + var key = string.Format(CacheKey.CHECKOUTATTRIBUTES_ALL_KEY, storeId, excludeShippableAttributes, ignoreAcl, applyAcl ? string.Join(",", customer.GetCustomerGroupIds()) : string.Empty); return await _cacheBase.GetAsync(key, async () => diff --git a/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeServiceTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeServiceTests.cs index 434206c953..c3af21c7fa 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeServiceTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeServiceTests.cs @@ -76,6 +76,13 @@ public void DeleteCheckoutAttribute_NullArgument_ThrowException() Assert.ThrowsExactlyAsync(async () => await _service.DeleteCheckoutAttribute(null)); } + [TestMethod] + public async Task GetAllCheckoutAttributes_WithoutCustomerWhenAclApplies_ThrowException() + { + await Assert.ThrowsExactlyAsync(async () => + await _service.GetAllCheckoutAttributes("store", null)); + } + [TestMethod] public async Task GetAllCheckoutAttributes_FiltersByTheGroupsOfTheGivenCustomer() { From a059be49a24dfbef5cdea5d7e9a97d5b23ed9a7f Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 18:19:08 +0200 Subject: [PATCH 29/33] Check required products against the guest cart in the login merge Skipping the check entirely let an item whose required product was missing from the guest cart move to the account. The guest cart holds the whole set being moved, so the check runs there, as it did before the explicit context. --- .../Services/Orders/ShoppingCartService.cs | 13 +++++++-- .../Orders/ShoppingCartServiceTests.cs | 29 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartService.cs b/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartService.cs index 273cd2b68d..47d4e63491 100644 --- a/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartService.cs +++ b/src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartService.cs @@ -1,4 +1,4 @@ -using Grand.Business.Core.Commands.Checkout.Orders; +using Grand.Business.Core.Commands.Checkout.Orders; using Grand.Business.Core.Events.Checkout.ShoppingCart; using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Checkout.Orders; @@ -407,14 +407,23 @@ public virtual async Task MigrateShoppingCart(Customer fromCustomer, Customer to return; //the same customer //shopping cart items; the whole cart moves together, so a product's required products - //may not be in the target cart yet when the product itself is added + //may not be in the target cart yet when the product itself is added - they are checked + //against the source cart instead, which holds the whole set being moved var fromCart = fromCustomer.ShoppingCartItems.ToList(); foreach (var sci in fromCart) + { + var product = await _productService.GetProductById(sci.ProductId); + if (product != null && + (await _shoppingCartValidator.GetRequiredProductWarnings(fromCustomer, sci, product, sci.StoreId)).Any()) + continue; + await AddToCart(toCustomer, sci.ProductId, sci.ShoppingCartTypeId, sci.StoreId, sci.WarehouseId, sci.Attributes, sci.EnteredPrice, sci.RentalStartDateUtc, sci.RentalEndDateUtc, sci.Quantity, false, sci.ReservationId, sci.Parameter, sci.Duration, new ShoppingCartValidatorOptions { GetRequiredProductWarnings = false }); + } + foreach (var sci in fromCart) await DeleteShoppingCartItem(fromCustomer, sci); //copy discount and gift voucher coupon codes diff --git a/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartServiceTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartServiceTests.cs index 8376c7708c..9c883d8dc3 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartServiceTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Services/Orders/ShoppingCartServiceTests.cs @@ -195,6 +195,9 @@ public async Task MigrateShoppingCartTest() _shoppingCartValidatorMock.Setup(x => x.GetShoppingCartItemWarnings(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(() => Task.FromResult((IList)new List())); + _shoppingCartValidatorMock.Setup(x => x.GetRequiredProductWarnings(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new List()); var item = await _shoppingCartService.AddToCart(customer, "2", ShoppingCartType.ShoppingCart, ""); var customer2 = new Customer(); @@ -239,6 +242,9 @@ public async Task MigrateShoppingCart_DoesNotRequireRequiredProductsInTheTargetC _shoppingCartValidatorMock.Setup(x => x.GetShoppingCartItemWarnings(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new List()); + _shoppingCartValidatorMock.Setup(x => x.GetRequiredProductWarnings(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new List()); //Act await _shoppingCartService.MigrateShoppingCart(guest, customer, "", false); @@ -249,6 +255,29 @@ public async Task MigrateShoppingCart_DoesNotRequireRequiredProductsInTheTargetC It.IsAny(), It.Is(o => !o.GetRequiredProductWarnings)), Times.Once); } + [TestMethod] + public async Task MigrateShoppingCart_ChecksRequiredProductsAgainstTheSourceCart() + { + //Arrange + var guest = new Customer(); + var item = new ShoppingCartItem + { ProductId = "A", ShoppingCartTypeId = ShoppingCartType.ShoppingCart, StoreId = "store", Quantity = 1 }; + guest.ShoppingCartItems.Add(item); + var customer = new Customer(); + var product = new Product { Id = "A", Published = true, RequireOtherProducts = true, RequiredProductIds = "B" }; + _productServiceMock.Setup(a => a.GetProductById("A", false)).ReturnsAsync(product); + _shoppingCartValidatorMock.Setup(x => x.GetRequiredProductWarnings(guest, item, product, "store")) + .ReturnsAsync(new List { "Product B is required" }); + + //Act + await _shoppingCartService.MigrateShoppingCart(guest, customer, "store", false); + + //Assert + Assert.IsEmpty(customer.ShoppingCartItems); + _shoppingCartValidatorMock.Verify(x => x.GetShoppingCartItemWarnings(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + [TestMethod] public async Task MigrateShoppingCart_MovesCheckoutAttributesOfTheGivenStore() { From 058f08932e40bfd22936c37fff03f460266685d1 Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 18:19:09 +0200 Subject: [PATCH 30/33] Build the mini cart and add-to-cart models from their request Both handlers read the cart and the calculation context from the request context while the rest of the model used the request's customer and store. Every sender fills them from the current request, so the result is unchanged. --- .../ShoppingCart/GetAddToCartHandler.cs | 15 +++++++-------- .../ShoppingCart/GetMiniShoppingCartHandler.cs | 17 ++++++++--------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs index 536b30f433..122c294b4e 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs @@ -5,12 +5,11 @@ using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Checkout; using Grand.Domain.Catalog; using Grand.Domain.Media; using Grand.Domain.Orders; using Grand.Domain.Tax; -using Grand.Infrastructure; -using Grand.Web.Common.Extensions; using Grand.Web.Extensions; using Grand.Web.Features.Models.ShoppingCart; using Grand.Web.Models.Media; @@ -21,7 +20,6 @@ namespace Grand.Web.Features.Handlers.ShoppingCart; public class GetAddToCartHandler : IRequestHandler { - private readonly IContextAccessor _contextAccessor; private readonly IAuctionService _auctionService; private readonly MediaSettings _mediaSettings; private readonly IOrderCalculationService _orderTotalCalculationService; @@ -49,10 +47,8 @@ public GetAddToCartHandler( IAuctionService auctionService, ShoppingCartSettings shoppingCartSettings, TaxSettings taxSettings, - MediaSettings mediaSettings, - IContextAccessor contextAccessor) + MediaSettings mediaSettings) { - _contextAccessor = contextAccessor; _productAttributeFormatter = productAttributeFormatter; _translationService = translationService; _taxService = taxService; @@ -68,6 +64,9 @@ public GetAddToCartHandler( _mediaSettings = mediaSettings; } + private static CalculationContext CalculationContextOf(GetAddToCart request) => + new(request.Customer, request.Store, request.Currency, request.TaxDisplayType); + public async Task Handle(GetAddToCart request, CancellationToken cancellationToken) { var model = new AddToCartModel { @@ -136,7 +135,7 @@ await _productAttributeFormatter.FormatAttributes(request.Product, request.Attri model.Picture = await PrepareCartItemPicture(request); } - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, request.Store.Id, request.CartType); + var cart = await _shoppingCartService.GetShoppingCart(request.Customer, request.Store.Id, request.CartType); if (request.CartType != ShoppingCartType.Auctions) { @@ -161,7 +160,7 @@ await _productAttributeFormatter.FormatAttributes(request.Product, request.Attri var subTotalIncludingTax = request.TaxDisplayType == TaxDisplayType.IncludingTax && !_taxSettings.ForceTaxExclusionFromOrderSubtotal; var shoppingCartSubTotal = - await _orderTotalCalculationService.GetShoppingCartSubTotal(cart, _contextAccessor.CalculationContext(), subTotalIncludingTax); + await _orderTotalCalculationService.GetShoppingCartSubTotal(cart, CalculationContextOf(request), subTotalIncludingTax); model.SubTotalIncludingTax = subTotalIncludingTax; model.SubTotal = _priceFormatter.FormatPrice(shoppingCartSubTotal.subTotalWithoutDiscount, request.Currency); diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs index d1e90bc3c6..1e970b4293 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs @@ -8,14 +8,13 @@ using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Checkout; using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Media; using Grand.Domain.Orders; using Grand.Domain.Tax; using Grand.Infrastructure.Configuration; -using Grand.Infrastructure; -using Grand.Web.Common.Extensions; using Grand.Web.Extensions; using Grand.Web.Features.Models.ShoppingCart; using Grand.Web.Models.Media; @@ -26,7 +25,6 @@ namespace Grand.Web.Features.Handlers.ShoppingCart; public class GetMiniShoppingCartHandler : IRequestHandler { - private readonly IContextAccessor _contextAccessor; private readonly ICheckoutAttributeService _checkoutAttributeService; private readonly IGroupService _groupService; private readonly LinkGenerator _linkGenerator; @@ -64,10 +62,8 @@ public GetMiniShoppingCartHandler( OrderSettings orderSettings, OidcLoginConfig oidcLoginConfig, TaxSettings taxSettings, - MediaSettings mediaSettings, - IContextAccessor contextAccessor) + MediaSettings mediaSettings) { - _contextAccessor = contextAccessor; _shoppingCartService = shoppingCartService; _orderTotalCalculationService = orderTotalCalculationService; _priceFormatter = priceFormatter; @@ -88,6 +84,9 @@ public GetMiniShoppingCartHandler( _mediaSettings = mediaSettings; } + private static CalculationContext CalculationContextOf(GetMiniShoppingCart request) => + new(request.Customer, request.Store, request.Currency, request.TaxDisplayType); + public async Task Handle(GetMiniShoppingCart request, CancellationToken cancellationToken) { @@ -107,7 +106,7 @@ public async Task Handle(GetMiniShoppingCart request, if (_shoppingCartSettings.AllowOnHoldCart) shoppingCartTypes.Add(ShoppingCartType.OnHoldCart); - var cart = await _shoppingCartService.GetShoppingCart(_contextAccessor.WorkContext.CurrentCustomer, request.Store.Id, shoppingCartTypes.ToArray()); + var cart = await _shoppingCartService.GetShoppingCart(request.Customer, request.Store.Id, shoppingCartTypes.ToArray()); model.TotalProducts = cart.Sum(x => x.Quantity); if (!cart.Any()) return model; @@ -115,7 +114,7 @@ public async Task Handle(GetMiniShoppingCart request, var subTotalIncludingTax = request.TaxDisplayType == TaxDisplayType.IncludingTax && !_taxSettings.ForceTaxExclusionFromOrderSubtotal; var shoppingCartSubTotal = - await _orderTotalCalculationService.GetShoppingCartSubTotal(cart, _contextAccessor.CalculationContext(), subTotalIncludingTax); + await _orderTotalCalculationService.GetShoppingCartSubTotal(cart, CalculationContextOf(request), subTotalIncludingTax); model.SubTotalIncludingTax = subTotalIncludingTax; model.SubTotal = _priceFormatter.FormatPrice(shoppingCartSubTotal.subTotalWithoutDiscount, request.Currency); @@ -126,7 +125,7 @@ public async Task Handle(GetMiniShoppingCart request, .Any(); var minOrderSubtotalAmountOk = await _mediator.Send(new ValidateMinShoppingCartSubtotalAmountCommand { - Context = _contextAccessor.CalculationContext(), + Context = CalculationContextOf(request), Customer = request.Customer, Cart = cart.Where (x => x.ShoppingCartTypeId is ShoppingCartType.ShoppingCart or ShoppingCartType.Auctions).ToList() From 8694a5cb47825b3669f4c4529be4906ac047d5f6 Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 18:19:09 +0200 Subject: [PATCH 31/33] Validate a shared wishlist for its owner The cart warnings used the visitor's calculation context. They now use the wishlist owner and store from the request, with the visitor's currency and tax display. --- .../Features/Handlers/ShoppingCart/GetWishlistHandler.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs index 9bf902eead..472408f59a 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs @@ -77,6 +77,9 @@ public GetWishlistHandler( _enumTranslationService = enumTranslationService; } + private static CalculationContext CalculationContextOf(GetWishlist request) => + new(request.Customer, request.Store, request.Currency, request.TaxDisplayType); + public async Task Handle(GetWishlist request, CancellationToken cancellationToken) { var model = new WishlistModel { @@ -97,7 +100,7 @@ public async Task Handle(GetWishlist request, CancellationToken c //cart warnings var cartWarnings = await _shoppingCartValidator.GetShoppingCartWarnings(request.Cart, null, - _contextAccessor.CalculationContext(), false, false); + CalculationContextOf(request), false, false); foreach (var warning in cartWarnings) model.Warnings.Add(warning); From b4914b430d94b98cdb769d752083d8f24d3fc479 Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 21:19:40 +0200 Subject: [PATCH 32/33] Pass a CatalogAccess to the catalog read services The catalog read services took a customer, a store id and a showHidden flag whose meaning depended on each other: the customer had to be null exactly when hidden records were asked for. CatalogAccess.For(customer, storeId) and CatalogAccess.IncludingHidden(storeId) make that choice one argument. The store limitation now applies whenever a store id is set, so the back office passes IncludingHidden() where it ignored the store before. The storefront builds its access from the request with IContextAccessor.CatalogAccess(). --- .ai/knowledge/scoping.md | 9 + .../GetPersonalizedProductsQueryHandler.cs | 5 +- .../GetRecommendedProductsQueryHandler.cs | 5 +- .../GetSuggestedProductsQueryHandler.cs | 5 +- .../Services/Brands/BrandService.cs | 25 +- .../Services/Categories/CategoryService.cs | 220 ++++++------------ .../Categories/ProductCategoryService.cs | 44 ++-- .../Services/Collections/CollectionService.cs | 45 ++-- .../Collections/ProductCollectionService.cs | 42 ++-- .../ExportImport/BrandImportDataObject.cs | 3 +- .../ExportImport/CategoryImportDataObject.cs | 3 +- .../CollectionImportDataObject.cs | 3 +- .../Services/Products/ProductService.cs | 63 +++-- .../Products/RecentlyViewedProductsService.cs | 3 +- .../Catalog/Brands/IBrandService.cs | 12 +- .../Catalog/Categories/ICategoryService.cs | 44 ++-- .../Categories/IProductCategoryService.cs | 9 +- .../Catalog/Collections/ICollectionService.cs | 19 +- .../Collections/IProductCollectionService.cs | 9 +- .../Catalog/Products/IProductService.cs | 30 +-- .../Utilities/Catalog/CatalogAccess.cs | 50 ++++ .../Common/GetSitemapXMLCommandHandler.cs | 6 +- .../DiscountRulesHasAllProductsController.cs | 10 +- .../DiscountRulesHasOneProductController.cs | 10 +- ...etPersonalizedProductsQueryHandlerTests.cs | 6 +- ...GetRecommendedProductsQueryHandlerTests.cs | 6 +- .../GetSuggestedProductsQueryHandlerTests.cs | 6 +- .../Services/Brands/BrandServiceTests.cs | 7 +- .../CategoryServiceCacheKeyTests.cs | 17 +- .../Categories/CategoryServiceDbTests.cs | 32 ++- .../Categories/CategoryServiceTests.cs | 10 +- .../Categories/CategoryServiceTreeTests.cs | 13 +- .../Categories/ProductCategoryServiceTests.cs | 26 ++- .../Collections/CollectionServiceTests.cs | 7 +- .../ProductCollectionServiceTests.cs | 22 +- .../Services/Products/ProductServiceTests.cs | 31 ++- .../RecentlyViewedProductsServiceTests.cs | 4 +- .../SpecificationAttributeServiceTests.cs | 4 +- .../Utilities/CatalogAccessTests.cs | 54 +++++ .../GetSitemapXmlCommandHandlerTests.cs | 17 +- .../Controllers/BaseBrandControllerTests.cs | 20 +- .../BaseCollectionControllerTests.cs | 6 +- .../BaseDiscountControllerTests.cs | 27 ++- .../Controllers/BaseOrderControllerTests.cs | 15 +- .../Controllers/BaseProductControllerTests.cs | 34 +-- .../BaseProductReviewControllerTests.cs | 9 +- .../Controllers/BaseSearchControllerTests.cs | 29 ++- ...seSpecificationAttributeControllerTests.cs | 9 +- .../Services/ProductViewModelServiceTests.cs | 13 +- .../CatalogAccessExtensionsTests.cs | 30 +++ .../SearchControllerAttributeTests.cs | 6 +- .../Controllers/ProductTagsController.cs | 4 +- .../Controllers/SearchController.cs | 21 +- .../Controllers/BaseBrandController.cs | 6 +- .../Controllers/BaseCategoryController.cs | 4 +- .../Controllers/BaseCollectionController.cs | 6 +- .../Controllers/BaseDiscountController.cs | 11 +- .../Controllers/BaseOrderController.cs | 7 +- .../BaseOrderManagementController.cs | 8 +- .../Controllers/BaseProductController.cs | 17 +- .../BaseProductReviewController.cs | 7 +- .../Controllers/BaseSearchController.cs | 18 +- .../BaseSpecificationAttributeController.cs | 7 +- .../Services/CategoryViewModelService.cs | 12 +- .../Services/CollectionViewModelService.cs | 10 +- .../Services/ProductViewModelService.cs | 21 +- .../Extensions/CatalogAccessExtensions.cs | 19 ++ .../Extensions/ProductList.cs | 8 +- .../Grand.Web/Components/BlogPostProducts.cs | 5 +- .../Components/HomePageBestSellers.cs | 4 +- .../Grand.Web/Components/HomePageProducts.cs | 4 +- .../Components/ProductsAlsoPurchased.cs | 4 +- .../Components/RecommendedProducts.cs | 4 +- .../Grand.Web/Components/RelatedProducts.cs | 4 +- .../Grand.Web/Components/SimilarProducts.cs | 4 +- .../Controllers/ProductController.cs | 6 +- .../Handlers/Catalog/GetBrandAllHandler.cs | 4 +- .../Handlers/Catalog/GetCategoryAllHandler.cs | 5 +- .../GetCategoryFeaturedProductsHandler.cs | 5 +- .../Handlers/Catalog/GetCategoryHandler.cs | 10 +- .../Catalog/GetCategorySimpleHandler.cs | 9 +- .../Catalog/GetChildCategoryIdsHandler.cs | 6 +- .../Catalog/GetCollectionAllHandler.cs | 4 +- .../GetCollectionFeaturedProductsHandler.cs | 5 +- .../Catalog/GetCollectionNavigationHandler.cs | 3 +- .../Catalog/GetHomepageBrandsHandler.cs | 4 +- .../Catalog/GetHomepageCategoryHandler.cs | 5 +- .../Catalog/GetHomepageCollectionsHandler.cs | 4 +- .../Handlers/Catalog/GetMenuHandler.cs | 7 +- .../Handlers/Catalog/GetSearchHandler.cs | 12 +- .../Handlers/Common/GetSitemapHandler.cs | 11 +- .../Products/GetProductDetailsPageHandler.cs | 7 +- .../Products/GetProductOverviewHandler.cs | 4 +- 93 files changed, 807 insertions(+), 653 deletions(-) create mode 100644 src/Business/Grand.Business.Core/Utilities/Catalog/CatalogAccess.cs create mode 100644 src/Tests/Grand.Business.Catalog.Tests/Utilities/CatalogAccessTests.cs create mode 100644 src/Tests/Grand.Web.Common.Tests/Extensions/CatalogAccessExtensionsTests.cs create mode 100644 src/Web/Grand.Web.Common/Extensions/CatalogAccessExtensions.cs diff --git a/.ai/knowledge/scoping.md b/.ai/knowledge/scoping.md index f58444cf9a..19e88f54d7 100644 --- a/.ai/knowledge/scoping.md +++ b/.ai/knowledge/scoping.md @@ -85,6 +85,15 @@ Prices are stored in the store's primary currency and converted for display. Do Code in those paths must receive store, customer, or language **explicitly as parameters**. Reaching for `IWorkContext` there yields null or a stale context. +### Catalog reads take a `CatalogAccess` + +The catalog read services (`IBrandService`, `ICategoryService`, `ICollectionService`, `IProductService` and the product mapping services) take a `CatalogAccess` instead of a customer, a store id and a `showHidden` flag: + +- `CatalogAccess.For(customer, storeId)`: the published records the customer's groups may see in the store. The storefront gets it from the request with `IContextAccessor.CatalogAccess()` (`Grand.Web.Common.Extensions`). +- `CatalogAccess.IncludingHidden(storeId = "")`: every record, unpublished and group-limited ones included, for the admin panels, import, export and the sitemap. A store id still leaves out the records limited to other stores. + +A store id limits the records whenever it is set. Pass `IncludingHidden()` without a store when a back-office screen must see every store's records. + ## Review checklist - [ ] Every list query filters by store, and by vendor when the caller is a vendor. diff --git a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetPersonalizedProductsQueryHandler.cs b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetPersonalizedProductsQueryHandler.cs index d0b6e6637f..c3913254d1 100644 --- a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetPersonalizedProductsQueryHandler.cs +++ b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetPersonalizedProductsQueryHandler.cs @@ -1,5 +1,6 @@ using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Queries.Catalog; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Domain.Catalog; using Grand.Domain.Customers; @@ -41,8 +42,8 @@ orderby cr.DisplayOrder var productIds = query.Take(request.ProductsNumber).ToList(); - var ids = await _productService.GetProductsByIds(productIds.Distinct().ToArray(), customer: request.Customer, - storeId: request.StoreId); + var ids = await _productService.GetProductsByIds(productIds.Distinct().ToArray(), + access: CatalogAccess.For(request.Customer, request.StoreId)); return ids.Where(product => product.Published).ToList(); }); diff --git a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetRecommendedProductsQueryHandler.cs b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetRecommendedProductsQueryHandler.cs index 7a42935889..918e6bdb34 100644 --- a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetRecommendedProductsQueryHandler.cs +++ b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetRecommendedProductsQueryHandler.cs @@ -1,5 +1,6 @@ using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Queries.Catalog; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Domain.Catalog; using Grand.Domain.Customers; @@ -44,8 +45,8 @@ orderby cr.DisplayOrder var productIds = query.ToList(); - var ids = await _productService.GetProductsByIds(productIds.Distinct().ToArray(), customer: request.Customer, - storeId: request.StoreId); + var ids = await _productService.GetProductsByIds(productIds.Distinct().ToArray(), + access: CatalogAccess.For(request.Customer, request.StoreId)); return ids.Where(product => product.Published).ToList(); }); diff --git a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetSuggestedProductsQueryHandler.cs b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetSuggestedProductsQueryHandler.cs index 095dbdcd6c..cc544694dd 100644 --- a/src/Business/Grand.Business.Catalog/Queries/Handlers/GetSuggestedProductsQueryHandler.cs +++ b/src/Business/Grand.Business.Catalog/Queries/Handlers/GetSuggestedProductsQueryHandler.cs @@ -1,5 +1,6 @@ using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Queries.Catalog; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Domain.Catalog; using Grand.Domain.Customers; @@ -38,8 +39,8 @@ orderby cr.DisplayOrder var productIds = query.Take(request.ProductsNumber).ToList(); - var ids = await _productService.GetProductsByIds(productIds.Distinct().ToArray(), customer: request.Customer, - storeId: request.StoreId); + var ids = await _productService.GetProductsByIds(productIds.Distinct().ToArray(), + access: CatalogAccess.For(request.Customer, request.StoreId)); return ids.Where(product => product.Published).ToList(); }); diff --git a/src/Business/Grand.Business.Catalog/Services/Brands/BrandService.cs b/src/Business/Grand.Business.Catalog/Services/Brands/BrandService.cs index 1dfb747bc9..0870eb7346 100644 --- a/src/Business/Grand.Business.Catalog/Services/Brands/BrandService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Brands/BrandService.cs @@ -1,4 +1,5 @@ using Grand.Business.Core.Interfaces.Catalog.Brands; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Domain; using Grand.Domain.Catalog; @@ -48,43 +49,41 @@ public BrandService(ICacheBase cacheBase, /// Gets all brands /// /// Brand name - /// Store ident - /// Customer whose groups limit the result (not read when showHidden is true) + /// Whose view of the brands to return, and the store whose limitations apply /// Page index /// Page size - /// A value that indicates if it should shows hidden records /// Brands public virtual async Task> GetAllBrands(string brandName, - string storeId, - Customer customer, + CatalogAccess access, int pageIndex = 0, - int pageSize = int.MaxValue, - bool showHidden = false) + int pageSize = int.MaxValue) { + ArgumentNullException.ThrowIfNull(access); + var query = from m in _brandRepository.Table select m; - if (!showHidden) + if (!access.ShowHidden) query = query.Where(m => m.Published); if (!string.IsNullOrWhiteSpace(brandName)) query = query.Where(m => m.Name != null && m.Name.ToLower().Contains(brandName.ToLower())); if (!_accessControlConfig.IgnoreAcl || - (!string.IsNullOrEmpty(storeId) && !_accessControlConfig.IgnoreStoreLimitations)) + (!string.IsNullOrEmpty(access.StoreId) && !_accessControlConfig.IgnoreStoreLimitations)) { - if (!showHidden && !_accessControlConfig.IgnoreAcl) + if (!access.ShowHidden && !_accessControlConfig.IgnoreAcl) { //Limited to customer groups rules - var allowedCustomerGroupsIds = customer.GetCustomerGroupIds(); + var allowedCustomerGroupsIds = access.Customer.GetCustomerGroupIds(); query = from p in query where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x)) select p; } - if (!string.IsNullOrEmpty(storeId) && !_accessControlConfig.IgnoreStoreLimitations) + if (!string.IsNullOrEmpty(access.StoreId) && !_accessControlConfig.IgnoreStoreLimitations) //Limited to stores rules query = from p in query - where !p.LimitedToStores || p.Stores.Contains(storeId) + where !p.LimitedToStores || p.Stores.Contains(access.StoreId) select p; } diff --git a/src/Business/Grand.Business.Catalog/Services/Categories/CategoryService.cs b/src/Business/Grand.Business.Catalog/Services/Categories/CategoryService.cs index f09236181a..800df78688 100644 --- a/src/Business/Grand.Business.Catalog/Services/Categories/CategoryService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Categories/CategoryService.cs @@ -1,6 +1,7 @@ using Grand.Business.Core.Extensions; using Grand.Business.Core.Interfaces.Catalog.Categories; using Grand.Business.Core.Interfaces.Common.Security; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Domain; using Grand.Domain.Catalog; @@ -60,20 +61,19 @@ public CategoryService(ICacheBase cacheBase, /// /// Parent Id /// Category name - /// Store ident - /// Customer whose groups limit the result (not read when showHidden is true) + /// Whose view of the categories to return, and the store whose limitations apply /// Page index /// Page size - /// A value that indicates if it should shows hidden records /// Categories public virtual async Task> GetAllCategories(string parentId, string categoryName, - string storeId, Customer customer, - int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false) + CatalogAccess access, int pageIndex = 0, int pageSize = int.MaxValue) { + ArgumentNullException.ThrowIfNull(access); + var query = from c in _categoryRepository.Table select c; - if (!showHidden) + if (!access.ShowHidden) query = query.Where(c => c.Published); if (!string.IsNullOrWhiteSpace(categoryName)) query = query.Where(m => m.Name.ToLowerInvariant().Contains(categoryName.ToLowerInvariant())); @@ -81,25 +81,7 @@ public virtual async Task> GetAllCategories(string parentId if (parentId != null) query = query.Where(m => m.ParentCategoryId == parentId); - if (!_accessControlConfig.IgnoreAcl || - (!string.IsNullOrEmpty(storeId) && !_accessControlConfig.IgnoreStoreLimitations)) - { - if (!showHidden && !_accessControlConfig.IgnoreAcl) - { - //Limited to customer group (access control list) - var allowedCustomerGroupsIds = customer.GetCustomerGroupIds(); - query = from p in query - where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x)) - select p; - } - - if (!string.IsNullOrEmpty(storeId) && !_accessControlConfig.IgnoreStoreLimitations) - //Limited to stores rule - query = from p in query - where !p.LimitedToStores || p.Stores.Contains(storeId) - select p; - } - + query = ApplyAccess(query, access); query = query.OrderBy(c => c.DisplayOrder).ThenBy(c => c.Name); //pagination @@ -118,45 +100,23 @@ public virtual async Task> GetMenuCategories(Customer customer, select c; query = query.Where(c => c.Published && c.IncludeInMenu); - - switch (_accessControlConfig.IgnoreAcl) - { - case true when - string.IsNullOrEmpty(storeId) || _accessControlConfig.IgnoreStoreLimitations: - return await _categoryRepository.ToListAsync(query); - case false: - { - //Limited to customer group (access control list) - var allowedCustomerGroupsIds = customer.GetCustomerGroupIds(); - query = from p in query - where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x)) - select p; - break; - } - } - - if (!string.IsNullOrEmpty(storeId) && !_accessControlConfig.IgnoreStoreLimitations) - //Limited to stores rule - query = from p in query - where !p.LimitedToStores || p.Stores.Contains(storeId) - select p; + query = ApplyAccess(query, CatalogAccess.For(customer, storeId)); return await _categoryRepository.ToListAsync(query); } /// /// Gets all categories filtered by parent category identifier /// - /// Customer whose groups limit the result (not read when showHidden is true) - /// Store whose limitations apply + /// Whose view of the categories to return, and the store whose limitations apply /// Parent category identifier - /// A value that indicates if it should shows hidden records /// A value that indicates if we should load all child levels /// Categories - public virtual async Task> GetAllCategoriesByParentCategoryId(Customer customer, string storeId, - string parentCategoryId = "", - bool showHidden = false, bool includeAllLevels = false) + public virtual async Task> GetAllCategoriesByParentCategoryId(CatalogAccess access, + string parentCategoryId = "", bool includeAllLevels = false) { - var categories = await GetChildCategories(parentCategoryId, showHidden, customer, storeId); + ArgumentNullException.ThrowIfNull(access); + + var categories = await GetChildCategories(parentCategoryId, access); if (!includeAllLevels) return categories; //walk the tree outside the cache: every level resolves through its own cache entry, so no @@ -164,44 +124,26 @@ public virtual async Task> GetAllCategoriesByParentCategoryId(Cu //once per ancestor var allLevels = new List(categories); foreach (var category in categories) - allLevels.AddRange(await GetAllCategoriesByParentCategoryId(customer, storeId, category.Id, showHidden, true)); + allLevels.AddRange(await GetAllCategoriesByParentCategoryId(access, category.Id, true)); return allLevels; } /// - /// Gets the direct children of a category, filtered by the given customer's groups and store + /// Gets the direct children of a category, as the given access sees them /// - private async Task> GetChildCategories(string parentCategoryId, bool showHidden, Customer customer, - string storeId) + private async Task> GetChildCategories(string parentCategoryId, CatalogAccess access) { - //hidden records are not filtered by customer or store, so the key does not depend on them - var key = string.Format(CacheKey.CATEGORIES_BY_PARENT_CATEGORY_ID_KEY, parentCategoryId, showHidden, - showHidden ? "" : string.Join(",", customer.GetCustomerGroupIds()), showHidden ? "" : storeId); + //hidden records are not filtered by customer groups, so the key does not depend on them + var key = string.Format(CacheKey.CATEGORIES_BY_PARENT_CATEGORY_ID_KEY, parentCategoryId, access.ShowHidden, + access.ShowHidden ? "" : string.Join(",", access.Customer.GetCustomerGroupIds()), access.StoreId); return await _cacheBase.GetAsync(key, () => { var query = _categoryRepository.Table.Where(c => c.ParentCategoryId == parentCategoryId); - if (!showHidden) + if (!access.ShowHidden) query = query.Where(c => c.Published); - if (!showHidden && (!_accessControlConfig.IgnoreAcl || !_accessControlConfig.IgnoreStoreLimitations)) - { - if (!_accessControlConfig.IgnoreAcl) - { - //Limited to customer groups rules - var allowedCustomerGroupsIds = customer.GetCustomerGroupIds(); - query = from p in query - where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x)) - select p; - } - - if (!_accessControlConfig.IgnoreStoreLimitations) - //Limited to stores rules - query = from p in query - where !p.LimitedToStores || p.Stores.Contains(storeId) - select p; - } - + query = ApplyAccess(query, access); return Task.FromResult>(query.OrderBy(x => x.DisplayOrder).ToList()); }); } @@ -209,53 +151,31 @@ private async Task> GetChildCategories(string parentCategoryId, /// /// Gets all categories that should be displayed on the home page /// - /// Customer whose groups limit the result (not read when showHidden is true) - /// Store whose limitations apply - /// A value that indicates if it should shows hidden records + /// Whose view of the categories to return, and the store whose limitations apply /// Categories - public virtual async Task> GetAllCategoriesDisplayedOnHomePage(Customer customer, string storeId, - bool showHidden = false) + public virtual async Task> GetAllCategoriesDisplayedOnHomePage(CatalogAccess access) { - //without a customer the ACL check rejects every record, so fail instead of returning nothing - if (!showHidden) ArgumentNullException.ThrowIfNull(customer); + ArgumentNullException.ThrowIfNull(access); var query = _categoryRepository.Table .Where(x => x.Published && x.ShowOnHomePage) .OrderBy(x => x.DisplayOrder); - var categories = await _categoryRepository.ToListAsync(query); - if (!showHidden) - categories = categories - .Where(c => _aclService.Authorize(c, customer) && - _aclService.Authorize(c, storeId)) - .ToList(); - - return categories; + return (await _categoryRepository.ToListAsync(query)).Where(c => IsVisible(c, access)).ToList(); } /// /// Gets all featured products from categories that should be displayed on the home page /// - /// Customer whose groups limit the result (not read when showHidden is true) - /// Store whose limitations apply - /// A value that indicates if it should shows hidden records + /// Whose view of the categories to return, and the store whose limitations apply /// Categories - public virtual async Task> GetAllCategoriesFeaturedProductsOnHomePage(Customer customer, string storeId, - bool showHidden = false) + public virtual async Task> GetAllCategoriesFeaturedProductsOnHomePage(CatalogAccess access) { - //without a customer the ACL check rejects every record, so fail instead of returning nothing - if (!showHidden) ArgumentNullException.ThrowIfNull(customer); + ArgumentNullException.ThrowIfNull(access); var query = _categoryRepository.Table .Where(x => x.Published && x.FeaturedProductsOnHomePage) .OrderBy(x => x.DisplayOrder); - - var categories = await _categoryRepository.ToListAsync(query); - if (!showHidden) - categories = categories - .Where(c => _aclService.Authorize(c, customer) && - _aclService.Authorize(c, storeId)) - .ToList(); - return categories; + return (await _categoryRepository.ToListAsync(query)).Where(c => IsVisible(c, access)).ToList(); } /// @@ -266,45 +186,31 @@ public virtual async Task> GetAllCategoriesFeaturedProductsOnHom /// Categories public virtual async Task> GetAllCategoriesSearchBox(Customer customer, string storeId) { - //without a customer the ACL check rejects every record, so fail instead of returning nothing - ArgumentNullException.ThrowIfNull(customer); + var access = CatalogAccess.For(customer, storeId); var query = _categoryRepository.Table .Where(x => x.Published && x.ShowOnSearchBox) .OrderBy(x => x.SearchBoxDisplayOrder); - - var categories = (await _categoryRepository.ToListAsync(query)) - .Where(c => _aclService.Authorize(c, customer) && - _aclService.Authorize(c, storeId)) - .ToList(); - - return categories; + return (await _categoryRepository.ToListAsync(query)).Where(c => IsVisible(c, access)).ToList(); } /// /// Get category breadcrumb /// /// Category - /// Customer whose groups limit the result (not read when showHidden is true) - /// Store whose limitations apply - /// A value that indicates if it should shows hidden records + /// Whose view of the categories to return, and the store whose limitations apply /// Category breadcrumb - public virtual async Task> GetCategoryBreadCrumb(Category category, Customer customer, string storeId, - bool showHidden = false) + public virtual async Task> GetCategoryBreadCrumb(Category category, CatalogAccess access) { - //without a customer the ACL check rejects every record, so fail instead of returning nothing - if (!showHidden) ArgumentNullException.ThrowIfNull(customer); + ArgumentNullException.ThrowIfNull(access); var result = new List(); //used to avoid circular references var alreadyProcessedCategoryIds = new List(); - while (category != null && //not null - (showHidden || category.Published) && //published - (showHidden || - _aclService.Authorize(category, customer)) && //limited to customer groups - (showHidden || _aclService.Authorize(category, storeId)) && //limited to store + while (category != null && //not null + IsVisible(category, access) && //published, limited to customer groups and store !alreadyProcessedCategoryIds.Contains(category.Id)) { result.Add(category); @@ -323,26 +229,20 @@ public virtual async Task> GetCategoryBreadCrumb(Category catego /// /// Category /// All categories - /// Customer whose groups limit the result (not read when showHidden is true) - /// Store whose limitations apply - /// A value that indicates if it should shows hidden records + /// Whose view of the categories to return, and the store whose limitations apply /// Category breadcrumb public virtual IList GetCategoryBreadCrumb(Category category, IList allCategories, - Customer customer, string storeId, bool showHidden = false) + CatalogAccess access) { - //without a customer the ACL check rejects every record, so fail instead of returning nothing - if (!showHidden) ArgumentNullException.ThrowIfNull(customer); + ArgumentNullException.ThrowIfNull(access); var result = new List(); //used to avoid circular references var alreadyProcessedCategoryIds = new List(); - while (category != null && //not null - (showHidden || category.Published) && //published - (showHidden || - _aclService.Authorize(category, customer)) && //limited to customer groups - (showHidden || _aclService.Authorize(category, storeId)) && //limited to store + while (category != null && //not null + IsVisible(category, access) && //published, limited to customer groups and store !alreadyProcessedCategoryIds.Contains(category.Id)) //avoid circular references { result.Add(category); @@ -371,7 +271,7 @@ public virtual async Task GetFormattedBreadCrumb(Category category, stri { var result = string.Empty; - var breadcrumb = await GetCategoryBreadCrumb(category, customer: null, storeId: "", showHidden: true); + var breadcrumb = await GetCategoryBreadCrumb(category, CatalogAccess.IncludingHidden()); for (var i = 0; i <= breadcrumb.Count - 1; i++) { var categoryName = breadcrumb[i].GetTranslation(x => x.Name, languageId); @@ -397,7 +297,7 @@ public virtual string GetFormattedBreadCrumb(Category category, { var result = string.Empty; - var breadcrumb = GetCategoryBreadCrumb(category, allCategories, customer: null, storeId: "", showHidden: true); + var breadcrumb = GetCategoryBreadCrumb(category, allCategories, CatalogAccess.IncludingHidden()); for (var i = 0; i <= breadcrumb.Count - 1; i++) { var categoryName = breadcrumb[i].GetTranslation(x => x.Name, languageId); @@ -513,5 +413,37 @@ public virtual async Task DeleteCategory(Category category) await _mediator.EntityDeleted(category); } + /// + /// Limits the query to the categories the access may see + /// + private IQueryable ApplyAccess(IQueryable query, CatalogAccess access) + { + if (!access.ShowHidden && !_accessControlConfig.IgnoreAcl) + { + //Limited to customer group (access control list) + var allowedCustomerGroupsIds = access.Customer.GetCustomerGroupIds(); + query = from p in query + where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x)) + select p; + } + + if (!string.IsNullOrEmpty(access.StoreId) && !_accessControlConfig.IgnoreStoreLimitations) + //Limited to stores rule + query = from p in query + where !p.LimitedToStores || p.Stores.Contains(access.StoreId) + select p; + + return query; + } + + /// + /// Gets a value indicating whether the access may see the category + /// + private bool IsVisible(Category category, CatalogAccess access) + { + return (access.ShowHidden || (category.Published && _aclService.Authorize(category, access.Customer))) + && _aclService.Authorize(category, access.StoreId); + } + #endregion } \ No newline at end of file diff --git a/src/Business/Grand.Business.Catalog/Services/Categories/ProductCategoryService.cs b/src/Business/Grand.Business.Catalog/Services/Categories/ProductCategoryService.cs index 48ae7e1204..6dba399179 100644 --- a/src/Business/Grand.Business.Catalog/Services/Categories/ProductCategoryService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Categories/ProductCategoryService.cs @@ -34,45 +34,41 @@ public ProductCategoryService( /// Gets product category mapping collection /// /// Category identifier - /// Customer whose groups limit the result (not read when showHidden is true) - /// Store whose limitations apply + /// Whose view of the products to return, and the store whose limitations apply /// Page index /// Page size - /// A value that indicates if it should shows hidden records /// Product a category mapping collection public virtual async Task> GetProductCategoriesByCategoryId(string categoryId, - Customer customer, string storeId, int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false) + CatalogAccess access, int pageIndex = 0, int pageSize = int.MaxValue) { + ArgumentNullException.ThrowIfNull(access); + if (string.IsNullOrEmpty(categoryId)) return new PagedList(new List(), pageIndex, pageSize); - var key = string.Format(CacheKey.PRODUCTCATEGORIES_ALLBYCATEGORYID_KEY, showHidden, categoryId, pageIndex, - pageSize, showHidden ? "" : string.Join(",", customer.GetCustomerGroupIds()), - showHidden ? "" : storeId); + //hidden records are not filtered by customer groups, so the key does not depend on them + var key = string.Format(CacheKey.PRODUCTCATEGORIES_ALLBYCATEGORYID_KEY, access.ShowHidden, categoryId, + pageIndex, pageSize, access.ShowHidden ? "" : string.Join(",", access.Customer.GetCustomerGroupIds()), + access.StoreId); return await _cacheBase.GetAsync(key, async () => { var query = _productRepository.Table.Where(x => x.ProductCategories.Any(y => y.CategoryId == categoryId)); - if (!showHidden && (!_accessControlConfig.IgnoreAcl || !_accessControlConfig.IgnoreStoreLimitations)) + if (!access.ShowHidden && !_accessControlConfig.IgnoreAcl) { - if (!_accessControlConfig.IgnoreAcl) - { - //Limited to customer groups - var allowedCustomerGroupsIds = customer.GetCustomerGroupIds(); - query = from p in query - where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x)) - select p; - } - - if (!_accessControlConfig.IgnoreStoreLimitations) - { - //Limited to stores - query = from p in query - where !p.LimitedToStores || p.Stores.Contains(storeId) - select p; - } + //Limited to customer groups + var allowedCustomerGroupsIds = access.Customer.GetCustomerGroupIds(); + query = from p in query + where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x)) + select p; } + if (!string.IsNullOrEmpty(access.StoreId) && !_accessControlConfig.IgnoreStoreLimitations) + //Limited to stores + query = from p in query + where !p.LimitedToStores || p.Stores.Contains(access.StoreId) + select p; + var queryProductCategories = from prod in query from pc in prod.ProductCategories select new ProductsCategory { diff --git a/src/Business/Grand.Business.Catalog/Services/Collections/CollectionService.cs b/src/Business/Grand.Business.Catalog/Services/Collections/CollectionService.cs index d5ac64bf89..68e98408c8 100644 --- a/src/Business/Grand.Business.Catalog/Services/Collections/CollectionService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Collections/CollectionService.cs @@ -1,5 +1,6 @@ using Grand.Business.Core.Interfaces.Catalog.Collections; using Grand.Business.Core.Interfaces.Common.Security; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Domain; using Grand.Domain.Catalog; @@ -52,43 +53,41 @@ public CollectionService(ICacheBase cacheBase, /// Gets all collections /// /// Collection name - /// Store ident - /// Customer whose groups limit the result (not read when showHidden is true) + /// Whose view of the collections to return, and the store whose limitations apply /// Page index /// Page size - /// A value that indicates if it should shows hidden records /// Collections public virtual async Task> GetAllCollections(string collectionName, - string storeId, - Customer customer, + CatalogAccess access, int pageIndex = 0, - int pageSize = int.MaxValue, - bool showHidden = false) + int pageSize = int.MaxValue) { + ArgumentNullException.ThrowIfNull(access); + var query = from m in _collectionRepository.Table select m; - if (!showHidden) + if (!access.ShowHidden) query = query.Where(m => m.Published); if (!string.IsNullOrWhiteSpace(collectionName)) query = query.Where(m => m.Name != null && m.Name.ToLower().Contains(collectionName.ToLower())); if (!_accessControlConfig.IgnoreAcl || - (!string.IsNullOrEmpty(storeId) && !_accessControlConfig.IgnoreStoreLimitations)) + (!string.IsNullOrEmpty(access.StoreId) && !_accessControlConfig.IgnoreStoreLimitations)) { - if (!showHidden && !_accessControlConfig.IgnoreAcl) + if (!access.ShowHidden && !_accessControlConfig.IgnoreAcl) { //Limited to customer groups rules - var allowedCustomerGroupsIds = customer.GetCustomerGroupIds(); + var allowedCustomerGroupsIds = access.Customer.GetCustomerGroupIds(); query = from p in query where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x)) select p; } - if (!string.IsNullOrEmpty(storeId) && !_accessControlConfig.IgnoreStoreLimitations) + if (!string.IsNullOrEmpty(access.StoreId) && !_accessControlConfig.IgnoreStoreLimitations) //Limited to stores rules query = from p in query - where !p.LimitedToStores || p.Stores.Contains(storeId) + where !p.LimitedToStores || p.Stores.Contains(access.StoreId) select p; } @@ -100,25 +99,19 @@ public virtual async Task> GetAllCollections(string colle /// /// Gets all featured products for collections displayed on the home page /// - /// Customer whose groups limit the result (not read when showHidden is true) - /// Store whose limitations apply - /// A value that indicates if it should shows hidden records + /// Whose view of the collections to return, and the store whose limitations apply /// Collections - public virtual async Task> GetAllCollectionFeaturedProductsOnHomePage(Customer customer, string storeId, - bool showHidden = false) + public virtual async Task> GetAllCollectionFeaturedProductsOnHomePage(CatalogAccess access) { - //without a customer the ACL check rejects every record, so fail instead of returning nothing - if (!showHidden) ArgumentNullException.ThrowIfNull(customer); + ArgumentNullException.ThrowIfNull(access); var query = _collectionRepository.Table.Where(x => x.Published && x.FeaturedProductsOnHomePage) .OrderBy(x => x.DisplayOrder); - var collections = await _collectionRepository.ToListAsync(query); - if (!showHidden) - collections = collections - .Where(c => _aclService.Authorize(c, customer) && _aclService.Authorize(c, storeId)) - .ToList(); - return collections; + return (await _collectionRepository.ToListAsync(query)) + .Where(c => (access.ShowHidden || _aclService.Authorize(c, access.Customer)) && + _aclService.Authorize(c, access.StoreId)) + .ToList(); } diff --git a/src/Business/Grand.Business.Catalog/Services/Collections/ProductCollectionService.cs b/src/Business/Grand.Business.Catalog/Services/Collections/ProductCollectionService.cs index 8386ab440b..9a2f8bac02 100644 --- a/src/Business/Grand.Business.Catalog/Services/Collections/ProductCollectionService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Collections/ProductCollectionService.cs @@ -32,41 +32,39 @@ public ProductCollectionService(ICacheBase cacheBase, /// Gets product collection by collection id /// /// Collection id - /// Store ident - /// Customer whose groups limit the result (not read when showHidden is true) + /// Whose view of the products to return, and the store whose limitations apply /// Page index /// Page size - /// A value that indicates if it should shows hidden records /// Product collection collection public virtual async Task> GetProductCollectionsByCollectionId(string collectionId, - string storeId, Customer customer, - int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false) + CatalogAccess access, int pageIndex = 0, int pageSize = int.MaxValue) { - var key = string.Format(CacheKey.PRODUCTCOLLECTIONS_ALLBYCOLLECTIONID_KEY, showHidden, collectionId, pageIndex, - pageSize, showHidden ? "" : string.Join(",", customer.GetCustomerGroupIds()), showHidden ? "" : storeId); + ArgumentNullException.ThrowIfNull(access); + + //hidden records are not filtered by customer groups, so the key does not depend on them + var key = string.Format(CacheKey.PRODUCTCOLLECTIONS_ALLBYCOLLECTIONID_KEY, access.ShowHidden, collectionId, + pageIndex, pageSize, access.ShowHidden ? "" : string.Join(",", access.Customer.GetCustomerGroupIds()), + access.StoreId); return await _cacheBase.GetAsync(key, async () => { var query = _productRepository.Table.Where(x => x.ProductCollections.Any(y => y.CollectionId == collectionId)); - if (!showHidden && (!_accessControlConfig.IgnoreAcl || !_accessControlConfig.IgnoreStoreLimitations)) + if (!access.ShowHidden && !_accessControlConfig.IgnoreAcl) { - if (!_accessControlConfig.IgnoreAcl) - { - //ACL (access control list) - var allowedCustomerGroupsIds = customer.GetCustomerGroupIds(); - query = from p in query - where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x)) - select p; - } - - if (!_accessControlConfig.IgnoreStoreLimitations && !string.IsNullOrEmpty(storeId)) - //Store acl - query = from p in query - where !p.LimitedToStores || p.Stores.Contains(storeId) - select p; + //Limited to customer groups + var allowedCustomerGroupsIds = access.Customer.GetCustomerGroupIds(); + query = from p in query + where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x)) + select p; } + if (!string.IsNullOrEmpty(access.StoreId) && !_accessControlConfig.IgnoreStoreLimitations) + //Limited to stores + query = from p in query + where !p.LimitedToStores || p.Stores.Contains(access.StoreId) + select p; + var queryProductCollection = from prod in query from pm in prod.ProductCollections select new ProductsCollection { diff --git a/src/Business/Grand.Business.Catalog/Services/ExportImport/BrandImportDataObject.cs b/src/Business/Grand.Business.Catalog/Services/ExportImport/BrandImportDataObject.cs index 62c5b1e904..a2fc29e120 100644 --- a/src/Business/Grand.Business.Catalog/Services/ExportImport/BrandImportDataObject.cs +++ b/src/Business/Grand.Business.Catalog/Services/ExportImport/BrandImportDataObject.cs @@ -3,6 +3,7 @@ using Grand.Business.Core.Interfaces.Common.Seo; using Grand.Business.Core.Interfaces.ExportImport; using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Catalog; using Grand.Business.Core.Utilities.System; using Grand.Domain.Catalog; using Grand.Domain.Seo; @@ -148,7 +149,7 @@ private static string Key(BrandDto dto) if (string.IsNullOrEmpty(dto.Name)) return (null, false, null); - var candidates = await _brandService.GetAllBrands(dto.Name, "", customer: null, showHidden: true); + var candidates = await _brandService.GetAllBrands(dto.Name, CatalogAccess.IncludingHidden()); var (found, error) = ImportRows.SingleByName(candidates, b => b.Name, dto.Name, "brand"); return (found, found != null, error); } diff --git a/src/Business/Grand.Business.Catalog/Services/ExportImport/CategoryImportDataObject.cs b/src/Business/Grand.Business.Catalog/Services/ExportImport/CategoryImportDataObject.cs index ec55123978..c034c8cbe8 100644 --- a/src/Business/Grand.Business.Catalog/Services/ExportImport/CategoryImportDataObject.cs +++ b/src/Business/Grand.Business.Catalog/Services/ExportImport/CategoryImportDataObject.cs @@ -3,6 +3,7 @@ using Grand.Business.Core.Interfaces.Common.Seo; using Grand.Business.Core.Interfaces.ExportImport; using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Catalog; using Grand.Business.Core.Utilities.System; using Grand.Domain.Catalog; using Grand.Domain.Seo; @@ -155,7 +156,7 @@ private static string Key(CategoryDto dto) return (null, false, null); var parentId = string.IsNullOrEmpty(dto.ParentCategoryId) ? null : dto.ParentCategoryId; - var candidates = await _categoryService.GetAllCategories(parentId, dto.Name, "", customer: null, showHidden: true); + var candidates = await _categoryService.GetAllCategories(parentId, dto.Name, CatalogAccess.IncludingHidden()); var (found, error) = ImportRows.SingleByName(candidates, c => c.Name, dto.Name, "category"); return (found, found != null, error); } diff --git a/src/Business/Grand.Business.Catalog/Services/ExportImport/CollectionImportDataObject.cs b/src/Business/Grand.Business.Catalog/Services/ExportImport/CollectionImportDataObject.cs index 955130bf75..ebc7dfd26e 100644 --- a/src/Business/Grand.Business.Catalog/Services/ExportImport/CollectionImportDataObject.cs +++ b/src/Business/Grand.Business.Catalog/Services/ExportImport/CollectionImportDataObject.cs @@ -3,6 +3,7 @@ using Grand.Business.Core.Interfaces.Common.Seo; using Grand.Business.Core.Interfaces.ExportImport; using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Catalog; using Grand.Business.Core.Utilities.System; using Grand.Domain.Catalog; using Grand.Domain.Seo; @@ -149,7 +150,7 @@ private static string Key(CollectionDto dto) if (string.IsNullOrEmpty(dto.Name)) return (null, false, null); - var candidates = await _collectionService.GetAllCollections(dto.Name, "", customer: null, showHidden: true); + var candidates = await _collectionService.GetAllCollections(dto.Name, CatalogAccess.IncludingHidden()); var (found, error) = ImportRows.SingleByName(candidates, c => c.Name, dto.Name, "collection"); return (found, found != null, error); } diff --git a/src/Business/Grand.Business.Catalog/Services/Products/ProductService.cs b/src/Business/Grand.Business.Catalog/Services/Products/ProductService.cs index a775eef454..6170d7a7bf 100644 --- a/src/Business/Grand.Business.Catalog/Services/Products/ProductService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Products/ProductService.cs @@ -2,6 +2,7 @@ using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Common.Security; using Grand.Business.Core.Queries.Catalog; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Domain; using Grand.Domain.Catalog; @@ -115,26 +116,22 @@ public virtual async Task GetProductByIdIncludeArch(string productId) /// Get products by identifiers /// /// Product identifiers - /// Customer whose groups limit the result (not read when showHidden is true) - /// Store whose limitations apply - /// Show hidden + /// Whose view of the products to return, and the store whose limitations apply /// Products - public virtual async Task> GetProductsByIds(string[] productIds, Customer customer, string storeId, - bool showHidden = false) + public virtual async Task> GetProductsByIds(string[] productIds, CatalogAccess access) { + ArgumentNullException.ThrowIfNull(access); + if (productIds == null || productIds.Length == 0) return new List(); - //without a customer the ACL check rejects every record, so fail instead of returning nothing - if (!showHidden) ArgumentNullException.ThrowIfNull(customer); - var products = new List(); foreach (var id in productIds) { var product = await GetProductById(id); - if (product != null && (showHidden || (_aclService.Authorize(product, customer) && - _aclService.Authorize(product, storeId) && - product.IsAvailable()))) + if (product != null && + (access.ShowHidden || (_aclService.Authorize(product, access.Customer) && product.IsAvailable())) && + _aclService.Authorize(product, access.StoreId)) products.Add(product); } @@ -453,7 +450,7 @@ public virtual int GetCategoryProductNumber(Customer customer, IList cat /// /// Search products /// - /// Customer whose groups limit the search + /// Whose view of the products to search, and the store whose limitations apply /// /// A value indicating whether we should load the specification /// attribute option identifiers applied to loaded products (all pages) @@ -463,7 +460,6 @@ public virtual int GetCategoryProductNumber(Customer customer, IList cat /// Category identifiers /// Brand ident /// Collection identifier; "" to load all records - /// Store identifier; "" to load all records /// Vendor identifier; "" to load all records /// Warehouse identifier; "" to load all records /// Product type; "" to load all records @@ -488,9 +484,8 @@ public virtual int GetCategoryProductNumber(Customer customer, IList cat /// Filtered product specification identifiers /// Specification options identifiers /// Order by - /// A value indicating whether to show hidden records /// - /// null - process "Published" property according to "showHidden" parameter + /// null - process "Published" property according to "access" /// true - load only "Published" products /// false - load only "Unpublished" products /// @@ -498,15 +493,14 @@ public virtual int GetCategoryProductNumber(Customer customer, IList cat /// Products public virtual async Task<(IPagedList products, IList filterableSpecificationAttributeOptionIds)> SearchProducts( - Customer customer, + CatalogAccess access, bool loadFilterableSpecificationAttributeOptionIds = false, int pageIndex = 0, int pageSize = int.MaxValue, IList categoryIds = null, string brandId = "", string collectionId = "", - string storeId = "", - string vendorId = "", + string vendorId = "", string warehouseId = "", ProductType? productType = null, bool visibleIndividuallyOnly = false, @@ -524,18 +518,19 @@ public virtual int GetCategoryProductNumber(Customer customer, IList cat IList filteredSpecs = null, IList specificationOptions = null, ProductSortingEnum orderBy = ProductSortingEnum.Position, - bool showHidden = false, - bool? overridePublished = null) + bool? overridePublished = null) { + ArgumentNullException.ThrowIfNull(access); + var model = await _mediator.Send(new GetSearchProductsQuery { - Customer = customer, + Customer = access.Customer, LoadFilterableSpecificationAttributeOptionIds = loadFilterableSpecificationAttributeOptionIds, PageIndex = pageIndex, PageSize = pageSize, CategoryIds = categoryIds, BrandId = brandId, CollectionId = collectionId, - StoreId = storeId, + StoreId = access.StoreId, VendorId = vendorId, WarehouseId = warehouseId, ProductType = productType, @@ -554,7 +549,7 @@ public virtual int GetCategoryProductNumber(Customer customer, IList cat FilteredSpecs = filteredSpecs, SpecificationOptions = specificationOptions, OrderBy = orderBy, - ShowHidden = showHidden, + ShowHidden = access.ShowHidden, OverridePublished = overridePublished }); @@ -588,24 +583,21 @@ public virtual async Task> GetProductsByProductAttributeId(s /// Gets associated products /// /// Parent product identifier (used with grouped products) - /// Customer whose groups limit the result (not read when showHidden is true) - /// Store identifier; "" to load all records + /// Whose view of the products to return, and the store whose limitations apply /// Vendor identifier; "" to load all records - /// Show hidden /// Products - public virtual async Task> GetAssociatedProducts(string parentGroupedProductId, Customer customer, - string storeId = "", string vendorId = "", bool showHidden = false) + public virtual async Task> GetAssociatedProducts(string parentGroupedProductId, CatalogAccess access, + string vendorId = "") { - //without a customer the ACL check rejects every record, so fail instead of returning nothing - if (!showHidden) ArgumentNullException.ThrowIfNull(customer); + ArgumentNullException.ThrowIfNull(access); var query = from p in _productRepository.Table select p; query = query.Where(p => p.ParentGroupedProductId == parentGroupedProductId); - if (!showHidden) query = query.Where(p => p.Published); - if (!showHidden) + if (!access.ShowHidden) query = query.Where(p => p.Published); + if (!access.ShowHidden) { var nowUtc = DateTime.UtcNow; //available dates @@ -620,11 +612,10 @@ public virtual async Task> GetAssociatedProducts(string parentGro var products = query.OrderBy(x => x.DisplayOrder).ToList(); //ACL mapping - if (!showHidden) - products = products.Where(x => _aclService.Authorize(x, customer)).ToList(); + if (!access.ShowHidden) + products = products.Where(x => _aclService.Authorize(x, access.Customer)).ToList(); //Store acl - if (!showHidden && !string.IsNullOrEmpty(storeId)) - products = products.Where(x => _aclService.Authorize(x, storeId)).ToList(); + products = products.Where(x => _aclService.Authorize(x, access.StoreId)).ToList(); return await Task.FromResult(products); } diff --git a/src/Business/Grand.Business.Catalog/Services/Products/RecentlyViewedProductsService.cs b/src/Business/Grand.Business.Catalog/Services/Products/RecentlyViewedProductsService.cs index d79d38ee4a..696b6c118f 100644 --- a/src/Business/Grand.Business.Catalog/Services/Products/RecentlyViewedProductsService.cs +++ b/src/Business/Grand.Business.Catalog/Services/Products/RecentlyViewedProductsService.cs @@ -1,4 +1,5 @@ using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Domain.Catalog; using Grand.Domain.Customers; @@ -89,7 +90,7 @@ orderby p.CreatedOnUtc descending public virtual async Task> GetRecentlyViewedProducts(Customer customer, string storeId, int number) { var productIds = await GetRecentlyViewedProductsIds(customer.Id, number); - return (await _productService.GetProductsByIds(productIds.ToArray(), customer, storeId)) + return (await _productService.GetProductsByIds(productIds.ToArray(), CatalogAccess.For(customer, storeId))) .Where(product => product.Published) .ToList(); } diff --git a/src/Business/Grand.Business.Core/Interfaces/Catalog/Brands/IBrandService.cs b/src/Business/Grand.Business.Core/Interfaces/Catalog/Brands/IBrandService.cs index 940a00ec8c..b19bb291a1 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Catalog/Brands/IBrandService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Catalog/Brands/IBrandService.cs @@ -1,6 +1,6 @@ +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain; using Grand.Domain.Catalog; -using Grand.Domain.Customers; namespace Grand.Business.Core.Interfaces.Catalog.Brands; @@ -13,18 +13,14 @@ public interface IBrandService /// Gets all brands /// /// Brand name - /// Store ident - /// Customer whose groups limit the result (not read when showHidden is true) + /// Whose view of the brands to return, and the store whose limitations apply /// Page index /// Page size - /// A value that indicates if it should shows hidden records /// Brands Task> GetAllBrands(string brandName, - string storeId, - Customer customer, + CatalogAccess access, int pageIndex = 0, - int pageSize = int.MaxValue, - bool showHidden = false); + int pageSize = int.MaxValue); /// /// Gets an existing brand by id diff --git a/src/Business/Grand.Business.Core/Interfaces/Catalog/Categories/ICategoryService.cs b/src/Business/Grand.Business.Core/Interfaces/Catalog/Categories/ICategoryService.cs index d9453d3bb2..fbb1760ea0 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Catalog/Categories/ICategoryService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Catalog/Categories/ICategoryService.cs @@ -1,3 +1,4 @@ +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain; using Grand.Domain.Catalog; using Grand.Domain.Customers; @@ -11,14 +12,12 @@ public interface ICategoryService /// /// /// Category name - /// Store id - /// Customer whose groups limit the result (not read when showHidden is true) + /// Whose view of the categories to return, and the store whose limitations apply /// Page index /// Page size - /// A value that indicates if it should shows hidden records /// Categories - Task> GetAllCategories(string parentId, string categoryName, string storeId, Customer customer, - int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false); + Task> GetAllCategories(string parentId, string categoryName, CatalogAccess access, + int pageIndex = 0, int pageSize = int.MaxValue); /// /// Gets all categories @@ -31,32 +30,25 @@ Task> GetAllCategories(string parentId, string categoryName /// /// Gets all categories by parent category /// - /// Customer whose groups limit the result (not read when showHidden is true) - /// Store whose limitations apply + /// Whose view of the categories to return, and the store whose limitations apply /// Parent category id - /// A value that indicates if it should shows hidden records /// A value indicates if we should load all child levels /// Categories - Task> GetAllCategoriesByParentCategoryId(Customer customer, string storeId, string parentCategoryId = "", - bool showHidden = false, bool includeAllLevels = false); + Task> GetAllCategoriesByParentCategoryId(CatalogAccess access, string parentCategoryId = "", + bool includeAllLevels = false); /// /// Gets all categories displayed on the home page /// - /// Customer whose groups limit the result (not read when showHidden is true) - /// Store whose limitations apply - /// A value that indicates if it should shows hidden records + /// Whose view of the categories to return, and the store whose limitations apply /// Categories - Task> GetAllCategoriesDisplayedOnHomePage(Customer customer, string storeId, bool showHidden = false); + Task> GetAllCategoriesDisplayedOnHomePage(CatalogAccess access); /// /// Gets all categories displayed that should be visible on the home page, in featured products section /// - /// Customer whose groups limit the result (not read when showHidden is true) - /// Store whose limitations apply - /// A value that indicates if it should shows hidden records - Task> GetAllCategoriesFeaturedProductsOnHomePage(Customer customer, string storeId, - bool showHidden = false); + /// Whose view of the categories to return, and the store whose limitations apply + Task> GetAllCategoriesFeaturedProductsOnHomePage(CatalogAccess access); /// /// Gets all categories that should be displayed in search box @@ -70,24 +62,18 @@ Task> GetAllCategoriesFeaturedProductsOnHomePage(Customer custom /// Get category breadcrumb /// /// Category - /// Customer whose groups limit the result (not read when showHidden is true) - /// Store whose limitations apply - /// A value that indicates if it should shows hidden records + /// Whose view of the categories to return, and the store whose limitations apply /// Category breadcrumb - Task> GetCategoryBreadCrumb(Category category, Customer customer, string storeId, - bool showHidden = false); + Task> GetCategoryBreadCrumb(Category category, CatalogAccess access); /// /// Get category breadcrumb /// /// Category /// All categories - /// Customer whose groups limit the result (not read when showHidden is true) - /// Store whose limitations apply - /// A value that indicates if it should shows hidden records + /// Whose view of the categories to return, and the store whose limitations apply /// Category breadcrumb - IList GetCategoryBreadCrumb(Category category, IList allCategories, Customer customer, - string storeId, bool showHidden = false); + IList GetCategoryBreadCrumb(Category category, IList allCategories, CatalogAccess access); /// /// Get formatted category breadcrumb diff --git a/src/Business/Grand.Business.Core/Interfaces/Catalog/Categories/IProductCategoryService.cs b/src/Business/Grand.Business.Core/Interfaces/Catalog/Categories/IProductCategoryService.cs index e5d1db8781..c8e57b5ac7 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Catalog/Categories/IProductCategoryService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Catalog/Categories/IProductCategoryService.cs @@ -1,7 +1,6 @@ using Grand.Business.Core.Utilities.Catalog; using Grand.Domain; using Grand.Domain.Catalog; -using Grand.Domain.Customers; namespace Grand.Business.Core.Interfaces.Catalog.Categories; @@ -11,14 +10,12 @@ public interface IProductCategoryService /// Gets product category mapping collection /// /// Category id - /// Customer whose groups limit the result (not read when showHidden is true) - /// Store whose limitations apply + /// Whose view of the products to return, and the store whose limitations apply /// Page index /// Page size - /// A value that indicates if it should shows hidden records /// Product a category mapping collection - Task> GetProductCategoriesByCategoryId(string categoryId, Customer customer, - string storeId, int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false); + Task> GetProductCategoriesByCategoryId(string categoryId, CatalogAccess access, + int pageIndex = 0, int pageSize = int.MaxValue); /// /// Inserts a mapping for product category diff --git a/src/Business/Grand.Business.Core/Interfaces/Catalog/Collections/ICollectionService.cs b/src/Business/Grand.Business.Core/Interfaces/Catalog/Collections/ICollectionService.cs index 3f43b1ad34..e21220c815 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Catalog/Collections/ICollectionService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Catalog/Collections/ICollectionService.cs @@ -1,6 +1,6 @@ +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain; using Grand.Domain.Catalog; -using Grand.Domain.Customers; namespace Grand.Business.Core.Interfaces.Catalog.Collections; @@ -13,28 +13,21 @@ public interface ICollectionService /// Gets all collections /// /// Collection name - /// Store ident - /// Customer whose groups limit the result (not read when showHidden is true) + /// Whose view of the collections to return, and the store whose limitations apply /// Page index /// Page size - /// A value that indicates if it should shows hidden records /// Collections Task> GetAllCollections(string collectionName, - string storeId, - Customer customer, + CatalogAccess access, int pageIndex = 0, - int pageSize = int.MaxValue, - bool showHidden = false); + int pageSize = int.MaxValue); /// /// Gets all collections products displayed on the home page /// - /// Customer whose groups limit the result (not read when showHidden is true) - /// Store whose limitations apply - /// A value that indicates if it should shows hidden records + /// Whose view of the collections to return, and the store whose limitations apply /// Collections - Task> GetAllCollectionFeaturedProductsOnHomePage(Customer customer, string storeId, - bool showHidden = false); + Task> GetAllCollectionFeaturedProductsOnHomePage(CatalogAccess access); /// /// Gets an existing collection by id diff --git a/src/Business/Grand.Business.Core/Interfaces/Catalog/Collections/IProductCollectionService.cs b/src/Business/Grand.Business.Core/Interfaces/Catalog/Collections/IProductCollectionService.cs index bb14935380..415e513ddb 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Catalog/Collections/IProductCollectionService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Catalog/Collections/IProductCollectionService.cs @@ -1,7 +1,6 @@ using Grand.Business.Core.Utilities.Catalog; using Grand.Domain; using Grand.Domain.Catalog; -using Grand.Domain.Customers; namespace Grand.Business.Core.Interfaces.Catalog.Collections; @@ -11,14 +10,12 @@ public interface IProductCollectionService /// Gets product collection by collection id /// /// Collection id - /// Store id - /// Customer whose groups limit the result (not read when showHidden is true) + /// Whose view of the products to return, and the store whose limitations apply /// Page index /// Page size - /// A value that indicates if it should shows hidden records /// Product collection collection - Task> GetProductCollectionsByCollectionId(string collectionId, string storeId, - Customer customer, int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false); + Task> GetProductCollectionsByCollectionId(string collectionId, CatalogAccess access, + int pageIndex = 0, int pageSize = int.MaxValue); /// diff --git a/src/Business/Grand.Business.Core/Interfaces/Catalog/Products/IProductService.cs b/src/Business/Grand.Business.Core/Interfaces/Catalog/Products/IProductService.cs index 6edd4aebde..2235b54ecf 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Catalog/Products/IProductService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Catalog/Products/IProductService.cs @@ -1,3 +1,4 @@ +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain; using Grand.Domain.Catalog; using Grand.Domain.Customers; @@ -44,12 +45,9 @@ public interface IProductService /// Gets products by identifier /// /// Product identifiers - /// Customer whose groups limit the result (not read when showHidden is true) - /// Store whose limitations apply - /// Show hidden + /// Whose view of the products to return, and the store whose limitations apply /// Products - Task> GetProductsByIds(string[] productIds, Customer customer, string storeId, - bool showHidden = false); + Task> GetProductsByIds(string[] productIds, CatalogAccess access); /// /// Gets products by discount @@ -118,7 +116,7 @@ int GetCategoryProductNumber(Customer customer, IList categoryIds = null /// /// Search products /// - /// Customer whose groups limit the search + /// Whose view of the products to search, and the store whose limitations apply /// /// A value indicating whether we should load the specification /// attribute option identifiers applied to loaded products (all pages) @@ -128,7 +126,6 @@ int GetCategoryProductNumber(Customer customer, IList categoryIds = null /// Category identifiers /// Brand ident /// Collection identifier; "" to load all records - /// Store identifier; "" to load all records /// Vendor identifier; "" to load all records /// Warehouse identifier; "" to load all records /// Product type; "" to load all records @@ -154,23 +151,21 @@ int GetCategoryProductNumber(Customer customer, IList categoryIds = null /// Filtered product specification identifiers /// Specification options identifiers /// Order by - /// A value indicating whether to show hidden records /// - /// null - process "Published" property according to "showHidden" parameter + /// null - process "Published" property according to "access" /// true - load only "Published" products /// false - load only "Unpublished" products /// /// Products Task<(IPagedList products, IList filterableSpecificationAttributeOptionIds)> SearchProducts( - Customer customer, + CatalogAccess access, bool loadFilterableSpecificationAttributeOptionIds = false, int pageIndex = 0, int pageSize = int.MaxValue, IList categoryIds = null, string brandId = "", string collectionId = "", - string storeId = "", - string vendorId = "", + string vendorId = "", string warehouseId = "", ProductType? productType = null, bool visibleIndividuallyOnly = false, @@ -188,8 +183,7 @@ int GetCategoryProductNumber(Customer customer, IList categoryIds = null IList filteredSpecs = null, IList specificationOptions = null, ProductSortingEnum orderBy = ProductSortingEnum.Position, - bool showHidden = false, - bool? overridePublished = null); + bool? overridePublished = null); /// /// Gets products by product attribute @@ -206,13 +200,11 @@ Task> GetProductsByProductAttributeId(string productAttribut /// Gets associated products /// /// Parent product identifier (used with grouped products) - /// Customer whose groups limit the result (not read when showHidden is true) - /// Store identifier; "" to load all records + /// Whose view of the products to return, and the store whose limitations apply /// Vendor identifier; "" to load all records - /// A value indicating whether to show hidden records /// Products - Task> GetAssociatedProducts(string parentGroupedProductId, Customer customer, - string storeId = "", string vendorId = "", bool showHidden = false); + Task> GetAssociatedProducts(string parentGroupedProductId, CatalogAccess access, + string vendorId = ""); /// /// Update product associated diff --git a/src/Business/Grand.Business.Core/Utilities/Catalog/CatalogAccess.cs b/src/Business/Grand.Business.Core/Utilities/Catalog/CatalogAccess.cs new file mode 100644 index 0000000000..9c4e93fe66 --- /dev/null +++ b/src/Business/Grand.Business.Core/Utilities/Catalog/CatalogAccess.cs @@ -0,0 +1,50 @@ +using Grand.Domain.Customers; + +namespace Grand.Business.Core.Utilities.Catalog; + +/// +/// Whose view of the catalog a read returns: the published records a customer's groups may see, or every +/// record, hidden and group-limited ones included, for the back office, import and the sitemap. A store id +/// keeps out the records limited to other stores in both cases. Two accesses are equal when they hold the same +/// customer and store. +/// +public sealed record CatalogAccess +{ + private CatalogAccess(Customer customer, string storeId) + { + Customer = customer; + StoreId = storeId ?? ""; + } + + /// + /// Gets the customer whose groups limit the records; null when hidden records are included + /// + public Customer Customer { get; } + + /// + /// Gets the store whose limitations apply; empty when no store limits the records + /// + public string StoreId { get; } + + /// + /// Gets a value indicating whether unpublished and group-limited records are included + /// + public bool ShowHidden => Customer == null; + + /// + /// The published records the customer's groups may see in the store + /// + public static CatalogAccess For(Customer customer, string storeId) + { + ArgumentNullException.ThrowIfNull(customer); + return new CatalogAccess(customer, storeId); + } + + /// + /// Every record, hidden and group-limited ones included; a store id leaves out those limited to other stores + /// + public static CatalogAccess IncludingHidden(string storeId = "") + { + return new CatalogAccess(null, storeId); + } +} diff --git a/src/Business/Grand.Business.Messages/Commands/Handlers/Common/GetSitemapXMLCommandHandler.cs b/src/Business/Grand.Business.Messages/Commands/Handlers/Common/GetSitemapXMLCommandHandler.cs index 01eb015671..f83dd5bb8b 100644 --- a/src/Business/Grand.Business.Messages/Commands/Handlers/Common/GetSitemapXMLCommandHandler.cs +++ b/src/Business/Grand.Business.Messages/Commands/Handlers/Common/GetSitemapXMLCommandHandler.cs @@ -6,6 +6,7 @@ using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Storage; using Grand.Business.Core.Queries.Catalog; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Blogs; using Grand.Domain.Catalog; using Grand.Domain.Common; @@ -196,7 +197,8 @@ private async Task> GenerateUrls(Language language, Store stor //categories if (_commonSettings.SitemapIncludeCategories) { - var categories = (await _categoryService.GetAllCategories(null, "", store.Id, customer: null, showHidden: true)) + var categories = (await _categoryService.GetAllCategories(null, "", + CatalogAccess.IncludingHidden(store.Id))) .Where(c => c.Published && IsVisibleToGuests(c)) .ToLookup(c => c.ParentCategoryId ?? ""); sitemapUrls.AddRange(await GetCategoryUrls("", categories, language)); @@ -259,7 +261,7 @@ private async Task> GetCategoryUrls(string parentCategor private async Task> GetBrandUrls(Language language, Store store) { - var brands = (await _brandService.GetAllBrands(brandName: "", storeId: store.Id, customer: null, showHidden: true)) + var brands = (await _brandService.GetAllBrands(brandName: "", access: CatalogAccess.IncludingHidden(store.Id))) .Where(b => b.Published && IsVisibleToGuests(b)); var brandUrls = new List(); var storeLocation = GetStoreLocation(); diff --git a/src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesHasAllProductsController.cs b/src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesHasAllProductsController.cs index 189053cf85..aaef90d616 100644 --- a/src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesHasAllProductsController.cs +++ b/src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesHasAllProductsController.cs @@ -5,6 +5,7 @@ using Grand.Business.Core.Interfaces.Common.Security; using Grand.Business.Core.Interfaces.Common.Stores; using Grand.Business.Core.Interfaces.Customers; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Discounts; using Grand.Domain.Permissions; @@ -174,17 +175,14 @@ public async Task ProductAddPopupList(DataSourceRequest command, searchCategoryIds.Add(model.SearchCategoryId); var products = (await _productService.SearchProducts( - customer: null, + access: CatalogAccess.IncludingHidden(model.SearchStoreId), categoryIds: searchCategoryIds, collectionId: model.SearchCollectionId, - storeId: model.SearchStoreId, vendorId: model.SearchVendorId, productType: model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null, keywords: model.SearchProductName, pageIndex: command.Page - 1, - pageSize: command.PageSize, - showHidden: true - )).products; + pageSize: command.PageSize)).products; var gridModel = new DataSourceResult { Data = products.Select(x => new RequirementAllProductsModel.ProductModel { Id = x.Id, @@ -230,7 +228,7 @@ public async Task LoadProductFriendlyNames(string productIds) ids.Add(str2); } - var products = await _productService.GetProductsByIds(ids.ToArray(), customer: null, storeId: "", showHidden: true); + var products = await _productService.GetProductsByIds(ids.ToArray(), access: CatalogAccess.IncludingHidden()); for (var i = 0; i <= products.Count - 1; i++) { result += products[i].Name; diff --git a/src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesHasOneProductController.cs b/src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesHasOneProductController.cs index 83dbb85c04..ec0de48df5 100644 --- a/src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesHasOneProductController.cs +++ b/src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesHasOneProductController.cs @@ -5,6 +5,7 @@ using Grand.Business.Core.Interfaces.Common.Security; using Grand.Business.Core.Interfaces.Common.Stores; using Grand.Business.Core.Interfaces.Customers; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Discounts; using Grand.Domain.Permissions; @@ -186,17 +187,14 @@ public async Task ProductAddPopupList(DataSourceRequest command, searchCategoryIds.Add(model.SearchCategoryId); var products = (await _productService.SearchProducts( - customer: null, + access: CatalogAccess.IncludingHidden(model.SearchStoreId), categoryIds: searchCategoryIds, collectionId: model.SearchCollectionId, - storeId: model.SearchStoreId, vendorId: model.SearchVendorId, productType: model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null, keywords: model.SearchProductName, pageIndex: command.Page - 1, - pageSize: command.PageSize, - showHidden: true - )).products; + pageSize: command.PageSize)).products; var gridModel = new DataSourceResult { Data = products.Select(x => new RequirementOneProductModel.ProductModel { Id = x.Id, @@ -224,7 +222,7 @@ public async Task LoadProductFriendlyNames(string productIds) .Select(x => x.Split(':')[0].Trim()) .ToList(); - var products = await _productService.GetProductsByIds(ids.ToArray(), customer: null, storeId: "", showHidden: true); + var products = await _productService.GetProductsByIds(ids.ToArray(), access: CatalogAccess.IncludingHidden()); result = string.Join(", ", products.Select(p => p.Name)); return new JsonResult(new { Text = result }); diff --git a/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetPersonalizedProductsQueryHandlerTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetPersonalizedProductsQueryHandlerTests.cs index 7908e58409..b208304959 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetPersonalizedProductsQueryHandlerTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetPersonalizedProductsQueryHandlerTests.cs @@ -1,6 +1,7 @@ using Grand.Business.Catalog.Queries.Handlers; using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Queries.Catalog; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Domain.Catalog; using Grand.Domain.Customers; @@ -60,7 +61,7 @@ public async Task Handle_KeysAndLoadsByIdOfQueryCustomer() .Returns(new List { new() { CustomerId = "c1", ProductId = "p1" }, new() { CustomerId = "c2", ProductId = "p2" } }.AsQueryable()); - _productServiceMock.Setup(p => p.GetProductsByIds(It.IsAny(), customer, "s1", false)) + _productServiceMock.Setup(p => p.GetProductsByIds(It.IsAny(), CatalogAccess.For(customer, "s1"))) .ReturnsAsync(new List()); //Act @@ -70,6 +71,7 @@ await handler.Handle(new GetPersonalizedProductsQuery { Customer = customer, Sto //Assert _casheManagerMock.Verify(c => c.GetAsync(string.Format(CacheKey.PRODUCTS_CUSTOMER_PERSONAL_KEY, "c1"), It.IsAny>>>()), Times.Once); - _productServiceMock.Verify(p => p.GetProductsByIds(new[] { "p1" }, customer, "s1", false), Times.Once); + _productServiceMock.Verify(p => p.GetProductsByIds(new[] { "p1" }, + CatalogAccess.For(customer, "s1")), Times.Once); } } \ No newline at end of file diff --git a/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetRecommendedProductsQueryHandlerTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetRecommendedProductsQueryHandlerTests.cs index c08ee133fc..45e2ac3e7e 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetRecommendedProductsQueryHandlerTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetRecommendedProductsQueryHandlerTests.cs @@ -1,6 +1,7 @@ using Grand.Business.Catalog.Queries.Handlers; using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Queries.Catalog; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Domain.Catalog; using Grand.Domain.Customers; @@ -64,7 +65,7 @@ public async Task Handle_AuthorizesProductsAgainstCustomerAndStoreOfQuery() _customerGroupProductRepositoryMock.Setup(r => r.Table) .Returns(new List { new() { CustomerGroupId = "1", ProductId = "p1" } } .AsQueryable()); - _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "p1" }, customer, "1", false)) + _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "p1" }, CatalogAccess.For(customer, "1"))) .ReturnsAsync(new List()); //Act @@ -75,6 +76,7 @@ await handler.Handle(new GetRecommendedProductsQuery { //Assert _casheManagerMock.Verify(c => c.GetAsync(string.Format(CacheKey.PRODUCTS_CUSTOMER_GROUP, "1", "1"), It.IsAny>>>()), Times.Once); - _productServiceMock.Verify(p => p.GetProductsByIds(new[] { "p1" }, customer, "1", false), Times.Once); + _productServiceMock.Verify(p => p.GetProductsByIds(new[] { "p1" }, + CatalogAccess.For(customer, "1")), Times.Once); } } \ No newline at end of file diff --git a/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetSuggestedProductsQueryHandlerTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetSuggestedProductsQueryHandlerTests.cs index fb8bba39d3..ccd9094c96 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetSuggestedProductsQueryHandlerTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Queries/Handlers/GetSuggestedProductsQueryHandlerTests.cs @@ -1,6 +1,7 @@ using Grand.Business.Catalog.Queries.Handlers; using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Queries.Catalog; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Domain.Catalog; using Grand.Domain.Customers; @@ -65,7 +66,7 @@ public async Task Handle_KeysAndFiltersByTagsOfQueryCustomer() .Returns(new List { new() { CustomerTagId = "t1", ProductId = "p1" }, new() { CustomerTagId = "t2", ProductId = "p2" } }.AsQueryable()); - _productServiceMock.Setup(p => p.GetProductsByIds(It.IsAny(), customer, "s1", false)) + _productServiceMock.Setup(p => p.GetProductsByIds(It.IsAny(), CatalogAccess.For(customer, "s1"))) .ReturnsAsync(new List()); //Act @@ -75,6 +76,7 @@ await handler.Handle(new GetSuggestedProductsQuery { Customer = customer, StoreI //Assert _casheManagerMock.Verify(c => c.GetAsync(string.Format(CacheKey.PRODUCTS_CUSTOMER_TAG, "t1"), It.IsAny>>>()), Times.Once); - _productServiceMock.Verify(p => p.GetProductsByIds(new[] { "p1" }, customer, "s1", false), Times.Once); + _productServiceMock.Verify(p => p.GetProductsByIds(new[] { "p1" }, + CatalogAccess.For(customer, "s1")), Times.Once); } } \ No newline at end of file diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Brands/BrandServiceTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Brands/BrandServiceTests.cs index 96ba3dd7bb..e789b10b62 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Brands/BrandServiceTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Brands/BrandServiceTests.cs @@ -1,4 +1,5 @@ using Grand.Business.Catalog.Services.Brands; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Data.Tests.MongoDb; using Grand.Domain.Catalog; @@ -41,7 +42,7 @@ public async Task GetAllBrandsTest() await _brandService.InsertBrand(new Brand { Published = true }); //Act - var brand = await _brandService.GetAllBrands(brandName: "", storeId: "", customer: new Customer()); + var brand = await _brandService.GetAllBrands(brandName: "", access: CatalogAccess.For(new Customer(), "")); //Assert Assert.HasCount(3, brand); @@ -60,8 +61,8 @@ await _repository.InsertAsync(new Brand { vipCustomer.Groups.Add("vip"); //Act - var guest = await _brandService.GetAllBrands("", "", new Customer()); - var vip = await _brandService.GetAllBrands("", "", vipCustomer); + var guest = await _brandService.GetAllBrands("", CatalogAccess.For(new Customer(), "")); + var vip = await _brandService.GetAllBrands("", CatalogAccess.For(vipCustomer, "")); //Assert Assert.HasCount(1, guest); diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceCacheKeyTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceCacheKeyTests.cs index 481cab086d..8760991d54 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceCacheKeyTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceCacheKeyTests.cs @@ -1,5 +1,6 @@ using Grand.Business.Catalog.Services.Categories; using Grand.Business.Common.Services.Security; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Data.Tests.MongoDb; using Grand.Domain.Catalog; @@ -53,11 +54,11 @@ public async Task TwoCustomersOfTheSameGroup_ShareTheCachedTree() { await _categoryService.InsertCategory(new Category { ParentCategoryId = "root", Published = true }); - var first = await _categoryService.GetAllCategoriesByParentCategoryId(WithGroups("customer-1", "group-a"), - "store", "root"); + var first = await _categoryService.GetAllCategoriesByParentCategoryId( + CatalogAccess.For(WithGroups("customer-1", "group-a"), "store"), "root"); - var second = await _categoryService.GetAllCategoriesByParentCategoryId(WithGroups("customer-2", "group-a"), - "store", "root"); + var second = await _categoryService.GetAllCategoriesByParentCategoryId( + CatalogAccess.For(WithGroups("customer-2", "group-a"), "store"), "root"); Assert.AreSame(first, second, "same groups must resolve to the same cache entry"); } @@ -67,11 +68,11 @@ public async Task CustomersOfDifferentGroups_DoNotShareTheCachedTree() { await _categoryService.InsertCategory(new Category { ParentCategoryId = "root", Published = true }); - var first = await _categoryService.GetAllCategoriesByParentCategoryId(WithGroups("customer-1", "group-a"), - "store", "root"); + var first = await _categoryService.GetAllCategoriesByParentCategoryId( + CatalogAccess.For(WithGroups("customer-1", "group-a"), "store"), "root"); - var second = await _categoryService.GetAllCategoriesByParentCategoryId(WithGroups("customer-2", "group-b"), - "store", "root"); + var second = await _categoryService.GetAllCategoriesByParentCategoryId( + CatalogAccess.For(WithGroups("customer-2", "group-b"), "store"), "root"); Assert.AreNotSame(first, second, "the ACL filter differs per group, so the entries must differ"); } diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceDbTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceDbTests.cs index 2848acaa52..af7a47e690 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceDbTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceDbTests.cs @@ -1,6 +1,7 @@ using Grand.Business.Catalog.Services.Categories; using Grand.Business.Common.Services.Security; using Grand.Business.Core.Interfaces.Common.Security; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Data.Tests.MongoDb; using Grand.Domain.Catalog; @@ -49,8 +50,9 @@ await _categoryRepository.InsertAsync(new Category { }); //Act - var children = await _categoryService.GetAllCategoriesByParentCategoryId(customer: null, storeId: "", - parentCategoryId: "parent", showHidden: true); + var children = await _categoryService.GetAllCategoriesByParentCategoryId( + access: CatalogAccess.IncludingHidden(), + parentCategoryId: "parent"); //Assert Assert.HasCount(1, children); @@ -102,8 +104,8 @@ public async Task GetCategoryBreadCrumb_StopsAtCategoryHiddenFromGivenCustomer() vipCustomer.Groups.Add("vip"); //Act - var guest = await _categoryService.GetCategoryBreadCrumb(child, new Customer(), ""); - var vip = await _categoryService.GetCategoryBreadCrumb(child, vipCustomer, ""); + var guest = await _categoryService.GetCategoryBreadCrumb(child, CatalogAccess.For(new Customer(), "")); + var vip = await _categoryService.GetCategoryBreadCrumb(child, CatalogAccess.For(vipCustomer, "")); //Assert Assert.HasCount(1, guest); @@ -169,7 +171,7 @@ public async Task GetCategoryBreadCrumb() allCategory.ToList().ForEach(x => _categoryService.InsertCategory(x).GetAwaiter().GetResult()); var category = new Category { Id = "6", ParentCategoryId = "3", Published = true }; await _categoryService.InsertCategory(category); - var result = await _categoryService.GetCategoryBreadCrumb(category, new Customer(), ""); + var result = await _categoryService.GetCategoryBreadCrumb(category, CatalogAccess.For(new Customer(), "")); Assert.HasCount(2, result); Assert.IsTrue(result.Any(c => c.Id.Equals("6"))); Assert.IsTrue(result.Any(c => c.Id.Equals("3"))); @@ -181,7 +183,8 @@ public void GetCategoryBreadCrumb_AllCategory() var allCategory = GetMockCategoryList(); allCategory.ToList().ForEach(x => _categoryService.InsertCategory(x).GetAwaiter().GetResult()); var category = new Category { ParentCategoryId = "3" }; - var result = _categoryService.GetCategoryBreadCrumb(category, allCategory, new Customer(), ""); + var result = _categoryService.GetCategoryBreadCrumb(category, allCategory, + CatalogAccess.For(new Customer(), "")); Assert.IsEmpty(result); } @@ -190,7 +193,8 @@ public async Task GetAllCategories() { var allCategory = GetMockCategoryList(); allCategory.ToList().ForEach(x => _categoryService.InsertCategory(x).GetAwaiter().GetResult()); - var result = await _categoryService.GetAllCategories(parentId: null, categoryName: "", storeId: "", customer: new Customer()); + var result = await _categoryService.GetAllCategories(parentId: null, categoryName: "", + access: CatalogAccess.For(new Customer(), "")); Assert.HasCount(5, result); } @@ -208,7 +212,8 @@ public async Task GetAllCategoriesByParentCategoryId() { var allCategory = GetMockCategoryList(); allCategory.ToList().ForEach(x => _categoryService.InsertCategory(x).GetAwaiter().GetResult()); - var result = await _categoryService.GetAllCategoriesByParentCategoryId(new Customer(), "", "5"); + var result = await _categoryService.GetAllCategoriesByParentCategoryId( + CatalogAccess.For(new Customer(), ""), "5"); Assert.HasCount(1, result); } @@ -217,7 +222,7 @@ public async Task GetAllCategoriesDisplayedOnHomePage() { var allCategory = GetMockCategoryList(); allCategory.ToList().ForEach(x => _categoryService.InsertCategory(x).GetAwaiter().GetResult()); - var result = await _categoryService.GetAllCategoriesDisplayedOnHomePage(new Customer(), ""); + var result = await _categoryService.GetAllCategoriesDisplayedOnHomePage(CatalogAccess.For(new Customer(), "")); Assert.HasCount(2, result); } @@ -226,7 +231,8 @@ public async Task GetAllCategoriesFeaturedProductsOnHomePage() { var allCategory = GetMockCategoryList(); allCategory.ToList().ForEach(x => _categoryService.InsertCategory(x).GetAwaiter().GetResult()); - var result = await _categoryService.GetAllCategoriesFeaturedProductsOnHomePage(new Customer(), ""); + var result = await _categoryService.GetAllCategoriesFeaturedProductsOnHomePage( + CatalogAccess.For(new Customer(), "")); Assert.HasCount(2, result); } @@ -264,7 +270,8 @@ public async Task GetCategoryBreadCrumb_ShouldReturnTwoElement() allCategory.ToList().ForEach(x => _categoryService.InsertCategory(x).GetAwaiter().GetResult()); var category = new Category { Id = "6", ParentCategoryId = "3", Published = true }; await _categoryService.InsertCategory(category); - var result = _categoryService.GetCategoryBreadCrumb(category, allCategory, new Customer(), ""); + var result = _categoryService.GetCategoryBreadCrumb(category, allCategory, + CatalogAccess.For(new Customer(), "")); Assert.HasCount(2, result); Assert.IsTrue(result.Any(c => c.Id.Equals("6"))); Assert.IsTrue(result.Any(c => c.Id.Equals("3"))); @@ -276,7 +283,8 @@ public void GetCategoryBreadCrumb_ShouldReturnThreeElement() var allCategory = GetMockCategoryList(); allCategory.ToList().ForEach(x => _categoryService.InsertCategory(x).GetAwaiter().GetResult()); var category = new Category { Id = "6", ParentCategoryId = "1", Published = true }; - var result = _categoryService.GetCategoryBreadCrumb(category, allCategory, new Customer(), ""); + var result = _categoryService.GetCategoryBreadCrumb(category, allCategory, + CatalogAccess.For(new Customer(), "")); Assert.HasCount(3, result); Assert.IsTrue(result.Any(c => c.Id.Equals("6"))); Assert.IsTrue(result.Any(c => c.Id.Equals("1"))); diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceTests.cs index 35a7df711c..c15c82ac32 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceTests.cs @@ -1,5 +1,6 @@ using Grand.Business.Catalog.Services.Categories; using Grand.Business.Core.Interfaces.Common.Security; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Data.Mongo; using Grand.Domain.Catalog; @@ -85,7 +86,8 @@ public void GetCategoryBreadCrumb_ShouldReturnEmptyList() var category = new Category { ParentCategoryId = "3" }; _aclServiceMock.Setup(a => a.Authorize(It.IsAny(), It.IsAny())).Returns(() => true); _aclServiceMock.Setup(a => a.Authorize(It.IsAny(), It.IsAny())).Returns(() => true); - var result = _categoryService.GetCategoryBreadCrumb(category, allCategory, new Customer { Id = "" }, ""); + var result = _categoryService.GetCategoryBreadCrumb(category, allCategory, + CatalogAccess.For(new Customer { Id = "" }, "")); Assert.IsEmpty(result); } @@ -96,7 +98,8 @@ public void GetCategoryBreadCrumb_ShouldReturnTwoElement() var category = new Category { Id = "6", ParentCategoryId = "3", Published = true }; _aclServiceMock.Setup(a => a.Authorize(It.IsAny(), It.IsAny())).Returns(() => true); _aclServiceMock.Setup(a => a.Authorize(It.IsAny(), It.IsAny())).Returns(() => true); - var result = _categoryService.GetCategoryBreadCrumb(category, allCategory, new Customer { Id = "" }, ""); + var result = _categoryService.GetCategoryBreadCrumb(category, allCategory, + CatalogAccess.For(new Customer { Id = "" }, "")); Assert.HasCount(2, result); Assert.IsTrue(result.Any(c => c.Id.Equals("6"))); Assert.IsTrue(result.Any(c => c.Id.Equals("3"))); @@ -109,7 +112,8 @@ public void GetCategoryBreadCrumb_ShouldReturnThreeElement() var category = new Category { Id = "6", ParentCategoryId = "1", Published = true }; _aclServiceMock.Setup(a => a.Authorize(It.IsAny(), It.IsAny())).Returns(() => true); _aclServiceMock.Setup(a => a.Authorize(It.IsAny(), It.IsAny())).Returns(() => true); - var result = _categoryService.GetCategoryBreadCrumb(category, allCategory, new Customer { Id = "" }, ""); + var result = _categoryService.GetCategoryBreadCrumb(category, allCategory, + CatalogAccess.For(new Customer { Id = "" }, "")); Assert.HasCount(3, result); Assert.IsTrue(result.Any(c => c.Id.Equals("6"))); Assert.IsTrue(result.Any(c => c.Id.Equals("1"))); diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceTreeTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceTreeTests.cs index 29cb87fc26..12499369ce 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceTreeTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/CategoryServiceTreeTests.cs @@ -1,5 +1,6 @@ using Grand.Business.Catalog.Services.Categories; using Grand.Business.Common.Services.Security; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Data.Tests.MongoDb; using Grand.Domain.Catalog; @@ -58,7 +59,8 @@ public async Task AllLevels_KeepsTheWholeLevelBeforeEachSubtree() { await GivenATwoLevelTree(); - var result = await _categoryService.GetAllCategoriesByParentCategoryId(_customer, "store", "root", includeAllLevels: true); + var result = await _categoryService.GetAllCategoriesByParentCategoryId( + CatalogAccess.For(_customer, "store"), "root", includeAllLevels: true); CollectionAssert.AreEqual(new[] { "a", "b", "a1", "b1" }, result.Select(x => x.Id).ToArray()); } @@ -68,7 +70,8 @@ public async Task WithoutAllLevels_ReturnsOnlyTheDirectChildren() { await GivenATwoLevelTree(); - var result = await _categoryService.GetAllCategoriesByParentCategoryId(_customer, "store", "root"); + var result = await _categoryService.GetAllCategoriesByParentCategoryId( + CatalogAccess.For(_customer, "store"), "root"); CollectionAssert.AreEqual(new[] { "a", "b" }, result.Select(x => x.Id).ToArray()); } @@ -82,9 +85,11 @@ public async Task AllLevels_HandsBackAListTheCallerOwns() { await GivenATwoLevelTree(); - var first = await _categoryService.GetAllCategoriesByParentCategoryId(_customer, "store", "root", includeAllLevels: true); + var first = await _categoryService.GetAllCategoriesByParentCategoryId( + CatalogAccess.For(_customer, "store"), "root", includeAllLevels: true); first.Clear(); - var second = await _categoryService.GetAllCategoriesByParentCategoryId(_customer, "store", "root", includeAllLevels: true); + var second = await _categoryService.GetAllCategoriesByParentCategoryId( + CatalogAccess.For(_customer, "store"), "root", includeAllLevels: true); Assert.HasCount(4, second); } diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/ProductCategoryServiceTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/ProductCategoryServiceTests.cs index bd917628c1..096b738585 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/ProductCategoryServiceTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Categories/ProductCategoryServiceTests.cs @@ -1,4 +1,5 @@ using Grand.Business.Catalog.Services.Categories; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Data.Tests.MongoDb; using Grand.Domain.Catalog; @@ -43,8 +44,10 @@ public async Task GetProductCategoriesByCategoryId_FiltersByGivenStore() await _repository.InsertAsync(product); //Act - var store1 = await _productCategoryService.GetProductCategoriesByCategoryId("cat", new Customer(), "store1"); - var store2 = await _productCategoryService.GetProductCategoriesByCategoryId("cat", new Customer(), "store2"); + var store1 = await _productCategoryService.GetProductCategoriesByCategoryId("cat", + CatalogAccess.For(new Customer(), "store1")); + var store2 = await _productCategoryService.GetProductCategoriesByCategoryId("cat", + CatalogAccess.For(new Customer(), "store2")); //Assert Assert.HasCount(1, store1); @@ -63,8 +66,8 @@ public async Task GetProductCategoriesByCategoryId_ShowHiddenWithoutCustomer_Ret await _repository.InsertAsync(product); //Act - var mappings = await _productCategoryService.GetProductCategoriesByCategoryId("cat", customer: null, storeId: "", - showHidden: true); + var mappings = await _productCategoryService.GetProductCategoriesByCategoryId("cat", + access: CatalogAccess.IncludingHidden()); //Assert Assert.HasCount(1, mappings); @@ -84,8 +87,10 @@ public async Task GetProductCategoriesByCategoryIdTest() await _repository.InsertAsync(p2); //Act - var pc1 = await _productCategoryService.GetProductCategoriesByCategoryId("1", new Customer(), ""); - var pc2 = await _productCategoryService.GetProductCategoriesByCategoryId("2", new Customer(), ""); + var pc1 = await _productCategoryService.GetProductCategoriesByCategoryId("1", + CatalogAccess.For(new Customer(), "")); + var pc2 = await _productCategoryService.GetProductCategoriesByCategoryId("2", + CatalogAccess.For(new Customer(), "")); //Assert Assert.HasCount(1, pc1); @@ -106,7 +111,8 @@ await _productCategoryService.InsertProductCategory(new ProductCategory { Catego p1.Id); await _productCategoryService.InsertProductCategory(new ProductCategory { CategoryId = "2" }, p1.Id); - var pc1 = await _productCategoryService.GetProductCategoriesByCategoryId("1", new Customer(), ""); + var pc1 = await _productCategoryService.GetProductCategoriesByCategoryId("1", + CatalogAccess.For(new Customer(), "")); //Assert Assert.HasCount(1, pc1); @@ -131,7 +137,8 @@ public async Task UpdateProductCategoryTest() pc.DisplayOrder = 5; await _productCategoryService.UpdateProductCategory(pc, p1.Id); - var pc1 = await _productCategoryService.GetProductCategoriesByCategoryId("10", new Customer(), ""); + var pc1 = await _productCategoryService.GetProductCategoriesByCategoryId("10", + CatalogAccess.For(new Customer(), "")); //Assert Assert.HasCount(1, pc1); @@ -154,7 +161,8 @@ public async Task DeleteProductCategoryTest() await _productCategoryService.DeleteProductCategory(pc, p1.Id); - var pc1 = await _productCategoryService.GetProductCategoriesByCategoryId("1", new Customer(), ""); + var pc1 = await _productCategoryService.GetProductCategoriesByCategoryId("1", + CatalogAccess.For(new Customer(), "")); //Assert Assert.IsEmpty(pc1); diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Collections/CollectionServiceTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Collections/CollectionServiceTests.cs index 89b11f8025..2157e0b0a4 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Collections/CollectionServiceTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Collections/CollectionServiceTests.cs @@ -1,6 +1,7 @@ using Grand.Business.Catalog.Services.Collections; using Grand.Business.Core.Interfaces.Catalog.Collections; using Grand.Business.Core.Interfaces.Common.Security; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Domain.Catalog; using Grand.Domain.Customers; @@ -48,8 +49,10 @@ public async Task GetAllCollectionFeaturedProductsOnHomePage_AuthorizesAgainstGi _aclMock.Setup(a => a.Authorize(collection, "store1")).Returns(true); //Act - var inStore1 = await _collectionService.GetAllCollectionFeaturedProductsOnHomePage(customer, "store1"); - var inStore2 = await _collectionService.GetAllCollectionFeaturedProductsOnHomePage(customer, "store2"); + var inStore1 = await _collectionService.GetAllCollectionFeaturedProductsOnHomePage( + CatalogAccess.For(customer, "store1")); + var inStore2 = await _collectionService.GetAllCollectionFeaturedProductsOnHomePage( + CatalogAccess.For(customer, "store2")); //Assert Assert.HasCount(1, inStore1); diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Collections/ProductCollectionServiceTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Collections/ProductCollectionServiceTests.cs index c561695f2c..0e130f3d8b 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Collections/ProductCollectionServiceTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Collections/ProductCollectionServiceTests.cs @@ -1,4 +1,5 @@ using Grand.Business.Catalog.Services.Collections; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Data.Tests.MongoDb; using Grand.Domain.Catalog; @@ -45,8 +46,10 @@ public async Task GetProductCollectionsByCollectionId_FiltersByGroupsOfGivenCust vipCustomer.Groups.Add("vip"); //Act - var guest = await _productCollectionService.GetProductCollectionsByCollectionId("col", "", new Customer()); - var vip = await _productCollectionService.GetProductCollectionsByCollectionId("col", "", vipCustomer); + var guest = await _productCollectionService.GetProductCollectionsByCollectionId("col", + CatalogAccess.For(new Customer(), "")); + var vip = await _productCollectionService.GetProductCollectionsByCollectionId("col", + CatalogAccess.For(vipCustomer, "")); //Assert Assert.IsEmpty(guest); @@ -67,8 +70,10 @@ public async Task GetProductCollectionsByCollectionIdTest() await _repository.InsertAsync(p2); //Act - var pc1 = await _productCollectionService.GetProductCollectionsByCollectionId("1", "", new Customer()); - var pc2 = await _productCollectionService.GetProductCollectionsByCollectionId("2", "", new Customer()); + var pc1 = await _productCollectionService.GetProductCollectionsByCollectionId("1", + CatalogAccess.For(new Customer(), "")); + var pc2 = await _productCollectionService.GetProductCollectionsByCollectionId("2", + CatalogAccess.For(new Customer(), "")); //Assert Assert.HasCount(1, pc1); @@ -89,7 +94,8 @@ await _productCollectionService.InsertProductCollection( new ProductCollection { CollectionId = "1", DisplayOrder = 10 }, p1.Id); await _productCollectionService.InsertProductCollection(new ProductCollection { CollectionId = "2" }, p1.Id); - var pc1 = await _productCollectionService.GetProductCollectionsByCollectionId("1", "", new Customer()); + var pc1 = await _productCollectionService.GetProductCollectionsByCollectionId("1", + CatalogAccess.For(new Customer(), "")); //Assert Assert.HasCount(1, pc1); @@ -114,7 +120,8 @@ public async Task UpdateProductCollectionTest() pc.DisplayOrder = 5; await _productCollectionService.UpdateProductCollection(pc, p1.Id); - var pc1 = await _productCollectionService.GetProductCollectionsByCollectionId("10", "", new Customer()); + var pc1 = await _productCollectionService.GetProductCollectionsByCollectionId("10", + CatalogAccess.For(new Customer(), "")); //Assert Assert.HasCount(1, pc1); @@ -137,7 +144,8 @@ public async Task DeleteProductCollectionTest() await _productCollectionService.DeleteProductCollection(pc, p1.Id); - var pc1 = await _productCollectionService.GetProductCollectionsByCollectionId("1", "", new Customer()); + var pc1 = await _productCollectionService.GetProductCollectionsByCollectionId("1", + CatalogAccess.For(new Customer(), "")); //Assert Assert.IsEmpty(pc1); diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Products/ProductServiceTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Products/ProductServiceTests.cs index a15652363c..e9b006affb 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Products/ProductServiceTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Products/ProductServiceTests.cs @@ -2,6 +2,7 @@ using Grand.Business.Common.Services.Security; using Grand.Business.Core.Interfaces.Common.Security; using Grand.Business.Core.Queries.Catalog; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Data.Tests.MongoDb; using Grand.Domain; @@ -53,8 +54,8 @@ await _productRepository.InsertAsync(new Product { }); //Act - var store1 = await _productService.GetProductsByIds(["p1"], new Customer(), "store1"); - var store2 = await _productService.GetProductsByIds(["p1"], new Customer(), "store2"); + var store1 = await _productService.GetProductsByIds(["p1"], CatalogAccess.For(new Customer(), "store1")); + var store2 = await _productService.GetProductsByIds(["p1"], CatalogAccess.For(new Customer(), "store2")); //Assert Assert.HasCount(1, store1); @@ -62,10 +63,24 @@ await _productRepository.InsertAsync(new Product { } [TestMethod] - public async Task GetProductsByIds_WithoutCustomer_Throws() + public async Task GetProductsByIds_WithoutAccess_Throws() { - await Assert.ThrowsExactlyAsync(() => - _productService.GetProductsByIds(["p1"], null, "store1")); + await Assert.ThrowsExactlyAsync(() => _productService.GetProductsByIds(["p1"], null)); + } + + [TestMethod] + public async Task GetProductsByIds_IncludingHidden_ReturnsUnpublishedAndGroupLimitedProducts() + { + //Arrange + await _productRepository.InsertAsync(new Product { + Id = "p1", Published = false, LimitedToGroups = true, CustomerGroups = new List { "vip" } + }); + + //Act + var products = await _productService.GetProductsByIds(["p1"], CatalogAccess.IncludingHidden()); + + //Assert + Assert.HasCount(1, products); } [TestMethod] @@ -77,7 +92,7 @@ public async Task SearchProducts_SendsGivenCustomerInQuery() .ReturnsAsync((new PagedList(new List(), 0, 1), (IList)new List())); //Act - await _productService.SearchProducts(customer, storeId: "s1"); + await _productService.SearchProducts(CatalogAccess.For(customer, "s1")); //Assert _mediatorMock.Verify(m => m.Send( @@ -178,7 +193,7 @@ public async Task GetProductsByIdsTest() { await insertSampleProducts(); //Act - var result = await _productService.GetProductsByIds(["1"], new Customer(), ""); + var result = await _productService.GetProductsByIds(["1"], CatalogAccess.For(new Customer(), "")); //Assert Assert.IsNotEmpty(result); @@ -295,7 +310,7 @@ public async Task GetAssociatedProductsTest() //Arrange await insertSampleProducts(); //Act - var result = await _productService.GetAssociatedProducts("1", new Customer()); + var result = await _productService.GetAssociatedProducts("1", CatalogAccess.For(new Customer(), "")); //Assert Assert.IsNotEmpty(result); diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Products/RecentlyViewedProductsServiceTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Products/RecentlyViewedProductsServiceTests.cs index b9afeb9577..10c4a711fb 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Products/RecentlyViewedProductsServiceTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Products/RecentlyViewedProductsServiceTests.cs @@ -1,5 +1,6 @@ using Grand.Business.Catalog.Services.Products; using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Data.Tests.MongoDb; using Grand.Domain.Catalog; @@ -28,7 +29,8 @@ public void Init() _repository = new MongoDBRepositoryTest(); _mediatorMock = new Mock(); _productServiceMock = new Mock(); - _productServiceMock.Setup(a => a.GetProductsByIds(It.IsAny(), It.IsAny(), It.IsAny(), false)).Returns(() => + _productServiceMock.Setup(a => a.GetProductsByIds(It.IsAny(), + It.IsAny())).Returns(() => Task.FromResult((IList)new List { new() { Id = "1", Published = true }, new() { Id = "2", Published = true }, new() { Id = "3", Published = true } diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Products/SpecificationAttributeServiceTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Products/SpecificationAttributeServiceTests.cs index 9a5a50f06e..e83da43448 100644 --- a/src/Tests/Grand.Business.Catalog.Tests/Services/Products/SpecificationAttributeServiceTests.cs +++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Products/SpecificationAttributeServiceTests.cs @@ -1,5 +1,6 @@ using Grand.Business.Catalog.Services.Products; using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Data.Tests.MongoDb; using Grand.Domain.Catalog; @@ -31,7 +32,8 @@ public void Init() _repositoryProduct = new MongoDBRepositoryTest(); _mediatorMock = new Mock(); _productServiceMock = new Mock(); - _productServiceMock.Setup(a => a.GetProductsByIds(It.IsAny(), It.IsAny(), It.IsAny(), false)).Returns(() => + _productServiceMock.Setup(a => a.GetProductsByIds(It.IsAny(), + It.IsAny())).Returns(() => Task.FromResult((IList)new List { new() { Id = "1", Published = true }, new() { Id = "2", Published = true }, new() { Id = "3", Published = true } diff --git a/src/Tests/Grand.Business.Catalog.Tests/Utilities/CatalogAccessTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Utilities/CatalogAccessTests.cs new file mode 100644 index 0000000000..17259b7ec4 --- /dev/null +++ b/src/Tests/Grand.Business.Catalog.Tests/Utilities/CatalogAccessTests.cs @@ -0,0 +1,54 @@ +using Grand.Business.Core.Utilities.Catalog; +using Grand.Domain.Customers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Grand.Business.Catalog.Tests.Utilities; + +[TestClass] +public class CatalogAccessTests +{ + [TestMethod] + public void For_WithoutCustomer_Throws() + { + Assert.ThrowsExactly(() => CatalogAccess.For(null, "s1")); + } + + [TestMethod] + public void For_LimitsToCustomerAndStore() + { + var customer = new Customer(); + + var access = CatalogAccess.For(customer, "s1"); + + Assert.AreSame(customer, access.Customer); + Assert.AreEqual("s1", access.StoreId); + Assert.IsFalse(access.ShowHidden); + } + + [TestMethod] + public void IncludingHidden_ShowsHiddenRecordsOfAnyStoreByDefault() + { + var access = CatalogAccess.IncludingHidden(); + + Assert.IsNull(access.Customer); + Assert.AreEqual("", access.StoreId); + Assert.IsTrue(access.ShowHidden); + } + + [TestMethod] + public void IncludingHidden_TreatsNullStoreAsNoStore() + { + Assert.AreEqual(CatalogAccess.IncludingHidden(), CatalogAccess.IncludingHidden(null)); + } + + [TestMethod] + public void Equals_ComparesCustomerAndStore() + { + var customer = new Customer(); + + Assert.AreEqual(CatalogAccess.For(customer, "s1"), CatalogAccess.For(customer, "s1")); + Assert.AreNotEqual(CatalogAccess.For(customer, "s1"), CatalogAccess.For(customer, "s2")); + Assert.AreNotEqual(CatalogAccess.For(customer, "s1"), CatalogAccess.For(new Customer(), "s1")); + Assert.AreNotEqual(CatalogAccess.IncludingHidden("s1"), CatalogAccess.IncludingHidden()); + } +} diff --git a/src/Tests/Grand.Business.Messages.Tests/Commands/GetSitemapXmlCommandHandlerTests.cs b/src/Tests/Grand.Business.Messages.Tests/Commands/GetSitemapXmlCommandHandlerTests.cs index 4c5d55ca7c..11e093ea89 100644 --- a/src/Tests/Grand.Business.Messages.Tests/Commands/GetSitemapXmlCommandHandlerTests.cs +++ b/src/Tests/Grand.Business.Messages.Tests/Commands/GetSitemapXmlCommandHandlerTests.cs @@ -5,6 +5,7 @@ using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Storage; using Grand.Business.Core.Queries.Catalog; +using Grand.Business.Core.Utilities.Catalog; using Grand.Business.Messages.Commands.Handlers.Common; using Grand.Domain; using Grand.Domain.Blogs; @@ -60,9 +61,10 @@ public void Init() groupServiceMock.Setup(x => x.GetCustomerGroupBySystemName(SystemCustomerGroupNames.Guests)) .ReturnsAsync(new CustomerGroup { Id = GuestsGroupId, SystemName = SystemCustomerGroupNames.Guests }); - _categoryServiceMock.Setup(x => x.GetAllCategories(null, "", StoreId, null, 0, int.MaxValue, true)) + _categoryServiceMock.Setup(x => x.GetAllCategories(null, "", + CatalogAccess.IncludingHidden(StoreId), 0, int.MaxValue)) .ReturnsAsync(new PagedList()); - _brandServiceMock.Setup(x => x.GetAllBrands("", StoreId, null, 0, int.MaxValue, true)) + _brandServiceMock.Setup(x => x.GetAllBrands("", CatalogAccess.IncludingHidden(StoreId), 0, int.MaxValue)) .ReturnsAsync(new PagedList()); _pageServiceMock.Setup(x => x.GetAllPages(StoreId, true)).ReturnsAsync(new List()); _knowledgebaseServiceMock.Setup(x => x.GetPublicKnowledgebaseArticles(StoreId, @@ -124,8 +126,10 @@ public async Task Handle_WithoutWorkContext_UsesOnlyExplicitlyScopedQueries() //Assert Assert.IsFalse(Locations(xml).Any(string.IsNullOrEmpty), "sitemap contains an empty "); - _categoryServiceMock.Verify(x => x.GetAllCategories(null, "", StoreId, null, 0, int.MaxValue, true), Times.Once); - _brandServiceMock.Verify(x => x.GetAllBrands("", StoreId, null, 0, int.MaxValue, true), Times.Once); + _categoryServiceMock.Verify(x => x.GetAllCategories(null, "", + CatalogAccess.IncludingHidden(StoreId), 0, int.MaxValue), Times.Once); + _brandServiceMock.Verify(x => x.GetAllBrands("", + CatalogAccess.IncludingHidden(StoreId), 0, int.MaxValue), Times.Once); _pageServiceMock.Verify(x => x.GetAllPages(StoreId, true), Times.Once); _mediatorMock.Verify(x => x.Send(It.Is(q => q.Customer == null && q.StoreId == StoreId && q.VisibleIndividuallyOnly && @@ -148,7 +152,8 @@ public async Task Handle_ListsOnlyWhatAGuestCanSee() }, new() { Id = "4", SystemName = "Draft", SeName = "draft-page", Published = false, IncludeInSitemap = true } }); - _categoryServiceMock.Setup(x => x.GetAllCategories(null, "", StoreId, null, 0, int.MaxValue, true)) + _categoryServiceMock.Setup(x => x.GetAllCategories(null, "", + CatalogAccess.IncludingHidden(StoreId), 0, int.MaxValue)) .ReturnsAsync(new PagedList(new List { new() { Id = "c1", ParentCategoryId = "", SeName = "open-category", Published = true }, new() { Id = "c2", ParentCategoryId = "c1", SeName = "child-category", Published = true }, @@ -159,7 +164,7 @@ public async Task Handle_ListsOnlyWhatAGuestCanSee() new() { Id = "c4", ParentCategoryId = "c3", SeName = "child-of-hidden-category", Published = true }, new() { Id = "c5", ParentCategoryId = "", SeName = "unpublished-category", Published = false } }, 0, int.MaxValue)); - _brandServiceMock.Setup(x => x.GetAllBrands("", StoreId, null, 0, int.MaxValue, true)) + _brandServiceMock.Setup(x => x.GetAllBrands("", CatalogAccess.IncludingHidden(StoreId), 0, int.MaxValue)) .ReturnsAsync(new PagedList(new List { new() { Id = "b1", SeName = "open-brand", Published = true }, new() { diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseBrandControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseBrandControllerTests.cs index 81ebc6bb7f..6f08b301f4 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseBrandControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseBrandControllerTests.cs @@ -1,6 +1,7 @@ using Grand.Business.Core.Interfaces.Catalog.Brands; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain; using Grand.Domain.Catalog; using Grand.Domain.Stores; @@ -122,7 +123,7 @@ public async Task ListPost_ForcesScopeDefaultStoreIdOntoSearchModel() { _scopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); _brandServiceMock - .Setup(b => b.GetAllBrands(It.IsAny(), "store-1", null, 0, 10, true)) + .Setup(b => b.GetAllBrands(It.IsAny(), CatalogAccess.IncludingHidden("store-1"), 0, 10)) .ReturnsAsync(new PagedList(new List(), 0, 10)); var model = new BrandListModel { SearchStoreId = "attacker-supplied-store" }; @@ -136,7 +137,8 @@ public async Task ListPost_GlobalScope_LeavesSubmittedSearchStoreIdUntouched() { _scopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); _brandServiceMock - .Setup(b => b.GetAllBrands(It.IsAny(), "admin-submitted-store", null, 0, 10, true)) + .Setup(b => b.GetAllBrands(It.IsAny(), + CatalogAccess.IncludingHidden("admin-submitted-store"), 0, 10)) .ReturnsAsync(new PagedList(new List(), 0, 10)); var model = new BrandListModel { SearchStoreId = "admin-submitted-store" }; @@ -377,7 +379,7 @@ public async Task PicturePopupPost_ValidRequest_CallsUpdatePicture() public async Task ExportXlsx_Success_ReturnsXlsxFile() { _brandServiceMock - .Setup(b => b.GetAllBrands("", "", null, 0, int.MaxValue, true)) + .Setup(b => b.GetAllBrands("", CatalogAccess.IncludingHidden(), 0, int.MaxValue)) .ReturnsAsync(new PagedList(new List { new() { Id = "b1" } }, 0, int.MaxValue)); var exportManagerMock = new Mock>(); exportManagerMock.Setup(e => e.Export(It.IsAny>(), It.IsAny())).ReturnsAsync([1, 2, 3]); @@ -394,7 +396,7 @@ public async Task ExportXlsx_StoreScoped_ScopesExportToDefaultStoreId() { _scopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); _brandServiceMock - .Setup(b => b.GetAllBrands("", "store-1", null, 0, int.MaxValue, true)) + .Setup(b => b.GetAllBrands("", CatalogAccess.IncludingHidden("store-1"), 0, int.MaxValue)) .ReturnsAsync(new PagedList(new List { new() { Id = "b1" } }, 0, int.MaxValue)); var exportManagerMock = new Mock>(); exportManagerMock.Setup(e => e.Export(It.IsAny>(), It.IsAny())).ReturnsAsync([1, 2, 3]); @@ -403,15 +405,17 @@ public async Task ExportXlsx_StoreScoped_ScopesExportToDefaultStoreId() var file = result as FileContentResult; Assert.IsNotNull(file); - _brandServiceMock.Verify(b => b.GetAllBrands("", "store-1", null, 0, int.MaxValue, true), Times.Once); - _brandServiceMock.Verify(b => b.GetAllBrands("", "", null, 0, int.MaxValue, true), Times.Never); + _brandServiceMock.Verify(b => b.GetAllBrands("", + CatalogAccess.IncludingHidden("store-1"), 0, int.MaxValue), Times.Once); + _brandServiceMock.Verify(b => b.GetAllBrands("", + CatalogAccess.IncludingHidden(), 0, int.MaxValue), Times.Never); } [TestMethod] public async Task ExportXlsx_ExportThrows_ShowsErrorAndRedirectsToList() { _brandServiceMock - .Setup(b => b.GetAllBrands("", "", null, 0, int.MaxValue, true)) + .Setup(b => b.GetAllBrands("", CatalogAccess.IncludingHidden(), 0, int.MaxValue)) .ThrowsAsync(new InvalidOperationException("boom")); var exportManagerMock = new Mock>(); @@ -495,7 +499,7 @@ await Assert.ThrowsExactlyAsync(() => public async Task ExportXlsx_CancelledWithoutAbortedRequest_ShowsErrorAndRedirectsToList() { _brandServiceMock - .Setup(b => b.GetAllBrands("", "", null, 0, int.MaxValue, true)) + .Setup(b => b.GetAllBrands("", CatalogAccess.IncludingHidden(), 0, int.MaxValue)) .ThrowsAsync(new OperationCanceledException()); var exportManagerMock = new Mock>(); diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCollectionControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCollectionControllerTests.cs index 9979a18faa..3f67b8c6fd 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCollectionControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCollectionControllerTests.cs @@ -2,6 +2,7 @@ using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Stores; using Grand.Infrastructure.Mapper; @@ -123,7 +124,7 @@ public async Task ListPost_ForcesScopeDefaultStoreIdOntoSearchModel() { _scopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); _collectionServiceMock - .Setup(c => c.GetAllCollections(It.IsAny(), "store-1", null, 0, 10, true)) + .Setup(c => c.GetAllCollections(It.IsAny(), CatalogAccess.IncludingHidden("store-1"), 0, 10)) .ReturnsAsync(new Grand.Domain.PagedList(new List(), 0, 10)); var model = new CollectionListModel { SearchStoreId = "attacker-supplied-store" }; @@ -137,7 +138,8 @@ public async Task ListPost_GlobalScope_LeavesSubmittedSearchStoreIdUntouched() { _scopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); _collectionServiceMock - .Setup(c => c.GetAllCollections(It.IsAny(), "admin-submitted-store", null, 0, 10, true)) + .Setup(c => c.GetAllCollections(It.IsAny(), + CatalogAccess.IncludingHidden("admin-submitted-store"), 0, 10)) .ReturnsAsync(new Grand.Domain.PagedList(new List(), 0, 10)); var model = new CollectionListModel { SearchStoreId = "admin-submitted-store" }; diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseDiscountControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseDiscountControllerTests.cs index 3ed0b458db..5e8a751517 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseDiscountControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseDiscountControllerTests.cs @@ -8,6 +8,7 @@ using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Queries.Catalog; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain; using Grand.Domain.Catalog; using Grand.Domain.Discounts; @@ -449,13 +450,13 @@ public async Task CategoryAddPopupList_GlobalScope_PassesEmptyStoreId() { _scope.Setup(x => x.GetDefaultStoreIdAsync()).ReturnsAsync((string?)null); var categoryService = new Mock(); - categoryService.Setup(x => x.GetAllCategories(null, null, "", null, 0, 10, true)) + categoryService.Setup(x => x.GetAllCategories(null, null, CatalogAccess.IncludingHidden(), 0, 10)) .ReturnsAsync(new PagedList(new List(), 0, 10, 0)); await _sut.CategoryAddPopupList(new DataSourceRequest { Page = 1, PageSize = 10 }, new DiscountModel.AddCategoryToDiscountModel(), categoryService.Object); - categoryService.Verify(x => x.GetAllCategories(null, null, "", null, 0, 10, true), Times.Once); + categoryService.Verify(x => x.GetAllCategories(null, null, CatalogAccess.IncludingHidden(), 0, 10), Times.Once); } [TestMethod] @@ -463,13 +464,14 @@ public async Task CategoryAddPopupList_StoreScope_PassesScopedStoreId() { _scope.Setup(x => x.GetDefaultStoreIdAsync()).ReturnsAsync("store-a"); var categoryService = new Mock(); - categoryService.Setup(x => x.GetAllCategories(null, null, "store-a", null, 0, 10, true)) + categoryService.Setup(x => x.GetAllCategories(null, null, CatalogAccess.IncludingHidden("store-a"), 0, 10)) .ReturnsAsync(new PagedList(new List(), 0, 10, 0)); await _sut.CategoryAddPopupList(new DataSourceRequest { Page = 1, PageSize = 10 }, new DiscountModel.AddCategoryToDiscountModel(), categoryService.Object); - categoryService.Verify(x => x.GetAllCategories(null, null, "store-a", null, 0, 10, true), Times.Once); + categoryService.Verify(x => x.GetAllCategories(null, null, + CatalogAccess.IncludingHidden("store-a"), 0, 10), Times.Once); } [TestMethod] @@ -531,13 +533,13 @@ public async Task BrandAddPopupList_GlobalScope_PassesEmptyStoreId() { _scope.Setup(x => x.GetDefaultStoreIdAsync()).ReturnsAsync((string?)null); var brandService = new Mock(); - brandService.Setup(x => x.GetAllBrands(null, "", null, 0, 10, true)) + brandService.Setup(x => x.GetAllBrands(null, CatalogAccess.IncludingHidden(), 0, 10)) .ReturnsAsync(new PagedList(new List(), 0, 10, 0)); await _sut.BrandAddPopupList(new DataSourceRequest { Page = 1, PageSize = 10 }, new DiscountModel.AddBrandToDiscountModel(), brandService.Object); - brandService.Verify(x => x.GetAllBrands(null, "", null, 0, 10, true), Times.Once); + brandService.Verify(x => x.GetAllBrands(null, CatalogAccess.IncludingHidden(), 0, 10), Times.Once); } [TestMethod] @@ -545,13 +547,13 @@ public async Task BrandAddPopupList_StoreScope_PassesScopedStoreId() { _scope.Setup(x => x.GetDefaultStoreIdAsync()).ReturnsAsync("store-a"); var brandService = new Mock(); - brandService.Setup(x => x.GetAllBrands(null, "store-a", null, 0, 10, true)) + brandService.Setup(x => x.GetAllBrands(null, CatalogAccess.IncludingHidden("store-a"), 0, 10)) .ReturnsAsync(new PagedList(new List(), 0, 10, 0)); await _sut.BrandAddPopupList(new DataSourceRequest { Page = 1, PageSize = 10 }, new DiscountModel.AddBrandToDiscountModel(), brandService.Object); - brandService.Verify(x => x.GetAllBrands(null, "store-a", null, 0, 10, true), Times.Once); + brandService.Verify(x => x.GetAllBrands(null, CatalogAccess.IncludingHidden("store-a"), 0, 10), Times.Once); } [TestMethod] @@ -613,13 +615,13 @@ public async Task CollectionAddPopupList_GlobalScope_PassesEmptyStoreId() { _scope.Setup(x => x.GetDefaultStoreIdAsync()).ReturnsAsync((string?)null); var collectionService = new Mock(); - collectionService.Setup(x => x.GetAllCollections(null, "", null, 0, 10, true)) + collectionService.Setup(x => x.GetAllCollections(null, CatalogAccess.IncludingHidden(), 0, 10)) .ReturnsAsync(new PagedList(new List(), 0, 10, 0)); await _sut.CollectionAddPopupList(new DataSourceRequest { Page = 1, PageSize = 10 }, new DiscountModel.AddCollectionToDiscountModel(), collectionService.Object); - collectionService.Verify(x => x.GetAllCollections(null, "", null, 0, 10, true), Times.Once); + collectionService.Verify(x => x.GetAllCollections(null, CatalogAccess.IncludingHidden(), 0, 10), Times.Once); } [TestMethod] @@ -627,13 +629,14 @@ public async Task CollectionAddPopupList_StoreScope_PassesScopedStoreId() { _scope.Setup(x => x.GetDefaultStoreIdAsync()).ReturnsAsync("store-a"); var collectionService = new Mock(); - collectionService.Setup(x => x.GetAllCollections(null, "store-a", null, 0, 10, true)) + collectionService.Setup(x => x.GetAllCollections(null, CatalogAccess.IncludingHidden("store-a"), 0, 10)) .ReturnsAsync(new PagedList(new List(), 0, 10, 0)); await _sut.CollectionAddPopupList(new DataSourceRequest { Page = 1, PageSize = 10 }, new DiscountModel.AddCollectionToDiscountModel(), collectionService.Object); - collectionService.Verify(x => x.GetAllCollections(null, "store-a", null, 0, 10, true), Times.Once); + collectionService.Verify(x => x.GetAllCollections(null, + CatalogAccess.IncludingHidden("store-a"), 0, 10), Times.Once); } [TestMethod] diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseOrderControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseOrderControllerTests.cs index 7f4658b67a..988bc2e7f7 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseOrderControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseOrderControllerTests.cs @@ -1,6 +1,7 @@ using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Common.Pdf; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Localization; using Grand.Domain.Orders; using Grand.Infrastructure; @@ -264,23 +265,25 @@ public async Task ProductSearchAutoComplete_VendorScope_ForcesVendorIdIntoSearch new Grand.Domain.PagedList(new List(), 0, 15); productServiceMock .Setup(p => p.SearchProducts( - null, It.IsAny(), It.IsAny(), 15, It.IsAny>(), It.IsAny(), It.IsAny(), - null, "vendor-A", It.IsAny(), It.IsAny(), It.IsAny(), + CatalogAccess.IncludingHidden(), It.IsAny(), It.IsAny(), 15, It.IsAny>(), + It.IsAny(), It.IsAny(), + "vendor-A", It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), "abc", It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny(), - true, It.IsAny())) + It.IsAny())) .ReturnsAsync((pagedProducts, (IList)new List())); await _controller.ProductSearchAutoComplete("abc", productServiceMock.Object); productServiceMock.Verify(p => p.SearchProducts( - null, It.IsAny(), It.IsAny(), 15, It.IsAny>(), It.IsAny(), It.IsAny(), - null, "vendor-A", It.IsAny(), It.IsAny(), It.IsAny(), + CatalogAccess.IncludingHidden(), It.IsAny(), It.IsAny(), 15, It.IsAny>(), + It.IsAny(), It.IsAny(), + "vendor-A", It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), "abc", It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny(), - true, It.IsAny()), Times.Once); + It.IsAny()), Times.Once); } [TestMethod] diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductControllerTests.cs index d66e54e95a..26a1fe9d63 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductControllerTests.cs @@ -5,6 +5,7 @@ using Grand.Business.Core.Interfaces.Common.Security; using Grand.Business.Core.Interfaces.ExportImport; using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain; using Grand.Domain.Catalog; using Grand.Domain.Media; @@ -411,7 +412,8 @@ public async Task DeleteSelected_FiltersOutProductsScopeDenies() // bulk-delete endpoint (any staff could delete any product id in the system). var owned = new Product { Id = "owned" }; var foreign = new Product { Id = "foreign" }; - _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "owned", "foreign" }, null, "", true)) + _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "owned", "foreign" }, + CatalogAccess.IncludingHidden())) .ReturnsAsync(new List { owned, foreign }); _scopeMock.Setup(s => s.HasAccess(owned)).ReturnsAsync(true); _scopeMock.Setup(s => s.HasAccess(foreign)).ReturnsAsync(false); @@ -426,7 +428,7 @@ public async Task DeleteSelected_FiltersOutProductsScopeDenies() public async Task DeleteSelected_AllProductsScopeDenies_DoesNotCallDeleteSelected() { var foreign = new Product { Id = "foreign" }; - _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "foreign" }, null, "", true)) + _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "foreign" }, CatalogAccess.IncludingHidden())) .ReturnsAsync(new List { foreign }); _scopeMock.Setup(s => s.HasAccess(foreign)).ReturnsAsync(false); @@ -522,7 +524,8 @@ public async Task LoadProductFriendlyNames_EmptyInput_ReturnsEmptyText() var json = result as JsonResult; Assert.IsNotNull(json); Assert.AreEqual("", GetTextProperty(json.Value)); - _productServiceMock.Verify(s => s.GetProductsByIds(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + _productServiceMock.Verify(s => s.GetProductsByIds(It.IsAny(), + It.IsAny()), Times.Never); } [TestMethod] @@ -530,7 +533,7 @@ public async Task LoadProductFriendlyNames_ScopeDeniesAccess_SkipsProduct() { var allowed = new Product { Id = "p1", Name = "Allowed" }; var denied = new Product { Id = "p2", Name = "Denied" }; - _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "p1", "p2" }, null, "", true)) + _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "p1", "p2" }, CatalogAccess.IncludingHidden())) .ReturnsAsync(new List { allowed, denied }); _scopeMock.Setup(s => s.HasAccess(allowed)).ReturnsAsync(true); _scopeMock.Setup(s => s.HasAccess(denied)).ReturnsAsync(false); @@ -551,7 +554,7 @@ public async Task LoadProductFriendlyNames_ScopeGrantsAccess_IncludesAllProducts { var p1 = new Product { Id = "p1", Name = "First" }; var p2 = new Product { Id = "p2", Name = "Second" }; - _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "p1", "p2" }, null, "", true)) + _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "p1", "p2" }, CatalogAccess.IncludingHidden())) .ReturnsAsync(new List { p1, p2 }); _scopeMock.Setup(s => s.HasAccess(It.IsAny())).ReturnsAsync(true); @@ -1979,7 +1982,7 @@ public async Task AssociatedProductList_ScopeGrantsAccess_ReturnsGrid() var product = new Product { Id = "p1" }; _productServiceMock.Setup(p => p.GetProductById("p1", false)).ReturnsAsync(product); _scopeMock.Setup(s => s.CanView(product)).ReturnsAsync(true); - _productServiceMock.Setup(p => p.GetAssociatedProducts("p1", null, "", "", true)) + _productServiceMock.Setup(p => p.GetAssociatedProducts("p1", CatalogAccess.IncludingHidden(), "")) .ReturnsAsync(new List { new() { Id = "a1", Name = "Assoc", DisplayOrder = 1 } }); var result = await _controller.AssociatedProductList(new Grand.Web.Common.DataSource.DataSourceRequest(), "p1"); @@ -2920,7 +2923,8 @@ public async Task ExportExcelSelected_NullSelectedIds_ExportsEmptyList() var file = result as FileContentResult; Assert.IsNotNull(file); - _productServiceMock.Verify(p => p.GetProductsByIds(It.IsAny(), It.IsAny(), It.IsAny(), true), Times.Never); + _productServiceMock.Verify(p => p.GetProductsByIds(It.IsAny(), + It.IsAny()), Times.Never); } [TestMethod] @@ -2931,7 +2935,8 @@ public async Task ExportExcelSelected_FiltersOutProductsScopeDenies() // Admin (where the original had no check at all) gets the same protection. var owned = new Product { Id = "owned" }; var foreign = new Product { Id = "foreign" }; - _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "owned", "foreign" }, null, "", true)) + _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "owned", "foreign" }, + CatalogAccess.IncludingHidden())) .ReturnsAsync([owned, foreign]); _scopeMock.Setup(s => s.HasAccess(owned)).ReturnsAsync(true); _scopeMock.Setup(s => s.HasAccess(foreign)).ReturnsAsync(false); @@ -3133,7 +3138,8 @@ public async Task BulkEditUpdate_FiltersOutProductsScopeDenies() // that per-item gate uniformly. var owned = new Product { Id = "owned" }; var foreign = new Product { Id = "foreign" }; - _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "owned", "foreign" }, null, "", true)) + _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "owned", "foreign" }, + CatalogAccess.IncludingHidden())) .ReturnsAsync(new List { owned, foreign }); _scopeMock.Setup(s => s.HasAccess(owned)).ReturnsAsync(true); _scopeMock.Setup(s => s.HasAccess(foreign)).ReturnsAsync(false); @@ -3152,7 +3158,7 @@ public async Task BulkEditUpdate_FiltersOutProductsScopeDenies() public async Task BulkEditUpdate_AllProductsScopeDenies_DoesNotCallUpdateBulkEdit() { var foreign = new Product { Id = "foreign" }; - _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "foreign" }, null, "", true)) + _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "foreign" }, CatalogAccess.IncludingHidden())) .ReturnsAsync(new List { foreign }); _scopeMock.Setup(s => s.HasAccess(foreign)).ReturnsAsync(false); @@ -3177,7 +3183,8 @@ public async Task BulkEditDelete_FiltersOutProductsScopeDenies() { var owned = new Product { Id = "owned" }; var foreign = new Product { Id = "foreign" }; - _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "owned", "foreign" }, null, "", true)) + _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "owned", "foreign" }, + CatalogAccess.IncludingHidden())) .ReturnsAsync(new List { owned, foreign }); _scopeMock.Setup(s => s.HasAccess(owned)).ReturnsAsync(true); _scopeMock.Setup(s => s.HasAccess(foreign)).ReturnsAsync(false); @@ -3195,7 +3202,7 @@ await _controller.BulkEditDelete(new List { public async Task BulkEditDelete_AllProductsScopeDenies_DoesNotCallDeleteBulkEdit() { var foreign = new Product { Id = "foreign" }; - _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "foreign" }, null, "", true)) + _productServiceMock.Setup(p => p.GetProductsByIds(new[] { "foreign" }, CatalogAccess.IncludingHidden())) .ReturnsAsync(new List { foreign }); _scopeMock.Setup(s => s.HasAccess(foreign)).ReturnsAsync(false); @@ -3211,7 +3218,8 @@ public async Task BulkEditUpdate_ProductsWithEmptyIds_AreIgnoredWithoutServiceCa var result = await _controller.BulkEditUpdate(new List { new() { Id = "" } }); Assert.IsInstanceOfType(result, typeof(JsonResult)); - _productServiceMock.Verify(p => p.GetProductsByIds(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + _productServiceMock.Verify(p => p.GetProductsByIds(It.IsAny(), + It.IsAny()), Times.Never); _productViewModelServiceMock.Verify( s => s.UpdateBulkEdit(It.IsAny>()), Times.Never); } diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductReviewControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductReviewControllerTests.cs index 9188be0c91..d80d4997b6 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductReviewControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductReviewControllerTests.cs @@ -1,5 +1,6 @@ using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Web.AdminShared.Controllers; using Grand.Web.AdminShared.Interfaces; @@ -320,14 +321,13 @@ public async Task ProductSearchAutoComplete_TermTooShort_ReturnsEmptyContent() Assert.IsNotNull(result); Assert.AreEqual("", result.Content); productService.Verify(s => s.SearchProducts( - customer: null, + access: It.IsAny(), loadFilterableSpecificationAttributeOptionIds: It.IsAny(), pageIndex: It.IsAny(), pageSize: It.IsAny(), categoryIds: It.IsAny>(), brandId: It.IsAny(), collectionId: It.IsAny(), - storeId: It.IsAny(), vendorId: It.IsAny(), warehouseId: It.IsAny(), productType: It.IsAny(), @@ -346,7 +346,6 @@ public async Task ProductSearchAutoComplete_TermTooShort_ReturnsEmptyContent() filteredSpecs: It.IsAny>(), specificationOptions: It.IsAny>(), orderBy: It.IsAny(), - showHidden: It.IsAny(), overridePublished: It.IsAny()), Times.Never); } @@ -356,14 +355,13 @@ public async Task ProductSearchAutoComplete_Store_PassesDefaultStoreId() _scope.Setup(s => s.DefaultStoreId).Returns("store-1"); var productService = new Mock(); productService.Setup(s => s.SearchProducts( - customer: null, + access: CatalogAccess.IncludingHidden("store-1"), loadFilterableSpecificationAttributeOptionIds: false, pageIndex: 0, pageSize: 15, categoryIds: null, brandId: "", collectionId: "", - storeId: "store-1", vendorId: "", warehouseId: "", productType: null, @@ -382,7 +380,6 @@ public async Task ProductSearchAutoComplete_Store_PassesDefaultStoreId() filteredSpecs: null, specificationOptions: null, orderBy: Grand.Domain.Catalog.ProductSortingEnum.Position, - showHidden: true, overridePublished: null)) .ReturnsAsync((new Grand.Domain.PagedList(new List(), 0, 15), new List())); diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseSearchControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseSearchControllerTests.cs index 6ca9a631cb..c88ab33958 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseSearchControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseSearchControllerTests.cs @@ -1,6 +1,7 @@ using Grand.Business.Core.Interfaces.Catalog.Brands; using Grand.Business.Core.Interfaces.Catalog.Categories; using Grand.Business.Core.Interfaces.Catalog.Collections; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain; using Grand.Domain.Admin; using Grand.Domain.Catalog; @@ -83,7 +84,7 @@ public async Task Category_DefaultPickerStoreId_PassesEmptyStoreIdToService() { var controller = CreateController(""); _categoryServiceMock - .Setup(s => s.GetAllCategories(null, null, "", _customer, 0, 10, false)) + .Setup(s => s.GetAllCategories(null, null, CatalogAccess.For(_customer, ""), 0, 10)) .ReturnsAsync(new PagedList([new Category { Id = "cat-1", Name = "Books" }], 0, 10)); _categoryServiceMock.Setup(s => s.GetFormattedBreadCrumb(It.IsAny(), ">>", "")) .ReturnsAsync("Books"); @@ -93,7 +94,8 @@ public async Task Category_DefaultPickerStoreId_PassesEmptyStoreIdToService() var rows = ReadRows(result); Assert.AreEqual(1, rows.Length); Assert.AreEqual("cat-1", rows[0].Id); - _categoryServiceMock.Verify(s => s.GetAllCategories(null, null, "", _customer, 0, 10, false), Times.Once); + _categoryServiceMock.Verify(s => s.GetAllCategories(null, null, + CatalogAccess.For(_customer, ""), 0, 10), Times.Once); } [TestMethod] @@ -101,12 +103,13 @@ public async Task Category_OverriddenPickerStoreId_PassesStoreIdToService() { var controller = CreateController("store-1"); _categoryServiceMock - .Setup(s => s.GetAllCategories(null, null, "store-1", _customer, 0, 10, false)) + .Setup(s => s.GetAllCategories(null, null, CatalogAccess.For(_customer, "store-1"), 0, 10)) .ReturnsAsync(new PagedList([], 0, 10)); await controller.Category(null, EmptyFilter()); - _categoryServiceMock.Verify(s => s.GetAllCategories(null, null, "store-1", _customer, 0, 10, false), Times.Once); + _categoryServiceMock.Verify(s => s.GetAllCategories(null, null, + CatalogAccess.For(_customer, "store-1"), 0, 10), Times.Once); } [TestMethod] @@ -114,7 +117,7 @@ public async Task Collection_DefaultPickerStoreId_PassesEmptyStoreIdToService() { var controller = CreateController(""); _collectionServiceMock - .Setup(s => s.GetAllCollections(null, "", _customer, 0, 10, false)) + .Setup(s => s.GetAllCollections(null, CatalogAccess.For(_customer, ""), 0, 10)) .ReturnsAsync(new PagedList([new Collection { Id = "col-1", Name = "Summer" }], 0, 10)); var result = await controller.Collection(null, EmptyFilter()); @@ -122,7 +125,8 @@ public async Task Collection_DefaultPickerStoreId_PassesEmptyStoreIdToService() var rows = ReadRows(result); Assert.AreEqual(1, rows.Length); Assert.AreEqual("Summer", rows[0].Name); - _collectionServiceMock.Verify(s => s.GetAllCollections(null, "", _customer, 0, 10, false), Times.Once); + _collectionServiceMock.Verify(s => s.GetAllCollections(null, + CatalogAccess.For(_customer, ""), 0, 10), Times.Once); } [TestMethod] @@ -130,12 +134,13 @@ public async Task Collection_OverriddenPickerStoreId_PassesStoreIdToService() { var controller = CreateController("store-1"); _collectionServiceMock - .Setup(s => s.GetAllCollections(null, "store-1", _customer, 0, 10, false)) + .Setup(s => s.GetAllCollections(null, CatalogAccess.For(_customer, "store-1"), 0, 10)) .ReturnsAsync(new PagedList([], 0, 10)); await controller.Collection(null, EmptyFilter()); - _collectionServiceMock.Verify(s => s.GetAllCollections(null, "store-1", _customer, 0, 10, false), Times.Once); + _collectionServiceMock.Verify(s => s.GetAllCollections(null, + CatalogAccess.For(_customer, "store-1"), 0, 10), Times.Once); } [TestMethod] @@ -143,7 +148,7 @@ public async Task Brand_DefaultPickerStoreId_PassesEmptyStoreIdToService() { var controller = CreateController(""); _brandServiceMock - .Setup(s => s.GetAllBrands(null, "", _customer, 0, 10, false)) + .Setup(s => s.GetAllBrands(null, CatalogAccess.For(_customer, ""), 0, 10)) .ReturnsAsync(new PagedList([new Brand { Id = "brand-1", Name = "Acme" }], 0, 10)); var result = await controller.Brand(null, EmptyFilter()); @@ -151,7 +156,7 @@ public async Task Brand_DefaultPickerStoreId_PassesEmptyStoreIdToService() var rows = ReadRows(result); Assert.AreEqual(1, rows.Length); Assert.AreEqual("Acme", rows[0].Name); - _brandServiceMock.Verify(s => s.GetAllBrands(null, "", _customer, 0, 10, false), Times.Once); + _brandServiceMock.Verify(s => s.GetAllBrands(null, CatalogAccess.For(_customer, ""), 0, 10), Times.Once); } [TestMethod] @@ -159,11 +164,11 @@ public async Task Brand_OverriddenPickerStoreId_PassesStoreIdToService() { var controller = CreateController("store-1"); _brandServiceMock - .Setup(s => s.GetAllBrands(null, "store-1", _customer, 0, 10, false)) + .Setup(s => s.GetAllBrands(null, CatalogAccess.For(_customer, "store-1"), 0, 10)) .ReturnsAsync(new PagedList([], 0, 10)); await controller.Brand(null, EmptyFilter()); - _brandServiceMock.Verify(s => s.GetAllBrands(null, "store-1", _customer, 0, 10, false), Times.Once); + _brandServiceMock.Verify(s => s.GetAllBrands(null, CatalogAccess.For(_customer, "store-1"), 0, 10), Times.Once); } } diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseSpecificationAttributeControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseSpecificationAttributeControllerTests.cs index 4934097165..8e763df5b1 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseSpecificationAttributeControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseSpecificationAttributeControllerTests.cs @@ -1,5 +1,6 @@ using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain; using Grand.Domain.Catalog; using Grand.Domain.Seo; @@ -84,14 +85,13 @@ public async Task UsedByProducts_PassesScopeStoreIdToSearchProducts() _service.Setup(x => x.GetSpecificationAttributeById("s1")).ReturnsAsync(spec); _scope.Setup(x => x.CanView(spec)).ReturnsAsync(true); _productService.Setup(x => x.SearchProducts( - customer: null, + access: CatalogAccess.IncludingHidden("store-a"), loadFilterableSpecificationAttributeOptionIds: false, pageIndex: 0, pageSize: 10, categoryIds: null, brandId: "", collectionId: "", - storeId: "store-a", vendorId: "", warehouseId: "", productType: null, @@ -110,21 +110,19 @@ public async Task UsedByProducts_PassesScopeStoreIdToSearchProducts() filteredSpecs: null, specificationOptions: It.Is>(l => l.Contains("o1")), orderBy: It.IsAny(), - showHidden: true, overridePublished: null)) .ReturnsAsync((new PagedList(new List(), 0, 10, 0), (IList)new List())); await _sut.UsedByProducts(new DataSourceRequest { Page = 1, PageSize = 10 }, "s1"); _productService.Verify(x => x.SearchProducts( - customer: null, + access: CatalogAccess.IncludingHidden("store-a"), loadFilterableSpecificationAttributeOptionIds: false, pageIndex: 0, pageSize: 10, categoryIds: null, brandId: "", collectionId: "", - storeId: "store-a", vendorId: "", warehouseId: "", productType: null, @@ -143,7 +141,6 @@ public async Task UsedByProducts_PassesScopeStoreIdToSearchProducts() filteredSpecs: null, specificationOptions: It.IsAny>(), orderBy: It.IsAny(), - showHidden: true, overridePublished: null), Times.Once); } } diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/ProductViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/ProductViewModelServiceTests.cs index 0194fc9ad8..bbca32b343 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Services/ProductViewModelServiceTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Services/ProductViewModelServiceTests.cs @@ -13,6 +13,7 @@ using Grand.Business.Core.Interfaces.Common.Stores; using Grand.Business.Core.Interfaces.Customers; using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain; using Grand.Domain.Catalog; using Grand.Domain.Customers; @@ -306,24 +307,24 @@ public async Task PrepareTierPriceModel_GlobalScope_IncludesStoreDropdown() private void SetupSearchProducts() { _productServiceMock.Setup(p => p.SearchProducts( - null, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), - It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny>(), - It.IsAny(), It.IsAny(), It.IsAny())) + It.IsAny(), It.IsAny())) .ReturnsAsync((new PagedList(), new List())); } private void VerifySearchProductsCalledWithVendorId(string expectedVendorId) { _productServiceMock.Verify(p => p.SearchProducts( - null, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), - It.IsAny(), It.IsAny(), It.IsAny(), expectedVendorId, It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), + It.IsAny(), It.IsAny(), expectedVendorId, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny>(), - It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + It.IsAny(), It.IsAny()), Times.Once); } [TestMethod] diff --git a/src/Tests/Grand.Web.Common.Tests/Extensions/CatalogAccessExtensionsTests.cs b/src/Tests/Grand.Web.Common.Tests/Extensions/CatalogAccessExtensionsTests.cs new file mode 100644 index 0000000000..f2bf011765 --- /dev/null +++ b/src/Tests/Grand.Web.Common.Tests/Extensions/CatalogAccessExtensionsTests.cs @@ -0,0 +1,30 @@ +using Grand.Domain.Customers; +using Grand.Domain.Stores; +using Grand.Infrastructure; +using Grand.Web.Common.Extensions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Common.Tests.Extensions; + +[TestClass] +public class CatalogAccessExtensionsTests +{ + [TestMethod] + public void CatalogAccess_TakesCustomerAndStoreOfTheRequest() + { + //Arrange + var customer = new Customer(); + var accessor = new Mock(); + accessor.Setup(a => a.WorkContext.CurrentCustomer).Returns(customer); + accessor.Setup(a => a.StoreContext.CurrentStore).Returns(new Store { Id = "s1" }); + + //Act + var access = accessor.Object.CatalogAccess(); + + //Assert + Assert.AreSame(customer, access.Customer); + Assert.AreEqual("s1", access.StoreId); + Assert.IsFalse(access.ShowHidden); + } +} diff --git a/src/Tests/Grand.Web.Store.Tests/Controllers/SearchControllerAttributeTests.cs b/src/Tests/Grand.Web.Store.Tests/Controllers/SearchControllerAttributeTests.cs index 69a0a46146..987d19122b 100644 --- a/src/Tests/Grand.Web.Store.Tests/Controllers/SearchControllerAttributeTests.cs +++ b/src/Tests/Grand.Web.Store.Tests/Controllers/SearchControllerAttributeTests.cs @@ -1,4 +1,5 @@ using System.Linq; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Admin; using Grand.Domain.Customers; using Grand.Business.Core.Interfaces.Catalog.Brands; @@ -63,7 +64,7 @@ public async System.Threading.Tasks.Task PickerStoreId_ReflectsCurrentCustomerSt var customer = new Customer { StaffStoreId = "store-42" }; var categoryServiceMock = new Mock(); categoryServiceMock - .Setup(s => s.GetAllCategories(null, null, "store-42", customer, 0, It.IsAny(), false)) + .Setup(s => s.GetAllCategories(null, null, CatalogAccess.For(customer, "store-42"), 0, It.IsAny())) .ReturnsAsync(new Grand.Domain.PagedList([], 0, 10)); var workContextMock = new Mock(); @@ -80,6 +81,7 @@ public async System.Threading.Tasks.Task PickerStoreId_ReflectsCurrentCustomerSt await controller.Category(null, new Grand.Web.Common.DataSource.DataSourceRequestFilter { Filters = [] }); - categoryServiceMock.Verify(s => s.GetAllCategories(null, null, "store-42", customer, 0, 10, false), Times.Once); + categoryServiceMock.Verify(s => s.GetAllCategories(null, null, + CatalogAccess.For(customer, "store-42"), 0, 10), Times.Once); } } diff --git a/src/Web/Grand.Web.Admin/Controllers/ProductTagsController.cs b/src/Web/Grand.Web.Admin/Controllers/ProductTagsController.cs index aba1076cf4..1572e71d95 100644 --- a/src/Web/Grand.Web.Admin/Controllers/ProductTagsController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/ProductTagsController.cs @@ -1,6 +1,7 @@ using Grand.Business.Core.Extensions; using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Permissions; using Grand.Domain.Catalog; using Grand.Domain.Seo; @@ -80,7 +81,8 @@ public async Task Products(string tagId, DataSourceRequest comman { var tag = await _productTagService.GetProductTagById(tagId); - var products = (await _productService.SearchProducts(customer: _contextAccessor.WorkContext.CurrentCustomer, + var products = (await _productService.SearchProducts( + access: CatalogAccess.For(_contextAccessor.WorkContext.CurrentCustomer, ""), pageIndex: command.Page - 1, pageSize: command.PageSize, productTag: tag.Name, orderBy: ProductSortingEnum.NameAsc)).products; var gridModel = new DataSourceResult { diff --git a/src/Web/Grand.Web.Admin/Controllers/SearchController.cs b/src/Web/Grand.Web.Admin/Controllers/SearchController.cs index 5e190cf7c7..9b212a3b04 100644 --- a/src/Web/Grand.Web.Admin/Controllers/SearchController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/SearchController.cs @@ -8,6 +8,7 @@ using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Common.Stores; using Grand.Business.Core.Interfaces.Customers; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain; using Grand.Domain.Admin; using Grand.Domain.Customers; @@ -82,8 +83,9 @@ public async Task Index(string searchTerm, FoundMenuItem[] foundM { if (result.Count < _adminSearchSettings.MaxSearchResultsCount && _adminSearchSettings.SearchInProducts) { - var products = (await _productService.SearchProducts(customer: null, keywords: searchTerm, - pageSize: _adminSearchSettings.MaxSearchResultsCount - result.Count, showHidden: true)).products; + var products = (await _productService.SearchProducts( + access: CatalogAccess.IncludingHidden(), keywords: searchTerm, + pageSize: _adminSearchSettings.MaxSearchResultsCount - result.Count)).products; foreach (var product in products) result.Add(new Tuple(new { @@ -95,10 +97,12 @@ public async Task Index(string searchTerm, FoundMenuItem[] foundM if (result.Count < _adminSearchSettings.MaxSearchResultsCount && _adminSearchSettings.SearchInCategories) { - var categories = await _categoryService.GetAllCategories(parentId: null, categoryName: searchTerm, storeId: "", - customer: _contextAccessor.WorkContext.CurrentCustomer, - pageSize: _adminSearchSettings.MaxSearchResultsCount - result.Count, showHidden: - await _groupService.IsAdmin(_contextAccessor.WorkContext.CurrentCustomer)); + var customer = _contextAccessor.WorkContext.CurrentCustomer; + var access = await _groupService.IsAdmin(customer) + ? CatalogAccess.IncludingHidden() + : CatalogAccess.For(customer, ""); + var categories = await _categoryService.GetAllCategories(parentId: null, categoryName: searchTerm, + access: access, pageSize: _adminSearchSettings.MaxSearchResultsCount - result.Count); foreach (var category in categories) result.Add(new Tuple(new { @@ -110,8 +114,9 @@ public async Task Index(string searchTerm, FoundMenuItem[] foundM if (result.Count < _adminSearchSettings.MaxSearchResultsCount && _adminSearchSettings.SearchInCollections) { - var collections = await _collectionService.GetAllCollections(searchTerm, storeId: "", customer: null, - pageSize: _adminSearchSettings.MaxSearchResultsCount - result.Count, showHidden: true); + var collections = await _collectionService.GetAllCollections(searchTerm, + access: CatalogAccess.IncludingHidden(), + pageSize: _adminSearchSettings.MaxSearchResultsCount - result.Count); foreach (var collection in collections) result.Add(new Tuple(new { diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseBrandController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseBrandController.cs index d4855a2660..324fa0cb14 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseBrandController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseBrandController.cs @@ -4,6 +4,7 @@ using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Common.Stores; using Grand.Business.Core.Interfaces.ExportImport; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Permissions; using Grand.Web.AdminShared.Extensions.Mapping; @@ -75,7 +76,7 @@ public async Task List(DataSourceRequest command, BrandListModel { if (scope.DefaultStoreId is not null) model.SearchStoreId = scope.DefaultStoreId; var brands = await brandService.GetAllBrands(model.SearchBrandName, - model.SearchStoreId, customer: null, command.Page - 1, command.PageSize, true); + CatalogAccess.IncludingHidden(model.SearchStoreId), command.Page - 1, command.PageSize); var gridModel = new DataSourceResult { Data = brands.Select(x => x.ToModel()), Total = brands.TotalCount @@ -251,7 +252,8 @@ public async Task ExportXlsx([FromServices] IExportManager { try { - var bytes = await exportManager.Export(await brandService.GetAllBrands(brandName: "", storeId: scope.DefaultStoreId ?? "", customer: null, showHidden: true), + var bytes = await exportManager.Export(await brandService.GetAllBrands(brandName: "", + access: CatalogAccess.IncludingHidden(scope.DefaultStoreId ?? "")), HttpContext.RequestAborted); return File(bytes, "text/xls", "brands.xlsx"); } diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseCategoryController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseCategoryController.cs index 97a5ef373c..ddb196c258 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseCategoryController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseCategoryController.cs @@ -4,6 +4,7 @@ using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.ExportImport; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Permissions; using Grand.Web.AdminShared.Extensions; @@ -219,7 +220,8 @@ public async Task ExportXlsx([FromServices] IExportManager List(DataSourceRequest command, CollectionListM { if (scope.DefaultStoreId is not null) model.SearchStoreId = scope.DefaultStoreId; var collections = await collectionService.GetAllCollections(model.SearchCollectionName, - model.SearchStoreId, customer: null, command.Page - 1, command.PageSize, true); + CatalogAccess.IncludingHidden(model.SearchStoreId), command.Page - 1, command.PageSize); var gridModel = new DataSourceResult { Data = collections.Select(x => x.ToModel()), Total = collections.TotalCount @@ -252,7 +253,8 @@ public async Task ExportXlsx([FromServices] IExportManager CategoryAddPopupList(DataSourceRequest command, DiscountModel.AddCategoryToDiscountModel model, [FromServices] ICategoryService categoryService) { var categories = await categoryService.GetAllCategories(parentId: null, categoryName: model.SearchCategoryName, - storeId: await scope.GetDefaultStoreIdAsync() ?? "", customer: null, pageIndex: command.Page - 1, - pageSize: command.PageSize, showHidden: true); + access: CatalogAccess.IncludingHidden(await scope.GetDefaultStoreIdAsync() ?? ""), + pageIndex: command.Page - 1, pageSize: command.PageSize); var items = new List(); foreach (var item in categories) { @@ -552,7 +553,8 @@ public async Task BrandAddPopupList(DataSourceRequest command, DiscountModel.AddBrandToDiscountModel model, [FromServices] IBrandService brandService) { var brands = await brandService.GetAllBrands(model.SearchBrandName, - await scope.GetDefaultStoreIdAsync() ?? "", customer: null, command.Page - 1, command.PageSize, true); + CatalogAccess.IncludingHidden(await scope.GetDefaultStoreIdAsync() ?? ""), command.Page - 1, + command.PageSize); return Json(new DataSourceResult { Data = brands.Select(x => x.ToModel()), Total = brands.TotalCount }); } @@ -637,7 +639,8 @@ public async Task CollectionAddPopupList(DataSourceRequest comman DiscountModel.AddCollectionToDiscountModel model, [FromServices] ICollectionService collectionService) { var collections = await collectionService.GetAllCollections(model.SearchCollectionName, - await scope.GetDefaultStoreIdAsync() ?? "", customer: null, command.Page - 1, command.PageSize, true); + CatalogAccess.IncludingHidden(await scope.GetDefaultStoreIdAsync() ?? ""), command.Page - 1, + command.PageSize); return Json(new DataSourceResult { Data = collections.Select(x => x.ToModel()), Total = collections.TotalCount }); } diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseOrderController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseOrderController.cs index 7fd21a1e8f..64a002b715 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseOrderController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseOrderController.cs @@ -1,6 +1,7 @@ using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Common.Pdf; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Orders; using Grand.Domain.Permissions; using Grand.Infrastructure; @@ -181,12 +182,10 @@ public async Task ProductSearchAutoComplete(string term, const int productNumber = 15; var products = (await productService.SearchProducts( - customer: null, - storeId: scope.DefaultStoreId, + access: CatalogAccess.IncludingHidden(scope.DefaultStoreId), vendorId: scope.DefaultVendorId, keywords: term, - pageSize: productNumber, - showHidden: true)).products; + pageSize: productNumber)).products; var result = products.Select(p => new { label = p.Name, productid = p.Id }).ToList(); return Json(result); diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseOrderManagementController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseOrderManagementController.cs index 9912928daa..9f7478123e 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseOrderManagementController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseOrderManagementController.cs @@ -2,6 +2,7 @@ using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Common.Pdf; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Orders; using Grand.Domain.Permissions; using Grand.Infrastructure; @@ -497,15 +498,14 @@ public async Task AddProductToOrder( if (!string.IsNullOrEmpty(model.SearchCategoryId)) categoryIds.Add(model.SearchCategoryId); var gridModel = new Grand.Web.Common.DataSource.DataSourceResult(); - var products = (await productService.SearchProducts(customer: null, categoryIds: categoryIds, - storeId: Scope.DefaultStoreId, + var products = (await productService.SearchProducts( + access: CatalogAccess.IncludingHidden(Scope.DefaultStoreId), categoryIds: categoryIds, brandId: model.SearchBrandId, collectionId: model.SearchCollectionId, productType: model.SearchProductTypeId > 0 ? (Grand.Domain.Catalog.ProductType?)model.SearchProductTypeId : null, keywords: model.SearchProductName, pageIndex: command.Page - 1, - pageSize: command.PageSize, - showHidden: true)).products; + pageSize: command.PageSize)).products; gridModel.Data = products.Select(x => new OrderModel.AddOrderProductModel.ProductModel { Id = x.Id, Name = x.Name, Sku = x.Sku diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductController.cs index 30fdee37af..d8039f7710 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductController.cs @@ -6,6 +6,7 @@ using Grand.Business.Core.Interfaces.Common.Security; using Grand.Business.Core.Interfaces.ExportImport; using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Media; @@ -225,7 +226,8 @@ public async Task DeleteSelected(ICollection selectedIds) // unscoped bulk-delete endpoint (any store staff could delete any product id in the system, // bypassing AccessToEntityByStore entirely) purely because MVC routes actions regardless of // whether a host's views ever link to them. - var products = await productService.GetProductsByIds(selectedIds.ToArray(), customer: null, storeId: "", showHidden: true); + var products = await productService.GetProductsByIds(selectedIds.ToArray(), + access: CatalogAccess.IncludingHidden()); var allowedIds = new List(); foreach (var product in products) if (await scope.HasAccess(product)) @@ -309,7 +311,8 @@ public async Task LoadProductFriendlyNames(string productIds) .Select(x => x.Trim()) .ToList(); - var products = await productService.GetProductsByIds(ids.ToArray(), customer: null, storeId: "", showHidden: true); + var products = await productService.GetProductsByIds(ids.ToArray(), + access: CatalogAccess.IncludingHidden()); for (var i = 0; i <= products.Count - 1; i++) { // Filters the friendly-name list, not a hard deny of the whole action: matches Store's @@ -1223,8 +1226,9 @@ public async Task AssociatedProductList(DataSourceRequest command // associated-product set, the ones they themselves own. That vendor filter is unconditional in // GetAssociatedProducts regardless of showHidden, unlike its storeId parameter (which only applies // when showHidden is false - moot here since all three hosts pass showHidden: true). - var associatedProducts = await productService.GetAssociatedProducts(productId, customer: null, - vendorId: AssociatedProductVendorId, showHidden: true); + var associatedProducts = await productService.GetAssociatedProducts(productId, + access: CatalogAccess.IncludingHidden(), + vendorId: AssociatedProductVendorId); var associatedProductsModel = associatedProducts .Select(x => new ProductModel.AssociatedProductModel { Id = x.Id, @@ -1835,7 +1839,7 @@ public async Task ExportExcelSelected(string selectedIds, .Split([','], StringSplitOptions.RemoveEmptyEntries) .Select(x => x) .ToArray(); - products.AddRange(await productService.GetProductsByIds(ids, customer: null, storeId: "", showHidden: true)); + products.AddRange(await productService.GetProductsByIds(ids, access: CatalogAccess.IncludingHidden())); } // Unlike ExportExcelAll, selectedIds is caller-supplied and not derived from a scoped search - @@ -2011,7 +2015,8 @@ private async Task> FilterBulkEditProductsByAccess( .ToDictionary(g => g.Key, g => g.First()); if (byId.Count == 0) return []; - var loadedProducts = await productService.GetProductsByIds(byId.Keys.ToArray(), customer: null, storeId: "", showHidden: true); + var loadedProducts = await productService.GetProductsByIds(byId.Keys.ToArray(), + access: CatalogAccess.IncludingHidden()); var validProducts = new List(); foreach (var product in loadedProducts) if (await scope.HasAccess(product)) diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductReviewController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductReviewController.cs index 11b33e7769..167386f84b 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductReviewController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductReviewController.cs @@ -1,5 +1,6 @@ using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Permissions; using Grand.Web.AdminShared.Interfaces; @@ -144,11 +145,9 @@ public async Task ProductSearchAutoComplete(string term, const int productNumber = 15; var products = (await productService.SearchProducts( - customer: null, - storeId: scope.DefaultStoreId ?? "", + access: CatalogAccess.IncludingHidden(scope.DefaultStoreId ?? ""), keywords: term, - pageSize: productNumber, - showHidden: true)).products; + pageSize: productNumber)).products; var result = (from p in products select new diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseSearchController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseSearchController.cs index 03b74e4b06..0e2ea63f73 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseSearchController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseSearchController.cs @@ -1,6 +1,7 @@ using Grand.Business.Core.Interfaces.Catalog.Brands; using Grand.Business.Core.Interfaces.Catalog.Categories; using Grand.Business.Core.Interfaces.Catalog.Collections; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Admin; using Grand.Domain.Customers; using Grand.Infrastructure; @@ -29,12 +30,9 @@ public virtual async Task Category(string categoryId, DataSourceR var categories = await categoryService.GetAllCategories( parentId: null, categoryName: model.GetNameFilterValue(), - storeId: PickerStoreId, - customer: CurrentCustomer, + access: CatalogAccess.For(CurrentCustomer, PickerStoreId), pageIndex: 0, - pageSize: adminSearchSettings.CategorySizeLimit, - showHidden: false - ); + pageSize: adminSearchSettings.CategorySizeLimit); var gridModel = await DataSourceResultHelper.GetSearchResult(categoryId, categories, async category => await categoryService.GetFormattedBreadCrumb(category)); return Json(gridModel); @@ -45,12 +43,9 @@ public virtual async Task Collection(string collectionId, DataSou { var collections = await collectionService.GetAllCollections( collectionName: model.GetNameFilterValue(), - storeId: PickerStoreId, - customer: CurrentCustomer, + access: CatalogAccess.For(CurrentCustomer, PickerStoreId), pageIndex: 0, - pageSize: adminSearchSettings.CollectionSizeLimit, - showHidden: false - ); + pageSize: adminSearchSettings.CollectionSizeLimit); var gridModel = await DataSourceResultHelper.GetSearchResult(collectionId, collections, collection => Task.FromResult(collection.Name)); return Json(gridModel); @@ -61,8 +56,7 @@ public virtual async Task Brand(string brandId, DataSourceRequest { var brands = await brandService.GetAllBrands( model.GetNameFilterValue(), - storeId: PickerStoreId, - customer: CurrentCustomer, + access: CatalogAccess.For(CurrentCustomer, PickerStoreId), pageSize: adminSearchSettings.BrandSizeLimit); var gridModel = await DataSourceResultHelper.GetSearchResult(brandId, brands, brand => Task.FromResult(brand.Name)); diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseSpecificationAttributeController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseSpecificationAttributeController.cs index ca95cd5855..447a7ff393 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseSpecificationAttributeController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseSpecificationAttributeController.cs @@ -1,6 +1,7 @@ using Grand.Business.Core.Extensions; using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Permissions; using Grand.Domain.Seo; @@ -43,12 +44,10 @@ public async Task UsedByProducts(DataSourceRequest command, strin if (optionIds.Count > 0) { var products = (await productService.SearchProducts( - customer: null, - storeId: scope.DefaultStoreId ?? "", + access: CatalogAccess.IncludingHidden(scope.DefaultStoreId ?? ""), specificationOptions: optionIds, pageIndex: command.Page - 1, - pageSize: command.PageSize, - showHidden: true)).products; + pageSize: command.PageSize)).products; total = products.TotalCount; foreach (var item in products) diff --git a/src/Web/Grand.Web.AdminShared/Services/CategoryViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CategoryViewModelService.cs index 558f484194..3f35911937 100644 --- a/src/Web/Grand.Web.AdminShared/Services/CategoryViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/CategoryViewModelService.cs @@ -6,6 +6,7 @@ using Grand.Business.Core.Interfaces.Common.Stores; using Grand.Business.Core.Interfaces.Customers; using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Discounts; using Grand.Web.AdminShared.Extensions; @@ -78,11 +79,9 @@ public virtual async Task PrepareCategoryListModel(string sto var categories = await _categoryService.GetAllCategories( parentId: null, categoryName: model.SearchCategoryName, - storeId: model.SearchStoreId, - customer: null, + access: CatalogAccess.IncludingHidden(model.SearchStoreId), pageSize: pageSize, - pageIndex: pageIndex - 1, - showHidden: true); + pageIndex: pageIndex - 1); var categoryListModel = new List(); foreach (var x in categories) @@ -199,9 +198,8 @@ public virtual async Task DeleteCategory(Category category) PrepareCategoryProductModel(string categoryId, int pageIndex, int pageSize) { var productCategories = await _productCategoryService.GetProductCategoriesByCategoryId(categoryId, - customer: null, - storeId: "", - pageIndex - 1, pageSize, true); + CatalogAccess.IncludingHidden(), + pageIndex - 1, pageSize); var categoryproducts = new List(); foreach (var item in productCategories) diff --git a/src/Web/Grand.Web.AdminShared/Services/CollectionViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CollectionViewModelService.cs index 38bbcaf387..3afcbf8947 100644 --- a/src/Web/Grand.Web.AdminShared/Services/CollectionViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/CollectionViewModelService.cs @@ -6,6 +6,7 @@ using Grand.Business.Core.Interfaces.Common.Stores; using Grand.Business.Core.Interfaces.Customers; using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Discounts; using Grand.Infrastructure; @@ -181,8 +182,8 @@ public virtual async Task DeleteCollection(Collection collection) //collections model.AvailableCollections.Add(new SelectListItem { Text = _translationService.GetResource("Admin.Common.All"), Value = " " }); - foreach (var m in await _collectionService.GetAllCollections(collectionName: "", storeId: "", customer: null, - showHidden: true)) + foreach (var m in await _collectionService.GetAllCollections(collectionName: "", + access: CatalogAccess.IncludingHidden())) model.AvailableCollections.Add(new SelectListItem { Text = m.Name, Value = m.Id }); //stores @@ -219,9 +220,8 @@ public virtual async PrepareCollectionProductModel(string collectionId, string storeId, int pageIndex, int pageSize) { var productCollections = await _productCollectionService.GetProductCollectionsByCollectionId(collectionId, - storeId, - customer: null, - pageIndex - 1, pageSize, true); + CatalogAccess.IncludingHidden(), + pageIndex - 1, pageSize); var items = new List(); foreach (var x in productCollections) items.Add(new CollectionModel.CollectionProductModel { diff --git a/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs index e4cbc47e4c..3f053f82e9 100644 --- a/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs @@ -14,6 +14,7 @@ using Grand.Business.Core.Interfaces.Common.Stores; using Grand.Business.Core.Interfaces.Customers; using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Directory; @@ -623,18 +624,16 @@ public virtual async Task PrepareProductListModel() // of any vendor filter on the model. Admin/Store have no DefaultVendorId, so this falls back to // model.SearchVendorId unchanged for them. var products = (await productService.SearchProducts( - customer: null, + access: CatalogAccess.IncludingHidden(model.SearchStoreId), categoryIds: categoryIds, brandId: model.SearchBrandId, collectionId: model.SearchCollectionId, - storeId: model.SearchStoreId, vendorId: scope.DefaultVendorId ?? model.SearchVendorId, warehouseId: model.SearchWarehouseId, productType: model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null, keywords: model.SearchProductName, pageIndex: pageIndex - 1, pageSize: pageSize, - showHidden: true, showOnHomePage: showOnHomePage, overridePublished: overridePublished, markedAsNewOnly: markedAsNewOnly @@ -719,16 +718,14 @@ public virtual async Task> PrepareProducts(ProductListModel model // vendorId: same override as PrepareProductsModel above - mirrors Vendor's original PrepareProducts, // which always passed vendorId: CurrentVendor.Id regardless of the search model. var products = (await productService.SearchProducts( - customer: null, + access: CatalogAccess.IncludingHidden(model.SearchStoreId), categoryIds: categoryIds, brandId: model.SearchBrandId, collectionId: model.SearchCollectionId, - storeId: model.SearchStoreId, vendorId: scope.DefaultVendorId ?? model.SearchVendorId, warehouseId: model.SearchWarehouseId, productType: model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null, keywords: model.SearchProductName, - showHidden: true, overridePublished: overridePublished )).products; @@ -878,7 +875,8 @@ public virtual async Task DeleteProduct(Product product) public virtual async Task DeleteSelected(IEnumerable selectedIds) { var products = new List(); - products.AddRange(await productService.GetProductsByIds(selectedIds.ToArray(), customer: null, storeId: "", showHidden: true)); + products.AddRange(await productService.GetProductsByIds(selectedIds.ToArray(), + access: CatalogAccess.IncludingHidden())); for (var i = 0; i < products.Count; i++) { await DeleteProduct(products[i]); @@ -1322,16 +1320,15 @@ public virtual async Task PrepareBulkEditListModel() // has no SearchVendorId field for a caller to (mis)supply in the first place, so there is nothing // to fall back to for Admin/Store, whose scope.DefaultVendorId is null and who intentionally see // every vendor's products in bulk-edit. - var products = (await productService.SearchProducts(customer: null, categoryIds: searchCategoryIds, + var products = (await productService.SearchProducts( + access: CatalogAccess.IncludingHidden(model.SearchStoreId), categoryIds: searchCategoryIds, brandId: model.SearchBrandId, collectionId: model.SearchCollectionId, - storeId: model.SearchStoreId, vendorId: scope.DefaultVendorId ?? "", productType: model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null, keywords: model.SearchProductName, pageIndex: pageIndex - 1, - pageSize: pageSize, - showHidden: true)).products; + pageSize: pageSize)).products; var items = new List(); foreach (var x in products) @@ -2602,7 +2599,7 @@ protected virtual async Task> GetChildCategoryIds(string parentCate { var categoriesIds = new List(); var categories = await categoryService.GetAllCategoriesByParentCategoryId( - customer: null, storeId: "", parentCategoryId: parentCategoryId, showHidden: true); + access: CatalogAccess.IncludingHidden(), parentCategoryId: parentCategoryId); foreach (var category in categories) { categoriesIds.Add(category.Id); diff --git a/src/Web/Grand.Web.Common/Extensions/CatalogAccessExtensions.cs b/src/Web/Grand.Web.Common/Extensions/CatalogAccessExtensions.cs new file mode 100644 index 0000000000..0380a6b9b0 --- /dev/null +++ b/src/Web/Grand.Web.Common/Extensions/CatalogAccessExtensions.cs @@ -0,0 +1,19 @@ +using Grand.Business.Core.Utilities.Catalog; +using Grand.Infrastructure; + +namespace Grand.Web.Common.Extensions; + +/// +/// Builds the catalog access of the current request +/// +public static class CatalogAccessExtensions +{ + /// + /// Gets the catalog access of the current request: what its customer may see in its store + /// + public static CatalogAccess CatalogAccess(this IContextAccessor contextAccessor) + { + return Business.Core.Utilities.Catalog.CatalogAccess.For(contextAccessor.WorkContext.CurrentCustomer, + contextAccessor.StoreContext.CurrentStore.Id); + } +} diff --git a/src/Web/Grand.Web.Common/Extensions/ProductList.cs b/src/Web/Grand.Web.Common/Extensions/ProductList.cs index 72c796d7aa..ddddfed04c 100644 --- a/src/Web/Grand.Web.Common/Extensions/ProductList.cs +++ b/src/Web/Grand.Web.Common/Extensions/ProductList.cs @@ -1,4 +1,5 @@ using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain; using Grand.Domain.Catalog; @@ -16,18 +17,15 @@ public static async Task> PrepareProductList(this IProductSe searchCategoryIds.Add(searchCategoryId); var products = (await productService.SearchProducts( - customer: null, + access: CatalogAccess.IncludingHidden(searchStoreId), categoryIds: searchCategoryIds, brandId: searchBrandId, collectionId: searchCollectionId, - storeId: searchStoreId, vendorId: searchVendorId, productType: productTypeId > 0 ? (ProductType?)productTypeId : null, keywords: searchProductName, pageIndex: pageIndex - 1, - pageSize: pageSize, - showHidden: true - )).products; + pageSize: pageSize)).products; return products; } diff --git a/src/Web/Grand.Web/Components/BlogPostProducts.cs b/src/Web/Grand.Web/Components/BlogPostProducts.cs index eaec4f048b..6342b6b9a4 100644 --- a/src/Web/Grand.Web/Components/BlogPostProducts.cs +++ b/src/Web/Grand.Web/Components/BlogPostProducts.cs @@ -3,6 +3,7 @@ using Grand.Domain.Catalog; using Grand.Infrastructure; using Grand.Web.Common.Components; +using Grand.Web.Common.Extensions; using Grand.Web.Features.Models.Products; using Grand.Mediator; using Microsoft.AspNetCore.Mvc; @@ -37,8 +38,8 @@ public async Task InvokeAsync(string blogPostId, int? prod if (!blogproducts.Any()) return Content(""); - var products = await _productService.GetProductsByIds(blogproducts.Select(x => x.ProductId).ToArray(), _contextAccessor.WorkContext.CurrentCustomer, - _contextAccessor.StoreContext.CurrentStore.Id); + var products = await _productService.GetProductsByIds(blogproducts.Select(x => x.ProductId).ToArray(), + _contextAccessor.CatalogAccess()); if (!products.Any()) return Content(""); diff --git a/src/Web/Grand.Web/Components/HomePageBestSellers.cs b/src/Web/Grand.Web/Components/HomePageBestSellers.cs index bf9de0371f..52434c9f57 100644 --- a/src/Web/Grand.Web/Components/HomePageBestSellers.cs +++ b/src/Web/Grand.Web/Components/HomePageBestSellers.cs @@ -5,6 +5,7 @@ using Grand.Infrastructure; using Grand.Infrastructure.Caching; using Grand.Web.Common.Components; +using Grand.Web.Common.Extensions; using Grand.Web.Events.Cache; using Grand.Web.Features.Models.Products; using Grand.Mediator; @@ -66,8 +67,7 @@ await _orderReportService.BestSellersReport( } //load products - var products = await _productService.GetProductsByIds(productIds.ToArray(), _contextAccessor.WorkContext.CurrentCustomer, - _contextAccessor.StoreContext.CurrentStore.Id); + var products = await _productService.GetProductsByIds(productIds.ToArray(), _contextAccessor.CatalogAccess()); if (!products.Any()) return Content(""); diff --git a/src/Web/Grand.Web/Components/HomePageProducts.cs b/src/Web/Grand.Web/Components/HomePageProducts.cs index bef60089f5..23a9e5a062 100644 --- a/src/Web/Grand.Web/Components/HomePageProducts.cs +++ b/src/Web/Grand.Web/Components/HomePageProducts.cs @@ -3,6 +3,7 @@ using Grand.Infrastructure; using Grand.Infrastructure.Caching; using Grand.Web.Common.Components; +using Grand.Web.Common.Extensions; using Grand.Web.Events.Cache; using Grand.Web.Features.Models.Products; using Grand.Mediator; @@ -37,8 +38,7 @@ public async Task InvokeAsync(int? productThumbPictureSize var productsIds = await _cacheBase.GetAsync(CacheKeyConst.HOMEPAGE_PRODUCTS_MODEL_KEY, async () => await _productService.GetAllProductsDisplayedOnHomePage()); - var products = await _productService.GetProductsByIds(productsIds.ToArray(), _contextAccessor.WorkContext.CurrentCustomer, - _contextAccessor.StoreContext.CurrentStore.Id); + var products = await _productService.GetProductsByIds(productsIds.ToArray(), _contextAccessor.CatalogAccess()); if (!products.Any()) return Content(""); diff --git a/src/Web/Grand.Web/Components/ProductsAlsoPurchased.cs b/src/Web/Grand.Web/Components/ProductsAlsoPurchased.cs index 75115dbbd1..3dd1746c37 100644 --- a/src/Web/Grand.Web/Components/ProductsAlsoPurchased.cs +++ b/src/Web/Grand.Web/Components/ProductsAlsoPurchased.cs @@ -4,6 +4,7 @@ using Grand.Infrastructure; using Grand.Infrastructure.Caching; using Grand.Web.Common.Components; +using Grand.Web.Common.Extensions; using Grand.Web.Events.Cache; using Grand.Web.Features.Models.Products; using Grand.Mediator; @@ -51,8 +52,7 @@ public async Task InvokeAsync(string productId, int? produ ); //load products - var products = await _productService.GetProductsByIds(productIds, _contextAccessor.WorkContext.CurrentCustomer, - _contextAccessor.StoreContext.CurrentStore.Id); + var products = await _productService.GetProductsByIds(productIds, _contextAccessor.CatalogAccess()); if (!products.Any()) return Content(""); diff --git a/src/Web/Grand.Web/Components/RecommendedProducts.cs b/src/Web/Grand.Web/Components/RecommendedProducts.cs index 1565e922d5..d7fc7b8ea4 100644 --- a/src/Web/Grand.Web/Components/RecommendedProducts.cs +++ b/src/Web/Grand.Web/Components/RecommendedProducts.cs @@ -2,6 +2,7 @@ using Grand.Domain.Catalog; using Grand.Infrastructure; using Grand.Web.Common.Components; +using Grand.Web.Common.Extensions; using Grand.Web.Features.Models.Products; using Grand.Mediator; using Microsoft.AspNetCore.Mvc; @@ -34,8 +35,7 @@ public async Task InvokeAsync(string productId, int? produ if (!productIds.Any()) return Content(""); - var products = await _productService.GetProductsByIds(productIds, _contextAccessor.WorkContext.CurrentCustomer, - _contextAccessor.StoreContext.CurrentStore.Id); + var products = await _productService.GetProductsByIds(productIds, _contextAccessor.CatalogAccess()); if (!products.Any()) return Content(""); diff --git a/src/Web/Grand.Web/Components/RelatedProducts.cs b/src/Web/Grand.Web/Components/RelatedProducts.cs index 22ee273035..1408cee2f6 100644 --- a/src/Web/Grand.Web/Components/RelatedProducts.cs +++ b/src/Web/Grand.Web/Components/RelatedProducts.cs @@ -2,6 +2,7 @@ using Grand.Domain.Catalog; using Grand.Infrastructure; using Grand.Web.Common.Components; +using Grand.Web.Common.Extensions; using Grand.Web.Features.Models.Products; using Grand.Mediator; using Microsoft.AspNetCore.Mvc; @@ -34,8 +35,7 @@ public async Task InvokeAsync(string productId, int? produ .Select(x => x.ProductId2).ToArray(); //load products - var products = await _productService.GetProductsByIds(productIds, _contextAccessor.WorkContext.CurrentCustomer, - _contextAccessor.StoreContext.CurrentStore.Id); + var products = await _productService.GetProductsByIds(productIds, _contextAccessor.CatalogAccess()); var model = await _mediator.Send(new GetProductOverview { PreparePictureModel = true, diff --git a/src/Web/Grand.Web/Components/SimilarProducts.cs b/src/Web/Grand.Web/Components/SimilarProducts.cs index f83729fd6d..6a3ef56da6 100644 --- a/src/Web/Grand.Web/Components/SimilarProducts.cs +++ b/src/Web/Grand.Web/Components/SimilarProducts.cs @@ -2,6 +2,7 @@ using Grand.Domain.Catalog; using Grand.Infrastructure; using Grand.Web.Common.Components; +using Grand.Web.Common.Extensions; using Grand.Web.Features.Models.Products; using Grand.Mediator; using Microsoft.AspNetCore.Mvc; @@ -35,8 +36,7 @@ public async Task InvokeAsync(string productId, int? produ if (!productIds.Any()) return Content(""); - var products = await _productService.GetProductsByIds(productIds, _contextAccessor.WorkContext.CurrentCustomer, - _contextAccessor.StoreContext.CurrentStore.Id); + var products = await _productService.GetProductsByIds(productIds, _contextAccessor.CatalogAccess()); if (!products.Any()) return Content(""); diff --git a/src/Web/Grand.Web/Controllers/ProductController.cs b/src/Web/Grand.Web/Controllers/ProductController.cs index db072f839a..bcb7801b8b 100644 --- a/src/Web/Grand.Web/Controllers/ProductController.cs +++ b/src/Web/Grand.Web/Controllers/ProductController.cs @@ -95,8 +95,7 @@ public virtual async Task RelatedProducts(string productId, int? .Select(x => x.ProductId2).ToArray(); //load products - var products = await _productService.GetProductsByIds(productIds, _contextAccessor.WorkContext.CurrentCustomer, - _contextAccessor.StoreContext.CurrentStore.Id); + var products = await _productService.GetProductsByIds(productIds, _contextAccessor.CatalogAccess()); var model = await _mediator.Send(new GetProductOverview { PreparePictureModel = true, @@ -121,8 +120,7 @@ public virtual async Task NewProducts() return Content(""); var products = (await _productService.SearchProducts( - customer: _contextAccessor.WorkContext.CurrentCustomer, - storeId: _contextAccessor.StoreContext.CurrentStore.Id, + access: _contextAccessor.CatalogAccess(), visibleIndividuallyOnly: true, markedAsNewOnly: true, orderBy: ProductSortingEnum.CreatedOn, diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetBrandAllHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetBrandAllHandler.cs index f49caf5232..de15ea1b9d 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetBrandAllHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetBrandAllHandler.cs @@ -2,6 +2,7 @@ using Grand.Business.Core.Interfaces.Catalog.Brands; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Media; using Grand.Web.Extensions; @@ -47,7 +48,8 @@ private async Task> PrepareBrands(GetBrandAll request, BrandLis request.Command.PageSize = _catalogSettings.MaxCatalogPageSize; var model = new List(); - var brands = await _brandService.GetAllBrands(brandName: "", storeId: request.Store.Id, customer: request.Customer, + var brands = await _brandService.GetAllBrands(brandName: "", + access: CatalogAccess.For(request.Customer, request.Store.Id), pageIndex: request.Command.PageNumber - 1, pageSize: request.Command.PageSize ); diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategoryAllHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategoryAllHandler.cs index 9fbe433aed..b5681ecc9d 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategoryAllHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategoryAllHandler.cs @@ -2,6 +2,7 @@ using Grand.Business.Core.Interfaces.Catalog.Categories; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Media; using Grand.Web.Extensions; @@ -48,8 +49,8 @@ private async Task> PrepareCategories(GetCategoryAll request request.Command.PageSize = _catalogSettings.MaxCatalogPageSize; var model = new List(); - var categories = await _categoryService.GetAllCategories(parentId: null, categoryName: "", storeId: request.Store.Id, - customer: request.Customer, + var categories = await _categoryService.GetAllCategories(parentId: null, categoryName: "", + access: CatalogAccess.For(request.Customer, request.Store.Id), pageIndex: request.Command.PageNumber - 1, pageSize: request.Command.PageSize ); diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategoryFeaturedProductsHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategoryFeaturedProductsHandler.cs index c80d555db9..720f40a0f6 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategoryFeaturedProductsHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategoryFeaturedProductsHandler.cs @@ -3,6 +3,7 @@ using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Storage; using Grand.Business.Core.Queries.Catalog; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain; using Grand.Domain.Catalog; using Grand.Domain.Customers; @@ -56,8 +57,8 @@ public async Task> Handle(GetCategoryFeaturedProducts reque var model = await _cacheBase.GetAsync(categoriesCacheKey, async () => { var catlistmodel = new List(); - foreach (var x in await _categoryService.GetAllCategoriesFeaturedProductsOnHomePage(customer: request.Customer, - storeId: request.Store.Id)) + foreach (var x in await _categoryService.GetAllCategoriesFeaturedProductsOnHomePage( + access: CatalogAccess.For(request.Customer, request.Store.Id))) { var catModel = x.ToModel(request.Language); //prepare picture model diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategoryHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategoryHandler.cs index 4738e46434..7fa4f846c4 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategoryHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategoryHandler.cs @@ -4,6 +4,7 @@ using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Storage; using Grand.Business.Core.Queries.Catalog; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain; using Grand.Domain.Catalog; using Grand.Domain.Customers; @@ -87,8 +88,8 @@ public async Task Handle(GetCategory request, CancellationToken c storeId, languageId); model.CategoryBreadcrumb = await _cacheBase.GetAsync(breadcrumbCacheKey, async () => - (await _categoryService.GetCategoryBreadCrumb(request.Category, customer: request.Customer, - storeId: request.Store.Id)) + (await _categoryService.GetCategoryBreadCrumb(request.Category, + access: CatalogAccess.For(request.Customer, request.Store.Id))) .Select(catBr => new CategoryModel { Id = catBr.Id, Name = catBr.GetTranslation(x => x.Name, languageId), @@ -100,8 +101,9 @@ public async Task Handle(GetCategory request, CancellationToken c //subcategories var subCategories = new List(); - foreach (var x in (await _categoryService.GetAllCategoriesByParentCategoryId(customer: request.Customer, - storeId: request.Store.Id, parentCategoryId: request.Category.Id)).Where(x => + foreach (var x in (await _categoryService.GetAllCategoriesByParentCategoryId( + access: CatalogAccess.For(request.Customer, request.Store.Id), + parentCategoryId: request.Category.Id)).Where(x => !x.HideOnCatalog)) { var subCatModel = new CategoryModel.SubCategoryModel { diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategorySimpleHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategorySimpleHandler.cs index 7c1d7c8d21..71d9c4e2d3 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategorySimpleHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetCategorySimpleHandler.cs @@ -2,6 +2,7 @@ using Grand.Business.Core.Interfaces.Catalog.Categories; using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Customers; using Grand.Domain.Media; @@ -65,8 +66,8 @@ private async Task> PrepareCategorySimpleModels(GetCat async Task PrepareCategories(string categoryId) { - var parentCategories = await _categoryService.GetAllCategories(categoryId, categoryName: "", storeId: request.Store.Id, - customer: request.Customer); + var parentCategories = await _categoryService.GetAllCategories(categoryId, categoryName: "", + access: CatalogAccess.For(request.Customer, request.Store.Id)); if (parentCategories.Any()) { categories.AddRange(parentCategories); @@ -80,8 +81,8 @@ async Task PrepareCategories(string categoryId) if (currentCategory != null) { var currentCategories = - await _categoryService.GetAllCategories(currentCategory.Id, categoryName: "", storeId: request.Store.Id, - customer: request.Customer); + await _categoryService.GetAllCategories(currentCategory.Id, categoryName: "", + access: CatalogAccess.For(request.Customer, request.Store.Id)); categories.AddRange(currentCategories); await PrepareCategories(currentCategory.ParentCategoryId); } diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetChildCategoryIdsHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetChildCategoryIdsHandler.cs index cf366788a7..f9507d152e 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetChildCategoryIdsHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetChildCategoryIdsHandler.cs @@ -1,4 +1,5 @@ using Grand.Business.Core.Interfaces.Catalog.Categories; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Customers; using Grand.Infrastructure.Caching; using Grand.Web.Events.Cache; @@ -32,8 +33,9 @@ private async Task> GetChildCategoryIds(GetChildCategoryIds request return await _cacheBase.GetAsync(cacheKey, async () => { var categoriesIds = new List(); - var categories = await _categoryService.GetAllCategoriesByParentCategoryId(customer: request.Customer, - storeId: request.Store.Id, parentCategoryId: request.ParentCategoryId); + var categories = await _categoryService.GetAllCategoriesByParentCategoryId( + access: CatalogAccess.For(request.Customer, request.Store.Id), + parentCategoryId: request.ParentCategoryId); foreach (var category in categories) { categoriesIds.Add(category.Id); diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetCollectionAllHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetCollectionAllHandler.cs index b4e08ceace..82cf7c2e3b 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetCollectionAllHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetCollectionAllHandler.cs @@ -2,6 +2,7 @@ using Grand.Business.Core.Interfaces.Catalog.Collections; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Media; using Grand.Web.Extensions; @@ -48,7 +49,8 @@ private async Task> PrepareCollectionAll(GetCollectionAll request.Command.PageSize = _catalogSettings.MaxCatalogPageSize; var model = new List(); - var collections = await _collectionService.GetAllCollections(collectionName: "", storeId: request.Store.Id, customer: request.Customer, + var collections = await _collectionService.GetAllCollections(collectionName: "", + access: CatalogAccess.For(request.Customer, request.Store.Id), pageIndex: request.Command.PageNumber - 1, pageSize: request.Command.PageSize); diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetCollectionFeaturedProductsHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetCollectionFeaturedProductsHandler.cs index 09a35cd967..64d9299dd3 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetCollectionFeaturedProductsHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetCollectionFeaturedProductsHandler.cs @@ -3,6 +3,7 @@ using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Storage; using Grand.Business.Core.Queries.Catalog; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain; using Grand.Domain.Catalog; using Grand.Domain.Customers; @@ -57,8 +58,8 @@ public async Task> Handle(GetCollectionFeaturedProducts r var model = await _cacheBase.GetAsync(collectionCacheKey, async () => { var collectionList = new List(); - var collectionmodel = await _collectionService.GetAllCollectionFeaturedProductsOnHomePage(customer: request.Customer, - storeId: request.Store.Id); + var collectionmodel = await _collectionService.GetAllCollectionFeaturedProductsOnHomePage( + access: CatalogAccess.For(request.Customer, request.Store.Id)); foreach (var x in collectionmodel) { var colModel = x.ToModel(request.Language); diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetCollectionNavigationHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetCollectionNavigationHandler.cs index 05603b31df..3b9f5fd1c6 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetCollectionNavigationHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetCollectionNavigationHandler.cs @@ -1,5 +1,6 @@ using Grand.Business.Core.Extensions; using Grand.Business.Core.Interfaces.Catalog.Collections; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Customers; using Grand.Infrastructure.Caching; @@ -36,7 +37,7 @@ public async Task Handle(GetCollectionNavigation requ var currentCollection = await _collectionService.GetCollectionById(request.CurrentCollectionId); var collections = await _collectionService.GetAllCollections(collectionName: "", pageSize: _catalogSettings.CollectionsBlockItemsToDisplay, - storeId: request.Store.Id, customer: request.Customer); + access: CatalogAccess.For(request.Customer, request.Store.Id)); var model = new CollectionNavigationModel { TotalCollections = collections.TotalCount }; diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageBrandsHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageBrandsHandler.cs index 03db61ad19..36f1c0590e 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageBrandsHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageBrandsHandler.cs @@ -1,6 +1,7 @@ using Grand.Business.Core.Interfaces.Catalog.Brands; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Media; using Grand.Infrastructure.Caching; using Grand.Web.Events.Cache; @@ -41,7 +42,8 @@ public async Task> Handle(GetHomepageBrands request, Cancellat var model = await _cacheBase.GetAsync(brandsCacheKey, async () => { var modelBrands = new List(); - var allBrands = await _brandService.GetAllBrands(brandName: "", storeId: request.Store.Id, customer: request.Customer); + var allBrands = await _brandService.GetAllBrands(brandName: "", + access: CatalogAccess.For(request.Customer, request.Store.Id)); foreach (var x in allBrands.Where(x => x.ShowOnHomePage)) { var brandModel = x.ToModel(request.Language); diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageCategoryHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageCategoryHandler.cs index 0c5d47c9a7..a4293a798e 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageCategoryHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageCategoryHandler.cs @@ -2,6 +2,7 @@ using Grand.Business.Core.Interfaces.Catalog.Categories; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Customers; using Grand.Domain.Media; using Grand.Infrastructure.Caching; @@ -46,8 +47,8 @@ public async Task> Handle(GetHomepageCategory request, Canc var model = await _cacheBase.GetAsync(categoriesCacheKey, async () => { var cat = new List(); - foreach (var x in await _categoryService.GetAllCategoriesDisplayedOnHomePage(customer: request.Customer, - storeId: request.Store.Id)) + foreach (var x in await _categoryService.GetAllCategoriesDisplayedOnHomePage( + access: CatalogAccess.For(request.Customer, request.Store.Id))) { var catModel = x.ToModel(request.Language); //prepare picture model diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageCollectionsHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageCollectionsHandler.cs index 4a76e200c6..6c78822489 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageCollectionsHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetHomepageCollectionsHandler.cs @@ -1,6 +1,7 @@ using Grand.Business.Core.Interfaces.Catalog.Collections; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Media; using Grand.Infrastructure.Caching; using Grand.Web.Events.Cache; @@ -43,7 +44,8 @@ public async Task> Handle(GetHomepageCollections request, var model = await _cacheBase.GetAsync(collectionsCacheKey, async () => { var modelCollect = new List(); - var allcollections = await _collectionService.GetAllCollections(collectionName: "", storeId: request.Store.Id, customer: request.Customer); + var allcollections = await _collectionService.GetAllCollections(collectionName: "", + access: CatalogAccess.For(request.Customer, request.Store.Id)); foreach (var x in allcollections.Where(x => x.ShowOnHomePage)) { var _model = x.ToModel(request.Language); diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetMenuHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetMenuHandler.cs index 5e7024cb90..09bb4ac65f 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetMenuHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetMenuHandler.cs @@ -4,6 +4,7 @@ using Grand.Business.Core.Interfaces.Catalog.Collections; using Grand.Business.Core.Interfaces.Cms; using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Blogs; using Grand.Domain.Catalog; using Grand.Domain.Common; @@ -80,7 +81,8 @@ public async Task Handle(GetMenu request, CancellationToken cancellat request.Language.Id, request.Store.Id); var cachedBrandModel = await _cacheBase.GetAsync(brandCacheKey, async () => - (await _brandService.GetAllBrands(brandName: "", storeId: request.Store.Id, customer: request.Customer)) + (await _brandService.GetAllBrands(brandName: "", + access: CatalogAccess.For(request.Customer, request.Store.Id))) .Where(x => x.IncludeInMenu) .Select(t => new MenuModel.MenuBrandModel { Id = t.Id, @@ -95,7 +97,8 @@ public async Task Handle(GetMenu request, CancellationToken cancellat request.Language.Id, request.Store.Id); var cachedCollectionModel = await _cacheBase.GetAsync(collectionCacheKey, async () => - (await _collectionService.GetAllCollections(collectionName: "", storeId: request.Store.Id, customer: request.Customer)) + (await _collectionService.GetAllCollections(collectionName: "", + access: CatalogAccess.For(request.Customer, request.Store.Id))) .Where(x => x.IncludeInMenu) .Select(t => new MenuModel.MenuCollectionModel { Id = t.Id, diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetSearchHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetSearchHandler.cs index 934c40a487..6639be3b6b 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetSearchHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetSearchHandler.cs @@ -7,6 +7,7 @@ using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Customers; using Grand.Business.Core.Queries.Catalog; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Customers; @@ -99,14 +100,14 @@ public async Task Handle(GetSearch request, CancellationToken cance { var categoriesModel = new List(); //all categories - var allCategories = await _categoryService.GetAllCategories(parentId: null, categoryName: "", storeId: request.Store.Id, - customer: request.Customer, pageSize: 100); + var allCategories = await _categoryService.GetAllCategories(parentId: null, categoryName: "", + access: CatalogAccess.For(request.Customer, request.Store.Id), pageSize: 100); foreach (var c in allCategories) { //generate full category name (breadcrumb) var categoryBreadcrumb = ""; - var breadcrumb = _categoryService.GetCategoryBreadCrumb(c, allCategories, customer: request.Customer, - storeId: request.Store.Id); + var breadcrumb = _categoryService.GetCategoryBreadCrumb(c, allCategories, + access: CatalogAccess.For(request.Customer, request.Store.Id)); for (var i = 0; i <= breadcrumb.Count - 1; i++) { categoryBreadcrumb += breadcrumb[i].GetTranslation(x => x.Name, request.Language.Id); @@ -138,7 +139,8 @@ public async Task Handle(GetSearch request, CancellationToken cance }); } - var collections = await _collectionService.GetAllCollections(collectionName: "", storeId: request.Store.Id, customer: request.Customer, + var collections = await _collectionService.GetAllCollections(collectionName: "", + access: CatalogAccess.For(request.Customer, request.Store.Id), pageSize: 100); if (collections.Any()) { diff --git a/src/Web/Grand.Web/Features/Handlers/Common/GetSitemapHandler.cs b/src/Web/Grand.Web/Features/Handlers/Common/GetSitemapHandler.cs index b108f24818..e85959c073 100644 --- a/src/Web/Grand.Web/Features/Handlers/Common/GetSitemapHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Common/GetSitemapHandler.cs @@ -3,6 +3,7 @@ using Grand.Business.Core.Interfaces.Catalog.Categories; using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Cms; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Blogs; using Grand.Domain.Common; using Grand.Domain.Customers; @@ -78,15 +79,16 @@ public async Task Handle(GetSitemap request, CancellationToken can //categories if (_commonSettings.SitemapIncludeCategories) { - var categories = await _categoryService.GetAllCategories(parentId: null, categoryName: "", storeId: request.Store.Id, - customer: request.Customer); + var categories = await _categoryService.GetAllCategories(parentId: null, categoryName: "", + access: CatalogAccess.For(request.Customer, request.Store.Id)); model.Categories = categories.Select(x => x.ToModel(request.Language)).ToList(); } //collections if (_commonSettings.SitemapIncludeBrands) { - var brands = await _brandService.GetAllBrands(brandName: "", storeId: request.Store.Id, customer: request.Customer); + var brands = await _brandService.GetAllBrands(brandName: "", + access: CatalogAccess.For(request.Customer, request.Store.Id)); model.Brands = brands.Select(x => x.ToModel(request.Language)).ToList(); } @@ -95,8 +97,7 @@ public async Task Handle(GetSitemap request, CancellationToken can { //limit product to 200 until paging is supported on this page var products = (await _productService.SearchProducts( - customer: request.Customer, - storeId: request.Store.Id, + access: CatalogAccess.For(request.Customer, request.Store.Id), visibleIndividuallyOnly: true, pageSize: 200)).products; model.Products = products.Select(product => new ProductOverviewModel { diff --git a/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsPageHandler.cs b/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsPageHandler.cs index c678397a0b..7414d62688 100644 --- a/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsPageHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsPageHandler.cs @@ -23,6 +23,7 @@ using Grand.Infrastructure; using Grand.Infrastructure.Caching; using Grand.Web.Events.Cache; +using Grand.Web.Common.Extensions; using Grand.Web.Extensions; using Grand.Web.Features.Models.Catalog; using Grand.Web.Features.Models.Products; @@ -312,8 +313,7 @@ or ManageInventoryMethod.ManageStockByAttributes && if (!isAssociatedProduct) { var associatedProducts = - await _productService.GetAssociatedProducts(product.Id, _contextAccessor.WorkContext.CurrentCustomer, - _contextAccessor.StoreContext.CurrentStore.Id); + await _productService.GetAssociatedProducts(product.Id, _contextAccessor.CatalogAccess()); foreach (var associatedProduct in associatedProducts) model.AssociatedProducts.Add( await PrepareProductDetailsModel(store, associatedProduct, null, true)); @@ -551,8 +551,7 @@ private async Task PrepareVendorBriefInfoModel(Product pro if (category == null) return breadcrumbModel; foreach (var catBr in await _categoryService.GetCategoryBreadCrumb(category, - customer: _contextAccessor.WorkContext.CurrentCustomer, - storeId: _contextAccessor.StoreContext.CurrentStore.Id)) + _contextAccessor.CatalogAccess())) breadcrumbModel.CategoryBreadcrumb.Add(new CategorySimpleModel { Id = catBr.Id, Name = catBr.GetTranslation(x => x.Name, _contextAccessor.WorkContext.WorkingLanguage.Id), diff --git a/src/Web/Grand.Web/Features/Handlers/Products/GetProductOverviewHandler.cs b/src/Web/Grand.Web/Features/Handlers/Products/GetProductOverviewHandler.cs index 71c21d0ab5..cd8e484e7b 100644 --- a/src/Web/Grand.Web/Features/Handlers/Products/GetProductOverviewHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Products/GetProductOverviewHandler.cs @@ -12,6 +12,7 @@ using Grand.Domain.Orders; using Grand.Domain.Tax; using Grand.Infrastructure; +using Grand.Web.Common.Extensions; using Grand.Web.Features.Models.Catalog; using Grand.Web.Features.Models.Products; using Grand.Web.Models.Catalog; @@ -225,8 +226,7 @@ private async Task PrepareProductOverviewModel(Product pro #region Grouped product var associatedProducts = - await _productService.GetAssociatedProducts(product.Id, _contextAccessor.WorkContext.CurrentCustomer, - _contextAccessor.StoreContext.CurrentStore.Id); + await _productService.GetAssociatedProducts(product.Id, _contextAccessor.CatalogAccess()); //add to cart button (ignore "DisableBuyButton" property for grouped products) priceModel.DisableBuyButton = !enableShoppingCart || !displayPrices; From 33a59f61f8d8afb92b29f7a3ac3e9ef9022809c1 Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 21:50:24 +0200 Subject: [PATCH 33/33] Pass a CatalogAccess to the checkout attribute query GetAllCheckoutAttributes took a store id, a nullable customer and an ignoreAcl flag that had to agree with each other. It now takes the CatalogAccess the catalog read services use: CatalogAccess.For(customer, storeId) on the storefront, CatalogAccess.IncludingHidden(storeId) in the admin panel. A missing customer can no longer reach the group filter. --- .../CheckoutAttributeService.cs | 46 ++++++++----------- ...ShoppingCartCheckoutAttributesValidator.cs | 3 +- .../ICheckoutAttributeService.cs | 10 ++-- .../Caching/Constants/OrdersCacheKeys.cs | 4 +- .../CheckoutAttributeServiceTests.cs | 30 ++++++++++-- .../Validators/ShoppingCartValidatorsTests.cs | 5 +- .../CheckoutAttributeViewModelServiceTests.cs | 4 +- .../CheckoutAttributeViewModelService.cs | 5 +- .../SaveCheckoutAttributesCommandHandler.cs | 3 +- .../Controllers/ShoppingCartController.cs | 3 +- .../GetMiniShoppingCartHandler.cs | 3 +- .../ShoppingCart/GetShoppingCartHandler.cs | 3 +- 12 files changed, 67 insertions(+), 52 deletions(-) diff --git a/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeService.cs b/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeService.cs index 8190329e68..9d9a98047c 100644 --- a/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeService.cs +++ b/src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeService.cs @@ -1,4 +1,5 @@ using Grand.Business.Core.Interfaces.Checkout.CheckoutAttributes; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Domain.Customers; using Grand.Domain.Orders; @@ -48,20 +49,18 @@ public CheckoutAttributeService( /// /// Gets all checkout attributes /// - /// Store identifier - /// Customer whose groups limit the result (not read when ignoreAcl or AccessControlConfig.IgnoreAcl is set) + /// Customer whose groups limit the result and the store, or every attribute of the store /// A value indicating whether we should exclude shippable attributes - /// /// Checkout attributes - public virtual async Task> GetAllCheckoutAttributes(string storeId, Customer customer, - bool excludeShippableAttributes = false, bool ignoreAcl = false) + public virtual async Task> GetAllCheckoutAttributes(CatalogAccess access, + bool excludeShippableAttributes = false) { - var applyAcl = !ignoreAcl && !_accessControlConfig.IgnoreAcl; - if (applyAcl) - ArgumentNullException.ThrowIfNull(customer); + ArgumentNullException.ThrowIfNull(access); - var key = string.Format(CacheKey.CHECKOUTATTRIBUTES_ALL_KEY, storeId, excludeShippableAttributes, ignoreAcl, - applyAcl ? string.Join(",", customer.GetCustomerGroupIds()) : string.Empty); + var applyAcl = !access.ShowHidden && !_accessControlConfig.IgnoreAcl; + var allowedCustomerGroupsIds = applyAcl ? access.Customer.GetCustomerGroupIds() : []; + var key = string.Format(CacheKey.CHECKOUTATTRIBUTES_ALL_KEY, access.StoreId, excludeShippableAttributes, + access.ShowHidden, string.Join(",", allowedCustomerGroupsIds)); return await _cacheBase.GetAsync(key, async () => { var query = from p in _checkoutAttributeRepository.Table @@ -69,23 +68,16 @@ public virtual async Task> GetAllCheckoutAttributes(str query = query.OrderBy(c => c.DisplayOrder); - if ((!string.IsNullOrEmpty(storeId) && !_accessControlConfig.IgnoreStoreLimitations) || - (!ignoreAcl && !_accessControlConfig.IgnoreAcl)) - { - if (!ignoreAcl && !_accessControlConfig.IgnoreAcl) - { - var allowedCustomerGroupsIds = customer.GetCustomerGroupIds(); - query = from p in query - where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x)) - select p; - } - - //Store acl - if (!string.IsNullOrEmpty(storeId) && !_accessControlConfig.IgnoreStoreLimitations) - query = from p in query - where !p.LimitedToStores || p.Stores.Contains(storeId) - select p; - } + if (applyAcl) + query = from p in query + where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x)) + select p; + + //Store acl + if (!string.IsNullOrEmpty(access.StoreId) && !_accessControlConfig.IgnoreStoreLimitations) + query = from p in query + where !p.LimitedToStores || p.Stores.Contains(access.StoreId) + select p; if (excludeShippableAttributes) query = query.Where(x => !x.ShippableProductRequired); return await _checkoutAttributeRepository.ToListAsync(query); diff --git a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartCheckoutAttributesValidator.cs b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartCheckoutAttributesValidator.cs index fa358049b0..8ff102e60a 100644 --- a/src/Business/Grand.Business.Checkout/Validators/ShoppingCartCheckoutAttributesValidator.cs +++ b/src/Business/Grand.Business.Checkout/Validators/ShoppingCartCheckoutAttributesValidator.cs @@ -1,6 +1,7 @@ using FluentValidation; using Grand.Business.Core.Interfaces.Checkout.CheckoutAttributes; using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Customers; @@ -27,7 +28,7 @@ public ShoppingCartCheckoutAttributesValidator(ITranslationService translationSe //existing checkout attributes var attributes2 = - await checkoutAttributeService.GetAllCheckoutAttributes(value.Store.Id, value.Customer, + await checkoutAttributeService.GetAllCheckoutAttributes(CatalogAccess.For(value.Customer, value.Store.Id), !value.ShoppingCarts.RequiresShipping()); foreach (var a2 in attributes2) { diff --git a/src/Business/Grand.Business.Core/Interfaces/Checkout/CheckoutAttributes/ICheckoutAttributeService.cs b/src/Business/Grand.Business.Core/Interfaces/Checkout/CheckoutAttributes/ICheckoutAttributeService.cs index 23c4814b2d..abf28f5e54 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Checkout/CheckoutAttributes/ICheckoutAttributeService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Checkout/CheckoutAttributes/ICheckoutAttributeService.cs @@ -1,4 +1,4 @@ -using Grand.Domain.Customers; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Orders; namespace Grand.Business.Core.Interfaces.Checkout.CheckoutAttributes; @@ -13,13 +13,11 @@ public interface ICheckoutAttributeService /// /// Gets all checkout attributes /// - /// Store identifier - /// Customer whose groups limit the result (not read when ignoreAcl or AccessControlConfig.IgnoreAcl is set) + /// Customer whose groups limit the result and the store, or every attribute of the store /// A value indicating whether we should exclude shippable attributes - /// /// Checkout attributes - Task> GetAllCheckoutAttributes(string storeId, Customer customer, - bool excludeShippableAttributes = false, bool ignoreAcl = false); + Task> GetAllCheckoutAttributes(CatalogAccess access, + bool excludeShippableAttributes = false); /// /// Gets a checkout attribute diff --git a/src/Core/Grand.Infrastructure/Caching/Constants/OrdersCacheKeys.cs b/src/Core/Grand.Infrastructure/Caching/Constants/OrdersCacheKeys.cs index 62fdacac9d..5376c984a7 100644 --- a/src/Core/Grand.Infrastructure/Caching/Constants/OrdersCacheKeys.cs +++ b/src/Core/Grand.Infrastructure/Caching/Constants/OrdersCacheKeys.cs @@ -19,8 +19,8 @@ public static partial class CacheKey /// /// {0} : store ID /// {1} : >A value indicating whether we should exclude shippable attributes - /// {2} : ignore ACL? - /// {3} : customer group ids (empty when ACL is ignored) + /// {2} : show hidden (group-limited) attributes? + /// {3} : customer group ids (empty when ACL does not apply) /// public static string CHECKOUTATTRIBUTES_ALL_KEY => "Grand.checkoutattribute.all-{0}-{1}-{2}-{3}"; diff --git a/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeServiceTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeServiceTests.cs index c3af21c7fa..573c44188e 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeServiceTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Services/CheckoutAttributes/CheckoutAttributeServiceTests.cs @@ -1,5 +1,6 @@ using Grand.Business.Checkout.Services.CheckoutAttributes; using Grand.Business.Core.Interfaces.Checkout.CheckoutAttributes; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Data.Tests.MongoDb; using Grand.Domain.Customers; @@ -77,10 +78,29 @@ public void DeleteCheckoutAttribute_NullArgument_ThrowException() } [TestMethod] - public async Task GetAllCheckoutAttributes_WithoutCustomerWhenAclApplies_ThrowException() + public async Task GetAllCheckoutAttributes_WithoutAccess_ThrowException() { await Assert.ThrowsExactlyAsync(async () => - await _service.GetAllCheckoutAttributes("store", null)); + await _service.GetAllCheckoutAttributes(null)); + } + + [TestMethod] + public async Task GetAllCheckoutAttributes_IncludingHidden_ReturnsGroupLimitedAttributes() + { + //Arrange + var repository = new MongoDBRepositoryTest(); + await repository.InsertAsync(new CheckoutAttribute { Name = "members only", LimitedToGroups = true, CustomerGroups = ["members"] }); + await repository.InsertAsync(new CheckoutAttribute { Name = "everyone" }); + var cache = new Mock(); + cache.Setup(c => c.GetAsync(It.IsAny(), It.IsAny>>>())) + .Returns((string _, Func>> acquire) => acquire()); + var service = new CheckoutAttributeService(cache.Object, repository, _mediatorMock.Object, new AccessControlConfig()); + + //Act + var result = await service.GetAllCheckoutAttributes(CatalogAccess.IncludingHidden()); + + //Assert + Assert.AreEqual(2, result.Count); } [TestMethod] @@ -98,7 +118,7 @@ public async Task GetAllCheckoutAttributes_FiltersByTheGroupsOfTheGivenCustomer( guest.Groups.Add("guests"); //Act - var result = await service.GetAllCheckoutAttributes("", guest); + var result = await service.GetAllCheckoutAttributes(CatalogAccess.For(guest, "")); //Assert Assert.AreEqual(1, result.Count); @@ -119,8 +139,8 @@ public async Task GetAllCheckoutAttributes_CacheKeyDependsOnTheCustomerGroups() guest.Groups.Add("guests"); //Act - await _service.GetAllCheckoutAttributes("store", member); - await _service.GetAllCheckoutAttributes("store", guest); + await _service.GetAllCheckoutAttributes(CatalogAccess.For(member, "store")); + await _service.GetAllCheckoutAttributes(CatalogAccess.For(guest, "store")); //Assert Assert.AreNotEqual(keys[0], keys[1]); diff --git a/src/Tests/Grand.Business.Checkout.Tests/Validators/ShoppingCartValidatorsTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Validators/ShoppingCartValidatorsTests.cs index 6e993b70a3..259ffd2ffe 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Validators/ShoppingCartValidatorsTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Validators/ShoppingCartValidatorsTests.cs @@ -4,6 +4,7 @@ using Grand.Business.Core.Interfaces.Checkout.CheckoutAttributes; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Common.Security; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain; using Grand.Domain.Catalog; using Grand.Domain.Common; @@ -123,7 +124,7 @@ public async Task ShoppingCartCheckoutAttributesValidator_Success() } })); - _checkoutAttributeServiceMock.Setup(x => x.GetAllCheckoutAttributes(It.IsAny(), It.IsAny(), true, false)) + _checkoutAttributeServiceMock.Setup(x => x.GetAllCheckoutAttributes(It.Is(a => !a.ShowHidden), true)) .Returns(() => Task.FromResult((IList)new List { new() { Id = "1", Name = "test", IsRequired = true, AttributeControlTypeId = AttributeControlType.TextBox @@ -162,7 +163,7 @@ public async Task ShoppingCartCheckoutAttributesValidator_Fail() } })); - _checkoutAttributeServiceMock.Setup(x => x.GetAllCheckoutAttributes(It.IsAny(), It.IsAny(), true, false)) + _checkoutAttributeServiceMock.Setup(x => x.GetAllCheckoutAttributes(It.Is(a => !a.ShowHidden), true)) .Returns(() => Task.FromResult((IList)new List { new() { Id = "1", Name = "test", IsRequired = true, AttributeControlTypeId = AttributeControlType.TextBox, diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/CheckoutAttributeViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/CheckoutAttributeViewModelServiceTests.cs index c37b2643a4..16564e2f88 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Services/CheckoutAttributeViewModelServiceTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Services/CheckoutAttributeViewModelServiceTests.cs @@ -3,9 +3,9 @@ using Grand.Business.Core.Interfaces.Checkout.CheckoutAttributes; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Common; -using Grand.Domain.Customers; using Grand.Domain.Directory; using Grand.Domain.Orders; using Grand.Web.AdminShared.Models.Orders; @@ -48,7 +48,7 @@ public async Task PrepareConditionAttributes_OffersAttributesLimitedToCustomerGr AttributeControlTypeId = AttributeControlType.DropdownList }; _checkoutAttributeServiceMock - .Setup(x => x.GetAllCheckoutAttributes(It.IsAny(), It.IsAny(), false, true)) + .Setup(x => x.GetAllCheckoutAttributes(It.Is(a => a.ShowHidden), false)) .ReturnsAsync(new List { limited }); var model = new CheckoutAttributeModel(); diff --git a/src/Web/Grand.Web.AdminShared/Services/CheckoutAttributeViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CheckoutAttributeViewModelService.cs index e92b6d5238..33753db972 100644 --- a/src/Web/Grand.Web.AdminShared/Services/CheckoutAttributeViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/CheckoutAttributeViewModelService.cs @@ -3,6 +3,7 @@ using Grand.Business.Core.Interfaces.Checkout.CheckoutAttributes; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Directory; @@ -29,7 +30,7 @@ public class CheckoutAttributeViewModelService( { public virtual async Task> PrepareCheckoutAttributeListModel(string storeId = "") { - var checkoutAttributes = await checkoutAttributeService.GetAllCheckoutAttributes(storeId: storeId, customer: null, ignoreAcl: true); + var checkoutAttributes = await checkoutAttributeService.GetAllCheckoutAttributes(CatalogAccess.IncludingHidden(storeId)); return checkoutAttributes.Select((Func)(x => { var attributeModel = x.ToModel(); @@ -170,7 +171,7 @@ public virtual async Task PrepareConditionAttributes(CheckoutAttributeModel mode model.ConditionModel = new ConditionModel { EnableCondition = checkoutAttribute.ConditionAttribute.Any(), SelectedAttributeId = selectedAttribute != null ? selectedAttribute.Id : "", - ConditionAttributes = (await checkoutAttributeService.GetAllCheckoutAttributes(storeId: "", customer: null, ignoreAcl: true)) + ConditionAttributes = (await checkoutAttributeService.GetAllCheckoutAttributes(CatalogAccess.IncludingHidden())) //ignore this attribute and non-combinable attributes .Where(x => x.Id != checkoutAttribute.Id && x.CanBeUsedAsCondition()) .Select(x => diff --git a/src/Web/Grand.Web/Commands/Handler/ShoppingCart/SaveCheckoutAttributesCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/ShoppingCart/SaveCheckoutAttributesCommandHandler.cs index 9e040f4117..c380626aaa 100644 --- a/src/Web/Grand.Web/Commands/Handler/ShoppingCart/SaveCheckoutAttributesCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/ShoppingCart/SaveCheckoutAttributesCommandHandler.cs @@ -1,5 +1,6 @@ using Grand.Business.Core.Interfaces.Checkout.CheckoutAttributes; using Grand.Business.Core.Interfaces.Customers; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Customers; @@ -32,7 +33,7 @@ public async Task> Handle(SaveCheckoutAttributesCommand r var customAttributes = new List(); var checkoutAttributes = - await _checkoutAttributeService.GetAllCheckoutAttributes(request.Store.Id, request.Customer, + await _checkoutAttributeService.GetAllCheckoutAttributes(CatalogAccess.For(request.Customer, request.Store.Id), !request.Cart.RequiresShipping()); foreach (var attribute in checkoutAttributes) switch (attribute.AttributeControlTypeId) diff --git a/src/Web/Grand.Web/Controllers/ShoppingCartController.cs b/src/Web/Grand.Web/Controllers/ShoppingCartController.cs index 7328aa6f95..c4d7369fbc 100644 --- a/src/Web/Grand.Web/Controllers/ShoppingCartController.cs +++ b/src/Web/Grand.Web/Controllers/ShoppingCartController.cs @@ -140,8 +140,7 @@ public virtual async Task CheckoutAttributeChange(CheckoutAttribu var enabledAttributeIds = new List(); var disabledAttributeIds = new List(); var attributes = - await _checkoutAttributeService.GetAllCheckoutAttributes(_contextAccessor.StoreContext.CurrentStore.Id, - _contextAccessor.WorkContext.CurrentCustomer, !cart.RequiresShipping()); + await _checkoutAttributeService.GetAllCheckoutAttributes(_contextAccessor.CatalogAccess(), !cart.RequiresShipping()); foreach (var attribute in attributes) { var conditionMet = await checkoutAttributeParser.IsConditionMet(attribute, checkoutAttributes); diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs index 655fcbe81f..40fb640ec8 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs @@ -8,6 +8,7 @@ using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Catalog; using Grand.Business.Core.Utilities.Checkout; using Grand.Domain.Catalog; using Grand.Domain.Common; @@ -121,7 +122,7 @@ public async Task Handle(GetMiniShoppingCart request, var requiresShipping = cart.RequiresShipping(); var checkoutAttributesExist = - (await _checkoutAttributeService.GetAllCheckoutAttributes(request.Store.Id, request.Customer, !requiresShipping)) + (await _checkoutAttributeService.GetAllCheckoutAttributes(CatalogAccess.For(request.Customer, request.Store.Id), !requiresShipping)) .Any(); var minOrderSubtotalAmountOk = await _mediator.Send(new ValidateMinShoppingCartSubtotalAmountCommand { diff --git a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs index 9f574e6418..3d1799d183 100644 --- a/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs @@ -12,6 +12,7 @@ using Grand.Business.Core.Interfaces.Common.Security; using Grand.Business.Core.Interfaces.Customers; using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Catalog; using Grand.Business.Core.Utilities.Checkout; using Grand.Domain.Permissions; using Grand.Domain.Catalog; @@ -221,7 +222,7 @@ private async Task PrepareCheckoutAttributes(ShoppingCartModel model, GetShoppin #region Checkout attributes var checkoutAttributes = - await _checkoutAttributeService.GetAllCheckoutAttributes(request.Store.Id, request.Customer, + await _checkoutAttributeService.GetAllCheckoutAttributes(CatalogAccess.For(request.Customer, request.Store.Id), !request.Cart.RequiresShipping()); foreach (var attribute in checkoutAttributes) {