@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,40 +1,134 @@
1
1
  ---
2
- title: "Workflows"
2
+ title: "Catalog workflows"
3
3
  sidebarTitle: "Workflows"
4
- description: "Mercur's product create, review, and allowlist workflows."
4
+ description: "Mercur's product create, review, allowlist, and category workflows: inputs, hooks, and events."
5
5
  ---
6
6
 
7
- This reference lists the Mercur workflows for the Catalog domain. Import them
8
- from `@mercurjs/core/workflows` and run them against the Medusa container. They
9
- wrap Medusa's native product flows to add the marketplace layer: the store
10
- allowlist, the review lifecycle, and the audit trail.
7
+ Mercur ships 7 catalog workflows in `packages/core/src/workflows/product`. Import them from `@mercurjs/core/workflows` and run them with the Medusa container. For plain updates and deletes of products, variants, and categories, use Medusa's core flows, such as `updateProductsWorkflow`.
11
8
 
12
- ## Product workflows
9
+ ```ts
10
+ import { confirmProductsWorkflow } from "@mercurjs/core/workflows"
13
11
 
14
- | Workflow | Input | Purpose |
12
+ await confirmProductsWorkflow(container).run({
13
+ input: { product_ids: ["prod_01JD8Q1234567"] },
14
+ })
15
+ ```
16
+
17
+ ## createProductsWorkflow
18
+
19
+ Creates master products with attributes and variants, links stores to them, and records each submission. Workflow id `mercur-create-products`. Returns the created `ProductDTO[]` in input order.
20
+
21
+ | Input | Type | Description |
22
+ | --- | --- | --- |
23
+ | `products` | `(CreateProductDTO & { seller_ids?: string[] })[]` | Products to create. `CreateProductDTO` from `@mercurjs/types` replaces `options` with `attributes` |
24
+ | `products[].seller_ids` | `string[]`, optional | Stores to put on the product's allowlist |
25
+ | `created_by` | `string` | Submitter id, a seller id or a user id, stored on the audit record |
26
+ | `additional_data` | `Record<string, unknown>`, optional | Forwarded to Medusa's product and variant workflows and the `productsCreated` hook |
27
+
28
+ The workflow runs these steps in order:
29
+
30
+ 1. Runs the `validate` hook with `input` and `products`
31
+ 2. Creates the products with Medusa's `createProductsWorkflow`, each with an internal `__default__` option
32
+ 3. Attaches `attributes` with `addProductAttributesToProductWorkflow`
33
+ 4. Removes the `__default__` option from products that gained a variant axis
34
+ 5. Creates `variants` with `createProductVariantsWorkflow`, defaulting `manage_inventory` to `false`
35
+ 6. Links `seller_ids` with `associateSellersWithProductStep`
36
+ 7. Records a `PRODUCT_ADD` change per product with `recordProductAuditChangeWorkflow`
37
+ 8. Runs the `productsCreated` hook with `products` and `additional_data`
38
+ 9. Emits `product.created`
39
+
40
+ It exposes the `validate` and `productsCreated` hooks. It emits `product.created` with `{ id }` for each product.
41
+
42
+ ## confirmProductsWorkflow
43
+
44
+ Publishes `proposed` products. Workflow id `mercur-confirm-products`.
45
+
46
+ | Input | Type | Description |
15
47
  | --- | --- | --- |
16
- | `createProductsWorkflow` | `{ products[], created_by, additional_data? }` | Create master products, attach attributes/variants, record the submission, allowlist `seller_ids` |
17
- | `confirmProductsWorkflow` | `{ product_ids[], actor_id?, internal_note? }` | `proposed` → `published` |
18
- | `rejectProductWorkflow` | `{ product_id, message?, actor_id? }` | `proposed` → `rejected` |
19
- | `requestProductChangeWorkflow` | `{ product_id, message?, actor_id? }` | Ask the submitter for a revision (stays `proposed`) |
48
+ | `product_ids` | `string[]` | Products to publish. All must be `proposed` |
49
+ | `actor_id` | `string`, optional | Reviewer, stored as `created_by` and `confirmed_by` |
50
+ | `internal_note` | `string`, optional | Operator note on each audit record |
51
+ | `additional_data` | `Record<string, unknown>`, optional | Passed to the hook |
20
52
 
21
- ## Allowlist & organization workflows
53
+ Records a `STATUS_CHANGE` action with `{ status: "published" }` per product, then sets the status.
22
54
 
23
- | Workflow | Input | Purpose |
55
+ It exposes the `productsConfirmed` hook with `product_ids`, `internal_note`, and `additional_data`. It emits `product.published` with `{ id, internal_note }` for each product.
56
+
57
+ ## rejectProductWorkflow
58
+
59
+ Rejects one `proposed` product. Workflow id `mercur-reject-product`.
60
+
61
+ | Input | Type | Description |
62
+ | --- | --- | --- |
63
+ | `product_id` | `string` | Product to reject |
64
+ | `message` | `string`, optional | Reason, stored as the change's `external_note` |
65
+ | `actor_id` | `string`, optional | Reviewer |
66
+ | `additional_data` | `Record<string, unknown>`, optional | Passed to the hook |
67
+
68
+ Records a `STATUS_CHANGE` action with `{ status: "rejected" }`, then sets the status.
69
+
70
+ It exposes the `productRejected` hook with `product_id`, `message`, and `additional_data`. It emits `product.rejected` with `{ id, message }`.
71
+
72
+ ## requestProductChangeWorkflow
73
+
74
+ Asks the submitter to revise a `proposed` product without changing its status. Workflow id `mercur-request-product-change`.
75
+
76
+ | Input | Type | Description |
24
77
  | --- | --- | --- |
25
- | `linkSellersToProductWorkflow` | `{ id, add?, remove? }` | Add/remove stores on a product's allowlist |
26
- | `linkSellersToProductCategoryWorkflow` | `{ id, add?, remove? }` | Add/remove stores on a category's allowlist |
27
- | `assignProductsToCategoryWorkflow` | `{ id, add?, remove? }` | Assign products to a single category |
28
-
29
- <Note>
30
- For plain create/update/delete of products, variants, and categories, Mercur
31
- reuses Medusa's native core-flows (`createProductsWorkflow`,
32
- `updateProductsWorkflow`, `createProductVariantsWorkflow`, and so on) under the
33
- hood. The workflows above are the Mercur-specific entry points that add the
34
- allowlist, review lifecycle, and audit history.
35
- </Note>
36
-
37
- To work with records directly instead of through a workflow, see the
38
- [Service reference](/platform/catalog/reference/service). To run side effects
39
- when a product changes, see the
40
- [Event reference](/platform/catalog/reference/events).
78
+ | `product_id` | `string` | Product to send back |
79
+ | `message` | `string`, optional | Requested changes, stored as `external_note` |
80
+ | `actor_id` | `string`, optional | Reviewer |
81
+ | `additional_data` | `Record<string, unknown>`, optional | Passed to the hook |
82
+
83
+ Records a `CHANGE_REQUESTED` action with `{ message }`.
84
+
85
+ It exposes the `productChangeRequested` hook with `product_id`, `message`, and `additional_data`. It emits `product.change-requested` with `{ id, message, actor_id }`.
86
+
87
+ ## linkSellersToProductWorkflow
88
+
89
+ Adds stores to and removes stores from a product's allowlist. Workflow id `mercur-link-sellers-to-product`.
90
+
91
+ | Input | Type | Description |
92
+ | --- | --- | --- |
93
+ | `id` | `string` | Product id |
94
+ | `add` | `string[]`, optional | Seller ids to link |
95
+ | `remove` | `string[]`, optional | Seller ids to unlink |
96
+
97
+ Runs `associateSellersWithProductStep` and `detachSellersFromProductStep`, which undo their changes if the run fails. No hooks or events.
98
+
99
+ ## linkSellersToProductCategoryWorkflow
100
+
101
+ Adds and removes `category_seller` rows for a category. Workflow id `mercur-link-sellers-to-product-category`.
102
+
103
+ | Input | Type | Description |
104
+ | --- | --- | --- |
105
+ | `id` | `string` | Category id |
106
+ | `add` | `string[]`, optional | Seller ids to link |
107
+ | `remove` | `string[]`, optional | Seller ids to unlink |
108
+
109
+ Runs `associateSellersWithProductCategoryStep` and `detachSellersFromProductCategoryStep`. No hooks or events.
110
+
111
+ ## assignProductsToCategoryWorkflow
112
+
113
+ Moves products into one category. Workflow id `mercur-assign-products-to-category`.
114
+
115
+ | Input | Type | Description |
116
+ | --- | --- | --- |
117
+ | `id` | `string` | Category id |
118
+ | `add` | `string[]`, optional | Product ids. Each product's categories become `[id]` |
119
+ | `remove` | `string[]`, optional | Product ids. Each product's categories become empty |
120
+
121
+ Runs Medusa's `updateProductsWorkflow` with `category_ids`, so Medusa's product update events fire. No hooks of its own.
122
+
123
+ ## Exported steps
124
+
125
+ These steps are exported for use in your own workflows:
126
+
127
+ | Step | Does |
128
+ | --- | --- |
129
+ | `associateSellersWithProductStep` | Creates `product_seller` rows from `{ links: { product_id, seller_id }[] }` |
130
+ | `detachSellersFromProductStep` | Dismisses `product_seller` rows with the same input |
131
+ | `associateSellersWithProductCategoryStep` | Creates `category_seller` rows from `{ links: { category_id, seller_id }[] }` |
132
+ | `detachSellersFromProductCategoryStep` | Dismisses `category_seller` rows with the same input |
133
+ | `validateProductsStatusStep` | Throws `NOT_ALLOWED` unless every product in `products` has the `expected_status` |
134
+ | `validateSellerProductPermissionsStep` | Throws `NOT_ALLOWED` when `seller_id` is linked to any of `category_ids` |
@@ -1,65 +1,67 @@
1
1
  ---
2
- title: "Order commission lines"
2
+ title: "Commission lines on orders"
3
3
  sidebarTitle: "Order commission lines"
4
- description: "Per-order commission lines, how they're computed, and BigNumber precision."
4
+ description: "What a commission line records, when Mercur writes and rewrites it, and how payouts read it."
5
5
  ---
6
6
 
7
- This page covers how commission is recorded on an order and kept in sync as the
8
- order changes.
7
+ A commission line is the resolved commission for one line item or one shipping method of an order. It’s derived data: Mercur computes it from the current rates and the order’s contents, writes it at checkout, and rewrites it whenever the order changes. Payouts read these lines to work out what the seller receives.
9
8
 
10
- ## Commission line
9
+ ## What a line records
11
10
 
12
- A commission line is the resolved commission for a single order line. It's
13
- represented by the `CommissionLine` data model (table `commission_line`, id
14
- prefix `comline`). Each line anchors to either an item (`item_id`) or a shipping
15
- method (`shipping_method_id`). It records the rate that matched
16
- (`commission_rate_id`, `code`), the applied `rate`, and the computed `amount`.
11
+ A line is a `CommissionLine` record (id prefix `comline`). It points at exactly one of `item_id` or `shipping_method_id`, and it stores the rate it came from and the amount.
17
12
 
18
13
  ```ts
19
- // A commission line, as written by the refresh workflow
20
- {
21
- item_id: "ordli_123",
14
+ const line = {
15
+ id: "comline_01JD8Q1234567",
16
+ item_id: "ordli_01JD8Q1234567",
22
17
  shipping_method_id: null,
23
- commission_rate_id: "comrate_123",
24
- code: "standard-a1b2c3",
25
- rate: 10,
26
- amount: 250, // 10% of a 2500 subtotal
18
+ commission_rate_id: "comrate_01JD8Q1234567",
19
+ provider_id: "system",
20
+ code: "acme-apparel-k3x9qa",
21
+ rate: 12,
22
+ amount: 5.64,
27
23
  description: null,
24
+ data: null,
28
25
  }
29
26
  ```
30
27
 
31
- Lines are generated automatically during checkout. When the cart is split into
32
- per-seller orders, `refreshOrderCommissionLinesWorkflow` runs against each new
33
- order. Shipping lines carry a `"Shipping Commission"` description. Item lines
34
- carry none.
35
-
36
- ## Recomputed on change
37
-
38
- Commission lines are **derived** data, not a one-time snapshot. The lines are
39
- recomputed whenever an order's composition changes, such as when an order edit
40
- is confirmed, a return is received, or a claim or exchange is created. That
41
- keeps the seller's commission tracking what the customer actually kept.
42
-
43
- The refresh is **idempotent**: it deletes any existing lines for the affected
44
- items and shipping methods, then inserts the freshly computed set. Re-running it
45
- never duplicates lines.
46
-
47
- <Note>
48
- Computed lines carry no `id`. The module's `upsertCommissionLines` deletes by
49
- anchor (`item_id` / `shipping_method_id`) before inserting, which is what
50
- makes repeated refreshes safe.
51
- </Note>
52
-
53
- ## BigNumber precision
54
-
55
- All commission arithmetic uses Medusa's `MathBN` (BigNumber). It's arbitrary
56
- precision, so percentages and per-currency amounts never accumulate
57
- floating-point error. A percentage amount is `subtotal × value ÷ 100`. A fixed
58
- amount is the per-currency value (or the fallback `value`). Each is computed in
59
- BigNumber before being stored.
60
-
61
- <Tip>
62
- Commission lines feed the payout pipeline: when a seller's payout is
63
- calculated, the order's commission lines are read straight from the commission
64
- module and deducted from the amount transferred to the seller.
65
- </Tip>
28
+ The fields carry these values from the `system` provider:
29
+
30
+ - **`code`**: the code of the winning rate, kept so the line stays readable if the rate is renamed or deleted
31
+ - **`rate`**: the percent for a percentage rate, or the fixed amount for a fixed rate
32
+ - **`amount`**: the commission for the line, computed with `MathBN`
33
+ - **`description`**: `Shipping Commission` on shipping lines, empty on item lines
34
+ - **`provider_id`**: the provider that computed the line, `system` by default
35
+ - **`data`**: free JSON a custom provider can fill
36
+
37
+ ## When lines are written
38
+
39
+ Mercur writes lines at checkout and rewrites them when the order’s contents change. Both paths run `refreshOrderCommissionLinesWorkflow`:
40
+
41
+ | Trigger | Where it runs |
42
+ | --- | --- |
43
+ | Cart completed | A nested step of `completeCartWithSplitOrdersWorkflow`, for every seller order in the group |
44
+ | `order-edit.confirmed` | The `order-commission-refresh-handler` subscriber |
45
+ | `order.return_received` | The same subscriber |
46
+ | `order.claim_created` | The same subscriber |
47
+ | `order.exchange_created` | The same subscriber |
48
+
49
+ A refresh resolves against the rates that exist when it runs. Changing a rate doesn’t touch existing lines until the order is refreshed again.
50
+
51
+ ## A refresh replaces, it doesn’t append
52
+
53
+ Computed lines carry no `id`. The module’s `upsertCommissionLines` first deletes every existing line whose `item_id` or `shipping_method_id` is in the new set, then inserts the new lines in the same transaction. Running a refresh twice leaves one line per item and shipping method.
54
+
55
+ <Warning>
56
+ Deletion is keyed on the anchors in the new set. If an item no longer gets a line, for example because you disabled the only rate that matched it, its old line stays in place.
57
+ </Warning>
58
+
59
+ ## How payouts use the lines
60
+
61
+ `createPayoutWorkflow` reads every commission line whose `item_id` or `shipping_method_id` belongs to the order. It sums their `amount` and transfers the order `total` minus that sum to the seller’s payout account. With the lines from the [overview figure](/platform/commission/overview), an order total of € 92.00 and commission of € 10.14 pays the seller € 81.86.
62
+
63
+ ## Related
64
+
65
+ - [Refresh an order’s lines from your own code](/platform/commission/guides/refresh-order-commission-lines)
66
+ - [The order events that trigger a refresh](/platform/commission/reference/events)
67
+ - [All `CommissionLine` fields](/platform/commission/reference/data-models)
@@ -1,86 +1,76 @@
1
1
  ---
2
- title: "Rule matching"
2
+ title: "How Mercur picks a commission rate"
3
3
  sidebarTitle: "Rule matching"
4
- description: "The five dimensions, most-specific-wins resolution, tie-breaks, and shipping."
4
+ description: "The five rule dimensions, AND and OR matching, most-specific-wins, the oldest-rate tie-break, and shipping."
5
5
  ---
6
6
 
7
- This page covers how Mercur decides which commission rate applies to a given
8
- order line.
7
+ Rule matching decides which single rate commissions each order line. Mercur finds every enabled rate whose rules fit the line’s product and seller, then keeps the one scoped on the most dimensions. Knowing the algorithm lets you predict a seller’s commission before an order exists.
9
8
 
10
- ## Commission rule
9
+ <img className="block dark:hidden" src="/images/figures/commission-rule-matching-light.svg" alt="Resolving the rate for a Linen shirt sold by ACME in the Shirts category: four enabled rates match, the acme-shirts rate scopes on two dimensions and wins, lighting does not match, and the default rate ranks last." />
10
+ <img className="hidden dark:block" src="/images/figures/commission-rule-matching-dark.svg" alt="Resolving the rate for a Linen shirt sold by ACME in the Shirts category: four enabled rates match, the acme-shirts rate scopes on two dimensions and wins, lighting does not match, and the default rate ranks last." />
11
11
 
12
- A commission rule scopes a rate to part of the catalog. It's represented by the
13
- `CommissionRule` data model (table `commission_rule`, id prefix `comrule`). A
14
- rule is a `reference` / `reference_id` pair that belongs to one rate. The
15
- `reference` names the dimension, and the `reference_id` names the specific
16
- record.
12
+ ## Each dimension checks one product field
13
+
14
+ A rule compares its `reference_id` with one field of the line’s product. The product comes from the order line, and the seller comes from the offer the customer bought.
15
+
16
+ | `reference` | Matches when |
17
+ | --- | --- |
18
+ | `product` | `product.id` equals `reference_id` |
19
+ | `product_type` | `product.type_id` equals `reference_id` |
20
+ | `product_collection` | `product.collection_id` equals `reference_id` |
21
+ | `product_category` | Any of `product.categories` has the id `reference_id` |
22
+ | `seller` | The offer’s `seller_id` equals `reference_id` |
23
+
24
+ Products are a shared catalog that no seller owns, which is why the `seller` dimension reads the offer on the line and not the product.
25
+
26
+ <Warning>
27
+ The module stores `reference` as free text and doesn’t validate it. A rule with any other `reference`, such as a typo, never matches, so the whole rate never matches.
28
+ </Warning>
29
+
30
+ ## AND across dimensions, OR within one
31
+
32
+ Mercur groups a rate’s rules by `reference`. The rate matches a line when every group has at least one rule that matches. Rules in the same group are alternatives, and groups narrow each other.
17
33
 
18
34
  ```ts
19
- await batchCommissionRulesWorkflow(container).run({
20
- input: {
21
- commission_rate_id: "comrate_123",
22
- create: [
23
- { reference: "seller", reference_id: "sel_123" },
24
- { reference: "product_category", reference_id: "pcat_shoes" },
25
- ],
26
- },
27
- })
35
+ const rules = [
36
+ { reference: "seller", reference_id: "sel_01JD8Q1234567" },
37
+ { reference: "product_category", reference_id: "pcat_01JD8Q1234567" },
38
+ { reference: "product_category", reference_id: "pcat_01JD8Q1234568" },
39
+ ]
28
40
  ```
29
41
 
30
- A rate with **no** rules is a catch-all that matches every line. A rate with
31
- rules only matches lines that satisfy them.
42
+ A rate with these rules matches lines sold by that seller in either category. It doesn’t match the seller’s lines in other categories, or other sellers’ lines in those categories.
32
43
 
33
- ## The five dimensions
44
+ ## The most specific rate wins
34
45
 
35
- A rule's `reference` is one of five dimensions, each resolved against the order
36
- line's product:
46
+ When several rates match, Mercur sorts them by specificity and keeps the first. Specificity is the number of distinct dimensions a rate’s rules use, not the number of rules. The rules above give a specificity of 2.
37
47
 
38
- | `reference` | Matches when |
39
- | --- | --- |
40
- | `product` | The line's product id equals `reference_id` |
41
- | `product_type` | The product's type id equals `reference_id` |
42
- | `product_collection` | The product's collection id equals `reference_id` |
43
- | `product_category` | One of the product's categories equals `reference_id` |
44
- | `seller` | The seller behind the line's offer equals `reference_id` |
48
+ Mercur resolves each line in this order:
45
49
 
46
- <Note>
47
- Products are the shared master catalog. The `seller` dimension resolves
48
- through the **offer** on the order line (`item.offer.seller_id`), not through
49
- product ownership.
50
- </Note>
50
+ 1. Drop disabled rates and rates whose `currency_code` is set to another currency.
51
+ 2. Keep the rates that match the line. A rate without rules always matches.
52
+ 3. Sort by specificity, highest first.
53
+ 4. Break ties by `created_at`, oldest first.
54
+ 5. Apply the first rate to the line’s base amount.
51
55
 
52
- ## Most-specific-wins
56
+ The global rate has no rules, so its specificity is 0 and any matching scoped rate beats it. If no enabled rate matches, the line gets no commission line.
53
57
 
54
- When several rates match a line, resolution is **AND across dimensions, OR
55
- within a dimension**. Rules are grouped by `reference`. A rate matches only when
56
- **every** group it defines has at least one matching rule. Among the matching
57
- rates, the one scoped on the **most distinct dimensions** wins.
58
+ <Warning>
59
+ The tie-break also applies between rule-less rates. If you create a second rate without rules, it competes with the global rate at specificity 0, and the older of the two wins.
60
+ </Warning>
58
61
 
59
- ```
60
- Rate A: seller = sel_123 (specificity 1)
61
- Rate B: seller = sel_123 AND category = pcat_shoes (specificity 2) ← wins
62
- ```
62
+ ## Shipping uses the global rate only
63
63
 
64
- <Tip>
65
- Specificity is the count of **distinct dimensions** a rate scopes on, not the
66
- number of rules. Two `product_category` rules on one rate still count as a
67
- single dimension (they OR together).
68
- </Tip>
64
+ Shipping methods aren’t matched against rules. If the global rate has `include_shipping` set, every shipping method on the order gets a line from it, with the description `Shipping Commission`. Otherwise shipping isn’t commissioned.
69
65
 
70
- ## Tie-break
66
+ The global rate’s `include_tax` flag applies to shipping too, adding the method’s `tax_total` to its `subtotal`. For a fixed global rate, each shipping method gets the fixed amount.
71
67
 
72
- When two matching rates have equal specificity, the **oldest** rate wins. Rates
73
- are evaluated `created_at` ascending, so the earliest-created rate is the
74
- deterministic winner.
68
+ ## Rates load once per batch
75
69
 
76
- ## Shipping commission
70
+ The `system` provider loads all enabled rates with their rules and values in one query per batch of orders. It then resolves every line of every order in memory. Adding rates doesn’t add queries per line.
77
71
 
78
- Item commission is resolved per line as above. Shipping is different: a
79
- shipping method is commissioned **only** by the global rate, and only when its
80
- `include_shipping` flag is on. No scoped rate can commission shipping.
72
+ ## Related
81
73
 
82
- <Note>
83
- `include_tax` is a separate, per-rate toggle. When on, the line's `tax_total`
84
- is added to the base amount before the rate is applied. This holds for both
85
- item and shipping commission.
86
- </Note>
74
+ - [Rate types, per-currency values, and the global commission](/platform/commission/concepts/rules-and-rates)
75
+ - [Scope a rate with batchCommissionRulesWorkflow](/platform/commission/guides/batch-update-rules)
76
+ - [Write your own provider to replace this algorithm](/platform/commission/reference/service)
@@ -1,84 +1,86 @@
1
1
  ---
2
- title: "Rules & rates"
2
+ title: "Commission rates and rules"
3
3
  sidebarTitle: "Rules & rates"
4
- description: "The commission rate, its fixed and percentage forms, and per-currency amounts."
4
+ description: "How a commission rate expresses the marketplace's cut, how rules scope it, and what the global commission does."
5
5
  ---
6
6
 
7
- This page covers how a commission rate is modeled and the two ways it can
8
- express the marketplace's cut.
7
+ A commission rate is the cut the marketplace takes from a sale, and rules decide which lines it applies to. Every marketplace has one global rate that covers everything, and you add scoped rates on top of it for specific sellers or parts of the catalog. Getting this model right decides what every seller is paid.
9
8
 
10
- ## Commission rate
9
+ ## A rate is a type and a value
11
10
 
12
- A commission rate is the number the marketplace takes from a sale. It's
13
- represented by the `CommissionRate` data model (table `commission_rate`, id
14
- prefix `comrate`). A rate has a `name`, a unique `code`, a `type`, and a
15
- `value`, plus the `include_tax` and `include_shipping` toggles.
11
+ A rate is a `CommissionRate` record (id prefix `comrate`). Its `type` says how to read its `value`, and two flags change the base amount the value applies to.
16
12
 
17
- ```ts
18
- const { result } = await createCommissionRatesWorkflow(container).run({
19
- input: [
20
- {
21
- name: "Standard",
22
- type: CommissionRateType.PERCENTAGE,
23
- value: 10,
24
- },
25
- ],
26
- })
27
- ```
13
+ | `type` | How `value` is read | Line `amount` |
14
+ | --- | --- | --- |
15
+ | `percentage` | A percent, so `10` means 10% | `base × value ÷ 100` |
16
+ | `fixed` | A flat amount per line item | The per-currency amount, or `value` |
28
17
 
29
- A rate is either **percentage** or **fixed**, set by `type`
30
- (`CommissionRateType`):
18
+ The base amount is the line’s `subtotal`. A rate changes it with these flags:
31
19
 
32
- | Type | How `value` is read |
33
- | --- | --- |
34
- | `percentage` | A percent of the line's base amount (e.g. `10` → 10%) |
35
- | `fixed` | A flat amount deducted per line |
20
+ - **`include_tax`**: adds the line’s `tax_total` to the base before the rate applies
21
+ - **`include_shipping`**: lets the rate commission shipping methods, and only takes effect on the global rate
22
+ - **`is_enabled`**: disabled rates are skipped during resolution
36
23
 
37
- <Note>
38
- A rate's `code` is unique. When you create a rate without one, the module
39
- auto-generates a URL-safe code from the `name` (e.g. `"Standard"` →
40
- `standard-a1b2c3`).
41
- </Note>
42
-
43
- ## Per-currency amounts
24
+ <Warning>
25
+ A fixed rate applies once per line item, not once per unit. A line with a quantity of 3 still gets one fixed amount. The `system` provider doesn’t cap a fixed amount at the line total.
26
+ </Warning>
44
27
 
45
- A fixed rate can carry a different amount for each currency. This is
46
- represented by the `CommissionRateValue` data model (table
47
- `commission_rate_value`, id prefix `comval`). Each value pairs a `currency_code`
48
- with an `amount`, and the calculation picks the value matching the order's
49
- currency.
28
+ Each rate also has a `name` and a unique `code`. When you create a rate without a `code`, the module builds one from the name plus a random suffix, so `"Standard"` becomes something like `standard-k3x9qa`.
50
29
 
51
30
  ```ts
31
+ import { CommissionRateType } from "@mercurjs/types"
32
+ import { createCommissionRatesWorkflow } from "@mercurjs/core/workflows"
33
+
52
34
  await createCommissionRatesWorkflow(container).run({
53
35
  input: [
54
- {
55
- name: "Flat fee",
56
- type: CommissionRateType.FIXED,
57
- value: 5, // fallback when no per-currency value matches
58
- values: [
59
- { currency_code: "usd", amount: 5 },
60
- { currency_code: "eur", amount: 4 },
61
- ],
62
- },
36
+ { name: "Standard", type: CommissionRateType.PERCENTAGE, value: 10 },
63
37
  ],
64
38
  })
65
39
  ```
66
40
 
67
- <Tip>
68
- When no `values` entry matches the order's currency, a fixed rate falls back
69
- to its scalar `value`. Percentage rates ignore `values` entirely. A percent
70
- is currency-independent.
71
- </Tip>
41
+ The example creates a 10% rate with no rules, which matches every line until a more specific rate wins.
42
+
43
+ ## Per-currency amounts for fixed rates
44
+
45
+ A fixed rate holds one amount per currency in `CommissionRateValue` records (id prefix `comval`). During resolution, Mercur picks the value whose `currency_code` equals the order’s currency. When none matches, it uses the rate’s own `value`.
46
+
47
+ Percentage rates ignore `values`, because a percent is the same in every currency. A rate’s own `currency_code` field is separate: when it’s set, the rate only applies to orders in that currency.
48
+
49
+ | Rate | `values` | Order in `usd` | Order in `gbp` |
50
+ | --- | --- | --- | --- |
51
+ | fixed, `value: 3` | `usd: 2.5`, `eur: 2` | 2.5 per line | 3 per line |
52
+ | percentage, `value: 10` | ignored | 10% of base | 10% of base |
53
+
54
+ ## Rules scope a rate
55
+
56
+ A rule is a `CommissionRule` record (id prefix `comrule`) that belongs to one rate. It’s a `reference` naming a dimension and a `reference_id` naming the record, for example `seller` and `sel_01JD8Q1234567`. A rate without rules matches every line. A rate with rules matches only the lines that satisfy them.
57
+
58
+ The five dimensions are:
59
+
60
+ - **`product`**: a master product id
61
+ - **`product_type`**: a product type id
62
+ - **`product_collection`**: a collection id
63
+ - **`product_category`**: a category id
64
+ - **`seller`**: the seller of the offer on the order line
65
+
66
+ [Rule matching](/platform/commission/concepts/rule-matching) explains how rules combine and which rate wins when several match.
72
67
 
73
68
  ## The global commission
74
69
 
75
- Every marketplace has exactly one **Global Commission**. This is the rate with
76
- `is_default` set to `true`. Mercur seeds it at boot (a `0%` percentage rate
77
- named `Default`) so a rate always exists, and it applies whenever no
78
- more-specific rate matches a line.
70
+ The global commission is the rate with `is_default: true`. The module seeds it on boot when none exists, as a 0% percentage rate named `Default` with the code `default`. You raise it to set the marketplace’s baseline cut.
71
+
72
+ The global rate has rules that set it apart:
73
+
74
+ - **Always present**: `deleteCommissionRatesWorkflow` throws `NOT_ALLOWED` if you try to delete it
75
+ - **Fallback for items**: it has no rules, so it matches every line and loses to any scoped rate that also matches
76
+ - **Only shipping rate**: its `include_shipping` flag is the only one that produces shipping commission lines
79
77
 
80
78
  <Note>
81
- The global rate is also the **only** rate that can commission shipping. See
82
- [Rule matching](/platform/commission/concepts/rule-matching) for how
83
- specificity and shipping are resolved.
79
+ Nothing stops you from setting `include_shipping` on a scoped rate, but the flag has no effect there. Shipping is resolved from the global rate alone.
84
80
  </Note>
81
+
82
+ ## Related
83
+
84
+ - [How Mercur picks one rate for each line](/platform/commission/concepts/rule-matching)
85
+ - [Create a rate from your server code](/platform/commission/guides/create-a-rate)
86
+ - [All `CommissionRate` fields](/platform/commission/reference/data-models)