Skip to content

ARCH-001 Phase 18: ProductReview controller/service consolidation (Admin/Store) - #813

Merged
KrzysztofPajak merged 6 commits into
developfrom
arch001/phase18-productreview-consolidation
Sep 4, 2026
Merged

ARCH-001 Phase 18: ProductReview controller/service consolidation (Admin/Store)#813
KrzysztofPajak merged 6 commits into
developfrom
arch001/phase18-productreview-consolidation

Conversation

@KrzysztofPajak

Copy link
Copy Markdown
Member

Type: refactor

Issue

Grand.Web.Admin/Controllers/ProductReviewController.cs (171 lines) and Grand.Web.Store/Controllers/ProductReviewController.cs (186 lines) duplicated the same List/Edit/Delete/ApproveSelected/DisapproveSelected/ProductSearchAutoComplete action surface — part of ARCH-001 (architecture/maintainability), continuing the consolidation pattern shipped in PRs #790-#812. IProductReviewViewModelService/IProductReviewService were already fully shared; only the controller shell was duplicated. No Vendor ProductReviewController exists.

Solution

Consolidated behind BaseProductReviewController in Grand.Web.AdminShared, using this initiative's established IAdminDataScope<TEntity> strategy pattern:

  • ProductReview : BaseEntity has a flat StoreId (not IStoreLinkEntity), same shape family as Order/GiftVoucher — required a bespoke StoreProductReviewDataScope/RoutedProductReviewDataScope rather than the generic StoreAdminDataScope<TEntity>.
  • Unlike GiftVoucher, ProductReview has no "global" concept — every review belongs to exactly one store, so HasAccess and CanView are identical throughout (no loose/strict split).
  • Admin/Store ProductReviewController reduced to thin BaseProductReviewController subclasses (each restating [Area]/[Authorize*]/[AuthorizeMenu]/[AutoValidateAntiforgeryToken], since the shared base can't inherit either host's own base controller).
  • Views: CreateOrUpdate.cshtml (byte-identical between hosts) and Edit.cshtml (differed only by a hardcoded area string) moved to Grand.Web.AdminShared/Views/AdminShared/ProductReview/. List.cshtml kept as a genuine per-host override (Admin has an extra store-filter dropdown; both hardcode area strings in inline JS Url.Action calls).

Real bug found and fixed during Task 4's review: the moved views carried forward 3 <vc:admin-widget> calls that were already dead literal HTML on Store (Grand.Web.Store doesn't reference Grand.Web.Admin, home of AdminWidgetViewComponent) — a pre-existing bug, same class already fixed in this initiative's Category/Blog/News/Page phases. Fixed by extracting WidgetZone.*.cshtml satellite partials: the default (<vc:admin-widget>) lives in Grand.Web.Admin's own view tree (not AdminShared, which has no project reference reaching AdminWidgetViewComponent), and a new Store override uses <vc:store-widget> with store_-prefixed zone names. A follow-up fix during final review found and closed the same gap on the one kept List.cshtml override (store_product_review_list_buttons), which this branch had made inconsistent (3 of 4 zones fixed, 1 still dead) relative to itself.

Breaking changes

None. Pure consolidation — no behavior change beyond the two disclosed pre-existing-bug fixes above (both were already dead/inert code on Store, now live extension points; no plugin currently targets these new store_product_review_* zone names, so nothing changes visibly today).

Testing

  1. dotnet build GrandNode.sln — 0 errors, 0 warnings.
  2. Per-project test suites (run separately, this repo's suite is flaky under a single full-solution run): Grand.Web.Admin.Tests 1168/1168, Grand.Web.Store.Tests 99/99, Grand.Mapping.Tests 234/234.
  3. Live-verified against a real Kestrel + MongoDB dev instance:
    • /Admin/ProductReview/List and /Store/ProductReview/List both render correctly against 31 real seeded reviews (the specific regression class this initiative's Page-15 phase hit: a shared List(GET) silently losing one host's model-prep call).
    • /Admin/ProductReview/Edit/{id} and /Store/ProductReview/Edit/{id} (own-store review) render correctly via the shared AdminShared views + new WidgetZone partials.
    • Cross-store negative control: a synthetic Store2-exclusive review was created directly in MongoDB; store1@store.com attempting GET /Store/ProductReview/Edit/{id} on it was denied (redirected to List), confirmed zero mutation. Synthetic data deleted afterward, DB confirmed restored.

Final whole-branch review (opus): Ready to merge: Yes (0 Critical, 1 Important + 3 Minor found and adjudicated — the Important finding and one Minor were fixed in a follow-up commit, verified by a scoped re-review; the remaining 2 Minors were parked as pre-existing/out-of-scope, disclosed above).

KrzysztofPajak and others added 6 commits September 4, 2026 06:17
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ttribute regression tests

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…iews to AdminShared, keep List.cshtml as host-override

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…or ProductReview (fixes dead <vc:admin-widget> on Store)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…t_buttons zone, CanFeatureOnHomepage comment

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 4, 2026 13:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The consolidation preserves the original action surface, adds explicit per-area scoping to maintain tenant isolation, and includes targeted regression/attribute tests for the new shared controller and scopes.

Pull request overview

This PR continues the ARCH-001 consolidation series by removing duplicated Admin/Store ProductReviewController implementations and centralizing the shared action surface in Grand.Web.AdminShared, while keeping only the genuinely host-specific controller attributes and view overrides in each host.

Changes:

  • Introduced BaseProductReviewController (AdminShared) to unify List/Edit/Delete/Approve/Disapprove/ProductSearchAutoComplete behavior across Admin + Store.
  • Added IAdminDataScope<ProductReview> implementations (StoreProductReviewDataScope, RoutedProductReviewDataScope) and registered them in AdminShared startup to preserve correct per-area scoping in the combined host.
  • Moved shared views to Grand.Web.AdminShared and replaced dead/incompatible widget invocations on Store by routing widget zones through per-host partials (Admin: admin-widget, Store: store-widget + store_ zone names).
File summaries
File Description
src/Web/Grand.Web.Store/Controllers/ProductReviewController.cs Reduced to thin Store subclass with Store-specific attributes and DI wiring.
src/Web/Grand.Web.Admin/Controllers/ProductReviewController.cs Reduced to thin Admin subclass with Admin-specific attributes and DI wiring.
src/Web/Grand.Web.AdminShared/Controllers/BaseProductReviewController.cs New consolidated controller implementation for all shared actions and access gates.
src/Web/Grand.Web.AdminShared/Services/StoreProductReviewDataScope.cs Store-specific access scope for ProductReview (strict store ownership).
src/Web/Grand.Web.AdminShared/Services/RoutedProductReviewDataScope.cs Area-based resolver to select correct scope implementation per request.
src/Web/Grand.Web.AdminShared/Startup/StartupApplication.cs DI registrations for ProductReview scopes (Global + Store + Routed).
src/Web/Grand.Web.AdminShared/Views/AdminShared/ProductReview/Edit.cshtml Shared Edit view updated to be area-agnostic and route widget zones via partials.
src/Web/Grand.Web.AdminShared/Views/AdminShared/ProductReview/Partials/CreateOrUpdate.cshtml Shared CreateOrUpdate partial now delegates widget zones to per-host partials.
src/Web/Grand.Web.Store/Areas/Store/Views/ProductReview/List.cshtml Store List override keeps host-specific UI; fixes widget zone to store-widget.
src/Web/Grand.Web.Store/Areas/Store/Views/ProductReview/Partials/WidgetZone.*.cshtml New Store widget-zone partials using store-widget and store_ zone names.
src/Web/Grand.Web.Admin/Areas/Admin/Views/ProductReview/Partials/WidgetZone.*.cshtml New Admin widget-zone partials using admin-widget and existing zone names.
src/Tests/Grand.Web.Admin.Tests/Controllers/BaseProductReviewControllerTests.cs New behavioral tests covering shared controller flows + scoping behavior.
src/Tests/Grand.Web.Admin.Tests/Controllers/ProductReviewTests.cs New scope + routing + attribute tests for Admin host.
src/Tests/Grand.Web.Store.Tests/Controllers/ProductReviewControllerAttributeTests.cs New attribute tests for Store thin subclass correctness.
Review details
  • Files reviewed: 22/22 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

var result = await CreateController().List() as ViewResult;

Assert.IsNotNull(result);
Assert.AreSame(listModel, result.Model);
var result = await CreateController().List() as ViewResult;

Assert.IsNotNull(result);
Assert.AreSame(listModel, result.Model);
var result = await CreateController().Edit("missing") as RedirectToActionResult;

Assert.IsNotNull(result);
Assert.AreEqual("List", result.ActionName);
var result = await CreateController().Edit("pr-1") as RedirectToActionResult;

Assert.IsNotNull(result);
Assert.AreEqual("List", result.ActionName);
var result = await CreateController().Edit("pr-1") as ViewResult;

Assert.IsNotNull(result);
Assert.IsInstanceOfType(result.Model, typeof(ProductReviewModel));
var result = await CreateController().Edit(new ProductReviewModel { Id = "pr-1" }, false) as RedirectToActionResult;

Assert.IsNotNull(result);
Assert.AreEqual("List", result.ActionName);
var result = await CreateController().Edit(new ProductReviewModel { Id = "pr-1" }, false) as RedirectToActionResult;

Assert.IsNotNull(result);
Assert.AreEqual("List", result.ActionName);
var result = await CreateController().Edit(new ProductReviewModel { Id = "pr-1" }, true) as RedirectToActionResult;

Assert.IsNotNull(result);
Assert.AreEqual("Edit", result.ActionName);
var result = await controller.Edit(model, false) as ViewResult;

Assert.IsNotNull(result);
Assert.AreSame(model, result.Model);
var result = await CreateController().Delete("missing") as RedirectToActionResult;

Assert.IsNotNull(result);
Assert.AreEqual("List", result.ActionName);
@KrzysztofPajak
KrzysztofPajak merged commit a86ab29 into develop Sep 4, 2026
7 checks passed
@KrzysztofPajak
KrzysztofPajak deleted the arch001/phase18-productreview-consolidation branch September 4, 2026 16:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants