Skip to content

ARCH-001 Phase 20: Customer controller/service consolidation (Admin/Store) - #819

Merged
KrzysztofPajak merged 23 commits into
developfrom
arch001/phase20-customer-consolidation
Sep 9, 2026
Merged

ARCH-001 Phase 20: Customer controller/service consolidation (Admin/Store)#819
KrzysztofPajak merged 23 commits into
developfrom
arch001/phase20-customer-consolidation

Conversation

@KrzysztofPajak

@KrzysztofPajak KrzysztofPajak commented Sep 9, 2026

Copy link
Copy Markdown
Member

Type: refactor

Issue

ARCH-001 Phase 20: Grand.Web.Admin.Controllers.CustomerController (1006 lines) and
Grand.Web.Store.Controllers.CustomerController (906 lines) duplicated nearly the entire Customer
management surface instead of sharing it via Grand.Web.AdminShared, following the pattern already
shipped for Product, Category, Collection, Order, Shipment, PaymentTransaction, MerchandiseReturn,
Reports, VendorReview, the attribute family, Discount, Brand, Blog, Page, News, GiftVoucher,
ProductReview, and MessageTemplate.

Solution

Merged both controllers behind BaseCustomerController/BaseCustomerManagementController in
Grand.Web.AdminShared, driven by a new IAdminDataScope<Customer> — two bespoke, non-generic
scope classes (AdminCustomerDataScope: Sales-Manager-aware; StoreCustomerDataScope:
store+registered-only), since Customer's ownership rules have no parallel among already-shipped
entities. Admin's concrete controller is a fully-empty thin subclass carrying the complete action
surface; Store's inherits the shared base directly (a genuine subset — Impersonate/Export/
CustomerNote*/ContactForm stay Admin-only) plus its own per-store feature gate, PerStoreDisabled
page, and ApplyPostConstraints override forcing ownership fields on every POST. All Customer views
migrated into AdminShared except Edit/Create/List (genuine per-host overrides), with
store_-prefixed widget-zone satellites added throughout (Store's Customer views had zero prior
<vc:> calls — all net-new extension points, not dead-code fixes).

Design spec: docs/superpowers/specs/2026-09-05-arch001-customer-consolidation-design.md
Implementation plan: docs/superpowers/plans/2026-09-05-arch001-customer-consolidation.md
(both gitignored, referenced here for anyone continuing this initiative)

Disclosed, intentional behavior changes (not pure refactor)

  1. List() now populates CustomerSettings-driven column-visibility flags for Store. Store's
    original silently returned new CustomerListModel(), always hiding the Username/Company/Phone/
    ZipPostalCode optional grid columns regardless of what the store owner configured. Real,
    user-visible bug fix.
  2. [HttpPost] added to 4 previously GET-reachable mutating actions
    (UpdateProductPrice/DeleteProductPrice/UpdatePersonalizedProduct/DeletePersonalizedProduct)
    on Admin — they never carried it, meaning [AutoValidateAntiforgeryToken] (which only validates
    non-safe verbs) never protected them. Store's originals already had [HttpPost]. Closes a real
    CSRF-bypass gap.
  3. AdminCustomerDataScope.HasAccess was missing a Deleted check present throughout Admin's
    original controller (customer.Deleted was checked inline before nearly every action). Found
    mid-implementation (surfaced as a minor note during one task's review, confirmed load-bearing),
    fixed at the scope-class level before later work consumed it — soft-deleted customers are once
    again inaccessible via Admin, matching the original exactly.
  4. Admin's Create (POST) required PermissionActionName.Edit instead of .Create — the
    only Admin Create action gated on the wrong permission (every other Create action, and Store's
    original Create, correctly required .Create). Same defect class already disclosed and fixed
    for GiftVoucher's consolidation. Harmonized on .Create; a user granted Create but not Edit
    now gains customer-creation access they lacked before, and vice versa for Edit-only users.
    Found in post-merge review of this PR. Regression test:
    CustomerControllerAttributeTests.CreatePost_RequiresCreatePermission.

Final whole-branch review — 2 Critical + 3 Important, all fixed

The final review (dispatched on the most capable available model, independently re-deriving every
access-control gate from the original pre-consolidation controllers rather than trusting per-task
review verdicts) found:

  • Critical ×2: ProductsPrice and PersonalizedProducts silently lost Store's ownership gate
    during the task that added them — Store's originals both opened with a GetStoreCustomer-style
    check denying access to another store's customer, dropped in the merge. Any store-panel user with
    Customer-preview permission could POST an arbitrary customerId and read that customer's
    negotiated prices / personalized-product assignments, including customers belonging to other
    stores. Same defect class this initiative has been bitten by before (News Phase 16's Comments/
    CommentDelete leak): the task's own framing named the mutating sibling actions
    (UpdateProductPrice/DeleteProductPrice, which got correct gates), and the adjacent read
    actions one region away were missed by both the implementer and its task reviewer.
  • Important: ReviewList lost the customer-ownership half of its original two-part gate (the
    store-id filter half survived) — bounded impact (results stay within the caller's own store), but
    a store manager could enumerate which foreign-store customers reviewed on their store, which the
    original denied.
  • Important: Store's List() lost [PermissionAuthorizeAction(PermissionActionName.List)],
    present on Store's original. A store user whose List permission was revoked could still load the
    page shell (no data exposure — CustomerList POST retained its own gate).
  • Important: Edit/Delete/SendEmail narrowed Admin's original catch (Exception) to
    catch (GrandException) (Store's original, narrower form) — an undisclosed behavior change for
    Admin: a non-GrandException failure now propagates as an unhandled 500 instead of being caught
    and surfaced via Error(exc.Message) with the form redisplayed.

All 5 fixed in one wave (regression tests added for the two Critical fixes and ReviewList;
List()'s permission grant independently verified against the real Customers permission seed
before adding; all three catch sites widened back to Exception), then independently
re-verified in a scoped re-review — all 5 confirmed ADDRESSED, no new breakage. 1271/1271
Grand.Web.Admin.Tests passing after the fix wave.

Minor findings, disclosed not fixed (triaged as non-blocking by the final reviewer)

  • store_customer_details_buttons/store_customer_list_buttons widget-zone satellites were not
    added (spec called for them on the kept-host-specific Edit/Create/List views) — two missing
    extension points, not a regression.
  • Two host-detection idioms (ViewContext.RouteData.Values["area"] and Scope.DefaultStoreId is null) are both used, sometimes in the same file — both are sanctioned by
    Views/AdminShared/_ViewImports.cshtml's own documentation; picking one per file would read
    better.
  • Store's tab indices shift (OutOfStock 6→8, LoyaltyPoints 7→9) since Notes/ActivityLog now reserve
    slots even when hidden for Store — self-healing on next save, cosmetic.
  • The spec's suggested List() perf micro-optimization (skip group/tag lookups for Store) wasn't
    implemented — both hosts pay for the full PrepareCustomerListModel() call. Two extra reads per
    page-load, acceptable.
  • AddressCreate/AddressEdit/ProductAddPopup views remain duplicated per host (expected to
    unify pending a task-time diff; the diff turned out non-trivial and this was descoped, not
    silently dropped).
  • Several other GET-reachable mutators (SendEmail, LoyaltyPointsHistoryAdd, CustomerNoteAdd,
    DeleteSelected) remain without [HttpPost], faithfully preserved from both originals — fixing
    4 of ~8 in one PR was a defensible scope line, not a gap introduced here.

Live smoke test — partial, honestly disclosed gap

Verified live against a real dev DB (Admin login, /Admin/Customer/List and /Admin/Customer/Edit
render correctly with the full action surface and all 11 tabs — confirms the attribute-cutover
regression class this initiative has hit repeatedly did not recur here, and confirms the
TabInfo.cshtml merge — this plan's own single biggest flagged risk — renders correctly live, not
just via build+unit-tests). Store login and the per-store gate (PerStoreDisabled redirect) also
confirmed live on Store's host.

Not verified live: the deeper Store-side flows (cross-store Edit denial, the List()
CustomerSettings-column fix, the two Critical ProductsPrice/PersonalizedProducts fixes, and the
ApplyPostConstraints anti-smuggling proof) — all require Customer:RegisterCustomersPerStore=true
in the test environment, which is false there (Store's Customer panel is disabled site-wide).
Attempting to flip it in an isolated throwaway config confirmed the setting's own documented
warning: it broke login resolution for the existing seeded accounts (global-unique vs.
per-store-unique lookup semantics). Reverted immediately, verified zero net change. These specific
fixes were independently re-derived from source twice already (the final review's own re-derivation,
plus its scoped re-review's independent test execution) but never round-tripped through an actual
live HTTP request as a Store user. Flagged here rather than silently omitted.

Breaking changes

None for installations at default settings. See "Disclosed, intentional behavior changes" above —
all are fixes to existing gaps, not new restrictions, except the narrower/wider exception-catch
adjustment (restored to original width, not a new narrowing).

Testing

  1. dotnet build GrandNode.sln — 0 errors, 0 warnings.
  2. Per-project test runs (this repo's suite is flaky when run in parallel across projects — always
    run one project at a time): Grand.Web.Admin.Tests 1271/1271, Grand.Web.Store.Tests 111/111,
    Grand.Mapping.Tests 234/234.
  3. Manual: log in to /Admin and /Store, confirm the Customer list/edit screens render and
    existing customer data displays correctly on both hosts.
  4. If Customer:RegisterCustomersPerStore=true in your environment: additionally verify a
    store-panel user cannot view/edit another store's customer, cannot smuggle Owner/VendorId/
    StaffStoreId/SeId/CustomerGroups via a crafted POST, and that ProductsPrice/
    PersonalizedProducts correctly deny cross-store access — this is the one class of check this PR
    itself could not complete live (see "Live smoke test" above).

KrzysztofPajak and others added 18 commits September 5, 2026 09:16
…pe<Customer>

Task 3 of ARCH-001 Phase 20 (Customer consolidation): routes to
AdminCustomerDataScope or StoreCustomerDataScope based on the request's
area route value, failing closed for Vendor/unrecognized/missing areas
(Customer has no Vendor screen). Registers the two concrete scopes plus
the routed IAdminDataScope<Customer> in StartupApplication.cs, inserted
directly after the MessageTemplate scope block without touching any
other entity's registration.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRztBQVfXimBPMFAioLabo
…List/Create, fix Store List() CustomerSettings gap

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRztBQVfXimBPMFAioLabo
…llable per plan spec

Enables #nullable on BaseCustomerController.cs (matching sibling
BaseReportsController.cs/BaseFullReportsController.cs) and changes
CheckTwoFactorEnabledWarning(Customer existingCustomer, ...) to
Customer? existingCustomer, as mandated by the plan's Global
Constraints (the brief's own code sample had this non-nullable,
which was incorrect). Also annotates the CustomerList() local
'tagIds' as string[]? since it is genuinely assigned null under
Store scope — the only other warning enabling #nullable surfaced.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRztBQVfXimBPMFAioLabo
…ns with LoadAuthorizedCustomer helper

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRztBQVfXimBPMFAioLabo
…via flow analysis

Replaces 'if (denied != null) return denied;' followed by up to three
customer! suppressions per method with 'if (customer is null) return
denied!;', which lets the compiler narrow customer to non-null for the
rest of each method. Removes all 13 customer! suppressions in the file,
down to one denied! suppression per call site (7 total), justified by
LoadAuthorizedCustomer's own invariant: it returns either (null, denied)
or (customer, null), never both non-null or both null.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRztBQVfXimBPMFAioLabo
…s regions

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRztBQVfXimBPMFAioLabo
Ported missing Deleted check from original Admin CustomerController that
prevented access to soft-deleted customers. Without this check, deleted
customers are silently accessible/editable again via the Admin panel.

- Add entity.Deleted check to HasAccess (deny if deleted)
- Add test cases for deleted customer (both sales-manager and non-sales-manager)
- Verify no regression: 7 AdminCustomerDataScopeTests pass, 20 BaseCustomerControllerTests pass

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRztBQVfXimBPMFAioLabo
…t personalize/price, out-of-stock), add missing [HttpPost] CSRF hardening

Adds the final block of BaseCustomerController's shared action surface:
ProductsPrice, PersonalizedProducts, ProductAddPopup (GET/POST),
ProductAddPopupList, UpdateProductPrice, DeleteProductPrice,
UpdatePersonalizedProduct, DeletePersonalizedProduct,
OutOfStockSubscriptionList, plus a new HasAccessToCustomer helper
(non-redirecting ownership check for actions that soft-deny with an
empty JsonResult instead of a redirect).

Security fix (disclosed): Admin's originals for UpdateProductPrice,
DeleteProductPrice, UpdatePersonalizedProduct, and
DeletePersonalizedProduct never carried [HttpPost] (only
PermissionAuthorizeAction), unlike Store's already-correct versions.
Since [AutoValidateAntiforgeryToken] only validates unsafe HTTP verbs,
these were reachable via GET, bypassing CSRF protection. All four now
carry [HttpPost], matching Store's convention.

Ownership-check asymmetry preserved, not fixed: Admin's originals for
every action in this region never checked customer ownership; Store's
did. Ported via 'if (scope.DefaultStoreId is not null) { ...check... }'
branches rather than a uniform check, mirroring GetCartList's
already-noted asymmetry from Task 7.

DeletePersonalizedProduct's global-scope branch calls
ICustomerViewModelService.DeletePersonalizedProduct(id) directly,
matching Admin's original convenience-method call, rather than a
two-step Get+Delete.

This is the last task adding to BaseCustomerController's shared action
surface; Task 9 starts the Admin-only BaseCustomerManagementController.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRztBQVfXimBPMFAioLabo
…ctions

Task 9 of ARCH-001 Phase 20 (Customer consolidation): adds
BaseCustomerManagementController, an abstract subclass of
BaseCustomerController holding Admin-only actions (Impersonate,
RemoveAffiliate, DeleteSelected, ExportExcelAll/Selected,
CustomerNotesSelect/Add/Delete, ContactFormList) plus the real
override of the CheckTwoFactorEnabledWarning hook.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRztBQVfXimBPMFAioLabo
…omerManagementController subclass

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRztBQVfXimBPMFAioLabo

Task 10 of ARCH-001 Phase 20 (Customer consolidation). Resumed from a prior
implementer's uncommitted work; verified it against the task-10 brief before
committing.

- CustomerController now a thin subclass of BaseCustomerManagementController,
  restating [AuthorizeAdmin]/[AutoValidateAntiforgeryToken]/[Area]/[AuthorizeMenu]
  (previously arrived transitively via BaseAdminController, which the new shared
  base cannot inherit).
- Added CustomerControllerAttributeTests.cs regression guard, verified by
  inspection to genuinely fail against the pre-cutover controller body
  (commit ed1d8b3): BaseAdminController carried those four attributes, and
  CustomerController itself carried none of its own, so inherit:false lookups
  return empty against the old body.
- No pre-existing CustomerControllerTests.cs existed in this repo to trim/delete
  (brief's Step 4 was moot here).
…omerController subclass with ApplyPostConstraints override

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRztBQVfXimBPMFAioLabo
…ed, add store_ widget-zone satellites

Moves the 9 shared tab partials + CreateOrUpdateAddress.cshtml + SendEmail.cshtml + the
CreateOrUpdate.cshtml tabstrip shell into Grand.Web.AdminShared. Every <vc:admin-widget> call is
replaced by a Partials/WidgetZone.*.cshtml per-area-partial (default in Grand.Web.Admin backed by
vc:admin-widget, override in Grand.Web.Store backed by vc:store-widget with a new store_-prefixed
zone name), per the established GiftVoucher Phase 17 pattern - no <vc:...> literal lives in any
AdminShared view.

Step 1's diff was not empty for 10 of 11 files (contradicting the brief's assumption); resolved per
coordinator ruling using idioms already established elsewhere in this AdminShared tree:
- Constants.AreaAdmin/Constants.AreaStore literals -> `var area = ViewContext.RouteData.Values["area"]?.ToString();`
  (matches Category/Page/CustomerAttribute's own AdminShared views; Constants isn't even resolvable
  in AdminShared, which imports neither host's Extensions namespace).
- AdminAreaSettings.HideStoreColumn-gated grid columns (TabCurrentShoppingCart/TabCurrentWishlist/
  TabOrders/TabOutOfStockSubscriptions/TabLoyaltyPoints) and TabLoyaltyPoints' extra
  AddLoyaltyPointsStoreId field/JS -> wrapped in `@if (area == "Admin" && ...)`, reproducing both
  hosts' current behavior by construction (Store's UI is always single-store, so it never rendered
  these regardless of the setting).
- CreateOrUpdateAddress.cshtml moved as-is (BOM-only diff).

The tabstrip shell adds a local `@inject IAdminDataScope<Customer> Scope` (shadowing the tree-wide
Product-typed one in _ViewImports.cshtml, matching Order/Shipment/VendorReview's own local
overrides) and gates the three Admin-only tabs (CustomerNotes, ActivityLog, Documents) on
`Scope.DefaultStoreId is null`. Those three tabs' own partials stay in Grand.Web.Admin's tree
(Store never had them) alongside CreateOrUpdate.TabInfo.cshtml, untouched per Task 13's scope.

Does not touch Partials/CreateOrUpdate.TabInfo.cshtml - Task 13's own field-by-field audit.
…by-field audit

Merged the last unmigrated Customer partial into AdminShared after a full
field-by-field diff (Admin 487 lines vs Store 406). Every ADMIN-ONLY diff
block accounted for:

- AJAX area literal (Constants.AreaAdmin/AreaStore) -> the established
  `var area = ViewContext.RouteData.Values["area"]?.ToString()` idiom from
  Task 12.
- customer_details_info_top/bottom widget zone calls -> per-area satellite
  partials (WidgetZone.InfoTop/InfoBottom.cshtml), Admin default +
  store_-prefixed Store override, same pattern as every other Customer tab.
- CustomerGroups/VendorId/StaffStoreId/StoreId/Owner/SeId fields: forced or
  blanked by BaseCustomerController.ApplyPostConstraints (Store-only
  override) -- confirmed against its source, which also documents StoreId
  as forced (the brief's own field list omitted it). Gated on
  `Scope.DefaultStoreId is null`.
- CustomerTags field: not forced by ApplyPostConstraints but never exposed
  by Store's panel; PrepareCustomerModel populates it unconditionally for
  both hosts, so this is an Admin-only UI capability, not a data
  constraint. Same Scope gate.
- AffiliateId block: not in the brief's worked examples. Its
  asp-action="RemoveAffiliate" only exists on BaseCustomerManagementController,
  which only Admin's CustomerController inherits -- Store's controller
  doesn't have the action at all, so rendering the block unconditionally
  would post to a nonexistent route. Same Scope gate, combined with the
  existing AffiliateId null-check.
- LastIpAddress/CreatedOn/LastActivityDate: Admin wraps each field in its
  own @if, Store wraps all three in one @if with identical conditions and
  identical inner markup -- cosmetic only, kept Admin's three-block form.
- BOM, comment-only lines, blank lines: whitespace-class, ignored.

Added two model-preparation characterization tests to
CustomerViewModelServiceTests.cs proving PrepareCustomerModel populates
every Scope-gated field for a global-scoped call, and that a store-scoped
existing-customer call still maps StoreId/VendorId from the entity (the
Store-only blanking happens in ApplyPostConstraints on POST, not here).

Build: dotnet build GrandNode.sln -> 0 errors.
Tests: Grand.Web.Admin.Tests 1264/1264 passed (was 1262 baseline + 2 new).
       Grand.Web.Store.Tests 111/111 passed (unchanged).
… gates

Fixes findings from the final whole-branch review of the Customer consolidation:

- ProductsPrice/PersonalizedProducts: restore Store's dropped ownership gate
  (cross-tenant PII read) using the LoadAuthorizedCustomer pattern already
  established by GetCartList/OutOfStockSubscriptionList.
- ReviewList: restore the customer-ownership half of its original two-part
  gate; the store-id filter passed into GetAllProductReviews was already
  correct and is left untouched.
- List(): add back [PermissionAuthorizeAction(PermissionActionName.List)],
  dropped from Store's original during the merge. Confirmed Admin's seeded
  ManageCustomers permission already grants List by default, so this cannot
  regress Admin's own access.
- Edit(POST)/Delete/SendEmail: widen catch (GrandException) back to
  catch (Exception), restoring Admin's original catch-all behavior that was
  silently narrowed by taking Store's catch clause during the merge.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRztBQVfXimBPMFAioLabo
…ates

Adds coverage for the review fixes in BaseCustomerController.cs:

- ProductsPrice/PersonalizedProducts: Store-scope denial (empty result,
  service never called) and global-scope pass-through pairs.
- ReviewList: same Store-scope/global-scope pair.
- DeleteProductPrice: renamed the misnamed
  DeleteProductPrice_StoreScope_DeniedOwnership_NoOpsSilently to
  DeleteProductPrice_StoreScope_PriceNotFound_NoOpsSilently (it exercised the
  not-found branch, not ownership denial) and added a new test for the real
  ownership-denial branch using a CustomerProductPrice that exists but
  belongs to a customer outside the caller's store.

Exposes CustomerProductServiceMock as a test-class field so individual tests
can control GetCustomerProductPriceById.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRztBQVfXimBPMFAioLabo
Copilot AI lite review requested due to automatic review settings September 9, 2026 04:32

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.

🟡 Changes recommended

Two mutating Admin actions in the new shared management controller remain GET-reachable (missing [HttpPost]), bypassing antiforgery validation and expanding CSRF exposure.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Refactors Customer management to eliminate duplicated Admin/Store controllers by moving the shared action surface into Grand.Web.AdminShared base controllers, using a routed IAdminDataScope<Customer> to enforce per-host access rules, and migrating most Customer views into AdminShared with host-specific overrides and new Store widget-zone extension points.

Changes:

  • Consolidated Admin + Store customer actions behind BaseCustomerController / BaseCustomerManagementController, with Store reduced to a thin controller plus store-only gate + post-constraint enforcement.
  • Introduced IAdminDataScope<Customer> implementations (AdminCustomerDataScope, StoreCustomerDataScope) and a routed resolver (RoutedCustomerDataScope) wired via DI.
  • Migrated/reworked Customer UI partials into AdminShared, adding host-specific WidgetZone partials and expanding test coverage for the new shared surface and scope routing.
File summaries
File Description
src/Web/Grand.Web.Store/Controllers/CustomerController.cs Store controller becomes thin subclass of shared base; keeps store-only gate + POST constraints.
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/WidgetZone.WishlistTop.cshtml Adds Store widget zone hook for wishlist tab (top).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/WidgetZone.WishlistBottom.cshtml Adds Store widget zone hook for wishlist tab (bottom).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/WidgetZone.ShoppingCartTop.cshtml Adds Store widget zone hook for shopping cart tab (top).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/WidgetZone.ShoppingCartBottom.cshtml Adds Store widget zone hook for shopping cart tab (bottom).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/WidgetZone.ReviewsTop.cshtml Adds Store widget zone hook for reviews tab (top).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/WidgetZone.ReviewsBottom.cshtml Adds Store widget zone hook for reviews tab (bottom).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/WidgetZone.ProductTop.cshtml Adds Store widget zone hook for personalized products tab (top).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/WidgetZone.ProductBottom.cshtml Adds Store widget zone hook for personalized products tab (bottom).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/WidgetZone.ProductPriceTop.cshtml Adds Store widget zone hook for product price tab (top).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/WidgetZone.ProductPriceBottom.cshtml Adds Store widget zone hook for product price tab (bottom).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/WidgetZone.OrdersTop.cshtml Adds Store widget zone hook for orders tab (top).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/WidgetZone.OrdersBottom.cshtml Adds Store widget zone hook for orders tab (bottom).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/WidgetZone.LoyaltyPointsTop.cshtml Adds Store widget zone hook for loyalty points tab (top).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/WidgetZone.LoyaltyPointsBottom.cshtml Adds Store widget zone hook for loyalty points tab (bottom).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/WidgetZone.InfoTop.cshtml Adds Store widget zone hook for customer info tab (top).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/WidgetZone.InfoBottom.cshtml Adds Store widget zone hook for customer info tab (bottom).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/WidgetZone.DetailsTabs.cshtml Adds Store widget zone hook for customer details tabs.
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/WidgetZone.BackInStockTop.cshtml Adds Store widget zone hook for back-in-stock tab (top).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/WidgetZone.BackInStockBottom.cshtml Adds Store widget zone hook for back-in-stock tab (bottom).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/WidgetZone.AddressesTop.cshtml Adds Store widget zone hook for addresses tab (top).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/WidgetZone.AddressesBottom.cshtml Adds Store widget zone hook for addresses tab (bottom).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/WidgetZone.AddressDetailsTop.cshtml Adds Store widget zone hook for address edit/create (top).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/WidgetZone.AddressDetailsBottom.cshtml Adds Store widget zone hook for address edit/create (bottom).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/CreateOrUpdate.TabReviews.cshtml Removes Store-specific implementation (now shared).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/CreateOrUpdate.TabProductPrice.cshtml Removes Store-specific implementation (now shared).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/CreateOrUpdate.TabProduct.cshtml Removes Store-specific implementation (now shared).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/CreateOrUpdate.TabOutOfStockSubscriptions.cshtml Removes Store-specific implementation (now shared).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/CreateOrUpdate.TabOrders.cshtml Removes Store-specific implementation (now shared).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/CreateOrUpdate.TabLoyaltyPoints.cshtml Removes Store-specific implementation (now shared).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/CreateOrUpdate.TabInfo.cshtml Removes Store-specific implementation (now shared).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/CreateOrUpdate.TabCurrentWishlist.cshtml Removes Store-specific implementation (now shared).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/CreateOrUpdate.TabCurrentShoppingCart.cshtml Removes Store-specific implementation (now shared).
src/Web/Grand.Web.Store/Areas/Store/Views/Customer/Partials/CreateOrUpdate.cshtml Removes Store-specific wrapper (now shared/overridden as needed).
src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/SendEmail.cshtml Makes SendEmail partial area-aware so it works for Admin and Store.
src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdateAddress.cshtml Adds widget-zone partial hooks around address editor.
src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabReviews.cshtml Uses area-aware routes + host widget-zone partials for reviews tab.
src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabProductPrice.cshtml Uses area-aware routes + host widget-zone partials for product price tab.
src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabProduct.cshtml Uses area-aware routes + host widget-zone partials for personalized products tab.
src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabOutOfStockSubscriptions.cshtml Uses area-aware routes + gates store-column to Admin only.
src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabOrders.cshtml Uses area-aware routes + gates store-column to Admin only.
src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabLoyaltyPoints.cshtml Uses area-aware routes + gates store selector fields to Admin only.
src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabInfo.cshtml Gates Admin-only fields based on Scope.DefaultStoreId and makes URLs area-aware.
src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabCurrentWishlist.cshtml Uses area-aware routes + gates store-column to Admin only + host widget zones.
src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabCurrentShoppingCart.cshtml Uses area-aware routes + gates store-column to Admin only + host widget zones.
src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.TabAddresses.cshtml Uses area-aware routes and adds host widget-zone hooks for addresses tab.
src/Web/Grand.Web.AdminShared/Views/AdminShared/Customer/Partials/CreateOrUpdate.cshtml Makes wrapper area-aware; gates notes/activity/documents tabs for Admin only; adds host widget-zone hook.
src/Web/Grand.Web.AdminShared/Startup/StartupApplication.cs Registers Customer data scopes + routed resolver in DI.
src/Web/Grand.Web.AdminShared/Services/StoreCustomerDataScope.cs Adds store-scoped customer access rules (store + registered-only).
src/Web/Grand.Web.AdminShared/Services/RoutedCustomerDataScope.cs Resolves correct Customer scope at request-time based on area route value.
src/Web/Grand.Web.AdminShared/Services/AdminCustomerDataScope.cs Adds Admin customer access rules (Sales Manager gating + deleted check).
src/Web/Grand.Web.AdminShared/Controllers/BaseCustomerManagementController.cs Introduces Admin-only customer management surface (impersonation, notes, export, etc.).
src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/WidgetZone.WishlistTop.cshtml Adds Admin widget zone partial (wishlist top) consumed by shared views.
src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/WidgetZone.WishlistBottom.cshtml Adds Admin widget zone partial (wishlist bottom) consumed by shared views.
src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/WidgetZone.ShoppingCartTop.cshtml Adds Admin widget zone partial (shopping cart top) consumed by shared views.
src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/WidgetZone.ShoppingCartBottom.cshtml Adds Admin widget zone partial (shopping cart bottom) consumed by shared views.
src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/WidgetZone.ReviewsTop.cshtml Adds Admin widget zone partial (reviews top) consumed by shared views.
src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/WidgetZone.ReviewsBottom.cshtml Adds Admin widget zone partial (reviews bottom) consumed by shared views.
src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/WidgetZone.ProductTop.cshtml Adds Admin widget zone partial (product top) consumed by shared views.
src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/WidgetZone.ProductPriceTop.cshtml Adds Admin widget zone partial (product price top) consumed by shared views.
src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/WidgetZone.ProductPriceBottom.cshtml Adds Admin widget zone partial (product price bottom) consumed by shared views.
src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/WidgetZone.ProductBottom.cshtml Adds Admin widget zone partial (product bottom) consumed by shared views.
src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/WidgetZone.OrdersTop.cshtml Adds Admin widget zone partial (orders top) consumed by shared views.
src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/WidgetZone.OrdersBottom.cshtml Adds Admin widget zone partial (orders bottom) consumed by shared views.
src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/WidgetZone.LoyaltyPointsTop.cshtml Adds Admin widget zone partial (loyalty points top) consumed by shared views.
src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/WidgetZone.LoyaltyPointsBottom.cshtml Adds Admin widget zone partial (loyalty points bottom) consumed by shared views.
src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/WidgetZone.InfoTop.cshtml Adds Admin widget zone partial (info top) consumed by shared views.
src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/WidgetZone.InfoBottom.cshtml Adds Admin widget zone partial (info bottom) consumed by shared views.
src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/WidgetZone.DetailsTabs.cshtml Adds Admin widget zone partial (details tabs) consumed by shared views.
src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/WidgetZone.BackInStockTop.cshtml Adds Admin widget zone partial (back-in-stock top) consumed by shared views.
src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/WidgetZone.BackInStockBottom.cshtml Adds Admin widget zone partial (back-in-stock bottom) consumed by shared views.
src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/WidgetZone.AddressesTop.cshtml Adds Admin widget zone partial (addresses top) consumed by shared views.
src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/WidgetZone.AddressesBottom.cshtml Adds Admin widget zone partial (addresses bottom) consumed by shared views.
src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/WidgetZone.AddressDetailsTop.cshtml Adds Admin widget zone partial (address details top) consumed by shared views.
src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/WidgetZone.AddressDetailsBottom.cshtml Adds Admin widget zone partial (address details bottom) consumed by shared views.
src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/SendEmail.cshtml Removes Admin-local SendEmail partial (now shared).
src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/CreateOrUpdateAddress.cshtml Removes Admin-local address partial (now shared).
src/Web/Grand.Web.Admin/Areas/Admin/Views/Customer/Partials/CreateOrUpdate.TabAddresses.cshtml Removes Admin-local addresses tab partial (now shared).
src/Tests/Grand.Web.Store.Tests/Controllers/CustomerControllerTests.cs Updates Store controller tests to cover only store-specific behavior + anti-smuggling constraints.
src/Tests/Grand.Web.Store.Tests/Controllers/CustomerControllerAttributeTests.cs Adds Store controller attribute/regression tests for the thin subclass.
src/Tests/Grand.Web.Admin.Tests/Services/CustomerViewModelServiceTests.cs Extends service tests for fields now conditionally rendered via scope-gated views.
src/Tests/Grand.Web.Admin.Tests/Controllers/StoreCustomerDataScopeTests.cs Adds unit tests for Store customer scope ownership + registered-only rules.
src/Tests/Grand.Web.Admin.Tests/Controllers/RoutedCustomerDataScopeTests.cs Adds unit tests ensuring correct per-area scope resolution and fail-closed behavior.
src/Tests/Grand.Web.Admin.Tests/Controllers/CustomerControllerAttributeTests.cs Adds Admin controller attribute/regression tests verifying thin subclass + base inheritance.
src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerManagementControllerTests.cs Adds tests for admin-only management actions (impersonation + 2FA warning hook).
src/Tests/Grand.Web.Admin.Tests/Controllers/BaseCustomerControllerTests.cs Adds broad shared-surface regression tests (list model prep, ownership gates, etc.).
src/Tests/Grand.Web.Admin.Tests/Controllers/AdminCustomerDataScopeTests.cs Adds unit tests for Admin customer scope (Sales Manager gating + deleted handling).
Review details

Suppressed comments (1)

src/Web/Grand.Web.AdminShared/Controllers/BaseCustomerManagementController.cs:142

  • CustomerNoteAdd mutates state but is missing [HttpPost], so it can be reached via GET and won’t trigger antiforgery validation. The Admin view posts to this endpoint already, so adding [HttpPost] should be compatible while closing the GET/CSRF surface.
  • Files reviewed: 89/89 changed files
  • Comments generated: 1
  • 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 view = result as ViewResult;
Assert.IsNotNull(view);
var model = view.Model as CustomerListModel;
Assert.IsTrue(model.UsernamesEnabled);

var view = result as ViewResult;
var model = view.Model as CustomerModel;
Assert.IsTrue(model.Active);
Assert.IsNull(customer);
var redirect = denied as RedirectToActionResult;
Assert.IsNotNull(redirect);
Assert.AreEqual("List", redirect.ActionName);
Assert.IsNull(result);
var redirect = denied as RedirectToActionResult;
Assert.IsNotNull(redirect);
Assert.AreEqual("List", 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("List", redirect.ActionName);

var redirect = result as RedirectToActionResult;
Assert.IsNotNull(redirect);
Assert.AreEqual("AddressEdit", redirect.ActionName);

var json = result as JsonResult;
var data = json.Value as DataSourceResult;
Assert.AreEqual(0, data.Total);

var json = result as JsonResult;
var data = json.Value as DataSourceResult;
Assert.AreEqual(0, data.Total);

var redirect = result as RedirectToActionResult;
Assert.IsNotNull(redirect);
Assert.AreEqual("List", redirect.ActionName);
@sonarqubecloud

sonarqubecloud Bot commented Sep 9, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
37.6% Coverage on New Code (required ≥ 80%)
5.0% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

KrzysztofPajak and others added 5 commits September 9, 2026 17:11
…tion

Admin's pre-consolidation Create(POST) required PermissionActionName.Edit
while every other Create action (and Store's original Create) required
.Create - same defect class already disclosed and fixed for GiftVoucher.
The consolidation harmonized on .Create but this went undisclosed in the
PR description and had no regression test.

Adds the disclosing comment (mirrors BaseGiftVoucherController's) and a
CreatePost_RequiresCreatePermission test mirroring
GiftVoucherControllerAttributeTests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nm3A9doUBAUWVM3FKHb45
…stomer-consolidation

# Conflicts:
#	src/Web/Grand.Web.AdminShared/Startup/StartupApplication.cs
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…dation' into arch001/phase20-customer-consolidation
…leteSelected

The prior 'Copilot Autofix' commit (5d3e60b) added [HttpPost] to
DeleteSelected but dropped the method's opening brace in the process,
which would not compile. Restored it; the [HttpPost] addition itself is
correct and matches the disclosed 'GET-reachable mutators' finding in
the PR description.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nm3A9doUBAUWVM3FKHb45
@KrzysztofPajak
KrzysztofPajak merged commit bc67ac4 into develop Sep 9, 2026
6 checks passed
@KrzysztofPajak
KrzysztofPajak deleted the arch001/phase20-customer-consolidation branch September 9, 2026 16:48
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