diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductAssociatedControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductAssociatedControllerTests.cs index 911787717..ff0f47de0 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductAssociatedControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductAssociatedControllerTests.cs @@ -23,14 +23,16 @@ public class BaseProductAssociatedControllerTests : ProductControllerTestsBase scope) - : BaseProductAssociatedController(productViewModelService, productService, translationService, scope); + : BaseProductAssociatedController(productAddPopupViewModelService, productAssociatedViewModelService, productService, translationService, scope); protected override TestProductAssociatedController CreateController() => new( - _productViewModelServiceMock.Object, + _productAddPopupViewModelServiceMock.Object, + _productAssociatedViewModelServiceMock.Object, _productServiceMock.Object, _translationServiceMock.Object, _scopeMock.Object); @@ -134,7 +136,7 @@ public async Task AssociatedProductDelete_ScopeDeniesAccess_ReturnsErrorJson_Doe Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify(s => s.DeleteAssociatedProduct(It.IsAny()), Times.Never); + _productAssociatedViewModelServiceMock.Verify(s => s.DeleteAssociatedProduct(It.IsAny()), Times.Never); } [TestMethod] @@ -148,7 +150,7 @@ public async Task AssociatedProductDelete_ScopeGrantsAccess_ValidModel_Deletes() var result = await _controller.AssociatedProductDelete(model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.DeleteAssociatedProduct(product), Times.Once); + _productAssociatedViewModelServiceMock.Verify(s => s.DeleteAssociatedProduct(product), Times.Once); } // --- AssociatedProductAddPopup (GET) ------------------------------------------------------------- @@ -157,7 +159,7 @@ public async Task AssociatedProductDelete_ScopeGrantsAccess_ValidModel_Deletes() public async Task AssociatedProductAddPopupGet_CallsPrepareAssociatedProductModel() { // PrepareAssociatedProductModel() now reads scope internally (Task 9). - _productViewModelServiceMock.Setup(s => s.PrepareAssociatedProductModel()) + _productAddPopupViewModelServiceMock.Setup(s => s.PrepareAssociatedProductModel()) .ReturnsAsync(new ProductModel.AddAssociatedProductModel()); var result = await _controller.AssociatedProductAddPopup("p1") as ViewResult; @@ -166,7 +168,7 @@ public async Task AssociatedProductAddPopupGet_CallsPrepareAssociatedProductMode var model = result.Model as ProductModel.AddAssociatedProductModel; Assert.IsNotNull(model); Assert.AreEqual("p1", model.ProductId); - _productViewModelServiceMock.Verify(s => s.PrepareAssociatedProductModel(), Times.Once); + _productAddPopupViewModelServiceMock.Verify(s => s.PrepareAssociatedProductModel(), Times.Once); } // --- AssociatedProductAddPopupList --------------------------------------------------------------- @@ -175,7 +177,7 @@ public async Task AssociatedProductAddPopupGet_CallsPrepareAssociatedProductMode public async Task AssociatedProductAddPopupList_UsesScopeDefaultStoreId() { _scopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - _productViewModelServiceMock + _productAddPopupViewModelServiceMock .Setup(s => s.PrepareProductModel(It.IsAny(), 0, 10)) .ReturnsAsync((new List(), 0)); @@ -191,7 +193,7 @@ public async Task AssociatedProductAddPopupList_UsesScopeDefaultStoreId() public async Task AssociatedProductAddPopupList_NoDefaultStoreId_DoesNotOverrideModelSearchStoreId() { _scopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - _productViewModelServiceMock + _productAddPopupViewModelServiceMock .Setup(s => s.PrepareProductModel(It.IsAny(), 0, 10)) .ReturnsAsync((new List(), 0)); @@ -227,7 +229,7 @@ public async Task AssociatedProductAddPopupPost_ScopeDeniesAccessToParent_Return Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify(s => s.InsertAssociatedProductModel(It.IsAny()), Times.Never); + _productAssociatedViewModelServiceMock.Verify(s => s.InsertAssociatedProductModel(It.IsAny()), Times.Never); } [TestMethod] @@ -247,7 +249,7 @@ public async Task AssociatedProductAddPopupPost_ScopeGrantsAccess_AllSelectedIds Assert.IsNotNull(content); Assert.AreEqual("", content.Content); CollectionAssert.AreEqual(new[] { "a1" }, model.SelectedProductIds); - _productViewModelServiceMock.Verify(s => s.InsertAssociatedProductModel(model), Times.Once); + _productAssociatedViewModelServiceMock.Verify(s => s.InsertAssociatedProductModel(model), Times.Once); } [TestMethod] @@ -268,7 +270,7 @@ public async Task AssociatedProductAddPopupPost_ScopeGrantsAccessToParent_Filter Assert.IsInstanceOfType(result); CollectionAssert.AreEqual(new[] { "a1" }, model.SelectedProductIds); - _productViewModelServiceMock.Verify(s => s.InsertAssociatedProductModel(model), Times.Once); + _productAssociatedViewModelServiceMock.Verify(s => s.InsertAssociatedProductModel(model), Times.Once); } [TestMethod] @@ -285,7 +287,7 @@ public async Task AssociatedProductAddPopupPost_AllSelectedIdsDenied_DoesNotInse var result = await _controller.AssociatedProductAddPopup(model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.InsertAssociatedProductModel(It.IsAny()), Times.Never); + _productAssociatedViewModelServiceMock.Verify(s => s.InsertAssociatedProductModel(It.IsAny()), Times.Never); } [TestMethod] @@ -295,7 +297,7 @@ public async Task AssociatedProductAddPopupPost_ScopeGrantsAccess_InvalidModel_R _productServiceMock.Setup(p => p.GetProductById("p1", false)).ReturnsAsync(parent); _scopeMock.Setup(s => s.HasAccess(parent)).ReturnsAsync(true); var reprepared = new ProductModel.AddAssociatedProductModel(); - _productViewModelServiceMock.Setup(s => s.PrepareAssociatedProductModel()).ReturnsAsync(reprepared); + _productAddPopupViewModelServiceMock.Setup(s => s.PrepareAssociatedProductModel()).ReturnsAsync(reprepared); var model = new ProductModel.AddAssociatedProductModel { ProductId = "p1" }; _controller.ModelState.AddModelError("x", "error"); @@ -304,6 +306,6 @@ public async Task AssociatedProductAddPopupPost_ScopeGrantsAccess_InvalidModel_R var view = result as ViewResult; Assert.IsNotNull(view); Assert.AreSame(reprepared, view.Model); - _productViewModelServiceMock.Verify(s => s.InsertAssociatedProductModel(It.IsAny()), Times.Never); + _productAssociatedViewModelServiceMock.Verify(s => s.InsertAssociatedProductModel(It.IsAny()), Times.Never); } } diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductAttributeCombinationControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductAttributeCombinationControllerTests.cs index 1addc05ff..11e0b6b53 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductAttributeCombinationControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductAttributeCombinationControllerTests.cs @@ -22,15 +22,15 @@ public class BaseProductAttributeCombinationControllerTests : ProductControllerT // BaseProductAttributeCombinationController is abstract. This minimal subclass exists only so the actions under test // can be invoked directly. public class TestProductAttributeCombinationController( - IProductViewModelService productViewModelService, + IProductAttributeCombinationViewModelService productAttributeCombinationViewModelService, IProductService productService, IInventoryManageService inventoryManageService, IRequestTranslationService translationService, IAdminDataScope scope) - : BaseProductAttributeCombinationController(productViewModelService, productService, inventoryManageService, translationService, scope); + : BaseProductAttributeCombinationController(productAttributeCombinationViewModelService, productService, inventoryManageService, translationService, scope); protected override TestProductAttributeCombinationController CreateController() => new( - _productViewModelServiceMock.Object, + _productAttributeCombinationViewModelServiceMock.Object, _productServiceMock.Object, _inventoryManageServiceMock.Object, _translationServiceMock.Object, @@ -49,7 +49,7 @@ public async Task ProductAttributeCombinationList_ScopeDeniesAccess_ReturnsError Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify( + _productAttributeCombinationViewModelServiceMock.Verify( s => s.PrepareProductAttributeCombinationModel(It.IsAny()), Times.Never); } @@ -59,7 +59,7 @@ public async Task ProductAttributeCombinationList_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); - _productViewModelServiceMock.Setup(s => s.PrepareProductAttributeCombinationModel(product)) + _productAttributeCombinationViewModelServiceMock.Setup(s => s.PrepareProductAttributeCombinationModel(product)) .ReturnsAsync(new List { new() }); var result = await _controller.ProductAttributeCombinationList(new DataSourceRequest(), "p1"); @@ -142,7 +142,7 @@ public async Task AttributeCombinationPopupGet_ScopeDeniesAccess_ReturnsPermissi var content = result as ContentResult; Assert.IsNotNull(content); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify( + _productAttributeCombinationViewModelServiceMock.Verify( s => s.PrepareProductAttributeCombinationModel(It.IsAny(), It.IsAny()), Times.Never); } @@ -153,7 +153,7 @@ public async Task AttributeCombinationPopupGet_ScopeGrantsAccess_ReturnsView() _productServiceMock.Setup(p => p.GetProductById("p1", false)).ReturnsAsync(product); _scopeMock.Setup(s => s.HasAccess(product)).ReturnsAsync(true); var model = new ProductAttributeCombinationModel { Id = "c1" }; - _productViewModelServiceMock.Setup(s => s.PrepareProductAttributeCombinationModel(product, "c1")) + _productAttributeCombinationViewModelServiceMock.Setup(s => s.PrepareProductAttributeCombinationModel(product, "c1")) .ReturnsAsync(model); var result = await _controller.AttributeCombinationPopup("p1", "c1"); @@ -161,7 +161,7 @@ public async Task AttributeCombinationPopupGet_ScopeGrantsAccess_ReturnsView() var view = result as ViewResult; Assert.IsNotNull(view); Assert.AreSame(model, view.Model); - _productViewModelServiceMock.Verify(s => s.PrepareAddProductAttributeCombinationModel(model, product), + _productAttributeCombinationViewModelServiceMock.Verify(s => s.PrepareAddProductAttributeCombinationModel(model, product), Times.Once); } @@ -194,7 +194,7 @@ public async Task AttributeCombinationPopupPost_ScopeDeniesAccess_ReturnsPermiss var content = result as ContentResult; Assert.IsNotNull(content); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify( + _productAttributeCombinationViewModelServiceMock.Verify( s => s.InsertOrUpdateProductAttributeCombinationPopup(It.IsAny(), It.IsAny()), Times.Never); } @@ -206,7 +206,7 @@ public async Task AttributeCombinationPopupPost_ScopeGrantsAccess_NoWarnings_Ret _productServiceMock.Setup(p => p.GetProductById("p1", false)).ReturnsAsync(product); _scopeMock.Setup(s => s.HasAccess(product)).ReturnsAsync(true); var model = new ProductAttributeCombinationModel(); - _productViewModelServiceMock.Setup(s => s.InsertOrUpdateProductAttributeCombinationPopup(product, model)) + _productAttributeCombinationViewModelServiceMock.Setup(s => s.InsertOrUpdateProductAttributeCombinationPopup(product, model)) .ReturnsAsync(new List()); var result = await _controller.AttributeCombinationPopup("p1", model); @@ -223,7 +223,7 @@ public async Task AttributeCombinationPopupPost_ScopeGrantsAccess_WithWarnings_R _productServiceMock.Setup(p => p.GetProductById("p1", false)).ReturnsAsync(product); _scopeMock.Setup(s => s.HasAccess(product)).ReturnsAsync(true); var model = new ProductAttributeCombinationModel(); - _productViewModelServiceMock.Setup(s => s.InsertOrUpdateProductAttributeCombinationPopup(product, model)) + _productAttributeCombinationViewModelServiceMock.Setup(s => s.InsertOrUpdateProductAttributeCombinationPopup(product, model)) .ReturnsAsync(new List { "warning" }); var result = await _controller.AttributeCombinationPopup("p1", model); @@ -232,7 +232,7 @@ public async Task AttributeCombinationPopupPost_ScopeGrantsAccess_WithWarnings_R Assert.IsNotNull(view); Assert.AreSame(model, view.Model); CollectionAssert.Contains(model.Warnings.ToList(), "warning"); - _productViewModelServiceMock.Verify(s => s.PrepareAddProductAttributeCombinationModel(model, product), + _productAttributeCombinationViewModelServiceMock.Verify(s => s.PrepareAddProductAttributeCombinationModel(model, product), Times.Once); } @@ -258,7 +258,7 @@ public async Task GenerateAllAttributeCombinations_ScopeDeniesAccess_ReturnsErro Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify(s => s.GenerateAllAttributeCombinations(It.IsAny()), + _productAttributeCombinationViewModelServiceMock.Verify(s => s.GenerateAllAttributeCombinations(It.IsAny()), Times.Never); } @@ -272,7 +272,7 @@ public async Task GenerateAllAttributeCombinations_ScopeGrantsAccess_Generates() var result = await _controller.GenerateAllAttributeCombinations("p1"); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.GenerateAllAttributeCombinations(product), Times.Once); + _productAttributeCombinationViewModelServiceMock.Verify(s => s.GenerateAllAttributeCombinations(product), Times.Once); } // --- ClearAllAttributeCombinations ----------------------------------------------------------------- @@ -297,7 +297,7 @@ public async Task ClearAllAttributeCombinations_ScopeDeniesAccess_ReturnsErrorJs Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify(s => s.ClearAllAttributeCombinations(It.IsAny()), Times.Never); + _productAttributeCombinationViewModelServiceMock.Verify(s => s.ClearAllAttributeCombinations(It.IsAny()), Times.Never); } [TestMethod] @@ -311,7 +311,7 @@ public async Task ClearAllAttributeCombinations_ScopeGrantsAccess_ValidModel_Cle var result = await _controller.ClearAllAttributeCombinations("p1"); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.ClearAllAttributeCombinations(product), Times.Once); + _productAttributeCombinationViewModelServiceMock.Verify(s => s.ClearAllAttributeCombinations(product), Times.Once); } [TestMethod] @@ -325,7 +325,7 @@ public async Task ClearAllAttributeCombinations_ScopeGrantsAccess_InvalidModelSt var result = await _controller.ClearAllAttributeCombinations("p1"); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.ClearAllAttributeCombinations(It.IsAny()), Times.Never); + _productAttributeCombinationViewModelServiceMock.Verify(s => s.ClearAllAttributeCombinations(It.IsAny()), Times.Never); } // --- ProductAttributeCombinationTierPriceList (POST) ----------------------------------------------- @@ -341,7 +341,7 @@ public async Task ProductAttributeCombinationTierPriceList_ScopeDeniesAccess_Ret Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify( + _productAttributeCombinationViewModelServiceMock.Verify( s => s.PrepareProductAttributeCombinationTierPricesModel(It.IsAny(), It.IsAny()), Times.Never); } @@ -352,7 +352,7 @@ public async Task ProductAttributeCombinationTierPriceList_ScopeGrantsAccess_Ret var product = new Product { Id = "p1" }; _productServiceMock.Setup(p => p.GetProductById("p1", false)).ReturnsAsync(product); _scopeMock.Setup(s => s.HasAccess(product)).ReturnsAsync(true); - _productViewModelServiceMock.Setup(s => s.PrepareProductAttributeCombinationTierPricesModel(product, "c1")) + _productAttributeCombinationViewModelServiceMock.Setup(s => s.PrepareProductAttributeCombinationTierPricesModel(product, "c1")) .ReturnsAsync(new List { new() }); var result = await _controller.ProductAttributeCombinationTierPriceList(new DataSourceRequest(), "p1", "c1"); @@ -388,7 +388,7 @@ public async Task ProductAttributeCombinationTierPriceInsert_ScopeDeniesAccess_R Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify( + _productAttributeCombinationViewModelServiceMock.Verify( s => s.InsertProductAttributeCombinationTierPricesModel(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); @@ -405,7 +405,7 @@ public async Task ProductAttributeCombinationTierPriceInsert_ScopeGrantsAccess_C new ProductModel.ProductAttributeCombinationTierPricesModel()); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify( + _productAttributeCombinationViewModelServiceMock.Verify( s => s.InsertProductAttributeCombinationTierPricesModel(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); @@ -424,7 +424,7 @@ public async Task ProductAttributeCombinationTierPriceInsert_ScopeGrantsAccess_I var result = await _controller.ProductAttributeCombinationTierPriceInsert("p1", "c1", model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify( + _productAttributeCombinationViewModelServiceMock.Verify( s => s.InsertProductAttributeCombinationTierPricesModel(product, combination, model), Times.Once); } @@ -453,7 +453,7 @@ public async Task ProductAttributeCombinationTierPriceUpdate_ScopeDeniesAccess_R Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify( + _productAttributeCombinationViewModelServiceMock.Verify( s => s.UpdateProductAttributeCombinationTierPricesModel(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); @@ -472,7 +472,7 @@ public async Task ProductAttributeCombinationTierPriceUpdate_ScopeGrantsAccess_U var result = await _controller.ProductAttributeCombinationTierPriceUpdate("p1", "c1", model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify( + _productAttributeCombinationViewModelServiceMock.Verify( s => s.UpdateProductAttributeCombinationTierPricesModel(product, combination, model), Times.Once); } @@ -499,7 +499,7 @@ public async Task ProductAttributeCombinationTierPriceDelete_ScopeDeniesAccess_R Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify( + _productAttributeCombinationViewModelServiceMock.Verify( s => s.DeleteProductAttributeCombinationTierPrices(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); @@ -516,7 +516,7 @@ public async Task ProductAttributeCombinationTierPriceDelete_ScopeGrantsAccess_T var result = await _controller.ProductAttributeCombinationTierPriceDelete("p1", "c1", "missing-tp"); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify( + _productAttributeCombinationViewModelServiceMock.Verify( s => s.DeleteProductAttributeCombinationTierPrices(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); @@ -536,7 +536,7 @@ public async Task ProductAttributeCombinationTierPriceDelete_ScopeGrantsAccess_D var result = await _controller.ProductAttributeCombinationTierPriceDelete("p1", "c1", "tp1"); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify( + _productAttributeCombinationViewModelServiceMock.Verify( s => s.DeleteProductAttributeCombinationTierPrices(product, combination, tierPrice), Times.Once); } } diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductAttributeMappingControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductAttributeMappingControllerTests.cs index 5f44fcffa..fd9795909 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductAttributeMappingControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductAttributeMappingControllerTests.cs @@ -24,15 +24,17 @@ public class BaseProductAttributeMappingControllerTests : ProductControllerTests // BaseProductAttributeMappingController is abstract. This minimal subclass exists only so the actions under test // can be invoked directly. public class TestProductAttributeMappingController( - IProductViewModelService productViewModelService, + IProductAddPopupViewModelService productAddPopupViewModelService, + IProductAttributeMappingViewModelService productAttributeMappingViewModelService, IProductService productService, ILanguageService languageService, IRequestTranslationService translationService, IAdminDataScope scope) - : BaseProductAttributeMappingController(productViewModelService, productService, languageService, translationService, scope); + : BaseProductAttributeMappingController(productAddPopupViewModelService, productAttributeMappingViewModelService, productService, languageService, translationService, scope); protected override TestProductAttributeMappingController CreateController() => new( - _productViewModelServiceMock.Object, + _productAddPopupViewModelServiceMock.Object, + _productAttributeMappingViewModelServiceMock.Object, _productServiceMock.Object, _languageServiceMock.Object, _translationServiceMock.Object, @@ -57,7 +59,7 @@ public async Task ProductAttributeMappingList_ScopeDeniesAccess_ReturnsErrorJson Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify( + _productAttributeMappingViewModelServiceMock.Verify( s => s.PrepareProductAttributeMappingModels(It.IsAny()), Times.Never); } @@ -83,7 +85,7 @@ public async Task ProductAttributeMappingList_ScopeGrantsAccess_ReturnsGrid() _productServiceMock.Setup(p => p.GetProductById("p1", false)).ReturnsAsync(product); _scopeMock.Setup(s => s.CanView(product)).ReturnsAsync(true); var attributes = new List { new() { Id = "pam1" } }; - _productViewModelServiceMock.Setup(s => s.PrepareProductAttributeMappingModels(product)) + _productAttributeMappingViewModelServiceMock.Setup(s => s.PrepareProductAttributeMappingModels(product)) .ReturnsAsync(attributes); var result = await _controller.ProductAttributeMappingList(new Grand.Web.Common.DataSource.DataSourceRequest(), "p1"); @@ -109,7 +111,7 @@ public async Task ProductAttributeMappingPopup_Get_ScopeDeniesAccess_ReturnsPerm var content = result as ContentResult; Assert.IsNotNull(content); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify( + _productAttributeMappingViewModelServiceMock.Verify( s => s.PrepareProductAttributeMappingModel(It.IsAny()), Times.Never); } @@ -119,13 +121,13 @@ public async Task ProductAttributeMappingPopup_Get_ScopeGrantsAccess_NoMappingId var product = new Product { Id = "p1" }; _productServiceMock.Setup(p => p.GetProductById("p1", false)).ReturnsAsync(product); _scopeMock.Setup(s => s.HasAccess(product)).ReturnsAsync(true); - _productViewModelServiceMock.Setup(s => s.PrepareProductAttributeMappingModel(product)) + _productAttributeMappingViewModelServiceMock.Setup(s => s.PrepareProductAttributeMappingModel(product)) .ReturnsAsync(new ProductModel.ProductAttributeMappingModel()); var result = await _controller.ProductAttributeMappingPopup("p1", null); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.PrepareProductAttributeMappingModel(product), Times.Once); + _productAttributeMappingViewModelServiceMock.Verify(s => s.PrepareProductAttributeMappingModel(product), Times.Once); } [TestMethod] @@ -136,13 +138,13 @@ public async Task ProductAttributeMappingPopup_Get_ScopeGrantsAccess_MappingId_P product.ProductAttributeMappings.Add(mapping); _productServiceMock.Setup(p => p.GetProductById("p1", false)).ReturnsAsync(product); _scopeMock.Setup(s => s.HasAccess(product)).ReturnsAsync(true); - _productViewModelServiceMock.Setup(s => s.PrepareProductAttributeMappingModel(product, mapping)) + _productAttributeMappingViewModelServiceMock.Setup(s => s.PrepareProductAttributeMappingModel(product, mapping)) .ReturnsAsync(new ProductModel.ProductAttributeMappingModel()); var result = await _controller.ProductAttributeMappingPopup("p1", "pam1"); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.PrepareProductAttributeMappingModel(product, mapping), Times.Once); + _productAttributeMappingViewModelServiceMock.Verify(s => s.PrepareProductAttributeMappingModel(product, mapping), Times.Once); } // --- ProductAttributeMappingPopup (POST) ----------------------------------------------------- @@ -161,10 +163,10 @@ public async Task ProductAttributeMappingPopup_Post_ScopeDeniesAccess_DoesNotIns var content = result as ContentResult; Assert.IsNotNull(content); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify( + _productAttributeMappingViewModelServiceMock.Verify( s => s.InsertProductAttributeMappingModel(It.IsAny()), Times.Never); - _productViewModelServiceMock.Verify( + _productAttributeMappingViewModelServiceMock.Verify( s => s.UpdateProductAttributeMappingModel(It.IsAny()), Times.Never); } @@ -180,8 +182,8 @@ public async Task ProductAttributeMappingPopup_Post_ScopeGrantsAccess_NoId_Inser var result = await _controller.ProductAttributeMappingPopup(model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.InsertProductAttributeMappingModel(model), Times.Once); - _productViewModelServiceMock.Verify( + _productAttributeMappingViewModelServiceMock.Verify(s => s.InsertProductAttributeMappingModel(model), Times.Once); + _productAttributeMappingViewModelServiceMock.Verify( s => s.UpdateProductAttributeMappingModel(It.IsAny()), Times.Never); } @@ -197,7 +199,7 @@ public async Task ProductAttributeMappingPopup_Post_ScopeGrantsAccess_WithId_Upd var result = await _controller.ProductAttributeMappingPopup(model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.UpdateProductAttributeMappingModel(model), Times.Once); + _productAttributeMappingViewModelServiceMock.Verify(s => s.UpdateProductAttributeMappingModel(model), Times.Once); } [TestMethod] @@ -214,7 +216,7 @@ public async Task ProductAttributeMappingPopup_Post_InvalidModelState_ReturnsVie { _controller.ModelState.AddModelError("x", "err"); var model = new ProductModel.ProductAttributeMappingModel { ProductId = "p1" }; - _productViewModelServiceMock.Setup(s => s.PrepareProductAttributeMappingModel(model)).ReturnsAsync(model); + _productAttributeMappingViewModelServiceMock.Setup(s => s.PrepareProductAttributeMappingModel(model)).ReturnsAsync(model); var result = await _controller.ProductAttributeMappingPopup(model); @@ -319,13 +321,13 @@ public async Task ProductAttributeValidationRulesPopup_Get_ScopeGrantsAccess_Pre product.ProductAttributeMappings.Add(mapping); _productServiceMock.Setup(p => p.GetProductById("p1", false)).ReturnsAsync(product); _scopeMock.Setup(s => s.HasAccess(product)).ReturnsAsync(true); - _productViewModelServiceMock.Setup(s => s.PrepareProductAttributeMappingModel(mapping)) + _productAttributeMappingViewModelServiceMock.Setup(s => s.PrepareProductAttributeMappingModel(mapping)) .ReturnsAsync(new ProductModel.ProductAttributeMappingModel()); var result = await _controller.ProductAttributeValidationRulesPopup("pam1", "p1"); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.PrepareProductAttributeMappingModel(mapping), Times.Once); + _productAttributeMappingViewModelServiceMock.Verify(s => s.PrepareProductAttributeMappingModel(mapping), Times.Once); } // --- ProductAttributeValidationRulesPopup (POST) ---------------------------------------------- @@ -369,7 +371,7 @@ public async Task ProductAttributeValidationRulesPopup_Post_ScopeDeniesAccess_Re var content = result as ContentResult; Assert.IsNotNull(content); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify( + _productAttributeMappingViewModelServiceMock.Verify( s => s.UpdateProductAttributeValidationRulesModel(It.IsAny(), It.IsAny()), Times.Never); } @@ -387,7 +389,7 @@ public async Task ProductAttributeValidationRulesPopup_Post_ScopeGrantsAccess_Va var result = await _controller.ProductAttributeValidationRulesPopup(model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify( + _productAttributeMappingViewModelServiceMock.Verify( s => s.UpdateProductAttributeValidationRulesModel(mapping, model), Times.Once); } @@ -401,12 +403,12 @@ public async Task ProductAttributeValidationRulesPopup_Post_ScopeGrantsAccess_In _scopeMock.Setup(s => s.HasAccess(product)).ReturnsAsync(true); _controller.ModelState.AddModelError("x", "err"); var model = new ProductModel.ProductAttributeMappingModel { Id = "pam1", ProductId = "p1" }; - _productViewModelServiceMock.Setup(s => s.PrepareProductAttributeMappingModel(mapping)).ReturnsAsync(model); + _productAttributeMappingViewModelServiceMock.Setup(s => s.PrepareProductAttributeMappingModel(mapping)).ReturnsAsync(model); var result = await _controller.ProductAttributeValidationRulesPopup(model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify( + _productAttributeMappingViewModelServiceMock.Verify( s => s.UpdateProductAttributeValidationRulesModel(It.IsAny(), It.IsAny()), Times.Never); } @@ -427,7 +429,7 @@ public async Task ProductAttributeConditionPopup_Get_ScopeDeniesAccess_ReturnsPe var content = result as ContentResult; Assert.IsNotNull(content); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify( + _productAttributeMappingViewModelServiceMock.Verify( s => s.PrepareProductAttributeConditionModel(It.IsAny(), It.IsAny()), Times.Never); } @@ -459,7 +461,7 @@ public async Task ProductAttributeConditionPopup_Get_ScopeGrantsAccess_MappingMi var content = result as ContentResult; Assert.IsNotNull(content); Assert.AreEqual("No attribute value found with the specified id", content.Content); - _productViewModelServiceMock.Verify( + _productAttributeMappingViewModelServiceMock.Verify( s => s.PrepareProductAttributeConditionModel(It.IsAny(), It.IsAny()), Times.Never); } @@ -472,13 +474,13 @@ public async Task ProductAttributeConditionPopup_Get_ScopeGrantsAccess_PreparesM product.ProductAttributeMappings.Add(mapping); _productServiceMock.Setup(p => p.GetProductById("p1", false)).ReturnsAsync(product); _scopeMock.Setup(s => s.HasAccess(product)).ReturnsAsync(true); - _productViewModelServiceMock.Setup(s => s.PrepareProductAttributeConditionModel(product, mapping)) + _productAttributeMappingViewModelServiceMock.Setup(s => s.PrepareProductAttributeConditionModel(product, mapping)) .ReturnsAsync(new ProductAttributeConditionModel()); var result = await _controller.ProductAttributeConditionPopup("p1", "pam1"); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.PrepareProductAttributeConditionModel(product, mapping), + _productAttributeMappingViewModelServiceMock.Verify(s => s.PrepareProductAttributeConditionModel(product, mapping), Times.Once); } @@ -527,7 +529,7 @@ public async Task ProductAttributeConditionPopup_Post_ScopeDeniesAccess_ReturnsP var content = result as ContentResult; Assert.IsNotNull(content); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify( + _productAttributeMappingViewModelServiceMock.Verify( s => s.UpdateProductAttributeConditionModel(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } @@ -545,7 +547,7 @@ public async Task ProductAttributeConditionPopup_Post_ScopeGrantsAccess_Updates( var result = await _controller.ProductAttributeConditionPopup(model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.UpdateProductAttributeConditionModel(product, mapping, model), + _productAttributeMappingViewModelServiceMock.Verify(s => s.UpdateProductAttributeConditionModel(product, mapping, model), Times.Once); } @@ -626,7 +628,7 @@ public async Task ProductAttributeValueList_ScopeDeniesAccess_ReturnsErrorJson() Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify( + _productAttributeMappingViewModelServiceMock.Verify( s => s.PrepareProductAttributeValueModels(It.IsAny(), It.IsAny()), Times.Never); } @@ -651,7 +653,7 @@ public async Task ProductAttributeValueList_ScopeGrantsAccess_ReturnsGrid() _productServiceMock.Setup(p => p.GetProductById("p1", false)).ReturnsAsync(product); _scopeMock.Setup(s => s.HasAccess(product)).ReturnsAsync(true); var values = new List { new() { Id = "pav1" } }; - _productViewModelServiceMock.Setup(s => s.PrepareProductAttributeValueModels(product, mapping)) + _productAttributeMappingViewModelServiceMock.Setup(s => s.PrepareProductAttributeValueModels(product, mapping)) .ReturnsAsync(values); var result = await _controller.ProductAttributeValueList("pam1", "p1", new Grand.Web.Common.DataSource.DataSourceRequest()); @@ -677,7 +679,7 @@ public async Task ProductAttributeValueCreatePopup_Get_ScopeDeniesAccess_Returns var content = result as ContentResult; Assert.IsNotNull(content); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify( + _productAttributeMappingViewModelServiceMock.Verify( s => s.PrepareProductAttributeValueModel(It.IsAny(), It.IsAny()), Times.Never); } @@ -701,13 +703,13 @@ public async Task ProductAttributeValueCreatePopup_Get_ScopeGrantsAccess_Prepare product.ProductAttributeMappings.Add(mapping); _productServiceMock.Setup(p => p.GetProductById("p1", false)).ReturnsAsync(product); _scopeMock.Setup(s => s.HasAccess(product)).ReturnsAsync(true); - _productViewModelServiceMock.Setup(s => s.PrepareProductAttributeValueModel(product, mapping)) + _productAttributeMappingViewModelServiceMock.Setup(s => s.PrepareProductAttributeValueModel(product, mapping)) .ReturnsAsync(new ProductModel.ProductAttributeValueModel()); var result = await _controller.ProductAttributeValueCreatePopup("pam1", "p1"); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.PrepareProductAttributeValueModel(product, mapping), Times.Once); + _productAttributeMappingViewModelServiceMock.Verify(s => s.PrepareProductAttributeValueModel(product, mapping), Times.Once); } // --- ProductAttributeValueCreatePopup (POST) ---------------------------------------------------- @@ -741,7 +743,7 @@ public async Task ProductAttributeValueCreatePopup_Post_ScopeDeniesAccess_Return var content = result as ContentResult; Assert.IsNotNull(content); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify( + _productAttributeMappingViewModelServiceMock.Verify( s => s.InsertProductAttributeValueModel(It.IsAny()), Times.Never); } @@ -764,7 +766,7 @@ public async Task ProductAttributeValueCreatePopup_Post_ScopeDeniesAccess_UsesVe Assert.IsNotNull(content); _translationServiceMock.Verify(t => t.GetResource("Vendor.Catalog.Products.Permissions"), Times.Once); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Never); - _productViewModelServiceMock.Verify( + _productAttributeMappingViewModelServiceMock.Verify( s => s.InsertProductAttributeValueModel(It.IsAny()), Times.Never); } @@ -798,7 +800,7 @@ public async Task ProductAttributeValueCreatePopup_Post_ScopeGrantsAccess_ValidM var result = await _controller.ProductAttributeValueCreatePopup(model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.InsertProductAttributeValueModel(model), Times.Once); + _productAttributeMappingViewModelServiceMock.Verify(s => s.InsertProductAttributeValueModel(model), Times.Once); } [TestMethod] @@ -815,10 +817,10 @@ public async Task ProductAttributeValueCreatePopup_Post_ScopeGrantsAccess_Invali var result = await _controller.ProductAttributeValueCreatePopup(model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify( + _productAttributeMappingViewModelServiceMock.Verify( s => s.InsertProductAttributeValueModel(It.IsAny()), Times.Never); - _productViewModelServiceMock.Verify(s => s.PrepareProductAttributeValueModel(product, model), Times.Once); + _productAttributeMappingViewModelServiceMock.Verify(s => s.PrepareProductAttributeValueModel(product, model), Times.Once); } // --- ProductAttributeValueEditPopup (GET) ------------------------------------------------------- @@ -878,14 +880,14 @@ public async Task ProductAttributeValueEditPopup_Get_ScopeGrantsAccess_PreparesM _productServiceMock.Setup(p => p.GetProductById("p1", false)).ReturnsAsync(product); _scopeMock.Setup(s => s.HasAccess(product)).ReturnsAsync(true); var model = new ProductModel.ProductAttributeValueModel(); - _productViewModelServiceMock.Setup(s => s.PrepareProductAttributeValueModel(mapping, pav)) + _productAttributeMappingViewModelServiceMock.Setup(s => s.PrepareProductAttributeValueModel(mapping, pav)) .ReturnsAsync(model); var result = await _controller.ProductAttributeValueEditPopup("pav1", "p1", "pam1"); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.PrepareProductAttributeValueModel(mapping, pav), Times.Once); - _productViewModelServiceMock.Verify(s => s.PrepareProductAttributeValueModel(product, model), Times.Once); + _productAttributeMappingViewModelServiceMock.Verify(s => s.PrepareProductAttributeValueModel(mapping, pav), Times.Once); + _productAttributeMappingViewModelServiceMock.Verify(s => s.PrepareProductAttributeValueModel(product, model), Times.Once); } // --- ProductAttributeValueEditPopup (POST) ------------------------------------------------------ @@ -915,7 +917,7 @@ public async Task ProductAttributeValueEditPopup_Post_ScopeDeniesAccess_ReturnsP var content = result as ContentResult; Assert.IsNotNull(content); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify( + _productAttributeMappingViewModelServiceMock.Verify( s => s.UpdateProductAttributeValueModel(It.IsAny(), It.IsAny()), Times.Never); } @@ -950,7 +952,7 @@ public async Task ProductAttributeValueEditPopup_Post_ScopeGrantsAccess_ValidMod var result = await _controller.ProductAttributeValueEditPopup("p1", model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.UpdateProductAttributeValueModel(pav, model), Times.Once); + _productAttributeMappingViewModelServiceMock.Verify(s => s.UpdateProductAttributeValueModel(pav, model), Times.Once); } [TestMethod] @@ -969,10 +971,10 @@ public async Task ProductAttributeValueEditPopup_Post_ScopeGrantsAccess_InvalidM var result = await _controller.ProductAttributeValueEditPopup("p1", model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify( + _productAttributeMappingViewModelServiceMock.Verify( s => s.UpdateProductAttributeValueModel(It.IsAny(), It.IsAny()), Times.Never); - _productViewModelServiceMock.Verify(s => s.PrepareProductAttributeValueModel(product, model), Times.Once); + _productAttributeMappingViewModelServiceMock.Verify(s => s.PrepareProductAttributeValueModel(product, model), Times.Once); } // --- ProductAttributeValueDelete ----------------------------------------------------------------- @@ -1063,13 +1065,13 @@ public async Task ProductAttributeValueDelete_ScopeGrantsAccess_InvalidModelStat public async Task AssociateProductToAttributeValuePopup_Get_CallsPrepareAssociateProductToAttributeValueModel() { // PrepareAssociateProductToAttributeValueModel() now reads scope internally (Task 9). - _productViewModelServiceMock.Setup(s => s.PrepareAssociateProductToAttributeValueModel()) + _productAddPopupViewModelServiceMock.Setup(s => s.PrepareAssociateProductToAttributeValueModel()) .ReturnsAsync(new ProductModel.ProductAttributeValueModel.AssociateProductToAttributeValueModel()); var result = await _controller.AssociateProductToAttributeValuePopup(); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.PrepareAssociateProductToAttributeValueModel(), + _productAddPopupViewModelServiceMock.Verify(s => s.PrepareAssociateProductToAttributeValueModel(), Times.Once); } @@ -1080,7 +1082,7 @@ public async Task AssociateProductToAttributeValuePopupList_DefaultStoreIdSet_Ap { _scopeMock.Setup(s => s.DefaultStoreId).Returns("store1"); var model = new ProductModel.ProductAttributeValueModel.AssociateProductToAttributeValueModel(); - _productViewModelServiceMock.Setup(s => s.PrepareProductModel(model, 1, 10)) + _productAddPopupViewModelServiceMock.Setup(s => s.PrepareProductModel(model, 1, 10)) .ReturnsAsync((new List(), 0)); var result = await _controller.AssociateProductToAttributeValuePopupList( @@ -1095,7 +1097,7 @@ public async Task AssociateProductToAttributeValuePopupList_DefaultStoreIdNull_L { var model = new ProductModel.ProductAttributeValueModel.AssociateProductToAttributeValueModel { SearchStoreId = "preset" }; - _productViewModelServiceMock.Setup(s => s.PrepareProductModel(model, 1, 10)) + _productAddPopupViewModelServiceMock.Setup(s => s.PrepareProductModel(model, 1, 10)) .ReturnsAsync((new List(), 0)); var result = await _controller.AssociateProductToAttributeValuePopupList( diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductBidControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductBidControllerTests.cs index 328524115..0981f8607 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductBidControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductBidControllerTests.cs @@ -23,15 +23,15 @@ public class BaseProductBidControllerTests : ProductControllerTestsBase scope) - : BaseProductBidController(productViewModelService, productService, translationService, auctionService, scope); + : BaseProductBidController(productBidViewModelService, productService, translationService, auctionService, scope); protected override TestProductBidController CreateController() => new( - _productViewModelServiceMock.Object, + _productBidViewModelServiceMock.Object, _productServiceMock.Object, _translationServiceMock.Object, _auctionServiceMock.Object, @@ -62,7 +62,7 @@ public async Task ListBids_ScopeDeniesAccess_ReturnsKendoGridError_DoesNotQuery( Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify( + _productBidViewModelServiceMock.Verify( s => s.PrepareBidMode(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } @@ -73,7 +73,7 @@ public async Task ListBids_ScopeGrantsAccess_ReturnsBids() _productServiceMock.Setup(p => p.GetProductById("p1")).ReturnsAsync(product); _scopeMock.Setup(s => s.CanView(product)).ReturnsAsync(true); _scopeMock.Setup(s => s.HasAccess(product)).ReturnsAsync(true); - _productViewModelServiceMock.Setup(s => s.PrepareBidMode("p1", 0, 10)) + _productBidViewModelServiceMock.Setup(s => s.PrepareBidMode("p1", 0, 10)) .ReturnsAsync((Enumerable.Empty(), 0)); var result = await _controller.ListBids( @@ -102,7 +102,7 @@ public async Task ListBids_ViewableButNotOwned_ReturnsEmptyGrid_DoesNotQuery() Assert.IsNotNull(gridModel); Assert.AreEqual(0, gridModel.Total); _translationServiceMock.Verify(t => t.GetResource(It.IsAny()), Times.Never); - _productViewModelServiceMock.Verify( + _productBidViewModelServiceMock.Verify( s => s.PrepareBidMode(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductBundleControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductBundleControllerTests.cs index e9fd3fa5d..bc8a65be6 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductBundleControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductBundleControllerTests.cs @@ -22,14 +22,16 @@ public class BaseProductBundleControllerTests : ProductControllerTestsBase scope) - : BaseProductBundleController(productViewModelService, productService, translationService, scope); + : BaseProductBundleController(productAddPopupViewModelService, productBundleViewModelService, productService, translationService, scope); protected override TestProductBundleController CreateController() => new( - _productViewModelServiceMock.Object, + _productAddPopupViewModelServiceMock.Object, + _productBundleViewModelServiceMock.Object, _productServiceMock.Object, _translationServiceMock.Object, _scopeMock.Object); @@ -82,7 +84,7 @@ public async Task BundleProductUpdate_ScopeDeniesAccess_ReturnsErrorJson_DoesNot Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify(s => s.UpdateBundleProductModel(It.IsAny()), Times.Never); + _productBundleViewModelServiceMock.Verify(s => s.UpdateBundleProductModel(It.IsAny()), Times.Never); } [TestMethod] @@ -96,7 +98,7 @@ public async Task BundleProductUpdate_ScopeGrantsAccess_ValidModel_Updates() var result = await _controller.BundleProductUpdate(model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.UpdateBundleProductModel(model), Times.Once); + _productBundleViewModelServiceMock.Verify(s => s.UpdateBundleProductModel(model), Times.Once); } // --- BundleProductDelete ---------------------------------------------------------------------- @@ -113,7 +115,7 @@ public async Task BundleProductDelete_ScopeDeniesAccess_ReturnsErrorJson_DoesNot Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify(s => s.DeleteBundleProductModel(It.IsAny()), Times.Never); + _productBundleViewModelServiceMock.Verify(s => s.DeleteBundleProductModel(It.IsAny()), Times.Never); } [TestMethod] @@ -127,7 +129,7 @@ public async Task BundleProductDelete_ScopeGrantsAccess_ValidModel_Deletes() var result = await _controller.BundleProductDelete(model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.DeleteBundleProductModel(model), Times.Once); + _productBundleViewModelServiceMock.Verify(s => s.DeleteBundleProductModel(model), Times.Once); } // --- BundleProductAddPopup (GET) -------------------------------------------------------------- @@ -136,7 +138,7 @@ public async Task BundleProductDelete_ScopeGrantsAccess_ValidModel_Deletes() public async Task BundleProductAddPopupGet_CallsPrepareBundleProductModel() { // PrepareBundleProductModel() now reads scope internally (Task 9). - _productViewModelServiceMock.Setup(s => s.PrepareBundleProductModel()) + _productAddPopupViewModelServiceMock.Setup(s => s.PrepareBundleProductModel()) .ReturnsAsync(new ProductModel.AddBundleProductModel()); var result = await _controller.BundleProductAddPopup("p1") as ViewResult; @@ -145,7 +147,7 @@ public async Task BundleProductAddPopupGet_CallsPrepareBundleProductModel() var model = result.Model as ProductModel.AddBundleProductModel; Assert.IsNotNull(model); Assert.AreEqual("p1", model.ProductId); - _productViewModelServiceMock.Verify(s => s.PrepareBundleProductModel(), Times.Once); + _productAddPopupViewModelServiceMock.Verify(s => s.PrepareBundleProductModel(), Times.Once); } // --- BundleProductAddPopupList ----------------------------------------------------------------- @@ -154,7 +156,7 @@ public async Task BundleProductAddPopupGet_CallsPrepareBundleProductModel() public async Task BundleProductAddPopupList_UsesScopeDefaultStoreId() { _scopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - _productViewModelServiceMock + _productAddPopupViewModelServiceMock .Setup(s => s.PrepareProductModel(It.IsAny(), 0, 10)) .ReturnsAsync((new List(), 0)); @@ -170,7 +172,7 @@ public async Task BundleProductAddPopupList_UsesScopeDefaultStoreId() public async Task BundleProductAddPopupList_NoDefaultStoreId_DoesNotOverrideModelSearchStoreId() { _scopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - _productViewModelServiceMock + _productAddPopupViewModelServiceMock .Setup(s => s.PrepareProductModel(It.IsAny(), 0, 10)) .ReturnsAsync((new List(), 0)); @@ -199,7 +201,7 @@ public async Task BundleProductAddPopupPost_ScopeDeniesAccess_ReturnsContentMess Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify(s => s.InsertBundleProductModel(It.IsAny()), Times.Never); + _productBundleViewModelServiceMock.Verify(s => s.InsertBundleProductModel(It.IsAny()), Times.Never); } [TestMethod] @@ -215,7 +217,7 @@ public async Task BundleProductAddPopupPost_ScopeGrantsAccess_ValidModel_Inserts var content = result as ContentResult; Assert.IsNotNull(content); Assert.AreEqual("", content.Content); - _productViewModelServiceMock.Verify(s => s.InsertBundleProductModel(model), Times.Once); + _productBundleViewModelServiceMock.Verify(s => s.InsertBundleProductModel(model), Times.Once); } [TestMethod] @@ -225,7 +227,7 @@ public async Task BundleProductAddPopupPost_ScopeGrantsAccess_InvalidModel_Retur _productServiceMock.Setup(p => p.GetProductById("p1", false)).ReturnsAsync(product); _scopeMock.Setup(s => s.HasAccess(product)).ReturnsAsync(true); var reprepared = new ProductModel.AddBundleProductModel(); - _productViewModelServiceMock.Setup(s => s.PrepareBundleProductModel()).ReturnsAsync(reprepared); + _productAddPopupViewModelServiceMock.Setup(s => s.PrepareBundleProductModel()).ReturnsAsync(reprepared); var model = new ProductModel.AddBundleProductModel { ProductId = "p1" }; _controller.ModelState.AddModelError("x", "error"); @@ -234,6 +236,6 @@ public async Task BundleProductAddPopupPost_ScopeGrantsAccess_InvalidModel_Retur var view = result as ViewResult; Assert.IsNotNull(view); Assert.AreSame(reprepared, view.Model); - _productViewModelServiceMock.Verify(s => s.InsertBundleProductModel(It.IsAny()), Times.Never); + _productBundleViewModelServiceMock.Verify(s => s.InsertBundleProductModel(It.IsAny()), Times.Never); } } diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductCategoryControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductCategoryControllerTests.cs index dbb7d145b..75f6e9ce2 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductCategoryControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductCategoryControllerTests.cs @@ -23,14 +23,14 @@ public class BaseProductCategoryControllerTests : ProductControllerTestsBase scope) - : BaseProductCategoryController(productViewModelService, productService, translationService, scope); + : BaseProductCategoryController(productCategoryViewModelService, productService, translationService, scope); protected override TestProductCategoryController CreateController() => new( - _productViewModelServiceMock.Object, + _productCategoryViewModelServiceMock.Object, _productServiceMock.Object, _translationServiceMock.Object, _scopeMock.Object); @@ -51,7 +51,7 @@ public async Task ProductCategoryList_ScopeDeniesAccess_ReturnsErrorJson() Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify(s => s.PrepareProductCategoryModel(It.IsAny()), Times.Never); + _productCategoryViewModelServiceMock.Verify(s => s.PrepareProductCategoryModel(It.IsAny()), Times.Never); } // Guards against a real regression found in review: the denial message must be templated via @@ -80,7 +80,7 @@ public async Task ProductCategoryList_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); - _productViewModelServiceMock.Setup(s => s.PrepareProductCategoryModel(product)) + _productCategoryViewModelServiceMock.Setup(s => s.PrepareProductCategoryModel(product)) .ReturnsAsync(new List { new() { Id = "c1", ProductId = "p1" } }); var result = await _controller.ProductCategoryList( @@ -102,7 +102,7 @@ public async Task ProductCategoryList_ViewableButNotOwned_ReturnsGrid() _productServiceMock.Setup(p => p.GetProductById("p1", false)).ReturnsAsync(product); _scopeMock.Setup(s => s.CanView(product)).ReturnsAsync(true); _scopeMock.Setup(s => s.HasAccess(product)).ReturnsAsync(false); - _productViewModelServiceMock.Setup(s => s.PrepareProductCategoryModel(product)) + _productCategoryViewModelServiceMock.Setup(s => s.PrepareProductCategoryModel(product)) .ReturnsAsync(new List { new() { Id = "c1", ProductId = "p1" } }); var result = await _controller.ProductCategoryList( @@ -125,7 +125,7 @@ public async Task ProductCategoryInsert_ViewableButNotOwned_IsDenied() Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify( + _productCategoryViewModelServiceMock.Verify( s => s.InsertProductCategoryModel(It.IsAny()), Times.Never); } @@ -143,7 +143,7 @@ public async Task ProductCategoryInsert_ScopeDeniesAccess_ReturnsErrorJson_DoesN Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify(s => s.InsertProductCategoryModel(It.IsAny()), Times.Never); + _productCategoryViewModelServiceMock.Verify(s => s.InsertProductCategoryModel(It.IsAny()), Times.Never); } [TestMethod] @@ -157,7 +157,7 @@ public async Task ProductCategoryInsert_ScopeGrantsAccess_ValidModel_Inserts() var result = await _controller.ProductCategoryInsert(model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.InsertProductCategoryModel(model), Times.Once); + _productCategoryViewModelServiceMock.Verify(s => s.InsertProductCategoryModel(model), Times.Once); } // --- ProductCategoryUpdate --------------------------------------------------------------------- @@ -174,7 +174,7 @@ public async Task ProductCategoryUpdate_ScopeDeniesAccess_ReturnsErrorJson_DoesN Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify(s => s.UpdateProductCategoryModel(It.IsAny()), Times.Never); + _productCategoryViewModelServiceMock.Verify(s => s.UpdateProductCategoryModel(It.IsAny()), Times.Never); } [TestMethod] @@ -188,7 +188,7 @@ public async Task ProductCategoryUpdate_ScopeGrantsAccess_ValidModel_Updates() var result = await _controller.ProductCategoryUpdate(model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.UpdateProductCategoryModel(model), Times.Once); + _productCategoryViewModelServiceMock.Verify(s => s.UpdateProductCategoryModel(model), Times.Once); } // --- ProductCategoryDelete --------------------------------------------------------------------- @@ -205,7 +205,7 @@ public async Task ProductCategoryDelete_ScopeDeniesAccess_ReturnsErrorJson_DoesN Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify(s => s.DeleteProductCategory(It.IsAny(), It.IsAny()), Times.Never); + _productCategoryViewModelServiceMock.Verify(s => s.DeleteProductCategory(It.IsAny(), It.IsAny()), Times.Never); } [TestMethod] @@ -219,6 +219,6 @@ public async Task ProductCategoryDelete_ScopeGrantsAccess_ValidModel_Deletes() var result = await _controller.ProductCategoryDelete(model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.DeleteProductCategory("c1", "p1"), Times.Once); + _productCategoryViewModelServiceMock.Verify(s => s.DeleteProductCategory("c1", "p1"), Times.Once); } } diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductCollectionControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductCollectionControllerTests.cs index e62e34398..c446a2058 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductCollectionControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductCollectionControllerTests.cs @@ -22,14 +22,14 @@ public class BaseProductCollectionControllerTests : ProductControllerTestsBase scope) - : BaseProductCollectionController(productViewModelService, productService, translationService, scope); + : BaseProductCollectionController(productCollectionViewModelService, productService, translationService, scope); protected override TestProductCollectionController CreateController() => new( - _productViewModelServiceMock.Object, + _productCollectionViewModelServiceMock.Object, _productServiceMock.Object, _translationServiceMock.Object, _scopeMock.Object); @@ -50,7 +50,7 @@ public async Task ProductCollectionList_ScopeDeniesAccess_ReturnsErrorJson() Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify(s => s.PrepareProductCollectionModel(It.IsAny()), Times.Never); + _productCollectionViewModelServiceMock.Verify(s => s.PrepareProductCollectionModel(It.IsAny()), Times.Never); } // Guards against the same regression class found in "Product categories": the denial message must be @@ -78,7 +78,7 @@ public async Task ProductCollectionList_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); - _productViewModelServiceMock.Setup(s => s.PrepareProductCollectionModel(product)) + _productCollectionViewModelServiceMock.Setup(s => s.PrepareProductCollectionModel(product)) .ReturnsAsync(new List { new() { Id = "c1", ProductId = "p1" } }); var result = await _controller.ProductCollectionList( @@ -105,7 +105,7 @@ public async Task ProductCollectionInsert_ScopeDeniesAccess_ReturnsErrorJson_Doe Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify(s => s.InsertProductCollection(It.IsAny()), Times.Never); + _productCollectionViewModelServiceMock.Verify(s => s.InsertProductCollection(It.IsAny()), Times.Never); } [TestMethod] @@ -119,7 +119,7 @@ public async Task ProductCollectionInsert_ScopeGrantsAccess_ValidModel_Inserts() var result = await _controller.ProductCollectionInsert(model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.InsertProductCollection(model), Times.Once); + _productCollectionViewModelServiceMock.Verify(s => s.InsertProductCollection(model), Times.Once); } // --- ProductCollectionUpdate ---------------------------------------------------------------- @@ -136,7 +136,7 @@ public async Task ProductCollectionUpdate_ScopeDeniesAccess_ReturnsErrorJson_Doe Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify(s => s.UpdateProductCollection(It.IsAny()), Times.Never); + _productCollectionViewModelServiceMock.Verify(s => s.UpdateProductCollection(It.IsAny()), Times.Never); } [TestMethod] @@ -150,7 +150,7 @@ public async Task ProductCollectionUpdate_ScopeGrantsAccess_ValidModel_Updates() var result = await _controller.ProductCollectionUpdate(model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.UpdateProductCollection(model), Times.Once); + _productCollectionViewModelServiceMock.Verify(s => s.UpdateProductCollection(model), Times.Once); } // --- ProductCollectionDelete ---------------------------------------------------------------- @@ -167,7 +167,7 @@ public async Task ProductCollectionDelete_ScopeDeniesAccess_ReturnsErrorJson_Doe Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify(s => s.DeleteProductCollection(It.IsAny(), It.IsAny()), Times.Never); + _productCollectionViewModelServiceMock.Verify(s => s.DeleteProductCollection(It.IsAny(), It.IsAny()), Times.Never); } [TestMethod] @@ -181,6 +181,6 @@ public async Task ProductCollectionDelete_ScopeGrantsAccess_ValidModel_Deletes() var result = await _controller.ProductCollectionDelete(model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.DeleteProductCollection("c1", "p1"), Times.Once); + _productCollectionViewModelServiceMock.Verify(s => s.DeleteProductCollection("c1", "p1"), Times.Once); } } diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductControllerTests.cs index b3a388005..3492ba202 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductControllerTests.cs @@ -33,17 +33,19 @@ public class BaseProductControllerTests : ProductControllerTestsBase scope) - : BaseProductController(productViewModelService, productService, languageService, translationService, + : BaseProductController(productViewModelService, productAddPopupViewModelService, productService, languageService, translationService, dateTimeService, permissionService, scope); protected override TestProductController CreateController() => new( _productViewModelServiceMock.Object, + _productAddPopupViewModelServiceMock.Object, _productServiceMock.Object, _languageServiceMock.Object, _translationServiceMock.Object, @@ -503,13 +505,13 @@ private static string GetTextProperty(object value) => public async Task RequiredProductAddPopup_CallsPrepareAddRequiredProductModel() { // PrepareAddRequiredProductModel() now reads scope internally (Task 9). - _productViewModelServiceMock.Setup(s => s.PrepareAddRequiredProductModel()) + _productAddPopupViewModelServiceMock.Setup(s => s.PrepareAddRequiredProductModel()) .ReturnsAsync(new ProductModel.AddRequiredProductModel()); var result = await _controller.RequiredProductAddPopup("input1") as ViewResult; Assert.IsNotNull(result); - _productViewModelServiceMock.Verify(s => s.PrepareAddRequiredProductModel(), Times.Once); + _productAddPopupViewModelServiceMock.Verify(s => s.PrepareAddRequiredProductModel(), Times.Once); Assert.AreEqual("input1", _controller.ViewBag.productIdsInput); } @@ -519,7 +521,7 @@ public async Task RequiredProductAddPopup_CallsPrepareAddRequiredProductModel() public async Task RequiredProductAddPopupList_UsesScopeDefaultStoreId() { _scopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - _productViewModelServiceMock + _productAddPopupViewModelServiceMock .Setup(s => s.PrepareProductModel(It.IsAny(), 0, 10)) .ReturnsAsync((new List(), 0)); @@ -535,7 +537,7 @@ public async Task RequiredProductAddPopupList_UsesScopeDefaultStoreId() public async Task RequiredProductAddPopupList_NoDefaultStoreId_DoesNotOverrideModelSearchStoreId() { _scopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - _productViewModelServiceMock + _productAddPopupViewModelServiceMock .Setup(s => s.PrepareProductModel(It.IsAny(), 0, 10)) .ReturnsAsync((new List(), 0)); diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductPictureControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductPictureControllerTests.cs index 74a940ccb..52ba8b947 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductPictureControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductPictureControllerTests.cs @@ -25,16 +25,16 @@ public class BaseProductPictureControllerTests : ProductControllerTestsBase scope) - : BaseProductPictureController(productViewModelService, productService, languageService, translationService, permissionService, scope); + : BaseProductPictureController(productPictureViewModelService, productService, languageService, translationService, permissionService, scope); protected override TestProductPictureController CreateController() => new( - _productViewModelServiceMock.Object, + _productPictureViewModelServiceMock.Object, _productServiceMock.Object, _languageServiceMock.Object, _translationServiceMock.Object, @@ -54,7 +54,7 @@ public async Task ProductPictureList_ScopeDeniesAccess_ReturnsErrorJson() Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify(s => s.PrepareProductPicturesModel(It.IsAny()), Times.Never); + _productPictureViewModelServiceMock.Verify(s => s.PrepareProductPicturesModel(It.IsAny()), Times.Never); } [TestMethod] @@ -63,7 +63,7 @@ public async Task ProductPictureList_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); - _productViewModelServiceMock.Setup(s => s.PrepareProductPicturesModel(product)) + _productPictureViewModelServiceMock.Setup(s => s.PrepareProductPicturesModel(product)) .ReturnsAsync(new List { new() { Id = "pic1" } }); var result = await _controller.ProductPictureList(new Grand.Web.Common.DataSource.DataSourceRequest(), "p1"); @@ -126,7 +126,7 @@ public async Task ProductPicturePopupGet_ScopeGrantsAccess_PictureFound_ReturnsV _productServiceMock.Setup(p => p.GetProductById("p1", false)).ReturnsAsync(product); _scopeMock.Setup(s => s.HasAccess(product)).ReturnsAsync(true); var model = new ProductModel.ProductPictureModel { Id = "pic1" }; - _productViewModelServiceMock.Setup(s => s.PrepareProductPictureModel(product, pp)) + _productPictureViewModelServiceMock.Setup(s => s.PrepareProductPictureModel(product, pp)) .ReturnsAsync((model, (Picture)null)); var result = await _controller.ProductPicturePopup("p1", "pic1"); @@ -159,7 +159,7 @@ public async Task ProductPicturePopupPost_ScopeDeniesAccess_Throws_DoesNotUpdate // letting any vendor rename/re-alt-text another vendor's product picture by posting its // productId/model.Id. await Assert.ThrowsExactlyAsync(() => _controller.ProductPicturePopup(model)); - _productViewModelServiceMock.Verify(s => s.UpdateProductPicture(It.IsAny()), Times.Never); + _productPictureViewModelServiceMock.Verify(s => s.UpdateProductPicture(It.IsAny()), Times.Never); } [TestMethod] @@ -188,7 +188,7 @@ public async Task ProductPicturePopupPost_ScopeGrantsAccess_ValidModel_Updates() var content = result as ContentResult; Assert.IsNotNull(content); Assert.AreEqual("", content.Content); - _productViewModelServiceMock.Verify(s => s.UpdateProductPicture(model), Times.Once); + _productPictureViewModelServiceMock.Verify(s => s.UpdateProductPicture(model), Times.Once); } [TestMethod] @@ -221,7 +221,7 @@ public async Task ProductPictureDelete_ScopeDeniesAccess_ReturnsErrorJson_DoesNo Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify(s => s.DeleteProductPicture(It.IsAny()), Times.Never); + _productPictureViewModelServiceMock.Verify(s => s.DeleteProductPicture(It.IsAny()), Times.Never); } [TestMethod] @@ -235,7 +235,7 @@ public async Task ProductPictureDelete_ScopeGrantsAccess_ValidModel_Deletes() var result = await _controller.ProductPictureDelete(model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.DeleteProductPicture(model), Times.Once); + _productPictureViewModelServiceMock.Verify(s => s.DeleteProductPicture(model), Times.Once); } [TestMethod] @@ -250,6 +250,6 @@ public async Task ProductPictureDelete_ScopeGrantsAccess_InvalidModelState_Retur var result = await _controller.ProductPictureDelete(model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.DeleteProductPicture(It.IsAny()), Times.Never); + _productPictureViewModelServiceMock.Verify(s => s.DeleteProductPicture(It.IsAny()), Times.Never); } } diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductPriceControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductPriceControllerTests.cs index 92808e9f4..e196c2117 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductPriceControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductPriceControllerTests.cs @@ -23,15 +23,15 @@ public class BaseProductPriceControllerTests : ProductControllerTestsBase scope) - : BaseProductPriceController(productViewModelService, productService, translationService, dateTimeService, scope); + : BaseProductPriceController(productPriceViewModelService, productService, translationService, dateTimeService, scope); protected override TestProductPriceController CreateController() => new( - _productViewModelServiceMock.Object, + _productPriceViewModelServiceMock.Object, _productServiceMock.Object, _translationServiceMock.Object, _dateTimeServiceMock.Object, @@ -208,7 +208,7 @@ public async Task TierPriceList_ScopeDeniesAccess_ReturnsErrorJson() Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify( + _productPriceViewModelServiceMock.Verify( s => s.PrepareTierPriceModel(It.IsAny()), Times.Never); } @@ -235,7 +235,7 @@ public async Task TierPriceList_ScopeGrantsAccess_ReturnsGrid() _productServiceMock.Setup(p => p.GetProductById("p1", false)).ReturnsAsync(product); _scopeMock.Setup(s => s.CanView(product)).ReturnsAsync(true); var tierPrices = new List { new() { Id = "tp1" } }; - _productViewModelServiceMock.Setup(s => s.PrepareTierPriceModel(product)).ReturnsAsync(tierPrices); + _productPriceViewModelServiceMock.Setup(s => s.PrepareTierPriceModel(product)).ReturnsAsync(tierPrices); var result = await _controller.TierPriceList(new Grand.Web.Common.DataSource.DataSourceRequest(), "p1"); @@ -255,7 +255,7 @@ public async Task TierPriceCreatePopup_Get_PreparesModel() var result = await _controller.TierPriceCreatePopup("p1"); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify( + _productPriceViewModelServiceMock.Verify( s => s.PrepareTierPriceModel(It.Is(m => m.ProductId == "p1")), Times.Once); } @@ -316,7 +316,7 @@ public async Task TierPriceEditPopup_Get_ScopeDeniesAccess_ReturnsNotYourProduct var content = result as ContentResult; Assert.IsNotNull(content); Assert.AreEqual("This is not your product", content.Content); - _productViewModelServiceMock.Verify( + _productPriceViewModelServiceMock.Verify( s => s.PrepareTierPriceModel(It.IsAny()), Times.Never); } @@ -346,7 +346,7 @@ public async Task TierPriceEditPopup_Get_ScopeGrantsAccess_PreparesModel() var result = await _controller.TierPriceEditPopup("tp1", "p1"); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify( + _productPriceViewModelServiceMock.Verify( s => s.PrepareTierPriceModel(It.Is(m => m.ProductId == "p1")), Times.Once); } diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductRelatedControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductRelatedControllerTests.cs index 5d8269fd9..060bd5bf2 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductRelatedControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductRelatedControllerTests.cs @@ -23,14 +23,16 @@ public class BaseProductRelatedControllerTests : ProductControllerTestsBase scope) - : BaseProductRelatedController(productViewModelService, productService, translationService, scope); + : BaseProductRelatedController(productAddPopupViewModelService, productRelatedViewModelService, productService, translationService, scope); protected override TestProductRelatedController CreateController() => new( - _productViewModelServiceMock.Object, + _productAddPopupViewModelServiceMock.Object, + _productRelatedViewModelServiceMock.Object, _productServiceMock.Object, _translationServiceMock.Object, _scopeMock.Object); @@ -83,7 +85,7 @@ public async Task RelatedProductUpdate_ScopeDeniesAccess_ReturnsErrorJson_DoesNo Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify(s => s.UpdateRelatedProductModel(It.IsAny()), Times.Never); + _productRelatedViewModelServiceMock.Verify(s => s.UpdateRelatedProductModel(It.IsAny()), Times.Never); } [TestMethod] @@ -97,7 +99,7 @@ public async Task RelatedProductUpdate_ScopeGrantsAccess_ValidModel_Updates() var result = await _controller.RelatedProductUpdate(model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.UpdateRelatedProductModel(model), Times.Once); + _productRelatedViewModelServiceMock.Verify(s => s.UpdateRelatedProductModel(model), Times.Once); } // --- RelatedProductDelete ---------------------------------------------------------------------- @@ -114,7 +116,7 @@ public async Task RelatedProductDelete_ScopeDeniesAccess_ReturnsErrorJson_DoesNo Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify(s => s.DeleteRelatedProductModel(It.IsAny()), Times.Never); + _productRelatedViewModelServiceMock.Verify(s => s.DeleteRelatedProductModel(It.IsAny()), Times.Never); } [TestMethod] @@ -128,7 +130,7 @@ public async Task RelatedProductDelete_ScopeGrantsAccess_ValidModel_Deletes() var result = await _controller.RelatedProductDelete(model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.DeleteRelatedProductModel(model), Times.Once); + _productRelatedViewModelServiceMock.Verify(s => s.DeleteRelatedProductModel(model), Times.Once); } // --- RelatedProductAddPopup (GET) -------------------------------------------------------------- @@ -137,7 +139,7 @@ public async Task RelatedProductDelete_ScopeGrantsAccess_ValidModel_Deletes() public async Task RelatedProductAddPopupGet_CallsPrepareRelatedProductModel() { // PrepareRelatedProductModel() now reads scope internally (Task 9). - _productViewModelServiceMock.Setup(s => s.PrepareRelatedProductModel()) + _productAddPopupViewModelServiceMock.Setup(s => s.PrepareRelatedProductModel()) .ReturnsAsync(new ProductModel.AddRelatedProductModel()); var result = await _controller.RelatedProductAddPopup("p1") as ViewResult; @@ -146,7 +148,7 @@ public async Task RelatedProductAddPopupGet_CallsPrepareRelatedProductModel() var model = result.Model as ProductModel.AddRelatedProductModel; Assert.IsNotNull(model); Assert.AreEqual("p1", model.ProductId); - _productViewModelServiceMock.Verify(s => s.PrepareRelatedProductModel(), Times.Once); + _productAddPopupViewModelServiceMock.Verify(s => s.PrepareRelatedProductModel(), Times.Once); } // --- RelatedProductAddPopupList ----------------------------------------------------------------- @@ -155,7 +157,7 @@ public async Task RelatedProductAddPopupGet_CallsPrepareRelatedProductModel() public async Task RelatedProductAddPopupList_UsesScopeDefaultStoreId() { _scopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - _productViewModelServiceMock + _productAddPopupViewModelServiceMock .Setup(s => s.PrepareProductModel(It.IsAny(), 0, 10)) .ReturnsAsync((new List(), 0)); @@ -171,7 +173,7 @@ public async Task RelatedProductAddPopupList_UsesScopeDefaultStoreId() public async Task RelatedProductAddPopupList_NoDefaultStoreId_DoesNotOverrideModelSearchStoreId() { _scopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - _productViewModelServiceMock + _productAddPopupViewModelServiceMock .Setup(s => s.PrepareProductModel(It.IsAny(), 0, 10)) .ReturnsAsync((new List(), 0)); @@ -200,7 +202,7 @@ public async Task RelatedProductAddPopupPost_ScopeDeniesAccess_ReturnsContentMes Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify(s => s.InsertRelatedProductModel(It.IsAny()), Times.Never); + _productRelatedViewModelServiceMock.Verify(s => s.InsertRelatedProductModel(It.IsAny()), Times.Never); } [TestMethod] @@ -216,7 +218,7 @@ public async Task RelatedProductAddPopupPost_ScopeGrantsAccess_ValidModel_Insert var content = result as ContentResult; Assert.IsNotNull(content); Assert.AreEqual("", content.Content); - _productViewModelServiceMock.Verify(s => s.InsertRelatedProductModel(model), Times.Once); + _productRelatedViewModelServiceMock.Verify(s => s.InsertRelatedProductModel(model), Times.Once); } [TestMethod] @@ -226,7 +228,7 @@ public async Task RelatedProductAddPopupPost_ScopeGrantsAccess_InvalidModel_Retu _productServiceMock.Setup(p => p.GetProductById("p1", false)).ReturnsAsync(product); _scopeMock.Setup(s => s.HasAccess(product)).ReturnsAsync(true); var reprepared = new ProductModel.AddRelatedProductModel(); - _productViewModelServiceMock.Setup(s => s.PrepareRelatedProductModel()).ReturnsAsync(reprepared); + _productAddPopupViewModelServiceMock.Setup(s => s.PrepareRelatedProductModel()).ReturnsAsync(reprepared); var model = new ProductModel.AddRelatedProductModel { ProductId = "p1" }; _controller.ModelState.AddModelError("x", "error"); @@ -235,7 +237,7 @@ public async Task RelatedProductAddPopupPost_ScopeGrantsAccess_InvalidModel_Retu var view = result as ViewResult; Assert.IsNotNull(view); Assert.AreSame(reprepared, view.Model); - _productViewModelServiceMock.Verify(s => s.InsertRelatedProductModel(It.IsAny()), Times.Never); + _productRelatedViewModelServiceMock.Verify(s => s.InsertRelatedProductModel(It.IsAny()), Times.Never); } // --- SimilarProductList ------------------------------------------------------------------------ @@ -286,7 +288,7 @@ public async Task SimilarProductUpdate_ScopeDeniesAccess_ReturnsErrorJson_DoesNo Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify(s => s.UpdateSimilarProductModel(It.IsAny()), Times.Never); + _productRelatedViewModelServiceMock.Verify(s => s.UpdateSimilarProductModel(It.IsAny()), Times.Never); } [TestMethod] @@ -300,7 +302,7 @@ public async Task SimilarProductUpdate_ScopeGrantsAccess_ValidModel_Updates() var result = await _controller.SimilarProductUpdate(model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.UpdateSimilarProductModel(model), Times.Once); + _productRelatedViewModelServiceMock.Verify(s => s.UpdateSimilarProductModel(model), Times.Once); } // --- SimilarProductDelete ---------------------------------------------------------------------- @@ -317,7 +319,7 @@ public async Task SimilarProductDelete_ScopeDeniesAccess_ReturnsErrorJson_DoesNo Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify(s => s.DeleteSimilarProductModel(It.IsAny()), Times.Never); + _productRelatedViewModelServiceMock.Verify(s => s.DeleteSimilarProductModel(It.IsAny()), Times.Never); } [TestMethod] @@ -331,7 +333,7 @@ public async Task SimilarProductDelete_ScopeGrantsAccess_ValidModel_Deletes() var result = await _controller.SimilarProductDelete(model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.DeleteSimilarProductModel(model), Times.Once); + _productRelatedViewModelServiceMock.Verify(s => s.DeleteSimilarProductModel(model), Times.Once); } // --- SimilarProductAddPopup (GET) -------------------------------------------------------------- @@ -340,7 +342,7 @@ public async Task SimilarProductDelete_ScopeGrantsAccess_ValidModel_Deletes() public async Task SimilarProductAddPopupGet_CallsPrepareSimilarProductModel() { // PrepareSimilarProductModel() now reads scope internally (Task 9). - _productViewModelServiceMock.Setup(s => s.PrepareSimilarProductModel()) + _productAddPopupViewModelServiceMock.Setup(s => s.PrepareSimilarProductModel()) .ReturnsAsync(new ProductModel.AddSimilarProductModel()); var result = await _controller.SimilarProductAddPopup("p1") as ViewResult; @@ -349,7 +351,7 @@ public async Task SimilarProductAddPopupGet_CallsPrepareSimilarProductModel() var model = result.Model as ProductModel.AddSimilarProductModel; Assert.IsNotNull(model); Assert.AreEqual("p1", model.ProductId); - _productViewModelServiceMock.Verify(s => s.PrepareSimilarProductModel(), Times.Once); + _productAddPopupViewModelServiceMock.Verify(s => s.PrepareSimilarProductModel(), Times.Once); } // --- SimilarProductAddPopupList ----------------------------------------------------------------- @@ -358,7 +360,7 @@ public async Task SimilarProductAddPopupGet_CallsPrepareSimilarProductModel() public async Task SimilarProductAddPopupList_UsesScopeDefaultStoreId() { _scopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - _productViewModelServiceMock + _productAddPopupViewModelServiceMock .Setup(s => s.PrepareProductModel(It.IsAny(), 0, 10)) .ReturnsAsync((new List(), 0)); @@ -374,7 +376,7 @@ public async Task SimilarProductAddPopupList_UsesScopeDefaultStoreId() public async Task SimilarProductAddPopupList_NoDefaultStoreId_DoesNotOverrideModelSearchStoreId() { _scopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - _productViewModelServiceMock + _productAddPopupViewModelServiceMock .Setup(s => s.PrepareProductModel(It.IsAny(), 0, 10)) .ReturnsAsync((new List(), 0)); @@ -403,7 +405,7 @@ public async Task SimilarProductAddPopupPost_ScopeDeniesAccess_ReturnsContentMes Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify(s => s.InsertSimilarProductModel(It.IsAny()), Times.Never); + _productRelatedViewModelServiceMock.Verify(s => s.InsertSimilarProductModel(It.IsAny()), Times.Never); } [TestMethod] @@ -419,7 +421,7 @@ public async Task SimilarProductAddPopupPost_ScopeGrantsAccess_ValidModel_Insert var content = result as ContentResult; Assert.IsNotNull(content); Assert.AreEqual("", content.Content); - _productViewModelServiceMock.Verify(s => s.InsertSimilarProductModel(model), Times.Once); + _productRelatedViewModelServiceMock.Verify(s => s.InsertSimilarProductModel(model), Times.Once); } [TestMethod] @@ -429,7 +431,7 @@ public async Task SimilarProductAddPopupPost_ScopeGrantsAccess_InvalidModel_Retu _productServiceMock.Setup(p => p.GetProductById("p1", false)).ReturnsAsync(product); _scopeMock.Setup(s => s.HasAccess(product)).ReturnsAsync(true); var reprepared = new ProductModel.AddSimilarProductModel(); - _productViewModelServiceMock.Setup(s => s.PrepareSimilarProductModel()).ReturnsAsync(reprepared); + _productAddPopupViewModelServiceMock.Setup(s => s.PrepareSimilarProductModel()).ReturnsAsync(reprepared); var model = new ProductModel.AddSimilarProductModel { ProductId = "p1" }; _controller.ModelState.AddModelError("x", "error"); @@ -438,7 +440,7 @@ public async Task SimilarProductAddPopupPost_ScopeGrantsAccess_InvalidModel_Retu var view = result as ViewResult; Assert.IsNotNull(view); Assert.AreSame(reprepared, view.Model); - _productViewModelServiceMock.Verify(s => s.InsertSimilarProductModel(It.IsAny()), Times.Never); + _productRelatedViewModelServiceMock.Verify(s => s.InsertSimilarProductModel(It.IsAny()), Times.Never); } // --- CrossSellProductList --------------------------------------------------------------------- @@ -502,7 +504,7 @@ public async Task CrossSellProductDelete_ScopeDeniesAccess_ReturnsErrorJson_Does Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify(s => s.DeleteCrossSellProduct(It.IsAny(), It.IsAny()), Times.Never); + _productRelatedViewModelServiceMock.Verify(s => s.DeleteCrossSellProduct(It.IsAny(), It.IsAny()), Times.Never); } [TestMethod] @@ -528,7 +530,7 @@ public async Task CrossSellProductDelete_ScopeGrantsAccess_ValidModel_Deletes() var result = await _controller.CrossSellProductDelete(model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.DeleteCrossSellProduct("p1", "p2"), Times.Once); + _productRelatedViewModelServiceMock.Verify(s => s.DeleteCrossSellProduct("p1", "p2"), Times.Once); } // --- CrossSellProductAddPopup (GET) ------------------------------------------------------------- @@ -537,7 +539,7 @@ public async Task CrossSellProductDelete_ScopeGrantsAccess_ValidModel_Deletes() public async Task CrossSellProductAddPopupGet_CallsPrepareCrossSellProductModel() { // PrepareCrossSellProductModel() now reads scope internally (Task 9). - _productViewModelServiceMock.Setup(s => s.PrepareCrossSellProductModel()) + _productAddPopupViewModelServiceMock.Setup(s => s.PrepareCrossSellProductModel()) .ReturnsAsync(new ProductModel.AddCrossSellProductModel()); var result = await _controller.CrossSellProductAddPopup("p1") as ViewResult; @@ -546,7 +548,7 @@ public async Task CrossSellProductAddPopupGet_CallsPrepareCrossSellProductModel( var model = result.Model as ProductModel.AddCrossSellProductModel; Assert.IsNotNull(model); Assert.AreEqual("p1", model.ProductId); - _productViewModelServiceMock.Verify(s => s.PrepareCrossSellProductModel(), Times.Once); + _productAddPopupViewModelServiceMock.Verify(s => s.PrepareCrossSellProductModel(), Times.Once); } // --- CrossSellProductAddPopupList --------------------------------------------------------------- @@ -555,7 +557,7 @@ public async Task CrossSellProductAddPopupGet_CallsPrepareCrossSellProductModel( public async Task CrossSellProductAddPopupList_UsesScopeDefaultStoreId() { _scopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - _productViewModelServiceMock + _productAddPopupViewModelServiceMock .Setup(s => s.PrepareProductModel(It.IsAny(), 0, 10)) .ReturnsAsync((new List(), 0)); @@ -571,7 +573,7 @@ public async Task CrossSellProductAddPopupList_UsesScopeDefaultStoreId() public async Task CrossSellProductAddPopupList_NoDefaultStoreId_DoesNotOverrideModelSearchStoreId() { _scopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - _productViewModelServiceMock + _productAddPopupViewModelServiceMock .Setup(s => s.PrepareProductModel(It.IsAny(), 0, 10)) .ReturnsAsync((new List(), 0)); @@ -600,7 +602,7 @@ public async Task CrossSellProductAddPopupPost_ScopeDeniesAccess_ReturnsContentM Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify(s => s.InsertCrossSellProductModel(It.IsAny()), Times.Never); + _productRelatedViewModelServiceMock.Verify(s => s.InsertCrossSellProductModel(It.IsAny()), Times.Never); } [TestMethod] @@ -616,7 +618,7 @@ public async Task CrossSellProductAddPopupPost_ScopeGrantsAccess_ValidModel_Inse var content = result as ContentResult; Assert.IsNotNull(content); Assert.AreEqual("", content.Content); - _productViewModelServiceMock.Verify(s => s.InsertCrossSellProductModel(model), Times.Once); + _productRelatedViewModelServiceMock.Verify(s => s.InsertCrossSellProductModel(model), Times.Once); } [TestMethod] @@ -626,7 +628,7 @@ public async Task CrossSellProductAddPopupPost_ScopeGrantsAccess_InvalidModel_Re _productServiceMock.Setup(p => p.GetProductById("p1", false)).ReturnsAsync(product); _scopeMock.Setup(s => s.HasAccess(product)).ReturnsAsync(true); var reprepared = new ProductModel.AddCrossSellProductModel(); - _productViewModelServiceMock.Setup(s => s.PrepareCrossSellProductModel()).ReturnsAsync(reprepared); + _productAddPopupViewModelServiceMock.Setup(s => s.PrepareCrossSellProductModel()).ReturnsAsync(reprepared); var model = new ProductModel.AddCrossSellProductModel { ProductId = "p1" }; _controller.ModelState.AddModelError("x", "error"); @@ -635,7 +637,7 @@ public async Task CrossSellProductAddPopupPost_ScopeGrantsAccess_InvalidModel_Re var view = result as ViewResult; Assert.IsNotNull(view); Assert.AreSame(reprepared, view.Model); - _productViewModelServiceMock.Verify(s => s.InsertCrossSellProductModel(It.IsAny()), Times.Never); + _productRelatedViewModelServiceMock.Verify(s => s.InsertCrossSellProductModel(It.IsAny()), Times.Never); } // --- RecommendedProductList --------------------------------------------------------------------- @@ -700,7 +702,7 @@ public async Task RecommendedProductDelete_ScopeDeniesAccess_ReturnsErrorJson_Do Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify(s => s.DeleteRecommendedProduct(It.IsAny(), It.IsAny()), Times.Never); + _productRelatedViewModelServiceMock.Verify(s => s.DeleteRecommendedProduct(It.IsAny(), It.IsAny()), Times.Never); } [TestMethod] @@ -726,7 +728,7 @@ public async Task RecommendedProductDelete_ScopeGrantsAccess_ValidModel_Deletes( var result = await _controller.RecommendedProductDelete(model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.DeleteRecommendedProduct("p1", "p2"), Times.Once); + _productRelatedViewModelServiceMock.Verify(s => s.DeleteRecommendedProduct("p1", "p2"), Times.Once); } // --- RecommendedProductAddPopup (GET) ------------------------------------------------------------- @@ -735,7 +737,7 @@ public async Task RecommendedProductDelete_ScopeGrantsAccess_ValidModel_Deletes( public async Task RecommendedProductAddPopupGet_CallsPrepareRecommendedProductModel() { // PrepareRecommendedProductModel() now reads scope internally (Task 9). - _productViewModelServiceMock.Setup(s => s.PrepareRecommendedProductModel()) + _productAddPopupViewModelServiceMock.Setup(s => s.PrepareRecommendedProductModel()) .ReturnsAsync(new ProductModel.AddRecommendedProductModel()); var result = await _controller.RecommendedProductAddPopup("p1") as ViewResult; @@ -744,7 +746,7 @@ public async Task RecommendedProductAddPopupGet_CallsPrepareRecommendedProductMo var model = result.Model as ProductModel.AddRecommendedProductModel; Assert.IsNotNull(model); Assert.AreEqual("p1", model.ProductId); - _productViewModelServiceMock.Verify(s => s.PrepareRecommendedProductModel(), Times.Once); + _productAddPopupViewModelServiceMock.Verify(s => s.PrepareRecommendedProductModel(), Times.Once); } // --- RecommendedProductAddPopupList --------------------------------------------------------------- @@ -753,7 +755,7 @@ public async Task RecommendedProductAddPopupGet_CallsPrepareRecommendedProductMo public async Task RecommendedProductAddPopupList_UsesScopeDefaultStoreId() { _scopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - _productViewModelServiceMock + _productAddPopupViewModelServiceMock .Setup(s => s.PrepareProductModel(It.IsAny(), 0, 10)) .ReturnsAsync((new List(), 0)); @@ -769,7 +771,7 @@ public async Task RecommendedProductAddPopupList_UsesScopeDefaultStoreId() public async Task RecommendedProductAddPopupList_NoDefaultStoreId_DoesNotOverrideModelSearchStoreId() { _scopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - _productViewModelServiceMock + _productAddPopupViewModelServiceMock .Setup(s => s.PrepareProductModel(It.IsAny(), 0, 10)) .ReturnsAsync((new List(), 0)); @@ -798,7 +800,7 @@ public async Task RecommendedProductAddPopupPost_ScopeDeniesAccess_ReturnsConten Assert.IsInstanceOfType(result); _translationServiceMock.Verify(t => t.GetResource("Admin.Catalog.Products.Permissions"), Times.Once); - _productViewModelServiceMock.Verify(s => s.InsertRecommendedProductModel(It.IsAny()), Times.Never); + _productRelatedViewModelServiceMock.Verify(s => s.InsertRecommendedProductModel(It.IsAny()), Times.Never); } [TestMethod] @@ -814,7 +816,7 @@ public async Task RecommendedProductAddPopupPost_ScopeGrantsAccess_ValidModel_In var content = result as ContentResult; Assert.IsNotNull(content); Assert.AreEqual("", content.Content); - _productViewModelServiceMock.Verify(s => s.InsertRecommendedProductModel(model), Times.Once); + _productRelatedViewModelServiceMock.Verify(s => s.InsertRecommendedProductModel(model), Times.Once); } [TestMethod] @@ -824,7 +826,7 @@ public async Task RecommendedProductAddPopupPost_ScopeGrantsAccess_InvalidModel_ _productServiceMock.Setup(p => p.GetProductById("p1", false)).ReturnsAsync(product); _scopeMock.Setup(s => s.HasAccess(product)).ReturnsAsync(true); var reprepared = new ProductModel.AddRecommendedProductModel(); - _productViewModelServiceMock.Setup(s => s.PrepareRecommendedProductModel()).ReturnsAsync(reprepared); + _productAddPopupViewModelServiceMock.Setup(s => s.PrepareRecommendedProductModel()).ReturnsAsync(reprepared); var model = new ProductModel.AddRecommendedProductModel { ProductId = "p1" }; _controller.ModelState.AddModelError("x", "error"); @@ -833,6 +835,6 @@ public async Task RecommendedProductAddPopupPost_ScopeGrantsAccess_InvalidModel_ var view = result as ViewResult; Assert.IsNotNull(view); Assert.AreSame(reprepared, view.Model); - _productViewModelServiceMock.Verify(s => s.InsertRecommendedProductModel(It.IsAny()), Times.Never); + _productRelatedViewModelServiceMock.Verify(s => s.InsertRecommendedProductModel(It.IsAny()), Times.Never); } } diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductSpecificationControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductSpecificationControllerTests.cs index 6bc7d0311..14db1b304 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductSpecificationControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductSpecificationControllerTests.cs @@ -23,14 +23,14 @@ public class BaseProductSpecificationControllerTests : ProductControllerTestsBas // BaseProductSpecificationController is abstract. This minimal subclass exists only so the actions under test // can be invoked directly. public class TestProductSpecificationController( - IProductViewModelService productViewModelService, + IProductSpecificationViewModelService productSpecificationViewModelService, IProductService productService, IRequestTranslationService translationService, IAdminDataScope scope) - : BaseProductSpecificationController(productViewModelService, productService, translationService, scope); + : BaseProductSpecificationController(productSpecificationViewModelService, productService, translationService, scope); protected override TestProductSpecificationController CreateController() => new( - _productViewModelServiceMock.Object, + _productSpecificationViewModelServiceMock.Object, _productServiceMock.Object, _translationServiceMock.Object, _scopeMock.Object); @@ -47,7 +47,7 @@ public async Task ProductSpecAttrList_ScopeDeniesAccess_ReturnsErrorJson() var result = await _controller.ProductSpecAttrList(new Grand.Web.Common.DataSource.DataSourceRequest(), "p1"); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.PrepareProductSpecificationAttributeModel(It.IsAny()), Times.Never); + _productSpecificationViewModelServiceMock.Verify(s => s.PrepareProductSpecificationAttributeModel(It.IsAny()), Times.Never); } [TestMethod] @@ -56,7 +56,7 @@ public async Task ProductSpecAttrList_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); - _productViewModelServiceMock.Setup(s => s.PrepareProductSpecificationAttributeModel(product)) + _productSpecificationViewModelServiceMock.Setup(s => s.PrepareProductSpecificationAttributeModel(product)) .ReturnsAsync(new List { new() { Id = "psa1" } }); var result = await _controller.ProductSpecAttrList(new Grand.Web.Common.DataSource.DataSourceRequest(), "p1"); @@ -79,7 +79,7 @@ public async Task ProductSpecAttrPopupGet_ScopeDeniesAccess_ReturnsContent() var result = await _controller.ProductSpecAttrPopup(new Mock().Object, "p1", "psa1"); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify( + _productSpecificationViewModelServiceMock.Verify( s => s.PrepareProductSpecificationAttributeModel(It.IsAny()), Times.Never); } @@ -134,7 +134,7 @@ public async Task ProductSpecAttrPopupPost_ProductNotFound_ReturnsContent() var result = await _controller.ProductSpecAttrPopup(new Mock().Object, model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify( + _productSpecificationViewModelServiceMock.Verify( s => s.UpdateProductSpecificationAttributeModel(It.IsAny(), It.IsAny()), Times.Never); } @@ -153,10 +153,10 @@ public async Task ProductSpecAttrPopupPost_ScopeDeniesAccess_ReturnsContent_Does var result = await _controller.ProductSpecAttrPopup(new Mock().Object, model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify( + _productSpecificationViewModelServiceMock.Verify( s => s.InsertProductSpecificationAttributeModel(It.IsAny(), It.IsAny()), Times.Never); - _productViewModelServiceMock.Verify( + _productSpecificationViewModelServiceMock.Verify( s => s.UpdateProductSpecificationAttributeModel(It.IsAny(), It.IsAny()), Times.Never); } @@ -172,7 +172,7 @@ public async Task ProductSpecAttrPopupPost_ScopeGrantsAccess_NewAttribute_Insert var result = await _controller.ProductSpecAttrPopup(new Mock().Object, model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.InsertProductSpecificationAttributeModel(model, product), Times.Once); + _productSpecificationViewModelServiceMock.Verify(s => s.InsertProductSpecificationAttributeModel(model, product), Times.Once); } [TestMethod] @@ -188,7 +188,7 @@ public async Task ProductSpecAttrPopupPost_ScopeGrantsAccess_ExistingAttribute_U var result = await _controller.ProductSpecAttrPopup(new Mock().Object, model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.UpdateProductSpecificationAttributeModel(psa, model), Times.Once); + _productSpecificationViewModelServiceMock.Verify(s => s.UpdateProductSpecificationAttributeModel(psa, model), Times.Once); } [TestMethod] @@ -217,7 +217,7 @@ public async Task ProductSpecAttrDelete_ProductNotFound_ReturnsContent() var result = await _controller.ProductSpecAttrDelete(model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify( + _productSpecificationViewModelServiceMock.Verify( s => s.DeleteProductSpecificationAttribute(It.IsAny(), It.IsAny()), Times.Never); } @@ -236,7 +236,7 @@ public async Task ProductSpecAttrDelete_ScopeDeniesAccess_ReturnsContent_DoesNot var result = await _controller.ProductSpecAttrDelete(model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify( + _productSpecificationViewModelServiceMock.Verify( s => s.DeleteProductSpecificationAttribute(It.IsAny(), It.IsAny()), Times.Never); } @@ -254,7 +254,7 @@ public async Task ProductSpecAttrDelete_ScopeGrantsAccess_ValidModel_Deletes() var result = await _controller.ProductSpecAttrDelete(model); Assert.IsInstanceOfType(result); - _productViewModelServiceMock.Verify(s => s.DeleteProductSpecificationAttribute(product, psa), Times.Once); + _productSpecificationViewModelServiceMock.Verify(s => s.DeleteProductSpecificationAttribute(product, psa), Times.Once); } [TestMethod] diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/ProductControllerTestsBase.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/ProductControllerTestsBase.cs index 560bbfe7d..0aa8be8a8 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/ProductControllerTestsBase.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/ProductControllerTestsBase.cs @@ -28,6 +28,18 @@ public abstract class ProductControllerTestsBase where TController protected TController _controller; protected Mock _productServiceMock; protected Mock _productViewModelServiceMock; + protected Mock _productAddPopupViewModelServiceMock; + protected Mock _productAttributeCombinationViewModelServiceMock; + protected Mock _productAttributeMappingViewModelServiceMock; + protected Mock _productAssociatedViewModelServiceMock; + protected Mock _productBundleViewModelServiceMock; + protected Mock _productRelatedViewModelServiceMock; + protected Mock _productPriceViewModelServiceMock; + protected Mock _productBidViewModelServiceMock; + protected Mock _productCollectionViewModelServiceMock; + protected Mock _productCategoryViewModelServiceMock; + protected Mock _productPictureViewModelServiceMock; + protected Mock _productSpecificationViewModelServiceMock; protected Mock _translationServiceMock; protected Mock> _scopeMock; protected Mock _permissionServiceMock; @@ -52,6 +64,18 @@ public void Setup() _productServiceMock = new Mock(); _productViewModelServiceMock = new Mock(); + _productAddPopupViewModelServiceMock = new Mock(); + _productAttributeCombinationViewModelServiceMock = new Mock(); + _productAttributeMappingViewModelServiceMock = new Mock(); + _productAssociatedViewModelServiceMock = new Mock(); + _productBundleViewModelServiceMock = new Mock(); + _productRelatedViewModelServiceMock = new Mock(); + _productPriceViewModelServiceMock = new Mock(); + _productBidViewModelServiceMock = new Mock(); + _productCollectionViewModelServiceMock = new Mock(); + _productCategoryViewModelServiceMock = new Mock(); + _productPictureViewModelServiceMock = new Mock(); + _productSpecificationViewModelServiceMock = new Mock(); _translationServiceMock = new Mock(); _translationServiceMock.Setup(t => t.GetResource(It.IsAny())).Returns("resource"); diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/ProductAddPopupViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/ProductAddPopupViewModelServiceTests.cs new file mode 100644 index 000000000..f8c567250 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Services/ProductAddPopupViewModelServiceTests.cs @@ -0,0 +1,110 @@ +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Interfaces.Common.Directory; +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.Catalog; +using Grand.Domain.Stores; +using Grand.Domain.Vendors; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Catalog; +using Grand.Web.AdminShared.Services; +using Grand.Web.Common.Localization; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Services; + +[TestClass] +public class ProductAddPopupViewModelServiceTests +{ + private Mock _productServiceMock; + private Mock _vendorServiceMock; + private Mock> _scopeMock; + private ProductAddPopupViewModelService _productAddPopupViewModelService; + + [TestInitialize] + public void Setup() + { + _productServiceMock = new Mock(); + _vendorServiceMock = new Mock(); + + var storeServiceMock = new Mock(); + storeServiceMock.Setup(s => s.GetAllStores()).ReturnsAsync(new List()); + + var translationServiceMock = new Mock(); + translationServiceMock.Setup(t => t.GetResource(It.IsAny())).Returns("resource"); + + var enumTranslationServiceMock = new Mock(); + enumTranslationServiceMock + .Setup(e => e.ToSelectList(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(new SelectList(Enumerable.Empty())); + + // Default: Admin's Global scope - no default store/vendor, store dropdown shows. + _scopeMock = new Mock>(); + _scopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); + _scopeMock.Setup(s => s.ShowStoreSelector).Returns(true); + _scopeMock.Setup(s => s.DefaultVendorId).Returns((string)null); + + _productAddPopupViewModelService = new ProductAddPopupViewModelService( + _productServiceMock.Object, + new Mock().Object, + _vendorServiceMock.Object, + storeServiceMock.Object, + translationServiceMock.Object, + enumTranslationServiceMock.Object, + _scopeMock.Object); + } + + [TestMethod] + public async Task PrepareProductModel_AddProductModel_VendorScope_OverridesModelVendorId() + { + _scopeMock.Setup(s => s.DefaultVendorId).Returns("vendor1"); + _productServiceMock.Setup(p => p.SearchProducts( + 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())); + + var model = new ProductModel.AddRelatedProductModel { SearchVendorId = "vendor2" }; + await _productAddPopupViewModelService.PrepareProductModel(model, 1, 10); + + _productServiceMock.Verify(p => p.SearchProducts( + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), + It.IsAny(), It.IsAny(), "vendor1", 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); + } + + [TestMethod] + public async Task PrepareRelatedProductModel_VendorScope_HidesVendorDropdown() + { + _scopeMock.Setup(s => s.DefaultVendorId).Returns("vendor1"); + + var model = await _productAddPopupViewModelService.PrepareRelatedProductModel(); + + Assert.IsFalse(model.AvailableVendors.Any(), + "Vendor's original PrepareAddProductModel never populated a vendor picker - the search is always forced to the current vendor."); + _vendorServiceMock.Verify(v => v.GetAllVendors(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + + [TestMethod] + public async Task PrepareRelatedProductModel_GlobalScope_IncludesVendorDropdown() + { + _vendorServiceMock.Setup(v => v.GetAllVendors(It.IsAny(), It.IsAny(), It.IsAny(), true)) + .ReturnsAsync(new PagedList { new() { Id = "vendor1", Name = "Vendor 1" } }); + + var model = await _productAddPopupViewModelService.PrepareRelatedProductModel(); + + Assert.IsTrue(model.AvailableVendors.Any(x => x.Value == "vendor1"), + "Admin/Store should keep offering a vendor picker."); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/ProductAttributeCombinationViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/ProductAttributeCombinationViewModelServiceTests.cs new file mode 100644 index 000000000..eda47d323 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Services/ProductAttributeCombinationViewModelServiceTests.cs @@ -0,0 +1,155 @@ +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Interfaces.Catalog.Tax; +using Grand.Business.Core.Interfaces.Checkout.Shipping; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Business.Core.Interfaces.Storage; +using Grand.Domain; +using Grand.Domain.Catalog; +using Grand.Domain.Common; +using Grand.Domain.Directory; +using Grand.Domain.Shipping; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Models.Catalog; +using Grand.Web.AdminShared.Services; +using Grand.Web.Common.Localization; +using Grand.Web.Common.Models; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Services; + +[TestClass] +public class ProductAttributeCombinationViewModelServiceTests +{ + private Mock _inventoryManageServiceMock; + private Mock _productAttributeServiceMock; + private Mock _warehouseServiceMock; + private ProductAttributeCombinationViewModelService _service; + + [TestInitialize] + public void Setup() + { + _inventoryManageServiceMock = new Mock(); + _productAttributeServiceMock = new Mock(); + _warehouseServiceMock = new Mock(); + + _service = new ProductAttributeCombinationViewModelService( + _inventoryManageServiceMock.Object, + new Mock().Object, + _productAttributeServiceMock.Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + _warehouseServiceMock.Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new CurrencySettings()); + } + + private static Product ProductWithOneCombination() + { + var product = new Product { + Id = "p1", + ManageInventoryMethodId = ManageInventoryMethod.ManageStockByAttributes + }; + var mapping = new ProductAttributeMapping { + Id = "m1", + Combination = true, + AttributeControlTypeId = AttributeControlType.DropdownList + }; + mapping.ProductAttributeValues.Add(new ProductAttributeValue { Id = "v1" }); + mapping.ProductAttributeValues.Add(new ProductAttributeValue { Id = "v2" }); + product.ProductAttributeMappings.Add(mapping); + product.ProductAttributeCombinations.Add(new ProductAttributeCombination { + Id = "c1", + Attributes = [new CustomAttribute { Key = "m1", Value = "v2" }], + StockQuantity = 5, + ReservedQuantity = 1 + }); + return product; + } + + [TestMethod] + public async Task Insert_ManageStockByAttributes_ProductStockIncludesTheNewCombination() + { + var product = ProductWithOneCombination(); + var model = new ProductAttributeCombinationModel { + SelectedAttributes = [new CustomAttributeModel { Key = "m1", Value = "v1" }], + StockQuantity = 7, + ReservedQuantity = 2 + }; + + var warnings = await _service.InsertOrUpdateProductAttributeCombinationPopup(product, model); + + Assert.AreEqual(0, warnings.Count); + _inventoryManageServiceMock.Verify(i => i.UpdateStockProduct( + It.Is(p => p.Id == "p1" && p.StockQuantity == 12 && p.ReservedQuantity == 3), false), Times.Once); + } + + [TestMethod] + public async Task Update_ManageStockByAttributes_RecalculatesTheGivenProductNotModelProductId() + { + var product = ProductWithOneCombination(); + var model = new ProductAttributeCombinationModel { + Id = "c1", + ProductId = "another-product", + StockQuantity = 3, + ReservedQuantity = 0 + }; + + var warnings = await _service.InsertOrUpdateProductAttributeCombinationPopup(product, model); + + Assert.AreEqual(0, warnings.Count); + _productAttributeServiceMock.Verify(s => s.UpdateProductAttributeCombination( + It.Is(c => c.Id == "c1"), "p1"), Times.Once); + _inventoryManageServiceMock.Verify(i => i.UpdateStockProduct( + It.Is(p => p.Id == "p1" && p.StockQuantity == 3), false), Times.Once); + _inventoryManageServiceMock.Verify(i => i.UpdateStockProduct( + It.Is(p => p.Id != "p1"), It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task Update_UnknownCombination_ReturnsWarningWithoutSaving() + { + var product = ProductWithOneCombination(); + var model = new ProductAttributeCombinationModel { Id = "missing" }; + + var warnings = await _service.InsertOrUpdateProductAttributeCombinationPopup(product, model); + + Assert.AreEqual(1, warnings.Count); + _productAttributeServiceMock.Verify(s => s.UpdateProductAttributeCombination( + It.IsAny(), It.IsAny()), Times.Never); + _inventoryManageServiceMock.Verify(i => i.UpdateStockProduct(It.IsAny(), It.IsAny()), + Times.Never); + } + + [TestMethod] + public async Task Update_MultipleWarehouses_ReservedQuantityIsTheWarehouseSum() + { + var product = ProductWithOneCombination(); + product.UseMultipleWarehouses = true; + _warehouseServiceMock.Setup(w => w.GetAllWarehouses(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new PagedList { new() { Id = "w1" } }); + var model = new ProductAttributeCombinationModel { + Id = "c1", + StockQuantity = 100, + ReservedQuantity = 90, + WarehouseInventoryModels = [ + new ProductAttributeCombinationModel.WarehouseInventoryModel { + WarehouseId = "w1", WarehouseUsed = true, StockQuantity = 4, ReservedQuantity = 2 + } + ] + }; + + await _service.InsertOrUpdateProductAttributeCombinationPopup(product, model); + + var combination = product.ProductAttributeCombinations.Single(c => c.Id == "c1"); + Assert.AreEqual(4, combination.StockQuantity); + Assert.AreEqual(2, combination.ReservedQuantity); + _inventoryManageServiceMock.Verify(i => i.UpdateStockProduct( + It.Is(p => p.StockQuantity == 4 && p.ReservedQuantity == 2), false), Times.Once); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/ProductBundleViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/ProductBundleViewModelServiceTests.cs new file mode 100644 index 000000000..81bad4034 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Services/ProductBundleViewModelServiceTests.cs @@ -0,0 +1,55 @@ +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Domain.Catalog; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Catalog; +using Grand.Web.AdminShared.Services; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Services; + +[TestClass] +public class ProductBundleViewModelServiceTests +{ + private Mock _productServiceMock; + private ProductBundleViewModelService _productBundleViewModelService; + private Mock> _scopeMock; + + [TestInitialize] + public void Setup() + { + _productServiceMock = new Mock(); + _scopeMock = new Mock>(); + _productBundleViewModelService = new ProductBundleViewModelService(_productServiceMock.Object, _scopeMock.Object); + } + + private void SetupOwnershipForInsertTests(Product ownedProduct, Product notOwnedProduct) + { + _productServiceMock.Setup(p => p.GetProductById(ownedProduct.Id, It.IsAny())).ReturnsAsync(ownedProduct); + _productServiceMock.Setup(p => p.GetProductById(notOwnedProduct.Id, It.IsAny())).ReturnsAsync(notOwnedProduct); + _scopeMock.Setup(s => s.HasAccess(ownedProduct)).ReturnsAsync(true); + _scopeMock.Setup(s => s.HasAccess(notOwnedProduct)).ReturnsAsync(false); + } + + [TestMethod] + public async Task InsertBundleProductModel_DropsNotOwnedSelectedId() + { + var mainProduct = new Product { Id = "main" }; + var owned = new Product { Id = "owned" }; + var notOwned = new Product { Id = "not-owned" }; + _productServiceMock.Setup(p => p.GetProductById("main", It.IsAny())).ReturnsAsync(mainProduct); + SetupOwnershipForInsertTests(owned, notOwned); + + var model = new ProductModel.AddBundleProductModel { + ProductId = "main", + SelectedProductIds = ["owned", "not-owned"] + }; + await _productBundleViewModelService.InsertBundleProductModel(model); + + Assert.IsTrue(mainProduct.BundleProducts.Any(x => x.ProductId == "owned"), + "The owned selected product should be inserted."); + Assert.IsFalse(mainProduct.BundleProducts.Any(x => x.ProductId == "not-owned"), + "A not-owned selected product must be silently dropped, not inserted."); + _productServiceMock.Verify(p => p.InsertBundleProduct(It.Is(r => r.ProductId == "not-owned"), It.IsAny()), Times.Never); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/ProductPriceViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/ProductPriceViewModelServiceTests.cs new file mode 100644 index 000000000..9c9dea095 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Services/ProductPriceViewModelServiceTests.cs @@ -0,0 +1,90 @@ +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Domain; +using Grand.Domain.Catalog; +using Grand.Domain.Customers; +using Grand.Domain.Directory; +using Grand.Domain.Stores; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Catalog; +using Grand.Web.AdminShared.Services; +using Grand.Web.Common.Localization; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Services; + +[TestClass] +public class ProductPriceViewModelServiceTests +{ + private Mock _groupServiceMock; + private ProductPriceViewModelService _productPriceViewModelService; + private Mock> _scopeMock; + private Mock _storeServiceMock; + private Mock _translationServiceMock; + + [TestInitialize] + public void Setup() + { + _groupServiceMock = new Mock(); + _storeServiceMock = new Mock(); + _translationServiceMock = new Mock(); + _translationServiceMock.Setup(t => t.GetResource(It.IsAny())).Returns("resource"); + + var currencyServiceMock = new Mock(); + currencyServiceMock.Setup(c => c.GetCurrencyById(It.IsAny())).ReturnsAsync((Currency)null); + currencyServiceMock.Setup(c => c.GetAllCurrencies(It.IsAny(), It.IsAny())).ReturnsAsync(new List()); + + _storeServiceMock.Setup(s => s.GetAllStores()).ReturnsAsync(new List()); + + _groupServiceMock.Setup(g => g.GetAllCustomerGroups(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new PagedList()); + + // Default: Admin's Global scope - no default store/vendor, store dropdown shows. + _scopeMock = new Mock>(); + _scopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); + _scopeMock.Setup(s => s.ResourceKeyPrefix).Returns("Admin"); + _scopeMock.Setup(s => s.ShowStoreSelector).Returns(true); + _scopeMock.Setup(s => s.DefaultVendorId).Returns((string)null); + + _productPriceViewModelService = new ProductPriceViewModelService( + currencyServiceMock.Object, + new Mock().Object, + _translationServiceMock.Object, + _groupServiceMock.Object, + _storeServiceMock.Object, + _scopeMock.Object); + } + + [TestMethod] + public async Task PrepareTierPriceModel_VendorScope_HidesStoreDropdown() + { + _scopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); + _scopeMock.Setup(s => s.ResourceKeyPrefix).Returns("Vendor"); + _scopeMock.Setup(s => s.ShowStoreSelector).Returns(false); + _storeServiceMock.Setup(s => s.GetAllStores()).ReturnsAsync(new List { + new() { Id = "store1", Shortcut = "Store 1" } + }); + + var model = new ProductModel.TierPriceModel(); + await _productPriceViewModelService.PrepareTierPriceModel(model); + + Assert.IsFalse(model.AvailableStores.Any(), + "Vendor doesn't pick stores - no dropdown should be populated at all, matching the original Vendor service's PrepareTierPriceModel."); + } + + [TestMethod] + public async Task PrepareTierPriceModel_GlobalScope_IncludesStoreDropdown() + { + // Default Setup() scope: Admin's Global scope (DefaultStoreId null, ShowStoreSelector true). + _storeServiceMock.Setup(s => s.GetAllStores()).ReturnsAsync(new List { + new() { Id = "store1", Shortcut = "Store 1" } + }); + + var model = new ProductModel.TierPriceModel(); + await _productPriceViewModelService.PrepareTierPriceModel(model); + + Assert.IsTrue(model.AvailableStores.Any(x => x.Value == "store1"), + "Admin should offer a store dropdown for tier prices."); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/ProductRelatedViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/ProductRelatedViewModelServiceTests.cs new file mode 100644 index 000000000..afe3345a4 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Services/ProductRelatedViewModelServiceTests.cs @@ -0,0 +1,117 @@ +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Domain.Catalog; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Catalog; +using Grand.Web.AdminShared.Services; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Services; + +[TestClass] +public class ProductRelatedViewModelServiceTests +{ + private Mock _productServiceMock; + private ProductRelatedViewModelService _productRelatedViewModelService; + private Mock> _scopeMock; + + [TestInitialize] + public void Setup() + { + _productServiceMock = new Mock(); + _scopeMock = new Mock>(); + _productRelatedViewModelService = new ProductRelatedViewModelService(_productServiceMock.Object, _scopeMock.Object); + } + + private void SetupOwnershipForInsertTests(Product ownedProduct, Product notOwnedProduct) + { + _productServiceMock.Setup(p => p.GetProductById(ownedProduct.Id, It.IsAny())).ReturnsAsync(ownedProduct); + _productServiceMock.Setup(p => p.GetProductById(notOwnedProduct.Id, It.IsAny())).ReturnsAsync(notOwnedProduct); + _scopeMock.Setup(s => s.HasAccess(ownedProduct)).ReturnsAsync(true); + _scopeMock.Setup(s => s.HasAccess(notOwnedProduct)).ReturnsAsync(false); + } + + [TestMethod] + public async Task InsertRelatedProductModel_DropsNotOwnedSelectedId() + { + var mainProduct = new Product { Id = "main" }; + var owned = new Product { Id = "owned" }; + var notOwned = new Product { Id = "not-owned" }; + _productServiceMock.Setup(p => p.GetProductById("main", It.IsAny())).ReturnsAsync(mainProduct); + SetupOwnershipForInsertTests(owned, notOwned); + + var model = new ProductModel.AddRelatedProductModel { + ProductId = "main", + SelectedProductIds = ["owned", "not-owned"] + }; + await _productRelatedViewModelService.InsertRelatedProductModel(model); + + Assert.IsTrue(mainProduct.RelatedProducts.Any(x => x.ProductId2 == "owned"), + "The owned selected product should be inserted."); + Assert.IsFalse(mainProduct.RelatedProducts.Any(x => x.ProductId2 == "not-owned"), + "A not-owned selected product must be silently dropped, not inserted."); + _productServiceMock.Verify(p => p.InsertRelatedProduct(It.Is(r => r.ProductId2 == "not-owned"), It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task InsertSimilarProductModel_DropsNotOwnedSelectedId() + { + var mainProduct = new Product { Id = "main" }; + var owned = new Product { Id = "owned" }; + var notOwned = new Product { Id = "not-owned" }; + _productServiceMock.Setup(p => p.GetProductById("main", It.IsAny())).ReturnsAsync(mainProduct); + SetupOwnershipForInsertTests(owned, notOwned); + + var model = new ProductModel.AddSimilarProductModel { + ProductId = "main", + SelectedProductIds = ["owned", "not-owned"] + }; + await _productRelatedViewModelService.InsertSimilarProductModel(model); + + Assert.IsTrue(mainProduct.SimilarProducts.Any(x => x.ProductId2 == "owned"), + "The owned selected product should be inserted."); + Assert.IsFalse(mainProduct.SimilarProducts.Any(x => x.ProductId2 == "not-owned"), + "A not-owned selected product must be silently dropped, not inserted."); + _productServiceMock.Verify(p => p.InsertSimilarProduct(It.Is(r => r.ProductId2 == "not-owned")), Times.Never); + } + + [TestMethod] + public async Task InsertCrossSellProductModel_DropsNotOwnedSelectedId() + { + var mainProduct = new Product { Id = "main" }; + var owned = new Product { Id = "owned" }; + var notOwned = new Product { Id = "not-owned" }; + _productServiceMock.Setup(p => p.GetProductById("main", It.IsAny())).ReturnsAsync(mainProduct); + SetupOwnershipForInsertTests(owned, notOwned); + + var model = new ProductModel.AddCrossSellProductModel { + ProductId = "main", + SelectedProductIds = ["owned", "not-owned"] + }; + await _productRelatedViewModelService.InsertCrossSellProductModel(model); + + _productServiceMock.Verify(p => p.InsertCrossSellProduct(It.Is(r => r.ProductId2 == "owned")), Times.Once); + _productServiceMock.Verify(p => p.InsertCrossSellProduct(It.Is(r => r.ProductId2 == "not-owned")), Times.Never, + "A not-owned selected product must be silently dropped, not inserted."); + } + + [TestMethod] + public async Task InsertRecommendedProductModel_DropsNotOwnedSelectedId() + { + var mainProduct = new Product { Id = "main" }; + var owned = new Product { Id = "owned" }; + var notOwned = new Product { Id = "not-owned" }; + _productServiceMock.Setup(p => p.GetProductById("main", It.IsAny())).ReturnsAsync(mainProduct); + SetupOwnershipForInsertTests(owned, notOwned); + + var model = new ProductModel.AddRecommendedProductModel { + ProductId = "main", + SelectedProductIds = ["owned", "not-owned"] + }; + await _productRelatedViewModelService.InsertRecommendedProductModel(model); + + _productServiceMock.Verify(p => p.InsertRecommendedProduct("main", "owned"), Times.Once); + _productServiceMock.Verify(p => p.InsertRecommendedProduct("main", "not-owned"), Times.Never, + "A not-owned selected product must be silently dropped, not inserted."); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/ProductViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/ProductViewModelServiceTests.cs index 994504124..c48489f36 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Services/ProductViewModelServiceTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Services/ProductViewModelServiceTests.cs @@ -1,14 +1,11 @@ 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.Interfaces.Catalog.Directory; using Grand.Business.Core.Interfaces.Catalog.Discounts; -using Grand.Business.Core.Interfaces.Catalog.Prices; using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Catalog.Tax; using Grand.Business.Core.Interfaces.Checkout.Shipping; using Grand.Business.Core.Interfaces.Common.Directory; -using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Common.Seo; using Grand.Business.Core.Interfaces.Common.Stores; using Grand.Business.Core.Interfaces.Customers; @@ -19,7 +16,6 @@ using Grand.Domain.Customers; using Grand.Domain.Directory; using Grand.Domain.Discounts; -using Grand.Domain.Media; using Grand.Domain.Seo; using Grand.Domain.Shipping; using Grand.Domain.Stores; @@ -47,7 +43,6 @@ public class ProductViewModelServiceTests private Mock _discountServiceMock; private Mock _enumTranslationServiceMock; - private Mock _groupServiceMock; private Mock _measureServiceMock; private Mock _productServiceMock; private ProductViewModelService _productViewModelService; @@ -66,7 +61,6 @@ public void Setup() _discountServiceMock = new Mock(); _enumTranslationServiceMock = new Mock(); - _groupServiceMock = new Mock(); _measureServiceMock = new Mock(); _productServiceMock = new Mock(); _storeServiceMock = new Mock(); @@ -112,9 +106,6 @@ public void Setup() _storeServiceMock.Setup(s => s.GetAllStores()).ReturnsAsync(new List()); - _groupServiceMock.Setup(g => g.GetAllCustomerGroups(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new PagedList()); - _enumTranslationServiceMock .Setup(e => e.ToSelectList(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new SelectList(Enumerable.Empty())); @@ -137,16 +128,11 @@ public void Setup() _measureServiceMock.Object, new Mock().Object, new Mock().Object, - new Mock().Object, - new Mock().Object, new Mock().Object, - new Mock().Object, _vendorServiceMock.Object, _translationServiceMock.Object, productLayoutServiceMock.Object, - new Mock().Object, contextAccessorMock.Object, - _groupServiceMock.Object, _warehouseServiceMock.Object, deliveryDateServiceMock.Object, _taxCategoryServiceMock.Object, @@ -155,15 +141,11 @@ public void Setup() _storeServiceMock.Object, new Mock().Object, new Mock().Object, - new Mock().Object, - new Mock().Object, new Mock().Object, new CurrencySettings(), new MeasureSettings(), new TaxSettings(), new SeoSettings(), - new Mock().Object, - new Mock().Object, new Mock().Object, _enumTranslationServiceMock.Object, _scopeMock.Object); @@ -272,38 +254,6 @@ public async Task PrepareProductListModel_VendorScope_HidesHomepageOptionAndStor "Vendor doesn't pick stores - no dropdown should be populated at all."); } - [TestMethod] - public async Task PrepareTierPriceModel_VendorScope_HidesStoreDropdown() - { - _scopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - _scopeMock.Setup(s => s.ResourceKeyPrefix).Returns("Vendor"); - _scopeMock.Setup(s => s.ShowStoreSelector).Returns(false); - _storeServiceMock.Setup(s => s.GetAllStores()).ReturnsAsync(new List { - new() { Id = "store1", Shortcut = "Store 1" } - }); - - var model = new ProductModel.TierPriceModel(); - await _productViewModelService.PrepareTierPriceModel(model); - - Assert.IsFalse(model.AvailableStores.Any(), - "Vendor doesn't pick stores - no dropdown should be populated at all, matching the original Vendor service's PrepareTierPriceModel."); - } - - [TestMethod] - public async Task PrepareTierPriceModel_GlobalScope_IncludesStoreDropdown() - { - // Default Setup() scope: Admin's Global scope (DefaultStoreId null, ShowStoreSelector true). - _storeServiceMock.Setup(s => s.GetAllStores()).ReturnsAsync(new List { - new() { Id = "store1", Shortcut = "Store 1" } - }); - - var model = new ProductModel.TierPriceModel(); - await _productViewModelService.PrepareTierPriceModel(model); - - Assert.IsTrue(model.AvailableStores.Any(x => x.Value == "store1"), - "Admin should offer a store dropdown for tier prices."); - } - private void SetupSearchProducts() { _productServiceMock.Setup(p => p.SearchProducts( @@ -413,44 +363,6 @@ public async Task PrepareProducts_VendorScope_OverridesModelVendorId() VerifySearchProductsCalledWithVendorId("vendor1"); } - [TestMethod] - public async Task PrepareProductModel_AddProductModel_VendorScope_OverridesModelVendorId() - { - _scopeMock.Setup(s => s.DefaultVendorId).Returns("vendor1"); - SetupSearchProducts(); - - var model = new ProductModel.AddRelatedProductModel { SearchVendorId = "vendor2" }; - await _productViewModelService.PrepareProductModel(model, 1, 10); - - VerifySearchProductsCalledWithVendorId("vendor1"); - } - - [TestMethod] - public async Task PrepareRelatedProductModel_VendorScope_HidesVendorDropdown() - { - _scopeMock.Setup(s => s.DefaultVendorId).Returns("vendor1"); - - var model = await _productViewModelService.PrepareRelatedProductModel(); - - Assert.IsFalse(model.AvailableVendors.Any(), - "Vendor's original PrepareAddProductModel never populated a vendor picker - the search is always forced to the current vendor."); - _vendorServiceMock.Verify(v => v.GetAllVendors(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), - Times.Never); - } - - [TestMethod] - public async Task PrepareRelatedProductModel_GlobalScope_IncludesVendorDropdown() - { - // Default Setup() scope: Admin's Global scope (DefaultVendorId null). - _vendorServiceMock.Setup(v => v.GetAllVendors(It.IsAny(), It.IsAny(), It.IsAny(), true)) - .ReturnsAsync(new PagedList { new() { Id = "vendor1", Name = "Vendor 1" } }); - - var model = await _productViewModelService.PrepareRelatedProductModel(); - - Assert.IsTrue(model.AvailableVendors.Any(x => x.Value == "vendor1"), - "Admin/Store should keep offering a vendor picker."); - } - [TestMethod] public async Task InsertProductModel_VendorScope_ForcesVendorId() { @@ -504,118 +416,4 @@ public async Task UpdateProductModel_GlobalScope_UsesModelVendorId() Assert.AreEqual("vendor2", result.VendorId, "Admin should keep being able to reassign a product's vendor ownership via the edit form."); } - - private void SetupOwnershipForInsertTests(Product ownedProduct, Product notOwnedProduct) - { - _productServiceMock.Setup(p => p.GetProductById(ownedProduct.Id, It.IsAny())).ReturnsAsync(ownedProduct); - _productServiceMock.Setup(p => p.GetProductById(notOwnedProduct.Id, It.IsAny())).ReturnsAsync(notOwnedProduct); - _scopeMock.Setup(s => s.HasAccess(ownedProduct)).ReturnsAsync(true); - _scopeMock.Setup(s => s.HasAccess(notOwnedProduct)).ReturnsAsync(false); - } - - [TestMethod] - public async Task InsertRelatedProductModel_DropsNotOwnedSelectedId() - { - var mainProduct = new Product { Id = "main" }; - var owned = new Product { Id = "owned" }; - var notOwned = new Product { Id = "not-owned" }; - _productServiceMock.Setup(p => p.GetProductById("main", It.IsAny())).ReturnsAsync(mainProduct); - SetupOwnershipForInsertTests(owned, notOwned); - - var model = new ProductModel.AddRelatedProductModel { - ProductId = "main", - SelectedProductIds = ["owned", "not-owned"] - }; - await _productViewModelService.InsertRelatedProductModel(model); - - Assert.IsTrue(mainProduct.RelatedProducts.Any(x => x.ProductId2 == "owned"), - "The owned selected product should be inserted."); - Assert.IsFalse(mainProduct.RelatedProducts.Any(x => x.ProductId2 == "not-owned"), - "A not-owned selected product must be silently dropped, not inserted."); - _productServiceMock.Verify(p => p.InsertRelatedProduct(It.Is(r => r.ProductId2 == "not-owned"), It.IsAny()), Times.Never); - } - - [TestMethod] - public async Task InsertSimilarProductModel_DropsNotOwnedSelectedId() - { - var mainProduct = new Product { Id = "main" }; - var owned = new Product { Id = "owned" }; - var notOwned = new Product { Id = "not-owned" }; - _productServiceMock.Setup(p => p.GetProductById("main", It.IsAny())).ReturnsAsync(mainProduct); - SetupOwnershipForInsertTests(owned, notOwned); - - var model = new ProductModel.AddSimilarProductModel { - ProductId = "main", - SelectedProductIds = ["owned", "not-owned"] - }; - await _productViewModelService.InsertSimilarProductModel(model); - - Assert.IsTrue(mainProduct.SimilarProducts.Any(x => x.ProductId2 == "owned"), - "The owned selected product should be inserted."); - Assert.IsFalse(mainProduct.SimilarProducts.Any(x => x.ProductId2 == "not-owned"), - "A not-owned selected product must be silently dropped, not inserted."); - _productServiceMock.Verify(p => p.InsertSimilarProduct(It.Is(r => r.ProductId2 == "not-owned")), Times.Never); - } - - [TestMethod] - public async Task InsertBundleProductModel_DropsNotOwnedSelectedId() - { - var mainProduct = new Product { Id = "main" }; - var owned = new Product { Id = "owned" }; - var notOwned = new Product { Id = "not-owned" }; - _productServiceMock.Setup(p => p.GetProductById("main", It.IsAny())).ReturnsAsync(mainProduct); - SetupOwnershipForInsertTests(owned, notOwned); - - var model = new ProductModel.AddBundleProductModel { - ProductId = "main", - SelectedProductIds = ["owned", "not-owned"] - }; - await _productViewModelService.InsertBundleProductModel(model); - - Assert.IsTrue(mainProduct.BundleProducts.Any(x => x.ProductId == "owned"), - "The owned selected product should be inserted."); - Assert.IsFalse(mainProduct.BundleProducts.Any(x => x.ProductId == "not-owned"), - "A not-owned selected product must be silently dropped, not inserted."); - _productServiceMock.Verify(p => p.InsertBundleProduct(It.Is(r => r.ProductId == "not-owned"), It.IsAny()), Times.Never); - } - - [TestMethod] - public async Task InsertCrossSellProductModel_DropsNotOwnedSelectedId() - { - var mainProduct = new Product { Id = "main" }; - var owned = new Product { Id = "owned" }; - var notOwned = new Product { Id = "not-owned" }; - _productServiceMock.Setup(p => p.GetProductById("main", It.IsAny())).ReturnsAsync(mainProduct); - SetupOwnershipForInsertTests(owned, notOwned); - - var model = new ProductModel.AddCrossSellProductModel { - ProductId = "main", - SelectedProductIds = ["owned", "not-owned"] - }; - await _productViewModelService.InsertCrossSellProductModel(model); - - _productServiceMock.Verify(p => p.InsertCrossSellProduct(It.Is(r => r.ProductId2 == "owned")), Times.Once); - _productServiceMock.Verify(p => p.InsertCrossSellProduct(It.Is(r => r.ProductId2 == "not-owned")), Times.Never, - "A not-owned selected product must be silently dropped, not inserted."); - } - - [TestMethod] - public async Task InsertRecommendedProductModel_DropsNotOwnedSelectedId() - { - var mainProduct = new Product { Id = "main" }; - var owned = new Product { Id = "owned" }; - var notOwned = new Product { Id = "not-owned" }; - _productServiceMock.Setup(p => p.GetProductById("main", It.IsAny())).ReturnsAsync(mainProduct); - SetupOwnershipForInsertTests(owned, notOwned); - - var model = new ProductModel.AddRecommendedProductModel { - ProductId = "main", - SelectedProductIds = ["owned", "not-owned"] - }; - await _productViewModelService.InsertRecommendedProductModel(model); - - _productServiceMock.Verify(p => p.InsertRecommendedProduct("main", "owned"), Times.Once); - _productServiceMock.Verify(p => p.InsertRecommendedProduct("main", "not-owned"), Times.Never, - "A not-owned selected product must be silently dropped, not inserted."); - } } diff --git a/src/Tests/Grand.Web.Store.Tests/Controllers/ProductControllerTests.cs b/src/Tests/Grand.Web.Store.Tests/Controllers/ProductControllerTests.cs index a683b3095..7a944a66f 100644 --- a/src/Tests/Grand.Web.Store.Tests/Controllers/ProductControllerTests.cs +++ b/src/Tests/Grand.Web.Store.Tests/Controllers/ProductControllerTests.cs @@ -47,6 +47,7 @@ public void Setup() _controller = new ProductController( productViewModelServiceMock.Object, + new Mock().Object, _productServiceMock.Object, languageServiceMock.Object, translationServiceMock.Object, diff --git a/src/Web/Grand.Web.Admin/Controllers/ProductAssociatedController.cs b/src/Web/Grand.Web.Admin/Controllers/ProductAssociatedController.cs index 74b0de7f3..89adebb23 100644 --- a/src/Web/Grand.Web.Admin/Controllers/ProductAssociatedController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/ProductAssociatedController.cs @@ -14,8 +14,9 @@ namespace Grand.Web.Admin.Controllers; [Area(Constants.AreaAdmin)] [AuthorizeMenu] public class ProductAssociatedController( - IProductViewModelService productViewModelService, + IProductAddPopupViewModelService productAddPopupViewModelService, + IProductAssociatedViewModelService productAssociatedViewModelService, IProductService productService, IRequestTranslationService translationService, IAdminDataScope scope) - : BaseProductAssociatedController(productViewModelService, productService, translationService, scope); + : BaseProductAssociatedController(productAddPopupViewModelService, productAssociatedViewModelService, productService, translationService, scope); diff --git a/src/Web/Grand.Web.Admin/Controllers/ProductAttributeCombinationController.cs b/src/Web/Grand.Web.Admin/Controllers/ProductAttributeCombinationController.cs index 8f8082f58..38f3215e1 100644 --- a/src/Web/Grand.Web.Admin/Controllers/ProductAttributeCombinationController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/ProductAttributeCombinationController.cs @@ -14,9 +14,9 @@ namespace Grand.Web.Admin.Controllers; [Area(Constants.AreaAdmin)] [AuthorizeMenu] public class ProductAttributeCombinationController( - IProductViewModelService productViewModelService, + IProductAttributeCombinationViewModelService productAttributeCombinationViewModelService, IProductService productService, IInventoryManageService inventoryManageService, IRequestTranslationService translationService, IAdminDataScope scope) - : BaseProductAttributeCombinationController(productViewModelService, productService, inventoryManageService, translationService, scope); + : BaseProductAttributeCombinationController(productAttributeCombinationViewModelService, productService, inventoryManageService, translationService, scope); diff --git a/src/Web/Grand.Web.Admin/Controllers/ProductAttributeMappingController.cs b/src/Web/Grand.Web.Admin/Controllers/ProductAttributeMappingController.cs index 2515f4d78..9db1b80d6 100644 --- a/src/Web/Grand.Web.Admin/Controllers/ProductAttributeMappingController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/ProductAttributeMappingController.cs @@ -15,9 +15,10 @@ namespace Grand.Web.Admin.Controllers; [Area(Constants.AreaAdmin)] [AuthorizeMenu] public class ProductAttributeMappingController( - IProductViewModelService productViewModelService, + IProductAddPopupViewModelService productAddPopupViewModelService, + IProductAttributeMappingViewModelService productAttributeMappingViewModelService, IProductService productService, ILanguageService languageService, IRequestTranslationService translationService, IAdminDataScope scope) - : BaseProductAttributeMappingController(productViewModelService, productService, languageService, translationService, scope); + : BaseProductAttributeMappingController(productAddPopupViewModelService, productAttributeMappingViewModelService, productService, languageService, translationService, scope); diff --git a/src/Web/Grand.Web.Admin/Controllers/ProductBidController.cs b/src/Web/Grand.Web.Admin/Controllers/ProductBidController.cs index c5f18311a..1b4e1d586 100644 --- a/src/Web/Grand.Web.Admin/Controllers/ProductBidController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/ProductBidController.cs @@ -14,9 +14,9 @@ namespace Grand.Web.Admin.Controllers; [Area(Constants.AreaAdmin)] [AuthorizeMenu] public class ProductBidController( - IProductViewModelService productViewModelService, + IProductBidViewModelService productBidViewModelService, IProductService productService, IRequestTranslationService translationService, IAuctionService auctionService, IAdminDataScope scope) - : BaseProductBidController(productViewModelService, productService, translationService, auctionService, scope); + : BaseProductBidController(productBidViewModelService, productService, translationService, auctionService, scope); diff --git a/src/Web/Grand.Web.Admin/Controllers/ProductBundleController.cs b/src/Web/Grand.Web.Admin/Controllers/ProductBundleController.cs index 147e0fbac..318f0f932 100644 --- a/src/Web/Grand.Web.Admin/Controllers/ProductBundleController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/ProductBundleController.cs @@ -14,8 +14,9 @@ namespace Grand.Web.Admin.Controllers; [Area(Constants.AreaAdmin)] [AuthorizeMenu] public class ProductBundleController( - IProductViewModelService productViewModelService, + IProductAddPopupViewModelService productAddPopupViewModelService, + IProductBundleViewModelService productBundleViewModelService, IProductService productService, IRequestTranslationService translationService, IAdminDataScope scope) - : BaseProductBundleController(productViewModelService, productService, translationService, scope); + : BaseProductBundleController(productAddPopupViewModelService, productBundleViewModelService, productService, translationService, scope); diff --git a/src/Web/Grand.Web.Admin/Controllers/ProductCategoryController.cs b/src/Web/Grand.Web.Admin/Controllers/ProductCategoryController.cs index 07976a66a..8892ec8eb 100644 --- a/src/Web/Grand.Web.Admin/Controllers/ProductCategoryController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/ProductCategoryController.cs @@ -14,8 +14,8 @@ namespace Grand.Web.Admin.Controllers; [Area(Constants.AreaAdmin)] [AuthorizeMenu] public class ProductCategoryController( - IProductViewModelService productViewModelService, + IProductCategoryViewModelService productCategoryViewModelService, IProductService productService, IRequestTranslationService translationService, IAdminDataScope scope) - : BaseProductCategoryController(productViewModelService, productService, translationService, scope); + : BaseProductCategoryController(productCategoryViewModelService, productService, translationService, scope); diff --git a/src/Web/Grand.Web.Admin/Controllers/ProductCollectionController.cs b/src/Web/Grand.Web.Admin/Controllers/ProductCollectionController.cs index c5c9fb3a0..11a74274f 100644 --- a/src/Web/Grand.Web.Admin/Controllers/ProductCollectionController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/ProductCollectionController.cs @@ -14,8 +14,8 @@ namespace Grand.Web.Admin.Controllers; [Area(Constants.AreaAdmin)] [AuthorizeMenu] public class ProductCollectionController( - IProductViewModelService productViewModelService, + IProductCollectionViewModelService productCollectionViewModelService, IProductService productService, IRequestTranslationService translationService, IAdminDataScope scope) - : BaseProductCollectionController(productViewModelService, productService, translationService, scope); + : BaseProductCollectionController(productCollectionViewModelService, productService, translationService, scope); diff --git a/src/Web/Grand.Web.Admin/Controllers/ProductController.cs b/src/Web/Grand.Web.Admin/Controllers/ProductController.cs index 0a7e0a44f..827f2a336 100644 --- a/src/Web/Grand.Web.Admin/Controllers/ProductController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/ProductController.cs @@ -18,11 +18,12 @@ namespace Grand.Web.Admin.Controllers; [AuthorizeMenu] public class ProductController( IProductViewModelService productViewModelService, + IProductAddPopupViewModelService productAddPopupViewModelService, IProductService productService, ILanguageService languageService, IRequestTranslationService translationService, IDateTimeService dateTimeService, IRequestPermissionService permissionService, IAdminDataScope scope) - : BaseProductController(productViewModelService, productService, languageService, translationService, + : BaseProductController(productViewModelService, productAddPopupViewModelService, productService, languageService, translationService, dateTimeService, permissionService, scope); diff --git a/src/Web/Grand.Web.Admin/Controllers/ProductPictureController.cs b/src/Web/Grand.Web.Admin/Controllers/ProductPictureController.cs index 392d4d2bb..aff93163a 100644 --- a/src/Web/Grand.Web.Admin/Controllers/ProductPictureController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/ProductPictureController.cs @@ -16,10 +16,10 @@ namespace Grand.Web.Admin.Controllers; [Area(Constants.AreaAdmin)] [AuthorizeMenu] public class ProductPictureController( - IProductViewModelService productViewModelService, + IProductPictureViewModelService productPictureViewModelService, IProductService productService, ILanguageService languageService, IRequestTranslationService translationService, IRequestPermissionService permissionService, IAdminDataScope scope) - : BaseProductPictureController(productViewModelService, productService, languageService, translationService, permissionService, scope); + : BaseProductPictureController(productPictureViewModelService, productService, languageService, translationService, permissionService, scope); diff --git a/src/Web/Grand.Web.Admin/Controllers/ProductPriceController.cs b/src/Web/Grand.Web.Admin/Controllers/ProductPriceController.cs index 558d3949e..cda6de000 100644 --- a/src/Web/Grand.Web.Admin/Controllers/ProductPriceController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/ProductPriceController.cs @@ -15,9 +15,9 @@ namespace Grand.Web.Admin.Controllers; [Area(Constants.AreaAdmin)] [AuthorizeMenu] public class ProductPriceController( - IProductViewModelService productViewModelService, + IProductPriceViewModelService productPriceViewModelService, IProductService productService, IRequestTranslationService translationService, IDateTimeService dateTimeService, IAdminDataScope scope) - : BaseProductPriceController(productViewModelService, productService, translationService, dateTimeService, scope); + : BaseProductPriceController(productPriceViewModelService, productService, translationService, dateTimeService, scope); diff --git a/src/Web/Grand.Web.Admin/Controllers/ProductRelatedController.cs b/src/Web/Grand.Web.Admin/Controllers/ProductRelatedController.cs index 7aba3b247..8077d6838 100644 --- a/src/Web/Grand.Web.Admin/Controllers/ProductRelatedController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/ProductRelatedController.cs @@ -14,8 +14,9 @@ namespace Grand.Web.Admin.Controllers; [Area(Constants.AreaAdmin)] [AuthorizeMenu] public class ProductRelatedController( - IProductViewModelService productViewModelService, + IProductAddPopupViewModelService productAddPopupViewModelService, + IProductRelatedViewModelService productRelatedViewModelService, IProductService productService, IRequestTranslationService translationService, IAdminDataScope scope) - : BaseProductRelatedController(productViewModelService, productService, translationService, scope); + : BaseProductRelatedController(productAddPopupViewModelService, productRelatedViewModelService, productService, translationService, scope); diff --git a/src/Web/Grand.Web.Admin/Controllers/ProductSpecificationController.cs b/src/Web/Grand.Web.Admin/Controllers/ProductSpecificationController.cs index 696e219f9..a83a87741 100644 --- a/src/Web/Grand.Web.Admin/Controllers/ProductSpecificationController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/ProductSpecificationController.cs @@ -14,8 +14,8 @@ namespace Grand.Web.Admin.Controllers; [Area(Constants.AreaAdmin)] [AuthorizeMenu] public class ProductSpecificationController( - IProductViewModelService productViewModelService, + IProductSpecificationViewModelService productSpecificationViewModelService, IProductService productService, IRequestTranslationService translationService, IAdminDataScope scope) - : BaseProductSpecificationController(productViewModelService, productService, translationService, scope); + : BaseProductSpecificationController(productSpecificationViewModelService, productService, translationService, scope); diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductAssociatedController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductAssociatedController.cs index 086d95892..82df40324 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductAssociatedController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductAssociatedController.cs @@ -18,7 +18,8 @@ namespace Grand.Web.AdminShared.Controllers; [PermissionAuthorize(PermissionSystemName.Products)] [AutoValidateAntiforgeryToken] public abstract class BaseProductAssociatedController( - IProductViewModelService productViewModelService, + IProductAddPopupViewModelService productAddPopupViewModelService, + IProductAssociatedViewModelService productAssociatedViewModelService, IProductService productService, IRequestTranslationService translationService, IAdminDataScope scope) @@ -104,7 +105,7 @@ public async Task AssociatedProductDelete(ProductModel.Associated if (ModelState.IsValid) { - await productViewModelService.DeleteAssociatedProduct(product); + await productAssociatedViewModelService.DeleteAssociatedProduct(product); return new JsonResult(""); } @@ -117,7 +118,7 @@ public async Task AssociatedProductAddPopup(string productId) // No access check here in any of the three original hosts (all open this popup unconditionally // once the Edit permission is satisfied) - only the mutating actions below tie access to a // specific product. PrepareAssociatedProductModel() now reads scope internally (Task 9). - var model = await productViewModelService.PrepareAssociatedProductModel(); + var model = await productAddPopupViewModelService.PrepareAssociatedProductModel(); model.ProductId = productId; return View(model); } @@ -130,7 +131,7 @@ public async Task AssociatedProductAddPopupList(DataSourceRequest if (scope.DefaultStoreId is not null) model.SearchStoreId = scope.DefaultStoreId; var (products, totalCount) = - await productViewModelService.PrepareProductModel(model, command.Page, command.PageSize); + await productAddPopupViewModelService.PrepareProductModel(model, command.Page, command.PageSize); var gridModel = new DataSourceResult { Data = products.ToList(), Total = totalCount @@ -159,15 +160,11 @@ public async Task AssociatedProductAddPopup(ProductModel.AddAssoc // ParentGroupedProductId directly onto that product's own record - unlike // Related/Similar/Bundle/Cross-sell/Recommended, whose Insert*ProductModel only adds a mapping // entry that references the selected product's id from the parent's own list; the selected - // product's record is never itself modified there. So here, unlike those simpler regions, - // every selected id must independently pass HasAccess too - the same per-id filtering Store's - // controller-level original already did, and which Vendor's original also had, but only at the - // service layer (Grand.Web.Vendor/Services/ProductViewModelService.cs InsertAssociatedProductModel: - // `if (product == null || !HasAccessToProduct(product)) continue;`). BaseProductController - // injects AdminShared's IProductViewModelService - the unfiltered variant - not Vendor's own, so - // once Vendor is subclassed onto this base (Task 11) it loses that service-layer filter - // entirely. Enforcing the per-id check here at the controller level is what preserves the - // invariant going forward. + // product's record is never itself modified there. So here every selected id must independently + // pass HasAccess too - the per-id filtering Store's and Vendor's originals both had. + // IProductAssociatedViewModelService.InsertAssociatedProductModel does not check scope itself + // (unlike the Related and Bundle services), so this filter is the only guard against reparenting + // another vendor's or store's products. if (model.SelectedProductIds != null) { var validIds = new List(); @@ -178,7 +175,7 @@ public async Task AssociatedProductAddPopup(ProductModel.AddAssoc } model.SelectedProductIds = validIds.ToArray(); - if (validIds.Count > 0) await productViewModelService.InsertAssociatedProductModel(model); + if (validIds.Count > 0) await productAssociatedViewModelService.InsertAssociatedProductModel(model); } return Content(""); @@ -189,7 +186,7 @@ public async Task AssociatedProductAddPopup(ProductModel.AddAssoc // AssociatedProductAddPopup(POST) does not use the Content(ModelState.GetErrors()) shortcut it // uses in those other regions, so no host-specific hook is needed for this action. Error(ModelState); - model = await productViewModelService.PrepareAssociatedProductModel(); + model = await productAddPopupViewModelService.PrepareAssociatedProductModel(); return View(model); } diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductAttributeCombinationController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductAttributeCombinationController.cs index 2e72b4438..3a1bb2924 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductAttributeCombinationController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductAttributeCombinationController.cs @@ -16,7 +16,7 @@ namespace Grand.Web.AdminShared.Controllers; [PermissionAuthorize(PermissionSystemName.Products)] [AutoValidateAntiforgeryToken] public abstract class BaseProductAttributeCombinationController( - IProductViewModelService productViewModelService, + IProductAttributeCombinationViewModelService productAttributeCombinationViewModelService, IProductService productService, IInventoryManageService inventoryManageService, IRequestTranslationService translationService, @@ -43,7 +43,7 @@ public async Task ProductAttributeCombinationList(DataSourceReque return ErrorForKendoGridJson( translationService.GetResource($"{scope.ResourceKeyPrefix}.Catalog.Products.Permissions")); - var combinationsModel = await productViewModelService.PrepareProductAttributeCombinationModel(product); + var combinationsModel = await productAttributeCombinationViewModelService.PrepareProductAttributeCombinationModel(product); var gridModel = new DataSourceResult { Data = combinationsModel, Total = combinationsModel.Count @@ -99,8 +99,8 @@ public async Task AttributeCombinationPopup(string productId, str if (!await scope.HasAccess(product)) return Content(translationService.GetResource($"{scope.ResourceKeyPrefix}.Catalog.Products.Permissions")); - var model = await productViewModelService.PrepareProductAttributeCombinationModel(product, id); - await productViewModelService.PrepareAddProductAttributeCombinationModel(model, product); + var model = await productAttributeCombinationViewModelService.PrepareProductAttributeCombinationModel(product, id); + await productAttributeCombinationViewModelService.PrepareAddProductAttributeCombinationModel(model, product); return View(model); } @@ -123,10 +123,10 @@ public async Task AttributeCombinationPopup(string productId, if (!await scope.HasAccess(product)) return Content(translationService.GetResource($"{scope.ResourceKeyPrefix}.Catalog.Products.Permissions")); - var warnings = await productViewModelService.InsertOrUpdateProductAttributeCombinationPopup(product, model); + var warnings = await productAttributeCombinationViewModelService.InsertOrUpdateProductAttributeCombinationPopup(product, model); if (!warnings.Any()) return Content(""); //If we got this far, something failed, redisplay form - await productViewModelService.PrepareAddProductAttributeCombinationModel(model, product); + await productAttributeCombinationViewModelService.PrepareAddProductAttributeCombinationModel(model, product); model.Warnings = warnings; return View(model); } @@ -155,7 +155,7 @@ public async Task GenerateAllAttributeCombinations(string product return ErrorForKendoGridJson( translationService.GetResource($"{scope.ResourceKeyPrefix}.Catalog.Products.Permissions")); - await productViewModelService.GenerateAllAttributeCombinations(product); + await productAttributeCombinationViewModelService.GenerateAllAttributeCombinations(product); return Json(new { Success = true }); } @@ -175,7 +175,7 @@ public async Task ClearAllAttributeCombinations(string productId) if (ModelState.IsValid) { - await productViewModelService.ClearAllAttributeCombinations(product); + await productAttributeCombinationViewModelService.ClearAllAttributeCombinations(product); if (product.ManageInventoryMethodId == ManageInventoryMethod.ManageStockByAttributes) { @@ -216,7 +216,7 @@ public async Task ProductAttributeCombinationTierPriceList(DataSo translationService.GetResource($"{scope.ResourceKeyPrefix}.Catalog.Products.Permissions")); var tierPriceModel = - await productViewModelService.PrepareProductAttributeCombinationTierPricesModel(product, + await productAttributeCombinationViewModelService.PrepareProductAttributeCombinationTierPricesModel(product, productAttributeCombinationId); var gridModel = new DataSourceResult { Data = tierPriceModel, @@ -243,7 +243,7 @@ public async Task ProductAttributeCombinationTierPriceInsert(stri var combination = product.ProductAttributeCombinations.FirstOrDefault(x => x.Id == productAttributeCombinationId); if (combination != null) - await productViewModelService.InsertProductAttributeCombinationTierPricesModel(product, combination, + await productAttributeCombinationViewModelService.InsertProductAttributeCombinationTierPricesModel(product, combination, model); return new JsonResult(""); @@ -264,7 +264,7 @@ public async Task ProductAttributeCombinationTierPriceUpdate(stri var combination = product.ProductAttributeCombinations.FirstOrDefault(x => x.Id == productAttributeCombinationId); if (combination != null) - await productViewModelService.UpdateProductAttributeCombinationTierPricesModel(product, combination, + await productAttributeCombinationViewModelService.UpdateProductAttributeCombinationTierPricesModel(product, combination, model); return new JsonResult(""); @@ -289,7 +289,7 @@ public async Task ProductAttributeCombinationTierPriceDelete(stri { var tierPrice = combination.TierPrices.FirstOrDefault(x => x.Id == id); if (tierPrice != null) - await productViewModelService.DeleteProductAttributeCombinationTierPrices(product, combination, + await productAttributeCombinationViewModelService.DeleteProductAttributeCombinationTierPrices(product, combination, tierPrice); } diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductAttributeMappingController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductAttributeMappingController.cs index 852efd3ce..a99ecedb9 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductAttributeMappingController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductAttributeMappingController.cs @@ -19,7 +19,8 @@ namespace Grand.Web.AdminShared.Controllers; [PermissionAuthorize(PermissionSystemName.Products)] [AutoValidateAntiforgeryToken] public abstract class BaseProductAttributeMappingController( - IProductViewModelService productViewModelService, + IProductAddPopupViewModelService productAddPopupViewModelService, + IProductAttributeMappingViewModelService productAttributeMappingViewModelService, IProductService productService, ILanguageService languageService, IRequestTranslationService translationService, @@ -45,7 +46,7 @@ public async Task ProductAttributeMappingList(DataSourceRequest c if (!await scope.CanView(product)) return ErrorForKendoGridJson(translationService.GetResource($"{scope.ResourceKeyPrefix}.Catalog.Products.Permissions")); - var attributesModel = await productViewModelService.PrepareProductAttributeMappingModels(product); + var attributesModel = await productAttributeMappingViewModelService.PrepareProductAttributeMappingModels(product); var gridModel = new DataSourceResult { Data = attributesModel, Total = attributesModel.Count @@ -65,13 +66,13 @@ public async Task ProductAttributeMappingPopup(string productId, if (string.IsNullOrEmpty(productAttributeMappingId)) { - var model = await productViewModelService.PrepareProductAttributeMappingModel(product); + var model = await productAttributeMappingViewModelService.PrepareProductAttributeMappingModel(product); return View(model); } var productAttributeMapping = product.ProductAttributeMappings.FirstOrDefault(x => x.Id == productAttributeMappingId); - var editModel = await productViewModelService.PrepareProductAttributeMappingModel(product, + var editModel = await productAttributeMappingViewModelService.PrepareProductAttributeMappingModel(product, productAttributeMapping); return View(editModel); } @@ -93,15 +94,15 @@ public async Task ProductAttributeMappingPopup(ProductModel.Produ return Content(translationService.GetResource($"{scope.ResourceKeyPrefix}.Catalog.Products.Permissions")); if (string.IsNullOrEmpty(model.Id)) - await productViewModelService.InsertProductAttributeMappingModel(model); + await productAttributeMappingViewModelService.InsertProductAttributeMappingModel(model); else - await productViewModelService.UpdateProductAttributeMappingModel(model); + await productAttributeMappingViewModelService.UpdateProductAttributeMappingModel(model); return Content(""); } Error(ModelState); - model = await productViewModelService.PrepareProductAttributeMappingModel(model); + model = await productAttributeMappingViewModelService.PrepareProductAttributeMappingModel(model); return View(model); } @@ -142,7 +143,7 @@ public async Task ProductAttributeValidationRulesPopup(string id, if (productAttributeMapping == null) return Content("No attribute value found with the specified id"); - var model = await productViewModelService.PrepareProductAttributeMappingModel(productAttributeMapping); + var model = await productAttributeMappingViewModelService.PrepareProductAttributeMappingModel(productAttributeMapping); return View(model); } @@ -167,12 +168,12 @@ public async Task ProductAttributeValidationRulesPopup( if (ModelState.IsValid) { - await productViewModelService.UpdateProductAttributeValidationRulesModel(productAttributeMapping, model); + await productAttributeMappingViewModelService.UpdateProductAttributeValidationRulesModel(productAttributeMapping, model); return Content(""); } Error(ModelState); - model = await productViewModelService.PrepareProductAttributeMappingModel(productAttributeMapping); + model = await productAttributeMappingViewModelService.PrepareProductAttributeMappingModel(productAttributeMapping); return View(model); } @@ -197,7 +198,7 @@ public async Task ProductAttributeConditionPopup(string productId //No attribute value found with the specified id return Content("No attribute value found with the specified id"); - var model = await productViewModelService.PrepareProductAttributeConditionModel(product, + var model = await productAttributeMappingViewModelService.PrepareProductAttributeConditionModel(product, productAttributeMapping); return View(model); } @@ -232,7 +233,7 @@ public async Task ProductAttributeConditionPopup(ProductAttribute if (!await scope.HasAccess(product)) return Content(translationService.GetResource($"{scope.ResourceKeyPrefix}.Catalog.Products.Permissions")); - await productViewModelService.UpdateProductAttributeConditionModel(product, productAttributeMapping, model); + await productAttributeMappingViewModelService.UpdateProductAttributeConditionModel(product, productAttributeMapping, model); return Content(""); } @@ -289,7 +290,7 @@ public async Task ProductAttributeValueList(string productAttribu throw new ArgumentException("No product attribute mapping found with the specified id"); var values = - await productViewModelService.PrepareProductAttributeValueModels(product, productAttributeMapping); + await productAttributeMappingViewModelService.PrepareProductAttributeValueModels(product, productAttributeMapping); var gridModel = new DataSourceResult { Data = values, Total = values.Count @@ -317,7 +318,7 @@ public async Task ProductAttributeValueCreatePopup(string product throw new ArgumentException("No product attribute mapping found with the specified id"); var model = - await productViewModelService.PrepareProductAttributeValueModel(product, productAttributeMapping); + await productAttributeMappingViewModelService.PrepareProductAttributeValueModel(product, productAttributeMapping); //locales await AddLocales(languageService, model.Locales); @@ -358,12 +359,12 @@ public async Task ProductAttributeValueCreatePopup(ProductModel.P if (ModelState.IsValid) { - await productViewModelService.InsertProductAttributeValueModel(model); + await productAttributeMappingViewModelService.InsertProductAttributeValueModel(model); return Content(""); } //If we got this far, something failed, redisplay form - await productViewModelService.PrepareProductAttributeValueModel(product, model); + await productAttributeMappingViewModelService.PrepareProductAttributeValueModel(product, model); return View(model); } @@ -387,14 +388,14 @@ public async Task ProductAttributeValueEditPopup(string id, strin //No attribute value found with the specified id return RedirectToAction("List", "Product"); - var model = await productViewModelService.PrepareProductAttributeValueModel(pa, pav); + var model = await productAttributeMappingViewModelService.PrepareProductAttributeValueModel(pa, pav); //locales await AddLocales(languageService, model.Locales, (locale, languageId) => { locale.Name = pav.GetTranslation(x => x.Name, languageId, false); }); //pictures - await productViewModelService.PrepareProductAttributeValueModel(product, model); + await productAttributeMappingViewModelService.PrepareProductAttributeValueModel(product, model); return View(model); } @@ -424,12 +425,12 @@ public async Task ProductAttributeValueEditPopup(string productId if (ModelState.IsValid) { - await productViewModelService.UpdateProductAttributeValueModel(pav, model); + await productAttributeMappingViewModelService.UpdateProductAttributeValueModel(pav, model); return Content(""); } //If we got this far, something failed, redisplay form - await productViewModelService.PrepareProductAttributeValueModel(product, model); + await productAttributeMappingViewModelService.PrepareProductAttributeValueModel(product, model); return View(model); } @@ -470,7 +471,7 @@ public async Task AssociateProductToAttributeValuePopup() // PrepareAssociateProductToAttributeValueModel() now reads scope internally (Task 9): // StaffStoreId for Store; null for Admin/Vendor (no store concept). var model = - await productViewModelService.PrepareAssociateProductToAttributeValueModel(); + await productAddPopupViewModelService.PrepareAssociateProductToAttributeValueModel(); return View(model); } @@ -482,7 +483,7 @@ public async Task AssociateProductToAttributeValuePopupList(DataS if (scope.DefaultStoreId is not null) model.SearchStoreId = scope.DefaultStoreId; var (products, totalCount) = - await productViewModelService.PrepareProductModel(model, command.Page, command.PageSize); + await productAddPopupViewModelService.PrepareProductModel(model, command.Page, command.PageSize); var gridModel = new DataSourceResult { Data = products.ToList(), Total = totalCount diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductBidController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductBidController.cs index 73a7a5b3c..e097239c1 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductBidController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductBidController.cs @@ -16,7 +16,7 @@ namespace Grand.Web.AdminShared.Controllers; [PermissionAuthorize(PermissionSystemName.Products)] [AutoValidateAntiforgeryToken] public abstract class BaseProductBidController( - IProductViewModelService productViewModelService, + IProductBidViewModelService productBidViewModelService, IProductService productService, IRequestTranslationService translationService, IAuctionService auctionService, @@ -46,7 +46,7 @@ public async Task ListBids(DataSourceRequest command, string prod return Json(new DataSourceResult { Data = Array.Empty(), Total = 0 }); var (bidModels, totalCount) = - await productViewModelService.PrepareBidMode(productId, command.Page, command.PageSize); + await productBidViewModelService.PrepareBidMode(productId, command.Page, command.PageSize); var gridModel = new DataSourceResult { Data = bidModels.ToList(), Total = totalCount diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductBundleController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductBundleController.cs index 858df800c..6f079e846 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductBundleController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductBundleController.cs @@ -16,7 +16,8 @@ namespace Grand.Web.AdminShared.Controllers; [PermissionAuthorize(PermissionSystemName.Products)] [AutoValidateAntiforgeryToken] public abstract class BaseProductBundleController( - IProductViewModelService productViewModelService, + IProductAddPopupViewModelService productAddPopupViewModelService, + IProductBundleViewModelService productBundleViewModelService, IProductService productService, IRequestTranslationService translationService, IAdminDataScope scope) @@ -69,7 +70,7 @@ public async Task BundleProductUpdate(ProductModel.BundleProductM if (ModelState.IsValid) { - await productViewModelService.UpdateBundleProductModel(model); + await productBundleViewModelService.UpdateBundleProductModel(model); return new JsonResult(""); } @@ -86,7 +87,7 @@ public async Task BundleProductDelete(ProductModel.BundleProductM if (ModelState.IsValid) { - await productViewModelService.DeleteBundleProductModel(model); + await productBundleViewModelService.DeleteBundleProductModel(model); return new JsonResult(""); } @@ -100,7 +101,7 @@ public async Task BundleProductAddPopup(string productId) // popup unconditionally once the Edit permission is satisfied) - only the mutating POST below // ties access to a specific product. PrepareBundleProductModel() now reads scope // internally (Task 9). - var model = await productViewModelService.PrepareBundleProductModel(); + var model = await productAddPopupViewModelService.PrepareBundleProductModel(); model.ProductId = productId; return View(model); } @@ -113,7 +114,7 @@ public async Task BundleProductAddPopupList(DataSourceRequest com if (scope.DefaultStoreId is not null) model.SearchStoreId = scope.DefaultStoreId; var (products, totalCount) = - await productViewModelService.PrepareProductModel(model, command.Page, command.PageSize); + await productAddPopupViewModelService.PrepareProductModel(model, command.Page, command.PageSize); var gridModel = new DataSourceResult { Data = products.ToList(), Total = totalCount @@ -136,7 +137,7 @@ public async Task BundleProductAddPopup(ProductModel.AddBundlePro if (ModelState.IsValid) { - if (model.SelectedProductIds != null) await productViewModelService.InsertBundleProductModel(model); + if (model.SelectedProductIds != null) await productBundleViewModelService.InsertBundleProductModel(model); return Content(""); } @@ -151,7 +152,7 @@ public async Task BundleProductAddPopup(ProductModel.AddBundlePro protected virtual async Task InvalidBundleProductAddPopupResult(ProductModel.AddBundleProductModel model) { Error(ModelState); - model = await productViewModelService.PrepareBundleProductModel(); + model = await productAddPopupViewModelService.PrepareBundleProductModel(); return View(model); } diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductCategoryController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductCategoryController.cs index 1c9eba016..592cc641a 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductCategoryController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductCategoryController.cs @@ -16,7 +16,7 @@ namespace Grand.Web.AdminShared.Controllers; [PermissionAuthorize(PermissionSystemName.Products)] [AutoValidateAntiforgeryToken] public abstract class BaseProductCategoryController( - IProductViewModelService productViewModelService, + IProductCategoryViewModelService productCategoryViewModelService, IProductService productService, IRequestTranslationService translationService, IAdminDataScope scope) @@ -47,7 +47,7 @@ public async Task ProductCategoryList(DataSourceRequest command, // narrower than "Admin-only" - don't cite it as precedent for skipping templating elsewhere. return ErrorForKendoGridJson(translationService.GetResource($"{scope.ResourceKeyPrefix}.Catalog.Products.Permissions")); - var productCategoriesModel = await productViewModelService.PrepareProductCategoryModel(product); + var productCategoriesModel = await productCategoryViewModelService.PrepareProductCategoryModel(product); var gridModel = new DataSourceResult { Data = productCategoriesModel, Total = productCategoriesModel.Count @@ -67,7 +67,7 @@ public async Task ProductCategoryInsert(ProductModel.ProductCateg if (ModelState.IsValid) try { - await productViewModelService.InsertProductCategoryModel(model); + await productCategoryViewModelService.InsertProductCategoryModel(model); return new JsonResult(""); } catch (Exception ex) @@ -89,7 +89,7 @@ public async Task ProductCategoryUpdate(ProductModel.ProductCateg if (ModelState.IsValid) try { - await productViewModelService.UpdateProductCategoryModel(model); + await productCategoryViewModelService.UpdateProductCategoryModel(model); return new JsonResult(""); } catch (Exception ex) @@ -110,7 +110,7 @@ public async Task ProductCategoryDelete(ProductModel.ProductCateg if (ModelState.IsValid) { - await productViewModelService.DeleteProductCategory(model.Id, model.ProductId); + await productCategoryViewModelService.DeleteProductCategory(model.Id, model.ProductId); return new JsonResult(""); } diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductCollectionController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductCollectionController.cs index 25de437be..084b0fc6e 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductCollectionController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductCollectionController.cs @@ -15,7 +15,7 @@ namespace Grand.Web.AdminShared.Controllers; [PermissionAuthorize(PermissionSystemName.Products)] [AutoValidateAntiforgeryToken] public abstract class BaseProductCollectionController( - IProductViewModelService productViewModelService, + IProductCollectionViewModelService productCollectionViewModelService, IProductService productService, IRequestTranslationService translationService, IAdminDataScope scope) @@ -40,7 +40,7 @@ public async Task ProductCollectionList(DataSourceRequest command if (!await scope.CanView(product)) return ErrorForKendoGridJson(translationService.GetResource($"{scope.ResourceKeyPrefix}.Catalog.Products.Permissions")); - var productCollectionsModel = await productViewModelService.PrepareProductCollectionModel(product); + var productCollectionsModel = await productCollectionViewModelService.PrepareProductCollectionModel(product); var gridModel = new DataSourceResult { Data = productCollectionsModel, Total = productCollectionsModel.Count @@ -60,7 +60,7 @@ public async Task ProductCollectionInsert(ProductModel.ProductCol if (ModelState.IsValid) try { - await productViewModelService.InsertProductCollection(model); + await productCollectionViewModelService.InsertProductCollection(model); return new JsonResult(""); } catch (Exception ex) @@ -82,7 +82,7 @@ public async Task ProductCollectionUpdate(ProductModel.ProductCol if (ModelState.IsValid) try { - await productViewModelService.UpdateProductCollection(model); + await productCollectionViewModelService.UpdateProductCollection(model); return new JsonResult(""); } catch (Exception ex) @@ -103,7 +103,7 @@ public async Task ProductCollectionDelete(ProductModel.ProductCol if (ModelState.IsValid) { - await productViewModelService.DeleteProductCollection(model.Id, model.ProductId); + await productCollectionViewModelService.DeleteProductCollection(model.Id, model.ProductId); return new JsonResult(""); } diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductController.cs index 754402b01..9ac771e03 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductController.cs @@ -29,6 +29,7 @@ namespace Grand.Web.AdminShared.Controllers; [AutoValidateAntiforgeryToken] public abstract class BaseProductController( IProductViewModelService productViewModelService, + IProductAddPopupViewModelService productAddPopupViewModelService, IProductService productService, ILanguageService languageService, IRequestTranslationService translationService, @@ -328,7 +329,7 @@ public async Task RequiredProductAddPopup(string productIdsInput) // PrepareAddRequiredProductModel() now reads scope internally (Task 9) - the injected // IAdminDataScope already encodes the per-host default exactly: null for Admin // (global) and Vendor (not store-scoped), StaffStoreId for Store. - var model = await productViewModelService.PrepareAddRequiredProductModel(); + var model = await productAddPopupViewModelService.PrepareAddRequiredProductModel(); // Unused by any of the three views (all three read productIdsInput straight off the query string // via Context.Request.Query, not ViewBag), but Admin and Vendor both set it and Store silently // drops its own parameter - kept here for parity; it is inert either way. @@ -344,7 +345,7 @@ public async Task RequiredProductAddPopupList(DataSourceRequest c if (scope.DefaultStoreId is not null) model.SearchStoreId = scope.DefaultStoreId; var (products, totalCount) = - await productViewModelService.PrepareProductModel(model, command.Page, command.PageSize); + await productAddPopupViewModelService.PrepareProductModel(model, command.Page, command.PageSize); var gridModel = new DataSourceResult { Data = products.ToList(), Total = totalCount diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductPictureController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductPictureController.cs index 6c7d9d936..2e6efd2e7 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductPictureController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductPictureController.cs @@ -24,7 +24,7 @@ namespace Grand.Web.AdminShared.Controllers; [PermissionAuthorize(PermissionSystemName.Products)] [AutoValidateAntiforgeryToken] public abstract class BaseProductPictureController( - IProductViewModelService productViewModelService, + IProductPictureViewModelService productPictureViewModelService, IProductService productService, ILanguageService languageService, IRequestTranslationService translationService, @@ -86,7 +86,7 @@ public async Task ProductPictureAdd( values.Add((pictureUrl, picture.Id)); //assign picture to the product - await productViewModelService.InsertProductPicture(product, picture, 0); + await productPictureViewModelService.InsertProductPicture(product, picture, 0); } } @@ -109,7 +109,7 @@ public async Task ProductPictureList(DataSourceRequest command, s if (!await scope.CanView(product)) return ErrorForKendoGridJson(translationService.GetResource($"{scope.ResourceKeyPrefix}.Catalog.Products.Permissions")); - var productPicturesModel = await productViewModelService.PrepareProductPicturesModel(product); + var productPicturesModel = await productPictureViewModelService.PrepareProductPicturesModel(product); var gridModel = new DataSourceResult { Data = productPicturesModel, Total = productPicturesModel.Count @@ -134,7 +134,7 @@ public async Task ProductPicturePopup(string productId, string id if (pp == null) return Content("Product picture not exist"); - var (model, picture) = await productViewModelService.PrepareProductPictureModel(product, pp); + var (model, picture) = await productPictureViewModelService.PrepareProductPictureModel(product, pp); //locales await AddLocales(languageService, model.Locales, (locale, languageId) => { @@ -165,7 +165,7 @@ public async Task ProductPicturePopup(ProductModel.ProductPicture if (product.ProductPictures.FirstOrDefault(x => x.Id == model.Id) == null) throw new ArgumentException("No product picture found with the specified id"); - await productViewModelService.UpdateProductPicture(model); + await productPictureViewModelService.UpdateProductPicture(model); return Content(""); } @@ -190,7 +190,7 @@ public async Task ProductPictureDelete(ProductModel.ProductPictur if (ModelState.IsValid) { - await productViewModelService.DeleteProductPicture(model); + await productPictureViewModelService.DeleteProductPicture(model); return new JsonResult(""); } diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductPriceController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductPriceController.cs index a0801ea91..4cb24b716 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductPriceController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductPriceController.cs @@ -19,7 +19,7 @@ namespace Grand.Web.AdminShared.Controllers; [PermissionAuthorize(PermissionSystemName.Products)] [AutoValidateAntiforgeryToken] public abstract class BaseProductPriceController( - IProductViewModelService productViewModelService, + IProductPriceViewModelService productPriceViewModelService, IProductService productService, IRequestTranslationService translationService, IDateTimeService dateTimeService, @@ -184,7 +184,7 @@ public async Task TierPriceList(DataSourceRequest command, string // PrepareTierPriceModel(product) now reads scope internally (Task 9); scope.DefaultStoreId // is null for Admin/Vendor (Global/VendorProduct scopes) and the staff store for Store. - var tierPricesModel = await productViewModelService.PrepareTierPriceModel(product); + var tierPricesModel = await productPriceViewModelService.PrepareTierPriceModel(product); var gridModel = new DataSourceResult { Data = tierPricesModel, Total = tierPricesModel.Count @@ -198,7 +198,7 @@ public async Task TierPriceCreatePopup(string productId) var model = new ProductModel.TierPriceModel { ProductId = productId }; - await productViewModelService.PrepareTierPriceModel(model); + await productPriceViewModelService.PrepareTierPriceModel(model); return View(model); } @@ -225,7 +225,7 @@ public async Task TierPriceCreatePopup(ProductModel.TierPriceMode Error(ModelState); //If we got this far, something failed, redisplay form - await productViewModelService.PrepareTierPriceModel(model); + await productPriceViewModelService.PrepareTierPriceModel(model); return View(model); } @@ -249,7 +249,7 @@ public async Task TierPriceEditPopup(string id, string productId) var model = tierPrice.ToModel(dateTimeService); model.ProductId = productId; - await productViewModelService.PrepareTierPriceModel(model); + await productPriceViewModelService.PrepareTierPriceModel(model); return View(model); } @@ -279,7 +279,7 @@ public async Task TierPriceEditPopup(string productId, ProductMod Error(ModelState); //stores - await productViewModelService.PrepareTierPriceModel(model); + await productPriceViewModelService.PrepareTierPriceModel(model); return View(model); } diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductRelatedController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductRelatedController.cs index f50e7024b..d4eb7b8fe 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductRelatedController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductRelatedController.cs @@ -16,7 +16,8 @@ namespace Grand.Web.AdminShared.Controllers; [PermissionAuthorize(PermissionSystemName.Products)] [AutoValidateAntiforgeryToken] public abstract class BaseProductRelatedController( - IProductViewModelService productViewModelService, + IProductAddPopupViewModelService productAddPopupViewModelService, + IProductRelatedViewModelService productRelatedViewModelService, IProductService productService, IRequestTranslationService translationService, IAdminDataScope scope) @@ -70,7 +71,7 @@ public async Task RelatedProductUpdate(ProductModel.RelatedProduc if (ModelState.IsValid) { - await productViewModelService.UpdateRelatedProductModel(model); + await productRelatedViewModelService.UpdateRelatedProductModel(model); return new JsonResult(""); } @@ -87,7 +88,7 @@ public async Task RelatedProductDelete(ProductModel.RelatedProduc if (ModelState.IsValid) { - await productViewModelService.DeleteRelatedProductModel(model); + await productRelatedViewModelService.DeleteRelatedProductModel(model); return new JsonResult(""); } @@ -101,7 +102,7 @@ public async Task RelatedProductAddPopup(string productId) // popup unconditionally once the Edit permission is satisfied) - only the mutating POST below // ties access to a specific product. PrepareRelatedProductModel() now reads scope // internally (Task 9). - var model = await productViewModelService.PrepareRelatedProductModel(); + var model = await productAddPopupViewModelService.PrepareRelatedProductModel(); model.ProductId = productId; return View(model); } @@ -114,7 +115,7 @@ public async Task RelatedProductAddPopupList(DataSourceRequest co if (scope.DefaultStoreId is not null) model.SearchStoreId = scope.DefaultStoreId; var (products, totalCount) = - await productViewModelService.PrepareProductModel(model, command.Page, command.PageSize); + await productAddPopupViewModelService.PrepareProductModel(model, command.Page, command.PageSize); var gridModel = new DataSourceResult { Data = products.ToList(), Total = totalCount @@ -137,7 +138,7 @@ public async Task RelatedProductAddPopup(ProductModel.AddRelatedP if (ModelState.IsValid) { - if (model.SelectedProductIds != null) await productViewModelService.InsertRelatedProductModel(model); + if (model.SelectedProductIds != null) await productRelatedViewModelService.InsertRelatedProductModel(model); return Content(""); } @@ -152,7 +153,7 @@ public async Task RelatedProductAddPopup(ProductModel.AddRelatedP protected virtual async Task InvalidRelatedProductAddPopupResult(ProductModel.AddRelatedProductModel model) { Error(ModelState); - model = await productViewModelService.PrepareRelatedProductModel(); + model = await productAddPopupViewModelService.PrepareRelatedProductModel(); return View(model); } @@ -205,7 +206,7 @@ public async Task SimilarProductUpdate(ProductModel.SimilarProduc if (ModelState.IsValid) { - await productViewModelService.UpdateSimilarProductModel(model); + await productRelatedViewModelService.UpdateSimilarProductModel(model); return new JsonResult(""); } @@ -222,7 +223,7 @@ public async Task SimilarProductDelete(ProductModel.SimilarProduc if (ModelState.IsValid) { - await productViewModelService.DeleteSimilarProductModel(model); + await productRelatedViewModelService.DeleteSimilarProductModel(model); return new JsonResult(""); } @@ -236,7 +237,7 @@ public async Task SimilarProductAddPopup(string productId) // popup unconditionally once the Edit permission is satisfied) - only the mutating POST below // ties access to a specific product. PrepareSimilarProductModel() now reads scope // internally (Task 9). - var model = await productViewModelService.PrepareSimilarProductModel(); + var model = await productAddPopupViewModelService.PrepareSimilarProductModel(); model.ProductId = productId; return View(model); } @@ -249,7 +250,7 @@ public async Task SimilarProductAddPopupList(DataSourceRequest co if (scope.DefaultStoreId is not null) model.SearchStoreId = scope.DefaultStoreId; var (products, totalCount) = - await productViewModelService.PrepareProductModel(model, command.Page, command.PageSize); + await productAddPopupViewModelService.PrepareProductModel(model, command.Page, command.PageSize); var gridModel = new DataSourceResult { Data = products.ToList(), Total = totalCount @@ -272,7 +273,7 @@ public async Task SimilarProductAddPopup(ProductModel.AddSimilarP if (ModelState.IsValid) { - if (model.SelectedProductIds != null) await productViewModelService.InsertSimilarProductModel(model); + if (model.SelectedProductIds != null) await productRelatedViewModelService.InsertSimilarProductModel(model); return Content(""); } @@ -287,7 +288,7 @@ public async Task SimilarProductAddPopup(ProductModel.AddSimilarP protected virtual async Task InvalidSimilarProductAddPopupResult(ProductModel.AddSimilarProductModel model) { Error(ModelState); - model = await productViewModelService.PrepareSimilarProductModel(); + model = await productAddPopupViewModelService.PrepareSimilarProductModel(); return View(model); } @@ -346,7 +347,7 @@ public async Task CrossSellProductDelete(ProductModel.CrossSellPr if (ModelState.IsValid) { - await productViewModelService.DeleteCrossSellProduct(product.Id, crossSellProduct); + await productRelatedViewModelService.DeleteCrossSellProduct(product.Id, crossSellProduct); return new JsonResult(""); } @@ -360,7 +361,7 @@ public async Task CrossSellProductAddPopup(string productId) // popup unconditionally once the Edit permission is satisfied) - only the mutating POST below // ties access to a specific product. PrepareCrossSellProductModel() now reads scope // internally (Task 9). - var model = await productViewModelService.PrepareCrossSellProductModel(); + var model = await productAddPopupViewModelService.PrepareCrossSellProductModel(); model.ProductId = productId; return View(model); } @@ -373,7 +374,7 @@ public async Task CrossSellProductAddPopupList(DataSourceRequest if (scope.DefaultStoreId is not null) model.SearchStoreId = scope.DefaultStoreId; var (products, totalCount) = - await productViewModelService.PrepareProductModel(model, command.Page, command.PageSize); + await productAddPopupViewModelService.PrepareProductModel(model, command.Page, command.PageSize); var gridModel = new DataSourceResult { Data = products.ToList(), Total = totalCount @@ -396,7 +397,7 @@ public async Task CrossSellProductAddPopup(ProductModel.AddCrossS if (ModelState.IsValid) { - if (model.SelectedProductIds != null) await productViewModelService.InsertCrossSellProductModel(model); + if (model.SelectedProductIds != null) await productRelatedViewModelService.InsertCrossSellProductModel(model); return Content(""); } @@ -411,7 +412,7 @@ public async Task CrossSellProductAddPopup(ProductModel.AddCrossS protected virtual async Task InvalidCrossSellProductAddPopupResult(ProductModel.AddCrossSellProductModel model) { Error(ModelState); - model = await productViewModelService.PrepareCrossSellProductModel(); + model = await productAddPopupViewModelService.PrepareCrossSellProductModel(); return View(model); } @@ -470,7 +471,7 @@ public async Task RecommendedProductDelete(ProductModel.Recommend if (ModelState.IsValid) { - await productViewModelService.DeleteRecommendedProduct(product.Id, recommendedProduct); + await productRelatedViewModelService.DeleteRecommendedProduct(product.Id, recommendedProduct); return new JsonResult(""); } @@ -484,7 +485,7 @@ public async Task RecommendedProductAddPopup(string productId) // popup unconditionally once the Edit permission is satisfied) - only the mutating POST below // ties access to a specific product. PrepareRecommendedProductModel() now reads scope // internally (Task 9). - var model = await productViewModelService.PrepareRecommendedProductModel(); + var model = await productAddPopupViewModelService.PrepareRecommendedProductModel(); model.ProductId = productId; return View(model); } @@ -497,7 +498,7 @@ public async Task RecommendedProductAddPopupList(DataSourceReques if (scope.DefaultStoreId is not null) model.SearchStoreId = scope.DefaultStoreId; var (products, totalCount) = - await productViewModelService.PrepareProductModel(model, command.Page, command.PageSize); + await productAddPopupViewModelService.PrepareProductModel(model, command.Page, command.PageSize); var gridModel = new DataSourceResult { Data = products.ToList(), Total = totalCount @@ -520,7 +521,7 @@ public async Task RecommendedProductAddPopup(ProductModel.AddReco if (ModelState.IsValid) { - if (model.SelectedProductIds != null) await productViewModelService.InsertRecommendedProductModel(model); + if (model.SelectedProductIds != null) await productRelatedViewModelService.InsertRecommendedProductModel(model); return Content(""); } @@ -535,7 +536,7 @@ public async Task RecommendedProductAddPopup(ProductModel.AddReco protected virtual async Task InvalidRecommendedProductAddPopupResult(ProductModel.AddRecommendedProductModel model) { Error(ModelState); - model = await productViewModelService.PrepareRecommendedProductModel(); + model = await productAddPopupViewModelService.PrepareRecommendedProductModel(); return View(model); } diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductSpecificationController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductSpecificationController.cs index fb2dc160e..003d43ebf 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductSpecificationController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductSpecificationController.cs @@ -18,7 +18,7 @@ namespace Grand.Web.AdminShared.Controllers; [PermissionAuthorize(PermissionSystemName.Products)] [AutoValidateAntiforgeryToken] public abstract class BaseProductSpecificationController( - IProductViewModelService productViewModelService, + IProductSpecificationViewModelService productSpecificationViewModelService, IProductService productService, IRequestTranslationService translationService, IAdminDataScope scope) @@ -59,7 +59,7 @@ public async Task ProductSpecAttrList(DataSourceRequest command, if (!await scope.CanView(product)) return ErrorForKendoGridJson(translationService.GetResource($"{scope.ResourceKeyPrefix}.Catalog.Products.Permissions")); - var productrSpecsModel = await productViewModelService.PrepareProductSpecificationAttributeModel(product); + var productrSpecsModel = await productSpecificationViewModelService.PrepareProductSpecificationAttributeModel(product); var gridModel = new DataSourceResult { Data = productrSpecsModel, Total = productrSpecsModel.Count @@ -113,9 +113,9 @@ public async Task ProductSpecAttrPopup( var psa = product.ProductSpecificationAttributes.FirstOrDefault(x => x.Id == model.Id); if (psa == null) - await productViewModelService.InsertProductSpecificationAttributeModel(model, product); + await productSpecificationViewModelService.InsertProductSpecificationAttributeModel(model, product); else - await productViewModelService.UpdateProductSpecificationAttributeModel(psa, model); + await productSpecificationViewModelService.UpdateProductSpecificationAttributeModel(psa, model); return new JsonResult(""); } @@ -162,7 +162,7 @@ public async Task ProductSpecAttrDelete(ProductSpecificationAttri if (psa == null) throw new ArgumentException("No specification attribute found with the specified id"); - await productViewModelService.DeleteProductSpecificationAttribute(product, psa); + await productSpecificationViewModelService.DeleteProductSpecificationAttribute(product, psa); return new JsonResult(""); } diff --git a/src/Web/Grand.Web.AdminShared/Interfaces/IProductAddPopupViewModelService.cs b/src/Web/Grand.Web.AdminShared/Interfaces/IProductAddPopupViewModelService.cs new file mode 100644 index 000000000..44bf167bc --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Interfaces/IProductAddPopupViewModelService.cs @@ -0,0 +1,20 @@ +using Grand.Web.AdminShared.Models.Catalog; + +namespace Grand.Web.AdminShared.Interfaces; + +public interface IProductAddPopupViewModelService +{ + Task PrepareAddRequiredProductModel(); + Task PrepareRelatedProductModel(); + Task PrepareSimilarProductModel(); + Task PrepareBundleProductModel(); + Task PrepareCrossSellProductModel(); + Task PrepareRecommendedProductModel(); + Task PrepareAssociatedProductModel(); + + Task + PrepareAssociateProductToAttributeValueModel(); + + Task<(IList products, int totalCount)> PrepareProductModel(ProductModel.AddProductModel model, + int pageIndex, int pageSize); +} diff --git a/src/Web/Grand.Web.AdminShared/Interfaces/IProductAssociatedViewModelService.cs b/src/Web/Grand.Web.AdminShared/Interfaces/IProductAssociatedViewModelService.cs new file mode 100644 index 000000000..698069c92 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Interfaces/IProductAssociatedViewModelService.cs @@ -0,0 +1,11 @@ +using Grand.Domain.Catalog; +using Grand.Web.AdminShared.Models.Catalog; + +namespace Grand.Web.AdminShared.Interfaces; + +public interface IProductAssociatedViewModelService +{ + Task InsertAssociatedProductModel(ProductModel.AddAssociatedProductModel model); + + Task DeleteAssociatedProduct(Product product); +} diff --git a/src/Web/Grand.Web.AdminShared/Interfaces/IProductAttributeCombinationViewModelService.cs b/src/Web/Grand.Web.AdminShared/Interfaces/IProductAttributeCombinationViewModelService.cs new file mode 100644 index 000000000..be0bf5194 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Interfaces/IProductAttributeCombinationViewModelService.cs @@ -0,0 +1,38 @@ +using Grand.Domain.Catalog; +using Grand.Web.AdminShared.Models.Catalog; + +namespace Grand.Web.AdminShared.Interfaces; + +public interface IProductAttributeCombinationViewModelService +{ + Task OutOfStockNotifications(Product product, ProductAttributeCombination combination, + ProductAttributeCombination prevcombination); + + Task PrepareAddProductAttributeCombinationModel(ProductAttributeCombinationModel model, Product product); + + Task> PrepareProductAttributeCombinationModel(Product product); + + Task PrepareProductAttributeCombinationModel(Product product, + string combinationId); + + Task> InsertOrUpdateProductAttributeCombinationPopup(Product product, + ProductAttributeCombinationModel model); + + Task GenerateAllAttributeCombinations(Product product); + + Task ClearAllAttributeCombinations(Product product); + + Task> + PrepareProductAttributeCombinationTierPricesModel(Product product, string productAttributeCombinationId); + + Task InsertProductAttributeCombinationTierPricesModel(Product product, + ProductAttributeCombination productAttributeCombination, + ProductModel.ProductAttributeCombinationTierPricesModel model); + + Task UpdateProductAttributeCombinationTierPricesModel(Product product, + ProductAttributeCombination productAttributeCombination, + ProductModel.ProductAttributeCombinationTierPricesModel model); + + Task DeleteProductAttributeCombinationTierPrices(Product product, + ProductAttributeCombination productAttributeCombination, ProductCombinationTierPrices tierPrice); +} diff --git a/src/Web/Grand.Web.AdminShared/Interfaces/IProductAttributeMappingViewModelService.cs b/src/Web/Grand.Web.AdminShared/Interfaces/IProductAttributeMappingViewModelService.cs new file mode 100644 index 000000000..c0d82d59d --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Interfaces/IProductAttributeMappingViewModelService.cs @@ -0,0 +1,48 @@ +using Grand.Domain.Catalog; +using Grand.Web.AdminShared.Models.Catalog; + +namespace Grand.Web.AdminShared.Interfaces; + +public interface IProductAttributeMappingViewModelService +{ + Task PrepareProductAttributeValueModel(Product product, ProductModel.ProductAttributeValueModel model); + + Task PrepareProductAttributeMappingModel(Product product); + + Task PrepareProductAttributeMappingModel( + ProductModel.ProductAttributeMappingModel model); + + Task PrepareProductAttributeMappingModel(Product product, + ProductAttributeMapping productAttributeMapping); + + Task> PrepareProductAttributeMappingModels(Product product); + + Task InsertProductAttributeMappingModel(ProductModel.ProductAttributeMappingModel model); + + Task UpdateProductAttributeMappingModel(ProductModel.ProductAttributeMappingModel model); + + Task PrepareProductAttributeMappingModel( + ProductAttributeMapping productAttributeMapping); + + Task UpdateProductAttributeValidationRulesModel(ProductAttributeMapping productAttributeMapping, + ProductModel.ProductAttributeMappingModel model); + + Task PrepareProductAttributeConditionModel(Product product, + ProductAttributeMapping productAttributeMapping); + + Task UpdateProductAttributeConditionModel(Product product, ProductAttributeMapping productAttributeMapping, + ProductAttributeConditionModel model); + + Task PrepareProductAttributeValueModel(Product product, + ProductAttributeMapping productAttributeMapping); + + Task> PrepareProductAttributeValueModels(Product product, + ProductAttributeMapping productAttributeMapping); + + Task PrepareProductAttributeValueModel(ProductAttributeMapping pa, + ProductAttributeValue pav); + + Task InsertProductAttributeValueModel(ProductModel.ProductAttributeValueModel model); + + Task UpdateProductAttributeValueModel(ProductAttributeValue pav, ProductModel.ProductAttributeValueModel model); +} diff --git a/src/Web/Grand.Web.AdminShared/Interfaces/IProductBidViewModelService.cs b/src/Web/Grand.Web.AdminShared/Interfaces/IProductBidViewModelService.cs new file mode 100644 index 000000000..9765205a9 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Interfaces/IProductBidViewModelService.cs @@ -0,0 +1,9 @@ +using Grand.Web.AdminShared.Models.Catalog; + +namespace Grand.Web.AdminShared.Interfaces; + +public interface IProductBidViewModelService +{ + Task<(IEnumerable bidModels, int totalCount)> PrepareBidMode(string productId, int pageIndex, + int pageSize); +} diff --git a/src/Web/Grand.Web.AdminShared/Interfaces/IProductBundleViewModelService.cs b/src/Web/Grand.Web.AdminShared/Interfaces/IProductBundleViewModelService.cs new file mode 100644 index 000000000..747374320 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Interfaces/IProductBundleViewModelService.cs @@ -0,0 +1,12 @@ +using Grand.Web.AdminShared.Models.Catalog; + +namespace Grand.Web.AdminShared.Interfaces; + +public interface IProductBundleViewModelService +{ + Task InsertBundleProductModel(ProductModel.AddBundleProductModel model); + + Task UpdateBundleProductModel(ProductModel.BundleProductModel model); + + Task DeleteBundleProductModel(ProductModel.BundleProductModel model); +} diff --git a/src/Web/Grand.Web.AdminShared/Interfaces/IProductCategoryViewModelService.cs b/src/Web/Grand.Web.AdminShared/Interfaces/IProductCategoryViewModelService.cs new file mode 100644 index 000000000..a6c678849 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Interfaces/IProductCategoryViewModelService.cs @@ -0,0 +1,15 @@ +using Grand.Domain.Catalog; +using Grand.Web.AdminShared.Models.Catalog; + +namespace Grand.Web.AdminShared.Interfaces; + +public interface IProductCategoryViewModelService +{ + Task> PrepareProductCategoryModel(Product product); + + Task InsertProductCategoryModel(ProductModel.ProductCategoryModel model); + + Task UpdateProductCategoryModel(ProductModel.ProductCategoryModel model); + + Task DeleteProductCategory(string id, string productId); +} diff --git a/src/Web/Grand.Web.AdminShared/Interfaces/IProductCollectionViewModelService.cs b/src/Web/Grand.Web.AdminShared/Interfaces/IProductCollectionViewModelService.cs new file mode 100644 index 000000000..f57badf35 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Interfaces/IProductCollectionViewModelService.cs @@ -0,0 +1,15 @@ +using Grand.Domain.Catalog; +using Grand.Web.AdminShared.Models.Catalog; + +namespace Grand.Web.AdminShared.Interfaces; + +public interface IProductCollectionViewModelService +{ + Task> PrepareProductCollectionModel(Product product); + + Task InsertProductCollection(ProductModel.ProductCollectionModel model); + + Task UpdateProductCollection(ProductModel.ProductCollectionModel model); + + Task DeleteProductCollection(string id, string productId); +} diff --git a/src/Web/Grand.Web.AdminShared/Interfaces/IProductPictureViewModelService.cs b/src/Web/Grand.Web.AdminShared/Interfaces/IProductPictureViewModelService.cs new file mode 100644 index 000000000..dc43d3aa4 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Interfaces/IProductPictureViewModelService.cs @@ -0,0 +1,20 @@ +using Grand.Domain.Catalog; +using Grand.Domain.Media; +using Grand.Web.AdminShared.Models.Catalog; + +namespace Grand.Web.AdminShared.Interfaces; + +public interface IProductPictureViewModelService +{ + //Pictures + Task> PrepareProductPicturesModel(Product product); + + Task<(ProductModel.ProductPictureModel model, Picture Picture)> PrepareProductPictureModel(Product product, + ProductPicture productPicture); + + Task InsertProductPicture(Product product, Picture picture, int displayOrder); + + Task UpdateProductPicture(ProductModel.ProductPictureModel model); + + Task DeleteProductPicture(ProductModel.ProductPictureModel model); +} diff --git a/src/Web/Grand.Web.AdminShared/Interfaces/IProductPriceViewModelService.cs b/src/Web/Grand.Web.AdminShared/Interfaces/IProductPriceViewModelService.cs new file mode 100644 index 000000000..22fa8c183 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Interfaces/IProductPriceViewModelService.cs @@ -0,0 +1,12 @@ +using Grand.Domain.Catalog; +using Grand.Web.AdminShared.Models.Catalog; + +namespace Grand.Web.AdminShared.Interfaces; + +public interface IProductPriceViewModelService +{ + Task PrepareTierPriceModel(ProductModel.TierPriceModel model); + + //tierprices + Task> PrepareTierPriceModel(Product product); +} diff --git a/src/Web/Grand.Web.AdminShared/Interfaces/IProductRelatedViewModelService.cs b/src/Web/Grand.Web.AdminShared/Interfaces/IProductRelatedViewModelService.cs new file mode 100644 index 000000000..045b1b5c0 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Interfaces/IProductRelatedViewModelService.cs @@ -0,0 +1,26 @@ +using Grand.Web.AdminShared.Models.Catalog; + +namespace Grand.Web.AdminShared.Interfaces; + +public interface IProductRelatedViewModelService +{ + Task InsertRelatedProductModel(ProductModel.AddRelatedProductModel model); + + Task UpdateRelatedProductModel(ProductModel.RelatedProductModel model); + + Task DeleteRelatedProductModel(ProductModel.RelatedProductModel model); + + Task InsertSimilarProductModel(ProductModel.AddSimilarProductModel model); + + Task UpdateSimilarProductModel(ProductModel.SimilarProductModel model); + + Task DeleteSimilarProductModel(ProductModel.SimilarProductModel model); + + Task InsertCrossSellProductModel(ProductModel.AddCrossSellProductModel model); + + Task DeleteCrossSellProduct(string productId, string crossSellProductId); + + Task InsertRecommendedProductModel(ProductModel.AddRecommendedProductModel model); + + Task DeleteRecommendedProduct(string productId, string recommendedProductId); +} diff --git a/src/Web/Grand.Web.AdminShared/Interfaces/IProductSpecificationViewModelService.cs b/src/Web/Grand.Web.AdminShared/Interfaces/IProductSpecificationViewModelService.cs new file mode 100644 index 000000000..77a1f4986 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Interfaces/IProductSpecificationViewModelService.cs @@ -0,0 +1,18 @@ +using Grand.Domain.Catalog; +using Grand.Web.AdminShared.Models.Catalog; + +namespace Grand.Web.AdminShared.Interfaces; + +public interface IProductSpecificationViewModelService +{ + //Product specification + Task> PrepareProductSpecificationAttributeModel(Product product); + + Task InsertProductSpecificationAttributeModel(ProductModel.AddProductSpecificationAttributeModel model, + Product product); + + Task UpdateProductSpecificationAttributeModel(ProductSpecificationAttribute psa, + ProductModel.AddProductSpecificationAttributeModel model); + + Task DeleteProductSpecificationAttribute(Product product, ProductSpecificationAttribute psa); +} diff --git a/src/Web/Grand.Web.AdminShared/Interfaces/IProductViewModelService.cs b/src/Web/Grand.Web.AdminShared/Interfaces/IProductViewModelService.cs index 06b9e8a59..3e5b79480 100644 --- a/src/Web/Grand.Web.AdminShared/Interfaces/IProductViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Interfaces/IProductViewModelService.cs @@ -1,5 +1,4 @@ using Grand.Domain.Catalog; -using Grand.Domain.Media; using Grand.Web.AdminShared.Models.Catalog; namespace Grand.Web.AdminShared.Interfaces; @@ -14,13 +13,7 @@ Task PrepareProductReviewModel(ProductReviewModel model, ProductReview productRe Task OutOfStockNotifications(Product product, int prevStockQuantity, List prevMultiWarehouseStock); - Task OutOfStockNotifications(Product product, ProductAttributeCombination combination, - ProductAttributeCombination prevcombination); - - Task PrepareAddProductAttributeCombinationModel(ProductAttributeCombinationModel model, Product product); Task SaveProductWarehouseInventory(Product product, IList model); - Task PrepareTierPriceModel(ProductModel.TierPriceModel model); - Task PrepareProductAttributeValueModel(Product product, ProductModel.ProductAttributeValueModel model); Task PrepareProductListModel(); Task<(IEnumerable productModels, int totalCount)> PrepareProductsModel(ProductListModel model, @@ -31,40 +24,6 @@ Task OutOfStockNotifications(Product product, ProductAttributeCombination combin Task UpdateProductModel(Product product, ProductModel model); Task DeleteProduct(Product product); Task DeleteSelected(IEnumerable selectedIds); - Task PrepareAddRequiredProductModel(); - - Task<(IList products, int totalCount)> PrepareProductModel(ProductModel.AddProductModel model, - int pageIndex, int pageSize); - - Task> PrepareProductCategoryModel(Product product); - Task InsertProductCategoryModel(ProductModel.ProductCategoryModel model); - Task UpdateProductCategoryModel(ProductModel.ProductCategoryModel model); - Task DeleteProductCategory(string id, string productId); - Task> PrepareProductCollectionModel(Product product); - Task InsertProductCollection(ProductModel.ProductCollectionModel model); - Task UpdateProductCollection(ProductModel.ProductCollectionModel model); - Task DeleteProductCollection(string id, string productId); - Task InsertRelatedProductModel(ProductModel.AddRelatedProductModel model); - Task UpdateRelatedProductModel(ProductModel.RelatedProductModel model); - Task DeleteRelatedProductModel(ProductModel.RelatedProductModel model); - Task InsertSimilarProductModel(ProductModel.AddSimilarProductModel model); - Task UpdateSimilarProductModel(ProductModel.SimilarProductModel model); - Task DeleteSimilarProductModel(ProductModel.SimilarProductModel model); - Task InsertBundleProductModel(ProductModel.AddBundleProductModel model); - Task UpdateBundleProductModel(ProductModel.BundleProductModel model); - Task DeleteBundleProductModel(ProductModel.BundleProductModel model); - Task InsertCrossSellProductModel(ProductModel.AddCrossSellProductModel model); - Task DeleteCrossSellProduct(string productId, string crossSellProductId); - Task InsertRecommendedProductModel(ProductModel.AddRecommendedProductModel model); - Task DeleteRecommendedProduct(string productId, string recommendedProductId); - Task InsertAssociatedProductModel(ProductModel.AddAssociatedProductModel model); - Task DeleteAssociatedProduct(Product product); - Task PrepareRelatedProductModel(); - Task PrepareSimilarProductModel(); - Task PrepareBundleProductModel(); - Task PrepareCrossSellProductModel(); - Task PrepareRecommendedProductModel(); - Task PrepareAssociatedProductModel(); Task PrepareBulkEditListModel(); Task<(IEnumerable bulkEditProductModels, int totalCount)> PrepareBulkEditProductModel( @@ -75,96 +34,4 @@ Task OutOfStockNotifications(Product product, ProductAttributeCombination combin Task InsertBulkEdit(IEnumerable products); Task DeleteBulkEdit(IEnumerable products); - - //tierprices - Task> PrepareTierPriceModel(Product product); - - Task<(IEnumerable bidModels, int totalCount)> PrepareBidMode(string productId, int pageIndex, - int pageSize); - - Task PrepareProductAttributeMappingModel(Product product); - - Task PrepareProductAttributeMappingModel( - ProductModel.ProductAttributeMappingModel model); - - Task PrepareProductAttributeMappingModel(Product product, - ProductAttributeMapping productAttributeMapping); - - Task> PrepareProductAttributeMappingModels(Product product); - Task InsertProductAttributeMappingModel(ProductModel.ProductAttributeMappingModel model); - Task UpdateProductAttributeMappingModel(ProductModel.ProductAttributeMappingModel model); - - Task PrepareProductAttributeMappingModel( - ProductAttributeMapping productAttributeMapping); - - Task UpdateProductAttributeValidationRulesModel(ProductAttributeMapping productAttributeMapping, - ProductModel.ProductAttributeMappingModel model); - - Task PrepareProductAttributeConditionModel(Product product, - ProductAttributeMapping productAttributeMapping); - - Task UpdateProductAttributeConditionModel(Product product, ProductAttributeMapping productAttributeMapping, - ProductAttributeConditionModel model); - - Task PrepareProductAttributeValueModel(Product product, - ProductAttributeMapping productAttributeMapping); - - Task> PrepareProductAttributeValueModels(Product product, - ProductAttributeMapping productAttributeMapping); - - Task PrepareProductAttributeValueModel(ProductAttributeMapping pa, - ProductAttributeValue pav); - - Task InsertProductAttributeValueModel(ProductModel.ProductAttributeValueModel model); - Task UpdateProductAttributeValueModel(ProductAttributeValue pav, ProductModel.ProductAttributeValueModel model); - - Task - PrepareAssociateProductToAttributeValueModel(); - - Task> PrepareProductAttributeCombinationModel(Product product); - - Task PrepareProductAttributeCombinationModel(Product product, - string combinationId); - - Task> InsertOrUpdateProductAttributeCombinationPopup(Product product, - ProductAttributeCombinationModel model); - - Task GenerateAllAttributeCombinations(Product product); - - Task ClearAllAttributeCombinations(Product product); - - Task> - PrepareProductAttributeCombinationTierPricesModel(Product product, string productAttributeCombinationId); - - Task InsertProductAttributeCombinationTierPricesModel(Product product, - ProductAttributeCombination productAttributeCombination, - ProductModel.ProductAttributeCombinationTierPricesModel model); - - Task UpdateProductAttributeCombinationTierPricesModel(Product product, - ProductAttributeCombination productAttributeCombination, - ProductModel.ProductAttributeCombinationTierPricesModel model); - - Task DeleteProductAttributeCombinationTierPrices(Product product, - ProductAttributeCombination productAttributeCombination, ProductCombinationTierPrices tierPrice); - - //Pictures - Task> PrepareProductPicturesModel(Product product); - - Task<(ProductModel.ProductPictureModel model, Picture Picture)> PrepareProductPictureModel(Product product, - ProductPicture productPicture); - - Task InsertProductPicture(Product product, Picture picture, int displayOrder); - Task UpdateProductPicture(ProductModel.ProductPictureModel model); - Task DeleteProductPicture(ProductModel.ProductPictureModel model); - - //Product specification - Task> PrepareProductSpecificationAttributeModel(Product product); - - Task InsertProductSpecificationAttributeModel(ProductModel.AddProductSpecificationAttributeModel model, - Product product); - - Task UpdateProductSpecificationAttributeModel(ProductSpecificationAttribute psa, - ProductModel.AddProductSpecificationAttributeModel model); - - Task DeleteProductSpecificationAttribute(Product product, ProductSpecificationAttribute psa); } \ No newline at end of file diff --git a/src/Web/Grand.Web.AdminShared/Services/ProductAddPopupViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/ProductAddPopupViewModelService.cs new file mode 100644 index 000000000..161604a17 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Services/ProductAddPopupViewModelService.cs @@ -0,0 +1,120 @@ +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Domain.Catalog; +using Grand.Web.AdminShared.Extensions.Mapping; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Catalog; +using Grand.Web.Common.Extensions; +using Grand.Web.Common.Localization; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace Grand.Web.AdminShared.Services; + +public class ProductAddPopupViewModelService( + IProductService productService, + IDateTimeService dateTimeService, + IVendorService vendorService, + IStoreService storeService, + IRequestTranslationService translationService, + IEnumTranslationService enumTranslationService, + IAdminDataScope scope) + : IProductAddPopupViewModelService +{ + public virtual async Task PrepareAddRequiredProductModel() + { + var model = await PrepareAddProductModel(); + return model; + } + + public virtual async Task PrepareRelatedProductModel() + { + var model = await PrepareAddProductModel(); + return model; + } + + public virtual async Task PrepareSimilarProductModel() + { + var model = await PrepareAddProductModel(); + return model; + } + + public virtual async Task PrepareBundleProductModel() + { + var model = await PrepareAddProductModel(); + return model; + } + + public virtual async Task PrepareCrossSellProductModel() + { + var model = await PrepareAddProductModel(); + return model; + } + + public virtual async Task PrepareRecommendedProductModel() + { + var model = await PrepareAddProductModel(); + return model; + } + + public virtual async Task PrepareAssociatedProductModel() + { + var model = await PrepareAddProductModel(); + return model; + } + + public virtual async Task PrepareAssociateProductToAttributeValueModel() + { + var model = await PrepareAddProductModel(); + return model; + } + + public virtual async Task<(IList products, int totalCount)> PrepareProductModel( + ProductModel.AddProductModel model, int pageIndex, int pageSize) + { + // vendorId: scope.DefaultVendorId (when set) overrides the search model - mirrors Vendor's original + // PrepareProductModel(AddProductModel,...), which always passed vendorId: CurrentVendor.Id (and + // storeId: string.Empty) regardless of the model, so the "add related/similar/bundle/etc. product" + // popups can never surface another vendor's products. + var products = await productService.PrepareProductList(model.SearchCategoryId, model.SearchBrandId, + model.SearchCollectionId, model.SearchStoreId, scope.DefaultVendorId ?? model.SearchVendorId, model.SearchProductTypeId, + model.SearchProductName, pageIndex, pageSize); + return (products.Select(x => x.ToModel(dateTimeService)).ToList(), products.TotalCount); + } + + protected virtual async Task PrepareAddProductModel() where T : ProductModel.AddProductModel, new() + { + var storeId = scope.DefaultStoreId ?? ""; + var model = new T(); + + //stores - same capability gate as PrepareProductListModel: Vendor's original PrepareAddProductModel + //never populated a stores dropdown at all (vendors don't pick stores), so this must not run for + //Vendor even though its storeId also resolves to "". + if (scope.ShowStoreSelector) + { + model.AvailableStores.Add(new SelectListItem { Text = translationService.GetResource("Admin.Common.All"), Value = " " }); + foreach (var s in (await storeService.GetAllStores()).Where(x => + x.Id == storeId || string.IsNullOrWhiteSpace(storeId))) + model.AvailableStores.Add(new SelectListItem { Text = s.Shortcut, Value = s.Id }); + } + + //vendors - only when the host isn't already forced onto a single vendor (scope.DefaultVendorId): + //Vendor's original PrepareAddProductModel never populated a vendor picker at all (matches the + //ShowStoreSelector gate above for the same reason - there's nothing to pick, the search is always + //forced to the current vendor). + if (scope.DefaultVendorId is null) + { + model.AvailableVendors.Add(new SelectListItem { Text = translationService.GetResource("Admin.Common.All"), Value = " " }); + foreach (var v in await vendorService.GetAllVendors(showHidden: true)) + model.AvailableVendors.Add(new SelectListItem { Text = v.Name, Value = v.Id }); + } + + //product types + model.AvailableProductTypes = enumTranslationService.ToSelectList(ProductType.SimpleProduct, false).ToList(); + model.AvailableProductTypes.Insert(0, + new SelectListItem { Text = translationService.GetResource("Admin.Common.All"), Value = "0" }); + + return model; + } +} diff --git a/src/Web/Grand.Web.AdminShared/Services/ProductAssociatedViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/ProductAssociatedViewModelService.cs new file mode 100644 index 000000000..bed6cb0d0 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Services/ProductAssociatedViewModelService.cs @@ -0,0 +1,31 @@ +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Domain.Catalog; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Catalog; +using System.Net; + +namespace Grand.Web.AdminShared.Services; + +public class ProductAssociatedViewModelService( + IProductService productService) + : IProductAssociatedViewModelService +{ + public virtual async Task InsertAssociatedProductModel(ProductModel.AddAssociatedProductModel model) + { + foreach (var id in model.SelectedProductIds) + { + var product = await productService.GetProductById(id); + if (product != null) + { + product.ParentGroupedProductId = model.ProductId; + await productService.UpdateAssociatedProduct(product); + } + } + } + + public virtual async Task DeleteAssociatedProduct(Product product) + { + product.ParentGroupedProductId = ""; + await productService.UpdateAssociatedProduct(product); + } +} diff --git a/src/Web/Grand.Web.AdminShared/Services/ProductAttributeCombinationViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/ProductAttributeCombinationViewModelService.cs new file mode 100644 index 000000000..730b9cc00 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Services/ProductAttributeCombinationViewModelService.cs @@ -0,0 +1,548 @@ +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Interfaces.Catalog.Tax; +using Grand.Business.Core.Interfaces.Checkout.Shipping; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Business.Core.Interfaces.Storage; +using Grand.Domain.Catalog; +using Grand.Domain.Common; +using Grand.Domain.Directory; +using Grand.Infrastructure; +using Grand.SharedKernel.Extensions; +using Grand.Web.AdminShared.Extensions.Mapping; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Catalog; +using Grand.Web.Common.Extensions; +using Grand.Web.Common.Localization; +using System.Net; +using ProductExtensions = Grand.Domain.Catalog.ProductExtensions; + +namespace Grand.Web.AdminShared.Services; + +public class ProductAttributeCombinationViewModelService( + IInventoryManageService inventoryManageService, + IPictureService pictureService, + IProductAttributeService productAttributeService, + ICurrencyService currencyService, + IRequestTranslationService translationService, + IContextAccessor contextAccessor, + IGroupService groupService, + IWarehouseService warehouseService, + IStoreService storeService, + IOutOfStockSubscriptionService outOfStockSubscriptionService, + IProductAttributeFormatter productAttributeFormatter, + CurrencySettings currencySettings) + : IProductAttributeCombinationViewModelService +{ + public virtual async Task PrepareAddProductAttributeCombinationModel(ProductAttributeCombinationModel model, + Product product) + { + ArgumentNullException.ThrowIfNull(model); + ArgumentNullException.ThrowIfNull(product); + + if (product.UseMultipleWarehouses) model.UseMultipleWarehouses = product.UseMultipleWarehouses; + if (string.IsNullOrEmpty(model.Id)) + { + model.ProductId = product.Id; + var attributes = product.ProductAttributeMappings + .Where(x => !x.IsNonCombinable()) + .ToList(); + foreach (var attribute in attributes) + { + var productAttribute = + await productAttributeService.GetProductAttributeById(attribute.ProductAttributeId); + var attributeModel = new ProductAttributeCombinationModel.ProductAttributeModel { + Id = attribute.Id, + ProductAttributeId = attribute.ProductAttributeId, + Name = productAttribute.Name, + TextPrompt = attribute.TextPrompt, + IsRequired = attribute.IsRequired, + AttributeControlType = attribute.AttributeControlTypeId + }; + + if (attribute.ShouldHaveValues()) + { + //values + var attributeValues = attribute.ProductAttributeValues; + foreach (var attributeValue in attributeValues) + { + var attributeValueModel = new ProductAttributeCombinationModel.ProductAttributeValueModel { + Id = attributeValue.Id, + Name = attributeValue.Name, + IsPreSelected = attributeValue.IsPreSelected + }; + attributeModel.Values.Add(attributeValueModel); + } + } + + model.ProductAttributes.Add(attributeModel); + } + } + + + if (!string.IsNullOrEmpty(model.PictureId)) + { + var pictureThumbnailUrl = await pictureService.GetPictureUrl(model.PictureId, 100, false); + model.PictureThumbnailUrl = pictureThumbnailUrl; + } + + foreach (var picture in product.ProductPictures) + model.ProductPictureModels.Add(new ProductModel.ProductPictureModel { + Id = picture.Id, + ProductId = product.Id, + PictureId = picture.PictureId, + PictureUrl = await pictureService.GetPictureUrl(picture.PictureId), + DisplayOrder = picture.DisplayOrder, + IsDefault = picture.IsDefault + }); + model.PrimaryStoreCurrencyCode = + (await currencyService.GetCurrencyById(currencySettings.PrimaryStoreCurrencyId))?.CurrencyCode; + } + + public virtual async Task OutOfStockNotifications(Product product, ProductAttributeCombination combination, + ProductAttributeCombination prevcombination) + { + if (product.ManageInventoryMethodId == ManageInventoryMethod.ManageStockByAttributes && + product.BackorderModeId == BackorderMode.NoBackorders && + product.AllowOutOfStockSubscriptions && + combination.StockQuantity > 0 && + prevcombination.StockQuantity <= 0 && !product.UseMultipleWarehouses && + product.Published) + await outOfStockSubscriptionService.SendNotificationsToSubscribers(product, combination.Attributes, ""); + if (product.ManageInventoryMethodId == ManageInventoryMethod.ManageStockByAttributes && + product.BackorderModeId == BackorderMode.NoBackorders && + product.AllowOutOfStockSubscriptions && + product.UseMultipleWarehouses && + product.Published) + { + foreach (var prevstock in prevcombination.WarehouseInventory) + if (prevstock.StockQuantity - prevstock.ReservedQuantity <= 0) + { + var actualStock = + combination.WarehouseInventory.FirstOrDefault(x => x.WarehouseId == prevstock.WarehouseId); + if (actualStock != null) + if (actualStock.StockQuantity - actualStock.ReservedQuantity > 0) + await outOfStockSubscriptionService.SendNotificationsToSubscribers(product, + combination.Attributes, prevstock.WarehouseId); + } + + if (combination.WarehouseInventory.Sum(x => x.StockQuantity - x.ReservedQuantity) > 0) + if (prevcombination.WarehouseInventory.Sum(x => x.StockQuantity - x.ReservedQuantity) <= 0) + await outOfStockSubscriptionService.SendNotificationsToSubscribers(product, combination.Attributes, + ""); + } + } + + public virtual async Task> + PrepareProductAttributeCombinationModel(Product product) + { + var items = new List(); + foreach (var x in product.ProductAttributeCombinations) + { + var attributes = await productAttributeFormatter.FormatAttributes(product, x.Attributes, + contextAccessor.CalculationContext(), contextAccessor.WorkContext.WorkingLanguage, + contextAccessor.StoreContext.CurrentHost, "
", true, true, true, false, true, true); + var pacModel = new ProductModel.ProductAttributeCombinationModel { + Id = x.Id, + ProductId = product.Id, + Attributes = string.IsNullOrEmpty(attributes) ? "(null)" : attributes, + StockQuantity = product.UseMultipleWarehouses + ? x.WarehouseInventory.Sum(y => y.StockQuantity - y.ReservedQuantity) + : x.StockQuantity, + AllowOutOfStockOrders = x.AllowOutOfStockOrders, + Sku = x.Sku, + Mpn = x.Mpn, + Gtin = x.Gtin, + OverriddenPrice = x.OverriddenPrice, + NotifyAdminForQuantityBelow = x.NotifyAdminForQuantityBelow + }; + items.Add(pacModel); + } + + return items; + } + + public virtual async Task PrepareProductAttributeCombinationModel(Product product, + string combinationId) + { + var model = new ProductAttributeCombinationModel(); + var wim = new List(); + foreach (var warehouse in await warehouseService.GetAllWarehouses()) + { + var pwiModel = new ProductAttributeCombinationModel.WarehouseInventoryModel { + WarehouseId = warehouse.Id, + WarehouseName = warehouse.Name + }; + wim.Add(pwiModel); + } + + if (product.UseMultipleWarehouses) + { + model.UseMultipleWarehouses = product.UseMultipleWarehouses; + model.WarehouseInventoryModels = wim; + } + + if (!string.IsNullOrEmpty(combinationId)) + { + var combination = product.ProductAttributeCombinations.FirstOrDefault(x => x.Id == combinationId); + if (combination != null) + { + model = combination.ToModel(); + model.UseMultipleWarehouses = product.UseMultipleWarehouses; + model.WarehouseInventoryModels = wim; + model.ProductId = product.Id; + model.Attributes = await productAttributeFormatter.FormatAttributes(product, combination.Attributes, + contextAccessor.CalculationContext(), contextAccessor.WorkContext.WorkingLanguage, + contextAccessor.StoreContext.CurrentHost, "
", true, true, true, false); + if (model.UseMultipleWarehouses) + foreach (var _winv in combination.WarehouseInventory) + { + var warehouseInventoryModel = + model.WarehouseInventoryModels.FirstOrDefault(x => x.WarehouseId == _winv.WarehouseId); + if (warehouseInventoryModel != null) + { + warehouseInventoryModel.WarehouseUsed = true; + warehouseInventoryModel.Id = _winv.Id; + warehouseInventoryModel.StockQuantity = _winv.StockQuantity; + warehouseInventoryModel.ReservedQuantity = _winv.ReservedQuantity; + } + } + } + } + + return model; + } + + public virtual async Task> InsertOrUpdateProductAttributeCombinationPopup(Product product, + ProductAttributeCombinationModel model) + { + var customAttributes = new List(); + var warnings = new List(); + + async Task PrepareCombinationWarehouseInventory(ProductAttributeCombination combination) + { + var warehouses = await warehouseService.GetAllWarehouses(); + + foreach (var warehouse in warehouses) + { + var whim = model.WarehouseInventoryModels.FirstOrDefault(x => x.WarehouseId == warehouse.Id); + var existingPwI = combination.WarehouseInventory.FirstOrDefault(x => x.WarehouseId == warehouse.Id); + if (existingPwI != null) + { + if (whim is { WarehouseUsed: true }) + { + //update + existingPwI.StockQuantity = whim.StockQuantity; + existingPwI.ReservedQuantity = whim.ReservedQuantity; + } + else + { + //delete + combination.WarehouseInventory.Remove(existingPwI); + } + } + else + { + if (whim is { WarehouseUsed: true }) + { + //no need to insert a record for qty 0 + existingPwI = new ProductCombinationWarehouseInventory { + WarehouseId = whim.WarehouseId, + StockQuantity = whim.StockQuantity, + ReservedQuantity = whim.ReservedQuantity + }; + combination.WarehouseInventory.Add(existingPwI); + } + } + } + } + + if (string.IsNullOrEmpty(model.Id)) + { + #region Product attributes + + var attributes = product.ProductAttributeMappings + .Where(x => !x.IsNonCombinable()) + .ToList(); + if (attributes.Count == 0) + { + warnings.Add("This combination attributes is empty!"); + return warnings; + } + + foreach (var attribute in attributes) + switch (attribute.AttributeControlTypeId) + { + case AttributeControlType.DropdownList: + case AttributeControlType.RadioList: + case AttributeControlType.ColorSquares: + case AttributeControlType.ImageSquares: + { + var ctrlAttributes = model.SelectedAttributes.FirstOrDefault(x => x.Key == attribute.Id)?.Value; + if (!string.IsNullOrEmpty(ctrlAttributes)) + customAttributes = ProductExtensions.AddProductAttribute(customAttributes, + attribute, ctrlAttributes).ToList(); + } + break; + case AttributeControlType.Checkboxes: + { + var cblAttributes = model.SelectedAttributes.FirstOrDefault(x => x.Key == attribute.Id)?.Value; + if (!string.IsNullOrEmpty(cblAttributes)) + foreach (var item in cblAttributes.Split([','], + StringSplitOptions.RemoveEmptyEntries)) + if (!string.IsNullOrEmpty(item)) + customAttributes = ProductExtensions.AddProductAttribute(customAttributes, + attribute, item).ToList(); + } + break; + case AttributeControlType.ReadonlyCheckboxes: + { + //load read-only (already server-side selected) values + var attributeValues = attribute.ProductAttributeValues; + foreach (var selectedAttributeId in attributeValues + .Where(v => v.IsPreSelected) + .Select(v => v.Id) + .ToList()) + customAttributes = ProductExtensions.AddProductAttribute(customAttributes, + attribute, selectedAttributeId).ToList(); + } + break; + } + + //validate conditional attributes (if specified) + foreach (var attribute in attributes) + { + var conditionMet = product.IsConditionMet(attribute, customAttributes); + if (conditionMet.HasValue && !conditionMet.Value) + customAttributes = ProductExtensions.RemoveProductAttribute(customAttributes, attribute).ToList(); + } + + if (customAttributes.Count == 0) + { + warnings.Add("This combination custom attributes is empty!"); + return warnings; + } + + foreach (var customAttribute in customAttributes) + if (string.IsNullOrEmpty(customAttribute.Value)) + { + warnings.Add("Combination custom attributes need to be selected value!"); + return warnings; + } + + #endregion + + if (product.FindProductAttributeCombination(customAttributes) != null) + warnings.Add("This combination attributes exists!"); + if (warnings.Count == 0) + { + var combination = new ProductAttributeCombination { + Attributes = customAttributes, + StockQuantity = model.StockQuantity, + ReservedQuantity = model.ReservedQuantity, + AllowOutOfStockOrders = model.AllowOutOfStockOrders, + Sku = model.Sku, + Text = model.Text, + Mpn = model.Mpn, + Gtin = model.Gtin, + OverriddenPrice = model.OverriddenPrice, + NotifyAdminForQuantityBelow = model.NotifyAdminForQuantityBelow, + PictureId = model.PictureId + }; + + if (product.UseMultipleWarehouses) + { + await PrepareCombinationWarehouseInventory(combination); + combination.StockQuantity = combination.WarehouseInventory.Sum(x => x.StockQuantity); + combination.ReservedQuantity = combination.WarehouseInventory.Sum(x => x.ReservedQuantity); + } + + await productAttributeService.InsertProductAttributeCombination(combination, product.Id); + product.ProductAttributeCombinations.Add(combination); + + if (product.ManageInventoryMethodId == ManageInventoryMethod.ManageStockByAttributes) + { + product.StockQuantity = product.ProductAttributeCombinations.Sum(x => x.StockQuantity); + product.ReservedQuantity = product.ProductAttributeCombinations.Sum(x => x.ReservedQuantity); + await inventoryManageService.UpdateStockProduct(product, false); + } + } + } + else + { + var combination = product.ProductAttributeCombinations.FirstOrDefault(x => x.Id == model.Id); + if (combination == null) + { + warnings.Add("This combination doesn't exist!"); + return warnings; + } + + var prevCombination = (ProductAttributeCombination)combination.Clone(); + + combination.StockQuantity = model.StockQuantity; + combination.ReservedQuantity = model.ReservedQuantity; + combination.AllowOutOfStockOrders = model.AllowOutOfStockOrders; + combination.Sku = model.Sku; + combination.Text = model.Text; + combination.Mpn = model.Mpn; + combination.Gtin = model.Gtin; + combination.OverriddenPrice = model.OverriddenPrice; + combination.NotifyAdminForQuantityBelow = model.NotifyAdminForQuantityBelow; + combination.PictureId = model.PictureId; + + if (product.UseMultipleWarehouses) + { + await PrepareCombinationWarehouseInventory(combination); + combination.StockQuantity = combination.WarehouseInventory.Sum(x => x.StockQuantity); + combination.ReservedQuantity = combination.WarehouseInventory.Sum(x => x.ReservedQuantity); + } + + //notification - out of stock + await OutOfStockNotifications(product, combination, prevCombination); + + //update combination + await productAttributeService.UpdateProductAttributeCombination(combination, product.Id); + + if (product.ManageInventoryMethodId == ManageInventoryMethod.ManageStockByAttributes) + { + product.StockQuantity = product.ProductAttributeCombinations.Sum(x => x.StockQuantity); + product.ReservedQuantity = product.ProductAttributeCombinations.Sum(x => x.ReservedQuantity); + await inventoryManageService.UpdateStockProduct(product, false); + } + } + + return warnings; + } + + public virtual async Task GenerateAllAttributeCombinations(Product product) + { + var allAttributesComb = product.GenerateAllCombinations(); + if (allAttributesComb == null || allAttributesComb.Count == 0) + return; + + foreach (var attributes in allAttributesComb) + { + var customAttributes = attributes.ToList(); + if (!customAttributes.Any()) + continue; + + var existingCombination = product.FindProductAttributeCombination(customAttributes); + + //already exists? + if (existingCombination != null) + continue; + + //save combination + var combination = new ProductAttributeCombination { + Attributes = customAttributes.ToList(), + StockQuantity = 0, + AllowOutOfStockOrders = false, + Sku = null, + Mpn = null, + Gtin = null, + OverriddenPrice = null, + NotifyAdminForQuantityBelow = 1 + }; + await productAttributeService.InsertProductAttributeCombination(combination, product.Id); + product.ProductAttributeCombinations.Add(combination); + } + + if (product.ManageInventoryMethodId == ManageInventoryMethod.ManageStockByAttributes) + { + product.StockQuantity = product.ProductAttributeCombinations.Sum(x => x.StockQuantity); + product.ReservedQuantity = product.ProductAttributeCombinations.Sum(x => x.ReservedQuantity); + await inventoryManageService.UpdateStockProduct(product, false); + } + } + + public virtual async Task ClearAllAttributeCombinations(Product product) + { + foreach (var combination in product.ProductAttributeCombinations) + await productAttributeService.DeleteProductAttributeCombination(combination, product.Id); + } + + public virtual async Task> + PrepareProductAttributeCombinationTierPricesModel(Product product, string productAttributeCombinationId) + { + var items = new List(); + foreach (var x in product.ProductAttributeCombinations.Where(x => x.Id == productAttributeCombinationId) + .SelectMany(x => x.TierPrices)) + { + string storeName; + if (!string.IsNullOrEmpty(x.StoreId)) + { + var store = await storeService.GetStoreById(x.StoreId); + storeName = store != null ? store.Shortcut : "Deleted"; + } + else + { + storeName = translationService.GetResource("Admin.Catalog.Products.TierPrices.Fields.Store.All"); + } + + var priceModel = new ProductModel.ProductAttributeCombinationTierPricesModel { + Id = x.Id, + CustomerGroupId = x.CustomerGroupId, + CustomerGroup = !string.IsNullOrEmpty(x.CustomerGroupId) + ? (await groupService.GetCustomerGroupById(x.CustomerGroupId)).Name + : translationService.GetResource("Admin.Catalog.Products.TierPrices.Fields.CustomerGroup.All"), + StoreId = x.StoreId, + Store = storeName, + Price = x.Price, + Quantity = x.Quantity + }; + items.Add(priceModel); + } + + return items; + } + + public virtual async Task InsertProductAttributeCombinationTierPricesModel(Product product, + ProductAttributeCombination productAttributeCombination, + ProductModel.ProductAttributeCombinationTierPricesModel model) + { + model.CustomerGroupId = !string.IsNullOrEmpty(model.CustomerGroupId) ? model.CustomerGroupId.Trim() : ""; + + model.StoreId = !string.IsNullOrEmpty(model.StoreId) ? model.StoreId.Trim() : ""; + + if (productAttributeCombination != null) + { + var pctp = new ProductCombinationTierPrices { + Price = model.Price, + Quantity = model.Quantity, + StoreId = model.StoreId, + CustomerGroupId = model.CustomerGroupId + }; + productAttributeCombination.TierPrices.Add(pctp); + await productAttributeService.UpdateProductAttributeCombination(productAttributeCombination, product.Id); + } + } + + public virtual async Task UpdateProductAttributeCombinationTierPricesModel(Product product, + ProductAttributeCombination productAttributeCombination, + ProductModel.ProductAttributeCombinationTierPricesModel model) + { + model.CustomerGroupId = !string.IsNullOrEmpty(model.CustomerGroupId) ? model.CustomerGroupId.Trim() : ""; + model.StoreId = !string.IsNullOrEmpty(model.StoreId) ? model.StoreId.Trim() : ""; + + if (productAttributeCombination != null) + { + var tierPrice = productAttributeCombination.TierPrices.FirstOrDefault(x => x.Id == model.Id); + if (tierPrice != null) + { + tierPrice.Price = model.Price; + tierPrice.Quantity = model.Quantity; + tierPrice.StoreId = model.StoreId; + tierPrice.CustomerGroupId = model.CustomerGroupId; + await productAttributeService.UpdateProductAttributeCombination(productAttributeCombination, + product.Id); + } + } + } + + public virtual async Task DeleteProductAttributeCombinationTierPrices(Product product, + ProductAttributeCombination productAttributeCombination, ProductCombinationTierPrices tierPrice) + { + productAttributeCombination.TierPrices.Remove(tierPrice); + await productAttributeService.UpdateProductAttributeCombination(productAttributeCombination, product.Id); + } +} diff --git a/src/Web/Grand.Web.AdminShared/Services/ProductAttributeMappingViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/ProductAttributeMappingViewModelService.cs new file mode 100644 index 000000000..d37fa9e7c --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Services/ProductAttributeMappingViewModelService.cs @@ -0,0 +1,559 @@ +using Grand.Business.Core.Extensions; +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Interfaces.Catalog.Tax; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Storage; +using Grand.Domain.Catalog; +using Grand.Domain.Common; +using Grand.Domain.Directory; +using Grand.Domain.Localization; +using Grand.Infrastructure; +using Grand.SharedKernel.Extensions; +using Grand.Web.AdminShared.Extensions.Mapping; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Catalog; +using Grand.Web.Common.Extensions; +using Grand.Web.Common.Localization; +using Microsoft.AspNetCore.Mvc.Rendering; +using System.Net; +using ProductExtensions = Grand.Domain.Catalog.ProductExtensions; + +namespace Grand.Web.AdminShared.Services; + +public class ProductAttributeMappingViewModelService( + IProductService productService, + IPictureService pictureService, + IProductAttributeService productAttributeService, + ICurrencyService currencyService, + IRequestTranslationService translationService, + IContextAccessor contextAccessor, + ILanguageService languageService, + CurrencySettings currencySettings, + IEnumTranslationService enumTranslationService) + : IProductAttributeMappingViewModelService +{ + public virtual async Task PrepareProductAttributeValueModel(Product product, + ProductModel.ProductAttributeValueModel model) + { + //pictures + foreach (var x in product.ProductPictures.OrderBy(x => x.DisplayOrder)) + model.ProductPictureModels.Add(new ProductModel.ProductPictureModel { + Id = x.Id, + ProductId = product.Id, + PictureId = x.PictureId, + PictureUrl = await pictureService.GetPictureUrl(x.PictureId), + DisplayOrder = x.DisplayOrder, + IsDefault = x.IsDefault + }); + + var associatedProduct = await productService.GetProductById(model.AssociatedProductId); + model.AssociatedProductName = associatedProduct != null ? associatedProduct.Name : ""; + } + + public virtual async Task PrepareProductAttributeMappingModel( + Product product) + { + var model = new ProductModel.ProductAttributeMappingModel { + ProductId = product.Id + }; + foreach (var attribute in await productAttributeService.GetAllProductAttributes(contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + model.AvailableProductAttribute.Add(new SelectListItem { + Value = attribute.Id, + Text = attribute.Name + }); + return model; + } + + public virtual async Task PrepareProductAttributeMappingModel( + Product product, ProductAttributeMapping productAttributeMapping) + { + var model = productAttributeMapping.ToModel(); + foreach (var attribute in await productAttributeService.GetAllProductAttributes(contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + model.AvailableProductAttribute.Add(new SelectListItem { + Value = attribute.Id, + Text = attribute.Name, + Selected = attribute.Id == model.ProductAttributeId + }); + return model; + } + + public virtual async Task PrepareProductAttributeMappingModel( + ProductModel.ProductAttributeMappingModel model) + { + foreach (var attribute in await productAttributeService.GetAllProductAttributes(contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + model.AvailableProductAttribute.Add(new SelectListItem { + Value = attribute.Id, + Text = attribute.Name + }); + return model; + } + + public virtual async Task> PrepareProductAttributeMappingModels( + Product product) + { + var items = new List(); + foreach (var x in product.ProductAttributeMappings.OrderBy(x => x.DisplayOrder)) + { + var attributeModel = new ProductModel.ProductAttributeMappingModel { + Id = x.Id, + ProductId = product.Id, + ProductAttribute = (await productAttributeService.GetProductAttributeById(x.ProductAttributeId))?.Name, + ProductAttributeId = x.ProductAttributeId, + TextPrompt = x.TextPrompt, + IsRequired = x.IsRequired, + ShowOnCatalogPage = x.ShowOnCatalogPage, + AttributeControlType = enumTranslationService.GetTranslationEnum(x.AttributeControlTypeId), + AttributeControlTypeId = x.AttributeControlTypeId, + DisplayOrder = x.DisplayOrder, + Combination = x.Combination + }; + + + if (x.ShouldHaveValues()) + { + attributeModel.ShouldHaveValues = true; + attributeModel.TotalValues = x.ProductAttributeValues.Count; + } + + if (x.ValidationRulesAllowed()) + { + var validationRules = new StringBuilder(string.Empty); + attributeModel.ValidationRulesAllowed = true; + if (x.ValidationMinLength != null) + validationRules.AppendFormat("{0}: {1}
", + translationService.GetResource( + "Admin.Catalog.Products.ProductAttributes.Attributes.ValidationRules.MinLength"), + x.ValidationMinLength); + if (x.ValidationMaxLength != null) + validationRules.AppendFormat("{0}: {1}
", + translationService.GetResource( + "Admin.Catalog.Products.ProductAttributes.Attributes.ValidationRules.MaxLength"), + x.ValidationMaxLength); + if (!string.IsNullOrEmpty(x.ValidationFileAllowedExtensions)) + validationRules.AppendFormat("{0}: {1}
", + translationService.GetResource( + "Admin.Catalog.Products.ProductAttributes.Attributes.ValidationRules.FileAllowedExtensions"), + WebUtility.HtmlEncode(x.ValidationFileAllowedExtensions)); + if (x.ValidationFileMaximumSize != null) + validationRules.AppendFormat("{0}: {1}
", + translationService.GetResource( + "Admin.Catalog.Products.ProductAttributes.Attributes.ValidationRules.FileMaximumSize"), + x.ValidationFileMaximumSize); + if (!string.IsNullOrEmpty(x.DefaultValue)) + validationRules.AppendFormat("{0}: {1}
", + translationService.GetResource( + "Admin.Catalog.Products.ProductAttributes.Attributes.ValidationRules.DefaultValue"), + WebUtility.HtmlEncode(x.DefaultValue)); + attributeModel.ValidationRulesString = validationRules.ToString(); + } + + //currenty any attribute can have condition. why not? + attributeModel.ConditionAllowed = true; + var conditionAttribute = product.ParseProductAttributeMappings(x.ConditionAttribute).FirstOrDefault(); + var conditionValue = product.ParseProductAttributeValues(x.ConditionAttribute).FirstOrDefault(); + if (conditionAttribute != null && conditionValue != null) + { + var productAttribute = + await productAttributeService.GetProductAttributeById(conditionAttribute.ProductAttributeId); + var _paname = productAttribute != null ? productAttribute.Name : ""; + attributeModel.ConditionString = + $"{WebUtility.HtmlEncode(_paname)}: {WebUtility.HtmlEncode(conditionValue.Name)}"; + } + else + { + attributeModel.ConditionString = string.Empty; + } + + items.Add(attributeModel); + } + + return items; + } + + public virtual async Task InsertProductAttributeMappingModel(ProductModel.ProductAttributeMappingModel model) + { + //insert mapping + var productAttributeMapping = model.ToEntity(); + //predefined values + var predefinedValues = (await productAttributeService.GetProductAttributeById(model.ProductAttributeId)) + .PredefinedProductAttributeValues; + foreach (var predefinedValue in predefinedValues) + { + var pav = predefinedValue.ToEntity(); + //locales + pav.Locales.Clear(); + var languages = await languageService.GetAllLanguages(true); + //localization + foreach (var lang in languages) + { + var name = predefinedValue.GetTranslation(x => x.Name, lang.Id, false); + if (!string.IsNullOrEmpty(name)) + pav.Locales.Add(new TranslationEntity { LanguageId = lang.Id, LocaleKey = "Name", LocaleValue = name }); + } + + productAttributeMapping.ProductAttributeValues.Add(pav); + } + + await productAttributeService.InsertProductAttributeMapping(productAttributeMapping, model.ProductId); + } + + public virtual async Task UpdateProductAttributeMappingModel(ProductModel.ProductAttributeMappingModel model) + { + var product = await productService.GetProductById(model.ProductId, true); + if (product != null) + { + var productAttributeMapping = product.ProductAttributeMappings.FirstOrDefault(x => x.Id == model.Id); + if (productAttributeMapping != null) + { + productAttributeMapping = model.ToEntity(productAttributeMapping); + await productAttributeService.UpdateProductAttributeMapping(productAttributeMapping, model.ProductId); + } + } + } + + public virtual async Task PrepareProductAttributeMappingModel( + ProductAttributeMapping productAttributeMapping) + { + var model = new ProductModel.ProductAttributeMappingModel { + //prepare only used properties + Id = productAttributeMapping.Id, + ValidationRulesAllowed = productAttributeMapping.ValidationRulesAllowed(), + AttributeControlTypeId = productAttributeMapping.AttributeControlTypeId, + ValidationMinLength = productAttributeMapping.ValidationMinLength, + ValidationMaxLength = productAttributeMapping.ValidationMaxLength, + ValidationFileAllowedExtensions = productAttributeMapping.ValidationFileAllowedExtensions, + ValidationFileMaximumSize = productAttributeMapping.ValidationFileMaximumSize, + DefaultValue = productAttributeMapping.DefaultValue + }; + return await Task.FromResult(model); + } + + public virtual async Task UpdateProductAttributeValidationRulesModel( + ProductAttributeMapping productAttributeMapping, ProductModel.ProductAttributeMappingModel model) + { + productAttributeMapping.ValidationMinLength = model.ValidationMinLength; + productAttributeMapping.ValidationMaxLength = model.ValidationMaxLength; + productAttributeMapping.ValidationFileAllowedExtensions = model.ValidationFileAllowedExtensions; + productAttributeMapping.ValidationFileMaximumSize = model.ValidationFileMaximumSize; + productAttributeMapping.DefaultValue = model.DefaultValue; + await productAttributeService.UpdateProductAttributeMapping(productAttributeMapping, model.ProductId); + } + + public virtual async Task PrepareProductAttributeConditionModel(Product product, + ProductAttributeMapping productAttributeMapping) + { + var model = new ProductAttributeConditionModel { + ProductAttributeMappingId = productAttributeMapping.Id, + EnableCondition = productAttributeMapping.ConditionAttribute.Any(), + ProductId = product.Id + }; + //pre-select attribute and values + var selectedPva = product.ParseProductAttributeMappings(productAttributeMapping.ConditionAttribute) + .FirstOrDefault(); + + var attributes = product.ProductAttributeMappings + //ignore non-combinable attributes (should have selectable values) + .Where(x => x.CanBeUsedAsCondition()) + //ignore this attribute (it cannot depend on itself) + .Where(x => x.Id != productAttributeMapping.Id) + .ToList(); + foreach (var attribute in attributes) + { + var pam = await productAttributeService.GetProductAttributeById(attribute.ProductAttributeId); + var attributeModel = new ProductAttributeConditionModel.ProductAttributeModel { + Id = attribute.Id, + ProductAttributeId = attribute.ProductAttributeId, + Name = pam.Name, + TextPrompt = attribute.TextPrompt, + IsRequired = attribute.IsRequired, + AttributeControlType = attribute.AttributeControlTypeId + }; + + if (attribute.ShouldHaveValues()) + { + //values + var attributeValues = product.ProductAttributeMappings.FirstOrDefault(x => x.Id == attribute.Id) + ?.ProductAttributeValues; + if (attributeValues != null) + foreach (var attributeValue in attributeValues) + { + var attributeValueModel = new ProductAttributeConditionModel.ProductAttributeValueModel { + Id = attributeValue.Id, + Name = attributeValue.Name, + IsPreSelected = attributeValue.IsPreSelected + }; + attributeModel.Values.Add(attributeValueModel); + } + + //pre-select attribute and value + if (selectedPva != null && attribute.Id == selectedPva.Id) + { + //attribute + model.SelectedProductAttributeId = selectedPva.Id; + + //values + switch (attribute.AttributeControlTypeId) + { + case AttributeControlType.DropdownList: + case AttributeControlType.RadioList: + case AttributeControlType.Checkboxes: + case AttributeControlType.ColorSquares: + case AttributeControlType.ImageSquares: + { + if (productAttributeMapping.ConditionAttribute.Any()) + { + //clear default selection + foreach (var item in attributeModel.Values) + item.IsPreSelected = false; + + //select new values + var selectedValues = + product.ParseProductAttributeValues(productAttributeMapping.ConditionAttribute); + foreach (var attributeValue in selectedValues) + foreach (var item in attributeModel.Values) + if (attributeValue.Id == item.Id) + item.IsPreSelected = true; + } + } + break; + case AttributeControlType.ReadonlyCheckboxes: + case AttributeControlType.TextBox: + case AttributeControlType.MultilineTextbox: + case AttributeControlType.Datepicker: + case AttributeControlType.FileUpload: + default: + //these attribute types are supported as conditions + break; + } + } + } + + model.ProductAttributes.Add(attributeModel); + } + + return model; + } + + public virtual async Task UpdateProductAttributeConditionModel(Product product, + ProductAttributeMapping productAttributeMapping, ProductAttributeConditionModel model) + { + var customAttributes = new List(); + if (model.EnableCondition) + { + var attribute = + product.ProductAttributeMappings.FirstOrDefault(x => x.Id == model.SelectedProductAttributeId); + if (attribute != null) + switch (attribute.AttributeControlTypeId) + { + case AttributeControlType.DropdownList: + case AttributeControlType.RadioList: + case AttributeControlType.ColorSquares: + case AttributeControlType.ImageSquares: + { + var ctrlAttributes = model.SelectedAttributes.FirstOrDefault(x => x.Key == attribute.Id)?.Value; + if (!string.IsNullOrEmpty(ctrlAttributes)) + customAttributes = ProductExtensions.AddProductAttribute(customAttributes, + attribute, ctrlAttributes).ToList(); + else + customAttributes = ProductExtensions.AddProductAttribute(customAttributes, + attribute, "").ToList(); + } + break; + case AttributeControlType.Checkboxes: + { + var cblAttributes = model.SelectedAttributes.FirstOrDefault(x => x.Key == attribute.Id)?.Value; + if (!string.IsNullOrEmpty(cblAttributes)) + { + var anyValueSelected = false; + foreach (var item in cblAttributes.Split([','], + StringSplitOptions.RemoveEmptyEntries)) + if (!string.IsNullOrEmpty(item)) + { + customAttributes = ProductExtensions.AddProductAttribute(customAttributes, + attribute, item).ToList(); + anyValueSelected = true; + } + + if (!anyValueSelected) + customAttributes = ProductExtensions.AddProductAttribute(customAttributes, + attribute, "").ToList(); + } + else + { + customAttributes = ProductExtensions.AddProductAttribute(customAttributes, + attribute, "").ToList(); + } + } + break; + case AttributeControlType.ReadonlyCheckboxes: + case AttributeControlType.TextBox: + case AttributeControlType.MultilineTextbox: + case AttributeControlType.Datepicker: + case AttributeControlType.FileUpload: + default: + //these attribute types are supported as conditions + break; + } + } + + productAttributeMapping.ConditionAttribute = customAttributes; + await productAttributeService.UpdateProductAttributeMapping(productAttributeMapping, model.ProductId); + } + + public virtual async Task PrepareProductAttributeValueModel( + Product product, ProductAttributeMapping productAttributeMapping) + { + var model = new ProductModel.ProductAttributeValueModel { + ProductAttributeMappingId = productAttributeMapping.Id, + ProductId = product.Id, + + //color squares + DisplayColorSquaresRgb = + productAttributeMapping.AttributeControlTypeId == AttributeControlType.ColorSquares, + ColorSquaresRgb = "#000000", + //image squares + DisplayImageSquaresPicture = + productAttributeMapping.AttributeControlTypeId == AttributeControlType.ImageSquares, + PrimaryStoreCurrencyCode = (await currencyService.GetCurrencyById(currencySettings.PrimaryStoreCurrencyId)) + ?.CurrencyCode, + //default qantity for associated product + Quantity = 1 + }; + + //pictures + foreach (var x in product.ProductPictures) + model.ProductPictureModels.Add(new ProductModel.ProductPictureModel { + Id = x.Id, + ProductId = product.Id, + PictureId = x.PictureId, + PictureUrl = await pictureService.GetPictureUrl(x.PictureId), + DisplayOrder = x.DisplayOrder, + IsDefault = x.IsDefault + }); + return model; + } + + public virtual async Task> PrepareProductAttributeValueModels( + Product product, ProductAttributeMapping productAttributeMapping) + { + var items = new List(); + foreach (var x in productAttributeMapping.ProductAttributeValues.OrderBy(x => x.DisplayOrder)) + { + Product associatedProduct = null; + if (x.AttributeValueTypeId == AttributeValueType.AssociatedToProduct) + associatedProduct = await productService.GetProductById(x.AssociatedProductId); + + var pictureThumbnailUrl = + await pictureService.GetPictureUrl( + string.IsNullOrEmpty(x.PictureId) ? x.ImageSquaresPictureId : x.PictureId, 100, false); + + if (string.IsNullOrEmpty(pictureThumbnailUrl)) + pictureThumbnailUrl = await pictureService.GetPictureUrl("", 1); + + items.Add(new ProductModel.ProductAttributeValueModel { + Id = x.Id, + ProductAttributeMappingId = productAttributeMapping.Id, //TODO - check x.ProductAttributeMappingId, + AttributeValueTypeId = x.AttributeValueTypeId, + AttributeValueTypeName = enumTranslationService.GetTranslationEnum(x.AttributeValueTypeId), + AssociatedProductId = x.AssociatedProductId, + AssociatedProductName = associatedProduct != null ? associatedProduct.Name : "", + Name = productAttributeMapping.AttributeControlTypeId != AttributeControlType.ColorSquares + ? x.Name + : $"{x.Name} - {x.ColorSquaresRgb}", + ColorSquaresRgb = x.ColorSquaresRgb, + ImageSquaresPictureId = x.ImageSquaresPictureId, + PriceAdjustment = x.PriceAdjustment, + PriceAdjustmentStr = x.PriceAdjustment.ToString("G29"), + WeightAdjustment = x.WeightAdjustment, + WeightAdjustmentStr = x.AttributeValueTypeId == AttributeValueType.Simple + ? x.WeightAdjustment.ToString("G29") + : "", + Cost = x.Cost, + PrimaryStoreCurrencyCode = + (await currencyService.GetCurrencyById(currencySettings.PrimaryStoreCurrencyId))?.CurrencyCode, + Quantity = x.Quantity, + IsPreSelected = x.IsPreSelected, + DisplayOrder = x.DisplayOrder, + PictureId = x.PictureId, + PictureThumbnailUrl = pictureThumbnailUrl, + ProductId = product.Id + }); + } + + return items; + } + + public virtual async Task PrepareProductAttributeValueModel( + ProductAttributeMapping pa, ProductAttributeValue pav) + { + var associatedProduct = await productService.GetProductById(pav.AssociatedProductId); + + var model = new ProductModel.ProductAttributeValueModel { + ProductAttributeMappingId = pa.Id, //TODO - check pav.ProductAttributeMappingId, + AttributeValueTypeId = pav.AttributeValueTypeId, + AttributeValueTypeName = enumTranslationService.GetTranslationEnum(pav.AttributeValueTypeId), + AssociatedProductId = pav.AssociatedProductId, + AssociatedProductName = associatedProduct != null ? associatedProduct.Name : "", + Name = pav.Name, + ColorSquaresRgb = pav.ColorSquaresRgb, + DisplayColorSquaresRgb = pa.AttributeControlTypeId == AttributeControlType.ColorSquares, + ImageSquaresPictureId = pav.ImageSquaresPictureId, + DisplayImageSquaresPicture = pa.AttributeControlTypeId == AttributeControlType.ImageSquares, + PriceAdjustment = pav.PriceAdjustment, + WeightAdjustment = pav.WeightAdjustment, + Cost = pav.Cost, + PrimaryStoreCurrencyCode = (await currencyService.GetCurrencyById(currencySettings.PrimaryStoreCurrencyId)) + ?.CurrencyCode, + Quantity = pav.Quantity, + IsPreSelected = pav.IsPreSelected, + DisplayOrder = pav.DisplayOrder, + PictureId = pav.PictureId + }; + if (model.DisplayColorSquaresRgb && string.IsNullOrEmpty(model.ColorSquaresRgb)) + model.ColorSquaresRgb = "#000000"; + return model; + } + + public virtual async Task InsertProductAttributeValueModel(ProductModel.ProductAttributeValueModel model) + { + var pav = new ProductAttributeValue { + AttributeValueTypeId = model.AttributeValueTypeId, + AssociatedProductId = model.AssociatedProductId, + Name = model.Name, + ColorSquaresRgb = model.ColorSquaresRgb, + ImageSquaresPictureId = model.ImageSquaresPictureId, + PriceAdjustment = model.PriceAdjustment, + WeightAdjustment = model.WeightAdjustment, + Cost = model.Cost, + Quantity = model.Quantity, + IsPreSelected = model.IsPreSelected, + DisplayOrder = model.DisplayOrder, + PictureId = model.PictureId, + Locales = model.Locales.ToTranslationProperty() + }; + await productAttributeService.InsertProductAttributeValue(pav, model.ProductId, + model.ProductAttributeMappingId); + } + + public virtual async Task UpdateProductAttributeValueModel(ProductAttributeValue pav, + ProductModel.ProductAttributeValueModel model) + { + pav.AttributeValueTypeId = model.AttributeValueTypeId; + pav.AssociatedProductId = model.AssociatedProductId; + pav.Name = model.Name; + pav.ColorSquaresRgb = model.ColorSquaresRgb; + pav.ImageSquaresPictureId = model.ImageSquaresPictureId; + pav.PriceAdjustment = model.PriceAdjustment; + pav.WeightAdjustment = model.WeightAdjustment; + pav.Cost = model.Cost; + pav.Quantity = model.Quantity; + pav.IsPreSelected = model.IsPreSelected; + pav.DisplayOrder = model.DisplayOrder; + pav.PictureId = model.PictureId; + pav.Locales = model.Locales.ToTranslationProperty(); + + await productAttributeService.UpdateProductAttributeValue(pav, model.ProductId, + model.ProductAttributeMappingId); + } +} diff --git a/src/Web/Grand.Web.AdminShared/Services/ProductBidViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/ProductBidViewModelService.cs new file mode 100644 index 000000000..ae69eafe1 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Services/ProductBidViewModelService.cs @@ -0,0 +1,38 @@ +using Grand.Business.Core.Interfaces.Catalog.Prices; +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Catalog; +using Grand.Web.Common.Extensions; +using System.Net; + +namespace Grand.Web.AdminShared.Services; + +public class ProductBidViewModelService( + IDateTimeService dateTimeService, + IContextAccessor contextAccessor, + ICustomerService customerService, + IAuctionService auctionService, + IPriceFormatter priceFormatter) + : IProductBidViewModelService +{ + public virtual async Task<(IEnumerable bidModels, int totalCount)> PrepareBidMode( + string productId, int pageIndex, int pageSize) + { + var bids = await auctionService.GetBidsByProductId(productId, pageIndex - 1, pageSize); + var bidsModel = new List(); + foreach (var x in bids) + bidsModel.Add(new ProductModel.BidModel { + BidId = x.Id, + ProductId = x.ProductId, + Amount = priceFormatter.FormatPrice(x.Amount, contextAccessor.WorkContext.WorkingCurrency), + Date = dateTimeService.ConvertToUserTime(x.Date, DateTimeKind.Utc), + CustomerId = x.CustomerId, + Email = (await customerService.GetCustomerById(x.CustomerId))?.Email, + OrderId = x.OrderId + }); + return (bidsModel, bids.TotalCount); + } +} diff --git a/src/Web/Grand.Web.AdminShared/Services/ProductBundleViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/ProductBundleViewModelService.cs new file mode 100644 index 000000000..b762ae67e --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Services/ProductBundleViewModelService.cs @@ -0,0 +1,66 @@ +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Domain.Catalog; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Catalog; +using System.Net; + +namespace Grand.Web.AdminShared.Services; + +public class ProductBundleViewModelService( + IProductService productService, + IAdminDataScope scope) + : IProductBundleViewModelService +{ + public virtual async Task InsertBundleProductModel(ProductModel.AddBundleProductModel model) + { + var productId1 = await productService.GetProductById(model.ProductId, true); + + foreach (var id in model.SelectedProductIds) + { + var product = await productService.GetProductById(id); + // scope.HasAccess: the popup's search is already scoped, but the posted ids are not - skip any + // product the current panel (Store, Vendor) has no access to, so it cannot be bundled by id. + if (product == null || !await scope.HasAccess(product)) continue; + + var existingBundleProducts = productId1.BundleProducts; + if (model.ProductId != id) + if (!existingBundleProducts.Any(x => x.ProductId == id)) + { + var bundle = new BundleProduct { + ProductId = id, + DisplayOrder = 1, + Quantity = 1 + }; + productId1.BundleProducts.Add(bundle); + await productService.InsertBundleProduct(bundle, model.ProductId); + } + } + } + + public virtual async Task UpdateBundleProductModel(ProductModel.BundleProductModel model) + { + var product = await productService.GetProductById(model.ProductBundleId, true); + var bundleProduct = product.BundleProducts.FirstOrDefault(x => x.Id == model.Id); + if (bundleProduct == null) + throw new ArgumentException("No bundle product found with the specified id"); + + var product2 = await productService.GetProductById(bundleProduct.ProductId); + if (product2 == null) + throw new ArgumentException("No product found with the specified id"); + + bundleProduct.ProductId = model.ProductId; + bundleProduct.Quantity = model.Quantity > 0 ? model.Quantity : 1; + bundleProduct.DisplayOrder = model.DisplayOrder; + await productService.UpdateBundleProduct(bundleProduct, model.ProductBundleId); + } + + public virtual async Task DeleteBundleProductModel(ProductModel.BundleProductModel model) + { + var product = await productService.GetProductById(model.ProductBundleId, true); + var bundleProduct = product.BundleProducts.FirstOrDefault(x => x.Id == model.Id); + if (bundleProduct == null) + throw new ArgumentException("No bundle product found with the specified id"); + + await productService.DeleteBundleProduct(bundleProduct, model.ProductBundleId); + } +} diff --git a/src/Web/Grand.Web.AdminShared/Services/ProductCategoryViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/ProductCategoryViewModelService.cs new file mode 100644 index 000000000..922ee5c76 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Services/ProductCategoryViewModelService.cs @@ -0,0 +1,75 @@ +using Grand.Business.Core.Interfaces.Catalog.Categories; +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Domain.Catalog; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Catalog; +using System.Net; + +namespace Grand.Web.AdminShared.Services; + +public class ProductCategoryViewModelService( + IProductService productService, + ICategoryService categoryService, + IProductCategoryService productCategoryService) + : IProductCategoryViewModelService +{ + public virtual async Task> PrepareProductCategoryModel(Product product) + { + var productCategories = product.ProductCategories.OrderBy(x => x.DisplayOrder); + var items = new List(); + foreach (var x in productCategories) + { + var category = await categoryService.GetCategoryById(x.CategoryId); + items.Add(new ProductModel.ProductCategoryModel { + Id = x.Id, + Category = await categoryService.GetFormattedBreadCrumb(category), + ProductId = product.Id, + CategoryId = x.CategoryId, + IsFeaturedProduct = x.IsFeaturedProduct, + DisplayOrder = x.DisplayOrder + }); + } + + return items; + } + + public virtual async Task InsertProductCategoryModel(ProductModel.ProductCategoryModel model) + { + var product = await productService.GetProductById(model.ProductId, true); + if (product != null && product.ProductCategories.All(x => x.CategoryId != model.CategoryId)) + { + var productCategory = new ProductCategory { + CategoryId = model.CategoryId, + DisplayOrder = model.DisplayOrder, + IsFeaturedProduct = model.IsFeaturedProduct + }; + await productCategoryService.InsertProductCategory(productCategory, product.Id); + } + } + + public virtual async Task UpdateProductCategoryModel(ProductModel.ProductCategoryModel model) + { + var product = await productService.GetProductById(model.ProductId, true); + var productCategory = product.ProductCategories.FirstOrDefault(x => x.Id == model.Id); + if (productCategory == null) + throw new ArgumentException("No product category mapping found with the specified id"); + + if (product.ProductCategories.Any(x => x.Id != model.Id && x.CategoryId == model.CategoryId)) + throw new ArgumentException("This category is already mapped with this product"); + + productCategory.CategoryId = model.CategoryId; + productCategory.DisplayOrder = model.DisplayOrder; + productCategory.IsFeaturedProduct = model.IsFeaturedProduct; + await productCategoryService.UpdateProductCategory(productCategory, product.Id); + } + + public virtual async Task DeleteProductCategory(string id, string productId) + { + var product = await productService.GetProductById(productId, true); + var productCategory = product.ProductCategories.FirstOrDefault(x => x.Id == id); + if (productCategory == null) + throw new ArgumentException("No product category mapping found with the specified id"); + + await productCategoryService.DeleteProductCategory(productCategory, product.Id); + } +} diff --git a/src/Web/Grand.Web.AdminShared/Services/ProductCollectionViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/ProductCollectionViewModelService.cs new file mode 100644 index 000000000..acead794b --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Services/ProductCollectionViewModelService.cs @@ -0,0 +1,72 @@ +using Grand.Business.Core.Interfaces.Catalog.Collections; +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Domain.Catalog; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Catalog; +using System.Net; + +namespace Grand.Web.AdminShared.Services; + +public class ProductCollectionViewModelService( + IProductService productService, + ICollectionService collectionService, + IProductCollectionService productCollectionService) + : IProductCollectionViewModelService +{ + public virtual async Task> PrepareProductCollectionModel(Product product) + { + var items = new List(); + foreach (var x in product.ProductCollections.OrderBy(x => x.DisplayOrder)) + items.Add(new ProductModel.ProductCollectionModel { + Id = x.Id, + Collection = (await collectionService.GetCollectionById(x.CollectionId)).Name, + ProductId = product.Id, + CollectionId = x.CollectionId, + IsFeaturedProduct = x.IsFeaturedProduct, + DisplayOrder = x.DisplayOrder + }); + return items; + } + + public virtual async Task InsertProductCollection(ProductModel.ProductCollectionModel model) + { + var collectionId = model.CollectionId; + var product = await productService.GetProductById(model.ProductId, true); + if (product != null && product.ProductCollections.All(x => x.CollectionId != collectionId)) + { + var productCollection = new ProductCollection { + CollectionId = collectionId, + DisplayOrder = model.DisplayOrder, + IsFeaturedProduct = model.IsFeaturedProduct + }; + await productCollectionService.InsertProductCollection(productCollection, model.ProductId); + } + } + + public virtual async Task UpdateProductCollection(ProductModel.ProductCollectionModel model) + { + var product = await productService.GetProductById(model.ProductId, true); + var productCollection = product.ProductCollections.FirstOrDefault(x => x.Id == model.Id); + if (productCollection == null) + throw new ArgumentException("No product collection mapping found with the specified id"); + + if (product.ProductCollections.Any(x => x.Id != model.Id && x.CollectionId == model.CollectionId)) + throw new ArgumentException("This collection is already mapped with this product"); + + productCollection.CollectionId = model.CollectionId; + productCollection.DisplayOrder = model.DisplayOrder; + productCollection.IsFeaturedProduct = model.IsFeaturedProduct; + + await productCollectionService.UpdateProductCollection(productCollection, product.Id); + } + + public virtual async Task DeleteProductCollection(string id, string productId) + { + var product = await productService.GetProductById(productId, true); + var productCollection = product.ProductCollections.FirstOrDefault(x => x.Id == id); + if (productCollection == null) + throw new ArgumentException("No product collection mapping found with the specified id"); + + await productCollectionService.DeleteProductCollection(productCollection, product.Id); + } +} diff --git a/src/Web/Grand.Web.AdminShared/Services/ProductPictureViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/ProductPictureViewModelService.cs new file mode 100644 index 000000000..5075c2b63 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Services/ProductPictureViewModelService.cs @@ -0,0 +1,117 @@ +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Interfaces.Storage; +using Grand.Domain.Catalog; +using Grand.Domain.Media; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Catalog; +using Grand.Web.Common.Extensions; +using System.Net; + +namespace Grand.Web.AdminShared.Services; + +public class ProductPictureViewModelService( + IProductService productService, + IPictureService pictureService) + : IProductPictureViewModelService +{ + //Pictures + public virtual async Task> PrepareProductPicturesModel(Product product) + { + var items = new List(); + foreach (var x in product.ProductPictures.OrderBy(x => x.DisplayOrder)) + { + var picture = await pictureService.GetPictureById(x.PictureId); + var m = new ProductModel.ProductPictureModel { + Id = x.Id, + ProductId = product.Id, + PictureId = x.PictureId, + PictureUrl = picture != null ? await pictureService.GetPictureUrl(picture) : null, + AltAttribute = picture?.AltAttribute, + TitleAttribute = picture?.TitleAttribute, + DisplayOrder = x.DisplayOrder, + IsDefault = x.IsDefault, + Style = picture?.Style, + ExtraField = picture?.ExtraField + }; + items.Add(m); + } + + return items; + } + + public virtual async Task<(ProductModel.ProductPictureModel model, Picture Picture)> PrepareProductPictureModel( + Product product, ProductPicture productPicture) + { + var picture = await pictureService.GetPictureById(productPicture.PictureId); + var model = new ProductModel.ProductPictureModel { + Id = productPicture.Id, + ProductId = product.Id, + PictureId = productPicture.PictureId, + PictureUrl = picture != null ? await pictureService.GetPictureUrl(picture) : null, + AltAttribute = picture?.AltAttribute, + TitleAttribute = picture?.TitleAttribute, + DisplayOrder = productPicture.DisplayOrder, + IsDefault = productPicture.IsDefault, + Style = picture?.Style, + ExtraField = picture?.ExtraField + }; + + return (model, picture); + } + + public virtual async Task InsertProductPicture(Product product, Picture picture, int displayOrder) + { + if (picture == null) + throw new ArgumentException("No picture found with the specified id"); + + if (product.ProductPictures.Any(x => x.PictureId == picture.Id)) + return; + + var productPicture = new ProductPicture { + PictureId = picture.Id, + DisplayOrder = displayOrder + }; + await productService.InsertProductPicture(productPicture, product.Id); + await pictureService.SetSeoFilename(picture, pictureService.GetPictureSeName(product.Name)); + } + + public virtual async Task UpdateProductPicture(ProductModel.ProductPictureModel model) + { + var product = await productService.GetProductById(model.ProductId, true); + + var productPicture = product.ProductPictures.FirstOrDefault(x => x.Id == model.Id); + if (productPicture == null) + throw new ArgumentException("No product picture found with the specified id"); + + var picture = await pictureService.GetPictureById(productPicture.PictureId); + if (picture == null) + throw new ArgumentException("No picture found with the specified id"); + + productPicture.DisplayOrder = model.DisplayOrder; + productPicture.IsDefault = model.IsDefault; + await productService.UpdateProductPicture(productPicture, product.Id); + + //Update picture fields + await pictureService.UpdatePictureField(picture, x => x.AltAttribute, model.AltAttribute); + await pictureService.UpdatePictureField(picture, x => x.TitleAttribute, model.TitleAttribute); + await pictureService.UpdatePictureField(picture, x => x.Locales, model.Locales.ToTranslationProperty()); + await pictureService.UpdatePictureField(picture, x => x.Style, model.Style); + await pictureService.UpdatePictureField(picture, x => x.ExtraField, model.ExtraField); + } + + public virtual async Task DeleteProductPicture(ProductModel.ProductPictureModel model) + { + var product = await productService.GetProductById(model.ProductId, true); + + var productPicture = product.ProductPictures.FirstOrDefault(x => x.Id == model.Id); + if (productPicture == null) + throw new ArgumentException("No product picture found with the specified id"); + + var pictureId = productPicture.PictureId; + await productService.DeleteProductPicture(productPicture, product.Id); + + var picture = await pictureService.GetPictureById(pictureId); + if (picture != null) + await pictureService.DeletePicture(picture); + } +} diff --git a/src/Web/Grand.Web.AdminShared/Services/ProductPriceViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/ProductPriceViewModelService.cs new file mode 100644 index 000000000..9a8834bfc --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Services/ProductPriceViewModelService.cs @@ -0,0 +1,98 @@ +using Grand.Business.Core.Interfaces.Catalog.Tax; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Domain.Catalog; +using Grand.SharedKernel.Extensions; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Catalog; +using Grand.Web.Common.Extensions; +using Grand.Web.Common.Localization; +using Microsoft.AspNetCore.Mvc.Rendering; +using System.Net; + +namespace Grand.Web.AdminShared.Services; + +public class ProductPriceViewModelService( + ICurrencyService currencyService, + IDateTimeService dateTimeService, + IRequestTranslationService translationService, + IGroupService groupService, + IStoreService storeService, + IAdminDataScope scope) + : IProductPriceViewModelService +{ + public virtual async Task PrepareTierPriceModel(ProductModel.TierPriceModel model) + { + var storeId = scope.DefaultStoreId ?? ""; + + //stores - same capability gate as PrepareProductListModel: Vendor's original PrepareTierPriceModel + //never populated a stores dropdown at all (vendors don't pick stores), so this must not run for + //Vendor even though its storeId also resolves to "". + if (scope.ShowStoreSelector) + { + if (string.IsNullOrEmpty(storeId)) + model.AvailableStores.Add(new SelectListItem { Text = translationService.GetResource("Admin.Common.All"), Value = " " }); + + foreach (var store in (await storeService.GetAllStores()).Where(x => + x.Id == storeId || string.IsNullOrWhiteSpace(storeId))) + model.AvailableStores.Add(new SelectListItem { Text = store.Shortcut, Value = store.Id }); + } + + //customer groups + model.AvailableCustomerGroups.Add(new SelectListItem { Text = translationService.GetResource("Admin.Common.All"), Value = " " }); + foreach (var role in await groupService.GetAllCustomerGroups(showHidden: true)) + model.AvailableCustomerGroups.Add(new SelectListItem { Text = role.Name, Value = role.Id }); + + foreach (var currency in await currencyService.GetAllCurrencies()) + model.AvailableCurrencies.Add(new SelectListItem { Text = currency.Name, Value = currency.CurrencyCode }); + } + + public virtual async Task> PrepareTierPriceModel(Product product) + { + var storeId = scope.DefaultStoreId ?? ""; + var items = new List(); + foreach (var x in product.TierPrices + .Where(x => x.StoreId == storeId || string.IsNullOrWhiteSpace(storeId) || + string.IsNullOrWhiteSpace(x.StoreId)).OrderBy(x => x.StoreId).ThenBy(x => x.Quantity) + .ThenBy(x => x.CustomerGroupId)) + { + string storeName; + if (!string.IsNullOrEmpty(x.StoreId)) + { + var store = await storeService.GetStoreById(x.StoreId); + storeName = store != null ? store.Shortcut : "Deleted"; + } + else + { + storeName = translationService.GetResource("Admin.Catalog.Products.TierPrices.Fields.Store.All"); + } + + var groupName = string.Empty; + if (string.IsNullOrEmpty(x.CustomerGroupId)) + groupName = translationService.GetResource( + "Admin.Catalog.Products.TierPrices.Fields.CustomerGroup.All"); + else + groupName = (await groupService.GetCustomerGroupById(x.CustomerGroupId))?.Name ?? "NOT FOUND"; + + items.Add(new ProductModel.TierPriceModel { + Id = x.Id, + StoreId = x.StoreId, + Store = storeName, + CurrencyCode = x.CurrencyCode, + CustomerGroup = groupName, + ProductId = product.Id, + CustomerGroupId = !string.IsNullOrEmpty(x.CustomerGroupId) ? x.CustomerGroupId : "", + Quantity = x.Quantity, + Price = x.Price, + StartDateTime = x.StartDateTimeUtc.HasValue + ? dateTimeService.ConvertToUserTime(x.StartDateTimeUtc.Value, DateTimeKind.Utc) + : new DateTime?(), + EndDateTime = x.EndDateTimeUtc.HasValue + ? dateTimeService.ConvertToUserTime(x.EndDateTimeUtc.Value, DateTimeKind.Utc) + : new DateTime?() + }); + } + + return items; + } +} diff --git a/src/Web/Grand.Web.AdminShared/Services/ProductRelatedViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/ProductRelatedViewModelService.cs new file mode 100644 index 000000000..458e570e6 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Services/ProductRelatedViewModelService.cs @@ -0,0 +1,163 @@ +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Domain.Catalog; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Catalog; +using System.Net; + +namespace Grand.Web.AdminShared.Services; + +public class ProductRelatedViewModelService( + IProductService productService, + IAdminDataScope scope) + : IProductRelatedViewModelService +{ + public virtual async Task InsertRelatedProductModel(ProductModel.AddRelatedProductModel model) + { + var productId1 = await productService.GetProductById(model.ProductId, true); + + foreach (var id in model.SelectedProductIds) + { + var product = await productService.GetProductById(id); + // scope.HasAccess: the popup's search is already scoped, but the posted ids are not - skip any + // product the current panel (Store, Vendor) has no access to, so it cannot be linked by id. + if (product == null || !await scope.HasAccess(product)) continue; + + var existingRelatedProducts = productId1.RelatedProducts; + if (model.ProductId != id) + if (!existingRelatedProducts.Any(x => x.ProductId2 == id)) + { + var related = new RelatedProduct { + ProductId2 = id, + DisplayOrder = 1 + }; + productId1.RelatedProducts.Add(related); + await productService.InsertRelatedProduct(related, model.ProductId); + } + } + } + + public virtual async Task UpdateRelatedProductModel(ProductModel.RelatedProductModel model) + { + var product1 = await productService.GetProductById(model.ProductId1, true); + var relatedProduct = product1.RelatedProducts.FirstOrDefault(x => x.Id == model.Id); + if (relatedProduct == null) + throw new ArgumentException("No related product found with the specified id"); + + var product2 = await productService.GetProductById(relatedProduct.ProductId2); + if (product2 == null) + throw new ArgumentException("No product found with the specified id"); + + relatedProduct.DisplayOrder = model.DisplayOrder; + await productService.UpdateRelatedProduct(relatedProduct, model.ProductId1); + } + + public virtual async Task DeleteRelatedProductModel(ProductModel.RelatedProductModel model) + { + var product = await productService.GetProductById(model.ProductId1, true); + var relatedProduct = product.RelatedProducts.FirstOrDefault(x => x.Id == model.Id); + if (relatedProduct == null) + throw new ArgumentException("No related product found with the specified id"); + + await productService.DeleteRelatedProduct(relatedProduct, model.ProductId1); + } + + public virtual async Task InsertSimilarProductModel(ProductModel.AddSimilarProductModel model) + { + var productId1 = await productService.GetProductById(model.ProductId, true); + + foreach (var id in model.SelectedProductIds) + { + var product = await productService.GetProductById(id); + // scope.HasAccess: see InsertRelatedProductModel above. + if (product == null || !await scope.HasAccess(product)) continue; + + var existingSimilarProducts = productId1.SimilarProducts; + if (model.ProductId != id) + if (!existingSimilarProducts.Any(x => x.ProductId2 == id)) + { + var similar = new SimilarProduct { + ProductId1 = model.ProductId, + ProductId2 = id, + DisplayOrder = 1 + }; + productId1.SimilarProducts.Add(similar); + await productService.InsertSimilarProduct(similar); + } + } + } + + public virtual async Task UpdateSimilarProductModel(ProductModel.SimilarProductModel model) + { + var product1 = await productService.GetProductById(model.ProductId1, true); + var similarProduct = product1.SimilarProducts.FirstOrDefault(x => x.Id == model.Id); + if (similarProduct == null) + throw new ArgumentException("No similar product found with the specified id"); + + var product2 = await productService.GetProductById(similarProduct.ProductId2); + if (product2 == null) + throw new ArgumentException("No product found with the specified id"); + + similarProduct.ProductId1 = model.ProductId1; + similarProduct.DisplayOrder = model.DisplayOrder; + await productService.UpdateSimilarProduct(similarProduct); + } + + public virtual async Task DeleteSimilarProductModel(ProductModel.SimilarProductModel model) + { + var product = await productService.GetProductById(model.ProductId1, true); + var similarProduct = product.SimilarProducts.FirstOrDefault(x => x.Id == model.Id); + if (similarProduct == null) + throw new ArgumentException("No similar product found with the specified id"); + + similarProduct.ProductId1 = model.ProductId1; + await productService.DeleteSimilarProduct(similarProduct); + } + + public virtual async Task InsertCrossSellProductModel(ProductModel.AddCrossSellProductModel model) + { + var crossSellProduct = await productService.GetProductById(model.ProductId, true); + foreach (var id in model.SelectedProductIds) + { + var product = await productService.GetProductById(id); + // scope.HasAccess: see InsertRelatedProductModel above. + if (product == null || !await scope.HasAccess(product)) continue; + + if (!crossSellProduct.CrossSellProduct.Any(x => x == id)) + if (model.ProductId != id) + await productService.InsertCrossSellProduct( + new CrossSellProduct { + ProductId1 = model.ProductId, + ProductId2 = id + }); + } + } + + public virtual async Task DeleteCrossSellProduct(string productId, string crossSellProductId) + { + var crosssell = new CrossSellProduct { + ProductId1 = productId, + ProductId2 = crossSellProductId + }; + await productService.DeleteCrossSellProduct(crosssell); + } + + public virtual async Task InsertRecommendedProductModel(ProductModel.AddRecommendedProductModel model) + { + var mainproduct = await productService.GetProductById(model.ProductId, true); + foreach (var id in model.SelectedProductIds) + { + var product = await productService.GetProductById(id); + // scope.HasAccess: see InsertRelatedProductModel above. + if (product == null || !await scope.HasAccess(product)) continue; + + if (!mainproduct.RecommendedProduct.Any(x => x == id)) + if (model.ProductId != id) + await productService.InsertRecommendedProduct(model.ProductId, id); + } + } + + public virtual async Task DeleteRecommendedProduct(string productId, string recommendedProductId) + { + await productService.DeleteRecommendedProduct(productId, recommendedProductId); + } +} diff --git a/src/Web/Grand.Web.AdminShared/Services/ProductSpecificationViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/ProductSpecificationViewModelService.cs new file mode 100644 index 000000000..ee3e66892 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Services/ProductSpecificationViewModelService.cs @@ -0,0 +1,98 @@ +using Grand.Business.Core.Extensions; +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Interfaces.Catalog.Tax; +using Grand.Domain.Catalog; +using Grand.SharedKernel.Extensions; +using Grand.Web.AdminShared.Extensions.Mapping; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Catalog; +using Grand.Web.Common.Localization; +using System.Net; + +namespace Grand.Web.AdminShared.Services; + +public class ProductSpecificationViewModelService( + ISpecificationAttributeService specificationAttributeService, + IEnumTranslationService enumTranslationService) + : IProductSpecificationViewModelService +{ + //Product specification + public virtual async Task> PrepareProductSpecificationAttributeModel( + Product product) + { + var items = new List(); + foreach (var x in product.ProductSpecificationAttributes.OrderBy(x => x.DisplayOrder)) + { + var psaModel = new ProductSpecificationAttributeModel { + Id = x.Id, + AttributeTypeId = (int)x.AttributeTypeId, + AttributeId = x.SpecificationAttributeId, + ProductId = product.Id, + AttributeTypeName = enumTranslationService.GetTranslationEnum(x.AttributeTypeId), + AllowFiltering = x.AllowFiltering, + ShowOnProductPage = x.ShowOnProductPage, + DisplayOrder = x.DisplayOrder, + AttributeName = x.CustomName + }; + + switch (x.AttributeTypeId) + { + case SpecificationAttributeType.Option: + var specificationAttribute = + await specificationAttributeService.GetSpecificationAttributeById(x.SpecificationAttributeId); + if (specificationAttribute != null) + { + psaModel.AttributeName = specificationAttribute.Name; + psaModel.ValueRaw = WebUtility.HtmlEncode(specificationAttribute.SpecificationAttributeOptions + .FirstOrDefault(y => y.Id == x.SpecificationAttributeOptionId)?.Name); + } + + psaModel.SpecificationAttributeOptionId = x.SpecificationAttributeOptionId; + break; + case SpecificationAttributeType.CustomText: + psaModel.ValueRaw = WebUtility.HtmlEncode(x.CustomValue); + break; + case SpecificationAttributeType.CustomHtmlText: + //do not encode? + psaModel.ValueRaw = WebUtility.HtmlEncode(x.CustomValue); + break; + case SpecificationAttributeType.Hyperlink: + psaModel.ValueRaw = x.CustomValue; + break; + } + + items.Add(psaModel); + } + + return items; + } + + public virtual async Task InsertProductSpecificationAttributeModel( + ProductModel.AddProductSpecificationAttributeModel model, Product product) + { + //we allow filtering only for "Option" attribute type + if (model.AttributeTypeId != (int)SpecificationAttributeType.Option) + { + model.AllowFiltering = false; + model.SpecificationAttributeOptionId = null; + } + + var psa = model.ToEntity(); + + await specificationAttributeService.InsertProductSpecificationAttribute(psa, product.Id); + product.ProductSpecificationAttributes.Add(psa); + } + + public virtual async Task UpdateProductSpecificationAttributeModel(ProductSpecificationAttribute psa, + ProductModel.AddProductSpecificationAttributeModel model) + { + psa = model.ToEntity(psa); + await specificationAttributeService.UpdateProductSpecificationAttribute(psa, model.ProductId); + } + + public virtual async Task DeleteProductSpecificationAttribute(Product product, ProductSpecificationAttribute psa) + { + product.ProductSpecificationAttributes.Remove(psa); + await specificationAttributeService.DeleteProductSpecificationAttribute(psa, product.Id); + } +} diff --git a/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs index 6221d4a0b..434205bac 100644 --- a/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs @@ -1,26 +1,20 @@ using Grand.Business.Core.Extensions; 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.Interfaces.Catalog.Directory; using Grand.Business.Core.Interfaces.Catalog.Discounts; -using Grand.Business.Core.Interfaces.Catalog.Prices; using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Catalog.Tax; using Grand.Business.Core.Interfaces.Checkout.Shipping; using Grand.Business.Core.Interfaces.Common.Directory; -using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Common.Seo; 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; using Grand.Domain.Discounts; -using Grand.Domain.Localization; -using Grand.Domain.Media; using Grand.Domain.Seo; using Grand.Domain.Tax; using Grand.Infrastructure; @@ -32,7 +26,6 @@ using Grand.Web.Common.Localization; using Microsoft.AspNetCore.Mvc.Rendering; using System.Net; -using ProductExtensions = Grand.Domain.Catalog.ProductExtensions; namespace Grand.Web.AdminShared.Services; @@ -46,16 +39,11 @@ public class ProductViewModelService( IMeasureService measureService, IDateTimeService dateTimeService, IBrandService brandService, - ICollectionService collectionService, - IProductCollectionService productCollectionService, ICategoryService categoryService, - IProductCategoryService productCategoryService, IVendorService vendorService, IRequestTranslationService translationService, IProductLayoutService productLayoutService, - ISpecificationAttributeService specificationAttributeService, IContextAccessor contextAccessor, - IGroupService groupService, IWarehouseService warehouseService, IDeliveryDateService deliveryDateService, ITaxCategoryService taxCategoryService, @@ -64,129 +52,16 @@ public class ProductViewModelService( IStoreService storeService, IOutOfStockSubscriptionService outOfStockSubscriptionService, IDownloadService downloadService, - ILanguageService languageService, - IProductAttributeFormatter productAttributeFormatter, IStockQuantityService stockQuantityService, CurrencySettings currencySettings, MeasureSettings measureSettings, TaxSettings taxSettings, SeoSettings seoSettings, - IAuctionService auctionService, - IPriceFormatter priceFormatter, ISeNameService seNameService, IEnumTranslationService enumTranslationService, IAdminDataScope scope) : IProductViewModelService { - public virtual async Task PrepareAddProductAttributeCombinationModel(ProductAttributeCombinationModel model, - Product product) - { - ArgumentNullException.ThrowIfNull(model); - ArgumentNullException.ThrowIfNull(product); - - if (product.UseMultipleWarehouses) model.UseMultipleWarehouses = product.UseMultipleWarehouses; - if (string.IsNullOrEmpty(model.Id)) - { - model.ProductId = product.Id; - var attributes = product.ProductAttributeMappings - .Where(x => !x.IsNonCombinable()) - .ToList(); - foreach (var attribute in attributes) - { - var productAttribute = - await productAttributeService.GetProductAttributeById(attribute.ProductAttributeId); - var attributeModel = new ProductAttributeCombinationModel.ProductAttributeModel { - Id = attribute.Id, - ProductAttributeId = attribute.ProductAttributeId, - Name = productAttribute.Name, - TextPrompt = attribute.TextPrompt, - IsRequired = attribute.IsRequired, - AttributeControlType = attribute.AttributeControlTypeId - }; - - if (attribute.ShouldHaveValues()) - { - //values - var attributeValues = attribute.ProductAttributeValues; - foreach (var attributeValue in attributeValues) - { - var attributeValueModel = new ProductAttributeCombinationModel.ProductAttributeValueModel { - Id = attributeValue.Id, - Name = attributeValue.Name, - IsPreSelected = attributeValue.IsPreSelected - }; - attributeModel.Values.Add(attributeValueModel); - } - } - - model.ProductAttributes.Add(attributeModel); - } - } - - - if (!string.IsNullOrEmpty(model.PictureId)) - { - var pictureThumbnailUrl = await pictureService.GetPictureUrl(model.PictureId, 100, false); - model.PictureThumbnailUrl = pictureThumbnailUrl; - } - - foreach (var picture in product.ProductPictures) - model.ProductPictureModels.Add(new ProductModel.ProductPictureModel { - Id = picture.Id, - ProductId = product.Id, - PictureId = picture.PictureId, - PictureUrl = await pictureService.GetPictureUrl(picture.PictureId), - DisplayOrder = picture.DisplayOrder, - IsDefault = picture.IsDefault - }); - model.PrimaryStoreCurrencyCode = - (await currencyService.GetCurrencyById(currencySettings.PrimaryStoreCurrencyId))?.CurrencyCode; - } - - public virtual async Task PrepareTierPriceModel(ProductModel.TierPriceModel model) - { - var storeId = scope.DefaultStoreId ?? ""; - - //stores - same capability gate as PrepareProductListModel: Vendor's original PrepareTierPriceModel - //never populated a stores dropdown at all (vendors don't pick stores), so this must not run for - //Vendor even though its storeId also resolves to "". - if (scope.ShowStoreSelector) - { - if (string.IsNullOrEmpty(storeId)) - model.AvailableStores.Add(new SelectListItem { Text = translationService.GetResource("Admin.Common.All"), Value = " " }); - - foreach (var store in (await storeService.GetAllStores()).Where(x => - x.Id == storeId || string.IsNullOrWhiteSpace(storeId))) - model.AvailableStores.Add(new SelectListItem { Text = store.Shortcut, Value = store.Id }); - } - - //customer groups - model.AvailableCustomerGroups.Add(new SelectListItem { Text = translationService.GetResource("Admin.Common.All"), Value = " " }); - foreach (var role in await groupService.GetAllCustomerGroups(showHidden: true)) - model.AvailableCustomerGroups.Add(new SelectListItem { Text = role.Name, Value = role.Id }); - - foreach (var currency in await currencyService.GetAllCurrencies()) - model.AvailableCurrencies.Add(new SelectListItem { Text = currency.Name, Value = currency.CurrencyCode }); - } - - public virtual async Task PrepareProductAttributeValueModel(Product product, - ProductModel.ProductAttributeValueModel model) - { - //pictures - foreach (var x in product.ProductPictures.OrderBy(x => x.DisplayOrder)) - model.ProductPictureModels.Add(new ProductModel.ProductPictureModel { - Id = x.Id, - ProductId = product.Id, - PictureId = x.PictureId, - PictureUrl = await pictureService.GetPictureUrl(x.PictureId), - DisplayOrder = x.DisplayOrder, - IsDefault = x.IsDefault - }); - - var associatedProduct = await productService.GetProductById(model.AssociatedProductId); - model.AssociatedProductName = associatedProduct != null ? associatedProduct.Name : ""; - } - public virtual async Task OutOfStockNotifications(Product product, int prevStockQuantity, List prevMultiWarehouseStock ) @@ -221,40 +96,6 @@ await outOfStockSubscriptionService.SendNotificationsToSubscribers(product, } } - public virtual async Task OutOfStockNotifications(Product product, ProductAttributeCombination combination, - ProductAttributeCombination prevcombination) - { - if (product.ManageInventoryMethodId == ManageInventoryMethod.ManageStockByAttributes && - product.BackorderModeId == BackorderMode.NoBackorders && - product.AllowOutOfStockSubscriptions && - combination.StockQuantity > 0 && - prevcombination.StockQuantity <= 0 && !product.UseMultipleWarehouses && - product.Published) - await outOfStockSubscriptionService.SendNotificationsToSubscribers(product, combination.Attributes, ""); - if (product.ManageInventoryMethodId == ManageInventoryMethod.ManageStockByAttributes && - product.BackorderModeId == BackorderMode.NoBackorders && - product.AllowOutOfStockSubscriptions && - product.UseMultipleWarehouses && - product.Published) - { - foreach (var prevstock in prevcombination.WarehouseInventory) - if (prevstock.StockQuantity - prevstock.ReservedQuantity <= 0) - { - var actualStock = - combination.WarehouseInventory.FirstOrDefault(x => x.WarehouseId == prevstock.WarehouseId); - if (actualStock != null) - if (actualStock.StockQuantity - actualStock.ReservedQuantity > 0) - await outOfStockSubscriptionService.SendNotificationsToSubscribers(product, - combination.Attributes, prevstock.WarehouseId); - } - - if (combination.WarehouseInventory.Sum(x => x.StockQuantity - x.ReservedQuantity) > 0) - if (prevcombination.WarehouseInventory.Sum(x => x.StockQuantity - x.ReservedQuantity) <= 0) - await outOfStockSubscriptionService.SendNotificationsToSubscribers(product, combination.Attributes, - ""); - } - } - public virtual async Task PrepareProductModel(ProductModel model, Product product, bool setPredefinedValues, bool excludeProperties) { @@ -883,396 +724,13 @@ public virtual async Task DeleteSelected(IEnumerable selectedIds) } } - public virtual async Task PrepareAddRequiredProductModel() - { - var model = await PrepareAddProductModel(); - return model; - } - - public virtual async Task<(IList products, int totalCount)> PrepareProductModel( - ProductModel.AddProductModel model, int pageIndex, int pageSize) - { - // vendorId: scope.DefaultVendorId (when set) overrides the search model - mirrors Vendor's original - // PrepareProductModel(AddProductModel,...), which always passed vendorId: CurrentVendor.Id (and - // storeId: string.Empty) regardless of the model, so the "add related/similar/bundle/etc. product" - // popups can never surface another vendor's products. - var products = await productService.PrepareProductList(model.SearchCategoryId, model.SearchBrandId, - model.SearchCollectionId, model.SearchStoreId, scope.DefaultVendorId ?? model.SearchVendorId, model.SearchProductTypeId, - model.SearchProductName, pageIndex, pageSize); - return (products.Select(x => x.ToModel(dateTimeService)).ToList(), products.TotalCount); - } - - public virtual async Task> PrepareProductCategoryModel(Product product) - { - var productCategories = product.ProductCategories.OrderBy(x => x.DisplayOrder); - var items = new List(); - foreach (var x in productCategories) - { - var category = await categoryService.GetCategoryById(x.CategoryId); - items.Add(new ProductModel.ProductCategoryModel { - Id = x.Id, - Category = await categoryService.GetFormattedBreadCrumb(category), - ProductId = product.Id, - CategoryId = x.CategoryId, - IsFeaturedProduct = x.IsFeaturedProduct, - DisplayOrder = x.DisplayOrder - }); - } - - return items; - } - - public virtual async Task InsertProductCategoryModel(ProductModel.ProductCategoryModel model) - { - var product = await productService.GetProductById(model.ProductId, true); - if (product != null && product.ProductCategories.All(x => x.CategoryId != model.CategoryId)) - { - var productCategory = new ProductCategory { - CategoryId = model.CategoryId, - DisplayOrder = model.DisplayOrder, - IsFeaturedProduct = model.IsFeaturedProduct - }; - await productCategoryService.InsertProductCategory(productCategory, product.Id); - } - } - - public virtual async Task UpdateProductCategoryModel(ProductModel.ProductCategoryModel model) - { - var product = await productService.GetProductById(model.ProductId, true); - var productCategory = product.ProductCategories.FirstOrDefault(x => x.Id == model.Id); - if (productCategory == null) - throw new ArgumentException("No product category mapping found with the specified id"); - - if (product.ProductCategories.Any(x => x.Id != model.Id && x.CategoryId == model.CategoryId)) - throw new ArgumentException("This category is already mapped with this product"); - - productCategory.CategoryId = model.CategoryId; - productCategory.DisplayOrder = model.DisplayOrder; - productCategory.IsFeaturedProduct = model.IsFeaturedProduct; - await productCategoryService.UpdateProductCategory(productCategory, product.Id); - } - - public virtual async Task DeleteProductCategory(string id, string productId) - { - var product = await productService.GetProductById(productId, true); - var productCategory = product.ProductCategories.FirstOrDefault(x => x.Id == id); - if (productCategory == null) - throw new ArgumentException("No product category mapping found with the specified id"); - - await productCategoryService.DeleteProductCategory(productCategory, product.Id); - } - - public virtual async Task> PrepareProductCollectionModel(Product product) - { - var items = new List(); - foreach (var x in product.ProductCollections.OrderBy(x => x.DisplayOrder)) - items.Add(new ProductModel.ProductCollectionModel { - Id = x.Id, - Collection = (await collectionService.GetCollectionById(x.CollectionId)).Name, - ProductId = product.Id, - CollectionId = x.CollectionId, - IsFeaturedProduct = x.IsFeaturedProduct, - DisplayOrder = x.DisplayOrder - }); - return items; - } - - public virtual async Task InsertProductCollection(ProductModel.ProductCollectionModel model) - { - var collectionId = model.CollectionId; - var product = await productService.GetProductById(model.ProductId, true); - if (product != null && product.ProductCollections.All(x => x.CollectionId != collectionId)) - { - var productCollection = new ProductCollection { - CollectionId = collectionId, - DisplayOrder = model.DisplayOrder, - IsFeaturedProduct = model.IsFeaturedProduct - }; - await productCollectionService.InsertProductCollection(productCollection, model.ProductId); - } - } - - public virtual async Task UpdateProductCollection(ProductModel.ProductCollectionModel model) - { - var product = await productService.GetProductById(model.ProductId, true); - var productCollection = product.ProductCollections.FirstOrDefault(x => x.Id == model.Id); - if (productCollection == null) - throw new ArgumentException("No product collection mapping found with the specified id"); - - if (product.ProductCollections.Any(x => x.Id != model.Id && x.CollectionId == model.CollectionId)) - throw new ArgumentException("This collection is already mapped with this product"); - - productCollection.CollectionId = model.CollectionId; - productCollection.DisplayOrder = model.DisplayOrder; - productCollection.IsFeaturedProduct = model.IsFeaturedProduct; - - await productCollectionService.UpdateProductCollection(productCollection, product.Id); - } - - public virtual async Task DeleteProductCollection(string id, string productId) - { - var product = await productService.GetProductById(productId, true); - var productCollection = product.ProductCollections.FirstOrDefault(x => x.Id == id); - if (productCollection == null) - throw new ArgumentException("No product collection mapping found with the specified id"); - - await productCollectionService.DeleteProductCollection(productCollection, product.Id); - } - - public virtual async Task InsertRelatedProductModel(ProductModel.AddRelatedProductModel model) - { - var productId1 = await productService.GetProductById(model.ProductId, true); - - foreach (var id in model.SelectedProductIds) - { - var product = await productService.GetProductById(id); - if (product == null || !await scope.HasAccess(product)) continue; - - var existingRelatedProducts = productId1.RelatedProducts; - if (model.ProductId != id) - if (!existingRelatedProducts.Any(x => x.ProductId2 == id)) - { - var related = new RelatedProduct { - ProductId2 = id, - DisplayOrder = 1 - }; - productId1.RelatedProducts.Add(related); - await productService.InsertRelatedProduct(related, model.ProductId); - } - } - } - - public virtual async Task UpdateRelatedProductModel(ProductModel.RelatedProductModel model) - { - var product1 = await productService.GetProductById(model.ProductId1, true); - var relatedProduct = product1.RelatedProducts.FirstOrDefault(x => x.Id == model.Id); - if (relatedProduct == null) - throw new ArgumentException("No related product found with the specified id"); - - var product2 = await productService.GetProductById(relatedProduct.ProductId2); - if (product2 == null) - throw new ArgumentException("No product found with the specified id"); - - relatedProduct.DisplayOrder = model.DisplayOrder; - await productService.UpdateRelatedProduct(relatedProduct, model.ProductId1); - } - - public virtual async Task DeleteRelatedProductModel(ProductModel.RelatedProductModel model) - { - var product = await productService.GetProductById(model.ProductId1, true); - var relatedProduct = product.RelatedProducts.FirstOrDefault(x => x.Id == model.Id); - if (relatedProduct == null) - throw new ArgumentException("No related product found with the specified id"); - - await productService.DeleteRelatedProduct(relatedProduct, model.ProductId1); - } - - public virtual async Task InsertSimilarProductModel(ProductModel.AddSimilarProductModel model) - { - var productId1 = await productService.GetProductById(model.ProductId, true); - - foreach (var id in model.SelectedProductIds) - { - var product = await productService.GetProductById(id); - // scope.HasAccess: see InsertRelatedProductModel above. - if (product == null || !await scope.HasAccess(product)) continue; - - var existingSimilarProducts = productId1.SimilarProducts; - if (model.ProductId != id) - if (!existingSimilarProducts.Any(x => x.ProductId2 == id)) - { - var similar = new SimilarProduct { - ProductId1 = model.ProductId, - ProductId2 = id, - DisplayOrder = 1 - }; - productId1.SimilarProducts.Add(similar); - await productService.InsertSimilarProduct(similar); - } - } - } - - public virtual async Task UpdateSimilarProductModel(ProductModel.SimilarProductModel model) - { - var product1 = await productService.GetProductById(model.ProductId1, true); - var similarProduct = product1.SimilarProducts.FirstOrDefault(x => x.Id == model.Id); - if (similarProduct == null) - throw new ArgumentException("No similar product found with the specified id"); - - var product2 = await productService.GetProductById(similarProduct.ProductId2); - if (product2 == null) - throw new ArgumentException("No product found with the specified id"); - - similarProduct.ProductId1 = model.ProductId1; - similarProduct.DisplayOrder = model.DisplayOrder; - await productService.UpdateSimilarProduct(similarProduct); - } - - public virtual async Task DeleteSimilarProductModel(ProductModel.SimilarProductModel model) - { - var product = await productService.GetProductById(model.ProductId1, true); - var similarProduct = product.SimilarProducts.FirstOrDefault(x => x.Id == model.Id); - if (similarProduct == null) - throw new ArgumentException("No similar product found with the specified id"); - - similarProduct.ProductId1 = model.ProductId1; - await productService.DeleteSimilarProduct(similarProduct); - } - - public virtual async Task InsertBundleProductModel(ProductModel.AddBundleProductModel model) - { - var productId1 = await productService.GetProductById(model.ProductId, true); - - foreach (var id in model.SelectedProductIds) - { - var product = await productService.GetProductById(id); - // scope.HasAccess: see InsertRelatedProductModel above. - if (product == null || !await scope.HasAccess(product)) continue; - - var existingBundleProducts = productId1.BundleProducts; - if (model.ProductId != id) - if (!existingBundleProducts.Any(x => x.ProductId == id)) - { - var bundle = new BundleProduct { - ProductId = id, - DisplayOrder = 1, - Quantity = 1 - }; - productId1.BundleProducts.Add(bundle); - await productService.InsertBundleProduct(bundle, model.ProductId); - } - } - } - - public virtual async Task UpdateBundleProductModel(ProductModel.BundleProductModel model) - { - var product = await productService.GetProductById(model.ProductBundleId, true); - var bundleProduct = product.BundleProducts.FirstOrDefault(x => x.Id == model.Id); - if (bundleProduct == null) - throw new ArgumentException("No bundle product found with the specified id"); - - var product2 = await productService.GetProductById(bundleProduct.ProductId); - if (product2 == null) - throw new ArgumentException("No product found with the specified id"); - - bundleProduct.ProductId = model.ProductId; - bundleProduct.Quantity = model.Quantity > 0 ? model.Quantity : 1; - bundleProduct.DisplayOrder = model.DisplayOrder; - await productService.UpdateBundleProduct(bundleProduct, model.ProductBundleId); - } - - public virtual async Task DeleteBundleProductModel(ProductModel.BundleProductModel model) - { - var product = await productService.GetProductById(model.ProductBundleId, true); - var bundleProduct = product.BundleProducts.FirstOrDefault(x => x.Id == model.Id); - if (bundleProduct == null) - throw new ArgumentException("No bundle product found with the specified id"); - - await productService.DeleteBundleProduct(bundleProduct, model.ProductBundleId); - } - - public virtual async Task InsertCrossSellProductModel(ProductModel.AddCrossSellProductModel model) - { - var crossSellProduct = await productService.GetProductById(model.ProductId, true); - foreach (var id in model.SelectedProductIds) - { - var product = await productService.GetProductById(id); - // scope.HasAccess: see InsertRelatedProductModel above. - if (product == null || !await scope.HasAccess(product)) continue; - - if (!crossSellProduct.CrossSellProduct.Any(x => x == id)) - if (model.ProductId != id) - await productService.InsertCrossSellProduct( - new CrossSellProduct { - ProductId1 = model.ProductId, - ProductId2 = id - }); - } - } - - public virtual async Task DeleteCrossSellProduct(string productId, string crossSellProductId) - { - var crosssell = new CrossSellProduct { - ProductId1 = productId, - ProductId2 = crossSellProductId - }; - await productService.DeleteCrossSellProduct(crosssell); - } - - public virtual async Task InsertRecommendedProductModel(ProductModel.AddRecommendedProductModel model) - { - var mainproduct = await productService.GetProductById(model.ProductId, true); - foreach (var id in model.SelectedProductIds) - { - var product = await productService.GetProductById(id); - // scope.HasAccess: see InsertRelatedProductModel above. - if (product == null || !await scope.HasAccess(product)) continue; - - if (!mainproduct.RecommendedProduct.Any(x => x == id)) - if (model.ProductId != id) - await productService.InsertRecommendedProduct(model.ProductId, id); - } - } - - public virtual async Task DeleteRecommendedProduct(string productId, string recommendedProductId) - { - await productService.DeleteRecommendedProduct(productId, recommendedProductId); - } - - public virtual async Task InsertAssociatedProductModel(ProductModel.AddAssociatedProductModel model) - { - foreach (var id in model.SelectedProductIds) - { - var product = await productService.GetProductById(id); - if (product != null) - { - product.ParentGroupedProductId = model.ProductId; - await productService.UpdateAssociatedProduct(product); - } - } - } - public virtual async Task DeleteAssociatedProduct(Product product) - { - product.ParentGroupedProductId = ""; - await productService.UpdateAssociatedProduct(product); - } - public virtual async Task PrepareRelatedProductModel() - { - var model = await PrepareAddProductModel(); - return model; - } - public virtual async Task PrepareSimilarProductModel() - { - var model = await PrepareAddProductModel(); - return model; - } - public virtual async Task PrepareBundleProductModel() - { - var model = await PrepareAddProductModel(); - return model; - } - public virtual async Task PrepareCrossSellProductModel() - { - var model = await PrepareAddProductModel(); - return model; - } - public virtual async Task PrepareRecommendedProductModel() - { - var model = await PrepareAddProductModel(); - return model; - } - public virtual async Task PrepareAssociatedProductModel() - { - var model = await PrepareAddProductModel(); - return model; - } public virtual async Task PrepareBulkEditListModel() { @@ -1421,1211 +879,47 @@ public virtual async Task DeleteBulkEdit(IEnumerable produ } } - public virtual async Task> PrepareTierPriceModel(Product product) + + protected virtual async Task UpdatePictureSeoNames(Product product) { - var storeId = scope.DefaultStoreId ?? ""; - var items = new List(); - foreach (var x in product.TierPrices - .Where(x => x.StoreId == storeId || string.IsNullOrWhiteSpace(storeId) || - string.IsNullOrWhiteSpace(x.StoreId)).OrderBy(x => x.StoreId).ThenBy(x => x.Quantity) - .ThenBy(x => x.CustomerGroupId)) + var picturesename = pictureService.GetPictureSeName(product.Name); + foreach (var pp in product.ProductPictures) { - string storeName; - if (!string.IsNullOrEmpty(x.StoreId)) - { - var store = await storeService.GetStoreById(x.StoreId); - storeName = store != null ? store.Shortcut : "Deleted"; - } - else - { - storeName = translationService.GetResource("Admin.Catalog.Products.TierPrices.Fields.Store.All"); - } - - var groupName = string.Empty; - if (string.IsNullOrEmpty(x.CustomerGroupId)) - groupName = translationService.GetResource( - "Admin.Catalog.Products.TierPrices.Fields.CustomerGroup.All"); - else - groupName = (await groupService.GetCustomerGroupById(x.CustomerGroupId))?.Name ?? "NOT FOUND"; - - items.Add(new ProductModel.TierPriceModel { - Id = x.Id, - StoreId = x.StoreId, - Store = storeName, - CurrencyCode = x.CurrencyCode, - CustomerGroup = groupName, - ProductId = product.Id, - CustomerGroupId = !string.IsNullOrEmpty(x.CustomerGroupId) ? x.CustomerGroupId : "", - Quantity = x.Quantity, - Price = x.Price, - StartDateTime = x.StartDateTimeUtc.HasValue - ? dateTimeService.ConvertToUserTime(x.StartDateTimeUtc.Value, DateTimeKind.Utc) - : new DateTime?(), - EndDateTime = x.EndDateTimeUtc.HasValue - ? dateTimeService.ConvertToUserTime(x.EndDateTimeUtc.Value, DateTimeKind.Utc) - : new DateTime?() - }); + var picture = await pictureService.GetPictureById(pp.PictureId); + if (picture != null) + await pictureService.SetSeoFilename(picture, picturesename); } - - return items; } - public virtual async Task<(IEnumerable bidModels, int totalCount)> PrepareBidMode( - string productId, int pageIndex, int pageSize) + protected virtual async Task> GetChildCategoryIds(string parentCategoryId) { - var bids = await auctionService.GetBidsByProductId(productId, pageIndex - 1, pageSize); - var bidsModel = new List(); - foreach (var x in bids) - bidsModel.Add(new ProductModel.BidModel { - BidId = x.Id, - ProductId = x.ProductId, - Amount = priceFormatter.FormatPrice(x.Amount, contextAccessor.WorkContext.WorkingCurrency), - Date = dateTimeService.ConvertToUserTime(x.Date, DateTimeKind.Utc), - CustomerId = x.CustomerId, - Email = (await customerService.GetCustomerById(x.CustomerId))?.Email, - OrderId = x.OrderId - }); - return (bidsModel, bids.TotalCount); - } + var categoriesIds = new List(); + var categories = await categoryService.GetAllCategoriesByParentCategoryId( + access: CatalogAccess.IncludingHidden(), parentCategoryId: parentCategoryId); + foreach (var category in categories) + { + categoriesIds.Add(category.Id); + categoriesIds.AddRange(await GetChildCategoryIds(category.Id)); + } - public virtual async Task PrepareProductAttributeMappingModel( - Product product) - { - var model = new ProductModel.ProductAttributeMappingModel { - ProductId = product.Id - }; - foreach (var attribute in await productAttributeService.GetAllProductAttributes(contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) - model.AvailableProductAttribute.Add(new SelectListItem { - Value = attribute.Id, - Text = attribute.Name - }); - return model; + return categoriesIds; } - public virtual async Task PrepareProductAttributeMappingModel( - Product product, ProductAttributeMapping productAttributeMapping) + protected virtual string[] ParseProductTags(string productTags) { - var model = productAttributeMapping.ToModel(); - foreach (var attribute in await productAttributeService.GetAllProductAttributes(contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) - model.AvailableProductAttribute.Add(new SelectListItem { - Value = attribute.Id, - Text = attribute.Name, - Selected = attribute.Id == model.ProductAttributeId - }); - return model; + var result = new List(); + if (!string.IsNullOrWhiteSpace(productTags)) + { + var values = productTags.Split([','], StringSplitOptions.RemoveEmptyEntries); + foreach (var val1 in values) + if (!string.IsNullOrEmpty(val1.Trim())) + result.Add(val1.Trim()); + } + + return result.ToArray(); } - public virtual async Task PrepareProductAttributeMappingModel( - ProductModel.ProductAttributeMappingModel model) - { - foreach (var attribute in await productAttributeService.GetAllProductAttributes(contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) - model.AvailableProductAttribute.Add(new SelectListItem { - Value = attribute.Id, - Text = attribute.Name - }); - return model; - } - - public virtual async Task> PrepareProductAttributeMappingModels( - Product product) - { - var items = new List(); - foreach (var x in product.ProductAttributeMappings.OrderBy(x => x.DisplayOrder)) - { - var attributeModel = new ProductModel.ProductAttributeMappingModel { - Id = x.Id, - ProductId = product.Id, - ProductAttribute = (await productAttributeService.GetProductAttributeById(x.ProductAttributeId))?.Name, - ProductAttributeId = x.ProductAttributeId, - TextPrompt = x.TextPrompt, - IsRequired = x.IsRequired, - ShowOnCatalogPage = x.ShowOnCatalogPage, - AttributeControlType = enumTranslationService.GetTranslationEnum(x.AttributeControlTypeId), - AttributeControlTypeId = x.AttributeControlTypeId, - DisplayOrder = x.DisplayOrder, - Combination = x.Combination - }; - - - if (x.ShouldHaveValues()) - { - attributeModel.ShouldHaveValues = true; - attributeModel.TotalValues = x.ProductAttributeValues.Count; - } - - if (x.ValidationRulesAllowed()) - { - var validationRules = new StringBuilder(string.Empty); - attributeModel.ValidationRulesAllowed = true; - if (x.ValidationMinLength != null) - validationRules.AppendFormat("{0}: {1}
", - translationService.GetResource( - "Admin.Catalog.Products.ProductAttributes.Attributes.ValidationRules.MinLength"), - x.ValidationMinLength); - if (x.ValidationMaxLength != null) - validationRules.AppendFormat("{0}: {1}
", - translationService.GetResource( - "Admin.Catalog.Products.ProductAttributes.Attributes.ValidationRules.MaxLength"), - x.ValidationMaxLength); - if (!string.IsNullOrEmpty(x.ValidationFileAllowedExtensions)) - validationRules.AppendFormat("{0}: {1}
", - translationService.GetResource( - "Admin.Catalog.Products.ProductAttributes.Attributes.ValidationRules.FileAllowedExtensions"), - WebUtility.HtmlEncode(x.ValidationFileAllowedExtensions)); - if (x.ValidationFileMaximumSize != null) - validationRules.AppendFormat("{0}: {1}
", - translationService.GetResource( - "Admin.Catalog.Products.ProductAttributes.Attributes.ValidationRules.FileMaximumSize"), - x.ValidationFileMaximumSize); - if (!string.IsNullOrEmpty(x.DefaultValue)) - validationRules.AppendFormat("{0}: {1}
", - translationService.GetResource( - "Admin.Catalog.Products.ProductAttributes.Attributes.ValidationRules.DefaultValue"), - WebUtility.HtmlEncode(x.DefaultValue)); - attributeModel.ValidationRulesString = validationRules.ToString(); - } - - //currenty any attribute can have condition. why not? - attributeModel.ConditionAllowed = true; - var conditionAttribute = product.ParseProductAttributeMappings(x.ConditionAttribute).FirstOrDefault(); - var conditionValue = product.ParseProductAttributeValues(x.ConditionAttribute).FirstOrDefault(); - if (conditionAttribute != null && conditionValue != null) - { - var productAttribute = - await productAttributeService.GetProductAttributeById(conditionAttribute.ProductAttributeId); - var _paname = productAttribute != null ? productAttribute.Name : ""; - attributeModel.ConditionString = - $"{WebUtility.HtmlEncode(_paname)}: {WebUtility.HtmlEncode(conditionValue.Name)}"; - } - else - { - attributeModel.ConditionString = string.Empty; - } - - items.Add(attributeModel); - } - - return items; - } - - public virtual async Task InsertProductAttributeMappingModel(ProductModel.ProductAttributeMappingModel model) - { - //insert mapping - var productAttributeMapping = model.ToEntity(); - //predefined values - var predefinedValues = (await productAttributeService.GetProductAttributeById(model.ProductAttributeId)) - .PredefinedProductAttributeValues; - foreach (var predefinedValue in predefinedValues) - { - var pav = predefinedValue.ToEntity(); - //locales - pav.Locales.Clear(); - var languages = await languageService.GetAllLanguages(true); - //localization - foreach (var lang in languages) - { - var name = predefinedValue.GetTranslation(x => x.Name, lang.Id, false); - if (!string.IsNullOrEmpty(name)) - pav.Locales.Add(new TranslationEntity { LanguageId = lang.Id, LocaleKey = "Name", LocaleValue = name }); - } - - productAttributeMapping.ProductAttributeValues.Add(pav); - } - - await productAttributeService.InsertProductAttributeMapping(productAttributeMapping, model.ProductId); - } - - public virtual async Task UpdateProductAttributeMappingModel(ProductModel.ProductAttributeMappingModel model) - { - var product = await productService.GetProductById(model.ProductId, true); - if (product != null) - { - var productAttributeMapping = product.ProductAttributeMappings.FirstOrDefault(x => x.Id == model.Id); - if (productAttributeMapping != null) - { - productAttributeMapping = model.ToEntity(productAttributeMapping); - await productAttributeService.UpdateProductAttributeMapping(productAttributeMapping, model.ProductId); - } - } - } - - public virtual async Task PrepareProductAttributeMappingModel( - ProductAttributeMapping productAttributeMapping) - { - var model = new ProductModel.ProductAttributeMappingModel { - //prepare only used properties - Id = productAttributeMapping.Id, - ValidationRulesAllowed = productAttributeMapping.ValidationRulesAllowed(), - AttributeControlTypeId = productAttributeMapping.AttributeControlTypeId, - ValidationMinLength = productAttributeMapping.ValidationMinLength, - ValidationMaxLength = productAttributeMapping.ValidationMaxLength, - ValidationFileAllowedExtensions = productAttributeMapping.ValidationFileAllowedExtensions, - ValidationFileMaximumSize = productAttributeMapping.ValidationFileMaximumSize, - DefaultValue = productAttributeMapping.DefaultValue - }; - return await Task.FromResult(model); - } - - public virtual async Task UpdateProductAttributeValidationRulesModel( - ProductAttributeMapping productAttributeMapping, ProductModel.ProductAttributeMappingModel model) - { - productAttributeMapping.ValidationMinLength = model.ValidationMinLength; - productAttributeMapping.ValidationMaxLength = model.ValidationMaxLength; - productAttributeMapping.ValidationFileAllowedExtensions = model.ValidationFileAllowedExtensions; - productAttributeMapping.ValidationFileMaximumSize = model.ValidationFileMaximumSize; - productAttributeMapping.DefaultValue = model.DefaultValue; - await productAttributeService.UpdateProductAttributeMapping(productAttributeMapping, model.ProductId); - } - - public virtual async Task PrepareProductAttributeConditionModel(Product product, - ProductAttributeMapping productAttributeMapping) - { - var model = new ProductAttributeConditionModel { - ProductAttributeMappingId = productAttributeMapping.Id, - EnableCondition = productAttributeMapping.ConditionAttribute.Any(), - ProductId = product.Id - }; - //pre-select attribute and values - var selectedPva = product.ParseProductAttributeMappings(productAttributeMapping.ConditionAttribute) - .FirstOrDefault(); - - var attributes = product.ProductAttributeMappings - //ignore non-combinable attributes (should have selectable values) - .Where(x => x.CanBeUsedAsCondition()) - //ignore this attribute (it cannot depend on itself) - .Where(x => x.Id != productAttributeMapping.Id) - .ToList(); - foreach (var attribute in attributes) - { - var pam = await productAttributeService.GetProductAttributeById(attribute.ProductAttributeId); - var attributeModel = new ProductAttributeConditionModel.ProductAttributeModel { - Id = attribute.Id, - ProductAttributeId = attribute.ProductAttributeId, - Name = pam.Name, - TextPrompt = attribute.TextPrompt, - IsRequired = attribute.IsRequired, - AttributeControlType = attribute.AttributeControlTypeId - }; - - if (attribute.ShouldHaveValues()) - { - //values - var attributeValues = product.ProductAttributeMappings.FirstOrDefault(x => x.Id == attribute.Id) - ?.ProductAttributeValues; - if (attributeValues != null) - foreach (var attributeValue in attributeValues) - { - var attributeValueModel = new ProductAttributeConditionModel.ProductAttributeValueModel { - Id = attributeValue.Id, - Name = attributeValue.Name, - IsPreSelected = attributeValue.IsPreSelected - }; - attributeModel.Values.Add(attributeValueModel); - } - - //pre-select attribute and value - if (selectedPva != null && attribute.Id == selectedPva.Id) - { - //attribute - model.SelectedProductAttributeId = selectedPva.Id; - - //values - switch (attribute.AttributeControlTypeId) - { - case AttributeControlType.DropdownList: - case AttributeControlType.RadioList: - case AttributeControlType.Checkboxes: - case AttributeControlType.ColorSquares: - case AttributeControlType.ImageSquares: - { - if (productAttributeMapping.ConditionAttribute.Any()) - { - //clear default selection - foreach (var item in attributeModel.Values) - item.IsPreSelected = false; - - //select new values - var selectedValues = - product.ParseProductAttributeValues(productAttributeMapping.ConditionAttribute); - foreach (var attributeValue in selectedValues) - foreach (var item in attributeModel.Values) - if (attributeValue.Id == item.Id) - item.IsPreSelected = true; - } - } - break; - case AttributeControlType.ReadonlyCheckboxes: - case AttributeControlType.TextBox: - case AttributeControlType.MultilineTextbox: - case AttributeControlType.Datepicker: - case AttributeControlType.FileUpload: - default: - //these attribute types are supported as conditions - break; - } - } - } - - model.ProductAttributes.Add(attributeModel); - } - - return model; - } - - public virtual async Task UpdateProductAttributeConditionModel(Product product, - ProductAttributeMapping productAttributeMapping, ProductAttributeConditionModel model) - { - var customAttributes = new List(); - if (model.EnableCondition) - { - var attribute = - product.ProductAttributeMappings.FirstOrDefault(x => x.Id == model.SelectedProductAttributeId); - if (attribute != null) - switch (attribute.AttributeControlTypeId) - { - case AttributeControlType.DropdownList: - case AttributeControlType.RadioList: - case AttributeControlType.ColorSquares: - case AttributeControlType.ImageSquares: - { - var ctrlAttributes = model.SelectedAttributes.FirstOrDefault(x => x.Key == attribute.Id)?.Value; - if (!string.IsNullOrEmpty(ctrlAttributes)) - customAttributes = ProductExtensions.AddProductAttribute(customAttributes, - attribute, ctrlAttributes).ToList(); - else - customAttributes = ProductExtensions.AddProductAttribute(customAttributes, - attribute, "").ToList(); - } - break; - case AttributeControlType.Checkboxes: - { - var cblAttributes = model.SelectedAttributes.FirstOrDefault(x => x.Key == attribute.Id)?.Value; - if (!string.IsNullOrEmpty(cblAttributes)) - { - var anyValueSelected = false; - foreach (var item in cblAttributes.Split([','], - StringSplitOptions.RemoveEmptyEntries)) - if (!string.IsNullOrEmpty(item)) - { - customAttributes = ProductExtensions.AddProductAttribute(customAttributes, - attribute, item).ToList(); - anyValueSelected = true; - } - - if (!anyValueSelected) - customAttributes = ProductExtensions.AddProductAttribute(customAttributes, - attribute, "").ToList(); - } - else - { - customAttributes = ProductExtensions.AddProductAttribute(customAttributes, - attribute, "").ToList(); - } - } - break; - case AttributeControlType.ReadonlyCheckboxes: - case AttributeControlType.TextBox: - case AttributeControlType.MultilineTextbox: - case AttributeControlType.Datepicker: - case AttributeControlType.FileUpload: - default: - //these attribute types are supported as conditions - break; - } - } - - productAttributeMapping.ConditionAttribute = customAttributes; - await productAttributeService.UpdateProductAttributeMapping(productAttributeMapping, model.ProductId); - } - - public virtual async Task PrepareProductAttributeValueModel( - Product product, ProductAttributeMapping productAttributeMapping) - { - var model = new ProductModel.ProductAttributeValueModel { - ProductAttributeMappingId = productAttributeMapping.Id, - ProductId = product.Id, - - //color squares - DisplayColorSquaresRgb = - productAttributeMapping.AttributeControlTypeId == AttributeControlType.ColorSquares, - ColorSquaresRgb = "#000000", - //image squares - DisplayImageSquaresPicture = - productAttributeMapping.AttributeControlTypeId == AttributeControlType.ImageSquares, - PrimaryStoreCurrencyCode = (await currencyService.GetCurrencyById(currencySettings.PrimaryStoreCurrencyId)) - ?.CurrencyCode, - //default qantity for associated product - Quantity = 1 - }; - - //pictures - foreach (var x in product.ProductPictures) - model.ProductPictureModels.Add(new ProductModel.ProductPictureModel { - Id = x.Id, - ProductId = product.Id, - PictureId = x.PictureId, - PictureUrl = await pictureService.GetPictureUrl(x.PictureId), - DisplayOrder = x.DisplayOrder, - IsDefault = x.IsDefault - }); - return model; - } - - public virtual async Task> PrepareProductAttributeValueModels( - Product product, ProductAttributeMapping productAttributeMapping) - { - var items = new List(); - foreach (var x in productAttributeMapping.ProductAttributeValues.OrderBy(x => x.DisplayOrder)) - { - Product associatedProduct = null; - if (x.AttributeValueTypeId == AttributeValueType.AssociatedToProduct) - associatedProduct = await productService.GetProductById(x.AssociatedProductId); - - var pictureThumbnailUrl = - await pictureService.GetPictureUrl( - string.IsNullOrEmpty(x.PictureId) ? x.ImageSquaresPictureId : x.PictureId, 100, false); - - if (string.IsNullOrEmpty(pictureThumbnailUrl)) - pictureThumbnailUrl = await pictureService.GetPictureUrl("", 1); - - items.Add(new ProductModel.ProductAttributeValueModel { - Id = x.Id, - ProductAttributeMappingId = productAttributeMapping.Id, //TODO - check x.ProductAttributeMappingId, - AttributeValueTypeId = x.AttributeValueTypeId, - AttributeValueTypeName = enumTranslationService.GetTranslationEnum(x.AttributeValueTypeId), - AssociatedProductId = x.AssociatedProductId, - AssociatedProductName = associatedProduct != null ? associatedProduct.Name : "", - Name = productAttributeMapping.AttributeControlTypeId != AttributeControlType.ColorSquares - ? x.Name - : $"{x.Name} - {x.ColorSquaresRgb}", - ColorSquaresRgb = x.ColorSquaresRgb, - ImageSquaresPictureId = x.ImageSquaresPictureId, - PriceAdjustment = x.PriceAdjustment, - PriceAdjustmentStr = x.PriceAdjustment.ToString("G29"), - WeightAdjustment = x.WeightAdjustment, - WeightAdjustmentStr = x.AttributeValueTypeId == AttributeValueType.Simple - ? x.WeightAdjustment.ToString("G29") - : "", - Cost = x.Cost, - PrimaryStoreCurrencyCode = - (await currencyService.GetCurrencyById(currencySettings.PrimaryStoreCurrencyId))?.CurrencyCode, - Quantity = x.Quantity, - IsPreSelected = x.IsPreSelected, - DisplayOrder = x.DisplayOrder, - PictureId = x.PictureId, - PictureThumbnailUrl = pictureThumbnailUrl, - ProductId = product.Id - }); - } - - return items; - } - - public virtual async Task PrepareProductAttributeValueModel( - ProductAttributeMapping pa, ProductAttributeValue pav) - { - var associatedProduct = await productService.GetProductById(pav.AssociatedProductId); - - var model = new ProductModel.ProductAttributeValueModel { - ProductAttributeMappingId = pa.Id, //TODO - check pav.ProductAttributeMappingId, - AttributeValueTypeId = pav.AttributeValueTypeId, - AttributeValueTypeName = enumTranslationService.GetTranslationEnum(pav.AttributeValueTypeId), - AssociatedProductId = pav.AssociatedProductId, - AssociatedProductName = associatedProduct != null ? associatedProduct.Name : "", - Name = pav.Name, - ColorSquaresRgb = pav.ColorSquaresRgb, - DisplayColorSquaresRgb = pa.AttributeControlTypeId == AttributeControlType.ColorSquares, - ImageSquaresPictureId = pav.ImageSquaresPictureId, - DisplayImageSquaresPicture = pa.AttributeControlTypeId == AttributeControlType.ImageSquares, - PriceAdjustment = pav.PriceAdjustment, - WeightAdjustment = pav.WeightAdjustment, - Cost = pav.Cost, - PrimaryStoreCurrencyCode = (await currencyService.GetCurrencyById(currencySettings.PrimaryStoreCurrencyId)) - ?.CurrencyCode, - Quantity = pav.Quantity, - IsPreSelected = pav.IsPreSelected, - DisplayOrder = pav.DisplayOrder, - PictureId = pav.PictureId - }; - if (model.DisplayColorSquaresRgb && string.IsNullOrEmpty(model.ColorSquaresRgb)) - model.ColorSquaresRgb = "#000000"; - return model; - } - - public virtual async Task InsertProductAttributeValueModel(ProductModel.ProductAttributeValueModel model) - { - var pav = new ProductAttributeValue { - AttributeValueTypeId = model.AttributeValueTypeId, - AssociatedProductId = model.AssociatedProductId, - Name = model.Name, - ColorSquaresRgb = model.ColorSquaresRgb, - ImageSquaresPictureId = model.ImageSquaresPictureId, - PriceAdjustment = model.PriceAdjustment, - WeightAdjustment = model.WeightAdjustment, - Cost = model.Cost, - Quantity = model.Quantity, - IsPreSelected = model.IsPreSelected, - DisplayOrder = model.DisplayOrder, - PictureId = model.PictureId, - Locales = model.Locales.ToTranslationProperty() - }; - await productAttributeService.InsertProductAttributeValue(pav, model.ProductId, - model.ProductAttributeMappingId); - } - - public virtual async Task UpdateProductAttributeValueModel(ProductAttributeValue pav, - ProductModel.ProductAttributeValueModel model) - { - pav.AttributeValueTypeId = model.AttributeValueTypeId; - pav.AssociatedProductId = model.AssociatedProductId; - pav.Name = model.Name; - pav.ColorSquaresRgb = model.ColorSquaresRgb; - pav.ImageSquaresPictureId = model.ImageSquaresPictureId; - pav.PriceAdjustment = model.PriceAdjustment; - pav.WeightAdjustment = model.WeightAdjustment; - pav.Cost = model.Cost; - pav.Quantity = model.Quantity; - pav.IsPreSelected = model.IsPreSelected; - pav.DisplayOrder = model.DisplayOrder; - pav.PictureId = model.PictureId; - pav.Locales = model.Locales.ToTranslationProperty(); - - await productAttributeService.UpdateProductAttributeValue(pav, model.ProductId, - model.ProductAttributeMappingId); - } - - public virtual async Task PrepareAssociateProductToAttributeValueModel() - { - var model = await PrepareAddProductModel(); - return model; - } - - public virtual async Task> - PrepareProductAttributeCombinationModel(Product product) - { - var items = new List(); - foreach (var x in product.ProductAttributeCombinations) - { - var attributes = await productAttributeFormatter.FormatAttributes(product, x.Attributes, - contextAccessor.CalculationContext(), contextAccessor.WorkContext.WorkingLanguage, - contextAccessor.StoreContext.CurrentHost, "
", true, true, true, false, true, true); - var pacModel = new ProductModel.ProductAttributeCombinationModel { - Id = x.Id, - ProductId = product.Id, - Attributes = string.IsNullOrEmpty(attributes) ? "(null)" : attributes, - StockQuantity = product.UseMultipleWarehouses - ? x.WarehouseInventory.Sum(y => y.StockQuantity - y.ReservedQuantity) - : x.StockQuantity, - AllowOutOfStockOrders = x.AllowOutOfStockOrders, - Sku = x.Sku, - Mpn = x.Mpn, - Gtin = x.Gtin, - OverriddenPrice = x.OverriddenPrice, - NotifyAdminForQuantityBelow = x.NotifyAdminForQuantityBelow - }; - items.Add(pacModel); - } - - return items; - } - - public virtual async Task PrepareProductAttributeCombinationModel(Product product, - string combinationId) - { - var model = new ProductAttributeCombinationModel(); - var wim = new List(); - foreach (var warehouse in await warehouseService.GetAllWarehouses()) - { - var pwiModel = new ProductAttributeCombinationModel.WarehouseInventoryModel { - WarehouseId = warehouse.Id, - WarehouseName = warehouse.Name - }; - wim.Add(pwiModel); - } - - if (product.UseMultipleWarehouses) - { - model.UseMultipleWarehouses = product.UseMultipleWarehouses; - model.WarehouseInventoryModels = wim; - } - - if (!string.IsNullOrEmpty(combinationId)) - { - var combination = product.ProductAttributeCombinations.FirstOrDefault(x => x.Id == combinationId); - if (combination != null) - { - model = combination.ToModel(); - model.UseMultipleWarehouses = product.UseMultipleWarehouses; - model.WarehouseInventoryModels = wim; - model.ProductId = product.Id; - model.Attributes = await productAttributeFormatter.FormatAttributes(product, combination.Attributes, - contextAccessor.CalculationContext(), contextAccessor.WorkContext.WorkingLanguage, - contextAccessor.StoreContext.CurrentHost, "
", true, true, true, false); - if (model.UseMultipleWarehouses) - foreach (var _winv in combination.WarehouseInventory) - { - var warehouseInventoryModel = - model.WarehouseInventoryModels.FirstOrDefault(x => x.WarehouseId == _winv.WarehouseId); - if (warehouseInventoryModel != null) - { - warehouseInventoryModel.WarehouseUsed = true; - warehouseInventoryModel.Id = _winv.Id; - warehouseInventoryModel.StockQuantity = _winv.StockQuantity; - warehouseInventoryModel.ReservedQuantity = _winv.ReservedQuantity; - } - } - } - } - - return model; - } - - public virtual async Task> InsertOrUpdateProductAttributeCombinationPopup(Product product, - ProductAttributeCombinationModel model) - { - var customAttributes = new List(); - var warnings = new List(); - - async Task PrepareCombinationWarehouseInventory(ProductAttributeCombination combination) - { - var warehouses = await warehouseService.GetAllWarehouses(); - - foreach (var warehouse in warehouses) - { - var whim = model.WarehouseInventoryModels.FirstOrDefault(x => x.WarehouseId == warehouse.Id); - var existingPwI = combination.WarehouseInventory.FirstOrDefault(x => x.WarehouseId == warehouse.Id); - if (existingPwI != null) - { - if (whim is { WarehouseUsed: true }) - { - //update - existingPwI.StockQuantity = whim.StockQuantity; - existingPwI.ReservedQuantity = whim.ReservedQuantity; - } - else - { - //delete - combination.WarehouseInventory.Remove(existingPwI); - } - } - else - { - if (whim is { WarehouseUsed: true }) - { - //no need to insert a record for qty 0 - existingPwI = new ProductCombinationWarehouseInventory { - WarehouseId = whim.WarehouseId, - StockQuantity = whim.StockQuantity, - ReservedQuantity = whim.ReservedQuantity - }; - combination.WarehouseInventory.Add(existingPwI); - } - } - } - } - - if (string.IsNullOrEmpty(model.Id)) - { - #region Product attributes - - var attributes = product.ProductAttributeMappings - .Where(x => !x.IsNonCombinable()) - .ToList(); - if (attributes.Count == 0) - { - warnings.Add("This combination attributes is empty!"); - return warnings; - } - - foreach (var attribute in attributes) - switch (attribute.AttributeControlTypeId) - { - case AttributeControlType.DropdownList: - case AttributeControlType.RadioList: - case AttributeControlType.ColorSquares: - case AttributeControlType.ImageSquares: - { - var ctrlAttributes = model.SelectedAttributes.FirstOrDefault(x => x.Key == attribute.Id)?.Value; - if (!string.IsNullOrEmpty(ctrlAttributes)) - customAttributes = ProductExtensions.AddProductAttribute(customAttributes, - attribute, ctrlAttributes).ToList(); - } - break; - case AttributeControlType.Checkboxes: - { - var cblAttributes = model.SelectedAttributes.FirstOrDefault(x => x.Key == attribute.Id)?.Value; - if (!string.IsNullOrEmpty(cblAttributes)) - foreach (var item in cblAttributes.Split([','], - StringSplitOptions.RemoveEmptyEntries)) - if (!string.IsNullOrEmpty(item)) - customAttributes = ProductExtensions.AddProductAttribute(customAttributes, - attribute, item).ToList(); - } - break; - case AttributeControlType.ReadonlyCheckboxes: - { - //load read-only (already server-side selected) values - var attributeValues = attribute.ProductAttributeValues; - foreach (var selectedAttributeId in attributeValues - .Where(v => v.IsPreSelected) - .Select(v => v.Id) - .ToList()) - customAttributes = ProductExtensions.AddProductAttribute(customAttributes, - attribute, selectedAttributeId).ToList(); - } - break; - } - - //validate conditional attributes (if specified) - foreach (var attribute in attributes) - { - var conditionMet = product.IsConditionMet(attribute, customAttributes); - if (conditionMet.HasValue && !conditionMet.Value) - customAttributes = ProductExtensions.RemoveProductAttribute(customAttributes, attribute).ToList(); - } - - if (customAttributes.Count == 0) - { - warnings.Add("This combination custom attributes is empty!"); - return warnings; - } - - foreach (var customAttribute in customAttributes) - if (string.IsNullOrEmpty(customAttribute.Value)) - { - warnings.Add("Combination custom attributes need to be selected value!"); - return warnings; - } - - #endregion - - if (product.FindProductAttributeCombination(customAttributes) != null) - warnings.Add("This combination attributes exists!"); - if (warnings.Count == 0) - { - var combination = new ProductAttributeCombination { - Attributes = customAttributes, - StockQuantity = model.StockQuantity, - ReservedQuantity = model.ReservedQuantity, - AllowOutOfStockOrders = model.AllowOutOfStockOrders, - Sku = model.Sku, - Text = model.Text, - Mpn = model.Mpn, - Gtin = model.Gtin, - OverriddenPrice = model.OverriddenPrice, - NotifyAdminForQuantityBelow = model.NotifyAdminForQuantityBelow, - PictureId = model.PictureId - }; - - if (product.UseMultipleWarehouses) - { - await PrepareCombinationWarehouseInventory(combination); - combination.StockQuantity = combination.WarehouseInventory.Sum(x => x.StockQuantity); - combination.ReservedQuantity = combination.WarehouseInventory.Sum(x => x.ReservedQuantity); - } - - await productAttributeService.InsertProductAttributeCombination(combination, product.Id); - - if (product.ManageInventoryMethodId == ManageInventoryMethod.ManageStockByAttributes) - { - product.StockQuantity = product.ProductAttributeCombinations.Sum(x => x.StockQuantity); - product.ReservedQuantity = product.ProductAttributeCombinations.Sum(x => x.ReservedQuantity); - await inventoryManageService.UpdateStockProduct(product, false); - } - } - } - else - { - var combination = product.ProductAttributeCombinations.FirstOrDefault(x => x.Id == model.Id); - var prevCombination = (ProductAttributeCombination)combination!.Clone(); - - combination.StockQuantity = model.StockQuantity; - combination.ReservedQuantity = model.ReservedQuantity; - combination.AllowOutOfStockOrders = model.AllowOutOfStockOrders; - combination.Sku = model.Sku; - combination.Text = model.Text; - combination.Mpn = model.Mpn; - combination.Gtin = model.Gtin; - combination.OverriddenPrice = model.OverriddenPrice; - combination.NotifyAdminForQuantityBelow = model.NotifyAdminForQuantityBelow; - combination.PictureId = model.PictureId; - - if (product.UseMultipleWarehouses) - { - await PrepareCombinationWarehouseInventory(combination); - combination.StockQuantity = combination.WarehouseInventory.Sum(x => x.StockQuantity); - } - - //notification - out of stock - await OutOfStockNotifications(product, combination, prevCombination); - - //update combination - await productAttributeService.UpdateProductAttributeCombination(combination, product.Id); - - if (product.ManageInventoryMethodId == ManageInventoryMethod.ManageStockByAttributes) - { - var pr = await productService.GetProductById(model.ProductId); - pr.StockQuantity = pr.ProductAttributeCombinations.Sum(x => x.StockQuantity); - pr.ReservedQuantity = pr.ProductAttributeCombinations.Sum(x => x.ReservedQuantity); - await inventoryManageService.UpdateStockProduct(pr, false); - } - } - - return warnings; - } - - public virtual async Task GenerateAllAttributeCombinations(Product product) - { - var allAttributesComb = product.GenerateAllCombinations(); - if (allAttributesComb == null || allAttributesComb.Count == 0) - return; - - foreach (var attributes in allAttributesComb) - { - var customAttributes = attributes.ToList(); - if (!customAttributes.Any()) - continue; - - var existingCombination = product.FindProductAttributeCombination(customAttributes); - - //already exists? - if (existingCombination != null) - continue; - - //save combination - var combination = new ProductAttributeCombination { - Attributes = customAttributes.ToList(), - StockQuantity = 0, - AllowOutOfStockOrders = false, - Sku = null, - Mpn = null, - Gtin = null, - OverriddenPrice = null, - NotifyAdminForQuantityBelow = 1 - }; - await productAttributeService.InsertProductAttributeCombination(combination, product.Id); - } - - if (product.ManageInventoryMethodId == ManageInventoryMethod.ManageStockByAttributes) - { - product.StockQuantity = product.ProductAttributeCombinations.Sum(x => x.StockQuantity); - product.ReservedQuantity = product.ProductAttributeCombinations.Sum(x => x.ReservedQuantity); - await inventoryManageService.UpdateStockProduct(product, false); - } - } - - public virtual async Task ClearAllAttributeCombinations(Product product) - { - foreach (var combination in product.ProductAttributeCombinations) - await productAttributeService.DeleteProductAttributeCombination(combination, product.Id); - } - - public virtual async Task> - PrepareProductAttributeCombinationTierPricesModel(Product product, string productAttributeCombinationId) - { - var items = new List(); - foreach (var x in product.ProductAttributeCombinations.Where(x => x.Id == productAttributeCombinationId) - .SelectMany(x => x.TierPrices)) - { - string storeName; - if (!string.IsNullOrEmpty(x.StoreId)) - { - var store = await storeService.GetStoreById(x.StoreId); - storeName = store != null ? store.Shortcut : "Deleted"; - } - else - { - storeName = translationService.GetResource("Admin.Catalog.Products.TierPrices.Fields.Store.All"); - } - - var priceModel = new ProductModel.ProductAttributeCombinationTierPricesModel { - Id = x.Id, - CustomerGroupId = x.CustomerGroupId, - CustomerGroup = !string.IsNullOrEmpty(x.CustomerGroupId) - ? (await groupService.GetCustomerGroupById(x.CustomerGroupId)).Name - : translationService.GetResource("Admin.Catalog.Products.TierPrices.Fields.CustomerGroup.All"), - StoreId = x.StoreId, - Store = storeName, - Price = x.Price, - Quantity = x.Quantity - }; - items.Add(priceModel); - } - - return items; - } - - public virtual async Task InsertProductAttributeCombinationTierPricesModel(Product product, - ProductAttributeCombination productAttributeCombination, - ProductModel.ProductAttributeCombinationTierPricesModel model) - { - model.CustomerGroupId = !string.IsNullOrEmpty(model.CustomerGroupId) ? model.CustomerGroupId.Trim() : ""; - - model.StoreId = !string.IsNullOrEmpty(model.StoreId) ? model.StoreId.Trim() : ""; - - if (productAttributeCombination != null) - { - var pctp = new ProductCombinationTierPrices { - Price = model.Price, - Quantity = model.Quantity, - StoreId = model.StoreId, - CustomerGroupId = model.CustomerGroupId - }; - productAttributeCombination.TierPrices.Add(pctp); - await productAttributeService.UpdateProductAttributeCombination(productAttributeCombination, product.Id); - } - } - - public virtual async Task UpdateProductAttributeCombinationTierPricesModel(Product product, - ProductAttributeCombination productAttributeCombination, - ProductModel.ProductAttributeCombinationTierPricesModel model) - { - model.CustomerGroupId = !string.IsNullOrEmpty(model.CustomerGroupId) ? model.CustomerGroupId.Trim() : ""; - model.StoreId = !string.IsNullOrEmpty(model.StoreId) ? model.StoreId.Trim() : ""; - - if (productAttributeCombination != null) - { - var tierPrice = productAttributeCombination.TierPrices.FirstOrDefault(x => x.Id == model.Id); - if (tierPrice != null) - { - tierPrice.Price = model.Price; - tierPrice.Quantity = model.Quantity; - tierPrice.StoreId = model.StoreId; - tierPrice.CustomerGroupId = model.CustomerGroupId; - await productAttributeService.UpdateProductAttributeCombination(productAttributeCombination, - product.Id); - } - } - } - - public virtual async Task DeleteProductAttributeCombinationTierPrices(Product product, - ProductAttributeCombination productAttributeCombination, ProductCombinationTierPrices tierPrice) - { - productAttributeCombination.TierPrices.Remove(tierPrice); - await productAttributeService.UpdateProductAttributeCombination(productAttributeCombination, product.Id); - } - - //Pictures - public virtual async Task> PrepareProductPicturesModel(Product product) - { - var items = new List(); - foreach (var x in product.ProductPictures.OrderBy(x => x.DisplayOrder)) - { - var picture = await pictureService.GetPictureById(x.PictureId); - var m = new ProductModel.ProductPictureModel { - Id = x.Id, - ProductId = product.Id, - PictureId = x.PictureId, - PictureUrl = picture != null ? await pictureService.GetPictureUrl(picture) : null, - AltAttribute = picture?.AltAttribute, - TitleAttribute = picture?.TitleAttribute, - DisplayOrder = x.DisplayOrder, - IsDefault = x.IsDefault, - Style = picture?.Style, - ExtraField = picture?.ExtraField - }; - items.Add(m); - } - - return items; - } - - public virtual async Task<(ProductModel.ProductPictureModel model, Picture Picture)> PrepareProductPictureModel( - Product product, ProductPicture productPicture) - { - var picture = await pictureService.GetPictureById(productPicture.PictureId); - var model = new ProductModel.ProductPictureModel { - Id = productPicture.Id, - ProductId = product.Id, - PictureId = productPicture.PictureId, - PictureUrl = picture != null ? await pictureService.GetPictureUrl(picture) : null, - AltAttribute = picture?.AltAttribute, - TitleAttribute = picture?.TitleAttribute, - DisplayOrder = productPicture.DisplayOrder, - IsDefault = productPicture.IsDefault, - Style = picture?.Style, - ExtraField = picture?.ExtraField - }; - - return (model, picture); - } - - public virtual async Task InsertProductPicture(Product product, Picture picture, int displayOrder) - { - if (picture == null) - throw new ArgumentException("No picture found with the specified id"); - - if (product.ProductPictures.Any(x => x.PictureId == picture.Id)) - return; - - var productPicture = new ProductPicture { - PictureId = picture.Id, - DisplayOrder = displayOrder - }; - await productService.InsertProductPicture(productPicture, product.Id); - await pictureService.SetSeoFilename(picture, pictureService.GetPictureSeName(product.Name)); - } - - public virtual async Task UpdateProductPicture(ProductModel.ProductPictureModel model) - { - var product = await productService.GetProductById(model.ProductId, true); - - var productPicture = product.ProductPictures.FirstOrDefault(x => x.Id == model.Id); - if (productPicture == null) - throw new ArgumentException("No product picture found with the specified id"); - - var picture = await pictureService.GetPictureById(productPicture.PictureId); - if (picture == null) - throw new ArgumentException("No picture found with the specified id"); - - productPicture.DisplayOrder = model.DisplayOrder; - productPicture.IsDefault = model.IsDefault; - await productService.UpdateProductPicture(productPicture, product.Id); - - //Update picture fields - await pictureService.UpdatePictureField(picture, x => x.AltAttribute, model.AltAttribute); - await pictureService.UpdatePictureField(picture, x => x.TitleAttribute, model.TitleAttribute); - await pictureService.UpdatePictureField(picture, x => x.Locales, model.Locales.ToTranslationProperty()); - await pictureService.UpdatePictureField(picture, x => x.Style, model.Style); - await pictureService.UpdatePictureField(picture, x => x.ExtraField, model.ExtraField); - } - - public virtual async Task DeleteProductPicture(ProductModel.ProductPictureModel model) - { - var product = await productService.GetProductById(model.ProductId, true); - - var productPicture = product.ProductPictures.FirstOrDefault(x => x.Id == model.Id); - if (productPicture == null) - throw new ArgumentException("No product picture found with the specified id"); - - var pictureId = productPicture.PictureId; - await productService.DeleteProductPicture(productPicture, product.Id); - - var picture = await pictureService.GetPictureById(pictureId); - if (picture != null) - await pictureService.DeletePicture(picture); - } - - //Product specification - public virtual async Task> PrepareProductSpecificationAttributeModel( - Product product) - { - var items = new List(); - foreach (var x in product.ProductSpecificationAttributes.OrderBy(x => x.DisplayOrder)) - { - var psaModel = new ProductSpecificationAttributeModel { - Id = x.Id, - AttributeTypeId = (int)x.AttributeTypeId, - AttributeId = x.SpecificationAttributeId, - ProductId = product.Id, - AttributeTypeName = enumTranslationService.GetTranslationEnum(x.AttributeTypeId), - AllowFiltering = x.AllowFiltering, - ShowOnProductPage = x.ShowOnProductPage, - DisplayOrder = x.DisplayOrder, - AttributeName = x.CustomName - }; - - switch (x.AttributeTypeId) - { - case SpecificationAttributeType.Option: - var specificationAttribute = - await specificationAttributeService.GetSpecificationAttributeById(x.SpecificationAttributeId); - if (specificationAttribute != null) - { - psaModel.AttributeName = specificationAttribute.Name; - psaModel.ValueRaw = WebUtility.HtmlEncode(specificationAttribute.SpecificationAttributeOptions - .FirstOrDefault(y => y.Id == x.SpecificationAttributeOptionId)?.Name); - } - - psaModel.SpecificationAttributeOptionId = x.SpecificationAttributeOptionId; - break; - case SpecificationAttributeType.CustomText: - psaModel.ValueRaw = WebUtility.HtmlEncode(x.CustomValue); - break; - case SpecificationAttributeType.CustomHtmlText: - //do not encode? - psaModel.ValueRaw = WebUtility.HtmlEncode(x.CustomValue); - break; - case SpecificationAttributeType.Hyperlink: - psaModel.ValueRaw = x.CustomValue; - break; - } - - items.Add(psaModel); - } - - return items; - } - - public virtual async Task InsertProductSpecificationAttributeModel( - ProductModel.AddProductSpecificationAttributeModel model, Product product) - { - //we allow filtering only for "Option" attribute type - if (model.AttributeTypeId != (int)SpecificationAttributeType.Option) - { - model.AllowFiltering = false; - model.SpecificationAttributeOptionId = null; - } - - var psa = model.ToEntity(); - - await specificationAttributeService.InsertProductSpecificationAttribute(psa, product.Id); - product.ProductSpecificationAttributes.Add(psa); - } - - public virtual async Task UpdateProductSpecificationAttributeModel(ProductSpecificationAttribute psa, - ProductModel.AddProductSpecificationAttributeModel model) - { - psa = model.ToEntity(psa); - await specificationAttributeService.UpdateProductSpecificationAttribute(psa, model.ProductId); - } - - public virtual async Task DeleteProductSpecificationAttribute(Product product, ProductSpecificationAttribute psa) - { - product.ProductSpecificationAttributes.Remove(psa); - await specificationAttributeService.DeleteProductSpecificationAttribute(psa, product.Id); - } - - protected virtual async Task UpdatePictureSeoNames(Product product) - { - var picturesename = pictureService.GetPictureSeName(product.Name); - foreach (var pp in product.ProductPictures) - { - var picture = await pictureService.GetPictureById(pp.PictureId); - if (picture != null) - await pictureService.SetSeoFilename(picture, picturesename); - } - } - - protected virtual async Task> GetChildCategoryIds(string parentCategoryId) - { - var categoriesIds = new List(); - var categories = await categoryService.GetAllCategoriesByParentCategoryId( - access: CatalogAccess.IncludingHidden(), parentCategoryId: parentCategoryId); - foreach (var category in categories) - { - categoriesIds.Add(category.Id); - categoriesIds.AddRange(await GetChildCategoryIds(category.Id)); - } - - return categoriesIds; - } - - protected virtual string[] ParseProductTags(string productTags) - { - var result = new List(); - if (!string.IsNullOrWhiteSpace(productTags)) - { - var values = productTags.Split([','], StringSplitOptions.RemoveEmptyEntries); - foreach (var val1 in values) - if (!string.IsNullOrEmpty(val1.Trim())) - result.Add(val1.Trim()); - } - - return result.ToArray(); - } - - protected virtual async Task SaveProductTags(Product product, string[] productTags) + protected virtual async Task SaveProductTags(Product product, string[] productTags) { ArgumentNullException.ThrowIfNull(product); @@ -2675,39 +969,4 @@ protected virtual async Task SaveProductTags(Product product, string[] productTa await productTagService.AttachProductTag(productTag, product.Id); } } - - protected virtual async Task PrepareAddProductModel() where T : ProductModel.AddProductModel, new() - { - var storeId = scope.DefaultStoreId ?? ""; - var model = new T(); - - //stores - same capability gate as PrepareProductListModel: Vendor's original PrepareAddProductModel - //never populated a stores dropdown at all (vendors don't pick stores), so this must not run for - //Vendor even though its storeId also resolves to "". - if (scope.ShowStoreSelector) - { - model.AvailableStores.Add(new SelectListItem { Text = translationService.GetResource("Admin.Common.All"), Value = " " }); - foreach (var s in (await storeService.GetAllStores()).Where(x => - x.Id == storeId || string.IsNullOrWhiteSpace(storeId))) - model.AvailableStores.Add(new SelectListItem { Text = s.Shortcut, Value = s.Id }); - } - - //vendors - only when the host isn't already forced onto a single vendor (scope.DefaultVendorId): - //Vendor's original PrepareAddProductModel never populated a vendor picker at all (matches the - //ShowStoreSelector gate above for the same reason - there's nothing to pick, the search is always - //forced to the current vendor). - if (scope.DefaultVendorId is null) - { - model.AvailableVendors.Add(new SelectListItem { Text = translationService.GetResource("Admin.Common.All"), Value = " " }); - foreach (var v in await vendorService.GetAllVendors(showHidden: true)) - model.AvailableVendors.Add(new SelectListItem { Text = v.Name, Value = v.Id }); - } - - //product types - model.AvailableProductTypes = enumTranslationService.ToSelectList(ProductType.SimpleProduct, false).ToList(); - model.AvailableProductTypes.Insert(0, - new SelectListItem { Text = translationService.GetResource("Admin.Common.All"), Value = "0" }); - - return model; - } } \ No newline at end of file diff --git a/src/Web/Grand.Web.AdminShared/Startup/StartupApplication.cs b/src/Web/Grand.Web.AdminShared/Startup/StartupApplication.cs index 30b882e20..573519c64 100644 --- a/src/Web/Grand.Web.AdminShared/Startup/StartupApplication.cs +++ b/src/Web/Grand.Web.AdminShared/Startup/StartupApplication.cs @@ -67,6 +67,18 @@ public void ConfigureServices(IServiceCollection services, IConfiguration config services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/Web/Grand.Web.Store/Controllers/ProductAssociatedController.cs b/src/Web/Grand.Web.Store/Controllers/ProductAssociatedController.cs index 94d04e926..ae06af7e6 100644 --- a/src/Web/Grand.Web.Store/Controllers/ProductAssociatedController.cs +++ b/src/Web/Grand.Web.Store/Controllers/ProductAssociatedController.cs @@ -14,8 +14,9 @@ namespace Grand.Web.Store.Controllers; [AuthorizeStore] [AuthorizeMenu] public class ProductAssociatedController( - IProductViewModelService productViewModelService, + IProductAddPopupViewModelService productAddPopupViewModelService, + IProductAssociatedViewModelService productAssociatedViewModelService, IProductService productService, IRequestTranslationService translationService, IAdminDataScope scope) - : BaseProductAssociatedController(productViewModelService, productService, translationService, scope); + : BaseProductAssociatedController(productAddPopupViewModelService, productAssociatedViewModelService, productService, translationService, scope); diff --git a/src/Web/Grand.Web.Store/Controllers/ProductAttributeCombinationController.cs b/src/Web/Grand.Web.Store/Controllers/ProductAttributeCombinationController.cs index a2d89bd69..72987c514 100644 --- a/src/Web/Grand.Web.Store/Controllers/ProductAttributeCombinationController.cs +++ b/src/Web/Grand.Web.Store/Controllers/ProductAttributeCombinationController.cs @@ -14,9 +14,9 @@ namespace Grand.Web.Store.Controllers; [AuthorizeStore] [AuthorizeMenu] public class ProductAttributeCombinationController( - IProductViewModelService productViewModelService, + IProductAttributeCombinationViewModelService productAttributeCombinationViewModelService, IProductService productService, IInventoryManageService inventoryManageService, IRequestTranslationService translationService, IAdminDataScope scope) - : BaseProductAttributeCombinationController(productViewModelService, productService, inventoryManageService, translationService, scope); + : BaseProductAttributeCombinationController(productAttributeCombinationViewModelService, productService, inventoryManageService, translationService, scope); diff --git a/src/Web/Grand.Web.Store/Controllers/ProductAttributeMappingController.cs b/src/Web/Grand.Web.Store/Controllers/ProductAttributeMappingController.cs index 7d0ff3863..e0dde0f45 100644 --- a/src/Web/Grand.Web.Store/Controllers/ProductAttributeMappingController.cs +++ b/src/Web/Grand.Web.Store/Controllers/ProductAttributeMappingController.cs @@ -15,9 +15,10 @@ namespace Grand.Web.Store.Controllers; [AuthorizeStore] [AuthorizeMenu] public class ProductAttributeMappingController( - IProductViewModelService productViewModelService, + IProductAddPopupViewModelService productAddPopupViewModelService, + IProductAttributeMappingViewModelService productAttributeMappingViewModelService, IProductService productService, ILanguageService languageService, IRequestTranslationService translationService, IAdminDataScope scope) - : BaseProductAttributeMappingController(productViewModelService, productService, languageService, translationService, scope); + : BaseProductAttributeMappingController(productAddPopupViewModelService, productAttributeMappingViewModelService, productService, languageService, translationService, scope); diff --git a/src/Web/Grand.Web.Store/Controllers/ProductBidController.cs b/src/Web/Grand.Web.Store/Controllers/ProductBidController.cs index 24d92f9bd..ff79450d3 100644 --- a/src/Web/Grand.Web.Store/Controllers/ProductBidController.cs +++ b/src/Web/Grand.Web.Store/Controllers/ProductBidController.cs @@ -14,9 +14,9 @@ namespace Grand.Web.Store.Controllers; [AuthorizeStore] [AuthorizeMenu] public class ProductBidController( - IProductViewModelService productViewModelService, + IProductBidViewModelService productBidViewModelService, IProductService productService, IRequestTranslationService translationService, IAuctionService auctionService, IAdminDataScope scope) - : BaseProductBidController(productViewModelService, productService, translationService, auctionService, scope); + : BaseProductBidController(productBidViewModelService, productService, translationService, auctionService, scope); diff --git a/src/Web/Grand.Web.Store/Controllers/ProductBundleController.cs b/src/Web/Grand.Web.Store/Controllers/ProductBundleController.cs index a2b37a6cb..a2cd8baed 100644 --- a/src/Web/Grand.Web.Store/Controllers/ProductBundleController.cs +++ b/src/Web/Grand.Web.Store/Controllers/ProductBundleController.cs @@ -14,8 +14,9 @@ namespace Grand.Web.Store.Controllers; [AuthorizeStore] [AuthorizeMenu] public class ProductBundleController( - IProductViewModelService productViewModelService, + IProductAddPopupViewModelService productAddPopupViewModelService, + IProductBundleViewModelService productBundleViewModelService, IProductService productService, IRequestTranslationService translationService, IAdminDataScope scope) - : BaseProductBundleController(productViewModelService, productService, translationService, scope); + : BaseProductBundleController(productAddPopupViewModelService, productBundleViewModelService, productService, translationService, scope); diff --git a/src/Web/Grand.Web.Store/Controllers/ProductCategoryController.cs b/src/Web/Grand.Web.Store/Controllers/ProductCategoryController.cs index 61b1b41f2..ad4d7abf5 100644 --- a/src/Web/Grand.Web.Store/Controllers/ProductCategoryController.cs +++ b/src/Web/Grand.Web.Store/Controllers/ProductCategoryController.cs @@ -14,8 +14,8 @@ namespace Grand.Web.Store.Controllers; [AuthorizeStore] [AuthorizeMenu] public class ProductCategoryController( - IProductViewModelService productViewModelService, + IProductCategoryViewModelService productCategoryViewModelService, IProductService productService, IRequestTranslationService translationService, IAdminDataScope scope) - : BaseProductCategoryController(productViewModelService, productService, translationService, scope); + : BaseProductCategoryController(productCategoryViewModelService, productService, translationService, scope); diff --git a/src/Web/Grand.Web.Store/Controllers/ProductCollectionController.cs b/src/Web/Grand.Web.Store/Controllers/ProductCollectionController.cs index b2bdbfb34..402b4430a 100644 --- a/src/Web/Grand.Web.Store/Controllers/ProductCollectionController.cs +++ b/src/Web/Grand.Web.Store/Controllers/ProductCollectionController.cs @@ -14,8 +14,8 @@ namespace Grand.Web.Store.Controllers; [AuthorizeStore] [AuthorizeMenu] public class ProductCollectionController( - IProductViewModelService productViewModelService, + IProductCollectionViewModelService productCollectionViewModelService, IProductService productService, IRequestTranslationService translationService, IAdminDataScope scope) - : BaseProductCollectionController(productViewModelService, productService, translationService, scope); + : BaseProductCollectionController(productCollectionViewModelService, productService, translationService, scope); diff --git a/src/Web/Grand.Web.Store/Controllers/ProductController.cs b/src/Web/Grand.Web.Store/Controllers/ProductController.cs index 4ff451b3c..b0770dccf 100644 --- a/src/Web/Grand.Web.Store/Controllers/ProductController.cs +++ b/src/Web/Grand.Web.Store/Controllers/ProductController.cs @@ -18,13 +18,14 @@ namespace Grand.Web.Store.Controllers; [AuthorizeMenu] public class ProductController( IProductViewModelService productViewModelService, + IProductAddPopupViewModelService productAddPopupViewModelService, IProductService productService, ILanguageService languageService, IRequestTranslationService translationService, IDateTimeService dateTimeService, IRequestPermissionService permissionService, IAdminDataScope scope) - : BaseProductController(productViewModelService, productService, languageService, translationService, + : BaseProductController(productViewModelService, productAddPopupViewModelService, productService, languageService, translationService, dateTimeService, permissionService, scope) { // Re-derived from the original Store ProductController.Edit(GET) (pre-Task-11: diff --git a/src/Web/Grand.Web.Store/Controllers/ProductPictureController.cs b/src/Web/Grand.Web.Store/Controllers/ProductPictureController.cs index 883eef413..b4fbfdf51 100644 --- a/src/Web/Grand.Web.Store/Controllers/ProductPictureController.cs +++ b/src/Web/Grand.Web.Store/Controllers/ProductPictureController.cs @@ -16,10 +16,10 @@ namespace Grand.Web.Store.Controllers; [AuthorizeStore] [AuthorizeMenu] public class ProductPictureController( - IProductViewModelService productViewModelService, + IProductPictureViewModelService productPictureViewModelService, IProductService productService, ILanguageService languageService, IRequestTranslationService translationService, IRequestPermissionService permissionService, IAdminDataScope scope) - : BaseProductPictureController(productViewModelService, productService, languageService, translationService, permissionService, scope); + : BaseProductPictureController(productPictureViewModelService, productService, languageService, translationService, permissionService, scope); diff --git a/src/Web/Grand.Web.Store/Controllers/ProductPriceController.cs b/src/Web/Grand.Web.Store/Controllers/ProductPriceController.cs index 175241a06..3f7077d63 100644 --- a/src/Web/Grand.Web.Store/Controllers/ProductPriceController.cs +++ b/src/Web/Grand.Web.Store/Controllers/ProductPriceController.cs @@ -15,9 +15,9 @@ namespace Grand.Web.Store.Controllers; [AuthorizeStore] [AuthorizeMenu] public class ProductPriceController( - IProductViewModelService productViewModelService, + IProductPriceViewModelService productPriceViewModelService, IProductService productService, IRequestTranslationService translationService, IDateTimeService dateTimeService, IAdminDataScope scope) - : BaseProductPriceController(productViewModelService, productService, translationService, dateTimeService, scope); + : BaseProductPriceController(productPriceViewModelService, productService, translationService, dateTimeService, scope); diff --git a/src/Web/Grand.Web.Store/Controllers/ProductRelatedController.cs b/src/Web/Grand.Web.Store/Controllers/ProductRelatedController.cs index 08f465227..ce785e36b 100644 --- a/src/Web/Grand.Web.Store/Controllers/ProductRelatedController.cs +++ b/src/Web/Grand.Web.Store/Controllers/ProductRelatedController.cs @@ -14,8 +14,9 @@ namespace Grand.Web.Store.Controllers; [AuthorizeStore] [AuthorizeMenu] public class ProductRelatedController( - IProductViewModelService productViewModelService, + IProductAddPopupViewModelService productAddPopupViewModelService, + IProductRelatedViewModelService productRelatedViewModelService, IProductService productService, IRequestTranslationService translationService, IAdminDataScope scope) - : BaseProductRelatedController(productViewModelService, productService, translationService, scope); + : BaseProductRelatedController(productAddPopupViewModelService, productRelatedViewModelService, productService, translationService, scope); diff --git a/src/Web/Grand.Web.Store/Controllers/ProductSpecificationController.cs b/src/Web/Grand.Web.Store/Controllers/ProductSpecificationController.cs index 37b1f1f72..191b48357 100644 --- a/src/Web/Grand.Web.Store/Controllers/ProductSpecificationController.cs +++ b/src/Web/Grand.Web.Store/Controllers/ProductSpecificationController.cs @@ -14,8 +14,8 @@ namespace Grand.Web.Store.Controllers; [AuthorizeStore] [AuthorizeMenu] public class ProductSpecificationController( - IProductViewModelService productViewModelService, + IProductSpecificationViewModelService productSpecificationViewModelService, IProductService productService, IRequestTranslationService translationService, IAdminDataScope scope) - : BaseProductSpecificationController(productViewModelService, productService, translationService, scope); + : BaseProductSpecificationController(productSpecificationViewModelService, productService, translationService, scope); diff --git a/src/Web/Grand.Web.Vendor/Controllers/ProductAssociatedController.cs b/src/Web/Grand.Web.Vendor/Controllers/ProductAssociatedController.cs index fae06caa5..f8e2b4bad 100644 --- a/src/Web/Grand.Web.Vendor/Controllers/ProductAssociatedController.cs +++ b/src/Web/Grand.Web.Vendor/Controllers/ProductAssociatedController.cs @@ -15,12 +15,13 @@ namespace Grand.Web.Vendor.Controllers; [AuthorizeVendor] [AuthorizeMenu] public class ProductAssociatedController( - IProductViewModelService productViewModelService, + IProductAddPopupViewModelService productAddPopupViewModelService, + IProductAssociatedViewModelService productAssociatedViewModelService, IProductService productService, IRequestTranslationService translationService, IAdminDataScope scope, IContextAccessor contextAccessor) - : BaseProductAssociatedController(productViewModelService, productService, translationService, scope) + : BaseProductAssociatedController(productAddPopupViewModelService, productAssociatedViewModelService, productService, translationService, scope) { // Vendor's original passed CurrentVendor.Id into GetAssociatedProducts(vendorId:) so a vendor only // sees the subset of a grouped product's associated products that they themselves own. Overriding diff --git a/src/Web/Grand.Web.Vendor/Controllers/ProductAttributeCombinationController.cs b/src/Web/Grand.Web.Vendor/Controllers/ProductAttributeCombinationController.cs index 4c2d5aaba..19385071e 100644 --- a/src/Web/Grand.Web.Vendor/Controllers/ProductAttributeCombinationController.cs +++ b/src/Web/Grand.Web.Vendor/Controllers/ProductAttributeCombinationController.cs @@ -14,9 +14,9 @@ namespace Grand.Web.Vendor.Controllers; [AuthorizeVendor] [AuthorizeMenu] public class ProductAttributeCombinationController( - IProductViewModelService productViewModelService, + IProductAttributeCombinationViewModelService productAttributeCombinationViewModelService, IProductService productService, IInventoryManageService inventoryManageService, IRequestTranslationService translationService, IAdminDataScope scope) - : BaseProductAttributeCombinationController(productViewModelService, productService, inventoryManageService, translationService, scope); + : BaseProductAttributeCombinationController(productAttributeCombinationViewModelService, productService, inventoryManageService, translationService, scope); diff --git a/src/Web/Grand.Web.Vendor/Controllers/ProductAttributeMappingController.cs b/src/Web/Grand.Web.Vendor/Controllers/ProductAttributeMappingController.cs index 65ee93019..bf816911d 100644 --- a/src/Web/Grand.Web.Vendor/Controllers/ProductAttributeMappingController.cs +++ b/src/Web/Grand.Web.Vendor/Controllers/ProductAttributeMappingController.cs @@ -15,9 +15,10 @@ namespace Grand.Web.Vendor.Controllers; [AuthorizeVendor] [AuthorizeMenu] public class ProductAttributeMappingController( - IProductViewModelService productViewModelService, + IProductAddPopupViewModelService productAddPopupViewModelService, + IProductAttributeMappingViewModelService productAttributeMappingViewModelService, IProductService productService, ILanguageService languageService, IRequestTranslationService translationService, IAdminDataScope scope) - : BaseProductAttributeMappingController(productViewModelService, productService, languageService, translationService, scope); + : BaseProductAttributeMappingController(productAddPopupViewModelService, productAttributeMappingViewModelService, productService, languageService, translationService, scope); diff --git a/src/Web/Grand.Web.Vendor/Controllers/ProductBidController.cs b/src/Web/Grand.Web.Vendor/Controllers/ProductBidController.cs index 4af7ea3fa..874ccfea1 100644 --- a/src/Web/Grand.Web.Vendor/Controllers/ProductBidController.cs +++ b/src/Web/Grand.Web.Vendor/Controllers/ProductBidController.cs @@ -14,9 +14,9 @@ namespace Grand.Web.Vendor.Controllers; [AuthorizeVendor] [AuthorizeMenu] public class ProductBidController( - IProductViewModelService productViewModelService, + IProductBidViewModelService productBidViewModelService, IProductService productService, IRequestTranslationService translationService, IAuctionService auctionService, IAdminDataScope scope) - : BaseProductBidController(productViewModelService, productService, translationService, auctionService, scope); + : BaseProductBidController(productBidViewModelService, productService, translationService, auctionService, scope); diff --git a/src/Web/Grand.Web.Vendor/Controllers/ProductBundleController.cs b/src/Web/Grand.Web.Vendor/Controllers/ProductBundleController.cs index 7930fa945..8317c4b83 100644 --- a/src/Web/Grand.Web.Vendor/Controllers/ProductBundleController.cs +++ b/src/Web/Grand.Web.Vendor/Controllers/ProductBundleController.cs @@ -15,11 +15,12 @@ namespace Grand.Web.Vendor.Controllers; [AuthorizeVendor] [AuthorizeMenu] public class ProductBundleController( - IProductViewModelService productViewModelService, + IProductAddPopupViewModelService productAddPopupViewModelService, + IProductBundleViewModelService productBundleViewModelService, IProductService productService, IRequestTranslationService translationService, IAdminDataScope scope) - : BaseProductBundleController(productViewModelService, productService, translationService, scope) + : BaseProductBundleController(productAddPopupViewModelService, productBundleViewModelService, productService, translationService, scope) { // Vendor's original AddPopup(POST) actions returned Content(ModelState.GetErrors()) on an invalid // model, instead of Admin/Store's re-prepare-and-View. Overriding the base's Admin/Store default, diff --git a/src/Web/Grand.Web.Vendor/Controllers/ProductCategoryController.cs b/src/Web/Grand.Web.Vendor/Controllers/ProductCategoryController.cs index 9e3c44605..89bf06c74 100644 --- a/src/Web/Grand.Web.Vendor/Controllers/ProductCategoryController.cs +++ b/src/Web/Grand.Web.Vendor/Controllers/ProductCategoryController.cs @@ -14,8 +14,8 @@ namespace Grand.Web.Vendor.Controllers; [AuthorizeVendor] [AuthorizeMenu] public class ProductCategoryController( - IProductViewModelService productViewModelService, + IProductCategoryViewModelService productCategoryViewModelService, IProductService productService, IRequestTranslationService translationService, IAdminDataScope scope) - : BaseProductCategoryController(productViewModelService, productService, translationService, scope); + : BaseProductCategoryController(productCategoryViewModelService, productService, translationService, scope); diff --git a/src/Web/Grand.Web.Vendor/Controllers/ProductCollectionController.cs b/src/Web/Grand.Web.Vendor/Controllers/ProductCollectionController.cs index be6571f24..0b72fa7e3 100644 --- a/src/Web/Grand.Web.Vendor/Controllers/ProductCollectionController.cs +++ b/src/Web/Grand.Web.Vendor/Controllers/ProductCollectionController.cs @@ -14,8 +14,8 @@ namespace Grand.Web.Vendor.Controllers; [AuthorizeVendor] [AuthorizeMenu] public class ProductCollectionController( - IProductViewModelService productViewModelService, + IProductCollectionViewModelService productCollectionViewModelService, IProductService productService, IRequestTranslationService translationService, IAdminDataScope scope) - : BaseProductCollectionController(productViewModelService, productService, translationService, scope); + : BaseProductCollectionController(productCollectionViewModelService, productService, translationService, scope); diff --git a/src/Web/Grand.Web.Vendor/Controllers/ProductController.cs b/src/Web/Grand.Web.Vendor/Controllers/ProductController.cs index b790514ca..a15f7a401 100644 --- a/src/Web/Grand.Web.Vendor/Controllers/ProductController.cs +++ b/src/Web/Grand.Web.Vendor/Controllers/ProductController.cs @@ -18,11 +18,12 @@ namespace Grand.Web.Vendor.Controllers; [AuthorizeMenu] public class ProductController( IProductViewModelService productViewModelService, + IProductAddPopupViewModelService productAddPopupViewModelService, IProductService productService, ILanguageService languageService, IRequestTranslationService translationService, IDateTimeService dateTimeService, IRequestPermissionService permissionService, IAdminDataScope scope) - : BaseProductController(productViewModelService, productService, languageService, translationService, + : BaseProductController(productViewModelService, productAddPopupViewModelService, productService, languageService, translationService, dateTimeService, permissionService, scope); diff --git a/src/Web/Grand.Web.Vendor/Controllers/ProductPictureController.cs b/src/Web/Grand.Web.Vendor/Controllers/ProductPictureController.cs index dde7eeca7..95f369c47 100644 --- a/src/Web/Grand.Web.Vendor/Controllers/ProductPictureController.cs +++ b/src/Web/Grand.Web.Vendor/Controllers/ProductPictureController.cs @@ -16,10 +16,10 @@ namespace Grand.Web.Vendor.Controllers; [AuthorizeVendor] [AuthorizeMenu] public class ProductPictureController( - IProductViewModelService productViewModelService, + IProductPictureViewModelService productPictureViewModelService, IProductService productService, ILanguageService languageService, IRequestTranslationService translationService, IRequestPermissionService permissionService, IAdminDataScope scope) - : BaseProductPictureController(productViewModelService, productService, languageService, translationService, permissionService, scope); + : BaseProductPictureController(productPictureViewModelService, productService, languageService, translationService, permissionService, scope); diff --git a/src/Web/Grand.Web.Vendor/Controllers/ProductPriceController.cs b/src/Web/Grand.Web.Vendor/Controllers/ProductPriceController.cs index 226c25ba1..bc458dded 100644 --- a/src/Web/Grand.Web.Vendor/Controllers/ProductPriceController.cs +++ b/src/Web/Grand.Web.Vendor/Controllers/ProductPriceController.cs @@ -15,9 +15,9 @@ namespace Grand.Web.Vendor.Controllers; [AuthorizeVendor] [AuthorizeMenu] public class ProductPriceController( - IProductViewModelService productViewModelService, + IProductPriceViewModelService productPriceViewModelService, IProductService productService, IRequestTranslationService translationService, IDateTimeService dateTimeService, IAdminDataScope scope) - : BaseProductPriceController(productViewModelService, productService, translationService, dateTimeService, scope); + : BaseProductPriceController(productPriceViewModelService, productService, translationService, dateTimeService, scope); diff --git a/src/Web/Grand.Web.Vendor/Controllers/ProductRelatedController.cs b/src/Web/Grand.Web.Vendor/Controllers/ProductRelatedController.cs index a5e8cd422..7c534ac57 100644 --- a/src/Web/Grand.Web.Vendor/Controllers/ProductRelatedController.cs +++ b/src/Web/Grand.Web.Vendor/Controllers/ProductRelatedController.cs @@ -15,11 +15,12 @@ namespace Grand.Web.Vendor.Controllers; [AuthorizeVendor] [AuthorizeMenu] public class ProductRelatedController( - IProductViewModelService productViewModelService, + IProductAddPopupViewModelService productAddPopupViewModelService, + IProductRelatedViewModelService productRelatedViewModelService, IProductService productService, IRequestTranslationService translationService, IAdminDataScope scope) - : BaseProductRelatedController(productViewModelService, productService, translationService, scope) + : BaseProductRelatedController(productAddPopupViewModelService, productRelatedViewModelService, productService, translationService, scope) { // Vendor's original AddPopup(POST) actions returned Content(ModelState.GetErrors()) on an invalid // model, instead of Admin/Store's re-prepare-and-View. Overriding the base's Admin/Store default, diff --git a/src/Web/Grand.Web.Vendor/Controllers/ProductSpecificationController.cs b/src/Web/Grand.Web.Vendor/Controllers/ProductSpecificationController.cs index 58aad6a4e..bad45468a 100644 --- a/src/Web/Grand.Web.Vendor/Controllers/ProductSpecificationController.cs +++ b/src/Web/Grand.Web.Vendor/Controllers/ProductSpecificationController.cs @@ -14,8 +14,8 @@ namespace Grand.Web.Vendor.Controllers; [AuthorizeVendor] [AuthorizeMenu] public class ProductSpecificationController( - IProductViewModelService productViewModelService, + IProductSpecificationViewModelService productSpecificationViewModelService, IProductService productService, IRequestTranslationService translationService, IAdminDataScope scope) - : BaseProductSpecificationController(productViewModelService, productService, translationService, scope); + : BaseProductSpecificationController(productSpecificationViewModelService, productService, translationService, scope);