Skip to content

ARCH-001 Phase 23: Shipping sub-entities (Warehouse/ShippingMethod/DeliveryDate/PickupPoint) consolidation - #824

Merged
KrzysztofPajak merged 10 commits into
developfrom
arch001/phase23-shipping-consolidation
Sep 11, 2026
Merged

KrzysztofPajak merged 10 commits into
developfrom
arch001/phase23-shipping-consolidation

Conversation

@KrzysztofPajak

Copy link
Copy Markdown
Member

Type: refactor (architecture consolidation, ARCH-001)

Issue

Grand.Web.Admin and Grand.Web.Store each carried a duplicate ShippingController handling
ShippingMethod/Warehouse/DeliveryDate/PickupPoint CRUD independently — the same class of
duplication ARCH-001 has been eliminating across ~22 other entities (Product, Category, Order,
TaxCategory, EmailAccount, etc.). A bug fix or security patch to one host's shipping-entity logic
had no mechanism to reach the other; Store's tenant-isolation checks and Admin's global-scope
behavior had already drifted independently.

Solution

Consolidated the four entity-CRUD regions (Methods/DeliveryDates/Warehouses/PickupPoints)
into shared Grand.Web.AdminShared base controllers, each backed by a per-entity
IAdminDataScope<TEntity> scope class (Store{Entity}DataScope + Routed{Entity}DataScope,
reusing GlobalAdminDataScope<TEntity> for Admin) — the same pattern used by every prior ARCH-001
phase. Providers/Settings/Restrictions regions are untouched and remain duplicated per host
(not entity-shaped, out of scope).

New to this phase: since a C# class can only have one base class and this phase needed four
different base controllers, each host's single ShippingController was split into 5 concrete
controllers (the original, shrunk to Providers/Settings/Restrictions, plus one thin controller
per entity). Each new controller carries [Route("[area]/Shipping/[action]")] so every URL stays
byte-identical to today (/Admin/Shipping/Warehouses, etc.) despite the new class names — the
existing, untouched .cshtml views' Url.Action/asp-action/asp-controller references keep
working unmodified. No view files were changed anywhere in this phase.

A real Critical bug was found and fixed mid-branch, via the live smoke test, not any of the 8
per-task reviews or 1547 unit tests
: Razor's view-engine resolves the view-search folder from a
controller's C#-class-derived name, independent of the [Route] template — so WarehouseController
searched Views/Warehouse/... instead of the actual Views/Shipping/..., and every GET
view-returning action across all 8 new controllers threw view not found. Fixed with a small,
reusable mechanism: a [SharedViewFolder("Shipping")] attribute (in Grand.Web.Common, respecting
this codebase's existing dependency direction — Grand.Web.AdminShared depends on
Grand.Web.Common, never the reverse) applied once per abstract base controller, read by a new
IControllerModelConvention registered in the single shared AddGrandMvc extension. Verified live
against a real running instance (10/10 URLs returning 200, including the untouched
Providers/Settings regression check) before continuing. The final whole-branch review
independently found this fix also closes an unrelated fail-open hazard: AuthorizeMenuFilter
resolves the admin sitemap by the request's RouteData["controller"] value, so without the rename
every split entity action would have silently skipped menu-permission enforcement — a real benefit
nobody set out to fix.

Disclosed behavior notes (all verified non-breaking, all Minor)

  • Admin's DeleteWarehouse success-message resource key was changed from a lowercase-cased key
    (Admin.Configuration.Shipping.warehouses.Deleted) to the correct PascalCase form
    (...Warehouses.Deleted). Correction to an earlier internal note: this is a cosmetic no-op,
    not a user-visible fix — TranslationService.GetResource lowercases the lookup key before
    matching, so both spellings already resolved to the identical resource string. No behavior
    changed.
  • Admin's DeleteDeliveryDate dropped a dead, unreachable if (ModelState.IsValid) wrapper (the
    action binds only a route string id, so ModelState was always valid; the else branch never
    ran). Behavior-neutral, now matches all three sibling entities' shape.
  • GetAllShippingMethods(storeId) returns own-store-or-global; GetAllWarehouses/
    GetAllDeliveryDates/GetAllPickupPoints(storeId) return own-store-only. This is existing,
    pre-consolidation, per-entity service behavior, preserved exactly — confirmed live (store1's
    ShippingMethod grid showed its own method plus 3 seeded global ones; its Warehouse/DeliveryDate
    grids showed only its own).

Testing

Automated: dotnet build GrandNode.sln (0 errors); Grand.Web.Admin.Tests 1413/1413,
Grand.Web.Store.Tests 136/136, Grand.Web.Common.Tests 35/35 — all per-project runs, all green.
Includes new regression coverage for the [SharedViewFolder] mechanism itself (attribute-presence
reflection tests + a direct IControllerModelConvention.Apply unit test), added after the final
whole-branch review flagged that the one Critical bug this branch produced had zero automated
defense.

Live, end-to-end, against a real Kestrel instance + a live MongoDB dev database:

  1. store1 created one of each entity (Warehouse, ShippingMethod, DeliveryDate, and a PickupPoint
    referencing the new warehouse) — all 4 creates succeeded.
  2. store2 attempted Edit (GET+POST) and Delete on all 4 of store1's entities via crafted
    antiforgery-tokened requests — 9/9 denied (redirected to the entity's list, zero mutation,
    re-verified by reading store1's data back unchanged).
  3. store1 positive control: renamed its own warehouse — succeeded, re-verified.
  4. Admin's grids correctly show entities from every store plus global ones, unfiltered.
  5. Providers/Settings/Restrictions — the three untouched regions — all still return 200 on
    both hosts (confirms the controller split didn't disturb DI resolution or routing there).
  6. PickupPoint's AvailableWarehouses dropdown on Store's create form correctly showed only
    store1's own warehouse, matching the unit-tested scoping.
  7. Every rendered page's generated link (asp-controller="Shipping" tag helpers and Kendo grid
    row templates, across every tab, both hosts) resolved correctly post-fix — closes the one open
    concern the view-folder-fix's own reviewer flagged as unexercised by that fix's narrower check.

All synthetic test data was deleted afterward and re-verified empty.

Review

9 task-level reviews (spec + quality, all Approved, zero Critical/Important beyond the two
plan-mandated/disclosed items above) + 1 final whole-branch review (opus) — verdict "Ready to
merge: With fixes" — + 1 fix wave (regression tests for the view-folder fix, removed a field left
dead by two earlier tasks' incremental trims, minor using cleanup, test coverage top-up) + 1 scoped
re-review confirming all findings addressed with no new breakage.

🤖 Generated with Claude Code

KrzysztofPajak and others added 10 commits September 10, 2026 00:13
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session
…e cutover

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

- Add BasePickupPointController in Grand.Web.AdminShared consuming
  IAdminDataScope<PickupPoint> (StorePickupPointDataScope /
  RoutedPickupPointDataScope from Task 7), with the address-preparation
  loop inlined independently (matching Admin's original two-copy shape;
  Store's shared PrepareAddressModel helper was an incidental artifact,
  not a scope boundary).
- Add concrete PickupPointController for Admin and Store, preserving
  existing routes via [Route("[area]/Shipping/[action]")].
- Trim both hosts' ShippingController.cs down to Providers/Settings/
  Restrictions only, removing PickupPoint/Warehouse service dependencies
  no longer used by this file.
- Fix pre-existing Grand.Web.Store.Tests ShippingControllerTests
  constructor call to match the trimmed ShippingController signature.
- Add attribute/routing tests for both hosts' PickupPointController and
  BasePickupPointControllerTests covering global/store scope plus the
  store-scoped AvailableWarehouses dropdown restriction.

This is the fourth and final entity split of ARCH-001 Phase 23, completing
the ShippingController Admin/Store consolidation.
…controllers

Task 9's live smoke test found Critical: all 16 GET view-returning actions
across the 8 new concrete controllers (Warehouse/ShippingMethod/DeliveryDate/
PickupPoint x Admin/Store) threw InvalidOperationException 'view not found'.
Root cause: [Route("[area]/Shipping/[action]")] only affects URL matching;
Razor's view-location convention derives the view-search folder from
ControllerModel.ControllerName (class-name-derived), not the route template,
so e.g. WarehouseController searched Views/Warehouse/... instead of the
actual, untouched Views/Shipping/... folder.

Fix: new [SharedViewFolder(string)] attribute + IControllerModelConvention
in Grand.Web.Common (which Grand.Web.AdminShared already depends on, never
the reverse), registered once in the shared AddGrandMvc. Applied once per
abstract Base{Entity}Controller (inherited, so both hosts' concrete
subclasses are covered without repetition).

Verified: dotnet build 0 errors; Grand.Web.Admin.Tests 1411/1411 and
Grand.Web.Store.Tests 136/136 pass; live Kestrel run with authenticated
admin session confirms all 8 previously-broken URLs plus 2 regression-check
URLs (Providers/Settings) now return 200.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DjfBG3opo33qNHQYVbgH9Q
- Add regression coverage for SharedViewFolderAttribute: reflection tests
  asserting the 4 abstract base controllers carry [SharedViewFolder("Shipping")],
  and unit tests for SharedViewFolderControllerNameConvention.Apply. Adds a
  test-only project reference from Grand.Web.Common.Tests to Grand.Web.AdminShared.
- Remove dead ILanguageService from Admin and Store ShippingController (moved
  out by earlier tasks, never cleaned up); update Grand.Web.Store.Tests
  ShippingControllerTests constructor call to match.
- Remove stale Grand.Web.AdminShared.Extensions.Mapping / Grand.Web.Common.DataSource
  usings from Admin and Store WarehouseController.
- Top up StoreShippingMethodDataScopeTests to the standard 5-case shape
  (HasAccess_MismatchedStoreId_False, HasAccess_NullEntity_False).

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

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

var result = await controller.EditDeliveryDate("dd-2") as RedirectToActionResult;

Assert.IsNotNull(result);
Assert.AreEqual("DeliveryDates", result.ActionName);

var model = result!.Model as DeliveryDateModel;
Assert.IsNotNull(model);
Assert.AreEqual("#000000", model.ColorSquaresRgb);

var model = result!.Model as DeliveryDateModel;
Assert.IsNotNull(model);
Assert.AreEqual("#000000", model.ColorSquaresRgb);

var model = result!.Model as DeliveryDateModel;
Assert.IsNotNull(model);
Assert.AreEqual("#ff0000", model.ColorSquaresRgb);
var result = await controller.EditPickupPoint("pp-2") as RedirectToActionResult;

Assert.IsNotNull(result);
Assert.AreEqual("PickupPoints", result.ActionName);
var result = await controller.EditMethod("sm-2") as RedirectToActionResult;

Assert.IsNotNull(result);
Assert.AreEqual("Methods", result.ActionName);
var result = await controller.EditWarehouse("wh-2") as RedirectToActionResult;

Assert.IsNotNull(result);
Assert.AreEqual("Warehouses", result.ActionName);
@KrzysztofPajak
KrzysztofPajak merged commit 9b13456 into develop Sep 11, 2026
6 checks passed
@KrzysztofPajak
KrzysztofPajak deleted the arch001/phase23-shipping-consolidation branch September 11, 2026 00:55
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