Skip to content

feat: admin hard invariants#910

Open
Si3r4dz wants to merge 11 commits into
006-admin-seller-valid-scopingfrom
007-admin-hard-invariants
Open

feat: admin hard invariants#910
Si3r4dz wants to merge 11 commits into
006-admin-seller-valid-scopingfrom
007-admin-hard-invariants

Conversation

@Si3r4dz

@Si3r4dz Si3r4dz commented May 4, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

Si3r4dz and others added 10 commits May 4, 2026 11:44
…rror

Parametrized parity spec covering six branches of the (fulfilled, returned)
matrix. describe.each runs the full case set against both helper copies
(core + admin mirror) so any drift between them fails one labeled arm.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure predicate over (fulfilled_quantity, returned_quantity) returning
{canRemove, minQty, reason}. Backend-side source of truth for
edit-order item mutation guards in require-editable-order-item.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure-function twin of the core helper, deliberately duplicated because
admin code cannot import from packages/core at runtime. Drift between
copies is caught by the parametrized parity spec
(integration-tests/http/order/unit/can-mutate-order-item.unit.spec.ts).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…T002)

Three behaviors on POST /admin/order-edits/:id/items/item/:item_id:
  1. Happy path — reducing to qty >= fulfilled+returned (=2) returns 200.
  2. Reducing to qty < fulfilled+returned returns 400 + code
     ITEM_CANNOT_REDUCE_BELOW_FULFILLED_RETURNED. Without the
     middleware, Medusa's built-in workflow rejects with a generic
     message but no admin-actionable code — the code is the load-bearing
     assertion, not the status.
  3. Side effect — after a rejected reduction, the order item quantity
     re-fetched from the order is unchanged.

Per-test seeding of seller, customer, region, sales channel, product,
stock location, shipping option mirrors the cancel-order-invariant
spec — storeHeaders publishable-key state is known to go stale across
tests, so each test owns its full fixture set.

itemA: qty=5, fulfilled=2, returned=0 → minQty=2. The returned-quantity
branch is exercised by the helper unit tests; wiring a Medusa return
through to processed state creates a competing orderChange that blocks
the order-edit session, covered separately by manual smoke + parity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Registers a pre-route middleware on
/admin/order-edits/:id/items/item/:item_id (no method, dispatches by
req.method internally — same pattern as the seller-valid family,
documented in middlewares.ts).

On POST: rejects body.quantity < fulfilled+returned with
INVALID_DATA(400) + code ITEM_CANNOT_REDUCE_BELOW_FULFILLED_RETURNED.
DELETE branch is wired but exercised in T003.

The middleware reads detail.fulfilled_quantity / detail.returned_quantity
through the order entity (entity: "order_item" rejects "detail" — that
property hangs off the order graph, not the item DTO). The pure
predicate getOrderItemMutationLimits is the source of truth shared with
the admin UI mirror.

The plan called for 422; Medusa 2.x has no UNPROCESSABLE_ENTITY in
MedusaError.Types, so INVALID_DATA→400 is the closest semantic match.
The error code remains the load-bearing assertion for admin UI mapping.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…path

The plan called for a DELETE matcher on
/admin/order-edits/:id/items/item/:item_id but Medusa 2.x ships only
a POST handler there (DELETE is on /items/:action_id, the action-id
path that handles newly-added items). A "remove" of an existing item
from the edit is expressed as POST quantity=0 and gated by the same
`requested < minQty` rule.

Two new tests cover the qty=0 semantics on the existing matcher:
  * POST quantity=0 on a plain item — allowed (200).
  * POST quantity=0 on an item with fulfilled history — rejected with
    ITEM_CANNOT_REDUCE_BELOW_FULFILLED_RETURNED (the REMOVE error code
    from the plan is unreachable on this path and is not emitted).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the admin mirror helper into order-edit-item.tsx so the UI
expresses the same invariant the backend middleware enforces:

* Qty input min = limits.minQty (was: only fulfilled_quantity).
* Qty input disabled when current quantity equals minQty.
* Remove action disabled when item is at minQty (cannot drop below
  fulfilled + returned).
* On reduction: client-side guard rejects quantity < minQty before
  hitting the network.
* On remove: drop quantity to minQty (was: only fulfilled_quantity).
* "Removed" badge fires when current quantity equals minQty (was:
  equals fulfilled_quantity).

Returned-quantity case is now handled symmetrically with fulfilled —
items with returned units cannot be dropped below fulfilled+returned
either through reduction or "remove".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n keys (T005 + T010)

Adds the new error code to the seller-scoped error mapper so a backend
rejection from `requireEditableOrderItem` surfaces in the admin UI as a
translated toast instead of the raw MedusaError message.

Three locale keys added across all 29 translation files (English copy
in non-Polish locales, Polish translation in pl.json):

* `orders.errors.itemCannotReduceBelowFulfilledReturned` — toast copy
  for the new backend rejection.
* `orders.activity.events.edit.declined` — admin order-activity
  timeline entry for declined order edits (FR-013).
* `orders.activity.events.edit.canceled` — same for canceled edits.

Per-locale translations beyond English + Polish are deferred to the
i18n pass; English copy is the safe fallback in the meantime.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…(T006 + T007)

The previous variantItemMap was built from `order.items` only, so a
newly-selected outbound variant (not yet on the order) returned
undefined from the map and the warning logic short-circuited to
"no warning" — masking inventory issues until backend rejected the
submit.

Refactor: merge the inventory snapshot into a single per-variant map
keyed by variant_id, populated for every variant in the current
outbound selection (original or newly-added). Each entry carries
manage_inventory + location_levels pulled in one productVariants.query
call. The variantItemMap (order.items only) is dropped — its sole
consumer was this warning logic.

Both claim and exchange outbound sections share the same shape and
now stay honest about inventory across the full selection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…08 + T009)

Three timeline bugs fixed in ClaimBody and ExchangeBody (audit P2):

1. ClaimBody.isCanceled was `!!claim.created_at` — every claim has
   `created_at`, so the Cancel button never rendered. Switched to
   `!!claim.canceled_at` (matches the existing ExchangeBody check).

2. Inbound/outbound translation keys were swapped in both bodies:
   `outboundItems` (additional_items count) was rendered under
   `itemsInbound`, and `inboundItems` (return items count) under
   `itemsOutbound`. Each label now uses its matching count source.

The plan called for separate `claim-body.tsx` / `exchange-body.tsx`
files; both bodies live in `order-timeline.tsx` in this codebase, so
both fixes ship as a single commit on that file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Si3r4dz Si3r4dz self-assigned this May 4, 2026
…limitation

Two pre-existing UX issues surfaced during 007 manual smoke and worth
shipping with the spec PR:

* `useCancelOrderEdit` hit DELETE `/admin/order-edits/:id/request`
  which Medusa 2.x doesn't expose (the path has POST only —
  request-confirmation). The OPTIONS preflight returned 401 because
  the auth middleware fired before any 404 handling, so the browser
  surfaced "Failed to fetch" with no actionable error. Switched to
  DELETE `/admin/order-edits/:id` (the actual cancel endpoint).
  Verified end-to-end: Cancel button on the pending edit banner now
  clears the edit and the section disappears.

* Documented in `order-edit-item.tsx` why removing an Added item is
  destructive (no Undo). Backend rejects POST quantity=0 on the
  action endpoint, so a soft-remove that keeps the row in the
  preview is not currently feasible. Tracked as MER-106 for a
  proper UX fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Si3r4dz Si3r4dz changed the title 007 admin hard invariants feat: admin hard invariants May 8, 2026
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.

1 participant