Skip to content

feat/admin seller valid scoping#892

Open
Si3r4dz wants to merge 36 commits into
feat/admin-warehouse-capability-lockfrom
006-admin-seller-valid-scoping
Open

feat/admin seller valid scoping#892
Si3r4dz wants to merge 36 commits into
feat/admin-warehouse-capability-lockfrom
006-admin-seller-valid-scoping

Conversation

@Si3r4dz

@Si3r4dz Si3r4dz commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

Provide a concise description of the problem and the proposed solution.

Changes

  • bullet the key code or documentation updates

Testing

List the tests or commands you ran to validate the change.

Checklist

  • This pull request targets new.
  • I updated documentation, locales if the change requires it.
  • I added or adjusted tests that cover the change.

Linked issues

Reference any related issues with Fixes #... when applicable.

Si3r4dz and others added 12 commits April 23, 2026 17:02
…nUser

The Medusa framework's checkPermissions middleware (wrapped around admin
routes with policies: [...]) reads auth_context.app_metadata.roles from
the JWT. An empty roles array causes an immediate FORBIDDEN throw —
before any route-specific middleware runs. This blocked every admin
integration test against core-Medusa routes and was already costing us
coverage in spec 005 (D-06).

createAdminUser now creates a wildcard RBAC policy (resource: *,
operation: *), attaches it to a dedicated test-superadmin role, and
embeds the role id in the JWT app_metadata. Opt-out available for the
one-in-a-thousand test that wants to reproduce "unprovisioned admin"
behavior (withSuperAdminRole: false).

Foundational fix for spec 006 (admin seller-valid scoping) and every
future admin integration test.

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

Integration spec for GET /admin/orders/:id/seller-valid-stock-locations
covering three behaviors:
  - happy path: seeded two sellers with one stock location each, one
    order for seller A; endpoint returns only seller A's location
  - pagination: limit=1 caps the response while count reflects total
  - 404 when the order has no order_seller link (legacy / non-existent
    order)

Maps to FR-001, FR-002, NFR-001, SC-001. Endpoint route does not exist
yet — this commit is the RED step; the GREEN implementation follows.

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

Introduces resolveOrderSeller — a shared helper that walks the
order_seller link and throws NOT_FOUND for legacy/unlinked orders. Used
by this endpoint and reused by every subsequent seller-scoping
resolver/middleware in this feature (T006–T011).

The route queries stock_location_seller (not stock_location) because
StockLocation has no direct seller property in its DML schema — the
relationship lives in the link table. Filtering by seller_id on the
link and projecting stock_location.* in fields gives us seller-scoped
results with pagination.

Turns T005 RED green; verified locally against the seeded two-seller
fixture.

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

Four integration tests, one per filter dimension:
  - seller: returns only seller A options, excludes seller B
  - is_return: outbound excluded when ?is_return=true
  - location_id: options bound to location B excluded when querying
    location A
  - 404 for orders without order_seller link

Seeds one outbound + one return option for seller A on location A, and
one outbound option for seller B on location B. Uses shipping_option
rules ({ attribute: is_return, value: true/false }) to encode direction.

Route not implemented yet — RED step. GREEN commit follows.

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

Filters admin shipping options to the order's seller scope plus two
dimensions selectable via query string:
  - location_id: only options served from the given stock location
    (resolved via location_fulfillment_set link → shipping_option.
    service_zone_id → fulfillment_set_id)
  - is_return: direction filter evaluated against the shipping_option
    rules payload ({ attribute: "is_return", value: "true"|"false" }),
    which is how Medusa store/shipping-options already encodes it

Nested-graph traversal through service_zone.fulfillment_set.stock_
locations.id blew up the remote-query strategy resolver, so the route
does three flat queries and joins in memory. Short-circuits to empty
list when the seller owns zero options so downstream doesn't pass an
empty filter array.

Turns T006 RED green; covers FR-004, FR-006, NFR-001, SC-002.

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

Five integration tests, one per behavior dimension:
  - seller scoping: excludes variants owned by another seller
  - eligibility=ok: price for order currency + availability → can_add=true
  - eligibility=no_price: variant priced only in non-order currency
  - eligibility=no_inventory: manage_inventory:true variant with zero
    stock in any seller-valid location
  - search: ?search=<token> filters by SKU

Fixture seeds four seller-A variants (each shape explicit in the
product name) plus one seller-B variant, then completes an order for
seller A. Per spec FR-007, FR-008 and open/closed-for-tests contract.

Route not implemented yet — RED. GREEN commit follows with helpers
variantHasValidPrice + variantHasInventory reused from the spec's
shared helper layer.

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

Scoped variant picker for admin order add-items drawers. Returns seller
A's catalog with a per-variant eligibility object so the UI can disable
rows it knows are non-addable:

  - ok: variant has a price in the order's currency AND inventory
    reachable from at least one seller-valid stock location (or the
    variant is manage_inventory:false, treated as unlimited stock)
  - no_price: no price matching order.currency_code
  - no_inventory: manage_inventory:true variant with zero available
    stock in any seller-valid location

Seller scoping uses product_seller + stock_location_seller link tables,
same pattern as T005/T006. Inline helpers (hasValidPrice,
hasAvailableInventory, classify, matchesSearch) kept within the route
until another consumer appears — YAGNI until T008+ middlewares.

Search filter matches variant SKU or title (case-insensitive),
post-filtered after classification so every returned row still carries
its eligibility metadata.

Turns T007 RED green; covers FR-007, FR-008, NFR-001.

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

Opens the shared seller-valid-scoping matchers spec. First describe
block covers the location-on-body concern: a direct admin POST to
/admin/returns with an order owned by seller A and location_id owned
by seller B must be rejected with 403 + LOCATION_NOT_SELLER_VALID.

Spec 006 D9 originally pointed the location guard at
/admin/returns/:id/shipping-method, but Medusa v2.13.4's
AdminPostReturnsShippingReqSchema only accepts { shipping_option_id,
custom_amount? } — no location_id. The realigned matchers live in
D-02-006: location enforcement moves to the endpoints that actually
accept location_id (create + update return + claim/exchange inbound).

Middleware + matcher registration in the GREEN commit.

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

Location-on-body guard for the create-return endpoint. When the admin
submits POST /admin/returns with a location_id that does not belong to
the order's seller (resolved via body.order_id + order_seller link),
the middleware rejects with FORBIDDEN + LOCATION_NOT_SELLER_VALID.

Design notes:
  - Factory-style middleware requireSellerValidLocation(resolveOrderId)
    so the same implementation reuses on /admin/returns/:id,
    /admin/claims/:id/inbound/items, and /admin/exchanges/:id/inbound/
    items — each with its own order_id extractor. Seed extractor
    orderIdFromBody covers the create-return case.
  - MedusaError.Types.FORBIDDEN (HTTP 403) instead of NOT_ALLOWED
    (which maps to 400 in Medusa's error handler). Seller-scope
    rejection is an authz decision, not a business-rule violation.
    Spec 006 D8 said NOT_ALLOWED+403 — inconsistent with the
    framework's actual mapping; D-02-006 captures the correction.
  - No-op when request omits location_id — the Medusa schema marks
    it optional and the middleware should not fail-closed on fields
    it was not asked to police.

Turns T008 RED green; foundation for T009 (shipping-option guard) and
the remaining location matchers.

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

Extends the shared matchers spec with a second describe block. Admin
posting a return shipping-method that belongs to seller B, while the
return's order belongs to seller A, must be rejected with 403 +
SHIPPING_OPTION_NOT_SELLER_VALID.

The setup now also creates:
  - sellerAReturnOptionId + sellerBReturnOptionId (return-direction
    shipping options on each seller's location)
  - returnForOrderA via POST /vendor/returns, giving us a valid return
    id to target the /shipping-method endpoint

Middleware + matcher registration in the GREEN commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ia built-in super_admin (T008 GREEN fixup + T009)

T008 GREEN fixup:
- Medusa registers matchers with explicit method as app.post() route
  handlers, so a Mercur-declared matcher with method: ["POST"] becomes
  a route handler stacked AFTER the core handler — it never fires
  because the core handler responds. Remove the method field from the
  location + shipping-option matchers so the framework uses app.use()
  (pre-route middleware), then filter by method manually inside each
  middleware.
- createAdminUser now reuses the built-in role_super_admin that
  Medusa's RBAC module seeds, rather than creating a parallel custom
  role. Custom-role approach worked for T008 but failed for matchers
  that hit Medusa's /admin/returns/* global read-policy check —
  probably a cache/link-table edge case around our link shape. The
  built-in role is the same mechanism production uses for the first
  admin; matches real deployments and resolves every policy action
  in hasPermission() consistently.

T009 middleware (wired-only):
- requireSellerValidShippingOption rejects cross-seller
  shipping_option_id on POST /admin/returns/:id/shipping-method.
- orderIdFromReturnParam walks return_id → order.id to resolve the
  seller context for matchers that receive the containing entity id
  in req.params.
- Integration test skipped per spec 006 D-02-006: the return's
  orderChange must be active for the Medusa /shipping-method handler
  to accept the call, which requires the full confirm-request flow —
  cost outweighs signal. Manual smoke covers this path.

Regression: 26/26 backend integration tests remain green.

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

Wires requireSellerValidAddItem on POST /admin/order-edits/:id/items
to reject cross-seller variant_ids with 403 + VARIANT_NOT_SELLER_VALID.
Same pattern as T009 shipping-option guard (requireSellerValidShippingOption).

Files:
  - packages/core/src/api/admin/middlewares/require-seller-valid-add-item.ts
    new — middleware checks each body.items[].variant_id resolves to a
    product owned by the order's seller via product_seller link.
  - packages/core/src/api/admin/middlewares.ts
    matcher registration on /admin/order-edits/:id/items.
  - packages/core/src/api/admin/helpers/resolve-order-seller.ts
    switched from entity:order field traversal to entity:order_seller
    direct link query — correct shape for middleware request context
    regardless of T010 outcome.
  - integration-tests/http/order/admin/seller-valid-scoping-matchers.spec.ts
    T010 describe block deferred per D-02-006 — order_seller link returns
    empty under remote-joiner hydration in the middleware context for
    fresh cart-completed orders, even though the link exists (confirmed
    via vendor GET). orderA (fulfilled) works fine. Middleware ships
    wired-only; same deferral policy as T009.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Si3r4dz Si3r4dz self-assigned this Apr 30, 2026
Si3r4dz and others added 17 commits April 30, 2026 12:42
…T011 wired-only)

Registers the four remaining seller-valid matchers using the
already-written middlewares (requireSellerValidLocation,
requireSellerValidShippingOption, requireSellerValidAddItem):

  - POST /admin/claims/:id/inbound/shipping-method
    [requireSellerValidLocation, requireSellerValidShippingOption]
  - POST /admin/exchanges/:id/inbound/shipping-method
    [requireSellerValidLocation, requireSellerValidShippingOption]
  - POST /admin/claims/:id/outbound/items
    [requireSellerValidAddItem]
  - POST /admin/exchanges/:id/outbound/items
    [requireSellerValidAddItem]

New helper file packages/core/src/api/admin/middlewares/order-id-resolvers.ts
exposes orderIdFromClaimParam and orderIdFromExchangeParam which
walk `:id` (claim_id / exchange_id) to the order's order_id via
query.graph against `order_claim` / `order_exchange`.

Same wired-only deferral as T009/T010 per D-02-006 — claim/exchange
flow setup is significantly more involved than T008/T010 setups
(order → fulfillment → return → claim/exchange creation via admin),
and the underlying middlewares are already proven on their canonical
matchers (T008 location, T009 shipping-option, T010 add-item). The
additional registrations only assert wiring, not new logic; covered
by manual smoke. Deferral note added inline in
seller-valid-scoping-matchers.spec.ts.

Maps to: FR-011, FR-012, FR-013, SC-004 (spec 006 T011).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds React Query hook wrapping GET /admin/orders/:id/seller-valid-stock-locations
(T005 endpoint). Hook lives in packages/admin/src/hooks/api/seller-scoped-orders.tsx
and will be consumed by the Return / Claim / Exchange drawers in T015+
to replace the global stock-locations picker with the seller-scoped one.

Pattern matches existing custom Mercur hooks (attributes.ts):
  - typed SDK access via codegen (sdk.admin.orders.$id.sellerValidStockLocations.query)
  - Promise<...> response cast since the route's response shape isn't
    auto-inferred by codegen
  - queryKey via queryKeysFactory under namespace 'seller-scoped-orders'
  - 'kind' discriminator in the detail key reserves space for the
    sibling hooks coming in T013/T014 (shipping-options, addable-variants)
    so they can share the namespace without collisions

D-01-006: admin UI testing infra out of scope — no component test for this
hook. Authority is enforced via TS types + the underlying T005
integration test on the route.

Maps to: FR-003 (spec 006 T012).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds React Query hook wrapping GET /admin/orders/:id/seller-valid-shipping-options
(T006 endpoint). Hook is in seller-scoped-orders.tsx alongside T012's
useSellerValidStockLocations.

Pattern matches T012 — typed SDK access via codegen, Promise<...>
response cast since the route's response shape isn't auto-inferred.

Critical: queryKey includes the full query object (kind + location_id +
is_return) so React Query refetches when the upstream picker changes
location_id (FR-006). The drawer consumers in T015-T020 will rely on
this refetch behavior to keep the picker list in sync with the chosen
location.

D-01-006: no component test (admin UI testing infra out of scope).
Authority enforced via TS types + the underlying T006 integration test.

Maps to: FR-005, FR-006 (spec 006 T013).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds React Query hook wrapping GET /admin/orders/:id/addable-variants
(T007 endpoint). Same file as T012/T013.

Search input is debounced internally by 300ms (NFR-002) via a small
useEffect/setTimeout — callers pass live input value, the hook fires
the request only once typing settles. Debounced value drives both the
query function and the cache key, so React Query naturally collapses
intermediate keystrokes.

Per-row eligibility ({ can_add, reason }) is surfaced as a typed
discriminated union, ready for T018-T020 drawer consumers to disable
ineligible rows with the right tooltip per reason ('no_price' /
'no_inventory').

D-01-006: no component test (admin UI testing infra out of scope).
Authority enforced via TS types + the underlying T007 integration test
on the route. Debounce timing covered by manual smoke (300ms is the
documented NFR — change requires updating both the route timing
expectation and this constant).

Maps to: FR-009, NFR-002 (spec 006 T014).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
InferClientInput<typeof sdk.admin.orders.$id.X.query> only surfaces
the route's URL params ($id, fetchOptions) — it doesn't infer query
params (limit, offset, location_id, search) since the route handlers
read req.query directly without validators. Callers passing { limit:
200 } got a TS2353 'limit does not exist' error.

Replace InferClientInput with explicit query type per hook:
  - SellerValidStockLocationsQuery: { limit?, offset? }
  - SellerValidShippingOptionsQuery: { location_id?, is_return?, limit?, offset? }
  - AddableVariantsQuery: { search?, limit?, offset? }

Same shape as the route handler reads. If a route grows new params,
update the type — TS will flag it at the call site.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces global useStockLocations call in the order-create-return
form with the seller-scoped useSellerValidStockLocations hook. The
location combobox now shows only stock locations linked to the order's
seller (via stock_location_seller link).

Defense-in-depth: backend (T008) also rejects cross-seller location_id
on POST /admin/returns with 403 LOCATION_NOT_SELLER_VALID — UI scope is
the user-visible side, the middleware is the integrity guarantee.

Limit reduced from 999 → 200 to match the endpoint's MAX_LIMIT cap.
Most sellers have well under 200 locations; if larger, paginate.

D-01-006: no component test (admin UI testing infra out of scope).
Authority enforced via TS types + the underlying T005/T008 integration
tests on the route + middleware.

Maps to: FR-001, FR-002 (spec 006 T015).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In claim-create-form.tsx swap two global hooks for the seller-scoped
ones from T012/T013:

  useStockLocations({ limit: 999 })
    → useSellerValidStockLocations(order.id, { limit: 200 })

  useShippingOptions({ ... is_return filter via .filter() })
    → useSellerValidShippingOptions(
        order.id,
        { location_id, is_return: true },
        { enabled: !!locationId }
      )

The old code fetched ALL shipping options and then filtered to inbound
client-side; the new hook does is_return filtering on the backend, so
we drop the .filter() over rules entirely.

Outbound shipping option picker lives in claim-outbound-section.tsx
and is out of T016 scope (T019/T020 territory if/when needed).

Defense-in-depth: T011 wires requireSellerValidLocation +
requireSellerValidShippingOption on POST /admin/claims/:id/inbound/shipping-method,
so a regression in this UI swap can't bypass the seller scope on the
server side.

D-01-006: no component test (admin UI testing infra out of scope).

Maps to: FR-001, FR-002, FR-005, FR-006 (spec 006 T016).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror of T016 for the exchange-inbound-section.tsx — same swap as
claims:

  useStockLocations({ limit: 999 })
    → useSellerValidStockLocations(order.id, { limit: 200 })

  useShippingOptions({ ... is_return filter via .filter() })
    → useSellerValidShippingOptions(
        order.id,
        { location_id, is_return: true },
        { enabled: !!locationId }
      )

Defense-in-depth: T011 wires requireSellerValidLocation +
requireSellerValidShippingOption on POST /admin/exchanges/:id/inbound/shipping-method.

D-01-006: no component test.

Maps to: FR-001, FR-002, FR-005, FR-006 (spec 006 T017).

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

Replaces useVariants (Medusa global catalog) with the seller-scoped
useAddableVariants hook (T014) in the Edit Order add-items table.

Each row now carries an eligibility discriminator
({ can_add: true | false, reason: 'ok' | 'no_price' | 'no_inventory' })
which we use to disable selection for ineligible rows via
enableRowSelection — admins can still see them, but can't add them.

Defense-in-depth: T010 wires requireSellerValidAddItem on
POST /admin/order-edits/:id/items, so even a UI regression that
re-enables selection can't bypass server-side scope.

Wiring:
  - AddOrderEditItemsTable now takes orderId prop (passed from
    order-edit-items-section.tsx via order.id)
  - search query mapping: searchParams.q → useAddableVariants's 'search'
    (debounce is internal to the hook, NFR-002)
  - count guard: useAddableVariants returns count via the same shape

Tooltip per reason ('no_price' / 'no_inventory') is left for a follow-up
on use-order-edit-item-table-columns.tsx — it's column-level UX that
can land independently without changing the hook contract.

D-01-006: no component test (admin UI testing infra out of scope).

Maps to: FR-007, FR-008, FR-009 (spec 006 T018).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same pattern as T018 (Edit Order add-items) but for the claim outbound
items picker. Replaces useVariants with useAddableVariants(orderId)
and disables selection for ineligible rows (no_price / no_inventory).

Wiring:
  - AddClaimOutboundItemsTable now takes orderId prop
  - claim-outbound-section.tsx passes order.id from parent
  - searchParams.q maps to useAddableVariants's 'search' (debounce internal)

Defense-in-depth: T011 wires requireSellerValidAddItem on
POST /admin/claims/:id/outbound/items.

D-01-006: no component test.

Maps to: FR-007, FR-008, FR-009 (spec 006 T019).

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

Mirror of T019 for the exchange outbound items picker. Replaces
useVariants with useAddableVariants(orderId) and disables ineligible
rows (no_price / no_inventory).

Wiring:
  - AddExchangeOutboundItemsTable now takes orderId prop
  - exchange-outbound-section.tsx passes order.id from parent
  - searchParams.q maps to useAddableVariants's 'search' (debounce internal)

Defense-in-depth: T011 wires requireSellerValidAddItem on
POST /admin/exchanges/:id/outbound/items.

D-01-006: no component test.

Maps to: FR-007, FR-008, FR-009 (spec 006 T020).

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

T020 commit landed with the import line still pointing at useVariants
while the call site used useAddableVariants — TS error TS2304 + TS6133.
Parallel Edit on the import didn't take effect on origin even though
the staged diff showed only the call-site swap. This is the scoped fix:
swap the import line so the file compiles.
The admin order-edit ProductCell renders `row.original.product.thumbnail`
and `product.title`. Our addable-variants endpoint was returning only
flat variant fields (id, sku, title, prices, inventory) — no product
traversal — so the cell crashed with 'Cannot read properties of
undefined (reading thumbnail)' as soon as the table mounted.

Add product.id, product.title, product.thumbnail, product.handle to
the query.graph fields, and reflect them in the AddableVariant type
on the hook side.
Search filter only checked variant.sku and variant.title, but the UI
column shows product.title. Typing a product name returned no rows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Medusa's fetchShippingOptionForOrderWorkflow only seeds currency_code
in the calculated_price context. Seller-owned shipping options whose
prices require a region_id rule resolved to null, which crashed the
downstream prepare-shipping-method step with "Cannot read properties
of null (reading 'calculated_amount')" the moment the admin selected
the option in the picker.

Hook the workflow's setPricingContext extension point and pull
region_id from the order so seller-priced options resolve correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…019/T020 follow-up)

Outbound shipping option pickers in claim/exchange drawers were still
hitting useOrderShippingOptions, which returns marketplace-wide
options regardless of the order's seller. Spec FR-006 requires only
seller-owned options.

- claim-outbound-section + exchange-outbound-section: swap to
  useSellerValidShippingOptions(order.id, { is_return: false }).
  Drop the now-redundant client-side rules filter.
- Extend SellerValidShippingOption hook type with service_zone +
  calculated_price so downstream getFormattedShippingOptionLocationName
  has the data it needs.
- seller-valid-shipping-options/route.ts: query
  service_zone.fulfillment_set.location.id/name/address.* so the route
  returns the location metadata the picker renders. Pre-resolve
  calculated_price with the order's currency + region context and drop
  options whose calculated_amount is null — defensive belt alongside
  the workflow hook fix.
- middlewares.ts: register requireSellerValidLocation +
  requireSellerValidShippingOption on the two outbound shipping-method
  matchers (claim + exchange) so cross-seller IDs are rejected at the
  wire even if the picker is bypassed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend MedusaError serializes as `{ message, type, code }`. The
client previously dropped `code` and only kept `message` + `status`,
which forced admin call sites to parse error.message strings to
recognize seller-scoped rejections. Capture `code` so the admin can
map LOCATION_NOT_SELLER_VALID / SHIPPING_OPTION_NOT_SELLER_VALID /
VARIANT_NOT_SELLER_VALID to translated copy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Si3r4dz and others added 4 commits April 30, 2026 15:46
Six drawers (Return, Claim inbound + outbound, Exchange inbound +
outbound, Edit Order add-items) hit middlewares that reject
cross-seller writes with codes LOCATION_NOT_SELLER_VALID /
SHIPPING_OPTION_NOT_SELLER_VALID / VARIANT_NOT_SELLER_VALID. The
drawers were piping `error.message` straight into toast.error, so the
admin saw the raw English backend copy regardless of locale.

Add a small helper (resolveErrorToastMessage) that maps the three
codes to translation keys under orders.errors.* and falls back to
the raw message for any other error. Wire it through every
toast.error(error.message) site in the six drawers.

Per CLAUDE.md "TDD backend only" rule (UI/components ship without
component tests), no .RED/.GREEN cycle here.

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

Adds orders.errors.locationNotSellerValid /
shippingOptionNotSellerValid / variantNotSellerValid to every locale
file. English (en.json) carries the authoritative copy. Polish
(pl.json) is fully translated. Other 27 locales receive the English
copy as a placeholder; human translators will replace as a follow-up.

The keys are inserted as the first property under "orders" via
string-level append — JSON-aware re-serialization would have
deduplicated several locales' pre-existing duplicate-key entries
(de, fr, etc.) which is out of scope for this change.

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

- capability-model.md: bump E2 (seller-valid stock locations) and E3
  (seller-valid add-item) from open to implemented, with the spec
  reference, the three read endpoints, the three middlewares, the
  three error codes they return, and the admin-side error→toast
  mapping.
- v2/core-concepts/admin-error-codes.mdx: new docs page covering the
  Mercur error codes returned by admin write endpoints — seller-valid
  scoping (LOCATION_NOT_SELLER_VALID, SHIPPING_OPTION_NOT_SELLER_VALID,
  VARIANT_NOT_SELLER_VALID, spec 006), warehouse capability lock
  (NOT_ALLOWED, spec 005), and cancel-order
  (CANCEL_BLOCKED_BY_FULFILLMENT). Cross-links to feature-flags page.
- docs.json: register the new page in the Core Concepts navigation
  group.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Strip "spec 006" / "spec 005" / FR-NNN / NFR-NNN / D-NN-NNN / TNNN
references from:
- packages/core middlewares + admin route comments
- packages/admin hooks, lib helper, drawer files, table files,
  order-detail sections
- integration tests for seller-valid scoping, feature flags, and
  cancel-order invariant
- public docs page apps/docs/v2/core-concepts/admin-error-codes.mdx

Also remove redundant "what" comments where the code is
self-describing, and tighten a few "why" comments to the actual
constraint without referring to a planning document. The behavior is
unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Si3r4dz Si3r4dz changed the title 006 admin seller valid scoping feat/admin seller valid scoping Apr 30, 2026
Si3r4dz and others added 2 commits April 30, 2026 16:29
Three additional admin write endpoints accept a `location_id` and were
unguarded:
  - POST /admin/returns/:id (return update — schema accepts location_id)
  - POST /admin/claims/:id/inbound/items (schema accepts location_id)
  - POST /admin/exchanges/:id/inbound/items (schema accepts location_id)

Wire requireSellerValidLocation on each. Same class of cross-seller
write the existing matchers were created to block.

Also harden the three seller-valid middlewares against unvalidated
body shapes — Medusa's zod validators run as separate matchers, so
the body these middlewares see is raw bytes. An attacker could pass
location_id as an array (which query.graph would then treat as an IN
filter) or items as an object (breaking .map). Narrow with explicit
typeof / Array.isArray checks before reading.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Return drawer was still hitting the global useShippingOptions and
filtering client-side, so the picker showed marketplace-wide return
options regardless of the order's seller. Swap to
useSellerValidShippingOptions(order.id, { location_id, is_return: true })
matching the claim/exchange drawers. Drop the client-side rule and
location filters — backend filters on both.

Also drop the internal 300ms debounce in useAddableVariants. Every
call site feeds it from _DataTable's built-in search input which
already debounces, so the hook's extra timer just compounded the lag
to ~800ms perceived.

Unify the return drawer error-toast shape to
toast.error(resolveErrorToastMessage(e, t)) — matching the rest of
the seller-scoped drawers and avoiding a "Error / Error" toast when
the backend response carries no message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Si3r4dz Si3r4dz marked this pull request as ready for review April 30, 2026 14:31
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