From cf1206024346c365237bf547d93f0c35009e2430 Mon Sep 17 00:00:00 2001 From: support Date: Thu, 22 Oct 2026 20:44:13 +0200 Subject: [PATCH 1/2] Split CustomerViewModelService into one service per customer edit tab Address, loyalty points, shopping cart, products (prices and personalized), out-of-stock subscriptions, notes and contact forms move to their own ICustomerViewModelService. The moved members inject the services they used to resolve from HttpContext.RequestServices. CustomerViewModelService keeps the customer list and form, insert/update/delete and SendEmail. --- .../BaseCustomerControllerTests.cs | 67 ++- .../BaseCustomerManagementControllerTests.cs | 22 +- .../CustomerAddressViewModelServiceTests.cs | 49 ++ ...tomerLoyaltyPointsViewModelServiceTests.cs | 69 +++ .../CustomerNoteViewModelServiceTests.cs | 79 +++ .../CustomerProductViewModelServiceTests.cs | 201 +++++++ .../Services/CustomerViewModelServiceTests.cs | 268 +-------- .../Controllers/CustomerControllerTests.cs | 16 +- .../Controllers/CustomerController.cs | 14 +- .../Controllers/BaseCustomerController.cs | 55 +- .../BaseCustomerManagementController.cs | 21 +- .../ICustomerAddressViewModelService.cs | 19 + .../ICustomerContactFormViewModelService.cs | 9 + .../ICustomerLoyaltyPointsViewModelService.cs | 13 + .../ICustomerNoteViewModelService.cs | 14 + ...rOutOfStockSubscriptionViewModelService.cs | 9 + .../ICustomerProductViewModelService.cs | 24 + .../ICustomerShoppingCartViewModelService.cs | 11 + .../Interfaces/ICustomerViewModelService.cs | 58 +- .../CustomerAddressViewModelService.cs | 148 +++++ .../CustomerContactFormViewModelService.cs | 35 ++ .../CustomerLoyaltyPointsViewModelService.cs | 43 ++ .../Services/CustomerNoteViewModelService.cs | 85 +++ ...rOutOfStockSubscriptionViewModelService.cs | 53 ++ .../CustomerProductViewModelService.cs | 158 ++++++ .../CustomerShoppingCartViewModelService.cs | 111 ++++ .../Services/CustomerViewModelService.cs | 535 ------------------ .../Startup/StartupApplication.cs | 9 +- .../Controllers/CustomerController.cs | 11 +- 29 files changed, 1285 insertions(+), 921 deletions(-) create mode 100644 src/Tests/Grand.Web.Admin.Tests/Services/CustomerAddressViewModelServiceTests.cs create mode 100644 src/Tests/Grand.Web.Admin.Tests/Services/CustomerLoyaltyPointsViewModelServiceTests.cs create mode 100644 src/Tests/Grand.Web.Admin.Tests/Services/CustomerNoteViewModelServiceTests.cs create mode 100644 src/Tests/Grand.Web.Admin.Tests/Services/CustomerProductViewModelServiceTests.cs create mode 100644 src/Web/Grand.Web.AdminShared/Interfaces/ICustomerAddressViewModelService.cs create mode 100644 src/Web/Grand.Web.AdminShared/Interfaces/ICustomerContactFormViewModelService.cs create mode 100644 src/Web/Grand.Web.AdminShared/Interfaces/ICustomerLoyaltyPointsViewModelService.cs create mode 100644 src/Web/Grand.Web.AdminShared/Interfaces/ICustomerNoteViewModelService.cs create mode 100644 src/Web/Grand.Web.AdminShared/Interfaces/ICustomerOutOfStockSubscriptionViewModelService.cs create mode 100644 src/Web/Grand.Web.AdminShared/Interfaces/ICustomerProductViewModelService.cs create mode 100644 src/Web/Grand.Web.AdminShared/Interfaces/ICustomerShoppingCartViewModelService.cs create mode 100644 src/Web/Grand.Web.AdminShared/Services/CustomerAddressViewModelService.cs create mode 100644 src/Web/Grand.Web.AdminShared/Services/CustomerContactFormViewModelService.cs create mode 100644 src/Web/Grand.Web.AdminShared/Services/CustomerLoyaltyPointsViewModelService.cs create mode 100644 src/Web/Grand.Web.AdminShared/Services/CustomerNoteViewModelService.cs create mode 100644 src/Web/Grand.Web.AdminShared/Services/CustomerOutOfStockSubscriptionViewModelService.cs create mode 100644 src/Web/Grand.Web.AdminShared/Services/CustomerProductViewModelService.cs create mode 100644 src/Web/Grand.Web.AdminShared/Services/CustomerShoppingCartViewModelService.cs diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerControllerTests.cs index 29fcf6fce4..14f5b63922 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerControllerTests.cs @@ -1,4 +1,4 @@ -using Grand.Business.Core.Interfaces.Common.Addresses; +using Grand.Business.Core.Interfaces.Common.Addresses; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Customers; using Grand.Business.Core.Interfaces.Marketing.Customers; @@ -27,11 +27,16 @@ namespace Grand.Web.Admin.Tests.Controllers; public class BaseCustomerControllerTests { // BaseCustomerController is abstract; minimal subclass so actions can be invoked directly. - // The full 17-dependency constructor is declared once here; every later task's tests reuse + // The full 22-dependency constructor is declared once here; every later task's tests reuse // this same TestCustomerController and Setup(). protected class TestCustomerController( ICustomerService customerService, ICustomerViewModelService customerViewModelService, + ICustomerAddressViewModelService customerAddressViewModelService, + ICustomerLoyaltyPointsViewModelService customerLoyaltyPointsViewModelService, + ICustomerShoppingCartViewModelService customerShoppingCartViewModelService, + ICustomerProductViewModelService customerProductViewModelService, + ICustomerOutOfStockSubscriptionViewModelService customerOutOfStockSubscriptionViewModelService, ICustomerManagerService customerManagerService, ICustomerProductService customerProductService, Grand.Business.Core.Interfaces.Catalog.Products.IProductReviewService productReviewService, @@ -47,7 +52,9 @@ protected class TestCustomerController( IContextAccessor contextAccessor, CustomerSettings customerSettings, IAdminDataScope scope) - : BaseCustomerController(customerService, customerViewModelService, customerManagerService, + : BaseCustomerController(customerService, customerViewModelService, customerAddressViewModelService, + customerLoyaltyPointsViewModelService, customerShoppingCartViewModelService, + customerProductViewModelService, customerOutOfStockSubscriptionViewModelService, customerManagerService, customerProductService, productReviewService, productReviewViewModelService, productViewModelService, customerAttributeParser, customerAttributeService, addressAttributeParser, addressAttributeService, messageProviderService, groupService, @@ -59,6 +66,11 @@ protected class TestCustomerController( protected TestCustomerController Controller; protected Mock CustomerServiceMock; protected Mock CustomerViewModelServiceMock; + protected Mock CustomerAddressViewModelServiceMock; + protected Mock CustomerLoyaltyPointsViewModelServiceMock; + protected Mock CustomerShoppingCartViewModelServiceMock; + protected Mock CustomerProductViewModelServiceMock; + protected Mock CustomerOutOfStockSubscriptionViewModelServiceMock; protected Mock CustomerManagerServiceMock; protected Mock GroupServiceMock; protected Mock> ScopeMock; @@ -72,6 +84,11 @@ public void Setup() { CustomerServiceMock = new Mock(); CustomerViewModelServiceMock = new Mock(); + CustomerAddressViewModelServiceMock = new Mock(); + CustomerLoyaltyPointsViewModelServiceMock = new Mock(); + CustomerShoppingCartViewModelServiceMock = new Mock(); + CustomerProductViewModelServiceMock = new Mock(); + CustomerOutOfStockSubscriptionViewModelServiceMock = new Mock(); CustomerManagerServiceMock = new Mock(); GroupServiceMock = new Mock(); ScopeMock = new Mock>(); @@ -89,6 +106,11 @@ public void Setup() Controller = new TestCustomerController( CustomerServiceMock.Object, CustomerViewModelServiceMock.Object, + CustomerAddressViewModelServiceMock.Object, + CustomerLoyaltyPointsViewModelServiceMock.Object, + CustomerShoppingCartViewModelServiceMock.Object, + CustomerProductViewModelServiceMock.Object, + CustomerOutOfStockSubscriptionViewModelServiceMock.Object, CustomerManagerServiceMock.Object, CustomerProductServiceMock.Object, ProductReviewServiceMock.Object, @@ -355,6 +377,9 @@ public async Task Delete_SelfDelete_ErrorsAndRedirectsWithoutDeleting() contextAccessorMock.Setup(c => c.WorkContext).Returns(workContextMock.Object); // Rebuild controller with a context accessor whose CurrentCustomer matches the target id. var controller = new TestCustomerController(CustomerServiceMock.Object, CustomerViewModelServiceMock.Object, + CustomerAddressViewModelServiceMock.Object, CustomerLoyaltyPointsViewModelServiceMock.Object, + CustomerShoppingCartViewModelServiceMock.Object, CustomerProductViewModelServiceMock.Object, + CustomerOutOfStockSubscriptionViewModelServiceMock.Object, CustomerManagerServiceMock.Object, new Mock().Object, new Mock().Object, new Mock().Object, @@ -386,14 +411,14 @@ public async Task LoyaltyPointsHistoryAdd_StoreScope_ForcesStaffStoreId_Ignoring CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync(customer); ScopeMock.Setup(s => s.HasAccess(customer)).ReturnsAsync(true); ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - CustomerViewModelServiceMock + CustomerLoyaltyPointsViewModelServiceMock .Setup(v => v.InsertLoyaltyPointsHistory(customer, "store-1", 10, "msg")) .ReturnsAsync(new Grand.Domain.Orders.LoyaltyPointsHistory()); // Caller submits a different, attacker-controlled storeId — must be ignored under Store scope. await Controller.LoyaltyPointsHistoryAdd("c1", "attacker-store", 10, "msg"); - CustomerViewModelServiceMock.Verify(v => v.InsertLoyaltyPointsHistory(customer, "store-1", 10, "msg"), Times.Once); + CustomerLoyaltyPointsViewModelServiceMock.Verify(v => v.InsertLoyaltyPointsHistory(customer, "store-1", 10, "msg"), Times.Once); } [TestMethod] @@ -403,13 +428,13 @@ public async Task LoyaltyPointsHistoryAdd_GlobalScope_UsesSubmittedStoreId() CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync(customer); ScopeMock.Setup(s => s.HasAccess(customer)).ReturnsAsync(true); ScopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - CustomerViewModelServiceMock + CustomerLoyaltyPointsViewModelServiceMock .Setup(v => v.InsertLoyaltyPointsHistory(customer, "admin-submitted-store", 10, "msg")) .ReturnsAsync(new Grand.Domain.Orders.LoyaltyPointsHistory()); await Controller.LoyaltyPointsHistoryAdd("c1", "admin-submitted-store", 10, "msg"); - CustomerViewModelServiceMock.Verify(v => v.InsertLoyaltyPointsHistory(customer, "admin-submitted-store", 10, "msg"), Times.Once); + CustomerLoyaltyPointsViewModelServiceMock.Verify(v => v.InsertLoyaltyPointsHistory(customer, "admin-submitted-store", 10, "msg"), Times.Once); } [TestMethod] @@ -429,7 +454,7 @@ public async Task AddressCreatePost_Authorized_InsertsAndRedirects() ScopeMock.Setup(s => s.HasAccess(customer)).ReturnsAsync(true); var model = new CustomerAddressModel { CustomerId = "c1", Address = new AddressModel() }; var inserted = new Grand.Domain.Common.Address { Id = "a1" }; - CustomerViewModelServiceMock + CustomerAddressViewModelServiceMock .Setup(v => v.InsertAddressModel(customer, model, It.IsAny>())) .ReturnsAsync(inserted); @@ -475,7 +500,7 @@ await Assert.ThrowsAsync(() => public async Task GetCartList_GlobalScope_NoOwnershipCheck_CallsServiceDirectly() { ScopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - CustomerViewModelServiceMock.Setup(v => v.PrepareShoppingCartItemModel("any-customer-id", 1)) + CustomerShoppingCartViewModelServiceMock.Setup(v => v.PrepareShoppingCartItemModel("any-customer-id", 1)) .ReturnsAsync(new List()); var result = await Controller.GetCartList("any-customer-id", 1); @@ -495,7 +520,7 @@ public async Task GetCartList_StoreScope_DeniedOwnership_ReturnsEmpty() var json = result as JsonResult; var data = json.Value as DataSourceResult; Assert.AreEqual(0, data.Total); - CustomerViewModelServiceMock.Verify(v => v.PrepareShoppingCartItemModel(It.IsAny(), It.IsAny()), Times.Never); + CustomerShoppingCartViewModelServiceMock.Verify(v => v.PrepareShoppingCartItemModel(It.IsAny(), It.IsAny()), Times.Never); } [TestMethod] @@ -512,12 +537,12 @@ public async Task ReviewDelete_StoreScope_MismatchedStore_ThrowsArgumentExceptio public async Task DeleteProductPrice_GlobalScope_NoOwnershipCheck_CallsServiceDirectly() { ScopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - CustomerViewModelServiceMock.Setup(v => v.DeleteProductPrice("pp1")).Returns(Task.CompletedTask); + CustomerProductViewModelServiceMock.Setup(v => v.DeleteProductPrice("pp1")).Returns(Task.CompletedTask); var result = await Controller.DeleteProductPrice("pp1"); Assert.IsInstanceOfType(result, typeof(JsonResult)); - CustomerViewModelServiceMock.Verify(v => v.DeleteProductPrice("pp1"), Times.Once); + CustomerProductViewModelServiceMock.Verify(v => v.DeleteProductPrice("pp1"), Times.Once); } [TestMethod] @@ -530,7 +555,7 @@ public async Task DeleteProductPrice_StoreScope_PriceNotFound_NoOpsSilently() var result = await Controller.DeleteProductPrice("pp1"); Assert.IsInstanceOfType(result, typeof(JsonResult)); - CustomerViewModelServiceMock.Verify(v => v.DeleteProductPrice(It.IsAny()), Times.Never); + CustomerProductViewModelServiceMock.Verify(v => v.DeleteProductPrice(It.IsAny()), Times.Never); CustomerProductServiceMock.Verify(s => s.DeleteCustomerProductPrice(It.IsAny()), Times.Never); } @@ -549,7 +574,7 @@ public async Task DeleteProductPrice_StoreScope_DeniedOwnership_NoOpsSilently() var result = await Controller.DeleteProductPrice("pp1"); Assert.IsInstanceOfType(result, typeof(JsonResult)); - CustomerViewModelServiceMock.Verify(v => v.DeleteProductPrice(It.IsAny()), Times.Never); + CustomerProductViewModelServiceMock.Verify(v => v.DeleteProductPrice(It.IsAny()), Times.Never); CustomerProductServiceMock.Verify(s => s.DeleteCustomerProductPrice(It.IsAny()), Times.Never); } @@ -577,7 +602,7 @@ public async Task ProductsPrice_StoreScope_DeniedOwnership_ReturnsEmpty() var json = result as JsonResult; var data = json.Value as DataSourceResult; Assert.AreEqual(0, data.Total); - CustomerViewModelServiceMock.Verify( + CustomerProductViewModelServiceMock.Verify( v => v.PrepareProductPriceModel(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } @@ -585,14 +610,14 @@ public async Task ProductsPrice_StoreScope_DeniedOwnership_ReturnsEmpty() public async Task ProductsPrice_GlobalScope_NoOwnershipCheck_CallsServiceDirectly() { ScopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - CustomerViewModelServiceMock.Setup(v => v.PrepareProductPriceModel("c1", 1, 10)) + CustomerProductViewModelServiceMock.Setup(v => v.PrepareProductPriceModel("c1", 1, 10)) .ReturnsAsync((Enumerable.Empty(), 0)); var result = await Controller.ProductsPrice(new DataSourceRequest { Page = 1, PageSize = 10 }, "c1"); Assert.IsInstanceOfType(result, typeof(JsonResult)); CustomerServiceMock.Verify(s => s.GetCustomerById(It.IsAny()), Times.Never); - CustomerViewModelServiceMock.Verify(v => v.PrepareProductPriceModel("c1", 1, 10), Times.Once); + CustomerProductViewModelServiceMock.Verify(v => v.PrepareProductPriceModel("c1", 1, 10), Times.Once); } [TestMethod] @@ -606,7 +631,7 @@ public async Task PersonalizedProducts_StoreScope_DeniedOwnership_ReturnsEmpty() var json = result as JsonResult; var data = json.Value as DataSourceResult; Assert.AreEqual(0, data.Total); - CustomerViewModelServiceMock.Verify( + CustomerProductViewModelServiceMock.Verify( v => v.PreparePersonalizedProducts(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } @@ -614,14 +639,14 @@ public async Task PersonalizedProducts_StoreScope_DeniedOwnership_ReturnsEmpty() public async Task PersonalizedProducts_GlobalScope_NoOwnershipCheck_CallsServiceDirectly() { ScopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - CustomerViewModelServiceMock.Setup(v => v.PreparePersonalizedProducts("c1", 1, 10)) + CustomerProductViewModelServiceMock.Setup(v => v.PreparePersonalizedProducts("c1", 1, 10)) .ReturnsAsync((Enumerable.Empty(), 0)); var result = await Controller.PersonalizedProducts(new DataSourceRequest { Page = 1, PageSize = 10 }, "c1"); Assert.IsInstanceOfType(result, typeof(JsonResult)); CustomerServiceMock.Verify(s => s.GetCustomerById(It.IsAny()), Times.Never); - CustomerViewModelServiceMock.Verify(v => v.PreparePersonalizedProducts("c1", 1, 10), Times.Once); + CustomerProductViewModelServiceMock.Verify(v => v.PreparePersonalizedProducts("c1", 1, 10), Times.Once); } [TestMethod] @@ -659,7 +684,7 @@ public async Task ReviewList_GlobalScope_NoOwnershipCheck_CallsServiceDirectly() public async Task OutOfStockSubscriptionList_GlobalScope_NoOwnershipCheck() { ScopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - CustomerViewModelServiceMock + CustomerOutOfStockSubscriptionViewModelServiceMock .Setup(v => v.PrepareOutOfStockSubscriptionModel("c1", 1, 10)) .ReturnsAsync((Enumerable.Empty(), 0)); diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerManagementControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerManagementControllerTests.cs index 7fd6b31677..71d2406d5a 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerManagementControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerManagementControllerTests.cs @@ -1,4 +1,4 @@ -using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Common.Addresses; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Customers; @@ -29,6 +29,13 @@ public class BaseCustomerManagementControllerTests { private class TestManagementController( ICustomerService customerService, ICustomerViewModelService customerViewModelService, + ICustomerAddressViewModelService customerAddressViewModelService, + ICustomerLoyaltyPointsViewModelService customerLoyaltyPointsViewModelService, + ICustomerShoppingCartViewModelService customerShoppingCartViewModelService, + ICustomerProductViewModelService customerProductViewModelService, + ICustomerOutOfStockSubscriptionViewModelService customerOutOfStockSubscriptionViewModelService, + ICustomerNoteViewModelService customerNoteViewModelService, + ICustomerContactFormViewModelService customerContactFormViewModelService, ICustomerManagerService customerManagerService, ICustomerProductService customerProductService, IProductReviewService productReviewService, IProductReviewViewModelService productReviewViewModelService, IProductViewModelService productViewModelService, ICustomerAttributeParser customerAttributeParser, @@ -37,8 +44,10 @@ private class TestManagementController( IGroupService groupService, IRequestTranslationService translationService, IContextAccessor contextAccessor, CustomerSettings customerSettings, IAdminDataScope scope, IRequestPermissionService permissionService, IExportManager exportManager) - : BaseCustomerManagementController(customerService, customerViewModelService, customerManagerService, - customerProductService, productReviewService, productReviewViewModelService, productViewModelService, + : BaseCustomerManagementController(customerService, customerViewModelService, customerAddressViewModelService, + customerLoyaltyPointsViewModelService, customerShoppingCartViewModelService, customerProductViewModelService, + customerOutOfStockSubscriptionViewModelService, customerNoteViewModelService, + customerContactFormViewModelService, customerManagerService, customerProductService, productReviewService, productReviewViewModelService, productViewModelService, customerAttributeParser, customerAttributeService, addressAttributeParser, addressAttributeService, messageProviderService, groupService, translationService, contextAccessor, customerSettings, scope, permissionService, exportManager) @@ -70,7 +79,12 @@ public void Setup() translationServiceMock.Setup(t => t.GetResource(It.IsAny())).Returns("resource"); _controller = new TestManagementController(_customerServiceMock.Object, - new Mock().Object, new Mock().Object, + new Mock().Object, new Mock().Object, + new Mock().Object, + new Mock().Object, new Mock().Object, + new Mock().Object, + new Mock().Object, new Mock().Object, + new Mock().Object, new Mock().Object, new Mock().Object, new Mock().Object, new Mock().Object, new Mock().Object, new Mock().Object, diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/CustomerAddressViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerAddressViewModelServiceTests.cs new file mode 100644 index 0000000000..2f3857280c --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerAddressViewModelServiceTests.cs @@ -0,0 +1,49 @@ +using Grand.Business.Core.Interfaces.Common.Addresses; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Domain.Common; +using Grand.Domain.Customers; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Services; +using Grand.Web.Common.Localization; +using Moq; +using Assert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Grand.Web.Admin.Tests.Services; + +[TestClass] +public class CustomerAddressViewModelServiceTests +{ + private Mock _customerServiceMock; + private CustomerAddressViewModelService _customerAddressViewModelService; + + [TestInitialize] + public void Setup() + { + _customerServiceMock = new Mock(); + _customerServiceMock.Setup(c => c.UpdateCustomerInAdminPanel(It.IsAny())).Returns(Task.CompletedTask); + + _customerAddressViewModelService = new CustomerAddressViewModelService( + _customerServiceMock.Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new AddressSettings()); + } + + [TestMethod] + public async Task DeleteAddress_RemovesAddressAndUpdatesCustomer() + { + var address = new Address { Id = "a1" }; + var customer = new Customer(); + customer.Addresses.Add(address); + + await _customerAddressViewModelService.DeleteAddress(customer, address); + + Assert.IsFalse(customer.Addresses.Any(a => a.Id == "a1")); + _customerServiceMock.Verify(c => c.UpdateCustomerInAdminPanel(customer), Times.Once); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/CustomerLoyaltyPointsViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerLoyaltyPointsViewModelServiceTests.cs new file mode 100644 index 0000000000..981f99ece2 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerLoyaltyPointsViewModelServiceTests.cs @@ -0,0 +1,69 @@ +using Grand.Business.Core.Interfaces.Checkout.Orders; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Domain.Customers; +using Grand.Domain.Orders; +using Grand.Web.AdminShared.Services; +using Moq; +using Assert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Grand.Web.Admin.Tests.Services; + +[TestClass] +public class CustomerLoyaltyPointsViewModelServiceTests +{ + private Mock _loyaltyPointsServiceMock; + private Mock _storeServiceMock; + private CustomerLoyaltyPointsViewModelService _customerLoyaltyPointsViewModelService; + + [TestInitialize] + public void Setup() + { + _loyaltyPointsServiceMock = new Mock(); + _storeServiceMock = new Mock(); + var dateTimeServiceMock = new Mock(); + dateTimeServiceMock.Setup(d => d.ConvertToUserTime(It.IsAny(), It.IsAny())) + .Returns((dt, _) => dt); + + _customerLoyaltyPointsViewModelService = new CustomerLoyaltyPointsViewModelService( + _loyaltyPointsServiceMock.Object, + _storeServiceMock.Object, + dateTimeServiceMock.Object); + } + + [TestMethod] + public async Task PrepareLoyaltyPointsHistoryModel_MapsHistory() + { + _loyaltyPointsServiceMock + .Setup(l => l.GetLoyaltyPointsHistory(It.IsAny(), It.IsAny(), true)) + .ReturnsAsync(new List { + new() { StoreId = "store-1", Points = 5, PointsBalance = 10, Message = "added" } + }); + _storeServiceMock.Setup(s => s.GetStoreById("store-1")) + .ReturnsAsync(new Grand.Domain.Stores.Store { Id = "store-1", Shortcut = "Store 1" }); + + var result = (await _customerLoyaltyPointsViewModelService.PrepareLoyaltyPointsHistoryModel("c1")).ToList(); + + Assert.AreEqual(1, result.Count); + Assert.AreEqual("Store 1", result[0].StoreName); + Assert.AreEqual(5, result[0].Points); + } + + [TestMethod] + public async Task InsertLoyaltyPointsHistory_DelegatesToService() + { + var history = new LoyaltyPointsHistory(); + _loyaltyPointsServiceMock + .Setup(l => l.AddLoyaltyPointsHistory("c1", 10, "store-1", "msg", It.IsAny(), It.IsAny())) + .ReturnsAsync(history); + + var result = await _customerLoyaltyPointsViewModelService.InsertLoyaltyPointsHistory( + new Customer { Id = "c1" }, "store-1", 10, "msg"); + + Assert.AreSame(history, result); + _loyaltyPointsServiceMock.Verify( + l => l.AddLoyaltyPointsHistory("c1", 10, "store-1", "msg", It.IsAny(), It.IsAny()), + Times.Once); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/CustomerNoteViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerNoteViewModelServiceTests.cs new file mode 100644 index 0000000000..22666cc311 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerNoteViewModelServiceTests.cs @@ -0,0 +1,79 @@ +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Business.Core.Interfaces.Messages; +using Grand.Business.Core.Interfaces.Storage; +using Grand.Domain.Customers; +using Grand.Domain.Media; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Services; +using Moq; +using Assert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Grand.Web.Admin.Tests.Services; + +[TestClass] +public class CustomerNoteViewModelServiceTests +{ + private Mock _customerNoteServiceMock; + private Mock _downloadServiceMock; + private CustomerNoteViewModelService _customerNoteViewModelService; + + [TestInitialize] + public void Setup() + { + _customerNoteServiceMock = new Mock(); + _downloadServiceMock = new Mock(); + _customerNoteServiceMock.Setup(c => c.InsertCustomerNote(It.IsAny())) + .Returns(Task.CompletedTask); + + _customerNoteViewModelService = new CustomerNoteViewModelService( + _customerNoteServiceMock.Object, + new Mock().Object, + _downloadServiceMock.Object, + new Mock().Object, + new Mock().Object, + new Mock().Object); + } + + [TestMethod] + public async Task InsertCustomerNote_InsertsNoteWithMappedFields() + { + var note = await _customerNoteViewModelService.InsertCustomerNote("c1", "d1", false, "title", "message"); + + Assert.AreEqual("c1", note.CustomerId); + Assert.AreEqual("d1", note.DownloadId); + Assert.AreEqual("title", note.Title); + Assert.AreEqual("message", note.Note); + Assert.IsFalse(note.DisplayToCustomer); + _customerNoteServiceMock.Verify( + c => c.InsertCustomerNote(It.Is(x => x.CustomerId == "c1" && x.Title == "title")), + Times.Once); + } + + [TestMethod] + public async Task DeleteCustomerNote_DeletesNoteAndAttachment() + { + var note = new CustomerNote { Id = "n1", DownloadId = "d1" }; + var download = new Download { Id = "d1" }; + _customerNoteServiceMock.Setup(c => c.GetCustomerNote("n1")).ReturnsAsync(note); + _customerNoteServiceMock.Setup(c => c.DeleteCustomerNote(It.IsAny())).Returns(Task.CompletedTask); + _downloadServiceMock.Setup(d => d.GetDownloadById("d1")).ReturnsAsync(download); + _downloadServiceMock.Setup(d => d.DeleteDownload(It.IsAny())).Returns(Task.CompletedTask); + + await _customerNoteViewModelService.DeleteCustomerNote("n1", "c1"); + + _customerNoteServiceMock.Verify(c => c.DeleteCustomerNote(note), Times.Once); + _downloadServiceMock.Verify(d => d.DeleteDownload(download), Times.Once); + } + + [TestMethod] + public async Task DeleteCustomerNote_NotFound_Throws() + { + _customerNoteServiceMock.Setup(c => c.GetCustomerNote(It.IsAny())) + .ReturnsAsync((CustomerNote)null); + + await Assert.ThrowsAsync( + () => _customerNoteViewModelService.DeleteCustomerNote("missing", "c1")); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/CustomerProductViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerProductViewModelServiceTests.cs new file mode 100644 index 0000000000..5185056346 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerProductViewModelServiceTests.cs @@ -0,0 +1,201 @@ +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Business.Core.Interfaces.Marketing.Customers; +using Grand.Domain; +using Grand.Domain.Catalog; +using Grand.Domain.Customers; +using Grand.Domain.Vendors; +using Grand.Web.AdminShared.Models.Customers; +using Grand.Web.AdminShared.Services; +using Grand.Web.Common.Localization; +using Microsoft.AspNetCore.Mvc.Rendering; +using Moq; +using Assert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Grand.Web.Admin.Tests.Services; + +[TestClass] +public class CustomerProductViewModelServiceTests +{ + private Mock _customerProductServiceMock; + private Mock _productServiceMock; + private Mock _storeServiceMock; + private Mock _vendorServiceMock; + private Mock _enumTranslationServiceMock; + private CustomerProductViewModelService _customerProductViewModelService; + + [TestInitialize] + public void Setup() + { + _customerProductServiceMock = new Mock(); + _productServiceMock = new Mock(); + _storeServiceMock = new Mock(); + _vendorServiceMock = new Mock(); + _enumTranslationServiceMock = new Mock(); + var translationServiceMock = new Mock(); + translationServiceMock.Setup(t => t.GetResource(It.IsAny())).Returns(k => k); + + _customerProductViewModelService = new CustomerProductViewModelService( + _customerProductServiceMock.Object, + _productServiceMock.Object, + _storeServiceMock.Object, + _vendorServiceMock.Object, + translationServiceMock.Object, + _enumTranslationServiceMock.Object); + } + + [TestMethod] + public async Task PrepareCustomerModelAddProductModel_BuildsAvailableLists() + { + _storeServiceMock.Setup(s => s.GetAllStores()) + .ReturnsAsync(new List { new() { Id = "store-1", Shortcut = "Store 1" } }); + _vendorServiceMock + .Setup(v => v.GetAllVendors(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new PagedList { new() { Id = "vendor-1", Name = "Vendor 1" } }); + _enumTranslationServiceMock + .Setup(e => e.ToSelectList(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(new SelectList(Enumerable.Empty())); + + var model = await _customerProductViewModelService.PrepareCustomerModelAddProductModel(); + + Assert.IsTrue(model.AvailableStores.Any(x => x.Value == "store-1")); + Assert.IsTrue(model.AvailableVendors.Any(x => x.Value == "vendor-1")); + Assert.IsTrue(model.AvailableProductTypes.Count > 0); + } + + [TestMethod] + public async Task UpdateProductPrice_UpdatesWhenFound() + { + var price = new CustomerProductPrice { Id = "pp1", Price = 1 }; + _customerProductServiceMock.Setup(c => c.GetCustomerProductPriceById("pp1")).ReturnsAsync(price); + _customerProductServiceMock.Setup(c => c.UpdateCustomerProductPrice(It.IsAny())) + .Returns(Task.CompletedTask); + + await _customerProductViewModelService.UpdateProductPrice( + new CustomerModel.ProductPriceModel { Id = "pp1", Price = 99 }); + + _customerProductServiceMock.Verify( + c => c.UpdateCustomerProductPrice(It.Is(x => x.Price == 99)), Times.Once); + } + + [TestMethod] + public async Task DeleteProductPrice_DeletesWhenFound() + { + var price = new CustomerProductPrice { Id = "pp1" }; + _customerProductServiceMock.Setup(c => c.GetCustomerProductPriceById("pp1")).ReturnsAsync(price); + _customerProductServiceMock.Setup(c => c.DeleteCustomerProductPrice(It.IsAny())) + .Returns(Task.CompletedTask); + + await _customerProductViewModelService.DeleteProductPrice("pp1"); + + _customerProductServiceMock.Verify(c => c.DeleteCustomerProductPrice(price), Times.Once); + } + + [TestMethod] + public async Task DeleteProductPrice_NotFound_Throws() + { + _customerProductServiceMock.Setup(c => c.GetCustomerProductPriceById(It.IsAny())) + .ReturnsAsync((CustomerProductPrice)null); + + await Assert.ThrowsAsync( + () => _customerProductViewModelService.DeleteProductPrice("missing")); + } + + [TestMethod] + public async Task UpdatePersonalizedProduct_UpdatesWhenFound() + { + var customerProduct = new CustomerProduct { Id = "cp1", DisplayOrder = 1 }; + _customerProductServiceMock.Setup(c => c.GetCustomerProduct("cp1")).ReturnsAsync(customerProduct); + _customerProductServiceMock.Setup(c => c.UpdateCustomerProduct(It.IsAny())) + .Returns(Task.CompletedTask); + + await _customerProductViewModelService.UpdatePersonalizedProduct( + new CustomerModel.ProductModel { Id = "cp1", DisplayOrder = 7 }); + + _customerProductServiceMock.Verify( + c => c.UpdateCustomerProduct(It.Is(x => x.DisplayOrder == 7)), Times.Once); + } + + [TestMethod] + public async Task DeletePersonalizedProduct_NotFound_Throws() + { + _customerProductServiceMock.Setup(c => c.GetCustomerProduct(It.IsAny())) + .ReturnsAsync((CustomerProduct)null); + + await Assert.ThrowsAsync( + () => _customerProductViewModelService.DeletePersonalizedProduct("missing")); + } + + [TestMethod] + public async Task PrepareProductPriceModel_MapsItems() + { + _customerProductServiceMock + .Setup(c => c.GetProductsPriceByCustomer(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new PagedList { new() { Id = "pp1", ProductId = "p1", Price = 3 } }); + _productServiceMock.Setup(p => p.GetProductById("p1", false)) + .ReturnsAsync(new Product { Id = "p1", Name = "Prod 1" }); + + var (items, _) = await _customerProductViewModelService.PrepareProductPriceModel("c1", 1, 10); + + var list = items.ToList(); + Assert.AreEqual(1, list.Count); + Assert.AreEqual("Prod 1", list[0].ProductName); + Assert.AreEqual(3, list[0].Price); + } + + [TestMethod] + public async Task InsertCustomerAddProductModel_Personalized_InsertsCustomerProduct() + { + _productServiceMock.Setup(p => p.GetProductById("p1", false)) + .ReturnsAsync(new Product { Id = "p1", Price = 5 }); + _customerProductServiceMock.Setup(c => c.GetCustomerProduct("c1", "p1")).ReturnsAsync((CustomerProduct)null); + _customerProductServiceMock.Setup(c => c.InsertCustomerProduct(It.IsAny())) + .Returns(Task.CompletedTask); + + await _customerProductViewModelService.InsertCustomerAddProductModel("c1", true, + new CustomerModel.AddProductModel { SelectedProductIds = new[] { "p1" } }); + + _customerProductServiceMock.Verify( + c => c.InsertCustomerProduct(It.Is(x => x.CustomerId == "c1" && x.ProductId == "p1")), + Times.Once); + } + + [TestMethod] + public async Task InsertCustomerAddProductModel_NotPersonalized_InsertsProductPrice() + { + _productServiceMock.Setup(p => p.GetProductById("p1", false)) + .ReturnsAsync(new Product { Id = "p1", Price = 5 }); + _customerProductServiceMock.Setup(c => c.GetPriceByCustomerProduct("c1", "p1")) + .ReturnsAsync((double?)null); + _customerProductServiceMock.Setup(c => c.InsertCustomerProductPrice(It.IsAny())) + .Returns(Task.CompletedTask); + + await _customerProductViewModelService.InsertCustomerAddProductModel("c1", false, + new CustomerModel.AddProductModel { SelectedProductIds = new[] { "p1" } }); + + _customerProductServiceMock.Verify( + c => c.InsertCustomerProductPrice( + It.Is(x => x.CustomerId == "c1" && x.ProductId == "p1" && x.Price == 5)), + Times.Once); + } + + [TestMethod] + public async Task PreparePersonalizedProducts_MapsItems() + { + _customerProductServiceMock + .Setup(c => c.GetProductsByCustomer(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new PagedList + { new() { Id = "cp1", ProductId = "p1", DisplayOrder = 2 } }); + _productServiceMock.Setup(p => p.GetProductById("p1", false)) + .ReturnsAsync(new Product { Id = "p1", Name = "Prod 1" }); + + var (items, _) = await _customerProductViewModelService.PreparePersonalizedProducts("c1", 1, 10); + + var list = items.ToList(); + Assert.AreEqual(1, list.Count); + Assert.AreEqual("Prod 1", list[0].ProductName); + Assert.AreEqual(2, list[0].DisplayOrder); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/CustomerViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerViewModelServiceTests.cs index 3e710e180f..047d38c48e 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Services/CustomerViewModelServiceTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerViewModelServiceTests.cs @@ -1,4 +1,4 @@ -using Grand.Business.Core.Interfaces.Authentication; +using Grand.Business.Core.Interfaces.Authentication; using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Business.Core.Interfaces.Common.Addresses; @@ -40,15 +40,9 @@ public class CustomerViewModelServiceTests private Mock _groupServiceMock; private Mock _storeServiceMock; private Mock _customerTagServiceMock; - private Mock _vendorServiceMock; private Mock _affiliateServiceMock; private Mock _enumTranslationServiceMock; - private Mock _customerNoteServiceMock; - private Mock _customerProductServiceMock; - private Mock _loyaltyPointsServiceMock; - private Mock _productServiceMock; private Mock _dateTimeServiceMock; - private Mock _downloadServiceMock; private Mock _newsLetterSubscriptionServiceMock; private Mock _translationServiceMock; private Customer _currentCustomer; @@ -61,15 +55,9 @@ public void Setup() _groupServiceMock = new Mock(); _storeServiceMock = new Mock(); _customerTagServiceMock = new Mock(); - _vendorServiceMock = new Mock(); _affiliateServiceMock = new Mock(); _enumTranslationServiceMock = new Mock(); - _customerNoteServiceMock = new Mock(); - _customerProductServiceMock = new Mock(); - _loyaltyPointsServiceMock = new Mock(); - _productServiceMock = new Mock(); _dateTimeServiceMock = new Mock(); - _downloadServiceMock = new Mock(); _newsLetterSubscriptionServiceMock = new Mock(); _translationServiceMock = new Mock(); _translationServiceMock.Setup(t => t.GetResource(It.IsAny())).Returns(k => k); @@ -123,39 +111,27 @@ public void Setup() It.IsAny())) .ReturnsAsync(new PagedList()); - _customerNoteServiceMock.Setup(c => c.InsertCustomerNote(It.IsAny())) - .Returns(Task.CompletedTask); - _groupServiceMock.Setup(g => g.GetAllByIds(It.IsAny())) .ReturnsAsync(new List()); _customerViewModelService = new CustomerViewModelService( _customerServiceMock.Object, _groupServiceMock.Object, - _customerProductServiceMock.Object, _newsLetterSubscriptionServiceMock.Object, _dateTimeServiceMock.Object, _translationServiceMock.Object, - _loyaltyPointsServiceMock.Object, new Mock().Object, contextAccessorMock.Object, - _vendorServiceMock.Object, _storeServiceMock.Object, new Mock().Object, customerAttributeServiceMock.Object, - new Mock().Object, - new Mock().Object, _affiliateServiceMock.Object, _customerTagServiceMock.Object, - _productServiceMock.Object, salesEmployeeServiceMock.Object, - _customerNoteServiceMock.Object, - _downloadServiceMock.Object, httpContextAccessorMock.Object, new CustomerSettings(), new TaxSettings(), new LoyaltyPointsSettings(), - new AddressSettings(), new CommonSettings(), _enumTranslationServiceMock.Object); } @@ -335,248 +311,6 @@ public async Task DeleteSelected_DeletesAllExceptCurrentUser() _customerServiceMock.Verify(c => c.DeleteCustomer(current), Times.Never); } - [TestMethod] - public async Task PrepareCustomerModelAddProductModel_BuildsAvailableLists() - { - _storeServiceMock.Setup(s => s.GetAllStores()) - .ReturnsAsync(new List { new() { Id = "store-1", Shortcut = "Store 1" } }); - _vendorServiceMock - .Setup(v => v.GetAllVendors(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new PagedList { new() { Id = "vendor-1", Name = "Vendor 1" } }); - _enumTranslationServiceMock - .Setup(e => e.ToSelectList(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(new SelectList(Enumerable.Empty())); - - var model = await _customerViewModelService.PrepareCustomerModelAddProductModel(); - - Assert.IsTrue(model.AvailableStores.Any(x => x.Value == "store-1")); - Assert.IsTrue(model.AvailableVendors.Any(x => x.Value == "vendor-1")); - Assert.IsTrue(model.AvailableProductTypes.Count > 0); - } - - [TestMethod] - public async Task InsertCustomerNote_InsertsNoteWithMappedFields() - { - var note = await _customerViewModelService.InsertCustomerNote("c1", "d1", false, "title", "message"); - - Assert.AreEqual("c1", note.CustomerId); - Assert.AreEqual("d1", note.DownloadId); - Assert.AreEqual("title", note.Title); - Assert.AreEqual("message", note.Note); - Assert.IsFalse(note.DisplayToCustomer); - _customerNoteServiceMock.Verify( - c => c.InsertCustomerNote(It.Is(x => x.CustomerId == "c1" && x.Title == "title")), - Times.Once); - } - - [TestMethod] - public async Task DeleteCustomerNote_DeletesNoteAndAttachment() - { - var note = new CustomerNote { Id = "n1", DownloadId = "d1" }; - var download = new Download { Id = "d1" }; - _customerNoteServiceMock.Setup(c => c.GetCustomerNote("n1")).ReturnsAsync(note); - _customerNoteServiceMock.Setup(c => c.DeleteCustomerNote(It.IsAny())).Returns(Task.CompletedTask); - _downloadServiceMock.Setup(d => d.GetDownloadById("d1")).ReturnsAsync(download); - _downloadServiceMock.Setup(d => d.DeleteDownload(It.IsAny())).Returns(Task.CompletedTask); - - await _customerViewModelService.DeleteCustomerNote("n1", "c1"); - - _customerNoteServiceMock.Verify(c => c.DeleteCustomerNote(note), Times.Once); - _downloadServiceMock.Verify(d => d.DeleteDownload(download), Times.Once); - } - - [TestMethod] - public async Task DeleteCustomerNote_NotFound_Throws() - { - _customerNoteServiceMock.Setup(c => c.GetCustomerNote(It.IsAny())) - .ReturnsAsync((CustomerNote)null); - - await Assert.ThrowsAsync( - () => _customerViewModelService.DeleteCustomerNote("missing", "c1")); - } - - [TestMethod] - public async Task PrepareLoyaltyPointsHistoryModel_MapsHistory() - { - _loyaltyPointsServiceMock - .Setup(l => l.GetLoyaltyPointsHistory(It.IsAny(), It.IsAny(), true)) - .ReturnsAsync(new List { - new() { StoreId = "store-1", Points = 5, PointsBalance = 10, Message = "added" } - }); - _storeServiceMock.Setup(s => s.GetStoreById("store-1")) - .ReturnsAsync(new Grand.Domain.Stores.Store { Id = "store-1", Shortcut = "Store 1" }); - - var result = (await _customerViewModelService.PrepareLoyaltyPointsHistoryModel("c1")).ToList(); - - Assert.AreEqual(1, result.Count); - Assert.AreEqual("Store 1", result[0].StoreName); - Assert.AreEqual(5, result[0].Points); - } - - [TestMethod] - public async Task InsertLoyaltyPointsHistory_DelegatesToService() - { - var history = new LoyaltyPointsHistory(); - _loyaltyPointsServiceMock - .Setup(l => l.AddLoyaltyPointsHistory("c1", 10, "store-1", "msg", It.IsAny(), It.IsAny())) - .ReturnsAsync(history); - - var result = await _customerViewModelService.InsertLoyaltyPointsHistory( - new Customer { Id = "c1" }, "store-1", 10, "msg"); - - Assert.AreSame(history, result); - _loyaltyPointsServiceMock.Verify( - l => l.AddLoyaltyPointsHistory("c1", 10, "store-1", "msg", It.IsAny(), It.IsAny()), - Times.Once); - } - - [TestMethod] - public async Task DeleteAddress_RemovesAddressAndUpdatesCustomer() - { - var address = new Address { Id = "a1" }; - var customer = new Customer(); - customer.Addresses.Add(address); - - await _customerViewModelService.DeleteAddress(customer, address); - - Assert.IsFalse(customer.Addresses.Any(a => a.Id == "a1")); - _customerServiceMock.Verify(c => c.UpdateCustomerInAdminPanel(customer), Times.Once); - } - - [TestMethod] - public async Task UpdateProductPrice_UpdatesWhenFound() - { - var price = new CustomerProductPrice { Id = "pp1", Price = 1 }; - _customerProductServiceMock.Setup(c => c.GetCustomerProductPriceById("pp1")).ReturnsAsync(price); - _customerProductServiceMock.Setup(c => c.UpdateCustomerProductPrice(It.IsAny())) - .Returns(Task.CompletedTask); - - await _customerViewModelService.UpdateProductPrice( - new CustomerModel.ProductPriceModel { Id = "pp1", Price = 99 }); - - _customerProductServiceMock.Verify( - c => c.UpdateCustomerProductPrice(It.Is(x => x.Price == 99)), Times.Once); - } - - [TestMethod] - public async Task DeleteProductPrice_DeletesWhenFound() - { - var price = new CustomerProductPrice { Id = "pp1" }; - _customerProductServiceMock.Setup(c => c.GetCustomerProductPriceById("pp1")).ReturnsAsync(price); - _customerProductServiceMock.Setup(c => c.DeleteCustomerProductPrice(It.IsAny())) - .Returns(Task.CompletedTask); - - await _customerViewModelService.DeleteProductPrice("pp1"); - - _customerProductServiceMock.Verify(c => c.DeleteCustomerProductPrice(price), Times.Once); - } - - [TestMethod] - public async Task DeleteProductPrice_NotFound_Throws() - { - _customerProductServiceMock.Setup(c => c.GetCustomerProductPriceById(It.IsAny())) - .ReturnsAsync((CustomerProductPrice)null); - - await Assert.ThrowsAsync( - () => _customerViewModelService.DeleteProductPrice("missing")); - } - - [TestMethod] - public async Task UpdatePersonalizedProduct_UpdatesWhenFound() - { - var customerProduct = new CustomerProduct { Id = "cp1", DisplayOrder = 1 }; - _customerProductServiceMock.Setup(c => c.GetCustomerProduct("cp1")).ReturnsAsync(customerProduct); - _customerProductServiceMock.Setup(c => c.UpdateCustomerProduct(It.IsAny())) - .Returns(Task.CompletedTask); - - await _customerViewModelService.UpdatePersonalizedProduct( - new CustomerModel.ProductModel { Id = "cp1", DisplayOrder = 7 }); - - _customerProductServiceMock.Verify( - c => c.UpdateCustomerProduct(It.Is(x => x.DisplayOrder == 7)), Times.Once); - } - - [TestMethod] - public async Task DeletePersonalizedProduct_NotFound_Throws() - { - _customerProductServiceMock.Setup(c => c.GetCustomerProduct(It.IsAny())) - .ReturnsAsync((CustomerProduct)null); - - await Assert.ThrowsAsync( - () => _customerViewModelService.DeletePersonalizedProduct("missing")); - } - - [TestMethod] - public async Task PrepareProductPriceModel_MapsItems() - { - _customerProductServiceMock - .Setup(c => c.GetProductsPriceByCustomer(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new PagedList { new() { Id = "pp1", ProductId = "p1", Price = 3 } }); - _productServiceMock.Setup(p => p.GetProductById("p1", false)) - .ReturnsAsync(new Product { Id = "p1", Name = "Prod 1" }); - - var (items, _) = await _customerViewModelService.PrepareProductPriceModel("c1", 1, 10); - - var list = items.ToList(); - Assert.AreEqual(1, list.Count); - Assert.AreEqual("Prod 1", list[0].ProductName); - Assert.AreEqual(3, list[0].Price); - } - - [TestMethod] - public async Task InsertCustomerAddProductModel_Personalized_InsertsCustomerProduct() - { - _productServiceMock.Setup(p => p.GetProductById("p1", false)) - .ReturnsAsync(new Product { Id = "p1", Price = 5 }); - _customerProductServiceMock.Setup(c => c.GetCustomerProduct("c1", "p1")).ReturnsAsync((CustomerProduct)null); - _customerProductServiceMock.Setup(c => c.InsertCustomerProduct(It.IsAny())) - .Returns(Task.CompletedTask); - - await _customerViewModelService.InsertCustomerAddProductModel("c1", true, - new CustomerModel.AddProductModel { SelectedProductIds = new[] { "p1" } }); - - _customerProductServiceMock.Verify( - c => c.InsertCustomerProduct(It.Is(x => x.CustomerId == "c1" && x.ProductId == "p1")), - Times.Once); - } - - [TestMethod] - public async Task InsertCustomerAddProductModel_NotPersonalized_InsertsProductPrice() - { - _productServiceMock.Setup(p => p.GetProductById("p1", false)) - .ReturnsAsync(new Product { Id = "p1", Price = 5 }); - _customerProductServiceMock.Setup(c => c.GetPriceByCustomerProduct("c1", "p1")) - .ReturnsAsync((double?)null); - _customerProductServiceMock.Setup(c => c.InsertCustomerProductPrice(It.IsAny())) - .Returns(Task.CompletedTask); - - await _customerViewModelService.InsertCustomerAddProductModel("c1", false, - new CustomerModel.AddProductModel { SelectedProductIds = new[] { "p1" } }); - - _customerProductServiceMock.Verify( - c => c.InsertCustomerProductPrice( - It.Is(x => x.CustomerId == "c1" && x.ProductId == "p1" && x.Price == 5)), - Times.Once); - } - - [TestMethod] - public async Task PreparePersonalizedProducts_MapsItems() - { - _customerProductServiceMock - .Setup(c => c.GetProductsByCustomer(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new PagedList - { new() { Id = "cp1", ProductId = "p1", DisplayOrder = 2 } }); - _productServiceMock.Setup(p => p.GetProductById("p1", false)) - .ReturnsAsync(new Product { Id = "p1", Name = "Prod 1" }); - - var (items, _) = await _customerViewModelService.PreparePersonalizedProducts("c1", 1, 10); - - var list = items.ToList(); - Assert.AreEqual(1, list.Count); - Assert.AreEqual("Prod 1", list[0].ProductName); - Assert.AreEqual(2, list[0].DisplayOrder); - } - [TestMethod] public async Task DeleteCustomer_RemovesNewsletterSubscriptions() { diff --git a/src/Tests/Grand.Web.Store.Tests/Controllers/CustomerControllerTests.cs b/src/Tests/Grand.Web.Store.Tests/Controllers/CustomerControllerTests.cs index c6689bd14e..1f6be17773 100644 --- a/src/Tests/Grand.Web.Store.Tests/Controllers/CustomerControllerTests.cs +++ b/src/Tests/Grand.Web.Store.Tests/Controllers/CustomerControllerTests.cs @@ -1,4 +1,4 @@ -using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Common.Addresses; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Customers; @@ -70,6 +70,11 @@ public void Setup() private class TestableCustomerController( ICustomerService customerService, ICustomerViewModelService customerViewModelService, + ICustomerAddressViewModelService customerAddressViewModelService, + ICustomerLoyaltyPointsViewModelService customerLoyaltyPointsViewModelService, + ICustomerShoppingCartViewModelService customerShoppingCartViewModelService, + ICustomerProductViewModelService customerProductViewModelService, + ICustomerOutOfStockSubscriptionViewModelService customerOutOfStockSubscriptionViewModelService, ICustomerManagerService customerManagerService, ICustomerProductService customerProductService, IProductReviewService productReviewService, @@ -86,7 +91,9 @@ private class TestableCustomerController( CustomerSettings customerSettings, IAdminDataScope scope, CustomerConfig customerConfig) - : CustomerController(customerService, customerViewModelService, customerManagerService, + : CustomerController(customerService, customerViewModelService, customerAddressViewModelService, + customerLoyaltyPointsViewModelService, customerShoppingCartViewModelService, + customerProductViewModelService, customerOutOfStockSubscriptionViewModelService, customerManagerService, customerProductService, productReviewService, productReviewViewModelService, productViewModelService, customerAttributeParser, customerAttributeService, addressAttributeParser, addressAttributeService, messageProviderService, groupService, translationService, contextAccessor, customerSettings, scope, @@ -101,6 +108,11 @@ private TestableCustomerController BuildStoreController(bool perStoreEnabled = t var controller = new TestableCustomerController( _customerServiceMock.Object, _customerViewModelServiceMock.Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, _customerManagerServiceMock.Object, new Mock().Object, new Mock().Object, diff --git a/src/Web/Grand.Web.Admin/Controllers/CustomerController.cs b/src/Web/Grand.Web.Admin/Controllers/CustomerController.cs index 3a72c24f40..dde10f5482 100644 --- a/src/Web/Grand.Web.Admin/Controllers/CustomerController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/CustomerController.cs @@ -1,4 +1,4 @@ -using Grand.Web.Admin.Extensions; +using Grand.Web.Admin.Extensions; using Grand.Web.AdminShared.Controllers; using Grand.Web.Common.Filters; using Microsoft.AspNetCore.Mvc; @@ -14,6 +14,13 @@ namespace Grand.Web.Admin.Controllers; public class CustomerController( Grand.Business.Core.Interfaces.Customers.ICustomerService customerService, Grand.Web.AdminShared.Interfaces.ICustomerViewModelService customerViewModelService, + Grand.Web.AdminShared.Interfaces.ICustomerAddressViewModelService customerAddressViewModelService, + Grand.Web.AdminShared.Interfaces.ICustomerLoyaltyPointsViewModelService customerLoyaltyPointsViewModelService, + Grand.Web.AdminShared.Interfaces.ICustomerShoppingCartViewModelService customerShoppingCartViewModelService, + Grand.Web.AdminShared.Interfaces.ICustomerProductViewModelService customerProductViewModelService, + Grand.Web.AdminShared.Interfaces.ICustomerOutOfStockSubscriptionViewModelService customerOutOfStockSubscriptionViewModelService, + Grand.Web.AdminShared.Interfaces.ICustomerNoteViewModelService customerNoteViewModelService, + Grand.Web.AdminShared.Interfaces.ICustomerContactFormViewModelService customerContactFormViewModelService, Grand.Business.Core.Interfaces.Customers.ICustomerManagerService customerManagerService, Grand.Business.Core.Interfaces.Marketing.Customers.ICustomerProductService customerProductService, Grand.Business.Core.Interfaces.Catalog.Products.IProductReviewService productReviewService, @@ -31,7 +38,10 @@ public class CustomerController( Grand.Web.AdminShared.Interfaces.IAdminDataScope scope, IRequestPermissionService permissionService, Grand.Business.Core.Interfaces.ExportImport.IExportManager exportManager) - : BaseCustomerManagementController(customerService, customerViewModelService, customerManagerService, + : BaseCustomerManagementController(customerService, customerViewModelService, customerAddressViewModelService, + customerLoyaltyPointsViewModelService, customerShoppingCartViewModelService, customerProductViewModelService, + customerOutOfStockSubscriptionViewModelService, customerNoteViewModelService, customerContactFormViewModelService, + customerManagerService, customerProductService, productReviewService, productReviewViewModelService, productViewModelService, customerAttributeParser, customerAttributeService, addressAttributeParser, addressAttributeService, messageProviderService, groupService, translationService, contextAccessor, customerSettings, scope, diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseCustomerController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseCustomerController.cs index e805f309ef..11535fcdde 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseCustomerController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseCustomerController.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using Grand.Business.Core.Interfaces.Authentication; using Grand.Business.Core.Interfaces.Catalog.Products; @@ -32,6 +32,11 @@ namespace Grand.Web.AdminShared.Controllers; public abstract class BaseCustomerController( ICustomerService customerService, ICustomerViewModelService customerViewModelService, + ICustomerAddressViewModelService customerAddressViewModelService, + ICustomerLoyaltyPointsViewModelService customerLoyaltyPointsViewModelService, + ICustomerShoppingCartViewModelService customerShoppingCartViewModelService, + ICustomerProductViewModelService customerProductViewModelService, + ICustomerOutOfStockSubscriptionViewModelService customerOutOfStockSubscriptionViewModelService, ICustomerManagerService customerManagerService, ICustomerProductService customerProductService, IProductReviewService productReviewService, @@ -427,7 +432,7 @@ public async Task LoyaltyPointsHistorySelect(string customerId) var (customer, denied) = await LoadAuthorizedCustomer(customerId); if (customer is null) throw new ArgumentException("No customer found with the specified id"); - var model = (await customerViewModelService.PrepareLoyaltyPointsHistoryModel(customerId)).ToList(); + var model = (await customerLoyaltyPointsViewModelService.PrepareLoyaltyPointsHistoryModel(customerId)).ToList(); var gridModel = new DataSourceResult { Data = model, Total = model.Count }; return Json(gridModel); } @@ -444,7 +449,7 @@ public async Task LoyaltyPointsHistoryAdd(string customerId, stri // and keeps using whatever the caller (its own store picker) submitted. var effectiveStoreId = scope.DefaultStoreId ?? storeId; - await customerViewModelService.InsertLoyaltyPointsHistory(customer, effectiveStoreId, + await customerLoyaltyPointsViewModelService.InsertLoyaltyPointsHistory(customer, effectiveStoreId, addLoyaltyPointsValue, addLoyaltyPointsMessage); return Json(new { Result = true }); } @@ -460,7 +465,7 @@ public async Task AddressesSelect(string customerId, DataSourceRe var (customer, denied) = await LoadAuthorizedCustomer(customerId); if (customer is null) throw new ArgumentException("No customer found with the specified id", nameof(customerId)); - var addresses = (await customerViewModelService.PrepareAddressModel(customer)).ToList(); + var addresses = (await customerAddressViewModelService.PrepareAddressModel(customer)).ToList(); var gridModel = new DataSourceResult { Data = addresses, Total = addresses.Count }; return Json(gridModel); } @@ -493,7 +498,7 @@ public async Task AddressDelete(string id, string customerId) if (address == null) return Content("No customer found with the specified id"); if (ModelState.IsValid) { - await customerViewModelService.DeleteAddress(customer, address); + await customerAddressViewModelService.DeleteAddress(customer, address); return new JsonResult(""); } @@ -507,7 +512,7 @@ public async Task AddressCreate(string customerId) if (customer is null) return denied!; var model = new CustomerAddressModel(); - await customerViewModelService.PrepareAddressModel(model, null, customer, false); + await customerAddressViewModelService.PrepareAddressModel(model, null, customer, false); return View(model); } @@ -522,12 +527,12 @@ public async Task AddressCreate(CustomerAddressModel model) { var customAttributes = await model.Address.ParseCustomAddressAttributes(addressAttributeParser, addressAttributeService); - var address = await customerViewModelService.InsertAddressModel(customer, model, customAttributes); + var address = await customerAddressViewModelService.InsertAddressModel(customer, model, customAttributes); Success(translationService.GetResource("Admin.Customers.Customers.Addresses.Added")); return RedirectToAction("AddressEdit", new { addressId = address.Id, customerId = model.CustomerId }); } - await customerViewModelService.PrepareAddressModel(model, null, customer, true); + await customerAddressViewModelService.PrepareAddressModel(model, null, customer, true); return View(model); } @@ -541,7 +546,7 @@ public async Task AddressEdit(string addressId, string customerId if (address == null) return RedirectToAction("Edit", new { id = customer.Id }); var model = new CustomerAddressModel(); - await customerViewModelService.PrepareAddressModel(model, address, customer, false); + await customerAddressViewModelService.PrepareAddressModel(model, address, customer, false); return View(model); } @@ -559,12 +564,12 @@ public async Task AddressEdit(CustomerAddressModel model) { var customAttributes = await model.Address.ParseCustomAddressAttributes(addressAttributeParser, addressAttributeService); - await customerViewModelService.UpdateAddressModel(customer, address, model, customAttributes); + await customerAddressViewModelService.UpdateAddressModel(customer, address, model, customAttributes); Success(translationService.GetResource("Admin.Customers.Customers.Addresses.Updated")); return RedirectToAction("AddressEdit", new { addressId = model.Address.Id, customerId = model.CustomerId }); } - await customerViewModelService.PrepareAddressModel(model, address, customer, true); + await customerAddressViewModelService.PrepareAddressModel(model, address, customer, true); return View(model); } @@ -682,7 +687,7 @@ public async Task GetCartList(string customerId, int cartTypeId) if (denied != null) return Json(new DataSourceResult { Data = null, Total = 0 }); } - var cart = await customerViewModelService.PrepareShoppingCartItemModel(customerId, cartTypeId); + var cart = await customerShoppingCartViewModelService.PrepareShoppingCartItemModel(customerId, cartTypeId); var gridModel = new DataSourceResult { Data = cart, Total = cart.Count }; return Json(gridModel); } @@ -694,7 +699,7 @@ public async Task UpdateCart(string id, string customerId, double var (customer, denied) = await LoadAuthorizedCustomer(customerId); if (customer is null) throw new ArgumentException("No customer found with the specified id", nameof(customerId)); - var warnings = await customerViewModelService.UpdateCart(customer, id, unitPriceValue); + var warnings = await customerShoppingCartViewModelService.UpdateCart(customer, id, unitPriceValue); if (warnings.Any()) return ErrorForKendoGridJson(string.Join(",", warnings)); return new JsonResult(""); } @@ -706,7 +711,7 @@ public async Task DeleteCart(string id, string customerId) var (customer, denied) = await LoadAuthorizedCustomer(customerId); if (customer is null) throw new ArgumentException("No customer found with the specified id", nameof(customerId)); - await customerViewModelService.DeleteCart(customer, id); + await customerShoppingCartViewModelService.DeleteCart(customer, id); return new JsonResult(""); } @@ -733,7 +738,7 @@ public async Task ProductsPrice(DataSourceRequest command, string } var (productPriceModels, totalCount) = - await customerViewModelService.PrepareProductPriceModel(customerId, command.Page, command.PageSize); + await customerProductViewModelService.PrepareProductPriceModel(customerId, command.Page, command.PageSize); var gridModel = new DataSourceResult { Data = productPriceModels.ToList(), Total = totalCount }; return Json(gridModel); } @@ -749,7 +754,7 @@ public async Task PersonalizedProducts(DataSourceRequest command, } var (productModels, totalCount) = - await customerViewModelService.PreparePersonalizedProducts(customerId, command.Page, command.PageSize); + await customerProductViewModelService.PreparePersonalizedProducts(customerId, command.Page, command.PageSize); var gridModel = new DataSourceResult { Data = productModels.ToList(), Total = totalCount }; return Json(gridModel); } @@ -763,7 +768,7 @@ public async Task ProductAddPopup(string customerId) if (denied != null) return denied; } - var model = await customerViewModelService.PrepareCustomerModelAddProductModel(); + var model = await customerProductViewModelService.PrepareCustomerModelAddProductModel(); return View(model); } @@ -771,7 +776,7 @@ public async Task ProductAddPopup(string customerId) [HttpPost] public async Task ProductAddPopupList(DataSourceRequest command, CustomerModel.AddProductModel model) { - var products = await customerViewModelService.PrepareProductModel(model, command.Page, command.PageSize); + var products = await customerProductViewModelService.PrepareProductModel(model, command.Page, command.PageSize); var gridModel = new DataSourceResult { Data = products.products.ToList(), Total = products.totalCount }; return Json(gridModel); } @@ -788,7 +793,7 @@ public async Task ProductAddPopup(string customerId, bool persona } if (model.SelectedProductIds != null) - await customerViewModelService.InsertCustomerAddProductModel(customerId, personalized, model); + await customerProductViewModelService.InsertCustomerAddProductModel(customerId, personalized, model); return Content(""); } @@ -803,7 +808,7 @@ public async Task UpdateProductPrice(CustomerModel.ProductPriceMo return new JsonResult(""); } - await customerViewModelService.UpdateProductPrice(model); + await customerProductViewModelService.UpdateProductPrice(model); return new JsonResult(""); } @@ -821,7 +826,7 @@ public async Task DeleteProductPrice(string id) return new JsonResult(""); } - await customerViewModelService.DeleteProductPrice(id); + await customerProductViewModelService.DeleteProductPrice(id); return new JsonResult(""); } @@ -836,7 +841,7 @@ public async Task UpdatePersonalizedProduct(CustomerModel.Product return new JsonResult(""); } - await customerViewModelService.UpdatePersonalizedProduct(model); + await customerProductViewModelService.UpdatePersonalizedProduct(model); return new JsonResult(""); } @@ -855,8 +860,8 @@ public async Task DeletePersonalizedProduct(string id) } // Admin's original convenience method, preserved for the global-scope branch (still present - // on ICustomerViewModelService — no need to fall back to a two-step Get+Delete). - await customerViewModelService.DeletePersonalizedProduct(id); + // on ICustomerProductViewModelService — no need to fall back to a two-step Get+Delete). + await customerProductViewModelService.DeletePersonalizedProduct(id); return new JsonResult(""); } @@ -875,7 +880,7 @@ public async Task OutOfStockSubscriptionList(DataSourceRequest co } var (outOfStockSubscriptionModels, totalCount) = - await customerViewModelService.PrepareOutOfStockSubscriptionModel(customerId, command.Page, command.PageSize); + await customerOutOfStockSubscriptionViewModelService.PrepareOutOfStockSubscriptionModel(customerId, command.Page, command.PageSize); var gridModel = new DataSourceResult { Data = outOfStockSubscriptionModels.ToList(), Total = totalCount }; return Json(gridModel); } diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseCustomerManagementController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseCustomerManagementController.cs index 0a923e3a8b..9b9a69d7f2 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseCustomerManagementController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseCustomerManagementController.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Common.Addresses; @@ -23,6 +23,13 @@ namespace Grand.Web.AdminShared.Controllers; public abstract class BaseCustomerManagementController( ICustomerService customerService, ICustomerViewModelService customerViewModelService, + ICustomerAddressViewModelService customerAddressViewModelService, + ICustomerLoyaltyPointsViewModelService customerLoyaltyPointsViewModelService, + ICustomerShoppingCartViewModelService customerShoppingCartViewModelService, + ICustomerProductViewModelService customerProductViewModelService, + ICustomerOutOfStockSubscriptionViewModelService customerOutOfStockSubscriptionViewModelService, + ICustomerNoteViewModelService customerNoteViewModelService, + ICustomerContactFormViewModelService customerContactFormViewModelService, ICustomerManagerService customerManagerService, ICustomerProductService customerProductService, IProductReviewService productReviewService, @@ -40,7 +47,9 @@ public abstract class BaseCustomerManagementController( IAdminDataScope scope, IRequestPermissionService permissionService, IExportManager exportManager) - : BaseCustomerController(customerService, customerViewModelService, customerManagerService, + : BaseCustomerController(customerService, customerViewModelService, customerAddressViewModelService, + customerLoyaltyPointsViewModelService, customerShoppingCartViewModelService, customerProductViewModelService, + customerOutOfStockSubscriptionViewModelService, customerManagerService, customerProductService, productReviewService, productReviewViewModelService, productViewModelService, customerAttributeParser, customerAttributeService, addressAttributeParser, addressAttributeService, messageProviderService, groupService, translationService, contextAccessor, customerSettings, scope) @@ -60,7 +69,7 @@ protected override void CheckTwoFactorEnabledWarning(Customer? existingCustomer, public async Task ContactFormList(DataSourceRequest command, string customerId) { var (contactFormModels, totalCount) = - await CustomerViewModelService.PrepareContactFormModel(customerId, command.Page, command.PageSize); + await customerContactFormViewModelService.PrepareContactFormModel(customerId, command.Page, command.PageSize); var gridModel = new DataSourceResult { Data = contactFormModels.ToList(), Total = totalCount }; return Json(gridModel); } @@ -124,7 +133,7 @@ public async Task CustomerNotesSelect(string customerId, DataSour var (_, denied) = await LoadAuthorizedCustomer(customerId); if (denied != null) throw new ArgumentException("No customer found with the specified id"); - var customerNoteModels = await CustomerViewModelService.PrepareCustomerNoteList(customerId); + var customerNoteModels = await customerNoteViewModelService.PrepareCustomerNoteList(customerId); var gridModel = new DataSourceResult { Data = customerNoteModels, Total = customerNoteModels.Count }; return Json(gridModel); } @@ -136,7 +145,7 @@ public async Task CustomerNoteAdd(string customerId, string downl var (_, denied) = await LoadAuthorizedCustomer(customerId); if (denied != null) return Json(new { Result = false }); - await CustomerViewModelService.InsertCustomerNote(customerId, downloadId, displayToCustomer, title, message); + await customerNoteViewModelService.InsertCustomerNote(customerId, downloadId, displayToCustomer, title, message); return Json(new { Result = true }); } @@ -147,7 +156,7 @@ public async Task CustomerNoteDelete(string id, string customerId var (_, denied) = await LoadAuthorizedCustomer(customerId); if (denied != null) throw new ArgumentException("No customer found with the specified id"); - await CustomerViewModelService.DeleteCustomerNote(id, customerId); + await customerNoteViewModelService.DeleteCustomerNote(id, customerId); return new JsonResult(""); } diff --git a/src/Web/Grand.Web.AdminShared/Interfaces/ICustomerAddressViewModelService.cs b/src/Web/Grand.Web.AdminShared/Interfaces/ICustomerAddressViewModelService.cs new file mode 100644 index 0000000000..14fc401e45 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Interfaces/ICustomerAddressViewModelService.cs @@ -0,0 +1,19 @@ +using Grand.Domain.Common; +using Grand.Domain.Customers; +using Grand.Web.AdminShared.Models.Common; +using Grand.Web.AdminShared.Models.Customers; + +namespace Grand.Web.AdminShared.Interfaces; + +public interface ICustomerAddressViewModelService +{ + Task> PrepareAddressModel(Customer customer); + Task DeleteAddress(Customer customer, Address address); + Task PrepareAddressModel(CustomerAddressModel model, Address address, Customer customer, bool excludeProperties); + + Task
InsertAddressModel(Customer customer, CustomerAddressModel model, + List customAttributes); + + Task
UpdateAddressModel(Customer customer, Address address, CustomerAddressModel model, + List customAttributes); +} diff --git a/src/Web/Grand.Web.AdminShared/Interfaces/ICustomerContactFormViewModelService.cs b/src/Web/Grand.Web.AdminShared/Interfaces/ICustomerContactFormViewModelService.cs new file mode 100644 index 0000000000..f6fecf317b --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Interfaces/ICustomerContactFormViewModelService.cs @@ -0,0 +1,9 @@ +using Grand.Web.AdminShared.Models.Messages; + +namespace Grand.Web.AdminShared.Interfaces; + +public interface ICustomerContactFormViewModelService +{ + Task<(IEnumerable contactFormModels, int totalCount)> PrepareContactFormModel(string customerId, + int pageIndex, int pageSize); +} diff --git a/src/Web/Grand.Web.AdminShared/Interfaces/ICustomerLoyaltyPointsViewModelService.cs b/src/Web/Grand.Web.AdminShared/Interfaces/ICustomerLoyaltyPointsViewModelService.cs new file mode 100644 index 0000000000..8bac28bbe7 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Interfaces/ICustomerLoyaltyPointsViewModelService.cs @@ -0,0 +1,13 @@ +using Grand.Domain.Customers; +using Grand.Domain.Orders; +using Grand.Web.AdminShared.Models.Customers; + +namespace Grand.Web.AdminShared.Interfaces; + +public interface ICustomerLoyaltyPointsViewModelService +{ + Task> PrepareLoyaltyPointsHistoryModel(string customerId); + + Task InsertLoyaltyPointsHistory(Customer customer, string storeId, int addLoyaltyPointsValue, + string addLoyaltyPointsMessage); +} diff --git a/src/Web/Grand.Web.AdminShared/Interfaces/ICustomerNoteViewModelService.cs b/src/Web/Grand.Web.AdminShared/Interfaces/ICustomerNoteViewModelService.cs new file mode 100644 index 0000000000..7898768e4f --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Interfaces/ICustomerNoteViewModelService.cs @@ -0,0 +1,14 @@ +using Grand.Domain.Customers; +using Grand.Web.AdminShared.Models.Customers; + +namespace Grand.Web.AdminShared.Interfaces; + +public interface ICustomerNoteViewModelService +{ + Task> PrepareCustomerNoteList(string customerId); + + Task InsertCustomerNote(string customerId, string downloadId, bool displayToCustomer, string title, + string message); + + Task DeleteCustomerNote(string id, string customerId); +} diff --git a/src/Web/Grand.Web.AdminShared/Interfaces/ICustomerOutOfStockSubscriptionViewModelService.cs b/src/Web/Grand.Web.AdminShared/Interfaces/ICustomerOutOfStockSubscriptionViewModelService.cs new file mode 100644 index 0000000000..e335bbd974 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Interfaces/ICustomerOutOfStockSubscriptionViewModelService.cs @@ -0,0 +1,9 @@ +using Grand.Web.AdminShared.Models.Customers; + +namespace Grand.Web.AdminShared.Interfaces; + +public interface ICustomerOutOfStockSubscriptionViewModelService +{ + Task<(IEnumerable outOfStockSubscriptionModels, int totalCount)> + PrepareOutOfStockSubscriptionModel(string customerId, int pageIndex, int pageSize); +} diff --git a/src/Web/Grand.Web.AdminShared/Interfaces/ICustomerProductViewModelService.cs b/src/Web/Grand.Web.AdminShared/Interfaces/ICustomerProductViewModelService.cs new file mode 100644 index 0000000000..2e0505d684 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Interfaces/ICustomerProductViewModelService.cs @@ -0,0 +1,24 @@ +using Grand.Web.AdminShared.Models.Catalog; +using Grand.Web.AdminShared.Models.Customers; + +namespace Grand.Web.AdminShared.Interfaces; + +public interface ICustomerProductViewModelService +{ + Task<(IEnumerable productPriceModels, int totalCount)> PrepareProductPriceModel( + string customerId, int pageIndex, int pageSize); + + Task<(IEnumerable productModels, int totalCount)> PreparePersonalizedProducts( + string customerId, int pageIndex, int pageSize); + + Task PrepareCustomerModelAddProductModel(); + + Task<(IList products, int totalCount)> PrepareProductModel(CustomerModel.AddProductModel model, + int pageIndex, int pageSize); + + Task InsertCustomerAddProductModel(string customerId, bool personalized, CustomerModel.AddProductModel model); + Task UpdateProductPrice(CustomerModel.ProductPriceModel model); + Task DeleteProductPrice(string id); + Task UpdatePersonalizedProduct(CustomerModel.ProductModel model); + Task DeletePersonalizedProduct(string id); +} diff --git a/src/Web/Grand.Web.AdminShared/Interfaces/ICustomerShoppingCartViewModelService.cs b/src/Web/Grand.Web.AdminShared/Interfaces/ICustomerShoppingCartViewModelService.cs new file mode 100644 index 0000000000..86594d09ae --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Interfaces/ICustomerShoppingCartViewModelService.cs @@ -0,0 +1,11 @@ +using Grand.Domain.Customers; +using Grand.Web.AdminShared.Models.ShoppingCart; + +namespace Grand.Web.AdminShared.Interfaces; + +public interface ICustomerShoppingCartViewModelService +{ + Task> PrepareShoppingCartItemModel(string customerId, int cartTypeId); + Task DeleteCart(Customer customer, string id); + Task> UpdateCart(Customer customer, string shoppingCartId, double? unitprice); +} diff --git a/src/Web/Grand.Web.AdminShared/Interfaces/ICustomerViewModelService.cs b/src/Web/Grand.Web.AdminShared/Interfaces/ICustomerViewModelService.cs index fce6cb99ef..cb8832ccff 100644 --- a/src/Web/Grand.Web.AdminShared/Interfaces/ICustomerViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Interfaces/ICustomerViewModelService.cs @@ -1,11 +1,5 @@ -using Grand.Domain.Common; -using Grand.Domain.Customers; -using Grand.Domain.Orders; -using Grand.Web.AdminShared.Models.Catalog; -using Grand.Web.AdminShared.Models.Common; +using Grand.Domain.Customers; using Grand.Web.AdminShared.Models.Customers; -using Grand.Web.AdminShared.Models.Messages; -using Grand.Web.AdminShared.Models.ShoppingCart; namespace Grand.Web.AdminShared.Interfaces; @@ -23,52 +17,4 @@ public interface ICustomerViewModelService Task DeleteCustomer(Customer customer); Task DeleteSelected(IEnumerable selectedIds); Task SendEmail(Customer customer, CustomerModel.SendEmailModel model); - Task> PrepareLoyaltyPointsHistoryModel(string customerId); - - Task InsertLoyaltyPointsHistory(Customer customer, string storeId, int addLoyaltyPointsValue, - string addLoyaltyPointsMessage); - - Task> PrepareAddressModel(Customer customer); - Task DeleteAddress(Customer customer, Address address); - Task PrepareAddressModel(CustomerAddressModel model, Address address, Customer customer, bool excludeProperties); - - Task
InsertAddressModel(Customer customer, CustomerAddressModel model, - List customAttributes); - - Task
UpdateAddressModel(Customer customer, Address address, CustomerAddressModel model, - List customAttributes); - - Task> PrepareShoppingCartItemModel(string customerId, int cartTypeId); - Task DeleteCart(Customer customer, string id); - Task> UpdateCart(Customer customer, string shoppingCartId, double? unitprice); - - Task<(IEnumerable productPriceModels, int totalCount)> PrepareProductPriceModel( - string customerId, int pageIndex, int pageSize); - - Task<(IEnumerable productModels, int totalCount)> PreparePersonalizedProducts( - string customerId, int pageIndex, int pageSize); - - Task PrepareCustomerModelAddProductModel(); - - Task<(IList products, int totalCount)> PrepareProductModel(CustomerModel.AddProductModel model, - int pageIndex, int pageSize); - - Task InsertCustomerAddProductModel(string customerId, bool personalized, CustomerModel.AddProductModel model); - Task UpdateProductPrice(CustomerModel.ProductPriceModel model); - Task DeleteProductPrice(string id); - Task UpdatePersonalizedProduct(CustomerModel.ProductModel model); - Task DeletePersonalizedProduct(string id); - - Task<(IEnumerable contactFormModels, int totalCount)> PrepareContactFormModel(string customerId, - int pageIndex, int pageSize); - - Task<(IEnumerable outOfStockSubscriptionModels, int totalCount)> - PrepareOutOfStockSubscriptionModel(string customerId, int pageIndex, int pageSize); - - Task> PrepareCustomerNoteList(string customerId); - - Task InsertCustomerNote(string customerId, string downloadId, bool displayToCustomer, string title, - string message); - - Task DeleteCustomerNote(string id, string customerId); -} \ No newline at end of file +} diff --git a/src/Web/Grand.Web.AdminShared/Services/CustomerAddressViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CustomerAddressViewModelService.cs new file mode 100644 index 0000000000..04b16d1f66 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Services/CustomerAddressViewModelService.cs @@ -0,0 +1,148 @@ +using Grand.Business.Core.Interfaces.Common.Addresses; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Domain.Common; +using Grand.Domain.Customers; +using Grand.Domain.Directory; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Extensions.Mapping; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Common; +using Grand.Web.AdminShared.Models.Customers; +using Grand.Web.Common.Extensions; +using Grand.Web.Common.Localization; +using Microsoft.AspNetCore.Mvc.Rendering; +using System.Net; +using System.Text; + +namespace Grand.Web.AdminShared.Services; + +public class CustomerAddressViewModelService( + ICustomerService customerService, + ICountryService countryService, + IAddressAttributeParser addressAttributeParser, + IAddressAttributeService addressAttributeService, + IRequestTranslationService translationService, + IContextAccessor contextAccessor, + AddressSettings addressSettings) + : ICustomerAddressViewModelService +{ + public virtual async Task> PrepareAddressModel(Customer customer) + { + var addresses = customer.Addresses.ToList(); + var addressesListModel = new List(); + foreach (var x in addresses) + { + var model = await x.ToModel(countryService); + var addressHtmlSb = new StringBuilder("
"); + if (addressSettings.CompanyEnabled && !string.IsNullOrEmpty(model.Company)) + addressHtmlSb.AppendFormat("{0}
", WebUtility.HtmlEncode(model.Company)); + if (addressSettings.StreetAddressEnabled && !string.IsNullOrEmpty(model.Address1)) + addressHtmlSb.AppendFormat("{0}
", WebUtility.HtmlEncode(model.Address1)); + if (addressSettings.StreetAddress2Enabled && !string.IsNullOrEmpty(model.Address2)) + addressHtmlSb.AppendFormat("{0}
", WebUtility.HtmlEncode(model.Address2)); + if (addressSettings.CityEnabled && !string.IsNullOrEmpty(model.City)) + addressHtmlSb.AppendFormat("{0},", WebUtility.HtmlEncode(model.City)); + if (addressSettings.StateProvinceEnabled && !string.IsNullOrEmpty(model.StateProvinceName)) + addressHtmlSb.AppendFormat("{0},", WebUtility.HtmlEncode(model.StateProvinceName)); + if (addressSettings.ZipPostalCodeEnabled && !string.IsNullOrEmpty(model.ZipPostalCode)) + addressHtmlSb.AppendFormat("{0}
", WebUtility.HtmlEncode(model.ZipPostalCode)); + if (addressSettings.CountryEnabled && !string.IsNullOrEmpty(model.CountryName)) + addressHtmlSb.AppendFormat("{0}", WebUtility.HtmlEncode(model.CountryName)); + var customAttributesFormatted = + await addressAttributeParser.FormatAttributes(contextAccessor.WorkContext.WorkingLanguage, x.Attributes); + if (!string.IsNullOrEmpty(customAttributesFormatted)) + //already encoded + addressHtmlSb.AppendFormat("
{0}", customAttributesFormatted); + addressHtmlSb.Append("
"); + model.AddressHtml = addressHtmlSb.ToString(); + addressesListModel.Add(model); + } + + return addressesListModel; + } + + public virtual async Task DeleteAddress(Customer customer, Address address) + { + customer.RemoveAddress(address); + await customerService.UpdateCustomerInAdminPanel(customer); + } + + public virtual async Task
InsertAddressModel(Customer customer, CustomerAddressModel model, + List customAttributes) + { + var address = model.Address.ToEntity(); + address.Attributes = customAttributes; + customer.Addresses.Add(address); + await customerService.UpdateCustomerInAdminPanel(customer); + return address; + } + + public virtual async Task PrepareAddressModel(CustomerAddressModel model, Address address, Customer customer, + bool excludeProperties) + { + ArgumentNullException.ThrowIfNull(customer); + + model.CustomerId = customer.Id; + if (address != null) + if (!excludeProperties) + model.Address = await address.ToModel(countryService); + + model.Address ??= new AddressModel(); + + model.Address.NameEnabled = addressSettings.NameEnabled; + model.Address.FirstNameEnabled = true; + model.Address.FirstNameRequired = true; + model.Address.LastNameEnabled = true; + model.Address.LastNameRequired = true; + model.Address.EmailEnabled = true; + model.Address.EmailRequired = true; + model.Address.CompanyEnabled = addressSettings.CompanyEnabled; + model.Address.CompanyRequired = addressSettings.CompanyRequired; + model.Address.VatNumberEnabled = addressSettings.VatNumberEnabled; + model.Address.VatNumberRequired = addressSettings.VatNumberRequired; + model.Address.CountryEnabled = addressSettings.CountryEnabled; + model.Address.StateProvinceEnabled = addressSettings.StateProvinceEnabled; + model.Address.CityEnabled = addressSettings.CityEnabled; + model.Address.CityRequired = addressSettings.CityRequired; + model.Address.StreetAddressEnabled = addressSettings.StreetAddressEnabled; + model.Address.StreetAddressRequired = addressSettings.StreetAddressRequired; + model.Address.StreetAddress2Enabled = addressSettings.StreetAddress2Enabled; + model.Address.StreetAddress2Required = addressSettings.StreetAddress2Required; + model.Address.ZipPostalCodeEnabled = addressSettings.ZipPostalCodeEnabled; + model.Address.ZipPostalCodeRequired = addressSettings.ZipPostalCodeRequired; + model.Address.PhoneEnabled = addressSettings.PhoneEnabled; + model.Address.PhoneRequired = addressSettings.PhoneRequired; + model.Address.FaxEnabled = addressSettings.FaxEnabled; + model.Address.FaxRequired = addressSettings.FaxRequired; + model.Address.NoteEnabled = addressSettings.NoteEnabled; + model.Address.AddressTypeEnabled = addressSettings.AddressTypeEnabled; + + //countries + model.Address.AvailableCountries.Add(new SelectListItem + { Text = translationService.GetResource("Admin.Address.SelectCountry"), Value = "" }); + foreach (var c in await countryService.GetAllCountries(showHidden: true)) + model.Address.AvailableCountries.Add(new SelectListItem + { Text = c.Name, Value = c.Id, Selected = c.Id == model.Address.CountryId }); + //states + var states = !string.IsNullOrEmpty(model.Address.CountryId) + ? (await countryService.GetCountryById(model.Address.CountryId))?.StateProvinces + : new List(); + if (states?.Count > 0) + foreach (var s in states) + model.Address.AvailableStates.Add(new SelectListItem + { Text = s.Name, Value = s.Id, Selected = s.Id == model.Address.StateProvinceId }); + + //customer attribute services + await model.Address.PrepareCustomAddressAttributes(address, addressAttributeService, addressAttributeParser); + } + + public virtual async Task
UpdateAddressModel(Customer customer, Address address, + CustomerAddressModel model, List customAttributes) + { + address = model.Address.ToEntity(address); + address.Attributes = customAttributes; + await customerService.UpdateCustomerInAdminPanel(customer); + return address; + } +} diff --git a/src/Web/Grand.Web.AdminShared/Services/CustomerContactFormViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CustomerContactFormViewModelService.cs new file mode 100644 index 0000000000..cedfa86c6e --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Services/CustomerContactFormViewModelService.cs @@ -0,0 +1,35 @@ +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Business.Core.Interfaces.Marketing.Contacts; +using Grand.Web.AdminShared.Extensions.Mapping; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Messages; + +namespace Grand.Web.AdminShared.Services; + +public class CustomerContactFormViewModelService( + IContactUsService contactUsService, + IStoreService storeService, + IDateTimeService dateTimeService) + : ICustomerContactFormViewModelService +{ + public virtual async Task<(IEnumerable contactFormModels, int totalCount)> + PrepareContactFormModel(string customerId, int pageIndex, int pageSize) + { + var contactform = await contactUsService.GetAllContactUs(storeId: "", vendorId: "", customerId: customerId, + pageIndex: pageIndex - 1, pageSize: pageSize); + var items = new List(); + foreach (var x in contactform) + { + var store = await storeService.GetStoreById(x.StoreId); + var m = x.ToModel(); + m.CreatedOn = dateTimeService.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc); + m.Enquiry = ""; + m.Email = m.FullName + " - " + m.Email; + m.Store = store != null ? store.Shortcut : "-empty-"; + items.Add(m); + } + + return (items, contactform.TotalCount); + } +} diff --git a/src/Web/Grand.Web.AdminShared/Services/CustomerLoyaltyPointsViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CustomerLoyaltyPointsViewModelService.cs new file mode 100644 index 0000000000..515a0ec891 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Services/CustomerLoyaltyPointsViewModelService.cs @@ -0,0 +1,43 @@ +using Grand.Business.Core.Interfaces.Checkout.Orders; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Domain.Customers; +using Grand.Domain.Orders; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Customers; + +namespace Grand.Web.AdminShared.Services; + +public class CustomerLoyaltyPointsViewModelService( + ILoyaltyPointsService loyaltyPointsService, + IStoreService storeService, + IDateTimeService dateTimeService) + : ICustomerLoyaltyPointsViewModelService +{ + public virtual async Task> PrepareLoyaltyPointsHistoryModel( + string customerId) + { + var model = new List(); + foreach (var rph in await loyaltyPointsService.GetLoyaltyPointsHistory(customerId, showAll: true)) + { + var store = await storeService.GetStoreById(rph.StoreId); + model.Add(new CustomerModel.LoyaltyPointsHistoryModel { + StoreName = store != null ? store.Shortcut : "Unknown", + Points = rph.Points, + PointsBalance = rph.PointsBalance, + Message = rph.Message, + CreatedOn = dateTimeService.ConvertToUserTime(rph.CreatedOnUtc, DateTimeKind.Utc) + }); + } + + return model; + } + + public virtual async Task InsertLoyaltyPointsHistory(Customer customer, string storeId, + int addLoyaltyPointsValue, string addLoyaltyPointsMessage) + { + return await loyaltyPointsService.AddLoyaltyPointsHistory(customer.Id, addLoyaltyPointsValue, storeId, + addLoyaltyPointsMessage); + } +} diff --git a/src/Web/Grand.Web.AdminShared/Services/CustomerNoteViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CustomerNoteViewModelService.cs new file mode 100644 index 0000000000..857bfcce7f --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Services/CustomerNoteViewModelService.cs @@ -0,0 +1,85 @@ +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Business.Core.Interfaces.Messages; +using Grand.Business.Core.Interfaces.Storage; +using Grand.Domain.Customers; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Customers; + +namespace Grand.Web.AdminShared.Services; + +public class CustomerNoteViewModelService( + ICustomerNoteService customerNoteService, + ICustomerService customerService, + IDownloadService downloadService, + IMessageProviderService messageProviderService, + IDateTimeService dateTimeService, + IContextAccessor contextAccessor) + : ICustomerNoteViewModelService +{ + public virtual async Task> PrepareCustomerNoteList(string customerId) + { + var customerNoteModels = new List(); + foreach (var customerNote in (await customerNoteService.GetCustomerNotes(customerId)) + .OrderByDescending(on => on.CreatedOnUtc)) + { + var download = !string.IsNullOrEmpty(customerNote.DownloadId) + ? await downloadService.GetDownloadById(customerNote.DownloadId) + : null; + customerNoteModels.Add(new CustomerModel.CustomerNote { + Id = customerNote.Id, + CustomerId = customerId, + DownloadId = string.IsNullOrEmpty(customerNote.DownloadId) ? "" : customerNote.DownloadId, + DownloadGuid = download?.DownloadGuid ?? Guid.Empty, + DisplayToCustomer = customerNote.DisplayToCustomer, + Title = customerNote.Title, + Note = customerNote.Note, + CreatedOn = dateTimeService.ConvertToUserTime(customerNote.CreatedOnUtc, DateTimeKind.Utc) + }); + } + + return customerNoteModels; + } + + public virtual async Task InsertCustomerNote(string customerId, string downloadId, + bool displayToCustomer, string title, string message) + { + var customerNote = new CustomerNote { + DisplayToCustomer = displayToCustomer, + Title = title, + Note = message, + DownloadId = downloadId, + CustomerId = customerId + }; + await customerNoteService.InsertCustomerNote(customerNote); + + //new customer note notification + if (displayToCustomer) + { + //email + await messageProviderService.SendNewCustomerNoteMessage(customerNote, + await customerService.GetCustomerById(customerId), contextAccessor.StoreContext.CurrentStore, + contextAccessor.WorkContext.WorkingLanguage.Id); + } + + return customerNote; + } + + public virtual async Task DeleteCustomerNote(string id, string customerId) + { + var customerNote = await customerNoteService.GetCustomerNote(id); + if (customerNote == null) + throw new ArgumentException("No customer note found with the specified id"); + + await customerNoteService.DeleteCustomerNote(customerNote); + + //delete an old "attachment" file + if (!string.IsNullOrEmpty(customerNote.DownloadId)) + { + var attachment = await downloadService.GetDownloadById(customerNote.DownloadId); + if (attachment != null) + await downloadService.DeleteDownload(attachment); + } + } +} diff --git a/src/Web/Grand.Web.AdminShared/Services/CustomerOutOfStockSubscriptionViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CustomerOutOfStockSubscriptionViewModelService.cs new file mode 100644 index 0000000000..0a85f6a9e4 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Services/CustomerOutOfStockSubscriptionViewModelService.cs @@ -0,0 +1,53 @@ +using Grand.Business.Core.Extensions; +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Customers; +using Grand.Web.Common.Extensions; + +namespace Grand.Web.AdminShared.Services; + +public class CustomerOutOfStockSubscriptionViewModelService( + IOutOfStockSubscriptionService outOfStockSubscriptionService, + IStoreService storeService, + IProductService productService, + IProductAttributeFormatter productAttributeFormatter, + IDateTimeService dateTimeService, + IContextAccessor contextAccessor) + : ICustomerOutOfStockSubscriptionViewModelService +{ + public virtual async + Task<(IEnumerable outOfStockSubscriptionModels, int totalCount)> + PrepareOutOfStockSubscriptionModel(string customerId, int pageIndex, int pageSize) + { + var subscriptions = + await outOfStockSubscriptionService.GetAllSubscriptionsByCustomerId(customerId, "", pageIndex - 1, + pageSize); + var items = new List(); + if (subscriptions.Any()) + { + foreach (var x in subscriptions) + { + var store = await storeService.GetStoreById(x.StoreId); + var product = await productService.GetProductById(x.ProductId); + var m = new CustomerModel.OutOfStockSubscriptionModel { + Id = x.Id, + StoreName = store != null ? store.Shortcut : "Unknown", + ProductId = x.ProductId, + ProductName = product != null ? product.Name : "Unknown", + AttributeDescription = x.Attributes == null || !x.Attributes.Any() + ? "" + : await productAttributeFormatter.FormatAttributes(product, x.Attributes, + contextAccessor.CalculationContext(), contextAccessor.WorkContext.WorkingLanguage, + contextAccessor.StoreContext.CurrentHost), + CreatedOn = dateTimeService.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc) + }; + items.Add(m); + } + } + + return (items, subscriptions.TotalCount); + } +} diff --git a/src/Web/Grand.Web.AdminShared/Services/CustomerProductViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CustomerProductViewModelService.cs new file mode 100644 index 0000000000..94d5ffe2b4 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Services/CustomerProductViewModelService.cs @@ -0,0 +1,158 @@ +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Business.Core.Interfaces.Marketing.Customers; +using Grand.Domain.Catalog; +using Grand.Domain.Customers; +using Grand.Web.AdminShared.Extensions.Mapping; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Catalog; +using Grand.Web.AdminShared.Models.Customers; +using Grand.Web.Common.Extensions; +using Grand.Web.Common.Localization; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace Grand.Web.AdminShared.Services; + +public class CustomerProductViewModelService( + ICustomerProductService customerProductService, + IProductService productService, + IStoreService storeService, + IVendorService vendorService, + IRequestTranslationService translationService, + IEnumTranslationService enumTranslationService) + : ICustomerProductViewModelService +{ + public virtual async Task<(IEnumerable productPriceModels, int totalCount)> + PrepareProductPriceModel(string customerId, int pageIndex, int pageSize) + { + var productPrices = + await customerProductService.GetProductsPriceByCustomer(customerId, pageIndex - 1, pageSize); + var items = new List(); + foreach (var x in productPrices) + { + var m = new CustomerModel.ProductPriceModel { + Id = x.Id, + Price = x.Price, + ProductId = x.ProductId, + ProductName = (await productService.GetProductById(x.ProductId))?.Name + }; + items.Add(m); + } + + return (items, productPrices.TotalCount); + } + + public virtual async Task<(IEnumerable productModels, int totalCount)> + PreparePersonalizedProducts(string customerId, int pageIndex, int pageSize) + { + var products = await customerProductService.GetProductsByCustomer(customerId, pageIndex - 1, pageSize); + var items = new List(); + foreach (var x in products) + { + var m = new CustomerModel.ProductModel { + Id = x.Id, + DisplayOrder = x.DisplayOrder, + ProductId = x.ProductId, + ProductName = (await productService.GetProductById(x.ProductId))?.Name + }; + items.Add(m); + } + + return (items, products.TotalCount); + } + + public virtual async Task PrepareCustomerModelAddProductModel() + { + var model = new CustomerModel.AddProductModel(); + + //stores + model.AvailableStores.Add(new SelectListItem + { Text = translationService.GetResource("Admin.Common.All"), Value = " " }); + foreach (var s in await storeService.GetAllStores()) + model.AvailableStores.Add(new SelectListItem { Text = s.Shortcut, Value = s.Id }); + + //vendors + 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 = " " }); + + return model; + } + + public virtual async Task<(IList products, int totalCount)> PrepareProductModel( + CustomerModel.AddProductModel model, int pageIndex, int pageSize) + { + var products = await productService.PrepareProductList(model.SearchCategoryId, model.SearchBrandId, + model.SearchCollectionId, model.SearchStoreId, model.SearchVendorId, model.SearchProductTypeId, + model.SearchProductName, pageIndex, pageSize); + return (products.Select(x => x.ToModel()).ToList(), products.TotalCount); + } + + public virtual async Task InsertCustomerAddProductModel(string customerId, bool personalized, + CustomerModel.AddProductModel model) + { + foreach (var id in model.SelectedProductIds) + { + var product = await productService.GetProductById(id); + if (product != null) + { + if (!personalized) + { + if (!(await customerProductService.GetPriceByCustomerProduct(customerId, id)).HasValue) + await customerProductService.InsertCustomerProductPrice(new CustomerProductPrice + { CustomerId = customerId, ProductId = id, Price = product.Price }); + } + else + { + if (await customerProductService.GetCustomerProduct(customerId, id) == null) + await customerProductService.InsertCustomerProduct(new CustomerProduct + { CustomerId = customerId, ProductId = id, DisplayOrder = 0 }); + } + } + } + } + + public virtual async Task UpdateProductPrice(CustomerModel.ProductPriceModel model) + { + var productPrice = await customerProductService.GetCustomerProductPriceById(model.Id); + if (productPrice != null) + { + productPrice.Price = model.Price; + await customerProductService.UpdateCustomerProductPrice(productPrice); + } + } + + public virtual async Task DeleteProductPrice(string id) + { + var productPrice = await customerProductService.GetCustomerProductPriceById(id); + if (productPrice == null) + throw new ArgumentException("No productPrice found with the specified id"); + + await customerProductService.DeleteCustomerProductPrice(productPrice); + } + + public virtual async Task UpdatePersonalizedProduct(CustomerModel.ProductModel model) + { + var customerproduct = await customerProductService.GetCustomerProduct(model.Id); + if (customerproduct != null) + { + customerproduct.DisplayOrder = model.DisplayOrder; + await customerProductService.UpdateCustomerProduct(customerproduct); + } + } + + public virtual async Task DeletePersonalizedProduct(string id) + { + var customerproduct = await customerProductService.GetCustomerProduct(id); + if (customerproduct == null) + throw new ArgumentException("No customerproduct found with the specified id"); + + await customerProductService.DeleteCustomerProduct(customerproduct); + } +} diff --git a/src/Web/Grand.Web.AdminShared/Services/CustomerShoppingCartViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CustomerShoppingCartViewModelService.cs new file mode 100644 index 0000000000..55c9c740e3 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Services/CustomerShoppingCartViewModelService.cs @@ -0,0 +1,111 @@ +using Grand.Business.Core.Extensions; +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.Orders; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Domain.Customers; +using Grand.Domain.Orders; +using Grand.Domain.Tax; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.ShoppingCart; +using Grand.Web.Common.Extensions; + +namespace Grand.Web.AdminShared.Services; + +public class CustomerShoppingCartViewModelService( + ICustomerService customerService, + IStoreService storeService, + IProductService productService, + IDateTimeService dateTimeService, + IContextAccessor contextAccessor, + ITaxService taxService, + IPricingService pricingService, + IPriceFormatter priceFormatter, + IProductAttributeFormatter productAttributeFormatter, + IShoppingCartService shoppingCartService) + : ICustomerShoppingCartViewModelService +{ + public virtual async Task> PrepareShoppingCartItemModel(string customerId, + int cartTypeId) + { + var customer = await customerService.GetCustomerById(customerId); + var cart = customer.ShoppingCartItems.Where(x => x.ShoppingCartTypeId == (ShoppingCartType)cartTypeId).ToList(); + var items = new List(); + if (cart.Any()) + { + foreach (var sci in cart) + { + var store = await storeService.GetStoreById(sci.StoreId); + var priceStore = store ?? contextAccessor.StoreContext.CurrentStore; + var product = await productService.GetProductById(sci.ProductId); + if (product != null) + { + var price = (await taxService.GetProductPrice(product, + (await pricingService.GetUnitPrice(sci, product, + customer, priceStore, + contextAccessor.WorkContext.WorkingCurrency)).unitprice, + contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + customer, priceStore)).productprice; + var sciModel = new ShoppingCartItemModel { + Id = sci.Id, + Store = store != null ? store.Shortcut : "Unknown", + ProductId = sci.ProductId, + Quantity = sci.Quantity, + ProductName = product.Name, + AttributeInfo = await productAttributeFormatter.FormatAttributes(product, sci.Attributes, + contextAccessor.CalculationContext() with { Customer = customer, Store = priceStore }, + contextAccessor.WorkContext.WorkingLanguage, contextAccessor.StoreContext.CurrentHost), + UnitPrice = priceFormatter.FormatPrice(price, contextAccessor.WorkContext.WorkingCurrency), + UnitPriceValue = price, + Total = priceFormatter.FormatPrice((await taxService.GetProductPrice(product, + (await pricingService.GetSubTotal(sci, product, + customer, priceStore, + contextAccessor.WorkContext.WorkingCurrency)).subTotal, + contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, + customer, priceStore)).productprice, + contextAccessor.WorkContext.WorkingCurrency), + UpdatedOn = sci.UpdatedOnUtc.HasValue + ? dateTimeService.ConvertToUserTime(sci.UpdatedOnUtc.Value, DateTimeKind.Utc) + : dateTimeService.ConvertToUserTime(sci.CreatedOnUtc, DateTimeKind.Utc) + }; + items.Add(sciModel); + } + } + } + + return items; + } + + public virtual async Task DeleteCart(Customer customer, string id) + { + var cart = customer.ShoppingCartItems.FirstOrDefault(a => a.Id == id); + if (cart != null) + { + await shoppingCartService.DeleteShoppingCartItem(customer, cart, ensureOnlyActiveCheckoutAttributes: true); + await customerService.UpdateCustomerInAdminPanel(customer); + } + } + + public virtual async Task> UpdateCart(Customer customer, string shoppingCartId, double? unitprice) + { + var cart = customer.ShoppingCartItems.FirstOrDefault(a => a.Id == shoppingCartId); + if (cart != null) + return await shoppingCartService.UpdateShoppingCartItem( + customer, contextAccessor.WorkContext.WorkingLanguage, + shoppingCartId, + cart.WarehouseId, + cart.Attributes, + unitprice, + cart.RentalStartDateUtc, + cart.RentalEndDateUtc, + cart.Quantity, + true, + cart.ReservationId, + shoppingCartId); + return new List(); + } +} diff --git a/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs index ec5ec8d565..fcad7fe8d4 100644 --- a/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/CustomerViewModelService.cs @@ -1,115 +1,81 @@ using Grand.Business.Core.Extensions; using Grand.Business.Core.Interfaces.Authentication; -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.Orders; -using Grand.Business.Core.Interfaces.Common.Addresses; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Stores; using Grand.Business.Core.Interfaces.Customers; -using Grand.Business.Core.Interfaces.Marketing.Contacts; using Grand.Business.Core.Interfaces.Marketing.Customers; using Grand.Business.Core.Interfaces.Marketing.Newsletters; using Grand.Business.Core.Interfaces.Messages; -using Grand.Business.Core.Interfaces.Storage; using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Customers; -using Grand.Domain.Directory; using Grand.Domain.Messages; using Grand.Domain.Orders; using Grand.Domain.Tax; using Grand.Infrastructure; using Grand.SharedKernel; -using Grand.Web.AdminShared.Extensions.Mapping; using Grand.Web.AdminShared.Interfaces; -using Grand.Web.AdminShared.Models.Catalog; -using Grand.Web.AdminShared.Models.Common; using Grand.Web.AdminShared.Models.Customers; -using Grand.Web.AdminShared.Models.Messages; -using Grand.Web.AdminShared.Models.ShoppingCart; -using Grand.Web.Common.Extensions; using Grand.Web.Common.Localization; using Grand.Web.Common.Models; using Grand.Web.Common.Security.Oidc; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.Extensions.DependencyInjection; -using System.Net; using System.Text; namespace Grand.Web.AdminShared.Services; public class CustomerViewModelService : ICustomerViewModelService { - private readonly IAddressAttributeParser _addressAttributeParser; - private readonly IAddressAttributeService _addressAttributeService; - private readonly AddressSettings _addressSettings; private readonly IAffiliateService _affiliateService; private readonly CommonSettings _commonSettings; private readonly ICountryService _countryService; private readonly ICustomerAttributeParser _customerAttributeParser; private readonly ICustomerAttributeService _customerAttributeService; - private readonly ICustomerNoteService _customerNoteService; - private readonly ICustomerProductService _customerProductService; private readonly ICustomerService _customerService; private readonly CustomerSettings _customerSettings; private readonly ICustomerTagService _customerTagService; private readonly IDateTimeService _dateTimeService; - private readonly IDownloadService _downloadService; private readonly IGroupService _groupService; private readonly IHttpContextAccessor _httpContextAccessor; - private readonly ILoyaltyPointsService _loyaltyPointsService; private readonly LoyaltyPointsSettings _loyaltyPointsSettings; private readonly INewsLetterSubscriptionService _newsLetterSubscriptionService; - private readonly IProductService _productService; private readonly ISalesEmployeeService _salesEmployeeService; private readonly IStoreService _storeService; private readonly TaxSettings _taxSettings; private readonly IRequestTranslationService _translationService; - private readonly IVendorService _vendorService; private readonly IContextAccessor _contextAccessor; private readonly IEnumTranslationService _enumTranslationService; public CustomerViewModelService( ICustomerService customerService, IGroupService groupService, - ICustomerProductService customerProductService, INewsLetterSubscriptionService newsLetterSubscriptionService, IDateTimeService dateTimeService, IRequestTranslationService translationService, - ILoyaltyPointsService loyaltyPointsService, ICountryService countryService, IContextAccessor contextAccessor, - IVendorService vendorService, IStoreService storeService, ICustomerAttributeParser customerAttributeParser, ICustomerAttributeService customerAttributeService, - IAddressAttributeParser addressAttributeParser, - IAddressAttributeService addressAttributeService, IAffiliateService affiliateService, ICustomerTagService customerTagService, - IProductService productService, ISalesEmployeeService salesEmployeeService, - ICustomerNoteService customerNoteService, - IDownloadService downloadService, IHttpContextAccessor httpContextAccessor, CustomerSettings customerSettings, TaxSettings taxSettings, LoyaltyPointsSettings loyaltyPointsSettings, - AddressSettings addressSettings, CommonSettings commonSettings, IEnumTranslationService enumTranslationService) { _customerService = customerService; _groupService = groupService; - _customerProductService = customerProductService; _newsLetterSubscriptionService = newsLetterSubscriptionService; _dateTimeService = dateTimeService; _translationService = translationService; - _loyaltyPointsService = loyaltyPointsService; _taxSettings = taxSettings; _loyaltyPointsSettings = loyaltyPointsSettings; _countryService = countryService; @@ -117,19 +83,12 @@ public CustomerViewModelService( _commonSettings = commonSettings; _enumTranslationService = enumTranslationService; _contextAccessor = contextAccessor; - _vendorService = vendorService; - _addressSettings = addressSettings; _storeService = storeService; _customerAttributeParser = customerAttributeParser; _customerAttributeService = customerAttributeService; - _addressAttributeParser = addressAttributeParser; - _addressAttributeService = addressAttributeService; _affiliateService = affiliateService; _customerTagService = customerTagService; - _productService = productService; _salesEmployeeService = salesEmployeeService; - _customerNoteService = customerNoteService; - _downloadService = downloadService; _httpContextAccessor = httpContextAccessor; } @@ -694,500 +653,6 @@ public async Task SendEmail(Customer customer, CustomerModel.SendEmailModel mode await queuedEmailService.InsertQueuedEmail(email); } - public virtual async Task> PrepareLoyaltyPointsHistoryModel( - string customerId) - { - var model = new List(); - foreach (var rph in await _loyaltyPointsService.GetLoyaltyPointsHistory(customerId, showAll: true)) - { - var store = await _storeService.GetStoreById(rph.StoreId); - model.Add(new CustomerModel.LoyaltyPointsHistoryModel { - StoreName = store != null ? store.Shortcut : "Unknown", - Points = rph.Points, - PointsBalance = rph.PointsBalance, - Message = rph.Message, - CreatedOn = _dateTimeService.ConvertToUserTime(rph.CreatedOnUtc, DateTimeKind.Utc) - }); - } - - return model; - } - - public virtual async Task InsertLoyaltyPointsHistory(Customer customer, string storeId, - int addLoyaltyPointsValue, string addLoyaltyPointsMessage) - { - return await _loyaltyPointsService.AddLoyaltyPointsHistory(customer.Id, addLoyaltyPointsValue, storeId, - addLoyaltyPointsMessage); - } - - public virtual async Task> PrepareAddressModel(Customer customer) - { - var addresses = customer.Addresses.ToList(); - var addressesListModel = new List(); - foreach (var x in addresses) - { - var model = await x.ToModel(_countryService); - var addressHtmlSb = new StringBuilder("
"); - if (_addressSettings.CompanyEnabled && !string.IsNullOrEmpty(model.Company)) - addressHtmlSb.AppendFormat("{0}
", WebUtility.HtmlEncode(model.Company)); - if (_addressSettings.StreetAddressEnabled && !string.IsNullOrEmpty(model.Address1)) - addressHtmlSb.AppendFormat("{0}
", WebUtility.HtmlEncode(model.Address1)); - if (_addressSettings.StreetAddress2Enabled && !string.IsNullOrEmpty(model.Address2)) - addressHtmlSb.AppendFormat("{0}
", WebUtility.HtmlEncode(model.Address2)); - if (_addressSettings.CityEnabled && !string.IsNullOrEmpty(model.City)) - addressHtmlSb.AppendFormat("{0},", WebUtility.HtmlEncode(model.City)); - if (_addressSettings.StateProvinceEnabled && !string.IsNullOrEmpty(model.StateProvinceName)) - addressHtmlSb.AppendFormat("{0},", WebUtility.HtmlEncode(model.StateProvinceName)); - if (_addressSettings.ZipPostalCodeEnabled && !string.IsNullOrEmpty(model.ZipPostalCode)) - addressHtmlSb.AppendFormat("{0}
", WebUtility.HtmlEncode(model.ZipPostalCode)); - if (_addressSettings.CountryEnabled && !string.IsNullOrEmpty(model.CountryName)) - addressHtmlSb.AppendFormat("{0}", WebUtility.HtmlEncode(model.CountryName)); - var customAttributesFormatted = - await _addressAttributeParser.FormatAttributes(_contextAccessor.WorkContext.WorkingLanguage, x.Attributes); - if (!string.IsNullOrEmpty(customAttributesFormatted)) - //already encoded - addressHtmlSb.AppendFormat("
{0}", customAttributesFormatted); - addressHtmlSb.Append("
"); - model.AddressHtml = addressHtmlSb.ToString(); - addressesListModel.Add(model); - } - - return addressesListModel; - } - - public virtual async Task DeleteAddress(Customer customer, Address address) - { - customer.RemoveAddress(address); - await _customerService.UpdateCustomerInAdminPanel(customer); - } - - public virtual async Task
InsertAddressModel(Customer customer, CustomerAddressModel model, - List customAttributes) - { - var address = model.Address.ToEntity(); - address.Attributes = customAttributes; - customer.Addresses.Add(address); - await _customerService.UpdateCustomerInAdminPanel(customer); - return address; - } - - public virtual async Task PrepareAddressModel(CustomerAddressModel model, Address address, Customer customer, - bool excludeProperties) - { - ArgumentNullException.ThrowIfNull(customer); - - model.CustomerId = customer.Id; - if (address != null) - if (!excludeProperties) - model.Address = await address.ToModel(_countryService); - - model.Address ??= new AddressModel(); - - model.Address.NameEnabled = _addressSettings.NameEnabled; - model.Address.FirstNameEnabled = true; - model.Address.FirstNameRequired = true; - model.Address.LastNameEnabled = true; - model.Address.LastNameRequired = true; - model.Address.EmailEnabled = true; - model.Address.EmailRequired = true; - model.Address.CompanyEnabled = _addressSettings.CompanyEnabled; - model.Address.CompanyRequired = _addressSettings.CompanyRequired; - model.Address.VatNumberEnabled = _addressSettings.VatNumberEnabled; - model.Address.VatNumberRequired = _addressSettings.VatNumberRequired; - model.Address.CountryEnabled = _addressSettings.CountryEnabled; - model.Address.StateProvinceEnabled = _addressSettings.StateProvinceEnabled; - model.Address.CityEnabled = _addressSettings.CityEnabled; - model.Address.CityRequired = _addressSettings.CityRequired; - model.Address.StreetAddressEnabled = _addressSettings.StreetAddressEnabled; - model.Address.StreetAddressRequired = _addressSettings.StreetAddressRequired; - model.Address.StreetAddress2Enabled = _addressSettings.StreetAddress2Enabled; - model.Address.StreetAddress2Required = _addressSettings.StreetAddress2Required; - model.Address.ZipPostalCodeEnabled = _addressSettings.ZipPostalCodeEnabled; - model.Address.ZipPostalCodeRequired = _addressSettings.ZipPostalCodeRequired; - model.Address.PhoneEnabled = _addressSettings.PhoneEnabled; - model.Address.PhoneRequired = _addressSettings.PhoneRequired; - model.Address.FaxEnabled = _addressSettings.FaxEnabled; - model.Address.FaxRequired = _addressSettings.FaxRequired; - model.Address.NoteEnabled = _addressSettings.NoteEnabled; - model.Address.AddressTypeEnabled = _addressSettings.AddressTypeEnabled; - - //countries - model.Address.AvailableCountries.Add(new SelectListItem - { Text = _translationService.GetResource("Admin.Address.SelectCountry"), Value = "" }); - foreach (var c in await _countryService.GetAllCountries(showHidden: true)) - model.Address.AvailableCountries.Add(new SelectListItem - { Text = c.Name, Value = c.Id, Selected = c.Id == model.Address.CountryId }); - //states - var states = !string.IsNullOrEmpty(model.Address.CountryId) - ? (await _countryService.GetCountryById(model.Address.CountryId))?.StateProvinces - : new List(); - if (states?.Count > 0) - foreach (var s in states) - model.Address.AvailableStates.Add(new SelectListItem - { Text = s.Name, Value = s.Id, Selected = s.Id == model.Address.StateProvinceId }); - - //customer attribute services - await model.Address.PrepareCustomAddressAttributes(address, _addressAttributeService, _addressAttributeParser); - } - - public virtual async Task
UpdateAddressModel(Customer customer, Address address, - CustomerAddressModel model, List customAttributes) - { - address = model.Address.ToEntity(address); - address.Attributes = customAttributes; - await _customerService.UpdateCustomerInAdminPanel(customer); - return address; - } - - public virtual async Task> PrepareShoppingCartItemModel(string customerId, - int cartTypeId) - { - var customer = await _customerService.GetCustomerById(customerId); - var cart = customer.ShoppingCartItems.Where(x => x.ShoppingCartTypeId == (ShoppingCartType)cartTypeId).ToList(); - var items = new List(); - if (cart.Any()) - { - var taxService = _httpContextAccessor.HttpContext!.RequestServices.GetRequiredService(); - var priceCalculationService = - _httpContextAccessor.HttpContext!.RequestServices.GetRequiredService(); - var priceFormatter = - _httpContextAccessor.HttpContext!.RequestServices.GetRequiredService(); - var productAttributeFormatter = _httpContextAccessor.HttpContext!.RequestServices - .GetRequiredService(); - foreach (var sci in cart) - { - var store = await _storeService.GetStoreById(sci.StoreId); - var priceStore = store ?? _contextAccessor.StoreContext.CurrentStore; - var product = await _productService.GetProductById(sci.ProductId); - if (product != null) - { - var price = (await taxService.GetProductPrice(product, - (await priceCalculationService.GetUnitPrice(sci, product, - customer, priceStore, - _contextAccessor.WorkContext.WorkingCurrency)).unitprice, - _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, - customer, priceStore)).productprice; - var sciModel = new ShoppingCartItemModel { - Id = sci.Id, - Store = store != null ? store.Shortcut : "Unknown", - ProductId = sci.ProductId, - Quantity = sci.Quantity, - ProductName = product.Name, - AttributeInfo = await productAttributeFormatter.FormatAttributes(product, sci.Attributes, - _contextAccessor.CalculationContext() with { Customer = customer, Store = priceStore }, - _contextAccessor.WorkContext.WorkingLanguage, _contextAccessor.StoreContext.CurrentHost), - UnitPrice = priceFormatter.FormatPrice(price, _contextAccessor.WorkContext.WorkingCurrency), - UnitPriceValue = price, - Total = priceFormatter.FormatPrice((await taxService.GetProductPrice(product, - (await priceCalculationService.GetSubTotal(sci, product, - customer, priceStore, - _contextAccessor.WorkContext.WorkingCurrency)).subTotal, - _contextAccessor.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax, - customer, priceStore)).productprice, - _contextAccessor.WorkContext.WorkingCurrency), - UpdatedOn = sci.UpdatedOnUtc.HasValue - ? _dateTimeService.ConvertToUserTime(sci.UpdatedOnUtc.Value, DateTimeKind.Utc) - : _dateTimeService.ConvertToUserTime(sci.CreatedOnUtc, DateTimeKind.Utc) - }; - items.Add(sciModel); - } - } - } - - return items; - } - - public virtual async Task DeleteCart(Customer customer, string id) - { - var cart = customer.ShoppingCartItems.FirstOrDefault(a => a.Id == id); - if (cart != null) - { - await _httpContextAccessor.HttpContext!.RequestServices.GetRequiredService() - .DeleteShoppingCartItem(customer, cart, ensureOnlyActiveCheckoutAttributes: true); - await _customerService.UpdateCustomerInAdminPanel(customer); - } - } - - public virtual async Task> UpdateCart(Customer customer, string shoppingCartId, double? unitprice) - { - var cart = customer.ShoppingCartItems.FirstOrDefault(a => a.Id == shoppingCartId); - if (cart != null) - return await _httpContextAccessor.HttpContext!.RequestServices.GetRequiredService() - .UpdateShoppingCartItem( - customer, _contextAccessor.WorkContext.WorkingLanguage, - shoppingCartId, - cart.WarehouseId, - cart.Attributes, - unitprice, - cart.RentalStartDateUtc, - cart.RentalEndDateUtc, - cart.Quantity, - true, - cart.ReservationId, - shoppingCartId); - return new List(); - } - - public virtual async Task<(IEnumerable productPriceModels, int totalCount)> - PrepareProductPriceModel(string customerId, int pageIndex, int pageSize) - { - var productPrices = - await _customerProductService.GetProductsPriceByCustomer(customerId, pageIndex - 1, pageSize); - var items = new List(); - foreach (var x in productPrices) - { - var m = new CustomerModel.ProductPriceModel { - Id = x.Id, - Price = x.Price, - ProductId = x.ProductId, - ProductName = (await _productService.GetProductById(x.ProductId))?.Name - }; - items.Add(m); - } - - return (items, productPrices.TotalCount); - } - - public virtual async Task<(IEnumerable productModels, int totalCount)> - PreparePersonalizedProducts(string customerId, int pageIndex, int pageSize) - { - var products = await _customerProductService.GetProductsByCustomer(customerId, pageIndex - 1, pageSize); - var items = new List(); - foreach (var x in products) - { - var m = new CustomerModel.ProductModel { - Id = x.Id, - DisplayOrder = x.DisplayOrder, - ProductId = x.ProductId, - ProductName = (await _productService.GetProductById(x.ProductId))?.Name - }; - items.Add(m); - } - - return (items, products.TotalCount); - } - - public virtual async Task PrepareCustomerModelAddProductModel() - { - var model = new CustomerModel.AddProductModel(); - - //stores - model.AvailableStores.Add(new SelectListItem - { Text = _translationService.GetResource("Admin.Common.All"), Value = " " }); - foreach (var s in await _storeService.GetAllStores()) - model.AvailableStores.Add(new SelectListItem { Text = s.Shortcut, Value = s.Id }); - - //vendors - 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 = " " }); - - return model; - } - - public virtual async Task<(IList products, int totalCount)> PrepareProductModel( - CustomerModel.AddProductModel model, int pageIndex, int pageSize) - { - var products = await _productService.PrepareProductList(model.SearchCategoryId, model.SearchBrandId, - model.SearchCollectionId, model.SearchStoreId, model.SearchVendorId, model.SearchProductTypeId, - model.SearchProductName, pageIndex, pageSize); - return (products.Select(x => x.ToModel()).ToList(), products.TotalCount); - } - - public virtual async Task InsertCustomerAddProductModel(string customerId, bool personalized, - CustomerModel.AddProductModel model) - { - foreach (var id in model.SelectedProductIds) - { - var product = await _productService.GetProductById(id); - if (product != null) - { - if (!personalized) - { - if (!(await _customerProductService.GetPriceByCustomerProduct(customerId, id)).HasValue) - await _customerProductService.InsertCustomerProductPrice(new CustomerProductPrice - { CustomerId = customerId, ProductId = id, Price = product.Price }); - } - else - { - if (await _customerProductService.GetCustomerProduct(customerId, id) == null) - await _customerProductService.InsertCustomerProduct(new CustomerProduct - { CustomerId = customerId, ProductId = id, DisplayOrder = 0 }); - } - } - } - } - - public virtual async Task UpdateProductPrice(CustomerModel.ProductPriceModel model) - { - var productPrice = await _customerProductService.GetCustomerProductPriceById(model.Id); - if (productPrice != null) - { - productPrice.Price = model.Price; - await _customerProductService.UpdateCustomerProductPrice(productPrice); - } - } - - public virtual async Task DeleteProductPrice(string id) - { - var productPrice = await _customerProductService.GetCustomerProductPriceById(id); - if (productPrice == null) - throw new ArgumentException("No productPrice found with the specified id"); - - await _customerProductService.DeleteCustomerProductPrice(productPrice); - } - - public virtual async Task UpdatePersonalizedProduct(CustomerModel.ProductModel model) - { - var customerproduct = await _customerProductService.GetCustomerProduct(model.Id); - if (customerproduct != null) - { - customerproduct.DisplayOrder = model.DisplayOrder; - await _customerProductService.UpdateCustomerProduct(customerproduct); - } - } - - public virtual async Task DeletePersonalizedProduct(string id) - { - var customerproduct = await _customerProductService.GetCustomerProduct(id); - if (customerproduct == null) - throw new ArgumentException("No customerproduct found with the specified id"); - - await _customerProductService.DeleteCustomerProduct(customerproduct); - } - - public virtual async Task<(IEnumerable contactFormModels, int totalCount)> - PrepareContactFormModel(string customerId, int pageIndex, int pageSize) - { - var contactUsService = - _httpContextAccessor.HttpContext!.RequestServices.GetRequiredService(); - var contactform = await contactUsService.GetAllContactUs(storeId: "", vendorId: "", customerId: customerId, - pageIndex: pageIndex - 1, pageSize: pageSize); - var items = new List(); - foreach (var x in contactform) - { - var store = await _storeService.GetStoreById(x.StoreId); - var m = x.ToModel(); - m.CreatedOn = _dateTimeService.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc); - m.Enquiry = ""; - m.Email = m.FullName + " - " + m.Email; - m.Store = store != null ? store.Shortcut : "-empty-"; - items.Add(m); - } - - return (items, contactform.TotalCount); - } - - public virtual async - Task<(IEnumerable outOfStockSubscriptionModels, int totalCount)> - PrepareOutOfStockSubscriptionModel(string customerId, int pageIndex, int pageSize) - { - var outOfStockSubscriptionService = _httpContextAccessor.HttpContext!.RequestServices - .GetRequiredService(); - var subscriptions = - await outOfStockSubscriptionService.GetAllSubscriptionsByCustomerId(customerId, "", pageIndex - 1, - pageSize); - var items = new List(); - if (subscriptions.Any()) - { - var productAttributeFormatter = _httpContextAccessor.HttpContext!.RequestServices - .GetRequiredService(); - foreach (var x in subscriptions) - { - var store = await _storeService.GetStoreById(x.StoreId); - var product = await _productService.GetProductById(x.ProductId); - var m = new CustomerModel.OutOfStockSubscriptionModel { - Id = x.Id, - StoreName = store != null ? store.Shortcut : "Unknown", - ProductId = x.ProductId, - ProductName = product != null ? product.Name : "Unknown", - AttributeDescription = x.Attributes == null || !x.Attributes.Any() - ? "" - : await productAttributeFormatter.FormatAttributes(product, x.Attributes, - _contextAccessor.CalculationContext(), _contextAccessor.WorkContext.WorkingLanguage, - _contextAccessor.StoreContext.CurrentHost), - CreatedOn = _dateTimeService.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc) - }; - items.Add(m); - } - } - - return (items, subscriptions.TotalCount); - } - - public virtual async Task> PrepareCustomerNoteList(string customerId) - { - var downloadService = _httpContextAccessor.HttpContext!.RequestServices.GetRequiredService(); - var customerNoteModels = new List(); - foreach (var customerNote in (await _customerNoteService.GetCustomerNotes(customerId)) - .OrderByDescending(on => on.CreatedOnUtc)) - { - var download = !string.IsNullOrEmpty(customerNote.DownloadId) - ? await downloadService.GetDownloadById(customerNote.DownloadId) - : null; - customerNoteModels.Add(new CustomerModel.CustomerNote { - Id = customerNote.Id, - CustomerId = customerId, - DownloadId = string.IsNullOrEmpty(customerNote.DownloadId) ? "" : customerNote.DownloadId, - DownloadGuid = download?.DownloadGuid ?? Guid.Empty, - DisplayToCustomer = customerNote.DisplayToCustomer, - Title = customerNote.Title, - Note = customerNote.Note, - CreatedOn = _dateTimeService.ConvertToUserTime(customerNote.CreatedOnUtc, DateTimeKind.Utc) - }); - } - - return customerNoteModels; - } - - public virtual async Task InsertCustomerNote(string customerId, string downloadId, - bool displayToCustomer, string title, string message) - { - var customerNote = new CustomerNote { - DisplayToCustomer = displayToCustomer, - Title = title, - Note = message, - DownloadId = downloadId, - CustomerId = customerId - }; - await _customerNoteService.InsertCustomerNote(customerNote); - - //new customer note notification - if (displayToCustomer) - { - //email - var messageProviderService = _httpContextAccessor.HttpContext!.RequestServices - .GetRequiredService(); - await messageProviderService.SendNewCustomerNoteMessage(customerNote, - await _customerService.GetCustomerById(customerId), _contextAccessor.StoreContext.CurrentStore, - _contextAccessor.WorkContext.WorkingLanguage.Id); - } - - return customerNote; - } - - public virtual async Task DeleteCustomerNote(string id, string customerId) - { - var customerNote = await _customerNoteService.GetCustomerNote(id); - if (customerNote == null) - throw new ArgumentException("No customer note found with the specified id"); - - await _customerNoteService.DeleteCustomerNote(customerNote); - - //delete an old "attachment" file - if (!string.IsNullOrEmpty(customerNote.DownloadId)) - { - var attachment = await _downloadService.GetDownloadById(customerNote.DownloadId); - if (attachment != null) - await _downloadService.DeleteDownload(attachment); - } - } - #region Utilities protected virtual string[] ParseCustomerTags(string customerTags) diff --git a/src/Web/Grand.Web.AdminShared/Startup/StartupApplication.cs b/src/Web/Grand.Web.AdminShared/Startup/StartupApplication.cs index 573519c64f..5f599f9478 100644 --- a/src/Web/Grand.Web.AdminShared/Startup/StartupApplication.cs +++ b/src/Web/Grand.Web.AdminShared/Startup/StartupApplication.cs @@ -1,4 +1,4 @@ -using elFinder.Net.AspNetCore.Extensions; +using elFinder.Net.AspNetCore.Extensions; using elFinder.Net.Drivers.FileSystem.Extensions; using Grand.Domain.Blogs; using Grand.Domain.Catalog; @@ -47,6 +47,13 @@ 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(); diff --git a/src/Web/Grand.Web.Store/Controllers/CustomerController.cs b/src/Web/Grand.Web.Store/Controllers/CustomerController.cs index d9577350c8..123e01bf7b 100644 --- a/src/Web/Grand.Web.Store/Controllers/CustomerController.cs +++ b/src/Web/Grand.Web.Store/Controllers/CustomerController.cs @@ -1,4 +1,4 @@ -using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Security; using Grand.Business.Core.Utilities.Customers; using Grand.Domain.Customers; @@ -29,6 +29,11 @@ namespace Grand.Web.Store.Controllers; public class CustomerController( Grand.Business.Core.Interfaces.Customers.ICustomerService customerService, ICustomerViewModelService customerViewModelService, + ICustomerAddressViewModelService customerAddressViewModelService, + ICustomerLoyaltyPointsViewModelService customerLoyaltyPointsViewModelService, + ICustomerShoppingCartViewModelService customerShoppingCartViewModelService, + ICustomerProductViewModelService customerProductViewModelService, + ICustomerOutOfStockSubscriptionViewModelService customerOutOfStockSubscriptionViewModelService, Grand.Business.Core.Interfaces.Customers.ICustomerManagerService customerManagerService, Grand.Business.Core.Interfaces.Marketing.Customers.ICustomerProductService customerProductService, Grand.Business.Core.Interfaces.Catalog.Products.IProductReviewService productReviewService, @@ -45,7 +50,9 @@ public class CustomerController( CustomerSettings customerSettings, IAdminDataScope scope, CustomerConfig customerConfig) - : BaseCustomerController(customerService, customerViewModelService, customerManagerService, + : BaseCustomerController(customerService, customerViewModelService, customerAddressViewModelService, + customerLoyaltyPointsViewModelService, customerShoppingCartViewModelService, customerProductViewModelService, + customerOutOfStockSubscriptionViewModelService, customerManagerService, customerProductService, productReviewService, productReviewViewModelService, productViewModelService, customerAttributeParser, customerAttributeService, addressAttributeParser, addressAttributeService, messageProviderService, groupService, translationService, contextAccessor, customerSettings, scope) From be6263107e0dd28b0a897ee907b356060c12e4d2 Mon Sep 17 00:00:00 2001 From: support Date: Thu, 22 Oct 2026 22:11:13 +0200 Subject: [PATCH 2/2] Split the customer edit tabs out of BaseCustomerController Each customer edit tab now has its own controller: Address, LoyaltyPoints, ShoppingCart, Product, OutOfStockSubscription, Order and Review in AdminShared with thin Admin and Store subclasses, plus Admin-only CustomerNote and CustomerContactForm. They share BaseCustomerScopedController, whose ownership check redirects to the customer list. BaseCustomerController keeps the list, the form and the Info tab, and its constructor drops from 22 to 11 parameters, so List and Edit no longer build the cart, pricing and tax services. The store panel's per-store customer gate moves from a CustomerController override to a [PerStoreCustomers] filter, applied to every customer controller in Grand.Web.Store so the tab controllers are closed too when RegisterCustomersPerStore is off. Fixes: - GetCartList loads and checks the customer in both panels and passes it to the service; an unknown customer gives an empty grid instead of a NullReferenceException, and the store panel no longer reads it twice. - DeleteCustomerNote rejects a note that belongs to another customer. - Removes the UTF-8 BOMs the previous commit added to existing files. Adds tests for the shopping cart, out-of-stock subscription and contact form view model services, the tab controllers, the note controller and the gate. --- .../BaseCustomerAddressControllerTests.cs | 91 ++++ .../BaseCustomerControllerTests.cs | 457 +--------------- ...aseCustomerLoyaltyPointsControllerTests.cs | 62 +++ .../BaseCustomerManagementControllerTests.cs | 37 +- .../BaseCustomerOrderControllerTests.cs | 69 +++ ...erOutOfStockSubscriptionControllerTests.cs | 55 ++ .../BaseCustomerProductControllerTests.cs | 142 +++++ .../BaseCustomerReviewControllerTests.cs | 72 +++ ...BaseCustomerShoppingCartControllerTests.cs | 79 +++ .../CustomerControllerAttributeTests.cs | 25 + .../CustomerControllerTestsBase.cs | 114 ++++ .../CustomerNoteControllerTests.cs | 51 ++ ...ustomerContactFormViewModelServiceTests.cs | 72 +++ .../CustomerNoteViewModelServiceTests.cs | 14 +- ...fStockSubscriptionViewModelServiceTests.cs | 75 +++ ...stomerShoppingCartViewModelServiceTests.cs | 114 ++++ .../Services/CustomerViewModelServiceTests.cs | 2 +- .../CustomerControllerAttributeTests.cs | 42 +- .../Controllers/CustomerControllerTests.cs | 119 ++-- .../CreateOrUpdate.TabActivityLog.cshtml | 2 +- .../Partials/CreateOrUpdate.TabNotes.cshtml | 6 +- .../AddressCreate.cshtml | 4 +- .../AddressEdit.cshtml | 4 +- .../WidgetZone.AddressDetailsBottom.cshtml | 0 .../WidgetZone.AddressDetailsTop.cshtml | 0 .../ProductAddPopup.cshtml | 4 +- .../Controllers/CustomerAddressController.cs | 23 + .../CustomerContactFormController.cs | 33 ++ .../Controllers/CustomerController.cs | 26 +- .../CustomerLoyaltyPointsController.cs | 18 + .../Controllers/CustomerNoteController.cs | 57 ++ .../Controllers/CustomerOrderController.cs | 22 + ...ustomerOutOfStockSubscriptionController.cs | 18 + .../Controllers/CustomerProductController.cs | 20 + .../Controllers/CustomerReviewController.cs | 21 + .../CustomerShoppingCartController.cs | 18 + .../BaseCustomerAddressController.cs | 123 +++++ .../Controllers/BaseCustomerController.cs | 508 +----------------- .../BaseCustomerLoyaltyPointsController.cs | 47 ++ .../BaseCustomerManagementController.cs | 80 +-- .../BaseCustomerOrderController.cs | 71 +++ ...ustomerOutOfStockSubscriptionController.cs | 35 ++ .../BaseCustomerProductController.cs | 160 ++++++ .../BaseCustomerReviewController.cs | 59 ++ .../BaseCustomerScopedController.cs | 44 ++ .../BaseCustomerShoppingCartController.cs | 53 ++ .../ICustomerShoppingCartViewModelService.cs | 4 +- .../Services/CustomerNoteViewModelService.cs | 2 +- .../CustomerShoppingCartViewModelService.cs | 5 +- .../Startup/StartupApplication.cs | 2 +- .../CreateOrUpdate.TabAddresses.cshtml | 8 +- ...eateOrUpdate.TabCurrentShoppingCart.cshtml | 8 +- .../CreateOrUpdate.TabCurrentWishlist.cshtml | 4 +- .../CreateOrUpdate.TabLoyaltyPoints.cshtml | 4 +- .../Partials/CreateOrUpdate.TabOrders.cshtml | 4 +- ...OrUpdate.TabOutOfStockSubscriptions.cshtml | 2 +- .../Partials/CreateOrUpdate.TabProduct.cshtml | 8 +- .../CreateOrUpdate.TabProductPrice.cshtml | 8 +- .../Partials/CreateOrUpdate.TabReviews.cshtml | 4 +- .../Partials/CreateOrUpdateAddress.cshtml | 0 .../AddressCreate.cshtml | 4 +- .../AddressEdit.cshtml | 4 +- .../WidgetZone.AddressDetailsBottom.cshtml | 0 .../WidgetZone.AddressDetailsTop.cshtml | 0 .../ProductAddPopup.cshtml | 4 +- .../Controllers/CustomerAddressController.cs | 25 + .../Controllers/CustomerController.cs | 52 +- .../CustomerLoyaltyPointsController.cs | 20 + .../Controllers/CustomerOrderController.cs | 24 + ...ustomerOutOfStockSubscriptionController.cs | 20 + .../Controllers/CustomerProductController.cs | 22 + .../Controllers/CustomerReviewController.cs | 23 + .../CustomerShoppingCartController.cs | 20 + .../Filters/PerStoreCustomersAttribute.cs | 38 ++ 74 files changed, 2243 insertions(+), 1224 deletions(-) create mode 100644 src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerAddressControllerTests.cs create mode 100644 src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerLoyaltyPointsControllerTests.cs create mode 100644 src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerOrderControllerTests.cs create mode 100644 src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerOutOfStockSubscriptionControllerTests.cs create mode 100644 src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerProductControllerTests.cs create mode 100644 src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerReviewControllerTests.cs create mode 100644 src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerShoppingCartControllerTests.cs create mode 100644 src/Tests/Grand.Web.Admin.Tests/Controllers/CustomerControllerTestsBase.cs create mode 100644 src/Tests/Grand.Web.Admin.Tests/Controllers/CustomerNoteControllerTests.cs create mode 100644 src/Tests/Grand.Web.Admin.Tests/Services/CustomerContactFormViewModelServiceTests.cs create mode 100644 src/Tests/Grand.Web.Admin.Tests/Services/CustomerOutOfStockSubscriptionViewModelServiceTests.cs create mode 100644 src/Tests/Grand.Web.Admin.Tests/Services/CustomerShoppingCartViewModelServiceTests.cs rename src/Web/Grand.Web.Admin/Areas/Admin/Views/{Customer => CustomerAddress}/AddressCreate.cshtml (81%) rename src/Web/Grand.Web.Admin/Areas/Admin/Views/{Customer => CustomerAddress}/AddressEdit.cshtml (81%) rename src/Web/Grand.Web.Admin/Areas/Admin/Views/{Customer => CustomerAddress}/Partials/WidgetZone.AddressDetailsBottom.cshtml (100%) rename src/Web/Grand.Web.Admin/Areas/Admin/Views/{Customer => CustomerAddress}/Partials/WidgetZone.AddressDetailsTop.cshtml (100%) rename src/Web/Grand.Web.Admin/Areas/Admin/Views/{Customer => CustomerProduct}/ProductAddPopup.cshtml (97%) create mode 100644 src/Web/Grand.Web.Admin/Controllers/CustomerAddressController.cs create mode 100644 src/Web/Grand.Web.Admin/Controllers/CustomerContactFormController.cs create mode 100644 src/Web/Grand.Web.Admin/Controllers/CustomerLoyaltyPointsController.cs create mode 100644 src/Web/Grand.Web.Admin/Controllers/CustomerNoteController.cs create mode 100644 src/Web/Grand.Web.Admin/Controllers/CustomerOrderController.cs create mode 100644 src/Web/Grand.Web.Admin/Controllers/CustomerOutOfStockSubscriptionController.cs create mode 100644 src/Web/Grand.Web.Admin/Controllers/CustomerProductController.cs create mode 100644 src/Web/Grand.Web.Admin/Controllers/CustomerReviewController.cs create mode 100644 src/Web/Grand.Web.Admin/Controllers/CustomerShoppingCartController.cs create mode 100644 src/Web/Grand.Web.AdminShared/Controllers/BaseCustomerAddressController.cs create mode 100644 src/Web/Grand.Web.AdminShared/Controllers/BaseCustomerLoyaltyPointsController.cs create mode 100644 src/Web/Grand.Web.AdminShared/Controllers/BaseCustomerOrderController.cs create mode 100644 src/Web/Grand.Web.AdminShared/Controllers/BaseCustomerOutOfStockSubscriptionController.cs create mode 100644 src/Web/Grand.Web.AdminShared/Controllers/BaseCustomerProductController.cs create mode 100644 src/Web/Grand.Web.AdminShared/Controllers/BaseCustomerReviewController.cs create mode 100644 src/Web/Grand.Web.AdminShared/Controllers/BaseCustomerScopedController.cs create mode 100644 src/Web/Grand.Web.AdminShared/Controllers/BaseCustomerShoppingCartController.cs rename src/Web/Grand.Web.AdminShared/Views/AdminShared/{Customer => CustomerAddress}/Partials/CreateOrUpdateAddress.cshtml (100%) rename src/Web/Grand.Web.Store/Areas/Store/Views/{Customer => CustomerAddress}/AddressCreate.cshtml (79%) rename src/Web/Grand.Web.Store/Areas/Store/Views/{Customer => CustomerAddress}/AddressEdit.cshtml (79%) rename src/Web/Grand.Web.Store/Areas/Store/Views/{Customer => CustomerAddress}/Partials/WidgetZone.AddressDetailsBottom.cshtml (100%) rename src/Web/Grand.Web.Store/Areas/Store/Views/{Customer => CustomerAddress}/Partials/WidgetZone.AddressDetailsTop.cshtml (100%) rename src/Web/Grand.Web.Store/Areas/Store/Views/{Customer => CustomerProduct}/ProductAddPopup.cshtml (96%) create mode 100644 src/Web/Grand.Web.Store/Controllers/CustomerAddressController.cs create mode 100644 src/Web/Grand.Web.Store/Controllers/CustomerLoyaltyPointsController.cs create mode 100644 src/Web/Grand.Web.Store/Controllers/CustomerOrderController.cs create mode 100644 src/Web/Grand.Web.Store/Controllers/CustomerOutOfStockSubscriptionController.cs create mode 100644 src/Web/Grand.Web.Store/Controllers/CustomerProductController.cs create mode 100644 src/Web/Grand.Web.Store/Controllers/CustomerReviewController.cs create mode 100644 src/Web/Grand.Web.Store/Controllers/CustomerShoppingCartController.cs create mode 100644 src/Web/Grand.Web.Store/Filters/PerStoreCustomersAttribute.cs diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerAddressControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerAddressControllerTests.cs new file mode 100644 index 0000000000..6e531ee61c --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerAddressControllerTests.cs @@ -0,0 +1,91 @@ +using Grand.Business.Core.Interfaces.Common.Addresses; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Domain.Common; +using Grand.Domain.Customers; +using Grand.Web.AdminShared.Controllers; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Common; +using Grand.Web.AdminShared.Models.Customers; +using Grand.Web.Common.DataSource; +using Grand.Web.Common.Localization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class BaseCustomerAddressControllerTests + : CustomerControllerTestsBase +{ + public class TestCustomerAddressController( + ICustomerService customerService, + ICustomerAddressViewModelService customerAddressViewModelService, + IAddressAttributeParser addressAttributeParser, + IAddressAttributeService addressAttributeService, + IRequestTranslationService translationService, + IAdminDataScope scope) + : BaseCustomerAddressController(customerService, customerAddressViewModelService, addressAttributeParser, + addressAttributeService, translationService, scope); + + protected override TestCustomerAddressController CreateController() => new( + CustomerServiceMock.Object, + CustomerAddressViewModelServiceMock.Object, + AddressAttributeParserMock.Object, + AddressAttributeServiceMock.Object, + TranslationServiceMock.Object, + ScopeMock.Object); + + [TestMethod] + public async Task AddressesSelect_Denied_ThrowsArgumentException() + { + CustomerServiceMock.Setup(s => s.GetCustomerById("missing")).ReturnsAsync((Customer)null); + + await Assert.ThrowsAsync(() => + Controller.AddressesSelect("missing", new DataSourceRequest())); + } + + [TestMethod] + public async Task AddressCreatePost_Authorized_InsertsAndRedirectsToAddressEdit() + { + var customer = SetupAuthorizedCustomer("c1"); + var model = new CustomerAddressModel { CustomerId = "c1", Address = new AddressModel() }; + CustomerAddressViewModelServiceMock + .Setup(v => v.InsertAddressModel(customer, model, It.IsAny>())) + .ReturnsAsync(new Address { Id = "a1" }); + + var result = await Controller.AddressCreate(model); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("AddressEdit", redirect.ActionName); + Assert.IsNull(redirect.ControllerName); + } + + [TestMethod] + public async Task AddressCreateGet_Denied_RedirectsToCustomerList() + { + CustomerServiceMock.Setup(s => s.GetCustomerById("missing")).ReturnsAsync((Customer)null); + + var result = await Controller.AddressCreate("missing"); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("List", redirect.ActionName); + Assert.AreEqual("Customer", redirect.ControllerName); + } + + [TestMethod] + public async Task AddressEditGet_UnknownAddress_RedirectsToCustomerEdit() + { + SetupAuthorizedCustomer("c1"); + + var result = await Controller.AddressEdit("missing-address", "c1"); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("Edit", redirect.ActionName); + Assert.AreEqual("Customer", redirect.ControllerName); + Assert.AreEqual("c1", redirect.RouteValues!["id"]); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerControllerTests.cs index 14f5b63922..ae96d0a240 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerControllerTests.cs @@ -1,144 +1,51 @@ -using Grand.Business.Core.Interfaces.Common.Addresses; -using Grand.Business.Core.Interfaces.Common.Directory; -using Grand.Business.Core.Interfaces.Customers; -using Grand.Business.Core.Interfaces.Marketing.Customers; using Grand.Domain.Customers; using Grand.Infrastructure; -using Grand.Infrastructure.Configuration; using Grand.Web.AdminShared.Controllers; -using Grand.Web.AdminShared.Interfaces; -using Grand.Web.AdminShared.Models.Common; using Grand.Web.AdminShared.Models.Customers; using Grand.Web.Common.DataSource; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.AspNetCore.Mvc.Routing; -using Microsoft.AspNetCore.Mvc.ViewFeatures; -using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; -using Grand.Web.Common.Localization; -using Grand.Web.Common.Security.Authorization; namespace Grand.Web.Admin.Tests.Controllers; [TestClass] -public class BaseCustomerControllerTests +public class BaseCustomerControllerTests : CustomerControllerTestsBase { // BaseCustomerController is abstract; minimal subclass so actions can be invoked directly. - // The full 22-dependency constructor is declared once here; every later task's tests reuse - // this same TestCustomerController and Setup(). - protected class TestCustomerController( - ICustomerService customerService, - ICustomerViewModelService customerViewModelService, - ICustomerAddressViewModelService customerAddressViewModelService, - ICustomerLoyaltyPointsViewModelService customerLoyaltyPointsViewModelService, - ICustomerShoppingCartViewModelService customerShoppingCartViewModelService, - ICustomerProductViewModelService customerProductViewModelService, - ICustomerOutOfStockSubscriptionViewModelService customerOutOfStockSubscriptionViewModelService, - ICustomerManagerService customerManagerService, - ICustomerProductService customerProductService, - Grand.Business.Core.Interfaces.Catalog.Products.IProductReviewService productReviewService, - Grand.Web.AdminShared.Interfaces.IProductReviewViewModelService productReviewViewModelService, - Grand.Web.AdminShared.Interfaces.IProductViewModelService productViewModelService, - ICustomerAttributeParser customerAttributeParser, - ICustomerAttributeService customerAttributeService, - IAddressAttributeParser addressAttributeParser, - IAddressAttributeService addressAttributeService, + public class TestCustomerController( + Grand.Business.Core.Interfaces.Customers.ICustomerService customerService, + Grand.Web.AdminShared.Interfaces.ICustomerViewModelService customerViewModelService, + Grand.Business.Core.Interfaces.Customers.ICustomerManagerService customerManagerService, + Grand.Business.Core.Interfaces.Customers.ICustomerAttributeParser customerAttributeParser, + Grand.Business.Core.Interfaces.Customers.ICustomerAttributeService customerAttributeService, Grand.Business.Core.Interfaces.Messages.IMessageProviderService messageProviderService, - IGroupService groupService, - IRequestTranslationService translationService, + Grand.Business.Core.Interfaces.Common.Directory.IGroupService groupService, + Grand.Web.Common.Localization.IRequestTranslationService translationService, IContextAccessor contextAccessor, CustomerSettings customerSettings, - IAdminDataScope scope) - : BaseCustomerController(customerService, customerViewModelService, customerAddressViewModelService, - customerLoyaltyPointsViewModelService, customerShoppingCartViewModelService, - customerProductViewModelService, customerOutOfStockSubscriptionViewModelService, customerManagerService, - customerProductService, productReviewService, productReviewViewModelService, - productViewModelService, customerAttributeParser, customerAttributeService, - addressAttributeParser, addressAttributeService, messageProviderService, groupService, + Grand.Web.AdminShared.Interfaces.IAdminDataScope scope) + : BaseCustomerController(customerService, customerViewModelService, customerManagerService, + customerAttributeParser, customerAttributeService, messageProviderService, groupService, translationService, contextAccessor, customerSettings, scope) { public Task<(Customer, IActionResult)> LoadAuthorizedCustomerPublic(string id) => LoadAuthorizedCustomer(id); } - protected TestCustomerController Controller; - protected Mock CustomerServiceMock; - protected Mock CustomerViewModelServiceMock; - protected Mock CustomerAddressViewModelServiceMock; - protected Mock CustomerLoyaltyPointsViewModelServiceMock; - protected Mock CustomerShoppingCartViewModelServiceMock; - protected Mock CustomerProductViewModelServiceMock; - protected Mock CustomerOutOfStockSubscriptionViewModelServiceMock; - protected Mock CustomerManagerServiceMock; - protected Mock GroupServiceMock; - protected Mock> ScopeMock; - protected Mock TranslationServiceMock; - protected Mock AddressAttributeServiceMock; - protected Mock ProductReviewServiceMock; - protected Mock CustomerProductServiceMock; - - [TestInitialize] - public void Setup() - { - CustomerServiceMock = new Mock(); - CustomerViewModelServiceMock = new Mock(); - CustomerAddressViewModelServiceMock = new Mock(); - CustomerLoyaltyPointsViewModelServiceMock = new Mock(); - CustomerShoppingCartViewModelServiceMock = new Mock(); - CustomerProductViewModelServiceMock = new Mock(); - CustomerOutOfStockSubscriptionViewModelServiceMock = new Mock(); - CustomerManagerServiceMock = new Mock(); - GroupServiceMock = new Mock(); - ScopeMock = new Mock>(); - ScopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - TranslationServiceMock = new Mock(); - TranslationServiceMock.Setup(t => t.GetResource(It.IsAny())).Returns("resource"); - AddressAttributeServiceMock = new Mock(); - AddressAttributeServiceMock.Setup(a => a.GetAllAddressAttributes()) - .ReturnsAsync(new List()); - ProductReviewServiceMock = new Mock(); - CustomerProductServiceMock = new Mock(); - - var contextAccessorMock = new Mock(); - - Controller = new TestCustomerController( - CustomerServiceMock.Object, - CustomerViewModelServiceMock.Object, - CustomerAddressViewModelServiceMock.Object, - CustomerLoyaltyPointsViewModelServiceMock.Object, - CustomerShoppingCartViewModelServiceMock.Object, - CustomerProductViewModelServiceMock.Object, - CustomerOutOfStockSubscriptionViewModelServiceMock.Object, - CustomerManagerServiceMock.Object, - CustomerProductServiceMock.Object, - ProductReviewServiceMock.Object, - new Mock().Object, - new Mock().Object, - new Mock().Object, - new Mock().Object, - new Mock().Object, - AddressAttributeServiceMock.Object, - new Mock().Object, - GroupServiceMock.Object, - TranslationServiceMock.Object, - contextAccessorMock.Object, - new CustomerSettings(), - ScopeMock.Object); - - var httpContext = new DefaultHttpContext(); - var loggerFactoryMock = new Mock(); - loggerFactoryMock.Setup(l => l.CreateLogger(It.IsAny())).Returns(new Mock().Object); - var urlHelperFactoryMock = new Mock(); - urlHelperFactoryMock.Setup(f => f.GetUrlHelper(It.IsAny())).Returns(new Mock().Object); - var requestServicesMock = new Mock(); - requestServicesMock.Setup(s => s.GetService(typeof(ILoggerFactory))).Returns(loggerFactoryMock.Object); - requestServicesMock.Setup(s => s.GetService(typeof(IUrlHelperFactory))).Returns(urlHelperFactoryMock.Object); - httpContext.RequestServices = requestServicesMock.Object; - Controller.ControllerContext = new ControllerContext { HttpContext = httpContext }; - Controller.TempData = new TempDataDictionary(httpContext, new Mock().Object); - } + protected override TestCustomerController CreateController() => new( + CustomerServiceMock.Object, + CustomerViewModelServiceMock.Object, + CustomerManagerServiceMock.Object, + CustomerAttributeParserMock.Object, + CustomerAttributeServiceMock.Object, + MessageProviderServiceMock.Object, + GroupServiceMock.Object, + TranslationServiceMock.Object, + ContextAccessorMock.Object, + new CustomerSettings(), + ScopeMock.Object); + [TestMethod] public async Task List_CallsPrepareCustomerListModel_ForBothHosts() @@ -311,6 +218,7 @@ public async Task LoadAuthorizedCustomer_NotFound_ReturnsRedirectToList() var redirect = denied as RedirectToActionResult; Assert.IsNotNull(redirect); Assert.AreEqual("List", redirect.ActionName); + Assert.AreEqual("Customer", redirect.ControllerName); ScopeMock.Verify(s => s.HasAccess(It.IsAny()), Times.Never); } @@ -371,32 +279,13 @@ public async Task EditGet_Denied_RedirectsToList() public async Task Delete_SelfDelete_ErrorsAndRedirectsWithoutDeleting() { var self = new Customer { Id = "self" }; - var contextAccessorMock = new Mock(); var workContextMock = new Mock(); workContextMock.Setup(w => w.CurrentCustomer).Returns(self); - contextAccessorMock.Setup(c => c.WorkContext).Returns(workContextMock.Object); - // Rebuild controller with a context accessor whose CurrentCustomer matches the target id. - var controller = new TestCustomerController(CustomerServiceMock.Object, CustomerViewModelServiceMock.Object, - CustomerAddressViewModelServiceMock.Object, CustomerLoyaltyPointsViewModelServiceMock.Object, - CustomerShoppingCartViewModelServiceMock.Object, CustomerProductViewModelServiceMock.Object, - CustomerOutOfStockSubscriptionViewModelServiceMock.Object, - CustomerManagerServiceMock.Object, new Mock().Object, - new Mock().Object, - new Mock().Object, - new Mock().Object, - new Mock().Object, new Mock().Object, - new Mock().Object, new Mock().Object, - new Mock().Object, - GroupServiceMock.Object, TranslationServiceMock.Object, contextAccessorMock.Object, - new CustomerSettings(), ScopeMock.Object) - { - ControllerContext = Controller.ControllerContext, - TempData = Controller.TempData - }; + ContextAccessorMock.Setup(c => c.WorkContext).Returns(workContextMock.Object); CustomerServiceMock.Setup(s => s.GetCustomerById("self")).ReturnsAsync(self); ScopeMock.Setup(s => s.HasAccess(self)).ReturnsAsync(true); - var result = await controller.Delete("self"); + var result = await Controller.Delete("self"); var redirect = result as RedirectToActionResult; Assert.IsNotNull(redirect); @@ -404,296 +293,6 @@ public async Task Delete_SelfDelete_ErrorsAndRedirectsWithoutDeleting() CustomerViewModelServiceMock.Verify(v => v.DeleteCustomer(It.IsAny()), Times.Never); } - [TestMethod] - public async Task LoyaltyPointsHistoryAdd_StoreScope_ForcesStaffStoreId_IgnoringSubmittedStoreId() - { - var customer = new Customer { Id = "c1" }; - CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync(customer); - ScopeMock.Setup(s => s.HasAccess(customer)).ReturnsAsync(true); - ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - CustomerLoyaltyPointsViewModelServiceMock - .Setup(v => v.InsertLoyaltyPointsHistory(customer, "store-1", 10, "msg")) - .ReturnsAsync(new Grand.Domain.Orders.LoyaltyPointsHistory()); - - // Caller submits a different, attacker-controlled storeId — must be ignored under Store scope. - await Controller.LoyaltyPointsHistoryAdd("c1", "attacker-store", 10, "msg"); - - CustomerLoyaltyPointsViewModelServiceMock.Verify(v => v.InsertLoyaltyPointsHistory(customer, "store-1", 10, "msg"), Times.Once); - } - - [TestMethod] - public async Task LoyaltyPointsHistoryAdd_GlobalScope_UsesSubmittedStoreId() - { - var customer = new Customer { Id = "c1" }; - CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync(customer); - ScopeMock.Setup(s => s.HasAccess(customer)).ReturnsAsync(true); - ScopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - CustomerLoyaltyPointsViewModelServiceMock - .Setup(v => v.InsertLoyaltyPointsHistory(customer, "admin-submitted-store", 10, "msg")) - .ReturnsAsync(new Grand.Domain.Orders.LoyaltyPointsHistory()); - - await Controller.LoyaltyPointsHistoryAdd("c1", "admin-submitted-store", 10, "msg"); - - CustomerLoyaltyPointsViewModelServiceMock.Verify(v => v.InsertLoyaltyPointsHistory(customer, "admin-submitted-store", 10, "msg"), Times.Once); - } - - [TestMethod] - public async Task AddressesSelect_Denied_ThrowsArgumentException() - { - CustomerServiceMock.Setup(s => s.GetCustomerById("missing")).ReturnsAsync((Customer)null); - - await Assert.ThrowsAsync(() => - Controller.AddressesSelect("missing", new DataSourceRequest())); - } - - [TestMethod] - public async Task AddressCreatePost_Authorized_InsertsAndRedirects() - { - var customer = new Customer { Id = "c1" }; - CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync(customer); - ScopeMock.Setup(s => s.HasAccess(customer)).ReturnsAsync(true); - var model = new CustomerAddressModel { CustomerId = "c1", Address = new AddressModel() }; - var inserted = new Grand.Domain.Common.Address { Id = "a1" }; - CustomerAddressViewModelServiceMock - .Setup(v => v.InsertAddressModel(customer, model, It.IsAny>())) - .ReturnsAsync(inserted); - - var result = await Controller.AddressCreate(model); - - var redirect = result as RedirectToActionResult; - Assert.IsNotNull(redirect); - Assert.AreEqual("AddressEdit", redirect.ActionName); - } - - [TestMethod] - public async Task OrderDetails_StoreScope_MismatchedStore_ReturnsEmptyNotException() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - var orderServiceMock = new Mock(); - orderServiceMock.Setup(s => s.GetOrderById("o1")) - .ReturnsAsync(new Grand.Domain.Orders.Order { Id = "o1", StoreId = "store-2" }); - - var result = await Controller.OrderDetails("o1", orderServiceMock.Object, - new Mock().Object, - new Mock().Object); - - var json = result as JsonResult; - var data = json.Value as DataSourceResult; - Assert.AreEqual(0, data.Total); - } - - [TestMethod] - public async Task OrderDetails_GlobalScope_NotFound_ThrowsArgumentException() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - var permissionServiceMock = new Mock(); - permissionServiceMock.Setup(p => p.Authorize(Grand.Domain.Permissions.StandardPermission.ManageOrders)).ReturnsAsync(true); - var orderServiceMock = new Mock(); - orderServiceMock.Setup(s => s.GetOrderById("missing")).ReturnsAsync((Grand.Domain.Orders.Order)null); - - await Assert.ThrowsAsync(() => - Controller.OrderDetails("missing", orderServiceMock.Object, - new Mock().Object, permissionServiceMock.Object)); - } - - [TestMethod] - public async Task GetCartList_GlobalScope_NoOwnershipCheck_CallsServiceDirectly() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - CustomerShoppingCartViewModelServiceMock.Setup(v => v.PrepareShoppingCartItemModel("any-customer-id", 1)) - .ReturnsAsync(new List()); - - var result = await Controller.GetCartList("any-customer-id", 1); - - Assert.IsInstanceOfType(result, typeof(JsonResult)); - CustomerServiceMock.Verify(s => s.GetCustomerById(It.IsAny()), Times.Never); - } - - [TestMethod] - public async Task GetCartList_StoreScope_DeniedOwnership_ReturnsEmpty() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync((Customer)null); - - var result = await Controller.GetCartList("c1", 1); - - var json = result as JsonResult; - var data = json.Value as DataSourceResult; - Assert.AreEqual(0, data.Total); - CustomerShoppingCartViewModelServiceMock.Verify(v => v.PrepareShoppingCartItemModel(It.IsAny(), It.IsAny()), Times.Never); - } - - [TestMethod] - public async Task ReviewDelete_StoreScope_MismatchedStore_ThrowsArgumentException() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - var review = new Grand.Domain.Catalog.ProductReview { Id = "r1", StoreId = "store-2" }; - ProductReviewServiceMock.Setup(s => s.GetProductReviewById("r1")).ReturnsAsync(review); - - await Assert.ThrowsAsync(() => Controller.ReviewDelete("r1")); - } - - [TestMethod] - public async Task DeleteProductPrice_GlobalScope_NoOwnershipCheck_CallsServiceDirectly() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - CustomerProductViewModelServiceMock.Setup(v => v.DeleteProductPrice("pp1")).Returns(Task.CompletedTask); - - var result = await Controller.DeleteProductPrice("pp1"); - - Assert.IsInstanceOfType(result, typeof(JsonResult)); - CustomerProductViewModelServiceMock.Verify(v => v.DeleteProductPrice("pp1"), Times.Once); - } - - [TestMethod] - public async Task DeleteProductPrice_StoreScope_PriceNotFound_NoOpsSilently() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - CustomerProductServiceMock.Setup(s => s.GetCustomerProductPriceById("pp1")) - .ReturnsAsync((Grand.Domain.Customers.CustomerProductPrice)null); - - var result = await Controller.DeleteProductPrice("pp1"); - - Assert.IsInstanceOfType(result, typeof(JsonResult)); - CustomerProductViewModelServiceMock.Verify(v => v.DeleteProductPrice(It.IsAny()), Times.Never); - CustomerProductServiceMock.Verify(s => s.DeleteCustomerProductPrice(It.IsAny()), Times.Never); - } - - [TestMethod] - public async Task DeleteProductPrice_StoreScope_DeniedOwnership_NoOpsSilently() - { - // Price exists but belongs to a customer outside the caller's store — exercises the - // ownership-denial half of the guard, distinct from the not-found half above. - ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - var price = new Grand.Domain.Customers.CustomerProductPrice { Id = "pp1", CustomerId = "c1" }; - CustomerProductServiceMock.Setup(s => s.GetCustomerProductPriceById("pp1")).ReturnsAsync(price); - var foreignCustomer = new Customer { Id = "c1" }; - CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync(foreignCustomer); - ScopeMock.Setup(s => s.HasAccess(foreignCustomer)).ReturnsAsync(false); - - var result = await Controller.DeleteProductPrice("pp1"); - - Assert.IsInstanceOfType(result, typeof(JsonResult)); - CustomerProductViewModelServiceMock.Verify(v => v.DeleteProductPrice(It.IsAny()), Times.Never); - CustomerProductServiceMock.Verify(s => s.DeleteCustomerProductPrice(It.IsAny()), Times.Never); - } - - [TestMethod] - public async Task ProductAddPopup_Get_StoreScope_Denied_RedirectsToList() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync((Customer)null); - - var result = await Controller.ProductAddPopup("c1"); - - var redirect = result as RedirectToActionResult; - Assert.IsNotNull(redirect); - Assert.AreEqual("List", redirect.ActionName); - } - - [TestMethod] - public async Task ProductsPrice_StoreScope_DeniedOwnership_ReturnsEmpty() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync((Customer)null); - - var result = await Controller.ProductsPrice(new DataSourceRequest { Page = 1, PageSize = 10 }, "c1"); - - var json = result as JsonResult; - var data = json.Value as DataSourceResult; - Assert.AreEqual(0, data.Total); - CustomerProductViewModelServiceMock.Verify( - v => v.PrepareProductPriceModel(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); - } - - [TestMethod] - public async Task ProductsPrice_GlobalScope_NoOwnershipCheck_CallsServiceDirectly() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - CustomerProductViewModelServiceMock.Setup(v => v.PrepareProductPriceModel("c1", 1, 10)) - .ReturnsAsync((Enumerable.Empty(), 0)); - - var result = await Controller.ProductsPrice(new DataSourceRequest { Page = 1, PageSize = 10 }, "c1"); - - Assert.IsInstanceOfType(result, typeof(JsonResult)); - CustomerServiceMock.Verify(s => s.GetCustomerById(It.IsAny()), Times.Never); - CustomerProductViewModelServiceMock.Verify(v => v.PrepareProductPriceModel("c1", 1, 10), Times.Once); - } - - [TestMethod] - public async Task PersonalizedProducts_StoreScope_DeniedOwnership_ReturnsEmpty() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync((Customer)null); - - var result = await Controller.PersonalizedProducts(new DataSourceRequest { Page = 1, PageSize = 10 }, "c1"); - - var json = result as JsonResult; - var data = json.Value as DataSourceResult; - Assert.AreEqual(0, data.Total); - CustomerProductViewModelServiceMock.Verify( - v => v.PreparePersonalizedProducts(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); - } - - [TestMethod] - public async Task PersonalizedProducts_GlobalScope_NoOwnershipCheck_CallsServiceDirectly() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - CustomerProductViewModelServiceMock.Setup(v => v.PreparePersonalizedProducts("c1", 1, 10)) - .ReturnsAsync((Enumerable.Empty(), 0)); - - var result = await Controller.PersonalizedProducts(new DataSourceRequest { Page = 1, PageSize = 10 }, "c1"); - - Assert.IsInstanceOfType(result, typeof(JsonResult)); - CustomerServiceMock.Verify(s => s.GetCustomerById(It.IsAny()), Times.Never); - CustomerProductViewModelServiceMock.Verify(v => v.PreparePersonalizedProducts("c1", 1, 10), Times.Once); - } - - [TestMethod] - public async Task ReviewList_StoreScope_DeniedOwnership_ReturnsEmpty() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync((Customer)null); - - var result = await Controller.ReviewList("c1", new DataSourceRequest { Page = 1, PageSize = 10 }); - - var json = result as JsonResult; - var data = json.Value as DataSourceResult; - Assert.AreEqual(0, data.Total); - ProductReviewServiceMock.Verify(s => s.GetAllProductReviews( - It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); - } - - [TestMethod] - public async Task ReviewList_GlobalScope_NoOwnershipCheck_CallsServiceDirectly() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - ProductReviewServiceMock.Setup(s => s.GetAllProductReviews( - "c1", null, null, null, "", null, "", 0, 10)) - .ReturnsAsync(new Grand.Domain.PagedList( - new List(), 0, 10, 0)); - - var result = await Controller.ReviewList("c1", new DataSourceRequest { Page = 1, PageSize = 10 }); - - Assert.IsInstanceOfType(result, typeof(JsonResult)); - CustomerServiceMock.Verify(s => s.GetCustomerById(It.IsAny()), Times.Never); - } - - [TestMethod] - public async Task OutOfStockSubscriptionList_GlobalScope_NoOwnershipCheck() - { - ScopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - CustomerOutOfStockSubscriptionViewModelServiceMock - .Setup(v => v.PrepareOutOfStockSubscriptionModel("c1", 1, 10)) - .ReturnsAsync((Enumerable.Empty(), 0)); - - var result = await Controller.OutOfStockSubscriptionList( - new DataSourceRequest { Page = 1, PageSize = 10 }, "c1"); - - Assert.IsInstanceOfType(result, typeof(JsonResult)); - CustomerServiceMock.Verify(s => s.GetCustomerById(It.IsAny()), Times.Never); - } [TestMethod] public async Task ExternalAuthenticationDelete_CustomerInScope_RemovesTheLink() diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerLoyaltyPointsControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerLoyaltyPointsControllerTests.cs new file mode 100644 index 0000000000..be60c78a44 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerLoyaltyPointsControllerTests.cs @@ -0,0 +1,62 @@ +using Grand.Business.Core.Interfaces.Customers; +using Grand.Domain.Customers; +using Grand.Domain.Orders; +using Grand.Web.AdminShared.Controllers; +using Grand.Web.AdminShared.Interfaces; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class BaseCustomerLoyaltyPointsControllerTests + : CustomerControllerTestsBase +{ + public class TestCustomerLoyaltyPointsController( + ICustomerService customerService, + ICustomerLoyaltyPointsViewModelService customerLoyaltyPointsViewModelService, + IAdminDataScope scope) + : BaseCustomerLoyaltyPointsController(customerService, customerLoyaltyPointsViewModelService, scope); + + protected override TestCustomerLoyaltyPointsController CreateController() => new( + CustomerServiceMock.Object, + CustomerLoyaltyPointsViewModelServiceMock.Object, + ScopeMock.Object); + + [TestMethod] + public async Task LoyaltyPointsHistoryAdd_StoreScope_ForcesStaffStoreId_IgnoringSubmittedStoreId() + { + var customer = SetupAuthorizedCustomer("c1"); + ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); + CustomerLoyaltyPointsViewModelServiceMock + .Setup(v => v.InsertLoyaltyPointsHistory(customer, "store-1", 10, "msg")) + .ReturnsAsync(new LoyaltyPointsHistory()); + + // Caller submits a different, attacker-controlled storeId — must be ignored under Store scope. + await Controller.LoyaltyPointsHistoryAdd("c1", "attacker-store", 10, "msg"); + + CustomerLoyaltyPointsViewModelServiceMock.Verify(v => v.InsertLoyaltyPointsHistory(customer, "store-1", 10, "msg"), Times.Once); + } + + [TestMethod] + public async Task LoyaltyPointsHistoryAdd_GlobalScope_UsesSubmittedStoreId() + { + var customer = SetupAuthorizedCustomer("c1"); + CustomerLoyaltyPointsViewModelServiceMock + .Setup(v => v.InsertLoyaltyPointsHistory(customer, "admin-submitted-store", 10, "msg")) + .ReturnsAsync(new LoyaltyPointsHistory()); + + await Controller.LoyaltyPointsHistoryAdd("c1", "admin-submitted-store", 10, "msg"); + + CustomerLoyaltyPointsViewModelServiceMock.Verify(v => v.InsertLoyaltyPointsHistory(customer, "admin-submitted-store", 10, "msg"), Times.Once); + } + + [TestMethod] + public async Task LoyaltyPointsHistorySelect_Denied_ThrowsArgumentException() + { + CustomerServiceMock.Setup(s => s.GetCustomerById("missing")).ReturnsAsync((Customer)null); + + await Assert.ThrowsAsync(() => Controller.LoyaltyPointsHistorySelect("missing")); + CustomerLoyaltyPointsViewModelServiceMock.Verify(v => v.PrepareLoyaltyPointsHistoryModel(It.IsAny()), Times.Never); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerManagementControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerManagementControllerTests.cs index 71d2406d5a..43272de193 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerManagementControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerManagementControllerTests.cs @@ -1,9 +1,6 @@ -using Grand.Business.Core.Interfaces.Catalog.Products; -using Grand.Business.Core.Interfaces.Common.Addresses; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Customers; using Grand.Business.Core.Interfaces.ExportImport; -using Grand.Business.Core.Interfaces.Marketing.Customers; using Grand.Business.Core.Interfaces.Messages; using Grand.Domain.Customers; using Grand.Domain.Permissions; @@ -29,28 +26,14 @@ public class BaseCustomerManagementControllerTests { private class TestManagementController( ICustomerService customerService, ICustomerViewModelService customerViewModelService, - ICustomerAddressViewModelService customerAddressViewModelService, - ICustomerLoyaltyPointsViewModelService customerLoyaltyPointsViewModelService, - ICustomerShoppingCartViewModelService customerShoppingCartViewModelService, - ICustomerProductViewModelService customerProductViewModelService, - ICustomerOutOfStockSubscriptionViewModelService customerOutOfStockSubscriptionViewModelService, - ICustomerNoteViewModelService customerNoteViewModelService, - ICustomerContactFormViewModelService customerContactFormViewModelService, - ICustomerManagerService customerManagerService, ICustomerProductService customerProductService, - IProductReviewService productReviewService, IProductReviewViewModelService productReviewViewModelService, - IProductViewModelService productViewModelService, ICustomerAttributeParser customerAttributeParser, - ICustomerAttributeService customerAttributeService, IAddressAttributeParser addressAttributeParser, - IAddressAttributeService addressAttributeService, IMessageProviderService messageProviderService, + ICustomerManagerService customerManagerService, ICustomerAttributeParser customerAttributeParser, + ICustomerAttributeService customerAttributeService, IMessageProviderService messageProviderService, IGroupService groupService, IRequestTranslationService translationService, IContextAccessor contextAccessor, CustomerSettings customerSettings, IAdminDataScope scope, IRequestPermissionService permissionService, IExportManager exportManager) - : BaseCustomerManagementController(customerService, customerViewModelService, customerAddressViewModelService, - customerLoyaltyPointsViewModelService, customerShoppingCartViewModelService, customerProductViewModelService, - customerOutOfStockSubscriptionViewModelService, customerNoteViewModelService, - customerContactFormViewModelService, customerManagerService, customerProductService, productReviewService, productReviewViewModelService, productViewModelService, - customerAttributeParser, customerAttributeService, addressAttributeParser, addressAttributeService, - messageProviderService, groupService, translationService, contextAccessor, customerSettings, scope, - permissionService, exportManager) + : BaseCustomerManagementController(customerService, customerViewModelService, customerManagerService, + customerAttributeParser, customerAttributeService, messageProviderService, groupService, + translationService, contextAccessor, customerSettings, scope, permissionService, exportManager) { public void CheckTwoFactorEnabledWarningPublic(Customer existingCustomer, CustomerModel model) => CheckTwoFactorEnabledWarning(existingCustomer, model); @@ -79,16 +62,8 @@ public void Setup() translationServiceMock.Setup(t => t.GetResource(It.IsAny())).Returns("resource"); _controller = new TestManagementController(_customerServiceMock.Object, - new Mock().Object, new Mock().Object, - new Mock().Object, - new Mock().Object, new Mock().Object, - new Mock().Object, - new Mock().Object, new Mock().Object, - new Mock().Object, - new Mock().Object, new Mock().Object, - new Mock().Object, new Mock().Object, + new Mock().Object, new Mock().Object, new Mock().Object, new Mock().Object, - new Mock().Object, new Mock().Object, new Mock().Object, _groupServiceMock.Object, translationServiceMock.Object, _contextAccessorMock.Object, new CustomerSettings(), scopeMock.Object, _permissionServiceMock.Object, new Mock>().Object); diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerOrderControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerOrderControllerTests.cs new file mode 100644 index 0000000000..01356e49e8 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerOrderControllerTests.cs @@ -0,0 +1,69 @@ +using Grand.Business.Core.Interfaces.Checkout.Orders; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Domain.Customers; +using Grand.Domain.Orders; +using Grand.Domain.Permissions; +using Grand.Web.AdminShared.Controllers; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Orders; +using Grand.Web.Common.DataSource; +using Grand.Web.Common.Security.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class BaseCustomerOrderControllerTests + : CustomerControllerTestsBase +{ + public class TestCustomerOrderController( + ICustomerService customerService, + IOrderService orderService, + IOrderViewModelService orderViewModelService, + IRequestPermissionService permissionService, + IAdminDataScope scope) + : BaseCustomerOrderController(customerService, orderService, orderViewModelService, permissionService, scope); + + protected override TestCustomerOrderController CreateController() => new( + CustomerServiceMock.Object, + OrderServiceMock.Object, + OrderViewModelServiceMock.Object, + PermissionServiceMock.Object, + ScopeMock.Object); + + [TestMethod] + public async Task OrderDetails_StoreScope_MismatchedStore_ReturnsEmptyNotException() + { + ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); + OrderServiceMock.Setup(s => s.GetOrderById("o1")).ReturnsAsync(new Order { Id = "o1", StoreId = "store-2" }); + + var result = await Controller.OrderDetails("o1"); + + var data = (DataSourceResult)((JsonResult)result).Value!; + Assert.AreEqual(0, data.Total); + } + + [TestMethod] + public async Task OrderDetails_GlobalScope_NotFound_ThrowsArgumentException() + { + PermissionServiceMock.Setup(p => p.Authorize(StandardPermission.ManageOrders)).ReturnsAsync(true); + OrderServiceMock.Setup(s => s.GetOrderById("missing")).ReturnsAsync((Order)null); + + await Assert.ThrowsAsync(() => Controller.OrderDetails("missing")); + } + + [TestMethod] + public async Task OrderList_GlobalScope_WithoutManageOrders_ReturnsEmpty() + { + PermissionServiceMock.Setup(p => p.Authorize(StandardPermission.ManageOrders)).ReturnsAsync(false); + + var result = await Controller.OrderList("c1", new DataSourceRequest { Page = 1, PageSize = 10 }); + + var data = (DataSourceResult)((JsonResult)result).Value!; + Assert.AreEqual(0, data.Total); + OrderViewModelServiceMock.Verify( + v => v.PrepareOrderModel(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerOutOfStockSubscriptionControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerOutOfStockSubscriptionControllerTests.cs new file mode 100644 index 0000000000..5ae489e238 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerOutOfStockSubscriptionControllerTests.cs @@ -0,0 +1,55 @@ +using Grand.Business.Core.Interfaces.Customers; +using Grand.Domain.Customers; +using Grand.Web.AdminShared.Controllers; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Customers; +using Grand.Web.Common.DataSource; +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class BaseCustomerOutOfStockSubscriptionControllerTests + : CustomerControllerTestsBase +{ + public class TestCustomerOutOfStockSubscriptionController( + ICustomerService customerService, + ICustomerOutOfStockSubscriptionViewModelService customerOutOfStockSubscriptionViewModelService, + IAdminDataScope scope) + : BaseCustomerOutOfStockSubscriptionController(customerService, customerOutOfStockSubscriptionViewModelService, + scope); + + protected override TestCustomerOutOfStockSubscriptionController CreateController() => new( + CustomerServiceMock.Object, + CustomerOutOfStockSubscriptionViewModelServiceMock.Object, + ScopeMock.Object); + + [TestMethod] + public async Task OutOfStockSubscriptionList_GlobalScope_NoOwnershipCheck() + { + CustomerOutOfStockSubscriptionViewModelServiceMock + .Setup(v => v.PrepareOutOfStockSubscriptionModel("c1", 1, 10)) + .ReturnsAsync((Enumerable.Empty(), 0)); + + var result = await Controller.OutOfStockSubscriptionList(new DataSourceRequest { Page = 1, PageSize = 10 }, "c1"); + + Assert.IsInstanceOfType(result, typeof(JsonResult)); + CustomerServiceMock.Verify(s => s.GetCustomerById(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task OutOfStockSubscriptionList_StoreScope_DeniedOwnership_ReturnsEmpty() + { + ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); + CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync((Customer)null); + + var result = await Controller.OutOfStockSubscriptionList(new DataSourceRequest { Page = 1, PageSize = 10 }, "c1"); + + var data = (DataSourceResult)((JsonResult)result).Value!; + Assert.AreEqual(0, data.Total); + CustomerOutOfStockSubscriptionViewModelServiceMock.Verify( + v => v.PrepareOutOfStockSubscriptionModel(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerProductControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerProductControllerTests.cs new file mode 100644 index 0000000000..7d043be899 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerProductControllerTests.cs @@ -0,0 +1,142 @@ +using Grand.Business.Core.Interfaces.Customers; +using Grand.Business.Core.Interfaces.Marketing.Customers; +using Grand.Domain.Customers; +using Grand.Web.AdminShared.Controllers; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Customers; +using Grand.Web.Common.DataSource; +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class BaseCustomerProductControllerTests + : CustomerControllerTestsBase +{ + public class TestCustomerProductController( + ICustomerService customerService, + ICustomerProductViewModelService customerProductViewModelService, + ICustomerProductService customerProductService, + IAdminDataScope scope) + : BaseCustomerProductController(customerService, customerProductViewModelService, customerProductService, scope); + + protected override TestCustomerProductController CreateController() => new( + CustomerServiceMock.Object, + CustomerProductViewModelServiceMock.Object, + CustomerProductServiceMock.Object, + ScopeMock.Object); + + [TestMethod] + public async Task DeleteProductPrice_GlobalScope_NoOwnershipCheck_CallsServiceDirectly() + { + CustomerProductViewModelServiceMock.Setup(v => v.DeleteProductPrice("pp1")).Returns(Task.CompletedTask); + + var result = await Controller.DeleteProductPrice("pp1"); + + Assert.IsInstanceOfType(result, typeof(JsonResult)); + CustomerProductViewModelServiceMock.Verify(v => v.DeleteProductPrice("pp1"), Times.Once); + } + + [TestMethod] + public async Task DeleteProductPrice_StoreScope_PriceNotFound_NoOpsSilently() + { + ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); + CustomerProductServiceMock.Setup(s => s.GetCustomerProductPriceById("pp1")) + .ReturnsAsync((CustomerProductPrice)null); + + var result = await Controller.DeleteProductPrice("pp1"); + + Assert.IsInstanceOfType(result, typeof(JsonResult)); + CustomerProductViewModelServiceMock.Verify(v => v.DeleteProductPrice(It.IsAny()), Times.Never); + CustomerProductServiceMock.Verify(s => s.DeleteCustomerProductPrice(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task DeleteProductPrice_StoreScope_DeniedOwnership_NoOpsSilently() + { + // Price exists but belongs to a customer outside the caller's store — exercises the + // ownership-denial half of the guard, distinct from the not-found half above. + ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); + var price = new CustomerProductPrice { Id = "pp1", CustomerId = "c1" }; + CustomerProductServiceMock.Setup(s => s.GetCustomerProductPriceById("pp1")).ReturnsAsync(price); + var foreignCustomer = new Customer { Id = "c1" }; + CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync(foreignCustomer); + ScopeMock.Setup(s => s.HasAccess(foreignCustomer)).ReturnsAsync(false); + + var result = await Controller.DeleteProductPrice("pp1"); + + Assert.IsInstanceOfType(result, typeof(JsonResult)); + CustomerProductViewModelServiceMock.Verify(v => v.DeleteProductPrice(It.IsAny()), Times.Never); + CustomerProductServiceMock.Verify(s => s.DeleteCustomerProductPrice(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task ProductAddPopup_Get_StoreScope_Denied_RedirectsToCustomerList() + { + ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); + CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync((Customer)null); + + var result = await Controller.ProductAddPopup("c1"); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("List", redirect.ActionName); + Assert.AreEqual("Customer", redirect.ControllerName); + } + + [TestMethod] + public async Task ProductsPrice_StoreScope_DeniedOwnership_ReturnsEmpty() + { + ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); + CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync((Customer)null); + + var result = await Controller.ProductsPrice(new DataSourceRequest { Page = 1, PageSize = 10 }, "c1"); + + var data = (DataSourceResult)((JsonResult)result).Value!; + Assert.AreEqual(0, data.Total); + CustomerProductViewModelServiceMock.Verify( + v => v.PrepareProductPriceModel(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task ProductsPrice_GlobalScope_NoOwnershipCheck_CallsServiceDirectly() + { + CustomerProductViewModelServiceMock.Setup(v => v.PrepareProductPriceModel("c1", 1, 10)) + .ReturnsAsync((Enumerable.Empty(), 0)); + + var result = await Controller.ProductsPrice(new DataSourceRequest { Page = 1, PageSize = 10 }, "c1"); + + Assert.IsInstanceOfType(result, typeof(JsonResult)); + CustomerServiceMock.Verify(s => s.GetCustomerById(It.IsAny()), Times.Never); + CustomerProductViewModelServiceMock.Verify(v => v.PrepareProductPriceModel("c1", 1, 10), Times.Once); + } + + [TestMethod] + public async Task PersonalizedProducts_StoreScope_DeniedOwnership_ReturnsEmpty() + { + ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); + CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync((Customer)null); + + var result = await Controller.PersonalizedProducts(new DataSourceRequest { Page = 1, PageSize = 10 }, "c1"); + + var data = (DataSourceResult)((JsonResult)result).Value!; + Assert.AreEqual(0, data.Total); + CustomerProductViewModelServiceMock.Verify( + v => v.PreparePersonalizedProducts(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task PersonalizedProducts_GlobalScope_NoOwnershipCheck_CallsServiceDirectly() + { + CustomerProductViewModelServiceMock.Setup(v => v.PreparePersonalizedProducts("c1", 1, 10)) + .ReturnsAsync((Enumerable.Empty(), 0)); + + var result = await Controller.PersonalizedProducts(new DataSourceRequest { Page = 1, PageSize = 10 }, "c1"); + + Assert.IsInstanceOfType(result, typeof(JsonResult)); + CustomerServiceMock.Verify(s => s.GetCustomerById(It.IsAny()), Times.Never); + CustomerProductViewModelServiceMock.Verify(v => v.PreparePersonalizedProducts("c1", 1, 10), Times.Once); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerReviewControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerReviewControllerTests.cs new file mode 100644 index 0000000000..a2256a1d44 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerReviewControllerTests.cs @@ -0,0 +1,72 @@ +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Domain; +using Grand.Domain.Catalog; +using Grand.Domain.Customers; +using Grand.Web.AdminShared.Controllers; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.Common.DataSource; +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class BaseCustomerReviewControllerTests + : CustomerControllerTestsBase +{ + public class TestCustomerReviewController( + ICustomerService customerService, + IProductReviewService productReviewService, + IProductReviewViewModelService productReviewViewModelService, + IProductViewModelService productViewModelService, + IAdminDataScope scope) + : BaseCustomerReviewController(customerService, productReviewService, productReviewViewModelService, + productViewModelService, scope); + + protected override TestCustomerReviewController CreateController() => new( + CustomerServiceMock.Object, + ProductReviewServiceMock.Object, + ProductReviewViewModelServiceMock.Object, + ProductViewModelServiceMock.Object, + ScopeMock.Object); + + [TestMethod] + public async Task ReviewDelete_StoreScope_MismatchedStore_ThrowsArgumentException() + { + ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); + ProductReviewServiceMock.Setup(s => s.GetProductReviewById("r1")) + .ReturnsAsync(new ProductReview { Id = "r1", StoreId = "store-2" }); + + await Assert.ThrowsAsync(() => Controller.ReviewDelete("r1")); + ProductReviewViewModelServiceMock.Verify(v => v.DeleteProductReview(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task ReviewList_StoreScope_DeniedOwnership_ReturnsEmpty() + { + ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); + CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync((Customer)null); + + var result = await Controller.ReviewList("c1", new DataSourceRequest { Page = 1, PageSize = 10 }); + + var data = (DataSourceResult)((JsonResult)result).Value!; + Assert.AreEqual(0, data.Total); + ProductReviewServiceMock.Verify(s => s.GetAllProductReviews( + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task ReviewList_GlobalScope_NoOwnershipCheck_CallsServiceDirectly() + { + ProductReviewServiceMock.Setup(s => s.GetAllProductReviews("c1", null, null, null, "", null, "", 0, 10)) + .ReturnsAsync(new PagedList(new List(), 0, 10, 0)); + + var result = await Controller.ReviewList("c1", new DataSourceRequest { Page = 1, PageSize = 10 }); + + Assert.IsInstanceOfType(result, typeof(JsonResult)); + CustomerServiceMock.Verify(s => s.GetCustomerById(It.IsAny()), Times.Never); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerShoppingCartControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerShoppingCartControllerTests.cs new file mode 100644 index 0000000000..5adfbd547c --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerShoppingCartControllerTests.cs @@ -0,0 +1,79 @@ +using Grand.Business.Core.Interfaces.Customers; +using Grand.Domain.Customers; +using Grand.Web.AdminShared.Controllers; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.ShoppingCart; +using Grand.Web.Common.DataSource; +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class BaseCustomerShoppingCartControllerTests + : CustomerControllerTestsBase +{ + public class TestCustomerShoppingCartController( + ICustomerService customerService, + ICustomerShoppingCartViewModelService customerShoppingCartViewModelService, + IAdminDataScope scope) + : BaseCustomerShoppingCartController(customerService, customerShoppingCartViewModelService, scope); + + protected override TestCustomerShoppingCartController CreateController() => new( + CustomerServiceMock.Object, + CustomerShoppingCartViewModelServiceMock.Object, + ScopeMock.Object); + + [TestMethod] + public async Task GetCartList_Authorized_PassesTheLoadedCustomerToTheService() + { + var customer = SetupAuthorizedCustomer("c1"); + CustomerShoppingCartViewModelServiceMock.Setup(v => v.PrepareShoppingCartItemModel(customer, 1)) + .ReturnsAsync(new List { new() }); + + var result = await Controller.GetCartList("c1", 1); + + var data = (DataSourceResult)((JsonResult)result).Value!; + Assert.AreEqual(1, data.Total); + CustomerServiceMock.Verify(s => s.GetCustomerById("c1"), Times.Once); + } + + [TestMethod] + public async Task GetCartList_GlobalScope_UnknownCustomer_ReturnsEmptyGrid() + { + CustomerServiceMock.Setup(s => s.GetCustomerById("missing")).ReturnsAsync((Customer)null); + + var result = await Controller.GetCartList("missing", 1); + + var data = (DataSourceResult)((JsonResult)result).Value!; + Assert.AreEqual(0, data.Total); + CustomerShoppingCartViewModelServiceMock.Verify( + v => v.PrepareShoppingCartItemModel(It.IsAny(), It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task GetCartList_StoreScope_DeniedOwnership_ReturnsEmptyGrid() + { + ScopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); + var customer = new Customer { Id = "c1" }; + CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync(customer); + ScopeMock.Setup(s => s.HasAccess(customer)).ReturnsAsync(false); + + var result = await Controller.GetCartList("c1", 1); + + var data = (DataSourceResult)((JsonResult)result).Value!; + Assert.AreEqual(0, data.Total); + CustomerShoppingCartViewModelServiceMock.Verify( + v => v.PrepareShoppingCartItemModel(It.IsAny(), It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task DeleteCart_Denied_ThrowsArgumentException_NothingDeleted() + { + CustomerServiceMock.Setup(s => s.GetCustomerById("missing")).ReturnsAsync((Customer)null); + + await Assert.ThrowsAsync(() => Controller.DeleteCart("sci1", "missing")); + CustomerShoppingCartViewModelServiceMock.Verify(v => v.DeleteCart(It.IsAny(), It.IsAny()), Times.Never); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/CustomerControllerAttributeTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/CustomerControllerAttributeTests.cs index 0013b88932..b5f4c1113b 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/CustomerControllerAttributeTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/CustomerControllerAttributeTests.cs @@ -93,4 +93,29 @@ public void CreatePost_RequiresCreatePermission() Assert.AreEqual(PermissionActionName.Create, attr.PermissionAction); } + + [TestMethod] + public void CustomerTabControllers_AllCarryAdminAreaAndAuthorization() + { + Type[] tabControllers = [ + typeof(CustomerAddressController), + typeof(CustomerLoyaltyPointsController), + typeof(CustomerShoppingCartController), + typeof(CustomerProductController), + typeof(CustomerOutOfStockSubscriptionController), + typeof(CustomerOrderController), + typeof(CustomerReviewController), + typeof(CustomerNoteController), + typeof(CustomerContactFormController) + ]; + foreach (var type in tabControllers) + { + var area = type.GetCustomAttributes(typeof(AreaAttribute), inherit: false).Cast().Single(); + Assert.AreEqual("Admin", area.RouteValue, type.Name); + Assert.AreEqual(1, type.GetCustomAttributes(typeof(AuthorizeAdminAttribute), inherit: false).Length, type.Name); + Assert.AreEqual(1, type.GetCustomAttributes(typeof(AuthorizeMenuAttribute), inherit: false).Length, type.Name); + // [PermissionAuthorize(Customers)] and antiforgery arrive from BaseCustomerScopedController. + Assert.IsTrue(typeof(BaseCustomerScopedController).IsAssignableFrom(type), type.Name); + } + } } diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/CustomerControllerTestsBase.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/CustomerControllerTestsBase.cs new file mode 100644 index 0000000000..ea932900cf --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/CustomerControllerTestsBase.cs @@ -0,0 +1,114 @@ +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Interfaces.Checkout.Orders; +using Grand.Business.Core.Interfaces.Common.Addresses; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Business.Core.Interfaces.Marketing.Customers; +using Grand.Business.Core.Interfaces.Messages; +using Grand.Domain.Customers; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.Common.Localization; +using Grand.Web.Common.Security.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Controllers; + +/// +/// Fixture shared by the tests of BaseCustomerController and the per-tab customer controllers split out +/// of it. Each derived class only says how to build its controller from the mocks below. +/// +public abstract class CustomerControllerTestsBase where TController : Controller +{ + protected TController Controller; + protected Mock CustomerServiceMock; + protected Mock CustomerViewModelServiceMock; + protected Mock CustomerAddressViewModelServiceMock; + protected Mock CustomerLoyaltyPointsViewModelServiceMock; + protected Mock CustomerShoppingCartViewModelServiceMock; + protected Mock CustomerProductViewModelServiceMock; + protected Mock CustomerOutOfStockSubscriptionViewModelServiceMock; + protected Mock CustomerNoteViewModelServiceMock; + protected Mock CustomerManagerServiceMock; + protected Mock CustomerProductServiceMock; + protected Mock ProductReviewServiceMock; + protected Mock ProductReviewViewModelServiceMock; + protected Mock ProductViewModelServiceMock; + protected Mock OrderServiceMock; + protected Mock OrderViewModelServiceMock; + protected Mock PermissionServiceMock; + protected Mock CustomerAttributeParserMock; + protected Mock CustomerAttributeServiceMock; + protected Mock AddressAttributeParserMock; + protected Mock AddressAttributeServiceMock; + protected Mock MessageProviderServiceMock; + protected Mock GroupServiceMock; + protected Mock TranslationServiceMock; + protected Mock ContextAccessorMock; + protected Mock> ScopeMock; + + protected abstract TController CreateController(); + + [TestInitialize] + public void Setup() + { + CustomerServiceMock = new Mock(); + CustomerViewModelServiceMock = new Mock(); + CustomerAddressViewModelServiceMock = new Mock(); + CustomerLoyaltyPointsViewModelServiceMock = new Mock(); + CustomerShoppingCartViewModelServiceMock = new Mock(); + CustomerProductViewModelServiceMock = new Mock(); + CustomerOutOfStockSubscriptionViewModelServiceMock = new Mock(); + CustomerNoteViewModelServiceMock = new Mock(); + CustomerManagerServiceMock = new Mock(); + CustomerProductServiceMock = new Mock(); + ProductReviewServiceMock = new Mock(); + ProductReviewViewModelServiceMock = new Mock(); + ProductViewModelServiceMock = new Mock(); + OrderServiceMock = new Mock(); + OrderViewModelServiceMock = new Mock(); + PermissionServiceMock = new Mock(); + CustomerAttributeParserMock = new Mock(); + CustomerAttributeServiceMock = new Mock(); + AddressAttributeParserMock = new Mock(); + AddressAttributeServiceMock = new Mock(); + AddressAttributeServiceMock.Setup(a => a.GetAllAddressAttributes()) + .ReturnsAsync(new List()); + MessageProviderServiceMock = new Mock(); + GroupServiceMock = new Mock(); + TranslationServiceMock = new Mock(); + TranslationServiceMock.Setup(t => t.GetResource(It.IsAny())).Returns("resource"); + ContextAccessorMock = new Mock(); + ScopeMock = new Mock>(); + ScopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); + + Controller = CreateController(); + + var httpContext = new DefaultHttpContext(); + var loggerFactoryMock = new Mock(); + loggerFactoryMock.Setup(l => l.CreateLogger(It.IsAny())).Returns(new Mock().Object); + var urlHelperFactoryMock = new Mock(); + urlHelperFactoryMock.Setup(f => f.GetUrlHelper(It.IsAny())).Returns(new Mock().Object); + var requestServicesMock = new Mock(); + requestServicesMock.Setup(s => s.GetService(typeof(ILoggerFactory))).Returns(loggerFactoryMock.Object); + requestServicesMock.Setup(s => s.GetService(typeof(IUrlHelperFactory))).Returns(urlHelperFactoryMock.Object); + httpContext.RequestServices = requestServicesMock.Object; + Controller.ControllerContext = new ControllerContext { HttpContext = httpContext }; + Controller.TempData = new TempDataDictionary(httpContext, new Mock().Object); + } + + /// Makes customerId resolve to a customer the scope grants access to. + protected Customer SetupAuthorizedCustomer(string customerId) + { + var customer = new Customer { Id = customerId }; + CustomerServiceMock.Setup(s => s.GetCustomerById(customerId)).ReturnsAsync(customer); + ScopeMock.Setup(s => s.HasAccess(customer)).ReturnsAsync(true); + return customer; + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/CustomerNoteControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/CustomerNoteControllerTests.cs new file mode 100644 index 0000000000..6655a2435b --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/CustomerNoteControllerTests.cs @@ -0,0 +1,51 @@ +using Grand.Domain.Customers; +using Grand.Web.Admin.Controllers; +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class CustomerNoteControllerTests : CustomerControllerTestsBase +{ + protected override CustomerNoteController CreateController() => new( + CustomerServiceMock.Object, + CustomerNoteViewModelServiceMock.Object, + ScopeMock.Object); + + [TestMethod] + public async Task CustomerNoteDelete_CustomerOutOfScope_Throws_NothingDeleted() + { + var customer = new Customer { Id = "c1" }; + CustomerServiceMock.Setup(s => s.GetCustomerById("c1")).ReturnsAsync(customer); + ScopeMock.Setup(s => s.HasAccess(customer)).ReturnsAsync(false); + + await Assert.ThrowsAsync(() => Controller.CustomerNoteDelete("n1", "c1")); + CustomerNoteViewModelServiceMock.Verify(v => v.DeleteCustomerNote(It.IsAny(), It.IsAny()), + Times.Never); + } + + [TestMethod] + public async Task CustomerNoteDelete_CustomerInScope_DeletesWithinThatCustomer() + { + SetupAuthorizedCustomer("c1"); + + var result = await Controller.CustomerNoteDelete("n1", "c1"); + + Assert.IsInstanceOfType(result, typeof(JsonResult)); + CustomerNoteViewModelServiceMock.Verify(v => v.DeleteCustomerNote("n1", "c1"), Times.Once); + } + + [TestMethod] + public async Task CustomerNoteAdd_CustomerOutOfScope_ReturnsFalse_NothingInserted() + { + CustomerServiceMock.Setup(s => s.GetCustomerById("missing")).ReturnsAsync((Customer)null); + + var result = await Controller.CustomerNoteAdd("missing", "", false, "title", "message"); + + Assert.IsInstanceOfType(result, typeof(JsonResult)); + CustomerNoteViewModelServiceMock.Verify(v => v.InsertCustomerNote(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/CustomerContactFormViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerContactFormViewModelServiceTests.cs new file mode 100644 index 0000000000..7c5faa0a1f --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerContactFormViewModelServiceTests.cs @@ -0,0 +1,72 @@ +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Business.Core.Interfaces.Marketing.Contacts; +using Grand.Domain; +using Grand.Domain.Messages; +using Grand.Domain.Stores; +using Grand.Infrastructure.Mapper; +using Grand.Mapping; +using Grand.Web.AdminShared.Mapper; +using Grand.Web.AdminShared.Services; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Services; + +[TestClass] +public class CustomerContactFormViewModelServiceTests +{ + private Mock _contactUsServiceMock; + private Mock _storeServiceMock; + private CustomerContactFormViewModelService _service; + + [TestInitialize] + public void Setup() + { + AutoMapperConfig.Init(new MapperConfiguration(cfg => cfg.AddProfile())); + + _contactUsServiceMock = new Mock(); + _storeServiceMock = new Mock(); + var dateTimeServiceMock = new Mock(); + dateTimeServiceMock.Setup(d => d.ConvertToUserTime(It.IsAny(), DateTimeKind.Utc)) + .Returns((d, _) => d); + + _service = new CustomerContactFormViewModelService(_contactUsServiceMock.Object, _storeServiceMock.Object, + dateTimeServiceMock.Object); + } + + [TestMethod] + public async Task PrepareContactFormModel_QueriesTheCustomersFormsWithZeroBasedPage() + { + _contactUsServiceMock.Setup(c => c.GetAllContactUs(null, null, "", "", "c1", "", 1, 10)) + .ReturnsAsync(new PagedList(new List(), 1, 10, 0)); + + var (models, totalCount) = await _service.PrepareContactFormModel("c1", 2, 10); + + Assert.AreEqual(0, models.Count()); + Assert.AreEqual(0, totalCount); + _contactUsServiceMock.Verify(c => c.GetAllContactUs(null, null, "", "", "c1", "", 1, 10), Times.Once); + } + + [TestMethod] + public async Task PrepareContactFormModel_HidesTheEnquiry_AndNamesTheStore() + { + var known = new ContactUs { + Id = "f1", StoreId = "s1", FullName = "Jan", Email = "jan@example.com", Enquiry = "secret" + }; + var orphan = new ContactUs { Id = "f2", StoreId = "gone", FullName = "Ewa", Email = "ewa@example.com" }; + _contactUsServiceMock.Setup(c => c.GetAllContactUs(null, null, "", "", "c1", "", 0, 10)) + .ReturnsAsync(new PagedList(new List { known, orphan }, 0, 10, 2)); + _storeServiceMock.Setup(s => s.GetStoreById("s1")).ReturnsAsync(new Store { Id = "s1", Shortcut = "Main" }); + _storeServiceMock.Setup(s => s.GetStoreById("gone")).ReturnsAsync((Store)null); + + var (models, totalCount) = await _service.PrepareContactFormModel("c1", 1, 10); + + var list = models.ToList(); + Assert.AreEqual(2, totalCount); + Assert.AreEqual("", list[0].Enquiry); + Assert.AreEqual("Jan - jan@example.com", list[0].Email); + Assert.AreEqual("Main", list[0].Store); + Assert.AreEqual("-empty-", list[1].Store); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/CustomerNoteViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerNoteViewModelServiceTests.cs index 22666cc311..f38f7afcd9 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Services/CustomerNoteViewModelServiceTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerNoteViewModelServiceTests.cs @@ -54,7 +54,7 @@ public async Task InsertCustomerNote_InsertsNoteWithMappedFields() [TestMethod] public async Task DeleteCustomerNote_DeletesNoteAndAttachment() { - var note = new CustomerNote { Id = "n1", DownloadId = "d1" }; + var note = new CustomerNote { Id = "n1", CustomerId = "c1", DownloadId = "d1" }; var download = new Download { Id = "d1" }; _customerNoteServiceMock.Setup(c => c.GetCustomerNote("n1")).ReturnsAsync(note); _customerNoteServiceMock.Setup(c => c.DeleteCustomerNote(It.IsAny())).Returns(Task.CompletedTask); @@ -76,4 +76,16 @@ public async Task DeleteCustomerNote_NotFound_Throws() await Assert.ThrowsAsync( () => _customerNoteViewModelService.DeleteCustomerNote("missing", "c1")); } + + [TestMethod] + public async Task DeleteCustomerNote_NoteOfAnotherCustomer_Throws_NothingDeleted() + { + var note = new CustomerNote { Id = "n1", CustomerId = "c2", DownloadId = "d1" }; + _customerNoteServiceMock.Setup(c => c.GetCustomerNote("n1")).ReturnsAsync(note); + + await Assert.ThrowsAsync( + () => _customerNoteViewModelService.DeleteCustomerNote("n1", "c1")); + _customerNoteServiceMock.Verify(c => c.DeleteCustomerNote(It.IsAny()), Times.Never); + _downloadServiceMock.Verify(d => d.DeleteDownload(It.IsAny()), Times.Never); + } } diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/CustomerOutOfStockSubscriptionViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerOutOfStockSubscriptionViewModelServiceTests.cs new file mode 100644 index 0000000000..dbd8807ff9 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerOutOfStockSubscriptionViewModelServiceTests.cs @@ -0,0 +1,75 @@ +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Domain; +using Grand.Domain.Catalog; +using Grand.Domain.Stores; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Services; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Services; + +[TestClass] +public class CustomerOutOfStockSubscriptionViewModelServiceTests +{ + private Mock _outOfStockSubscriptionServiceMock; + private Mock _storeServiceMock; + private Mock _productServiceMock; + private Mock _productAttributeFormatterMock; + private CustomerOutOfStockSubscriptionViewModelService _service; + + [TestInitialize] + public void Setup() + { + _outOfStockSubscriptionServiceMock = new Mock(); + _storeServiceMock = new Mock(); + _productServiceMock = new Mock(); + _productAttributeFormatterMock = new Mock(); + var dateTimeServiceMock = new Mock(); + dateTimeServiceMock.Setup(d => d.ConvertToUserTime(It.IsAny(), DateTimeKind.Utc)) + .Returns((d, _) => d); + + _service = new CustomerOutOfStockSubscriptionViewModelService(_outOfStockSubscriptionServiceMock.Object, + _storeServiceMock.Object, _productServiceMock.Object, _productAttributeFormatterMock.Object, + dateTimeServiceMock.Object, new Mock().Object); + } + + [TestMethod] + public async Task PrepareOutOfStockSubscriptionModel_QueriesWithZeroBasedPage() + { + _outOfStockSubscriptionServiceMock.Setup(s => s.GetAllSubscriptionsByCustomerId("c1", "", 1, 10)) + .ReturnsAsync(new PagedList(new List(), 1, 10, 0)); + + var (models, totalCount) = await _service.PrepareOutOfStockSubscriptionModel("c1", 2, 10); + + Assert.AreEqual(0, models.Count()); + Assert.AreEqual(0, totalCount); + _outOfStockSubscriptionServiceMock.Verify(s => s.GetAllSubscriptionsByCustomerId("c1", "", 1, 10), Times.Once); + } + + [TestMethod] + public async Task PrepareOutOfStockSubscriptionModel_MapsStoreAndProduct_UnknownWhenMissing() + { + var subscriptions = new List { + new() { Id = "o1", StoreId = "s1", ProductId = "p1" }, + new() { Id = "o2", StoreId = "gone", ProductId = "gone" } + }; + _outOfStockSubscriptionServiceMock.Setup(s => s.GetAllSubscriptionsByCustomerId("c1", "", 0, 10)) + .ReturnsAsync(new PagedList(subscriptions, 0, 10, 2)); + _storeServiceMock.Setup(s => s.GetStoreById("s1")).ReturnsAsync(new Store { Id = "s1", Shortcut = "Main" }); + _productServiceMock.Setup(p => p.GetProductById("p1", false)).ReturnsAsync(new Product { Id = "p1", Name = "Mug" }); + + var (models, totalCount) = await _service.PrepareOutOfStockSubscriptionModel("c1", 1, 10); + + var list = models.ToList(); + Assert.AreEqual(2, totalCount); + Assert.AreEqual("Main", list[0].StoreName); + Assert.AreEqual("Mug", list[0].ProductName); + Assert.AreEqual("", list[0].AttributeDescription); + Assert.AreEqual("Unknown", list[1].StoreName); + Assert.AreEqual("Unknown", list[1].ProductName); + _productAttributeFormatterMock.VerifyNoOtherCalls(); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/CustomerShoppingCartViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerShoppingCartViewModelServiceTests.cs new file mode 100644 index 0000000000..aeefb15c0f --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerShoppingCartViewModelServiceTests.cs @@ -0,0 +1,114 @@ +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.Orders; +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.Domain.Customers; +using Grand.Domain.Orders; +using Grand.Domain.Stores; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Services; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Services; + +[TestClass] +public class CustomerShoppingCartViewModelServiceTests +{ + private Mock _customerServiceMock; + private Mock _storeServiceMock; + private Mock _productServiceMock; + private Mock _shoppingCartServiceMock; + private CustomerShoppingCartViewModelService _service; + + [TestInitialize] + public void Setup() + { + _customerServiceMock = new Mock(); + _storeServiceMock = new Mock(); + _productServiceMock = new Mock(); + _shoppingCartServiceMock = new Mock(); + + _service = new CustomerShoppingCartViewModelService( + _customerServiceMock.Object, + _storeServiceMock.Object, + _productServiceMock.Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + _shoppingCartServiceMock.Object); + } + + [TestMethod] + public async Task PrepareShoppingCartItemModel_NoItemsOfTheRequestedType_ReturnsEmpty_WithoutLookups() + { + var customer = new Customer { Id = "c1" }; + customer.ShoppingCartItems.Add(new ShoppingCartItem { + Id = "w1", ShoppingCartTypeId = ShoppingCartType.Wishlist, ProductId = "p1" + }); + + var items = await _service.PrepareShoppingCartItemModel(customer, (int)ShoppingCartType.ShoppingCart); + + Assert.AreEqual(0, items.Count); + _productServiceMock.Verify(p => p.GetProductById(It.IsAny(), It.IsAny()), Times.Never); + _customerServiceMock.Verify(c => c.GetCustomerById(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task PrepareShoppingCartItemModel_SkipsItemsWhoseProductNoLongerExists() + { + var customer = new Customer { Id = "c1" }; + customer.ShoppingCartItems.Add(new ShoppingCartItem { + Id = "sci1", ShoppingCartTypeId = ShoppingCartType.ShoppingCart, ProductId = "gone", StoreId = "s1" + }); + _storeServiceMock.Setup(s => s.GetStoreById("s1")).ReturnsAsync(new Store { Id = "s1" }); + _productServiceMock.Setup(p => p.GetProductById("gone", false)).ReturnsAsync((Product)null); + + var items = await _service.PrepareShoppingCartItemModel(customer, (int)ShoppingCartType.ShoppingCart); + + Assert.AreEqual(0, items.Count); + _productServiceMock.Verify(p => p.GetProductById("gone", false), Times.Once); + } + + [TestMethod] + public async Task DeleteCart_KnownItem_DeletesItAndSavesTheCustomer() + { + var customer = new Customer { Id = "c1" }; + var item = new ShoppingCartItem { Id = "sci1", ShoppingCartTypeId = ShoppingCartType.ShoppingCart }; + customer.ShoppingCartItems.Add(item); + + await _service.DeleteCart(customer, "sci1"); + + _shoppingCartServiceMock.Verify(s => s.DeleteShoppingCartItem(customer, item, true, true), Times.Once); + _customerServiceMock.Verify(c => c.UpdateCustomerInAdminPanel(customer), Times.Once); + } + + [TestMethod] + public async Task DeleteCart_UnknownItem_DoesNothing() + { + var customer = new Customer { Id = "c1" }; + + await _service.DeleteCart(customer, "missing"); + + _shoppingCartServiceMock.VerifyNoOtherCalls(); + _customerServiceMock.VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task UpdateCart_UnknownItem_ReturnsNoWarnings_WithoutUpdating() + { + var customer = new Customer { Id = "c1" }; + + var warnings = await _service.UpdateCart(customer, "missing", 9.99); + + Assert.AreEqual(0, warnings.Count); + _shoppingCartServiceMock.VerifyNoOtherCalls(); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/CustomerViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerViewModelServiceTests.cs index 047d38c48e..17ce621e1e 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Services/CustomerViewModelServiceTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Services/CustomerViewModelServiceTests.cs @@ -1,4 +1,4 @@ -using Grand.Business.Core.Interfaces.Authentication; +using Grand.Business.Core.Interfaces.Authentication; using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Business.Core.Interfaces.Common.Addresses; diff --git a/src/Tests/Grand.Web.Store.Tests/Controllers/CustomerControllerAttributeTests.cs b/src/Tests/Grand.Web.Store.Tests/Controllers/CustomerControllerAttributeTests.cs index 94099932ad..de0b08ff46 100644 --- a/src/Tests/Grand.Web.Store.Tests/Controllers/CustomerControllerAttributeTests.cs +++ b/src/Tests/Grand.Web.Store.Tests/Controllers/CustomerControllerAttributeTests.cs @@ -1,6 +1,7 @@ using System.Reflection; using Grand.Web.Common.Filters; using Grand.Web.Store.Controllers; +using Grand.Web.Store.Filters; using Microsoft.AspNetCore.Mvc; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -67,16 +68,49 @@ public void CustomerController_DoesNotInheritBaseCustomerManagementController() } [TestMethod] - public void CustomerController_DeclaresOnlyItsThreeOwnMembers() + public void CustomerController_DeclaresOnlyItsStoreOnlyMembers() { - // The per-store gate override, PerStoreDisabled, and ApplyPostConstraints override are - // genuine Store-only infrastructure — everything else must live in BaseCustomerController. + // PerStoreDisabled and the ApplyPostConstraints override are genuine Store-only + // infrastructure — everything else must live in BaseCustomerController. var ownMethods = typeof(CustomerController).GetMethods( BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); var ownMethodNames = ownMethods.Select(m => m.Name) .Where(n => !n.StartsWith("get_") && !n.StartsWith("set_")).ToHashSet(); - var allowed = new HashSet { "OnActionExecutionAsync", "PerStoreDisabled", "ApplyPostConstraints", ".ctor" }; + var allowed = new HashSet { "PerStoreDisabled", "ApplyPostConstraints", ".ctor" }; Assert.IsTrue(ownMethodNames.IsSubsetOf(allowed), $"Unexpected own members on thin Store subclass: {string.Join(", ", ownMethodNames.Except(allowed))}"); } + + private static readonly Type[] CustomerControllers = [ + typeof(CustomerController), + typeof(CustomerAddressController), + typeof(CustomerLoyaltyPointsController), + typeof(CustomerShoppingCartController), + typeof(CustomerProductController), + typeof(CustomerOutOfStockSubscriptionController), + typeof(CustomerOrderController), + typeof(CustomerReviewController) + ]; + + [TestMethod] + public void CustomerControllers_AllCarryThePerStoreGate() + { + // The tab controllers serve the same customers as CustomerController, so turning + // Customer:RegisterCustomersPerStore off must close them too, not only the list and the form. + foreach (var type in CustomerControllers) + Assert.AreEqual(1, type.GetCustomAttributes(typeof(PerStoreCustomersAttribute), inherit: false).Length, + $"{type.Name} must carry [PerStoreCustomers]."); + } + + [TestMethod] + public void CustomerControllers_AllCarryStoreAreaAndAuthorization() + { + foreach (var type in CustomerControllers) + { + var area = type.GetCustomAttributes(typeof(AreaAttribute), inherit: false).Cast().Single(); + Assert.AreEqual("Store", area.RouteValue, type.Name); + Assert.AreEqual(1, type.GetCustomAttributes(typeof(AuthorizeStoreAttribute), inherit: false).Length, type.Name); + Assert.AreEqual(1, type.GetCustomAttributes(typeof(AuthorizeMenuAttribute), inherit: false).Length, type.Name); + } + } } diff --git a/src/Tests/Grand.Web.Store.Tests/Controllers/CustomerControllerTests.cs b/src/Tests/Grand.Web.Store.Tests/Controllers/CustomerControllerTests.cs index 1f6be17773..9fe50e6201 100644 --- a/src/Tests/Grand.Web.Store.Tests/Controllers/CustomerControllerTests.cs +++ b/src/Tests/Grand.Web.Store.Tests/Controllers/CustomerControllerTests.cs @@ -1,8 +1,5 @@ -using Grand.Business.Core.Interfaces.Catalog.Products; -using Grand.Business.Core.Interfaces.Common.Addresses; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Customers; -using Grand.Business.Core.Interfaces.Marketing.Customers; using Grand.Business.Core.Interfaces.Messages; using Grand.Business.Core.Utilities.Customers; using Grand.Domain.Customers; @@ -12,9 +9,9 @@ using Grand.Web.AdminShared.Models.Customers; using Grand.Web.Common.Models; using Grand.Web.Store.Controllers; +using Grand.Web.Store.Filters; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ViewFeatures; @@ -25,10 +22,9 @@ namespace Grand.Web.Store.Tests.Controllers; -// Trimmed for the Task 11 thin-subclass cutover: business logic now shared through -// BaseCustomerController is exercised by BaseCustomerControllerTests.cs (Admin.Tests, Tasks 4-8) -// and applies identically to Store. What remains here is genuinely Store-only: the per-store -// feature gate (OnActionExecutionAsync), the PerStoreDisabled page, and the +// Business logic shared through BaseCustomerController and the customer tab controllers is +// exercised in Admin.Tests and applies identically to Store. What remains here is genuinely +// Store-only: the per-store feature gate ([PerStoreCustomers]), the PerStoreDisabled page, and the // ApplyPostConstraints override's anti-smuggling contract. [TestClass] public class CustomerControllerTests @@ -70,65 +66,37 @@ public void Setup() private class TestableCustomerController( ICustomerService customerService, ICustomerViewModelService customerViewModelService, - ICustomerAddressViewModelService customerAddressViewModelService, - ICustomerLoyaltyPointsViewModelService customerLoyaltyPointsViewModelService, - ICustomerShoppingCartViewModelService customerShoppingCartViewModelService, - ICustomerProductViewModelService customerProductViewModelService, - ICustomerOutOfStockSubscriptionViewModelService customerOutOfStockSubscriptionViewModelService, ICustomerManagerService customerManagerService, - ICustomerProductService customerProductService, - IProductReviewService productReviewService, - IProductReviewViewModelService productReviewViewModelService, - IProductViewModelService productViewModelService, ICustomerAttributeParser customerAttributeParser, ICustomerAttributeService customerAttributeService, - IAddressAttributeParser addressAttributeParser, - IAddressAttributeService addressAttributeService, IMessageProviderService messageProviderService, IGroupService groupService, IRequestTranslationService translationService, IContextAccessor contextAccessor, CustomerSettings customerSettings, - IAdminDataScope scope, - CustomerConfig customerConfig) - : CustomerController(customerService, customerViewModelService, customerAddressViewModelService, - customerLoyaltyPointsViewModelService, customerShoppingCartViewModelService, - customerProductViewModelService, customerOutOfStockSubscriptionViewModelService, customerManagerService, - customerProductService, productReviewService, productReviewViewModelService, productViewModelService, - customerAttributeParser, customerAttributeService, addressAttributeParser, addressAttributeService, - messageProviderService, groupService, translationService, contextAccessor, customerSettings, scope, - customerConfig) + IAdminDataScope scope) + : CustomerController(customerService, customerViewModelService, customerManagerService, + customerAttributeParser, customerAttributeService, messageProviderService, groupService, + translationService, contextAccessor, customerSettings, scope) { public Task ApplyPostConstraintsPublic(CustomerModel model) => ApplyPostConstraints(model); } - private TestableCustomerController BuildStoreController(bool perStoreEnabled = true, - IGroupService groupService = null, IAdminDataScope scope = null) + private TestableCustomerController BuildStoreController(IGroupService groupService = null, + IAdminDataScope scope = null) { var controller = new TestableCustomerController( _customerServiceMock.Object, _customerViewModelServiceMock.Object, - new Mock().Object, - new Mock().Object, - new Mock().Object, - new Mock().Object, - new Mock().Object, _customerManagerServiceMock.Object, - new Mock().Object, - new Mock().Object, - new Mock().Object, - new Mock().Object, new Mock().Object, _customerAttributeServiceMock.Object, - new Mock().Object, - new Mock().Object, new Mock().Object, groupService ?? _groupServiceMock.Object, _translationServiceMock.Object, _contextAccessorMock.Object, new CustomerSettings(), - scope ?? _scopeMock.Object, - new CustomerConfig { RegisterCustomersPerStore = perStoreEnabled }); + scope ?? _scopeMock.Object); var httpContext = new DefaultHttpContext(); controller.ControllerContext = new ControllerContext { HttpContext = httpContext }; @@ -136,45 +104,54 @@ private TestableCustomerController BuildStoreController(bool perStoreEnabled = t return controller; } - private static ActionExecutingContext BuildGate(CustomerController controller, string actionName) + private static async Task<(ActionExecutingContext context, bool nextCalled)> RunGate(bool perStoreEnabled, + string controllerName, string actionName) { - var actionContext = new ActionContext(controller.ControllerContext.HttpContext, new RouteData(), - new ControllerActionDescriptor { ActionName = actionName }); - return new ActionExecutingContext(actionContext, new List(), - new Dictionary(), controller); + var filter = new PerStoreCustomersAttribute.PerStoreCustomersFilter( + new CustomerConfig { RegisterCustomersPerStore = perStoreEnabled }); + var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), + new ControllerActionDescriptor { ControllerName = controllerName, ActionName = actionName }); + var context = new ActionExecutingContext(actionContext, new List(), + new Dictionary(), null); + var nextCalled = false; + + await filter.OnActionExecutionAsync(context, () => + { + nextCalled = true; + return Task.FromResult(new ActionExecutedContext(context, new List(), null)); + }); + + return (context, nextCalled); } [TestMethod] public async Task Gate_PerStoreDisabled_RedirectsToPerStoreDisabled() { - var controller = BuildStoreController(perStoreEnabled: false); - var context = BuildGate(controller, nameof(CustomerController.List)); - var nextCalled = false; + var (context, nextCalled) = await RunGate(false, "Customer", nameof(CustomerController.List)); - await controller.OnActionExecutionAsync(context, () => - { - nextCalled = true; - return Task.FromResult(new ActionExecutedContext(context, new List(), controller)); - }); + Assert.IsFalse(nextCalled); + var redirect = context.Result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual(nameof(CustomerController.PerStoreDisabled), redirect.ActionName); + Assert.AreEqual("Customer", redirect.ControllerName); + } + + [TestMethod] + public async Task Gate_PerStoreDisabled_TabControllerAction_RedirectsToCustomerPerStoreDisabled() + { + var (context, nextCalled) = await RunGate(false, "CustomerAddress", "AddressCreate"); Assert.IsFalse(nextCalled); var redirect = context.Result as RedirectToActionResult; Assert.IsNotNull(redirect); Assert.AreEqual(nameof(CustomerController.PerStoreDisabled), redirect.ActionName); + Assert.AreEqual("Customer", redirect.ControllerName); } [TestMethod] public async Task Gate_PerStoreDisabled_AllowsPerStoreDisabledAction() { - var controller = BuildStoreController(perStoreEnabled: false); - var context = BuildGate(controller, nameof(CustomerController.PerStoreDisabled)); - var nextCalled = false; - - await controller.OnActionExecutionAsync(context, () => - { - nextCalled = true; - return Task.FromResult(new ActionExecutedContext(context, new List(), controller)); - }); + var (context, nextCalled) = await RunGate(false, "Customer", nameof(CustomerController.PerStoreDisabled)); Assert.IsTrue(nextCalled); Assert.IsNull(context.Result); @@ -183,15 +160,7 @@ await controller.OnActionExecutionAsync(context, () => [TestMethod] public async Task Gate_PerStoreEnabled_CallsNext() { - var controller = BuildStoreController(perStoreEnabled: true); - var context = BuildGate(controller, nameof(CustomerController.List)); - var nextCalled = false; - - await controller.OnActionExecutionAsync(context, () => - { - nextCalled = true; - return Task.FromResult(new ActionExecutedContext(context, new List(), controller)); - }); + var (context, nextCalled) = await RunGate(true, "Customer", nameof(CustomerController.List)); Assert.IsTrue(nextCalled); Assert.IsNull(context.Result); @@ -200,7 +169,7 @@ await controller.OnActionExecutionAsync(context, () => [TestMethod] public void PerStoreDisabled_ReturnsView() { - var controller = BuildStoreController(perStoreEnabled: false); + var controller = BuildStoreController(); var result = controller.PerStoreDisabled(); Assert.IsInstanceOfType(result, typeof(ViewResult)); } diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/CreateOrUpdate.TabActivityLog.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/CreateOrUpdate.TabActivityLog.cshtml index 69dc189e7d..0ac2b1a2a7 100644 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/CreateOrUpdate.TabActivityLog.cshtml +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/CreateOrUpdate.TabActivityLog.cshtml @@ -15,7 +15,7 @@
diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/CreateOrUpdate.TabNotes.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/CreateOrUpdate.TabNotes.cshtml index 6e03b0e78f..ae32a1a1ee 100644 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/CreateOrUpdate.TabNotes.cshtml +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/CreateOrUpdate.TabNotes.cshtml @@ -9,8 +9,8 @@
@@ -102,7 +102,7 @@ $.ajax({ cache:false, type: "POST", - url: "@(Url.Action("CustomerNoteAdd", "Customer", new { area = Constants.AreaAdmin }))", + url: "@(Url.Action("CustomerNoteAdd", "CustomerNote", new { area = Constants.AreaAdmin }))", data: postData, success: function (data) { var grid = GrandAdmin.grids.get("#customernotes-grid"); diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/AddressCreate.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/CustomerAddress/AddressCreate.cshtml similarity index 81% rename from src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/AddressCreate.cshtml rename to src/Web/Grand.Web.Admin/Areas/Admin/Views/CustomerAddress/AddressCreate.cshtml index e7efa69554..c267c4e6de 100644 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/AddressCreate.cshtml +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/CustomerAddress/AddressCreate.cshtml @@ -3,13 +3,13 @@ //page title ViewBag.Title = Loc["Admin.Customers.Customers.Addresses.AddNew"]; } -
+ @* one address form: Save as the one primary action, back to the customer it belongs to *@ +
@@ -24,7 +24,7 @@ - {{ $texts.edit }} + {{ $texts.edit }} diff --git a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabCurrentShoppingCart.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabCurrentShoppingCart.cshtml index 90d91147ae..54e1c6aac3 100644 --- a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabCurrentShoppingCart.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabCurrentShoppingCart.cshtml @@ -14,9 +14,9 @@ @*unit prices are edited in the cells and sent with Save changes, one request per changed item*@ @@ -56,7 +56,7 @@ var dataItem = grid.dataSource.get(id); var tokenInput = $('input[name=__RequestVerificationToken]'); $.ajax({ - url: "@Html.Raw(Url.Action("DeleteCart", "Customer", new { customerId = Model.Id, area = area }))", + url: "@Html.Raw(Url.Action("DeleteCart", "CustomerShoppingCart", new { customerId = Model.Id, area = area }))", type: "POST", data: { id: dataItem.Id, diff --git a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabCurrentWishlist.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabCurrentWishlist.cshtml index 440beb0fe0..0881938939 100644 --- a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabCurrentWishlist.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabCurrentWishlist.cshtml @@ -12,8 +12,8 @@
diff --git a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabLoyaltyPoints.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabLoyaltyPoints.cshtml index 72ee6ee759..1fe392442e 100644 --- a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabLoyaltyPoints.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabLoyaltyPoints.cshtml @@ -9,7 +9,7 @@
@if (area == "Admin" && !adminAreaSettings.HideStoreColumn) @@ -67,7 +67,7 @@ $.ajax({ cache:false, type: "POST", - url: "@(Url.Action("LoyaltyPointsHistoryAdd", "Customer", new { area = area }))", + url: "@(Url.Action("LoyaltyPointsHistoryAdd", "CustomerLoyaltyPoints", new { area = area }))", data: postData, success: function (data) { var grid = GrandAdmin.grids.get("#customer-loyaltypoints-grid"); diff --git a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabOrders.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabOrders.cshtml index b3435f1fcd..c3b74247af 100644 --- a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabOrders.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabOrders.cshtml @@ -6,7 +6,7 @@
@@ -34,7 +34,7 @@ } - + {{ ProductName }} diff --git a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabOutOfStockSubscriptions.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabOutOfStockSubscriptions.cshtml index e9d703c9ca..408b23e05c 100644 --- a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabOutOfStockSubscriptions.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabOutOfStockSubscriptions.cshtml @@ -6,7 +6,7 @@
@if (area == "Admin" && !adminAreaSettings.HideStoreColumn) { diff --git a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabProduct.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabProduct.cshtml index d5ac209c40..5164786a7c 100644 --- a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabProduct.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabProduct.cshtml @@ -4,15 +4,15 @@
diff --git a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabProductPrice.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabProductPrice.cshtml index 4db071eec6..ba0afda40d 100644 --- a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabProductPrice.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabProductPrice.cshtml @@ -4,15 +4,15 @@
diff --git a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabReviews.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabReviews.cshtml index db791f043c..426808f261 100644 --- a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabReviews.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabReviews.cshtml @@ -5,8 +5,8 @@
diff --git a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdateAddress.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/CustomerAddress/Partials/CreateOrUpdateAddress.cshtml similarity index 100% rename from src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdateAddress.cshtml rename to src/Web/Grand.Web.AdminShared/Views/AdminShared/CustomerAddress/Partials/CreateOrUpdateAddress.cshtml diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Customer/AddressCreate.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/CustomerAddress/AddressCreate.cshtml similarity index 79% rename from src/Web/Grand.Web.Store/Areas/Store/Views/Customer/AddressCreate.cshtml rename to src/Web/Grand.Web.Store/Areas/Store/Views/CustomerAddress/AddressCreate.cshtml index 797180afaf..57602413e4 100644 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Customer/AddressCreate.cshtml +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/CustomerAddress/AddressCreate.cshtml @@ -2,13 +2,13 @@ @{ ViewBag.Title = Loc["Admin.Customers.Customers.Addresses.AddNew"]; } - + @* one address form: Save as the one primary action, back to the customer it belongs to *@