@mercurjs/docs 2.3.8-canary.3 → 2.3.8-canary.4
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/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/llms.txt +111 -111
- package/package.json +1 -1
|
@@ -1,72 +1,86 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: "
|
|
2
|
+
title: "How offer prices and stock work"
|
|
3
3
|
sidebarTitle: "Pricing & inventory"
|
|
4
|
-
description: "Offer
|
|
4
|
+
description: "Offer prices live on the variant's shared price set behind an offer_id rule, and offer stock lives in inventory items linked to the offer."
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
without owning the master variant.
|
|
7
|
+
Several stores sell the same master variant, so prices and stock can’t live on the variant alone. Mercur keeps one price set per variant and tags each price row with the offer that owns it. Stock goes the other way: each offer gets its own inventory items, linked to the offer and never to the variant.
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
<img className="block dark:hidden" src="/images/figures/offer-price-rules-light.svg" alt="The shared price set of the Linen shirt M variant holds price rows for three offers; each row carries an offer_id rule, and the two rows for offer_01JD8QA form a quantity tier. Behind it, that offer links to its own inventory items." />
|
|
10
|
+
<img className="hidden dark:block" src="/images/figures/offer-price-rules-dark.svg" alt="The shared price set of the Linen shirt M variant holds price rows for three offers; each row carries an offer_id rule, and the two rows for offer_01JD8QA form a quantity tier. Behind it, that offer links to its own inventory items." />
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
## Prices on the shared price set
|
|
13
|
+
|
|
14
|
+
An offer has no price set of its own. Its prices are rows on the master variant’s `PriceSet`, and each row carries a price rule with the attribute `offer_id` set to the offer’s id.
|
|
15
|
+
|
|
16
|
+
When an offer is created, `ensureVariantPriceSetsStep` gives the variant a price set if it has none. The workflow then adds each price with the `offer_id` rule merged into any rules you pass, and links every new price to the offer through the `offer ↔ price` link. That link is what `offer.prices` reads.
|
|
17
17
|
|
|
18
18
|
```ts
|
|
19
|
-
// Every offer price row is written with an offer_id rule on the shared price set
|
|
20
19
|
prices: [
|
|
21
|
-
{
|
|
22
|
-
|
|
23
|
-
currency_code: "usd",
|
|
24
|
-
rules: { offer_id: "offer_123" },
|
|
25
|
-
},
|
|
20
|
+
{ amount: 4700, currency_code: "eur" },
|
|
21
|
+
{ amount: 4200, currency_code: "eur", min_quantity: 10 },
|
|
26
22
|
]
|
|
27
23
|
```
|
|
28
24
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
These two rows give the offer a quantity tier. Each price accepts `min_quantity`, `max_quantity`, and extra `rules`. You never write the `offer_id` rule yourself: the workflow adds it.
|
|
26
|
+
|
|
27
|
+
## How the right price is picked
|
|
28
|
+
|
|
29
|
+
Carts resolve offer prices through Medusa’s pricing context. Mercur hooks into `setPricingContext` on `addToCartWorkflow`, `updateLineItemInCartWorkflow`, and `refreshCartItemsWorkflow` and adds `offer_id` with the ids of the offers in the cart.
|
|
30
|
+
|
|
31
|
+
The pricing module then matches only the rows whose `offer_id` rule fits. If none of the items being added resolves to an offer, `addToCartWorkflow` fails with `Every cart line item must resolve to an offer_id`.
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
Because prices sit on the shared price set scoped by `offer_id`, the master
|
|
36
|
-
variant is never mutated per store. The variant keeps a single price set, and
|
|
37
|
-
the `offer_id` rule partitions it per offer.
|
|
38
|
-
</Note>
|
|
33
|
+
## Replacing an offer’s prices
|
|
39
34
|
|
|
40
|
-
|
|
35
|
+
Updating prices replaces the offer’s own rows and leaves every other offer on the variant alone. `upsertOfferPricesWorkflow` does the work, and `updateOffersWorkflow` runs it when an entry has a `prices` array.
|
|
41
36
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
The incoming array is the full new list:
|
|
38
|
+
|
|
39
|
+
- **Row with an `id`**: updates that price in place
|
|
40
|
+
- **Row without an `id`**: adds a new price with the `offer_id` rule
|
|
41
|
+
- **Owned price left out**: removes the price and its link
|
|
42
|
+
- **`id` owned by another offer**: the run fails with `Price … does not belong to offer …`
|
|
47
43
|
|
|
48
44
|
<Warning>
|
|
49
|
-
|
|
50
|
-
is empty for offer-based orders. Always resolve stock through
|
|
51
|
-
`offer.inventory_items`, never through the variant.
|
|
45
|
+
Removed prices are deleted, not soft-deleted. The `offerPricesUpserted` hook payload carries `previous_prices`, which is the only place the old values can still be read.
|
|
52
46
|
</Warning>
|
|
53
47
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
in
|
|
48
|
+
## Inventory linked to the offer
|
|
49
|
+
|
|
50
|
+
Offer stock lives in Medusa `InventoryItem` records linked to the offer. The `offer ↔ inventory_item` link is many-to-many and its table, `offer_inventory_item`, has a `required_quantity` column (default `1`).
|
|
51
|
+
|
|
52
|
+
One offer can draw on several inventory items, which makes it a kit. Ordering 1 unit of the offer reserves `required_quantity` units of each linked item.
|
|
53
|
+
|
|
54
|
+
<Warning>
|
|
55
|
+
`variant.inventory_items` is empty for offers. Read stock through the offer, never through the variant.
|
|
56
|
+
</Warning>
|
|
57
|
+
|
|
58
|
+
The link has two read paths in `query.graph`:
|
|
59
|
+
|
|
60
|
+
- **`inventory_items`**: the `InventoryItem` rows themselves, without the pivot columns
|
|
61
|
+
- **`inventory_item_link`**: the pivot rows, with `required_quantity` and a nested `inventory_item`
|
|
57
62
|
|
|
58
63
|
```ts
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
required_quantity
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
const { data: [offer] } = await query.graph({
|
|
65
|
+
entity: "offer",
|
|
66
|
+
fields: [
|
|
67
|
+
"inventory_item_link.required_quantity",
|
|
68
|
+
"inventory_item_link.inventory_item.location_levels.*",
|
|
69
|
+
],
|
|
70
|
+
filters: { id: "offer_01JD8Q1234567" },
|
|
71
|
+
})
|
|
66
72
|
```
|
|
67
73
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
## Inventory created with the offer
|
|
75
|
+
|
|
76
|
+
`createOffersWorkflow` creates fresh inventory items for every offer. Each `inventory_items` entry becomes one new item with optional starting `stock_levels`, linked to the offer and to the store.
|
|
77
|
+
|
|
78
|
+
Every offer needs at least one entry. The new item takes the variant’s title, and when the offer has a single item with `required_quantity` 1, it also copies the variant’s weight, dimensions, `hs_code`, `mid_code`, `origin_country`, and `material`.
|
|
79
|
+
|
|
80
|
+
The offer row’s `manage_inventory` and `allow_backorder` flags drive the stock check. When `manage_inventory` is `true` and `allow_backorder` is `false`, adding the offer to a cart confirms `required_quantity` × quantity for each linked item. To change which items back an existing offer, use `batchOfferInventoryItemsWorkflow`.
|
|
81
|
+
|
|
82
|
+
## Related
|
|
83
|
+
|
|
84
|
+
- [Manage offer inventory](/platform/offer/guides/manage-offer-inventory)
|
|
85
|
+
- [Price and inventory links](/platform/offer/reference/links)
|
|
86
|
+
- [upsertOfferPricesWorkflow and its hook](/platform/offer/reference/workflows)
|
|
@@ -1,48 +1,47 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: "
|
|
2
|
+
title: "How an offer ships"
|
|
3
3
|
sidebarTitle: "Shipping"
|
|
4
|
-
description: "
|
|
4
|
+
description: "Each offer names its store's shipping profile, and Mercur links the master product to a profile so multi-store carts keep their shipping methods."
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Every offer ships with a shipping profile that belongs to its store. The profile ties the offer’s items to that store’s shipping options, so in a cart with items from several stores, each store ships its own items at its own rates.
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## The offer’s shipping profile
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
at checkout, so each store fulfills its slice of a multi-seller cart with its own
|
|
15
|
-
rates.
|
|
11
|
+
An offer stores its profile in `shipping_profile_id`, a required field on the `Offer` row. A read-only link on that field exposes the profile as `offer.shipping_profile` in `query.graph`.
|
|
12
|
+
|
|
13
|
+
You set the profile when you create the offer and can change it with `updateOffersWorkflow`:
|
|
16
14
|
|
|
17
15
|
```ts
|
|
18
|
-
await
|
|
16
|
+
await updateOffersWorkflow(container).run({
|
|
19
17
|
input: {
|
|
20
18
|
offers: [
|
|
21
|
-
{
|
|
22
|
-
seller_id: "sel_123",
|
|
23
|
-
created_by: "mem_123",
|
|
24
|
-
variant_id: "variant_123",
|
|
25
|
-
shipping_profile_id: "sp_123",
|
|
26
|
-
sku: "ACME-WIDGET-01",
|
|
27
|
-
prices: [{ amount: 2500, currency_code: "usd" }],
|
|
28
|
-
inventory_items: [{ stock_levels: [{ location_id: "sloc_1", stocked_quantity: 50 }] }],
|
|
29
|
-
},
|
|
19
|
+
{ id: "offer_01JD8Q1234567", shipping_profile_id: "sp_01JD8Q1234567" },
|
|
30
20
|
],
|
|
31
21
|
},
|
|
32
22
|
})
|
|
33
23
|
```
|
|
34
24
|
|
|
35
|
-
|
|
25
|
+
This run moves one offer to a different profile and emits `offer.updated`. The offer workflows don’t check that the profile belongs to the offer’s store, so validate that in your route when the input comes from a seller.
|
|
26
|
+
|
|
27
|
+
## Shipping profiles on master products
|
|
28
|
+
|
|
29
|
+
Master products don’t carry a shipping profile of their own, but Medusa’s cart refresh keeps a shipping method only when a product in the cart matches the option’s profile. Without a profile on the product, multi-store carts would lose their shipping methods on every refresh.
|
|
30
|
+
|
|
31
|
+
`createOffersWorkflow` closes that gap. For each master product in the run, it links the product to the offer’s shipping profile when the product has no profile yet:
|
|
36
32
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
shipping profile. The product↔profile link is one-to-one, so the first offer's
|
|
41
|
-
profile wins for a given master product. Later offers (including other stores'
|
|
42
|
-
offers on the same product) skip the link if a profile is already attached.
|
|
33
|
+
- **Product without a profile**: the first offer in the run sets it
|
|
34
|
+
- **Product that already has a profile**: the link is skipped, even when another store’s offer names a different profile
|
|
35
|
+
- **Several offers on one product in one run**: only the first offer’s profile is linked
|
|
43
36
|
|
|
44
37
|
<Note>
|
|
45
|
-
The one-to-one
|
|
46
|
-
the source of truth for how a store ships. Each offer still carries its own
|
|
47
|
-
`shipping_profile_id`, which is what drives that store's fulfillment.
|
|
38
|
+
The product to profile link is one-to-one and exists only for the cart refresh. Each offer’s own `shipping_profile_id` is still what decides how that store ships.
|
|
48
39
|
</Note>
|
|
40
|
+
|
|
41
|
+
`updateOffersWorkflow` doesn’t touch the product link, so changing an offer’s profile later leaves the product’s profile as it was.
|
|
42
|
+
|
|
43
|
+
## Related
|
|
44
|
+
|
|
45
|
+
- [What is an offer](/platform/offer/concepts/what-is-an-offer)
|
|
46
|
+
- [The offer to shipping profile link](/platform/offer/reference/links)
|
|
47
|
+
- [Create an offer](/platform/offer/guides/create-an-offer)
|
|
@@ -1,65 +1,68 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "What is an offer"
|
|
3
3
|
sidebarTitle: "What is an offer"
|
|
4
|
-
description: "The offer record, how it points at a master
|
|
4
|
+
description: "The offer record, how it points at a master variant, its SKU rules, and the conditions a store can list under."
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
master catalog.
|
|
7
|
+
An offer is a store’s listing of one master product variant. The catalog product belongs to the marketplace, and the offer holds the store’s commercial terms on top of it: SKU, barcodes, condition, shipping profile, prices, and stock. You need this split in mind before you build anything that sells, because carts, orders, commissions, and payouts all follow the offer, not the product.
|
|
9
8
|
|
|
10
|
-
##
|
|
9
|
+
## The offer record
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
The `Offer` data model (table `offer`, id prefix `offer`) is a thin row that points at catalog records by id. Prices and inventory aren’t columns on it: they attach through module links.
|
|
12
|
+
|
|
13
|
+
The row carries these groups of fields:
|
|
14
|
+
|
|
15
|
+
- **Ownership**: `seller_id` is the store, and `created_by` is the member who created the offer
|
|
16
|
+
- **Catalog pointers**: `variant_id` and `product_id`, where `product_id` is read from the variant when the offer is created
|
|
17
|
+
- **Identity**: `sku`, plus optional `ean` and `upc` barcodes copied from the variant when you leave them out
|
|
18
|
+
- **Selling terms**: `shipping_profile_id`, `manage_inventory` (default `true`), `allow_backorder` (default `false`), and `leadtime_to_ship`
|
|
19
|
+
- **Condition**: an optional `condition_id` that points at an `OfferCondition`
|
|
18
20
|
|
|
19
21
|
```ts
|
|
20
|
-
const {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
seller_id: "sel_123",
|
|
25
|
-
created_by: "mem_123",
|
|
26
|
-
variant_id: "variant_123",
|
|
27
|
-
shipping_profile_id: "sp_123",
|
|
28
|
-
sku: "ACME-WIDGET-01",
|
|
29
|
-
prices: [{ amount: 2500, currency_code: "usd" }],
|
|
30
|
-
inventory_items: [{ stock_levels: [{ location_id: "sloc_1", stocked_quantity: 50 }] }],
|
|
31
|
-
},
|
|
32
|
-
],
|
|
33
|
-
},
|
|
22
|
+
const { data: [offer] } = await query.graph({
|
|
23
|
+
entity: "offer",
|
|
24
|
+
fields: ["id", "sku", "seller.name", "product_variant.title"],
|
|
25
|
+
filters: { id: "offer_01JD8Q1234567" },
|
|
34
26
|
})
|
|
35
27
|
```
|
|
36
28
|
|
|
37
|
-
|
|
38
|
-
**Products are not seller-owned.** The master product and its variants live in
|
|
39
|
-
the shared catalog. An offer never modifies the product. It points at a
|
|
40
|
-
variant and layers the store's own commercial terms on top.
|
|
41
|
-
</Note>
|
|
29
|
+
This query reads one offer with its store and master variant. `seller` and `product_variant` resolve through read-only links on `seller_id` and `variant_id`.
|
|
42
30
|
|
|
43
|
-
##
|
|
31
|
+
## Offers and master products
|
|
44
32
|
|
|
45
|
-
A
|
|
46
|
-
`Offer` carries the identity of that particular listing:
|
|
33
|
+
A master variant can back any number of offers. Each store that sells the variant holds its own offer, and the variant itself never changes when a store lists, prices, or stocks it.
|
|
47
34
|
|
|
48
|
-
|
|
49
|
-
- `seller_id`: the store that owns the offer (read-only link to the store).
|
|
50
|
-
- `sku`: the store's own stock-keeping unit for this listing.
|
|
51
|
-
- `ean` / `upc`: barcodes, snapshotted off the linked variant when not supplied.
|
|
35
|
+
The same store can also list a variant more than once, for example a new and a refurbished copy. The module doesn’t limit offers per variant. The only uniqueness rule is the SKU.
|
|
52
36
|
|
|
53
37
|
<Tip>
|
|
54
|
-
A
|
|
55
|
-
while `deleted_at IS NULL`). Two different stores may reuse the same SKU string
|
|
56
|
-
for their own offers.
|
|
38
|
+
A SKU is unique within a store: the index on `(seller_id, sku)` applies while `deleted_at` is null. Two stores can use the same SKU string, and a soft-deleted offer frees its SKU.
|
|
57
39
|
</Tip>
|
|
58
40
|
|
|
59
|
-
##
|
|
41
|
+
## Offer conditions
|
|
42
|
+
|
|
43
|
+
An offer condition describes the physical state an offer is sold in. Operators manage conditions, and stores pick one when they create or update an offer.
|
|
44
|
+
|
|
45
|
+
The `OfferCondition` model (id prefix `ofcond`) has a unique `code`, a display `label`, an `is_active` flag, and a `rank` that orders the list. The create and update workflows check every `condition_id` they receive:
|
|
46
|
+
|
|
47
|
+
- **Unknown id**: the run fails with a not found error
|
|
48
|
+
- **Inactive condition**: the run fails, so a deactivated condition can’t be assigned to new or edited offers
|
|
49
|
+
- **Condition in use**: `deleteOfferConditionsWorkflow` refuses to delete a condition while any offer still uses it
|
|
50
|
+
|
|
51
|
+
Deactivating a condition leaves the offers already listed under it unchanged.
|
|
52
|
+
|
|
53
|
+
## Grouping offers by store
|
|
54
|
+
|
|
55
|
+
A store’s offers on one product usually mirror that product’s variants. The service can collapse them into one row per product and store.
|
|
56
|
+
|
|
57
|
+
When you pass `group_by_seller: true` to `listOffers` or `listAndCountOffers`, the service returns the newest offer of each `(product_id, seller_id)` group and sets two computed fields on it:
|
|
58
|
+
|
|
59
|
+
- **`variant_count`**: how many offers the store has on that product
|
|
60
|
+
- **`offer_ids`**: the ids of all offers in the group
|
|
61
|
+
|
|
62
|
+
Only `product_id` and `seller_id` filters apply in grouped mode.
|
|
63
|
+
|
|
64
|
+
## Related
|
|
60
65
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
list for the grouped `(product, seller)` row. The dashboards use it to act on
|
|
65
|
-
all of a store's offers on a product at once.
|
|
66
|
+
- [How offer prices and stock work](/platform/offer/concepts/pricing-and-inventory)
|
|
67
|
+
- [How an offer ships](/platform/offer/concepts/shipping)
|
|
68
|
+
- [Offer and OfferCondition fields](/platform/offer/reference/data-models)
|
|
@@ -1,65 +1,84 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: "
|
|
2
|
+
title: "Create and update offers in bulk"
|
|
3
3
|
sidebarTitle: "Bulk-create offers"
|
|
4
|
-
description: "List
|
|
4
|
+
description: "List a store’s master variants in a single createOffersWorkflow run, then update or delete them in bulk."
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
```ts title="src/api/custom/bulk/route.ts"
|
|
17
|
-
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
18
|
-
import { createOffersWorkflow } from "@mercurjs/core/workflows"
|
|
19
|
-
|
|
20
|
-
export async function POST(req: MedusaRequest, res: MedusaResponse) {
|
|
21
|
-
const { result } = await createOffersWorkflow(req.scope).run({
|
|
22
|
-
input: {
|
|
23
|
-
offers: [
|
|
24
|
-
{
|
|
25
|
-
seller_id: "sel_123",
|
|
26
|
-
created_by: "mem_123",
|
|
27
|
-
variant_id: "variant_a",
|
|
28
|
-
shipping_profile_id: "sp_123",
|
|
29
|
-
sku: "ACME-A-01",
|
|
30
|
-
prices: [{ amount: 2500, currency_code: "usd" }],
|
|
31
|
-
inventory_items: [{ stock_levels: [{ location_id: "sloc_1", stocked_quantity: 50 }] }],
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
seller_id: "sel_123",
|
|
35
|
-
created_by: "mem_123",
|
|
36
|
-
variant_id: "variant_b",
|
|
37
|
-
shipping_profile_id: "sp_123",
|
|
38
|
-
sku: "ACME-B-01",
|
|
39
|
-
prices: [{ amount: 4000, currency_code: "usd" }],
|
|
40
|
-
inventory_items: [{ stock_levels: [{ location_id: "sloc_1", stocked_quantity: 20 }] }],
|
|
41
|
-
},
|
|
42
|
-
],
|
|
43
|
-
},
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
res.status(201).json({ offers: result })
|
|
47
|
-
}
|
|
48
|
-
```
|
|
7
|
+
Pass a list of entries to `createOffersWorkflow` when you onboard a store’s catalog or run an import. One run creates every offer or none of them.
|
|
8
|
+
|
|
9
|
+
## Before you begin
|
|
10
|
+
|
|
11
|
+
You need these records in place:
|
|
12
|
+
|
|
13
|
+
- **A store and a member**: the `seller_id` and the `created_by` member id for every entry
|
|
14
|
+
- **Master variants**: one `variant_id` per row you import
|
|
15
|
+
- **A shipping profile and a stock location**: both owned by the store
|
|
49
16
|
|
|
50
17
|
<Warning>
|
|
51
|
-
The
|
|
52
|
-
omits its inventory items, or reuses an existing `(seller_id, sku)` pair, the
|
|
53
|
-
run fails and its steps are compensated, so no partial offers are left behind.
|
|
18
|
+
Keep each run to one store. The workflow links all new inventory items to the store of the first entry, so mixing stores in one run assigns the other stores’ stock to the wrong store.
|
|
54
19
|
</Warning>
|
|
55
20
|
|
|
56
|
-
##
|
|
21
|
+
## Import a store’s rows
|
|
22
|
+
|
|
23
|
+
<Steps>
|
|
24
|
+
<Step title="Map your rows to offer entries">
|
|
25
|
+
Turn each source row into a `CreateOfferDTO` from `@mercurjs/types`.
|
|
26
|
+
|
|
27
|
+
```ts title="src/lib/to-offers.ts"
|
|
28
|
+
import type { CreateOfferDTO } from "@mercurjs/types"
|
|
29
|
+
|
|
30
|
+
type Row = { variant_id: string; sku: string; price: number; qty: number }
|
|
31
|
+
|
|
32
|
+
export const toOffers = (rows: Row[]): CreateOfferDTO[] =>
|
|
33
|
+
rows.map((row) => ({
|
|
34
|
+
seller_id: "sel_01JD8Q1234567",
|
|
35
|
+
created_by: "mem_01JD8Q1234567",
|
|
36
|
+
shipping_profile_id: "sp_01JD8Q1234567",
|
|
37
|
+
variant_id: row.variant_id,
|
|
38
|
+
sku: row.sku,
|
|
39
|
+
prices: [{ amount: row.price, currency_code: "eur" }],
|
|
40
|
+
inventory_items: [
|
|
41
|
+
{
|
|
42
|
+
stock_levels: [
|
|
43
|
+
{ location_id: "sloc_01JD8Q1234567", stocked_quantity: row.qty },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
}))
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Each entry is independent and carries its own SKU, price, and starting stock.
|
|
51
|
+
</Step>
|
|
52
|
+
<Step title="Run the workflow once for the batch">
|
|
53
|
+
Call the workflow with the mapped entries.
|
|
54
|
+
|
|
55
|
+
```ts title="src/api/admin/custom/offers/import/route.ts"
|
|
56
|
+
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
57
|
+
import { createOffersWorkflow } from "@mercurjs/core/workflows"
|
|
58
|
+
import { toOffers } from "../../../../../lib/to-offers"
|
|
59
|
+
|
|
60
|
+
export async function POST(req: MedusaRequest, res: MedusaResponse) {
|
|
61
|
+
const { rows } = req.body as { rows: Parameters<typeof toOffers>[0] }
|
|
62
|
+
|
|
63
|
+
const { result } = await createOffersWorkflow(req.scope).run({
|
|
64
|
+
input: { offers: toOffers(rows) },
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
res.status(201).json({ count: result.length })
|
|
68
|
+
}
|
|
69
|
+
```
|
|
57
70
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
71
|
+
The run emits one `offer.created` event per offer.
|
|
72
|
+
</Step>
|
|
73
|
+
</Steps>
|
|
74
|
+
|
|
75
|
+
<Note>
|
|
76
|
+
The batch is all or nothing. A missing variant, an entry without inventory items, or a SKU the store already uses fails the run, and compensation removes everything it created.
|
|
77
|
+
</Note>
|
|
78
|
+
|
|
79
|
+
## Update offers in bulk
|
|
80
|
+
|
|
81
|
+
`updateOffersWorkflow` takes the same array shape, keyed by offer `id`. Every field except `id` is optional.
|
|
63
82
|
|
|
64
83
|
```ts
|
|
65
84
|
import { updateOffersWorkflow } from "@mercurjs/core/workflows"
|
|
@@ -67,18 +86,35 @@ import { updateOffersWorkflow } from "@mercurjs/core/workflows"
|
|
|
67
86
|
await updateOffersWorkflow(req.scope).run({
|
|
68
87
|
input: {
|
|
69
88
|
offers: [
|
|
70
|
-
{ id: "
|
|
89
|
+
{ id: "offer_01JD8Q1234567", sku: "ACME-LS-M-2" },
|
|
71
90
|
{
|
|
72
|
-
id: "
|
|
73
|
-
prices: [{ amount:
|
|
91
|
+
id: "offer_01JD8Q1234568",
|
|
92
|
+
prices: [{ amount: 3900, currency_code: "eur" }],
|
|
74
93
|
},
|
|
75
94
|
],
|
|
95
|
+
updated_by: "mem_01JD8Q1234567",
|
|
76
96
|
},
|
|
77
97
|
})
|
|
78
98
|
```
|
|
79
99
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
100
|
+
The first entry renames a SKU and keeps its prices. The second replaces that offer’s prices: send an existing price `id` to update a row in place, since rows left out are deleted. `updated_by` reaches the `offersUpdated` and `offerPricesUpserted` hooks.
|
|
101
|
+
|
|
102
|
+
## Delete offers in bulk
|
|
103
|
+
|
|
104
|
+
`deleteOffersWorkflow` takes a list of ids and soft-deletes the offer rows.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { deleteOffersWorkflow } from "@mercurjs/core/workflows"
|
|
108
|
+
|
|
109
|
+
await deleteOffersWorkflow(req.scope).run({
|
|
110
|
+
input: { ids: ["offer_01JD8Q1234567", "offer_01JD8Q1234568"] },
|
|
111
|
+
})
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The run emits `offer.deleted` per offer. Its `offersDeleted` hook gets a snapshot of each offer and its prices taken before the delete. The workflow has no step that removes the offer’s prices or inventory items.
|
|
115
|
+
|
|
116
|
+
## Next steps
|
|
117
|
+
|
|
118
|
+
- [Manage offer inventory](/platform/offer/guides/manage-offer-inventory)
|
|
119
|
+
- [How offer prices are replaced](/platform/offer/concepts/pricing-and-inventory)
|
|
120
|
+
- [Offer events](/platform/offer/reference/events)
|