Repository navigation
Split OrderViewModelService into note, address and add-product services - #979
Merged
Merged
Conversation
added 3 commits
October 23, 2026 08:45
OrderViewModelService (1358 lines, 38 constructor dependencies) keeps the order list, details, export query and tags. The order edit page's notes, address editor and "add product to order" popup move to their own services: - IOrderNoteViewModelService: PrepareOrderNotes, InsertOrderNote, DeleteOrderNote - IOrderAddressViewModelService: PrepareOrderAddressModel, UpdateOrderAddress - IOrderProductViewModelService: PrepareAddOrderProductModel, PrepareAddProductToOrderModel, AddProductToOrderDetails Members are moved verbatim. OrderViewModelService drops to 968 lines and 30 dependencies. BaseOrderManagementController and the Admin and Store OrderController constructors take the three new services. Breaking: IOrderViewModelService lost 8 members, and the constructors of OrderViewModelService, BaseOrderManagementController and both OrderController classes changed.
…ting an address the order does not have - AddProductToOrderDetails stored the incl. tax unit price as excl. tax and the other way round (since #409). With a non-zero tax the new order item showed the wrong unit prices. - AddressEdit (GET and POST) used to fall back to a new, unattached address when the address id did not belong to the order; the POST then "saved" it and added an "Address has been edited" note. Both now throw, as the GET's existing check intended.
…n the panel's store - OrderNoteAdd, ShipmentNoteAdd, MerchandiseReturnNoteAdd, CustomerNoteAdd and VendorNoteAdd are marked [HttpPost]. Every view already calls them with POST. - AddProductToOrderDetails (GET and POST) checks the product against the panel's store with IAclService.Authorize, the same rule the product search on that page uses, and returns to the search otherwise.
…er-viewmodel-service
added 2 commits
October 23, 2026 16:54
…t the order's store - a note only takes a file uploaded for its own order, and deleting a note deletes only such a file - the product added to an order is checked against the order's store, so Main Admin is covered too - the address form edits the order's billing or shipping address instead of throwing on a stale id, and a failed validation shows the typed values again - a quantity below 1 is refused when adding a product to an order - the product and order are loaded once per add-product request; notes without a file skip the lookup
…se file belongs elsewhere - the add-product search lists what the order's store sells (and checks access to the order), and a refused product shows why instead of returning silently to the list - a note whose file was not uploaded for the order is not saved; the panel shows why - drop the BOM added to two files in the previous commit
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,
OrderViewModelService)Type: feature
Not a pure refactor: besides the split, this PR changes behavior in the places listed under Fixes. Each fix is in its own commit, so it can be reviewed or reverted apart from the split.
Issue
Grand.Web.AdminShared/Services/OrderViewModelService.cswas 1358 lines with 38 constructor dependencies. Eight of its members (order notes, the address editor and the "add product to order" page) are used only byBaseOrderManagementController(Admin and Store) and brought in eight dependencies nothing else in the class used.Solution
Three services split out, registered as scoped next to
IOrderViewModelService, the same lifetime as before:IOrderNoteViewModelServicePrepareOrderNotes,InsertOrderNote,DeleteOrderNoteIOrderAddressViewModelServicePrepareOrderAddressModel,UpdateOrderAddressIOrderProductViewModelServicePrepareAddOrderProductModel,PrepareAddProductToOrderModel,AddProductToOrderDetailsMembers moved unchanged (checked by script: every original block, with
_fieldrenamed to the constructor parameter, is in the new file).OrderViewModelServicekeeps the order list and filters, the grid query (PrepareOrderModel, also used by the customer and product order tabs and by the vendor panel),PrepareOrderDetailsModel,PrepareOrders(export) andSaveOrderTags: 968 lines, 6 members, 30 dependencies.The controllers are not split:
BaseOrderManagementControllertakes the three services and the Admin and StoreOrderControllerpass them through, so no route or view changes. The vendor panel (BaseOrderController) is unchanged.Fixes
Found while reviewing the moved code (separate commits):
AddProductToOrderDetailsstored the incl. tax unit price as the excl. tax one and the other way round (since Admin Panel - Replacing IFormCollection with strongly typed objects #409). With a non-zero tax the added order item had the wrong unit prices.AddressEditfell back to a new, unattached address when the address id did not belong to the order; the POST then "saved" it and added an "Address has been edited" note. The address is now taken from the order (billing or shipping, by thebillingAddressflag), so only the order's own address can be edited.OrderNoteAdd,ShipmentNoteAdd,MerchandiseReturnNoteAdd,CustomerNoteAddandVendorNoteAddaccept POST only. Every view already calls them with POST.AddProductToOrderDetails(GET and POST) checks the product against the order's store withIAclService.Authorizeand returns to the search otherwise.Found in the code reviews of this PR (commits
2f7699903,6ae0ec4ef):OrderNoteAddtook anydownloadIdfrom the request, and deleting the note deleted that file. A store manager could point a note at another store's product file and delete it. A note now takes only a file uploaded for its own order (DownloadType.Orderwith the order id asReferenceId, which is what the notes form uploads); any other file refuses the note (Result = falsewithAdmin.Orders.OrderNotes.DownloadNotOfOrder, shown by the panel) instead of saving it without the file. Deleting a note deletes only such a file.order.StoreId, which covers every panel. The product search on that page lists only what the order's store sells (it now gets the order id and checks access to the order), and a refused product returns to the search withAdmin.Orders.Products.AddNew.NotInOrderStoreinstead of silently.ArgumentException(a 500 page). An address stored without_idgets a new id on every load, so the id in the link never matched for such orders. The address is now picked by the flag, and an order without that address redirects to its edit page. When validation fails, the form shows what was typed (and keepsBillingAddress) instead of the saved address.ShoppingCart.QuantityShouldPositive(an existing resource) instead of creating an item with zero or negative totals.PrepareAddProductToOrderModelandAddProductToOrderDetails(they used to load the product, and the order, again). The notes grid no longer looks up a file for notes that have none.PrepareAddOrderProductModelreturnsTask.FromResultwithoutasync; a redundantOrderIdassignment inDeleteOrderNoteis gone.Breaking changes
IOrderViewModelServiceto the new interfaces. Code outside this repository that calls one of them must inject the matchingIOrder<Area>ViewModelService. A class that implementsIOrderViewModelService, or subclassesOrderViewModelServiceand overrides a moved virtual, must move that code to the new service.OrderViewModelService,BaseOrderManagementControllerand the Admin and StoreOrderControllerchanged.AddProductToOrderDetails(GET and POST) takesIProductServiceandIAclServiceas[FromServices]parameters.IOrderProductViewModelService.PrepareAddProductToOrderModel(Order, Product)andAddProductToOrderDetails(Order, Product, AddProductToOrderModel)take the loaded entities instead of ids;OrderProductViewModelServiceno longer takesIOrderServiceandIProductService.IOrderNoteViewModelService.InsertOrderNotereturnsTask<bool>: false when the file is not one uploaded for the order, and nothing is saved then.POST AddProductToOrder) needsOrderId; the view sends it.DefaultLanguage.xmlandUpgrade/en_250.xml:Admin.Orders.OrderNotes.DownloadNotOfOrder,Admin.Orders.Products.AddNew.NotInOrderStore. No route, permission or setting changes.Download.ReferenceId, 2023) are no longer deleted together with their note. Deleting them would need the same check that keeps other stores' files safe, so they stay in the database.Testing
dotnet build GrandNode.slnx: no errors.dotnet test:Grand.Web.Admin.Tests1647 (after the review fixes),Grand.Web.Store.Tests184,Grand.Web.Vendor.Tests53 passed. New tests: unit prices of an added order item, address not on the order (GET and POST), product outside the panel's store (GET and POST), POST-only note-add actions; review fixes: note files of another order are neither attached nor deleted, notes without a file skip the lookup, the product is checked against the order's store, a stale address id edits the order's address, a missing address redirects, a failed address validation keeps the typed values, a quantity below 1 is refused, the product search is limited to the order's store and to orders the panel can access, a refused note file returnsResult = falsewith a message.Customer:RegisterCustomersPerStore=true).PurchasedWithOrderson the vendor's product lists exactly its orders.