@mercurjs/docs 2.3.8-canary.3 → 2.3.9-canary.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/content/learn/architecture.mdx +166 -263
  2. package/content/learn/introduction.mdx +9 -1
  3. package/content/learn/use-cases.mdx +70 -0
  4. package/content/platform/attribute/concepts/attribute-types.mdx +73 -46
  5. package/content/platform/attribute/concepts/global-vs-inline.mdx +54 -32
  6. package/content/platform/attribute/concepts/variant-axes.mdx +54 -32
  7. package/content/platform/attribute/guides/attach-attributes-to-a-product.mdx +96 -53
  8. package/content/platform/attribute/guides/create-a-variant-axis.mdx +106 -57
  9. package/content/platform/attribute/guides/create-an-attribute.mdx +105 -56
  10. package/content/platform/attribute/overview.mdx +37 -33
  11. package/content/platform/attribute/reference/data-models.mdx +53 -38
  12. package/content/platform/attribute/reference/events.mdx +47 -24
  13. package/content/platform/attribute/reference/links.mdx +98 -27
  14. package/content/platform/attribute/reference/service.mdx +71 -32
  15. package/content/platform/attribute/reference/workflows.mdx +158 -28
  16. package/content/platform/catalog/concepts/master-products.mdx +39 -37
  17. package/content/platform/catalog/concepts/product-seller-allowlist.mdx +56 -35
  18. package/content/platform/catalog/concepts/status-lifecycle.mdx +55 -49
  19. package/content/platform/catalog/concepts/variants-categories-collections.mdx +55 -35
  20. package/content/platform/catalog/guides/allowlist-stores.mdx +100 -37
  21. package/content/platform/catalog/guides/create-a-master-product.mdx +124 -48
  22. package/content/platform/catalog/guides/publish-or-reject-a-product.mdx +120 -55
  23. package/content/platform/catalog/overview.mdx +39 -35
  24. package/content/platform/catalog/reference/data-models.mdx +154 -43
  25. package/content/platform/catalog/reference/events.mdx +41 -25
  26. package/content/platform/catalog/reference/links.mdx +196 -28
  27. package/content/platform/catalog/reference/service.mdx +57 -35
  28. package/content/platform/catalog/reference/workflows.mdx +124 -30
  29. package/content/platform/commission/concepts/order-commission-lines.mdx +54 -52
  30. package/content/platform/commission/concepts/rule-matching.mdx +53 -63
  31. package/content/platform/commission/concepts/rules-and-rates.mdx +61 -59
  32. package/content/platform/commission/guides/batch-update-rules.mdx +69 -41
  33. package/content/platform/commission/guides/create-a-rate.mdx +101 -53
  34. package/content/platform/commission/guides/refresh-order-commission-lines.mdx +104 -35
  35. package/content/platform/commission/overview.mdx +43 -35
  36. package/content/platform/commission/reference/data-models.mdx +59 -45
  37. package/content/platform/commission/reference/events.mdx +28 -34
  38. package/content/platform/commission/reference/links.mdx +42 -33
  39. package/content/platform/commission/reference/service.mdx +103 -28
  40. package/content/platform/commission/reference/workflows.mdx +101 -27
  41. package/content/platform/offer/concepts/pricing-and-inventory.mdx +63 -49
  42. package/content/platform/offer/concepts/shipping.mdx +28 -29
  43. package/content/platform/offer/concepts/what-is-an-offer.mdx +48 -45
  44. package/content/platform/offer/guides/bulk-create-offers.mdx +97 -61
  45. package/content/platform/offer/guides/create-an-offer.mdx +112 -60
  46. package/content/platform/offer/guides/manage-offer-inventory.mdx +78 -38
  47. package/content/platform/offer/overview.mdx +39 -32
  48. package/content/platform/offer/reference/data-models.mdx +77 -47
  49. package/content/platform/offer/reference/events.mdx +44 -18
  50. package/content/platform/offer/reference/links.mdx +92 -40
  51. package/content/platform/offer/reference/service.mdx +66 -31
  52. package/content/platform/offer/reference/workflows.mdx +146 -37
  53. package/content/platform/order-group/concepts/computed-totals.mdx +40 -37
  54. package/content/platform/order-group/concepts/order-splitting.mdx +41 -39
  55. package/content/platform/order-group/concepts/the-order-group.mdx +39 -32
  56. package/content/platform/order-group/guides/list-order-groups.mdx +96 -56
  57. package/content/platform/order-group/guides/retrieve-an-order-group.mdx +93 -42
  58. package/content/platform/order-group/guides/split-a-cart.mdx +115 -40
  59. package/content/platform/order-group/overview.mdx +42 -32
  60. package/content/platform/order-group/reference/data-models.mdx +48 -31
  61. package/content/platform/order-group/reference/events.mdx +29 -17
  62. package/content/platform/order-group/reference/links.mdx +90 -29
  63. package/content/platform/order-group/reference/service.mdx +55 -31
  64. package/content/platform/order-group/reference/workflows.mdx +83 -24
  65. package/content/platform/payout/concepts/account-lifecycle.mdx +64 -67
  66. package/content/platform/payout/concepts/accounts-and-onboarding.mdx +50 -40
  67. package/content/platform/payout/concepts/payout-pipeline.mdx +70 -73
  68. package/content/platform/payout/guides/create-a-payout-account.mdx +65 -41
  69. package/content/platform/payout/guides/process-a-provider-webhook.mdx +84 -38
  70. package/content/platform/payout/guides/start-provider-onboarding.mdx +90 -46
  71. package/content/platform/payout/overview.mdx +43 -34
  72. package/content/platform/payout/reference/data-models.mdx +63 -36
  73. package/content/platform/payout/reference/events.mdx +56 -30
  74. package/content/platform/payout/reference/links.mdx +52 -22
  75. package/content/platform/payout/reference/service.mdx +72 -33
  76. package/content/platform/payout/reference/workflows.mdx +80 -19
  77. package/content/platform/product-edit/concepts/change-actions.mdx +52 -45
  78. package/content/platform/product-edit/concepts/change-pipeline.mdx +60 -52
  79. package/content/platform/product-edit/concepts/status-and-auto-confirm.mdx +43 -46
  80. package/content/platform/product-edit/guides/confirm-or-decline-a-change.mdx +104 -56
  81. package/content/platform/product-edit/guides/edit-a-product.mdx +126 -46
  82. package/content/platform/product-edit/guides/request-a-revision.mdx +99 -39
  83. package/content/platform/product-edit/overview.mdx +41 -34
  84. package/content/platform/product-edit/reference/data-models.mdx +73 -47
  85. package/content/platform/product-edit/reference/events.mdx +39 -29
  86. package/content/platform/product-edit/reference/links.mdx +32 -24
  87. package/content/platform/product-edit/reference/service.mdx +45 -26
  88. package/content/platform/product-edit/reference/workflows.mdx +173 -31
  89. package/content/platform/review/concepts/product-vs-seller-reviews.mdx +42 -41
  90. package/content/platform/review/concepts/ratings-and-moderation.mdx +45 -44
  91. package/content/platform/review/concepts/the-review-model.mdx +52 -45
  92. package/content/platform/review/guides/compute-aggregate-ratings.mdx +62 -37
  93. package/content/platform/review/guides/create-a-review.mdx +109 -39
  94. package/content/platform/review/guides/moderate-a-review.mdx +83 -37
  95. package/content/platform/review/guides/respond-to-a-review.mdx +85 -40
  96. package/content/platform/review/overview.mdx +43 -33
  97. package/content/platform/review/reference/data-models.mdx +41 -27
  98. package/content/platform/review/reference/events.mdx +55 -37
  99. package/content/platform/review/reference/links.mdx +80 -22
  100. package/content/platform/review/reference/service.mdx +61 -33
  101. package/content/platform/review/reference/workflows.mdx +80 -24
  102. package/content/platform/store/concepts/lifecycle.mdx +68 -44
  103. package/content/platform/store/concepts/store-entity.mdx +45 -27
  104. package/content/platform/store/concepts/team.mdx +56 -35
  105. package/content/platform/store/guides/create-a-store.mdx +102 -35
  106. package/content/platform/store/guides/manage-the-team.mdx +122 -32
  107. package/content/platform/store/guides/moderate-a-store.mdx +107 -38
  108. package/content/platform/store/overview.mdx +41 -34
  109. package/content/platform/store/reference/data-models.mdx +132 -57
  110. package/content/platform/store/reference/events.mdx +42 -17
  111. package/content/platform/store/reference/links.mdx +67 -49
  112. package/content/platform/store/reference/service.mdx +105 -33
  113. package/content/platform/store/reference/workflows.mdx +304 -24
  114. package/content/references/api/conventions.mdx +20 -0
  115. package/content/resources/integrations/magento2.mdx +162 -0
  116. package/content/resources/integrations/overview.mdx +10 -0
  117. package/content/resources/integrations/stripe-connect.mdx +1 -1
  118. package/content/resources/tutorials/custom-api-route.mdx +3 -2
  119. package/llms.txt +113 -111
  120. package/package.json +1 -1
@@ -1,54 +1,56 @@
1
1
  ---
2
- title: "Master products"
2
+ title: "Master products in a shared catalog"
3
3
  sidebarTitle: "Master products"
4
- description: "The shared catalog, why products aren't seller-owned, and submission attribution."
4
+ description: "Why products belong to the marketplace catalog instead of a store, and how Mercur records who submitted each one."
5
5
  ---
6
6
 
7
- In this document, you'll learn how Mercur models products as a single shared
8
- catalog rather than per-seller listings.
7
+ A master product is the one catalog record for an item that any number of stores can sell. Mercur keeps products out of store ownership so that two stores selling the same chair share one title, one set of variants, and one product page, and compete on their offers instead.
9
8
 
10
- ## Product
9
+ ## One product, sold through offers
11
10
 
12
- A product is a **master product** in a catalog shared by the whole marketplace,
13
- represented by Medusa's native `Product` data model (table `product`, id prefix
14
- `prod`). Products carry the usual commerce fields such as `title`, `handle`,
15
- `description`, `status`, variants, options, and images, and are **not owned by
16
- any store**. Creating a product adds it to the shared catalog. Multiple stores
17
- can then sell the same master product.
11
+ A master product is Medusa's native `Product` model (table `product`, id prefix `prod`). It holds the shared catalog data: `title`, `handle`, `description`, `status`, images, options, variants, and categories. It has no seller column. Creating a product adds it to the catalog, and stores then sell it by creating [offers](/platform/offer/overview) against its variants.
12
+
13
+ The split between the two records works like this:
14
+
15
+ - **Master product**: what the item is, shared by every store that sells it
16
+ - **Offer**: how one store sells it, with the store's own SKU, price, stock, and shipping profile
17
+
18
+ A store's stock and price never live on the variant. Offer inventory links to the offer, so a variant's `inventory_items` stay empty for marketplace sales.
19
+
20
+ ## Attribution instead of ownership
21
+
22
+ Mercur records who submitted a product without making them its owner. `createProductsWorkflow` takes a required `created_by` and writes a confirmed `ProductChange` with that value as `created_by`, holding one `PRODUCT_ADD` action. The action's `details` store the starting `status` and the ids of the attributes the product began with.
23
+
24
+ The vendor API passes the store's seller id as `created_by`, and the admin API passes the operator's user id. Mercur reads that record back to decide which products a store submitted:
18
25
 
19
26
  ```ts
20
- const { result } = await createProductsWorkflow(container).run({
21
- input: {
22
- products: [
23
- {
24
- title: "Aeron Chair",
25
- status: "proposed",
26
- seller_ids: ["sel_123"],
27
- },
28
- ],
29
- created_by: "usr_123",
27
+ const { data: actions } = await query.graph({
28
+ entity: "product_change_action",
29
+ fields: ["product_id"],
30
+ filters: {
31
+ action: "PRODUCT_ADD",
32
+ product_change: { created_by: "sel_01JD8Q1234567" },
30
33
  },
31
34
  })
32
35
  ```
33
36
 
37
+ The query returns the ids of every product the store submitted. Mercur runs the same lookup to let a store see its own `draft` and `proposed` submissions, which other stores never see.
38
+
34
39
  <Note>
35
- A store never sells a bare master product directly. It sells against one by
36
- creating an [offer](/platform/offer/overview). The offer carries the store's
37
- own SKU, price, inventory, and shipping profile, while the master product
38
- holds the shared catalog data everyone shares.
40
+ Attribution grants visibility, not control of the catalog entry. Every edit a store makes through the vendor API, to its own submission or to a published product, goes through the [product edit pipeline](/platform/product-edit/overview).
39
41
  </Note>
40
42
 
41
- ## Attribution, not ownership
43
+ ## What a store sees
44
+
45
+ The vendor API filters product reads by status, attribution, and the [allowlist](/platform/catalog/concepts/product-seller-allowlist). A store's product list contains:
46
+
47
+ - **Its own submissions**: products it created that are `draft` or `proposed`
48
+ - **Published products open to it**: `published` products whose allowlist is empty or includes the store
49
+
50
+ A store can still read one of its own `rejected` products by id, but rejected products drop out of its list. The Store API returns `published` products only.
42
51
 
43
- Because the catalog is shared, the creator of a product does **not** own it.
44
- When a store submits a new product, Mercur records the submission as an immutable
45
- audit entry (a `PRODUCT_ADD` action in the product-change pipeline) so you know
46
- who proposed it. That attribution is for review and history only. Once
47
- published, the product belongs to the shared catalog like any other.
52
+ ## Related
48
53
 
49
- <Tip>
50
- Attribution is recorded automatically by `createProductsWorkflow` through its
51
- `created_by` input. You don't manage it by hand. See the
52
- [status lifecycle](/platform/catalog/concepts/status-lifecycle) for how a
53
- submission becomes a published catalog product.
54
- </Tip>
54
+ - [Status lifecycle](/platform/catalog/concepts/status-lifecycle) for how a submission becomes a published product
55
+ - [Create a master product](/platform/catalog/guides/create-a-master-product) to run the create workflow from your code
56
+ - [Offers](/platform/offer/overview) for how stores sell a master product
@@ -1,48 +1,69 @@
1
1
  ---
2
- title: "The store allowlist"
2
+ title: "Restrict a product to chosen stores"
3
3
  sidebarTitle: "The allowlist"
4
- description: "The product_seller link that controls which stores may sell a master product."
4
+ description: "How the product_seller link opens a product to every store or restricts it to the stores on its list."
5
5
  ---
6
6
 
7
- In this document, you'll learn how Mercur controls which stores are allowed to
8
- sell a shared master product.
7
+ The store allowlist decides which stores can see and sell a published master product. It's a many-to-many link between Medusa's `Product` and Mercur's `Seller`, stored in the `product_seller` table. Use it for exclusive ranges, brand-authorized resellers, or products only certain stores may carry.
9
8
 
10
- ## Product seller
9
+ ## Open by default, restricted by rows
11
10
 
12
- Since the catalog is shared, Mercur needs a way to say *which* stores may sell a
13
- given master product. That's the **product–seller allowlist**: a many-to-many
14
- link between Medusa's `Product` and Mercur's `Seller`, stored in the
15
- `product_seller` table.
11
+ A product's allowlist has two states, and the number of `product_seller` rows decides which one applies. An empty allowlist doesn't block anyone: it means every store can sell the product.
12
+
13
+ | `product_seller` rows | Who can see and sell the published product |
14
+ | --- | --- |
15
+ | None | Every store |
16
+ | One or more | Only the stores on the list |
17
+
18
+ Adding the first store to a product's allowlist closes it to every other store. Removing the last store opens it to all stores again.
19
+
20
+ ## Read a product's allowlist
21
+
22
+ The link exposes the stores under the `sellers` alias on a product. Read it with `query.graph`:
16
23
 
17
24
  ```ts
18
- const { data: products } = await query.graph({
25
+ const { data: [product] } = await query.graph({
19
26
  entity: "product",
20
27
  fields: ["id", "title", "sellers.id", "sellers.name"],
21
- filters: { id: "prod_123" },
28
+ filters: { id: "prod_01JD8Q1234567" },
22
29
  })
30
+
31
+ const isOpen = (product.sellers ?? []).length === 0
23
32
  ```
24
33
 
25
- A store appears in a product's `sellers` list only if it has been allowlisted.
26
- Being on the allowlist is what lets a store create an
27
- [offer](/platform/offer/overview) against the master product. Without it, the
28
- store cannot list that product for sale.
29
-
30
- <Note>
31
- The allowlist link is created automatically for the submitting store when a
32
- product is created with `seller_ids`, and managed afterwards with
33
- `linkSellersToProductWorkflow`. See
34
- [Allowlist stores](/platform/catalog/guides/allowlist-stores).
35
- </Note>
36
-
37
- ## Category allowlist
38
-
39
- Product categories carry the same pattern through the `category_seller` link. It
40
- is a many-to-many association between `ProductCategory` and `Seller` that scopes
41
- which categories a store is associated with. Manage it with
42
- `linkSellersToProductCategoryWorkflow`.
43
-
44
- <Tip>
45
- The allowlist governs the **right to sell**, not the sale itself. A store on
46
- the allowlist still has to create an offer to actually list the product.
47
- Allowlisting alone doesn't put anything on the storefront.
48
- </Tip>
34
+ An empty `sellers` array means the product is open to every store. You can also query the link table directly as the `product_seller` entity, with `product_id` and `seller_id` fields.
35
+
36
+ ## Where Mercur enforces it
37
+
38
+ Mercur applies the allowlist when it scopes a store's view of the catalog. These vendor reads hide a published product restricted to other stores:
39
+
40
+ - **Product list and detail**: the product returns `404` for a store not on its list
41
+ - **Variant list**: variants of restricted products are filtered out
42
+ - **Product variants under a product**: blocked with the product itself
43
+
44
+ A store always sees its own `draft` and `proposed` submissions, whatever their allowlist. The admin API and the Store API don't filter by allowlist.
45
+
46
+ <Warning>
47
+ `createOffersWorkflow` doesn't check the allowlist. If you create offers from your own server code, check the product's `sellers` first, or a store can sell a product it isn't allowed to.
48
+ </Warning>
49
+
50
+ ## How stores get on the list
51
+
52
+ Mercur writes `product_seller` rows in two places:
53
+
54
+ - **On create**: `createProductsWorkflow` links every id in a product's `seller_ids` input
55
+ - **Afterwards**: `linkSellersToProductWorkflow` adds and removes stores in one run
56
+
57
+ The vendor API never passes `seller_ids`, so a store's own submission starts open to every store. An operator restricts it with `linkSellersToProductWorkflow`. Removing a store from the list doesn't touch the offers it already created.
58
+
59
+ ## The category_seller link
60
+
61
+ Categories have a second link to stores, `category_seller`, between `ProductCategory` and `Seller` with the `sellers` alias on a category. `linkSellersToProductCategoryWorkflow` writes it.
62
+
63
+ No shipped route or workflow reads the link to filter products. Mercur exports `validateSellerProductPermissionsStep`, which throws `Seller is restricted from categories` when the seller is linked to any of the category ids you pass. Add that step to your own workflow if you treat the link as a category block list.
64
+
65
+ ## Related
66
+
67
+ - [Allowlist stores](/platform/catalog/guides/allowlist-stores) to manage the list from your code
68
+ - [Master products](/platform/catalog/concepts/master-products) for what a store sees by default
69
+ - [Links reference](/platform/catalog/reference/links) for every link on products and categories
@@ -1,58 +1,64 @@
1
1
  ---
2
- title: "Status lifecycle"
2
+ title: "Product status lifecycle"
3
3
  sidebarTitle: "Status lifecycle"
4
- description: "How a master product moves from draft to proposed, published, or rejected."
4
+ description: "How a master product moves from draft or proposed to published or rejected, and which workflow runs each step."
5
5
  ---
6
6
 
7
- In this document, you'll learn about the product status lifecycle and how a
8
- submission becomes a live catalog product.
7
+ Every master product has a `status` that decides who can see it and whether it's for sale. Mercur uses that status as a review gate: stores submit products as `proposed`, and an operator decides whether each one joins the published catalog.
9
8
 
10
- ## Status
9
+ ## The four statuses
11
10
 
12
- A product's state is held in the `status` field of the `Product` model, typed by
13
- Medusa's `ProductStatus` enum. A master product moves through four statuses:
11
+ The status lives in the `status` field of Medusa's `Product` model. Mercur re-exports the values as `ProductStatus` from `@mercurjs/types`. The model default is `draft`.
14
12
 
13
+ | Status | Meaning | Who sees it |
14
+ | --- | --- | --- |
15
+ | `draft` | Work in progress, not submitted | Operators and the submitting store |
16
+ | `proposed` | Submitted and waiting for review | Operators and the submitting store |
17
+ | `published` | Live in the shared catalog | Everyone the [allowlist](/platform/catalog/concepts/product-seller-allowlist) permits, and the Store API |
18
+ | `rejected` | Turned down in review | Operators, and the submitting store by id |
19
+
20
+ ## Review transitions
21
+
22
+ Mercur ships one workflow per review step. Each one reads the product, checks its status with `validateProductsStatusStep`, records a confirmed `ProductChange`, and emits an event.
23
+
24
+ <img className="block dark:hidden" src="/images/figures/catalog-status-lifecycle-light.svg" alt="A Status transitions table: createProductsWorkflow creates a product as proposed, then confirmProductsWorkflow, rejectProductWorkflow, and requestProductChangeWorkflow move it to published, to rejected, or keep it proposed, with a product_change audit table behind." />
25
+ <img className="hidden dark:block" src="/images/figures/catalog-status-lifecycle-dark.svg" alt="A Status transitions table: createProductsWorkflow creates a product as proposed, then confirmProductsWorkflow, rejectProductWorkflow, and requestProductChangeWorkflow move it to published, to rejected, or keep it proposed, with a product_change audit table behind." />
26
+
27
+ | Workflow | From | To | Audit action | Event |
28
+ | --- | --- | --- | --- | --- |
29
+ | `createProductsWorkflow` | none | input `status` | `PRODUCT_ADD` | `product.created` |
30
+ | `confirmProductsWorkflow` | `proposed` | `published` | `STATUS_CHANGE` | `product.published` |
31
+ | `rejectProductWorkflow` | `proposed` | `rejected` | `STATUS_CHANGE` | `product.rejected` |
32
+ | `requestProductChangeWorkflow` | `proposed` | `proposed` | `CHANGE_REQUESTED` | `product.change-requested` |
33
+
34
+ The three review workflows run only on `proposed` products. A product in any other status fails with a `NOT_ALLOWED` error that lists each product id and its current status.
35
+
36
+ ## Starting status
37
+
38
+ The status a product starts in depends on who creates it. `createProductsWorkflow` passes the `status` you give it to Medusa, which falls back to `draft`.
39
+
40
+ - **Vendor API**: defaults to `proposed`; a store may also send `draft`
41
+ - **Admin API**: uses the request's `status`, or `draft` when it has none
42
+ - **Your own code**: whatever you pass, or `draft`
43
+
44
+ The vendor rule comes from the `product_request` feature flag, which is on by default. Turn it off with `MEDUSA_FF_PRODUCT_REQUEST=false` and stores can create products in any status, including `published`.
45
+
46
+ ```bash title=".env"
47
+ MEDUSA_FF_PRODUCT_REQUEST=false
15
48
  ```
16
- ┌────────┐ submit ┌──────────┐ approve ┌────────────┐
17
- │ draft │ ──────────►│ proposed │ ──────────►│ published │
18
- └────────┘ └────┬─────┘ └────────────┘
19
- │ reject
20
- ▼
21
- ┌────────────┐
22
- │ rejected │
23
- └────────────┘
24
- ```
25
49
 
26
- | Status | Meaning |
27
- | --- | --- |
28
- | `draft` | Work in progress, not yet submitted for review |
29
- | `proposed` | Submitted, awaiting operator review |
30
- | `published` | Approved and live in the shared catalog, sellable via offers |
31
- | `rejected` | Turned down during review |
32
-
33
- <Note>
34
- Vendor-created products default to **`proposed`**. A store submits a product
35
- for review rather than publishing it directly. An operator (or a low-risk
36
- auto-confirm rule) is what promotes it to `published`.
37
- </Note>
38
-
39
- ## Transitions
40
-
41
- Each review transition has a dedicated Mercur workflow so the audit trail,
42
- events, and side effects run consistently:
43
-
44
- | Workflow | Transition |
45
- | --- | --- |
46
- | `confirmProductsWorkflow` | `proposed` → `published` |
47
- | `rejectProductWorkflow` | `proposed` → `rejected` |
48
- | `requestProductChangeWorkflow` | stays `proposed`, asks the submitter for a revision |
49
-
50
- Every transition validates that the product is currently `proposed` before it
51
- runs, and records an immutable `STATUS_CHANGE` (or `CHANGE_REQUESTED`) action in
52
- the product-change pipeline for a full history of who reviewed what.
53
-
54
- <Tip>
55
- Reviewing a product doesn't touch offers. Publishing makes the master product
56
- sellable, but each store still lists it independently through its own
57
- [offer](/platform/offer/overview).
58
- </Tip>
50
+ The same flag controls whether store edits to existing products wait for review. See [status and auto-confirm](/platform/product-edit/concepts/status-and-auto-confirm).
51
+
52
+ ## Changes outside the review workflows
53
+
54
+ Mercur ships no workflow that moves a `draft` to `proposed`, or a `published` product back. Those changes go through a regular product update. The admin API runs Medusa's `updateProductsWorkflow` for that, which accepts any status.
55
+
56
+ <Warning>
57
+ A direct status update skips the `proposed` check, writes no `STATUS_CHANGE` record, and emits none of Mercur's product events. Use the review workflows when you need the audit trail.
58
+ </Warning>
59
+
60
+ ## Related
61
+
62
+ - [Publish or reject a product](/platform/catalog/guides/publish-or-reject-a-product) to run the review workflows
63
+ - [Workflows reference](/platform/catalog/reference/workflows) for inputs and hooks
64
+ - [Change pipeline](/platform/product-edit/concepts/change-pipeline) for how edits to existing products are reviewed
@@ -1,49 +1,69 @@
1
1
  ---
2
- title: "Variants, categories & collections"
2
+ title: "Variants, categories, and collections"
3
3
  sidebarTitle: "Variants & categories"
4
- description: "The native Medusa Product structure the shared catalog is built on."
4
+ description: "The Medusa Product module models that structure the shared catalog, and the places Mercur extends them."
5
5
  ---
6
6
 
7
- In this document, you'll learn about the structural models that organize the
8
- shared catalog, all of them native to Medusa's Product module.
7
+ The shared catalog is built from Medusa's Product module models. Variants describe what a customer can buy, and categories, collections, tags, and types organize products for browsing. Mercur keeps these models as they are and changes how variants get their options and where a variant's price and stock come from.
9
8
 
10
- ## Product variant
9
+ ## Variants come from attributes
11
10
 
12
- A variant is a purchasable configuration of a master product, represented by the
13
- `ProductVariant` data model (table `product_variant`, id prefix `variant`).
14
- Variants are generated from a product's options, including Mercur attributes
15
- marked as variant axes, and hold the SKU-level structure of the catalog entry.
11
+ A variant is one purchasable configuration of a master product: Medusa's `ProductVariant` model (id prefix `variant`). Each variant binds to one value of every product option, such as Size `M`.
12
+
13
+ When you create a product through Mercur's `createProductsWorkflow`, you don't pass native `options`. You pass Mercur `attributes`, and every `multi_select` attribute marked `is_variant_axis` becomes a native `ProductOption`. Each variant then names its option values by title:
16
14
 
17
15
  ```ts
18
- const { data: products } = await query.graph({
19
- entity: "product",
20
- fields: ["id", "title", "variants.id", "variants.title", "options.*"],
21
- filters: { id: "prod_123" },
22
- })
16
+ const product = {
17
+ title: "Linen shirt",
18
+ attributes: [
19
+ { title: "Size", is_variant_axis: true, values: ["S", "M"] },
20
+ ],
21
+ variants: [
22
+ { title: "S", options: { Size: "S" } },
23
+ { title: "M", options: { Size: "M" } },
24
+ ],
25
+ }
23
26
  ```
24
27
 
25
- <Note>
26
- Offer-based inventory and pricing live on the [offer](/platform/offer/overview),
27
- not the variant. The variant defines the shared catalog structure. Each store's
28
- stock and price come from its own offer.
29
- </Note>
28
+ Medusa requires every product to have at least one option. Mercur creates each product with an internal `__default__` option and removes it once a variant-axis attribute attaches a real one. Products without an axis keep it.
29
+
30
+ ## Price and stock live on offers
31
+
32
+ A variant holds catalog data such as `title`, `sku`, `ean`, and dimensions. It doesn't hold a store's price or stock. Mercur's create workflow sets `manage_inventory` to `false` on new variants unless you pass it, because each store's stock lives on its [offer](/platform/offer/overview).
30
33
 
31
- ## Categories & collections
34
+ - **Price**: an offer-scoped price on the variant's price set, matched by an `offer_id` rule
35
+ - **Stock**: inventory items linked to the offer, not the variant
36
+ - **SKU**: the store's own SKU on the offer, unique per store
37
+
38
+ ## Grouping models
32
39
 
33
40
  Products are organized with Medusa's native grouping models:
34
41
 
35
- | Model | Purpose |
36
- | --- | --- |
37
- | `ProductCategory` | Hierarchical, nestable classification (id prefix `pcat`) |
38
- | `ProductCollection` | Flat, curated grouping (id prefix `pcol`) |
39
- | `ProductTag` | Free-form labels for filtering |
40
- | `ProductType` | A single type classification per product |
41
-
42
- Assign products to a category with `assignProductsToCategoryWorkflow`, which
43
- keeps each product in exactly one category at a time.
44
-
45
- <Tip>
46
- Categories participate in the marketplace layer too: the `category_seller`
47
- link scopes which stores a category is associated with, mirroring the product
48
- [allowlist](/platform/catalog/concepts/product-seller-allowlist).
49
- </Tip>
42
+ | Model | Id prefix | Shape | Mercur extension |
43
+ | --- | --- | --- | --- |
44
+ | `ProductCategory` | `pcat` | Nested tree through `parent_category` | `media_images`, `sellers`, attribute link |
45
+ | `ProductCollection` | `pcol` | Flat group, one per product | `media_images` |
46
+ | `ProductTag` | `ptag` | Free-form labels, many per product | None |
47
+ | `ProductType` | `ptyp` | One type per product | None |
48
+
49
+ Categories and collections link to Mercur's media module through `media_images` for gallery and icon images. A category also links to stores through `category_seller` and to product attributes through `product_category_attribute`. See the [links reference](/platform/catalog/reference/links).
50
+
51
+ ## One category per product
52
+
53
+ Medusa lets a product belong to several categories. Mercur's `assignProductsToCategoryWorkflow` keeps one category per product: adding a product to a category replaces the categories it had.
54
+
55
+ ```ts
56
+ import { assignProductsToCategoryWorkflow } from "@mercurjs/core/workflows"
57
+
58
+ await assignProductsToCategoryWorkflow(container).run({
59
+ input: { id: "pcat_01JD8Q1234567", add: ["prod_01JD8Q1234567"] },
60
+ })
61
+ ```
62
+
63
+ The product now belongs only to `pcat_01JD8Q1234567`. Passing a product id in `remove` clears all its categories, not only this one.
64
+
65
+ ## Related
66
+
67
+ - [Attributes](/platform/attribute/overview) for attribute types and variant axes
68
+ - [Data models](/platform/catalog/reference/data-models) for every field on these models
69
+ - [Offers](/platform/offer/overview) for price and stock per store
@@ -1,59 +1,122 @@
1
1
  ---
2
- title: "Allowlist stores"
2
+ title: "Restrict a product to chosen stores"
3
3
  sidebarTitle: "Allowlist stores"
4
- description: "Grant and revoke a store's right to sell a master product from server code."
4
+ description: "Add stores to a product's allowlist, remove them, and open a product to every store again from server code."
5
5
  ---
6
6
 
7
- In this guide, you'll learn how to control which stores may sell a shared master
8
- product by managing the `product_seller` allowlist from your own server code.
7
+ Run `linkSellersToProductWorkflow` to change which stores can see and sell a master product. Use it for exclusive ranges, authorized resellers, or to restrict a store's own submission after review.
9
8
 
10
- A store can only create an [offer](/platform/offer/overview) against a master
11
- product if it's on that product's allowlist. Mercur exposes
12
- `linkSellersToProductWorkflow` to add and remove stores in a single call.
9
+ ## Before you begin
13
10
 
14
- ## Add and remove stores
11
+ Know how the list behaves before you change it:
15
12
 
16
- ```ts title="src/api/custom/allowlist/route.ts"
17
- import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
18
- import { linkSellersToProductWorkflow } from "@mercurjs/core/workflows"
13
+ - **Empty list**: every store can sell the product
14
+ - **One or more stores**: only those stores can sell it
19
15
 
20
- export async function POST(req: MedusaRequest, res: MedusaResponse) {
21
- await linkSellersToProductWorkflow(req.scope).run({
22
- input: {
23
- id: req.params.id, // product id
24
- add: ["sel_123", "sel_456"],
25
- remove: ["sel_789"],
26
- },
27
- })
16
+ Read [the store allowlist](/platform/catalog/concepts/product-seller-allowlist) for where Mercur enforces it.
28
17
 
29
- res.sendStatus(200)
18
+ ## Change a product's allowlist
19
+
20
+ <Steps>
21
+ <Step title="Add a route that runs the workflow">
22
+ The workflow takes the product `id` and optional `add` and `remove` arrays of seller ids. It writes and deletes `product_seller` rows in one run.
23
+
24
+ ```ts title="src/api/admin/exclusive-products/[id]/route.ts"
25
+ import type {
26
+ MedusaRequest,
27
+ MedusaResponse,
28
+ } from "@medusajs/framework/http"
29
+ import { linkSellersToProductWorkflow } from "@mercurjs/core/workflows"
30
+
31
+ type Body = { add?: string[]; remove?: string[] }
32
+
33
+ export async function POST(
34
+ req: MedusaRequest<Body>,
35
+ res: MedusaResponse
36
+ ) {
37
+ await linkSellersToProductWorkflow(req.scope).run({
38
+ input: { id: req.params.id, ...req.body },
39
+ })
40
+
41
+ res.json({ id: req.params.id })
42
+ }
43
+ ```
44
+
45
+ If a step fails, the workflow undoes the rows it already changed.
46
+ </Step>
47
+ <Step title="Check the result">
48
+ Read the product's `sellers` to confirm the new list.
49
+
50
+ ```ts
51
+ const { data: [product] } = await query.graph({
52
+ entity: "product",
53
+ fields: ["id", "sellers.id"],
54
+ filters: { id: "prod_01JD8Q1234567" },
55
+ })
56
+ ```
57
+
58
+ `product.sellers` holds every store on the list. An empty array means the product is open to all stores.
59
+ </Step>
60
+ </Steps>
61
+
62
+ ## Open a product to every store again
63
+
64
+ Remove every store on the list. Read the current stores first, then pass all of them in `remove`.
65
+
66
+ ```ts
67
+ const { data: [product] } = await query.graph({
68
+ entity: "product",
69
+ fields: ["sellers.id"],
70
+ filters: { id: "prod_01JD8Q1234567" },
71
+ })
72
+
73
+ await linkSellersToProductWorkflow(container).run({
74
+ input: {
75
+ id: "prod_01JD8Q1234567",
76
+ remove: (product.sellers ?? []).map((seller) => seller.id),
77
+ },
78
+ })
79
+ ```
80
+
81
+ The product now has no `product_seller` rows, so every store can see and sell it once it's published.
82
+
83
+ ## Check the allowlist before creating offers
84
+
85
+ `createOffersWorkflow` doesn't read the allowlist. If your code creates offers for stores, read the product's `sellers` as shown above and check that the store may sell it first.
86
+
87
+ ```ts
88
+ import { MedusaError } from "@medusajs/framework/utils"
89
+
90
+ const sellerIds = (product.sellers ?? []).map((seller) => seller.id)
91
+ const allowed =
92
+ sellerIds.length === 0 || sellerIds.includes("sel_01JD8Q1234567")
93
+
94
+ if (!allowed) {
95
+ throw new MedusaError(
96
+ MedusaError.Types.NOT_ALLOWED,
97
+ "This store can't sell the product"
98
+ )
30
99
  }
31
100
  ```
32
101
 
33
- <Note>
34
- `add` and `remove` are both optional. Pass either or both. Adding a store
35
- that's already allowlisted is idempotent. Removing a store revokes its right
36
- to sell the product going forward.
37
- </Note>
102
+ `allowed` is true when the list is empty or names the store. Removing a store from the list later doesn't delete the offers it already has.
38
103
 
39
- ## Allowlist a category
104
+ ## Link stores to a category
40
105
 
41
- Categories use the same shape through `linkSellersToProductCategoryWorkflow`,
42
- scoping which stores a category is associated with:
106
+ `linkSellersToProductCategoryWorkflow` writes the `category_seller` link with the same input shape.
43
107
 
44
108
  ```ts
45
109
  import { linkSellersToProductCategoryWorkflow } from "@mercurjs/core/workflows"
46
110
 
47
111
  await linkSellersToProductCategoryWorkflow(container).run({
48
- input: {
49
- id: "pcat_123",
50
- add: ["sel_123"],
51
- },
112
+ input: { id: "pcat_01JD8Q1234567", add: ["sel_01JD8Q1234567"] },
52
113
  })
53
114
  ```
54
115
 
55
- <Tip>
56
- Allowlisting grants the **right to sell**, not the listing itself. After a
57
- store is allowlisted, it still has to create an offer for the product to
58
- appear on its storefront.
59
- </Tip>
116
+ Mercur doesn't filter products by this link. Run `validateSellerProductPermissionsStep` in your own workflow if you want linked stores blocked from a category.
117
+
118
+ ## Next steps
119
+
120
+ - [Create a master product](/platform/catalog/guides/create-a-master-product) with `seller_ids` set from the start
121
+ - [Create an offer](/platform/offer/guides/create-an-offer) for an allowed store
122
+ - [Links reference](/platform/catalog/reference/links) for the `product_seller` and `category_seller` definitions