fix(arch001): consolidate VendorReview Admin/Vendor duplication (Phase 10) - #802
Merged
KrzysztofPajak merged 9 commits intoAug 29, 2026
Merged
Conversation
…lService, delete Vendor's duplicate - IVendorViewModelService/VendorViewModelService: ApproveVendorReviews/ DisapproveVendorReviews now take an IAdminDataScope<VendorReview> scope parameter, consumed via scope.HasAccess before mutating (deliberately reorders the GetVendorById call to happen after the scope/null check). - Delete Grand.Web.Vendor's duplicate IVendorReviewViewModelService/ VendorReviewViewModelService (now fully superseded by AdminShared's VendorViewModelService + Task 1's VendorVendorReviewDataScope). - Add VendorViewModelServiceTests covering the entity-populate path, the list-scope path, and both approve/disapprove scope-filtering paths (global vs vendor-owned). - Necessary transitional fix beyond the task's named file list: Admin's own VendorReviewController.cs (not part of the duplication being removed) already called the old 1-arg signature and would otherwise fail to build; updated it to constructor-inject the already DI-registered IAdminDataScope<VendorReview> (Task 1) and pass it through unchanged behavior-wise (Admin's routed scope resolves to GlobalAdminDataScope for this host). Task 4/5 are expected to further refactor this controller into a shared base class. Remaining build errors after this change are confined to Grand.Web.Vendor/Controllers/VendorReviewController.cs, which still references the deleted IVendorReviewViewModelService/Vendor model types - resolved by Task 5.
…eview actions Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141KkNM5HDRq3ZqAXunF9Ug
…ubclasses Also removes the leftover Grand.Web.Vendor DI registration for the deleted IVendorReviewViewModelService/VendorReviewViewModelService (dangling since Task 2/3 deleted those types), which was blocking the solution build. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141KkNM5HDRq3ZqAXunF9Ug
…hared, extract widget-zone satellites Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141KkNM5HDRq3ZqAXunF9Ug
…VendorReview thin subclasses Found by Task 7's live cross-vendor smoke test: neither concrete VendorReviewController restated [Area]/[AuthorizeAdmin|Vendor]/[AuthorizeMenu] after BaseVendorReviewController stopped inheriting from BaseAdminController/BaseVendorController - every route 404'd on both hosts. Same bug class as Order's Task 17 and MerchandiseReturn's own live smoke test finding; a green build + passing unit tests cannot catch it since unit tests construct the controller directly, bypassing MVC routing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141KkNM5HDRq3ZqAXunF9Ug
…tion - ApproveVendorReviews/DisapproveVendorReviews: derive the vendor id used for totals refresh from the loaded, access-checked VendorReview entity instead of the client-supplied composite id half. A vendor could submit 'myReviewId:otherVendorId' and mutate another vendor's aggregate rating totals, or crash the request with a garbage/non-composite id. - PrepareVendorReviewModel: null-guard VendorName when the vendor behind a review has since been deleted. - Add service test coverage for the mismatched-composite-vendor-id case on both Approve/DisapproveVendorReviews. - Add controller routing/surface regression tests for both concrete VendorReviewControllers (Admin, Vendor) covering Area/Authorize attributes and the Admin-only VendorSearchAutoComplete action, mirroring PaymentTransactionControllerRoutingTests and OrderControllerSurfaceTests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141KkNM5HDRq3ZqAXunF9Ug
| { | ||
| var result = _controller.Index() as RedirectToActionResult; | ||
| Assert.IsNotNull(result); | ||
| Assert.AreEqual("List", result.ActionName); |
| { | ||
| var result = _controller.List() as ViewResult; | ||
| Assert.IsNotNull(result); | ||
| Assert.IsInstanceOfType(result.Model, typeof(VendorReviewListModel)); |
| var result = await _controller.Edit("missing") as RedirectToActionResult; | ||
|
|
||
| Assert.IsNotNull(result); | ||
| Assert.AreEqual("List", result.ActionName); |
| var result = await _controller.Edit("r1") as RedirectToActionResult; | ||
|
|
||
| Assert.IsNotNull(result); | ||
| Assert.AreEqual("List", result.ActionName); |
| var result = await _controller.Delete("r1") as RedirectToActionResult; | ||
|
|
||
| Assert.IsNotNull(result); | ||
| Assert.AreEqual("List", result.ActionName); |
| var result = await _controller.Delete("r1") as RedirectToActionResult; | ||
|
|
||
| Assert.IsNotNull(result); | ||
| Assert.AreEqual("List", result.ActionName); |
…ndorreview-consolidation # Conflicts: # src/Web/Grand.Web.AdminShared/Startup/StartupApplication.cs # src/Web/Grand.Web.Vendor/Areas/Vendor/Views/_ViewImports.cshtml # src/Web/Grand.Web.Vendor/Extensions/HasAccess.cs # src/Web/Grand.Web.Vendor/Startup/StartupApplication.cs
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.
Type: refactor
Issue
ARCH-001 (Architecture/Maintainability, P1): the back-office panels (
Grand.Web.Admin,Grand.Web.Vendor,Grand.Web.Store) largely duplicate rather than share code, despiteGrand.Web.AdminSharedexistingspecifically to prevent this. This is Phase 10, consolidating the
VendorReviewentity — the lastoriginally-scoped ARCH-001 candidate — following the exact same pattern already shipped for Product,
Category, Collection, Order, Shipment, PaymentTransaction, MerchandiseReturn, and Reports.
VendorReviewonly exists on Admin and Vendor (no Store screen). Before this PR:VendorReviewControllerwas duplicated 1:1 between the two hosts,
IVendorReviewViewModelService/VendorReviewViewModelServicewas a separate, drifted copy in
Grand.Web.Vendor(Admin's version already lived in the sharedGrand.Web.AdminShared.VendorViewModelService), andVendorReviewModel/VendorReviewListModelwere twoseparate model types.
Solution
IAdminDataScope<VendorReview>implementations:GlobalAdminDataScope<VendorReview>reusedunmodified for Admin (no restriction — Admin's original controller enforced none); new
VendorVendorReviewDataScope(flatVendorIdownership check, no child-collection scan); newRoutedVendorReviewDataScope(2-branch Admin/Vendor request-time resolver, fails closed on any otherarea including "Store" — this entity has no Store screen at all).
BaseVendorReviewControllerinGrand.Web.AdminSharedholds all 7 shared actions (List, Edit,Delete, ApproveSelected, DisapproveSelected).
VendorSearchAutoComplete— a genuinely non-duplicated,Admin-only action (Vendor never had an equivalent, and exposing it would leak other vendors'
names/ids to a vendor account) — stays on Admin's own thin subclass rather than being promoted to the
shared base.
Grand.Web.Vendor's duplicateIVendorReviewViewModelService/VendorReviewViewModelServiceand itstwo model files deleted;
Grand.Web.AdminShared.Services.VendorViewModelService's existingVendorReviewregion absorbed the reconciled logic (ApproveVendorReviews/DisapproveVendorReviewsgained a
scopeparameter, used to checkHasAccessper selected id before mutating).VendorId/VendorNameon the sharedVendorReviewModelare always populated from the loaded entity(not scope-derived) — this collapses what used to be two different
ApproveSelected/DisapproveSelectedselection-id encodings (Admin's composite"{reviewId}:{vendorId}"vs. Vendor'splain id) into one format used by both hosts.
Edit.cshtmlunified intoGrand.Web.AdminShared(zero structural difference beyondresource-key prefix/area, using the already-proven
Scope.ResourceKeyPrefix/ViewContext.RouteData.Values["area"]idioms).List.cshtml/Partials/CreateOrUpdate.cshtml/Admin'sPartials/SearchVendor.cshtmlstay host-specific — Admin's grid/form carry a genuine extraVendorNamecolumn/field that Vendor's don't, so mixing unification shapes for one entity's view set was judged not
worth it at this small a file count. Widget-zone call sites in
Edit.cshtmlextracted into per-hostWidgetZone.DetailsButtons.cshtmlsatellites, same precedent as every prior phase.Found and fixed during this phase
VendorReviewControllerrestated[Area]/[AuthorizeAdmin|AuthorizeVendor]/[AuthorizeMenu]afterBaseVendorReviewControllerstopped inheriting fromBaseAdminController/BaseVendorController(whichcan't happen — those attributes differ per host) — every route 404'd on both hosts. Same bug class as
Order's own Task 17 and MerchandiseReturn's live smoke test finding in this initiative. Fixed and
independently re-verified live before continuing. New regression tests added
(
VendorReviewControllerRoutingTests/VendorReviewControllerSurfaceTests) asserting the attribute setson both hosts, since no unit test can otherwise catch this (constructing a controller directly bypasses
MVC routing entirely).
ApproveVendorReviews/DisapproveVendorReviewsused the client-supplied vendor half of the composite selection id (rather than the loaded, access-
checked entity's own
VendorId) to refresh vendor rating totals — a tampered composite id could recomputethe wrong vendor's aggregate totals, or throw an unhandled exception for a malformed/legacy id. Fixed to
use
vendorReview.VendorId(the entity's own field) in both methods, with a regression test for themismatched-id case.
Breaking changes
None for end users. For anyone with custom localization overrides of
Vendor.VendorReviews.Fields.*keys:Vendor's kept edit-form field labels (Customer/Title/Rating/etc., in the still-host-specific
Partials/CreateOrUpdate.cshtml) now resolve off the shared model'sAdmin.VendorReviews.Fields.*keysinstead — the page title and buttons (which use
Scope.ResourceKeyPrefix) are unaffected and stillresolve
Vendor.*. This matches the same choice already shipped for Order/Shipment's Vendor screens; anystore that customized the
Vendor.VendorReviews.Fields.*resource strings specifically will see themrevert to the
Admin.*wording on this one screen.Testing
dotnet build GrandNode.sln— 0 errors, 0 warnings introduced by this change (4 pre-existing warningsunrelated to this phase, same as
develop).dotnet test src/Tests/Grand.Web.Admin.Tests(657/657),dotnet test src/Tests/Grand.Web.Vendor.Tests(15/15),
dotnet test src/Tests/Grand.Mapping.Tests(234/234).with 2 synthetic
VendorReviewdocuments (one owned by a real logged-in vendor, one owned by adifferent vendor) and real login credentials:
Listshows only the vendor's own review;Edit(GET)/Delete(POST) on the other vendor'sreview both deny (redirect to List) with zero DB mutation confirmed via a direct read; a tampered
ApproveSelectednaming the other vendor's review is silently skipped (confirmed via DB read, notjust the response shape); the same action on the vendor's own review succeeds (positive control,
proves the deny path isn't just a broken pipeline).
Listshows both vendors' reviews with theVendorNamecolumn populated;VendorSearchAutoCompletereturns matching vendors (Admin-only, confirmed absent/404 on Vendor'shost);
Edit(GET) on any vendor's review succeeds;Edit(POST, save-and-continue) redirects to.../Edit/<id>?VendorId=<vendorId>correctly.state.
one round, scoped re-review confirmed both addressed with no new breakage. Minor findings (localization
key change disclosed above; a few defensive/cosmetic notes already shipped identically in prior ARCH-001
phases) parked as non-blocking.