Repository navigation
Split CustomerViewModelService and the customer edit tabs into one service and controller per tab - #976
Merged
KrzysztofPajak merged 2 commits intoOct 23, 2026
Conversation
Address, loyalty points, shopping cart, products (prices and personalized), out-of-stock subscriptions, notes and contact forms move to their own ICustomer<Tab>ViewModelService. 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.
| new CustomerModel.ProductPriceModel { Id = "pp1", Price = 99 }); | ||
|
|
||
| _customerProductServiceMock.Verify( | ||
| c => c.UpdateCustomerProductPrice(It.Is<CustomerProductPrice>(x => x.Price == 99)), Times.Once); |
|
|
||
| _customerProductServiceMock.Verify( | ||
| c => c.InsertCustomerProductPrice( | ||
| It.Is<CustomerProductPrice>(x => x.CustomerId == "c1" && x.ProductId == "p1" && x.Price == 5)), |
| foreach (var x in addresses) | ||
| { | ||
| var model = await x.ToModel(countryService); | ||
| var addressHtmlSb = new StringBuilder("<div>"); |
Comment on lines
+87
to
+89
| if (address != null) | ||
| if (!excludeProperties) | ||
| model.Address = await address.ToModel(countryService); |
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.
|
|
||
| var redirect = result as RedirectToActionResult; | ||
| Assert.IsNotNull(redirect); | ||
| Assert.AreEqual("AddressEdit", redirect.ActionName); |
|
|
||
| var redirect = result as RedirectToActionResult; | ||
| Assert.IsNotNull(redirect); | ||
| Assert.AreEqual("List", redirect.ActionName); |
|
|
||
| var redirect = result as RedirectToActionResult; | ||
| Assert.IsNotNull(redirect); | ||
| Assert.AreEqual("Edit", redirect.ActionName); |
|
|
||
| var redirect = result as RedirectToActionResult; | ||
| Assert.IsNotNull(redirect); | ||
| Assert.AreEqual("List", redirect.ActionName); |
| Assert.IsFalse(nextCalled); | ||
| var redirect = context.Result as RedirectToActionResult; | ||
| Assert.IsNotNull(redirect); | ||
| Assert.AreEqual(nameof(CustomerController.PerStoreDisabled), redirect.ActionName); |
KrzysztofPajak
pushed a commit
that referenced
this pull request
Oct 23, 2026
Resolve conflicts with the customer tab split (#976) by taking develop's controllers and services and injecting ICustomerMessageProviderService in place of IMessageProviderService, including the new CustomerNoteViewModelService.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of the architecture backlog (item S5: god classes,
CustomerViewModelService)Type: feature
Issue
Grand.Web.AdminShared/Services/CustomerViewModelService.cswas 1432 lines behind a 34-memberICustomerViewModelService, with 28 constructor dependencies, and 11 more services resolved per call fromHttpContext.RequestServices. Every customer tab went through the whole service.Solution
One
ICustomer<Tab>ViewModelService/Customer<Tab>ViewModelServiceper customer edit tab, registered as scoped next toICustomerViewModelService, the same lifetime as before:CustomerAddressCustomerLoyaltyPointsCustomerShoppingCartCustomerProduct(product prices, personalized products, add-product popup)CustomerOutOfStockSubscriptionCustomerNoteCustomerContactFormMembers moved unchanged, except that the services they used to resolve from
HttpContext.RequestServices(ITaxService,IPricingService,IPriceFormatter,IProductAttributeFormatter,IShoppingCartService,IContactUsService,IOutOfStockSubscriptionService,IMessageProviderService,IDownloadService) are now constructor dependencies. They are the same scoped instances for the request.CustomerViewModelServicekeeps the customer list, the form (prepare, insert, update, delete),DeleteSelectedandSendEmail: 897 lines, 8 members, 19 dependencies. Its remainingRequestServiceslookups (VAT, e-mail, external authentication) are left as they were.The customer edit tabs are split out of
BaseCustomerControlleras well, one controller per tab, the same way the product tabs were (Split the product edit tabs out of BaseProductController #974):CustomerAddressCustomerLoyaltyPointsCustomerShoppingCartCustomerProductCustomerOutOfStockSubscriptionCustomerOrderCustomerReviewCustomerNote(Admin only)CustomerContactForm(Admin only)They share
BaseCustomerScopedController, which holdsLoadAuthorizedCustomer/HasAccessToCustomer; a missing or out-of-scope customer still redirects to the customer list (Customer/List). Each action keeps its permission attribute and its scope checks.BaseCustomerControllerkeeps the list, the form and the Info tab: 889 → about 400 lines, 22 → 11 constructor parameters, soListandEditno longer construct the cart, pricing and tax services.The address views (
AddressCreate,AddressEdit,CreateOrUpdateAddressand theAddressDetailswidget zone partials) moved toCustomerAddress/, the product picker popup toCustomerProduct/; the tab partials point at the new controllers.Store panel: the
RegisterCustomersPerStoregate was anOnActionExecutionAsyncoverride onCustomerController, so it would not have covered the tab controllers. It is now a[PerStoreCustomers]filter on every customer controller inGrand.Web.Store, and a test fails if one is missing.Fixes found in review:
GetCartListloads and checks the customer in both panels and passes it to the service. An unknown customer gives an empty grid instead of aNullReferenceException, and the store panel no longer reads the customer twice. In the Admin panel the cart grid now applies the same scope as the rest of the customer screen (a sales manager sees only their own customers).DeleteCustomerNote(id, customerId)refuses a note that belongs to another customer; it used to delete by id alone.Tests: the controller tests are split per controller on a shared
CustomerControllerTestsBasefixture (asProductControllerTestsBase). New tests cover both fixes, the note controller, the per-store filter, the attributes of every new controller, and the shopping cart, out-of-stock subscription and contact form view model services, which had none. The service tests of moved members are inCustomerProductViewModelServiceTests,CustomerNoteViewModelServiceTests,CustomerLoyaltyPointsViewModelServiceTestsandCustomerAddressViewModelServiceTests.Breaking changes
ICustomerViewModelServiceto the new interfaces. Code outside this repository that calls one of them must inject the matchingICustomer<Tab>ViewModelService.ICustomerViewModelService, or subclassesCustomerViewModelServiceand overrides a moved virtual, must move that code to the new service. TheCustomerViewModelServiceconstructor lost the parameters only the moved members used.BaseCustomerController,BaseCustomerManagementControllerand the Admin and StoreCustomerControllerchanged: they lost the tab services and the dependencies only the tab actions used./Admin/Customer/GetCartListis now/Admin/CustomerShoppingCart/GetCartList(same for/Store/...). Nothing else in this repository calls the old URLs; a plugin or theme that does must use the new controller names.Customer/AddressCreate,Customer/AddressEdit,Customer/ProductAddPopup,Customer/Partials/CreateOrUpdateAddressand theWidgetZone.AddressDetails*partials moved toCustomerAddress/andCustomerProduct/. A theme or plugin overriding them under the old folder must move the override. Widget zone names did not change.Testing
dotnet buildof Grand.Web (combined host), Grand.Web.Admin, Grand.Web.Store and both test projects: no errors, no new warnings in the changed files.dotnet test:Grand.Web.Admin.Tests1617 passed,Grand.Web.Store.Tests184 passed.Customer:RegisterCustomersPerStore=true, driven by a Playwright script. As admin, on a customer with a wishlist:Customer/Edit);GetCartListfor an unknown customer returns an empty grid;AddressCreateredirect to the list; cart, product prices, personalized products, orders, reviews and out-of-stock grids come back empty; loyalty history and addresses are refused; adding loyalty points returnsResult: false; deleting the customer's product price changes nothing (checked afterwards as admin).RegisterCustomersPerStore=false:Customer/List,CustomerAddress/AddressCreate,CustomerProduct/ProductAddPopup,CustomerShoppingCart/GetCartListandCustomerLoyaltyPoints/LoyaltyPointsHistorySelectin the store panel all redirect toCustomer/PerStoreDisabled, which renders.