ARCH-001 Phase 7: PaymentTransaction controller consolidation (Admin/Store) - #797
Merged
KrzysztofPajak merged 9 commits intoAug 26, 2026
Merged
Conversation
…s (Store/Routed) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
…t region Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
…tTransactionController Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
…RefreshPage bug Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
…hin subclasses Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
…nShared, extract per-host WidgetZone satellites Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
…, strengthen Admin routing test, add popup denial tests - Add in-tree Razor comment to the 5 Store PaymentTransaction WidgetZone partials explaining the vc:store-widget vs vc:admin-widget fix (Finding 1) - Strengthen Admin's PaymentTransactionControllerRoutingTests to assert [Area(Constants.AreaAdmin)] and AuthorizeAdminAttribute, mirroring Store's equivalent test (Finding 2) - Add denied-branch tests for PartiallyRefundPopup and PartiallyPaidPopup POST overloads in BasePaymentTransactionControllerTests (Finding 3) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
| var result = _controller.List() as ViewResult; | ||
|
|
||
| Assert.IsNotNull(result); | ||
| Assert.IsInstanceOfType(result.Model, typeof(PaymentTransactionListModel)); |
| var result = await _controller.GoToOrderNumber(new PaymentTransactionListModel { OrderNumber = "123" }) as RedirectToActionResult; | ||
|
|
||
| Assert.IsNotNull(result); | ||
| Assert.AreEqual("List", result.ActionName); |
| var result = await _controller.GoToOrderNumber(new PaymentTransactionListModel { OrderNumber = "123" }) as RedirectToActionResult; | ||
|
|
||
| Assert.IsNotNull(result); | ||
| Assert.AreEqual("Edit", result.ActionName); |
| var result = await _controller.Edit("pt-1") as RedirectToActionResult; | ||
|
|
||
| Assert.IsNotNull(result); | ||
| Assert.AreEqual("List", result.ActionName); |
| Assert.IsNotNull(result); | ||
| var model = result.Model as PaymentTransactionModel; | ||
| Assert.IsNotNull(model); | ||
| Assert.AreEqual("pt-1", model.Id); |
| var result = await _controller.CapturePaymentTransaction("pt-1") as RedirectToActionResult; | ||
|
|
||
| Assert.IsNotNull(result); | ||
| Assert.AreEqual("List", result.ActionName); |
| var result = await _controller.CapturePaymentTransaction("pt-1") as RedirectToActionResult; | ||
|
|
||
| Assert.IsNotNull(result); | ||
| Assert.AreEqual("Edit", result.ActionName); |
|
|
||
| Assert.IsNotNull(result); | ||
| var resultModel = result.Model as PaymentTransactionModel; | ||
| Assert.IsTrue(resultModel.RefreshPage); |
|
|
||
| Assert.IsNotNull(result); | ||
| var resultModel = result.Model as PaymentTransactionModel; | ||
| Assert.IsFalse(resultModel.RefreshPage); |
| var result = await _controller.PartiallyRefundPopup("pt-1", false, model) as RedirectToActionResult; | ||
|
|
||
| Assert.IsNotNull(result); | ||
| Assert.AreEqual("List", result.ActionName); |
…dation Resolves the expected additive conflict in Grand.Web.AdminShared/Startup/StartupApplication.cs between this phase's IAdminDataScope<PaymentTransaction> registration and Shipment's (PR #796, merged to develop) IAdminDataScope<Shipment> registration — both blocks kept, no logic changed on either side. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
- Add [AutoValidateAntiforgeryToken] to BasePaymentTransactionController itself, matching every other Base*Controller in this codebase (BaseCollectionController, BaseOrderController, BaseShipmentController). Runtime CSRF protection was already in place via the attribute restated on both concrete Admin/Store subclasses, so this is not a functional fix — but CodeQL's cross-project analysis doesn't follow the attribute across the base/derived project boundary, which is exactly the false-positive class BaseCollectionController's own doc comment already calls out and works around the same way. Resolves CodeQL alerts #1503-1513 (List, GoToOrderNumber, Capture/MarkAsPaid/Refund/RefundOffline/Void/VoidOffline, PartiallyRefundPopup, PartiallyPaidPopup, Delete). - Add null-forgiving (!) operators after Assert.IsNotNull(...) guards throughout BasePaymentTransactionControllerTests.cs. MSTest's Assert.IsNotNull narrows at runtime but CodeQL's C# nullable-flow analysis doesn't recognize it as a null-check, flagging the subsequent property access as a possible null dereference. Test-code-only, no behavior change; resolves 10 github-code-quality alerts plus 2 unreported instances of the identical pattern found by inspection. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
KrzysztofPajak
deleted the
arch001/phase7-paymenttransaction-consolidation
branch
August 26, 2026 15:59
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(471 lines) andGrand.Web.Store(524 lines) each shipped their ownPaymentTransactionController— the same class of duplication ARCH-001 already fixed forProduct (PR #790), Category (PR #792), Collection (PR #794), and Order (PR #795). No
Grand.Web.Vendorcontroller exists for this entity.Full design:
docs/superpowers/specs/2026-08-26-arch001-paymenttransaction-consolidation-design.mdFull plan:
docs/superpowers/plans/2026-08-26-arch001-paymenttransaction-consolidation.md(Both gitignored under
docs/superpowers/, same as every prior phase — on disk in this branch'shistory but not tracked by git.)
Different shape from every prior entity: only Admin and Store have a controller for
PaymentTransaction— no Vendor screen, and no separate combined-host controller either. Thatruled out the Vendor branch entirely, but did not rule out the routing-collision problem: the
combined
Grand.WebhostProjectReferences bothGrand.Web.AdminandGrand.Web.Storedirectly, so both hosts' controllers and
StartupApplicationregistrations load into the sameprocess. The first draft of the spec missed this and proposed per-host DI registration; caught
before implementation and corrected to a 2-branch
RoutedPaymentTransactionDataScope(fail-closed), same shape as
RoutedCategoryDataScope/RoutedCollectionDataScope— Category andCollection hit this identical shape despite also having no Vendor screen.
Solution
StorePaymentTransactionDataScope(bespoke —PaymentTransactionis a plainBaseEntitywith ascalar
StoreId, notIStoreLinkEntity) + reuse of the existing genericGlobalAdminDataScope<PaymentTransaction>for Admin (no restriction found in Admin's originalcontroller) +
RoutedPaymentTransactionDataScope, all registered centrally inGrand.Web.AdminShared/Startup/StartupApplication.cs.BasePaymentTransactionControllerinGrand.Web.AdminShared— both hosts' actionsurfaces were already an exact 1:1 match (List, GoToOrderNumber, Edit, Capture, MarkAsPaid,
Refund/RefundOffline, Void/VoidOffline, PartiallyRefundPopup ×2, PartiallyPaidPopup ×2, Delete —
15 actions total). A
LoadAuthorizedPaymentTransactionhelper replaces the repeated"load, check
StoreId != StaffStoreId, redirect if denied" boilerplate that Store's originalcontroller inlined at every single action site; Admin's original had no such check at all
(
GlobalAdminDataScope.HasAccessis always true, so behavior is unchanged for Admin).reconcile at that layer, unlike every prior phase.
PaymentTransactionControllers reduced to thin subclasses (each restating its ownhost's
[Area]/[Authorize*]/[AuthorizeMenu]attributes).Grand.Web.AdminShared/Views/AdminShared/PaymentTransaction/...; hardcodedConstants.AreaAdmin/Constants.AreaStorereplaced with the existingViewContext.RouteData.Values["area"]idiom (Category/Edit.cshtmlprecedent). Widget zonesextracted into per-host
WidgetZone.*.cshtmlsatellites.superpowers:subagent-driven-development, task-by-task review ledgeravailable on request. Final whole-branch review (opus) found 0 Critical, 2 Important (see
Breaking changes below), several Minor — all addressed in one fix round, independently
re-verified clean.
MongoDB (real order/payment-transaction data, no synthetic seeding needed): Admin
List/Edit render correctly with real data; the partial-paid popup's full write path exercised
end-to-end (
AmountToPaid=50submitted →PartiallyPaidOfflineCommandexecuted →Status/Paid amountcorrectly recomputed on reload, confirming the RefreshPage fix below firesa real page reload, not just a 200 response); Store's List correctly scoped to only its own
store's transactions; a direct cross-store access attempt via a known other-store transaction id
redirected to List rather than showing it (the direct regression check for
LoadAuthorizedPaymentTransaction'sscope.HasAccessgate replacing the per-action inlinechecks); Store's own transaction opened correctly under the
/Store/area.Breaking changes
None to any public method signature. Three disclosed, deliberate behavior changes, all judged
safe and kept after final review:
PartiallyRefundPopup/PartiallyPaidPopupPOSTactions set
ViewBag.RefreshPage = trueon success, but the view (identical on both hosts)reads
Model.RefreshPage— a dead write that meant Admin's popups never actually refreshed theparent page after a successful partial refund/paid. Store's original already set
model.RefreshPage = truecorrectly. Unified onto Store's already-correct behavior; verifiedlive (see Testing).
<vc:admin-widget>→<vc:store-widget>fix. Store's original 5 PaymentTransactionviews all called
<vc:admin-widget>, a tag helper never registered in the Store app(
Grand.Web.Store.csprojhas noProjectReferencetoGrand.Web.Admin) — inert markup thatnever rendered anything. Fixed to
<vc:store-widget>to match every other consolidated entity'sconvention; documented in-tree with a comment on each of the 5 new Store WidgetZone satellite
files. Can only add previously-absent widget-zone hooks on Store's PaymentTransaction pages,
never remove working behavior.
Deletedenial redirect target. Store's originalDeleteaction redirected toRedirectToAction("List", "MerchandiseReturn")on a store-mismatch denial — an evidentcopy-paste bug from another controller. The consolidated version redirects to the
PaymentTransaction list, an unavoidable consequence of centralizing the denial path through
LoadAuthorizedPaymentTransaction. Both are list pages; no security or data-exposure delta.Testing
dotnet build GrandNode.sln— 0 errors (4 pre-existing warnings, unrelated to this diff,already present on
develop).dotnet test src/Tests/Grand.Web.Admin.Tests(unfiltered) — 565 passed, 0 failed.dotnet test src/Tests/Grand.Web.Store.Tests(unfiltered) — 42 passed, 0 failed.correctly on both hosts with real data, full partial-paid write path verified end-to-end
including the RefreshPage fix's actual observable effect, cross-store denial confirmed via a
direct URL access attempt with a known other-store transaction id.
superpowers:subagent-driven-development: 7 plan tasks + one final-review fixround (2 Important + 1 cheap Minor addressed: undocumented widget-tag fix now has an in-tree
comment on all 5 affected files; Admin's routing test strengthened to match Store's
[Area]/[AuthorizeAdmin]assertions per the plan's own spec; denied-branch tests added forthe two popup POST actions) — task-by-task review ledger available on request if useful for
review context.
🤖 Generated with Claude Code