@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,72 +1,86 @@
1
1
  ---
2
- title: "Pricing & inventory"
2
+ title: "How offer prices and stock work"
3
3
  sidebarTitle: "Pricing & inventory"
4
- description: "Offer-scoped prices on the shared price set, and inventory linked to the 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
- In this document, you'll learn how an offer carries its own price and inventory
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
- ## Offer-scoped pricing
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
- An offer's prices don't live on a private price set. They live on the **master
13
- variant's shared `PriceSet`**, with each offer-owned row scoped by a
14
- `PriceRule` on the `offer_id` attribute. That's how many stores price the same
15
- variant independently: every price row Mercur writes for an offer is stamped with
16
- that offer's id, and reads filter the set back down to just that offer's rows.
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
- amount: 2500,
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
- The offer side reads its price ladder through the writable `offer ↔ price`
30
- list-link, so `offer.prices` resolves in a single query traversal. Each row is a
31
- standard Medusa money amount and supports `min_quantity` / `max_quantity` for
32
- quantity-break pricing.
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
- <Note>
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
- ## Offer-scoped inventory
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
- An offer's stock is held in Medusa `InventoryItem` records that link to the
43
- **offer**, not to the variant. The `offer ↔ inventory_item` link is a list-link
44
- whose pivot table (`offer_inventory_item`) carries a `required_quantity` column,
45
- so one offer can draw on several inventory items, each with its own required
46
- quantity.
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
- Offer inventory links to the **offer**, not the variant. `variant.inventory_items`
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
- When you create an offer, its `inventory_items` entries each create a brand-new
55
- `InventoryItem` (with optional starting `stock_levels`) and link it to the offer
56
- in the same workflow run. An offer must have at least one inventory item.
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
- inventory_items: [
60
- {
61
- sku: "ACME-WIDGET-01",
62
- required_quantity: 1,
63
- stock_levels: [{ location_id: "sloc_1", stocked_quantity: 50 }],
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
- <Tip>
69
- The inventory items an offer creates are also linked to the store, so they show
70
- up under the store's inventory. To change the set of items on an existing
71
- offer, use the [batch inventory workflow](/platform/offer/guides/manage-offer-inventory).
72
- </Tip>
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: "Shipping"
2
+ title: "How an offer ships"
3
3
  sidebarTitle: "Shipping"
4
- description: "How an offer carries its own shipping profile."
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
- In this document, you'll learn how an offer determines how its items ship.
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
- ## Shipping profile
9
+ ## The offer’s shipping profile
10
10
 
11
- Every offer points at a store's own **shipping profile** through its
12
- `shipping_profile_id` field, joined via the read-only `offer ↔ shipping_profile`
13
- link. The profile is what ties the offer's items to the store's shipping options
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 createOffersWorkflow(container).run({
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
- ## Master products carry no profile
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
- Master products live in the shared catalog and have no shipping profile of their
38
- own. To keep multi-seller carts from losing their shipping methods on refresh,
39
- `createOffersWorkflow` links the offer's **master product** to the offer's
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 product↔profile link is a checkout-refresh accommodation, not
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 product variant, and its SKU."
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
- In this document, you'll learn what an offer is and how it relates to the shared
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
- ## Offer
9
+ ## The offer record
11
10
 
12
- An offer is a store's listing against a master product variant, represented by
13
- the `Offer` data model (table `offer`, id prefix `offer`). It's a thin
14
- marketplace-side record: it holds the store (`seller_id`), the master variant it
15
- points at (`variant_id`), the master product (`product_id`), the store's own
16
- `sku`, optional `ean` / `upc` barcodes, and the shipping profile it ships with.
17
- Price and inventory are attached through links rather than stored on the row.
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 { result } = await createOffersWorkflow(container).run({
21
- input: {
22
- offers: [
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
- <Note>
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
- ## Offer vs. master product
31
+ ## Offers and master products
44
32
 
45
- A single master variant can back many offers, one per store that sells it. The
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
- - `product_id` / `variant_id`: the master records the offer points at (joined through read-only links).
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 store's `sku` is **unique within that store** (`(seller_id, sku)`, enforced
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
- ## One offer per variant
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
- Each offer points at exactly one variant, so a store's offers on a product mirror
62
- that product's variants. When offers are grouped by store
63
- (`group_by_seller`), the service computes a `variant_count` and an `offer_ids`
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: "Bulk-create offers"
2
+ title: "Create and update offers in bulk"
3
3
  sidebarTitle: "Bulk-create offers"
4
- description: "List many offers against the master catalog in a single workflow run."
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
- In this guide, you'll learn how to create many offers at once, for example when
8
- onboarding a store's catalog or running a CSV import.
9
-
10
- `createOffersWorkflow` accepts an array of offers, so a single run can list a
11
- store against many master variants at once. Each entry is independent and carries
12
- its own SKU, prices, inventory, and shipping profile.
13
-
14
- ## Run the workflow with many offers
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 batch is validated as a whole: if any entry references a missing variant,
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
- ## Update many offers
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
- `updateOffersWorkflow` mirrors the same array shape for edits. Each entry is
59
- keyed by the offer `id`; supplying a `prices` array **replaces** the offer's
60
- price ladder (rows with an `id` are updated in place, rows without one are added,
61
- and omitted rows are removed), while leaving `prices` out keeps the ladder
62
- untouched.
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: "offer_a", sku: "ACME-A-02" },
89
+ { id: "offer_01JD8Q1234567", sku: "ACME-LS-M-2" },
71
90
  {
72
- id: "offer_b",
73
- prices: [{ amount: 3500, currency_code: "usd" }],
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
- <Tip>
81
- Both workflows emit one event per affected offer (`offer.created` /
82
- `offer.updated`). Subscribe to those events to run downstream side effects like
83
- re-indexing search. See the [Event reference](/platform/offer/reference/events).
84
- </Tip>
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)