ARCH-001 Phase 5: Order controller/service consolidation (Admin/Store/Vendor) - #795
Merged
Merged
Conversation
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
…ric Global scope) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…Order> Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
…LoadAuthorizedOrder helper Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…re FilterOrderItems and vendor field gating into OrderViewModelService Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
…lService (superseded by AdminShared's) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
…ts/workflow region Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
…tDownload/ActivateDownload) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…re-soft-denies divergence Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
…bclasses Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
…per-host WidgetZone overrides if applicable Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
…el mismatch, C2 PdfInvoiceSelected vendor leak, I1 RoutedOrderDataScope CanView, I2 Vendor totals gating, I3 PDF/GoToOrderId test coverage, M2/M4) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
| Assert.IsNull(order); | ||
| var redirect = denied as RedirectToActionResult; | ||
| Assert.IsNotNull(redirect); | ||
| Assert.AreEqual("List", redirect.ActionName); |
| Assert.IsNull(resultOrder); | ||
| 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); |
| orderItem.Quantity = itemModel.Quantity; | ||
| orderItem.OpenQty = itemModel.Quantity; | ||
|
|
||
| if (orderItem.UnitPriceExclTax != itemModel.UnitPriceExclTaxValue) |
This was referenced Aug 26, 2026
Merged
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
Grand.Web.Admin(875 lines),Grand.Web.Store(858 lines), andGrand.Web.Vendor(220 lines)each shipped their own
OrderController— the same class of duplication ARCH-001 already fixedfor Product (PR #790), Category (PR #792), and Collection (PR #794). Vendor also carried its own
drifted duplicate
IOrderViewModelService/OrderViewModelService(613 lines vs AdminShared's1297), the same problem Product had before Phase 1.
Full design:
docs/superpowers/specs/2026-08-25-arch001-order-consolidation-design.mdFull plan:
docs/superpowers/plans/2026-08-25-arch001-order-consolidation.md(Both gitignored under
docs/superpowers/, same as every prior phase — on disk in this branch'shistory but not tracked by git.)
Order does not mirror Category/Collection's shape — it is closer to, and in places more
complex than, Product:
act on orders whose
SeIdmatches their own (CheckSalesManagerin the original code). NeitherStore nor Vendor has this. Reusing the generic always-true
GlobalAdminDataScope<Order>forAdmin would have silently dropped this restriction — a bespoke
AdminOrderDataScopewasrequired instead, the first entity in ARCH-001 where Admin itself needs non-trivial scope logic.
Orderis a plainBaseEntitywith a singleStoreIdfield, notIStoreLinkEntity— thegeneric
StoreAdminDataScope<TEntity>doesn't apply; a bespokeStoreOrderDataScopewas needed.OrderItems.Any(i => i.VendorId == ...)),not a flat field, and Vendor also filters line items inside a shared model-preparation method
(a vendor viewing a mixed-vendor order sees only its own lines) — a new interface member,
IAdminDataScope<TEntity>.FilterOrderItems, was added for this.differently-scoped copy of the same surface — the first entity needing a two-level base
controller (
BaseOrderControllerfor all three hosts,BaseOrderManagementController : BaseOrderControllerfor Admin+Store only).the underlying
Export/Deletepermission grants (used today by its PDF actions), so sharingthose three actions would have silently handed Store new working Excel-export/bulk-delete
routes as a side effect of consolidation.
Solution
IAdminDataScope<Order>implementations (none reusing the generic Global/Storescopes — a first for ARCH-001):
AdminOrderDataScope(Sales-Manager-aware),StoreOrderDataScope,VendorOrderDataScope(ownership overOrderItems, plusFilterOrderItems). Routed by a new3-branch
RoutedOrderDataScope(fails closed) — the first 3-branch routed scope in this effort;prior entities were 2-branch.
BaseOrderController(List/read/PDF, all three hosts) +BaseOrderManagementController : BaseOrderController(every mutating action, Admin+Store only) inGrand.Web.AdminShared. ALoadAuthorizedOrderhelper replaces ~20x-duplicated load-and-check logic per host. Everydenial redirects to
"List"(Order's own original controllers never redirected to"Edit"ondenial, unlike Category/Collection).
OrderControllers reduced to thin subclasses (each restating its own host's[Area]/[Authorize*]/[AuthorizeMenu]attributes, since the shared base classes arehost-agnostic by design).
ExportExcelAll/ExportExcelSelected/DeleteSelectedstay declaredonly on Admin's concrete controller.
original throws
ArgumentExceptionfor both not-found and Sales-Manager denial; Store'soriginal throws only for not-found and soft-denies (
Content("")/Json(false)) forstore-mismatch. Both preserved exactly.
IOrderViewModelService/OrderViewModelServicedeleted;AdminShared'sservice gained
scope-basedFilterOrderItemswiring plus vendor field-gating (Vendor neversaw
CustomerIp,AffiliateId,SalesEmployeeId,OrderTags,OrderStatuses, or the entireorder-totals block in its original pre-consolidation service — all now gated behind
scope.DefaultVendorId is null, closing a latent exposure risk of showing merchantProfittoa vendor via any future shared view).
Grand.Web.AdminShared/Views/AdminShared/Order/...; fixed the samedead-
<vc:admin-widget>-in-Store tag-helper bug already found and fixed in the Product andCategory phases (Store's
_ViewImports.cshtmlnever registers Admin's tag helper).superpowers:subagent-driven-development, task-by-task review ledgeravailable on request. Final whole-branch review (opus) found 2 Critical + 3 Important issues
that no per-task review caught — all fixed in one round, independently re-verified:
Areas/Vendor/Views/_ViewImports.cshtmlstill imported the now-deadGrand.Web.Vendor.Models.Ordersinstead ofGrand.Web.AdminShared.Models.Orders— everyVendor Order screen would have thrown a model-type
InvalidOperationExceptionat runtimedespite a clean build and 813 passing unit tests. Fixed with a scoped type-alias import
(a blanket import would have made
ShipmentModel/MerchandiseReturnModel/*ReportModelambiguous across Vendor's other, not-yet-consolidated views).
PdfInvoiceSelecteddropped the vendor-id argument its two sibling PDF actions correctlypass, leaking other vendors' line items into a vendor's exported PDF for mixed-vendor orders.
RoutedOrderDataScopedidn't forwardCanView(unlike the sibling routed scopes).GoToOrderId— exactly the class ofaction where a scope value threads into a third-party service argument, and exactly why the
PDF leak slipped through five rounds of task review. Added.
existing local MongoDB (built from this branch, three synthetic test orders inserted directly
and removed afterward, no git-tracked files touched): a store-exclusive order denied to the
other store's manager (direct URL and PDF) and permitted to its own store's manager; a
mixed-vendor order permitted to a vendor whose Products tab correctly showed only that vendor's
own line item; a zero-overlap order denied entirely, including from the vendor's own list; and
— the direct regression check for the PDF-leak fix above — the vendor's downloaded PDF invoice
for the mixed order, extracted with
pdftotext, contained only that vendor's own product line,confirmed byte-for-byte.
Breaking changes
None to
IOrderViewModelService's method signatures beyond internal scope wiring. Order notes'per-host denial-response shapes, the Sales-Manager restriction, and every other original behavior
are preserved exactly, with three disclosed, deliberate exceptions:
PdfInvoiceSelectednow appliesscope.HasAccessuniformly, which for Admin is asecurity-positive behavior change (closes a pre-existing gap where a Sales Manager could
previously export any order id via crafted
selectedIds, bypassing the Sales-Manager scopingevery other Admin action already enforced).
PrepareOrderDetailsModelnow (correctly) never populates order-totals/financialfields, matching what Vendor's original pre-consolidation service already did — this closes a
gap this branch itself introduced mid-flight (final-review finding), not a change from
pre-branch
developbehavior.AddressEdit's deadif (address == null) throw(never fires;addressis initialized vianew Address(), never null) and a PascalCase-vs-camelCase route-value casing quirk arepreserved byte-for-byte from both original Admin and Store controllers — verified against
develop's pre-branch source, not introduced by this branch, and deliberately left as-israther than silently "fixed" mid-consolidation.
Testing
dotnet build GrandNode.sln— 0 errors, 0 warnings.dotnet test src/Tests/Grand.Web.Admin.Tests(unfiltered) — 538 passed, 0 failed.dotnet test src/Tests/Grand.Web.Store.Tests(unfiltered) — 38 passed, 0 failed.dotnet test src/Tests/Grand.Web.Vendor.Tests(unfiltered) — 12 passed, 0 failed.dotnet test src/Tests/Grand.Mapping.Tests(unfiltered) — 234 passed, 0 failed.applicable denials correct, positive controls passed, PDF-export fix confirmed against the
actual generated PDF's extracted text, no unauthorized data exposure found.
superpowers:subagent-driven-development: 19 plan tasks + one final-review fixround, task-by-task review ledger available on request if useful for review context.
🤖 Generated with Claude Code