From 2f4c39de63147dfefba85df3bb406dafb0d4dc99 Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 08:14:40 +0200 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 10d8f731c96730656e81ad39e21f9c6309de51b9 Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 16:36:10 +0200 Subject: [PATCH 7/9] 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 8/9] 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 b4914b430d94b98cdb769d752083d8f24d3fc479 Mon Sep 17 00:00:00 2001 From: support Date: Fri, 16 Oct 2026 21:19:40 +0200 Subject: [PATCH 9/9] 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;