@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.
- package/content/learn/architecture.mdx +166 -263
- package/content/learn/introduction.mdx +9 -1
- package/content/learn/use-cases.mdx +70 -0
- package/content/platform/attribute/concepts/attribute-types.mdx +73 -46
- package/content/platform/attribute/concepts/global-vs-inline.mdx +54 -32
- package/content/platform/attribute/concepts/variant-axes.mdx +54 -32
- package/content/platform/attribute/guides/attach-attributes-to-a-product.mdx +96 -53
- package/content/platform/attribute/guides/create-a-variant-axis.mdx +106 -57
- package/content/platform/attribute/guides/create-an-attribute.mdx +105 -56
- package/content/platform/attribute/overview.mdx +37 -33
- package/content/platform/attribute/reference/data-models.mdx +53 -38
- package/content/platform/attribute/reference/events.mdx +47 -24
- package/content/platform/attribute/reference/links.mdx +98 -27
- package/content/platform/attribute/reference/service.mdx +71 -32
- package/content/platform/attribute/reference/workflows.mdx +158 -28
- package/content/platform/catalog/concepts/master-products.mdx +39 -37
- package/content/platform/catalog/concepts/product-seller-allowlist.mdx +56 -35
- package/content/platform/catalog/concepts/status-lifecycle.mdx +55 -49
- package/content/platform/catalog/concepts/variants-categories-collections.mdx +55 -35
- package/content/platform/catalog/guides/allowlist-stores.mdx +100 -37
- package/content/platform/catalog/guides/create-a-master-product.mdx +124 -48
- package/content/platform/catalog/guides/publish-or-reject-a-product.mdx +120 -55
- package/content/platform/catalog/overview.mdx +39 -35
- package/content/platform/catalog/reference/data-models.mdx +154 -43
- package/content/platform/catalog/reference/events.mdx +41 -25
- package/content/platform/catalog/reference/links.mdx +196 -28
- package/content/platform/catalog/reference/service.mdx +57 -35
- package/content/platform/catalog/reference/workflows.mdx +124 -30
- package/content/platform/commission/concepts/order-commission-lines.mdx +54 -52
- package/content/platform/commission/concepts/rule-matching.mdx +53 -63
- package/content/platform/commission/concepts/rules-and-rates.mdx +61 -59
- package/content/platform/commission/guides/batch-update-rules.mdx +69 -41
- package/content/platform/commission/guides/create-a-rate.mdx +101 -53
- package/content/platform/commission/guides/refresh-order-commission-lines.mdx +104 -35
- package/content/platform/commission/overview.mdx +43 -35
- package/content/platform/commission/reference/data-models.mdx +59 -45
- package/content/platform/commission/reference/events.mdx +28 -34
- package/content/platform/commission/reference/links.mdx +42 -33
- package/content/platform/commission/reference/service.mdx +103 -28
- package/content/platform/commission/reference/workflows.mdx +101 -27
- package/content/platform/offer/concepts/pricing-and-inventory.mdx +63 -49
- package/content/platform/offer/concepts/shipping.mdx +28 -29
- package/content/platform/offer/concepts/what-is-an-offer.mdx +48 -45
- package/content/platform/offer/guides/bulk-create-offers.mdx +97 -61
- package/content/platform/offer/guides/create-an-offer.mdx +112 -60
- package/content/platform/offer/guides/manage-offer-inventory.mdx +78 -38
- package/content/platform/offer/overview.mdx +39 -32
- package/content/platform/offer/reference/data-models.mdx +77 -47
- package/content/platform/offer/reference/events.mdx +44 -18
- package/content/platform/offer/reference/links.mdx +92 -40
- package/content/platform/offer/reference/service.mdx +66 -31
- package/content/platform/offer/reference/workflows.mdx +146 -37
- package/content/platform/order-group/concepts/computed-totals.mdx +40 -37
- package/content/platform/order-group/concepts/order-splitting.mdx +41 -39
- package/content/platform/order-group/concepts/the-order-group.mdx +39 -32
- package/content/platform/order-group/guides/list-order-groups.mdx +96 -56
- package/content/platform/order-group/guides/retrieve-an-order-group.mdx +93 -42
- package/content/platform/order-group/guides/split-a-cart.mdx +115 -40
- package/content/platform/order-group/overview.mdx +42 -32
- package/content/platform/order-group/reference/data-models.mdx +48 -31
- package/content/platform/order-group/reference/events.mdx +29 -17
- package/content/platform/order-group/reference/links.mdx +90 -29
- package/content/platform/order-group/reference/service.mdx +55 -31
- package/content/platform/order-group/reference/workflows.mdx +83 -24
- package/content/platform/payout/concepts/account-lifecycle.mdx +64 -67
- package/content/platform/payout/concepts/accounts-and-onboarding.mdx +50 -40
- package/content/platform/payout/concepts/payout-pipeline.mdx +70 -73
- package/content/platform/payout/guides/create-a-payout-account.mdx +65 -41
- package/content/platform/payout/guides/process-a-provider-webhook.mdx +84 -38
- package/content/platform/payout/guides/start-provider-onboarding.mdx +90 -46
- package/content/platform/payout/overview.mdx +43 -34
- package/content/platform/payout/reference/data-models.mdx +63 -36
- package/content/platform/payout/reference/events.mdx +56 -30
- package/content/platform/payout/reference/links.mdx +52 -22
- package/content/platform/payout/reference/service.mdx +72 -33
- package/content/platform/payout/reference/workflows.mdx +80 -19
- package/content/platform/product-edit/concepts/change-actions.mdx +52 -45
- package/content/platform/product-edit/concepts/change-pipeline.mdx +60 -52
- package/content/platform/product-edit/concepts/status-and-auto-confirm.mdx +43 -46
- package/content/platform/product-edit/guides/confirm-or-decline-a-change.mdx +104 -56
- package/content/platform/product-edit/guides/edit-a-product.mdx +126 -46
- package/content/platform/product-edit/guides/request-a-revision.mdx +99 -39
- package/content/platform/product-edit/overview.mdx +41 -34
- package/content/platform/product-edit/reference/data-models.mdx +73 -47
- package/content/platform/product-edit/reference/events.mdx +39 -29
- package/content/platform/product-edit/reference/links.mdx +32 -24
- package/content/platform/product-edit/reference/service.mdx +45 -26
- package/content/platform/product-edit/reference/workflows.mdx +173 -31
- package/content/platform/review/concepts/product-vs-seller-reviews.mdx +42 -41
- package/content/platform/review/concepts/ratings-and-moderation.mdx +45 -44
- package/content/platform/review/concepts/the-review-model.mdx +52 -45
- package/content/platform/review/guides/compute-aggregate-ratings.mdx +62 -37
- package/content/platform/review/guides/create-a-review.mdx +109 -39
- package/content/platform/review/guides/moderate-a-review.mdx +83 -37
- package/content/platform/review/guides/respond-to-a-review.mdx +85 -40
- package/content/platform/review/overview.mdx +43 -33
- package/content/platform/review/reference/data-models.mdx +41 -27
- package/content/platform/review/reference/events.mdx +55 -37
- package/content/platform/review/reference/links.mdx +80 -22
- package/content/platform/review/reference/service.mdx +61 -33
- package/content/platform/review/reference/workflows.mdx +80 -24
- package/content/platform/store/concepts/lifecycle.mdx +68 -44
- package/content/platform/store/concepts/store-entity.mdx +45 -27
- package/content/platform/store/concepts/team.mdx +56 -35
- package/content/platform/store/guides/create-a-store.mdx +102 -35
- package/content/platform/store/guides/manage-the-team.mdx +122 -32
- package/content/platform/store/guides/moderate-a-store.mdx +107 -38
- package/content/platform/store/overview.mdx +41 -34
- package/content/platform/store/reference/data-models.mdx +132 -57
- package/content/platform/store/reference/events.mdx +42 -17
- package/content/platform/store/reference/links.mdx +67 -49
- package/content/platform/store/reference/service.mdx +105 -33
- package/content/platform/store/reference/workflows.mdx +304 -24
- package/content/references/api/conventions.mdx +20 -0
- package/content/resources/integrations/magento2.mdx +162 -0
- package/content/resources/integrations/overview.mdx +10 -0
- package/content/resources/integrations/stripe-connect.mdx +1 -1
- package/content/resources/tutorials/custom-api-route.mdx +3 -2
- package/llms.txt +113 -111
- 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: "
|
|
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
|
-
|
|
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
|
-
##
|
|
9
|
+
## One product, sold through offers
|
|
11
10
|
|
|
12
|
-
A product is
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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 {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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: "
|
|
2
|
+
title: "Restrict a product to chosen stores"
|
|
3
3
|
sidebarTitle: "The allowlist"
|
|
4
|
-
description: "
|
|
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
|
-
|
|
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
|
-
##
|
|
9
|
+
## Open by default, restricted by rows
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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:
|
|
25
|
+
const { data: [product] } = await query.graph({
|
|
19
26
|
entity: "product",
|
|
20
27
|
fields: ["id", "title", "sellers.id", "sellers.name"],
|
|
21
|
-
filters: { id: "
|
|
28
|
+
filters: { id: "prod_01JD8Q1234567" },
|
|
22
29
|
})
|
|
30
|
+
|
|
31
|
+
const isOpen = (product.sellers ?? []).length === 0
|
|
23
32
|
```
|
|
24
33
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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: "
|
|
2
|
+
title: "Product status lifecycle"
|
|
3
3
|
sidebarTitle: "Status lifecycle"
|
|
4
|
-
description: "How a master product moves from draft
|
|
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
|
-
|
|
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
|
-
##
|
|
9
|
+
## The four statuses
|
|
11
10
|
|
|
12
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
2
|
+
title: "Variants, categories, and collections"
|
|
3
3
|
sidebarTitle: "Variants & categories"
|
|
4
|
-
description: "The
|
|
4
|
+
description: "The Medusa Product module models that structure the shared catalog, and the places Mercur extends them."
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
##
|
|
9
|
+
## Variants come from attributes
|
|
11
10
|
|
|
12
|
-
A variant is
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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 |
|
|
36
|
-
| --- | --- |
|
|
37
|
-
| `ProductCategory` |
|
|
38
|
-
| `ProductCollection` | Flat,
|
|
39
|
-
| `ProductTag` | Free-form labels
|
|
40
|
-
| `ProductType` |
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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: "
|
|
2
|
+
title: "Restrict a product to chosen stores"
|
|
3
3
|
sidebarTitle: "Allowlist stores"
|
|
4
|
-
description: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11
|
+
Know how the list behaves before you change it:
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
104
|
+
## Link stores to a category
|
|
40
105
|
|
|
41
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|