feat/admin seller valid scoping#892
Open
Si3r4dz wants to merge 36 commits into
Open
Conversation
…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>
…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>
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>
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>
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.
Summary
Provide a concise description of the problem and the proposed solution.
Changes
Testing
List the tests or commands you ran to validate the change.
Checklist
new.Linked issues
Reference any related issues with
Fixes #...when applicable.