@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,49 +1,191 @@
1
1
  ---
2
- title: "Workflows"
2
+ title: "Product Edit workflows"
3
3
  sidebarTitle: "Workflows"
4
- description: "Product change workflows, service methods, and events."
4
+ description: "Inputs, hooks, and events of every product change workflow, from staging to audit records."
5
5
  ---
6
6
 
7
- This reference lists the workflows for the Product Edit domain. Import them from
8
- `@mercurjs/core/workflows` and run them against the Medusa container.
7
+ This page lists the workflows that stage, resolve, apply, and record product changes. Import them from `@mercurjs/core/workflows` and run them with the Medusa container, for example `confirmProductChangeWorkflow(container).run({ input })`. Each section gives the workflow id, the input, the hooks, and the events.
9
8
 
10
- ## Lifecycle workflows
9
+ ## Edit workflows
11
10
 
12
- | Workflow | Input | Purpose |
11
+ These workflows load the current product, diff your input against it, check that the author has no pending change on the product, and stage the result through `stageProductChangeWorkflow`. They return the staged `ProductChangeDTO`.
12
+
13
+ ### productEditUpdateProductWorkflow
14
+
15
+ Id `product-edit-update-product`. Stages an `UPDATE` action per changed product field, and a `STATUS_CHANGE` action when `status` differs.
16
+
17
+ | Input | Type | Description |
13
18
  | --- | --- | --- |
14
- | `createProductChangeWorkflow` | `{ changes[], additional_data? }` | Create pending changes (rejects if one is already pending) |
15
- | `stageProductChangeWorkflow` | `{ product_id, actions[], created_by?, internal_note?, external_note?, auto_confirm? }` | Create a change with actions, then run auto-confirm |
16
- | `confirmProductChangeWorkflow` | `{ ids[], confirmed_by?, internal_note?, external_note? }` | Mark `confirmed` and apply pending actions |
17
- | `rejectProductChangeWorkflow` | `{ id, declined_by?, declined_reason? }` | Move a `pending` change to `declined` |
18
- | `cancelProductChangeWorkflow` | `{ id, canceled_by? }` | Move a `pending` change to `canceled` |
19
- | `autoConfirmProductChangeWorkflow` | `{ change_id, confirmed_by?, force? }` | Confirm when review is off, or when `force` is set |
19
+ | `product_id` | `string` | The product to edit |
20
+ | `created_by` | `string`, optional | The author. Scopes the pending check |
21
+ | `update` | `Record<string, unknown>` | Proposed values. Keys outside the diffed field list are ignored |
22
+ | `additional_data` | `Record<string, unknown>`, optional | Passed to the `productChangeCreated` hook |
20
23
 
21
- ## Apply workflows
24
+ Hooks: `validate` with `{ input }`, and `productChangeCreated` with `{ product_change, additional_data }`.
25
+
26
+ ### productEditUpdateVariantsWorkflow
27
+
28
+ Id `product-edit-update-variants`. Stages `VARIANT_ADD`, `VARIANT_UPDATE`, and `VARIANT_REMOVE` actions.
29
+
30
+ | Input | Type | Description |
31
+ | --- | --- | --- |
32
+ | `product_id` | `string` | The product to edit |
33
+ | `created_by` | `string`, optional | The author. Scopes the pending check |
34
+ | `operations` | `ProductEditVariantOperation[]` | `{ type: "add", variant }`, `{ type: "update", variant_id, fields }`, or `{ type: "remove", variant_id }` |
35
+
36
+ An update stages only the fields that differ, and `manage_inventory` is never staged. No hooks.
37
+
38
+ ### productEditUpdateAttributesWorkflow
39
+
40
+ Id `product-edit-update-attributes`. Stages `ATTRIBUTE_ADD`, `ATTRIBUTE_UPDATE`, and `ATTRIBUTE_REMOVE` actions, skipping entries that wouldn’t change the product.
41
+
42
+ | Input | Type | Description |
43
+ | --- | --- | --- |
44
+ | `product_id` | `string` | The product to edit |
45
+ | `created_by` | `string`, optional | The author. Scopes the pending check |
46
+ | `add` | `ProductAttributeBatchAdd[]`, optional | Attributes to attach |
47
+ | `remove` | `string[]`, optional | Attribute ids to detach |
48
+ | `update` | `ProductAttributeBatchUpdate[]`, optional | Changes to attached attributes |
49
+ | `additional_data` | `Record<string, unknown>`, optional | Passed to the `productChangeCreated` hook |
50
+
51
+ Hooks: `validate` with `{ input }`, and `productChangeCreated` with `{ product_change, additional_data }`.
52
+
53
+ ### productEditDeleteProductWorkflow
54
+
55
+ Id `product-edit-delete-product`. Stages a single `PRODUCT_DELETE` action.
56
+
57
+ | Input | Type | Description |
58
+ | --- | --- | --- |
59
+ | `product_id` | `string` | The product to delete |
60
+ | `created_by` | `string`, optional | The author. Scopes the pending check |
61
+ | `auto_confirm` | `boolean`, optional | Defaults to `true` when the product is a `draft`, else `false` |
62
+
63
+ No hooks. On confirm, the product and its offers are deleted.
64
+
65
+ ## Staging and resolution workflows
66
+
67
+ These workflows create a change and move it through its statuses.
68
+
69
+ ### stageProductChangeWorkflow
70
+
71
+ Id `stage-product-change`. Creates one `pending` change with the given actions, emits `product-change.created`, then runs `autoConfirmProductChangeWorkflow`. It doesn’t check for an open change.
72
+
73
+ | Input | Type | Description |
74
+ | --- | --- | --- |
75
+ | `product_id` | `string` | The product the change targets |
76
+ | `actions` | `Omit<CreateProductChangeActionDTO, "product_change_id">[]` | The actions to stage |
77
+ | `created_by` | `string`, optional | The author. Also used as `confirmed_by` on auto-confirm |
78
+ | `internal_note` | `string`, optional | Note for operators |
79
+ | `external_note` | `string`, optional | Note for the seller |
80
+ | `auto_confirm` | `boolean`, optional | Confirm and apply in the same run |
81
+
82
+ No hooks. Returns the created `ProductChangeDTO`, without its actions.
83
+
84
+ ### createProductChangeWorkflow
85
+
86
+ Id `create-product-change`. Creates `pending` changes without actions, after checking that none of the products has a pending change from the same author. When the batch spans several authors, the check covers every pending change on those products.
87
+
88
+ | Input | Type | Description |
89
+ | --- | --- | --- |
90
+ | `changes` | `CreateProductChangeDTO[]` | `product_id`, plus optional notes, `created_by`, and `metadata` |
91
+ | `additional_data` | `Record<string, unknown>`, optional | Passed to the `productChangeCreated` hook |
92
+
93
+ Hooks: `validate` with `{ input }`, and `productChangeCreated` with `{ changes, additional_data }`. Emits `product-change.created` per change.
22
94
 
23
- | Workflow | Input | Purpose |
95
+ ### autoConfirmProductChangeWorkflow
96
+
97
+ Id `auto-confirm-product-change`. Runs `confirmProductChangeWorkflow` when `force` is `true` or the `product_request` feature flag is off. Otherwise it does nothing.
98
+
99
+ | Input | Type | Description |
100
+ | --- | --- | --- |
101
+ | `change_id` | `string` | The change to confirm |
102
+ | `confirmed_by` | `string`, optional | Stamped on the change |
103
+ | `force` | `boolean`, optional | Confirm regardless of the flag |
104
+
105
+ ### confirmProductChangeWorkflow
106
+
107
+ Id `confirm-product-change`. Sets every change in `ids` to `confirmed`, runs `applyProductChangeActionsWorkflow`, and emits `product-change.confirmed` per change. Fails if any change is missing or not `pending`.
108
+
109
+ | Input | Type | Description |
24
110
  | --- | --- | --- |
25
- | `applyProductChangeActionsWorkflow` | `{ change_ids[] }` | Bucket not-yet-applied actions and dispatch them to product/variant workflows |
26
- | `applyProductAttributeChangeActionsWorkflow` | `{ product_id, add[], remove[], update[] }` | Apply the attribute batch for a change |
111
+ | `ids` | `string[]` | The changes to confirm |
112
+ | `confirmed_by` | `string`, optional | Stamped on each change |
113
+ | `internal_note` | `string`, optional | Overwrites the operator note |
114
+ | `external_note` | `string`, optional | Overwrites the seller note |
115
+ | `additional_data` | `Record<string, unknown>`, optional | Passed to the `productChangeConfirmed` hook |
27
116
 
28
- ## Edit helpers
117
+ Hooks: `validate` with `{ input }`, and `productChangeConfirmed` with `{ ids, additional_data }`.
29
118
 
30
- High-level workflows that diff your input and stage the right actions for you.
119
+ ### rejectProductChangeWorkflow
31
120
 
32
- | Workflow | Input | Purpose |
121
+ Id `reject-product-change`. Sets one `pending` change to `declined` and emits `product-change.declined`.
122
+
123
+ | Input | Type | Description |
33
124
  | --- | --- | --- |
34
- | `productEditUpdateProductWorkflow` | `{ product_id, update, created_by? }` | Diff and stage `UPDATE` actions per changed field |
35
- | `productEditUpdateVariantsWorkflow` | `{ product_id, ... }` | Stage `VARIANT_*` actions |
36
- | `productEditUpdateAttributesWorkflow` | `{ product_id, ... }` | Stage `ATTRIBUTE_*` actions |
37
- | `productEditDeleteProductWorkflow` | `{ product_id, ... }` | Stage a `PRODUCT_DELETE` action |
125
+ | `id` | `string` | The change to decline |
126
+ | `declined_by` | `string`, optional | Stamped on the change |
127
+ | `declined_reason` | `string`, optional | Reason for the seller |
128
+ | `additional_data` | `Record<string, unknown>`, optional | Passed to the `productChangeRejected` hook |
129
+
130
+ Hooks: `validate` with `{ input }`, and `productChangeRejected` with `{ id, additional_data }`.
38
131
 
39
- ## Audit-trail workflows
132
+ ### cancelProductChangeWorkflow
40
133
 
41
- | Workflow | Input | Purpose |
134
+ Id `cancel-product-change`. Sets one `pending` change to `canceled` and emits `product-change.canceled`.
135
+
136
+ | Input | Type | Description |
42
137
  | --- | --- | --- |
43
- | `recordProductAuditChangeWorkflow` | `{ actor_id?, changes[] }` | Record already-`confirmed` audit changes (actions stored `applied`) |
44
- | `requestProductChangeWorkflow` | `{ product_id, message?, actor_id? }` | Record a `CHANGE_REQUESTED` revision request on a `proposed` product |
138
+ | `id` | `string` | The change to cancel |
139
+ | `canceled_by` | `string`, optional | Stamped on the change |
140
+ | `additional_data` | `Record<string, unknown>`, optional | Passed to the `productChangeCanceled` hook |
141
+
142
+ Hooks: `validate` with `{ input }`, and `productChangeCanceled` with `{ id, additional_data }`.
143
+
144
+ ## Apply workflows
145
+
146
+ `confirmProductChangeWorkflow` runs these for you, so you rarely call them yourself.
147
+
148
+ ### applyProductChangeActionsWorkflow
149
+
150
+ Id `apply-product-change-actions`. Input `{ change_ids: string[] }`. Loads the unapplied actions of the given changes, groups them by type, runs Medusa’s product and variant workflows, deletes the offers of deleted products, and marks the handled actions `applied`. No hooks, no events of its own.
151
+
152
+ ### applyProductAttributeChangeActionsWorkflow
153
+
154
+ Id `apply-product-attribute-change-actions`. Input `{ product_id, add, remove, update }`. Runs `createAndLinkProductAttributesToProductWorkflow` when any array is non-empty. No hooks.
155
+
156
+ ## Audit workflows
157
+
158
+ These workflows record history that doesn’t need review.
159
+
160
+ ### recordProductAuditChangeWorkflow
161
+
162
+ Id `record-product-audit-change`. Creates changes that start `confirmed`, with `actor_id` as both `created_by` and `confirmed_by`, and stores every action with `applied: true`. It doesn’t touch the product and emits no event.
163
+
164
+ | Input | Type | Description |
165
+ | --- | --- | --- |
166
+ | `actor_id` | `string`, optional | The actor to stamp |
167
+ | `changes` | `array` | Each entry has `product_id`, `actions`, and optional `internal_note` and `external_note` |
168
+
169
+ ### requestProductChangeWorkflow
170
+
171
+ Id `mercur-request-product-change`. Checks that the product is `proposed`, records a `CHANGE_REQUESTED` audit change with the message as `external_note`, and emits `product.change-requested`.
172
+
173
+ | Input | Type | Description |
174
+ | --- | --- | --- |
175
+ | `product_id` | `string` | The proposed product |
176
+ | `message` | `string`, optional | What the seller should change |
177
+ | `actor_id` | `string`, optional | The operator making the request |
178
+ | `additional_data` | `Record<string, unknown>`, optional | Passed to the hook |
179
+
180
+ Hooks: `productChangeRequested` with `{ product_id, message, additional_data }`.
181
+
182
+ ## Steps
183
+
184
+ The module’s steps are exported from `@mercurjs/core/workflows` for your own workflows:
185
+
186
+ - **Create**: `createProductChangesStep`, `createProductChangeActionsStep`, `addProductChangeActionStep`
187
+ - **Resolve**: `confirmProductChangesStep`, `declineProductChangeStep`, `cancelProductChangeStep`
188
+ - **Update**: `updateProductChangeActionsStep`, `applyVariantImageLinksStep`
189
+ - **Validate**: `validateNoPendingProductChangeStep`, `validateProductChangeIsPendingStep`, `confirmProductChangeValidationStep`
45
190
 
46
- To work with records directly instead of through a workflow, see the
47
- [Service reference](/platform/product-edit/reference/service). To run side effects
48
- when a change resolves, see the
49
- [Event reference](/platform/product-edit/reference/events).
191
+ The create, resolve, and `updateProductChangeActionsStep` steps compensate by deleting or restoring what they wrote. `applyVariantImageLinksStep` has no compensation, and the validation steps write nothing.
@@ -1,63 +1,64 @@
1
1
  ---
2
- title: "Product vs seller reviews"
2
+ title: "Product reviews and store reviews"
3
3
  sidebarTitle: "Product vs seller reviews"
4
- description: "The reference discriminator and the links that anchor each review."
4
+ description: "How the reference field picks a review's target, and the four links that anchor every review."
5
5
  ---
6
6
 
7
- This page covers how one review model serves two targets and how each review is
8
- anchored to the rest of the marketplace.
7
+ A customer can rate the product they bought, the store that sold it, or both. Mercur uses one `Review` model for both cases and sets `reference` to `product` or `seller` to say which one a review rates. The target itself is stored as a module link, written when the review is created, alongside links to the order and the customer.
9
8
 
10
- ## Reference
9
+ ## The reference field picks the link
11
10
 
12
- The `reference` field on the `Review` model decides what a review is about. It's
13
- an enum with two values, `product` and `seller`, and it works together with the
14
- review's link to the target record. There is no polymorphic id column on the
15
- model itself; the target is resolved through a module link.
11
+ You pass `reference` and `reference_id` to `createReviewWorkflow`. The workflow stores `reference` on the review, then writes one link from `reference_id` to the review:
12
+
13
+ | `reference` | `reference_id` is | Link written |
14
+ | --- | --- | --- |
15
+ | `product` | A product id, such as `prod_01JD8Q1234567` | Product to review |
16
+ | `seller` | A seller id, such as `sel_01JD8Q1234567` | Seller to review |
17
+
18
+ `reference_id` isn’t stored on the review. To read the target back, follow the relation that matches `reference`:
16
19
 
17
20
  ```ts
18
- // A product review
19
- { reference: "product", reference_id: "prod_123", rating: 5 }
21
+ const { data: [review] } = await query.graph({
22
+ entity: "review",
23
+ fields: ["id", "reference", "rating", "product.title", "seller.name"],
24
+ filters: { id: "rev_01JD8Q1234567" },
25
+ })
20
26
 
21
- // A seller review
22
- { reference: "seller", reference_id: "sel_123", rating: 4 }
27
+ const target = review.reference === "product"
28
+ ? review.product
29
+ : review.seller
23
30
  ```
24
31
 
25
- When a review is created, the `reference` value selects which link is written:
26
- `reference: "product"` links the review to a product, and `reference: "seller"`
27
- links it to a seller. Reading a review back, you follow the matching relation
28
- (`review.product` or `review.seller`) to reach its target.
32
+ Only one of `review.product` and `review.seller` is set on any review.
29
33
 
30
34
  <Note>
31
- Products are the shared master catalog, not seller-owned. A **product** review
32
- rates the master product; a **seller** review rates the store. They are
33
- independent. A customer can leave both for the same order.
35
+ Products are Mercur’s shared master catalog, not owned by a store. A product review rates the master product, so it has no link to the store that sold it. Only seller reviews are linked to a seller.
34
36
  </Note>
35
37
 
36
- ## Order & customer links
38
+ ## Every review links to its order and customer
37
39
 
38
- Beyond its target, every review is anchored to the order that earned it and the
39
- customer who wrote it. Creating a review writes two more links, one to the
40
- `Order` and one to the `Customer`, so a review always has a verifiable purchase
41
- behind it.
40
+ Apart from its target, each review is linked to the order that earned it and the customer who wrote it. The `create-review` step writes both links in the same step that creates the record. These links back the ownership checks across the APIs:
42
41
 
43
- ```ts
44
- // resolved through the customer link when listing a customer's own reviews
45
- const { data } = await query.graph({
46
- entity: "customer_customer_review_review",
47
- fields: ["review.*"],
48
- filters: { customer_id: "cus_123" },
49
- })
50
- ```
42
+ - **Store API**: a customer can read, edit, and delete only reviews linked to their customer id
43
+ - **Vendor API**: a store can list, read, and answer only reviews linked to its seller id
44
+ - **Validation**: duplicate checks look up existing reviews through the order link
51
45
 
52
- ## One review per target, per order
46
+ ## Stores see only seller reviews
53
47
 
54
- Because a review is tied to an order, the create flow enforces that a customer
55
- can leave **at most one review per target per order**. Submitting a second review
56
- for the same `reference` and `reference_id` on the same order is rejected. The
57
- order must also belong to the customer submitting the review.
48
+ Because product reviews carry no seller link, the Vendor API never returns them. `GET /vendor/reviews` filters on the seller link, and answering a review checks the same link. A store can’t answer a review of a product it sells: only an operator can, through the Admin API.
49
+
50
+ ## One review per target per order
51
+
52
+ `createReviewWorkflow` rejects a second review of the same target from the same order. It loads every review linked to `order_id` and fails with “Review already exists” when one has the same `reference` and the same target id. It also fails with “Order not found!” when the order doesn’t belong to `customer_id`.
58
53
 
59
54
  <Tip>
60
- A single order can still produce several reviews, one per distinct target. For
61
- example, a customer may review the master product *and* the store that
62
- fulfilled it from the same order.
55
+ One order can back several reviews, one per distinct target. A customer can rate the master product and the store from the same order, as order `#1001` does in the overview figure.
63
56
  </Tip>
57
+
58
+ The workflow doesn’t check that `reference_id` appears in the order. A customer with any order can review any product or seller id. Add that check in your own route if your marketplace needs it.
59
+
60
+ ## Related
61
+
62
+ - [The four review links and their query.graph aliases](/platform/review/reference/links)
63
+ - [Respond to a review as a store](/platform/review/guides/respond-to-a-review)
64
+ - [What a review stores](/platform/review/concepts/the-review-model)
@@ -1,64 +1,65 @@
1
1
  ---
2
- title: "Ratings & moderation"
2
+ title: "How reviews are moderated and rated"
3
3
  sidebarTitle: "Ratings & moderation"
4
- description: "The review status lifecycle, store responses, and aggregate ratings."
4
+ description: "The review status values, who can change them, how a store answers, and how average ratings are computed."
5
5
  ---
6
6
 
7
- This page covers how a review moves from submission to a public rating, and how
8
- per-product and per-seller averages are computed.
7
+ A review reaches shoppers only after an operator approves it, and a store can add one public answer. Moderation is a change to the review’s `status` field, an answer is a write to its `seller_note`, and removal is a soft delete. Average ratings aren’t stored anywhere: the Review module service computes them from the review rows each time you ask.
9
8
 
10
- ## Status
9
+ ## Status values
11
10
 
12
- A review's state is held in the `status` field of the `Review` model. A review
13
- moves through three statuses:
14
-
15
- ```
16
- ┌──────────┐ publish ┌────────────┐
17
- │ pending │───────────►│ published │
18
- └────┬─────┘ └────────────┘
19
- │ reject
20
- ▼
21
- ┌────────────┐
22
- │ rejected │
23
- └────────────┘
24
- ```
11
+ A review’s `status` holds one of three values. Every review starts as `pending`, and only `updateReviewWorkflow` changes it.
25
12
 
26
13
  | Status | Meaning |
27
14
  | --- | --- |
28
- | `pending` | Submitted, awaiting moderation. The default on creation |
29
- | `published` | Approved and visible on the storefront |
30
- | `rejected` | Declined by a moderator |
15
+ | `pending` | Submitted and waiting for an operator. The default |
16
+ | `published` | Approved for display |
17
+ | `rejected` | Declined by an operator |
18
+
19
+ <img className="block dark:hidden" src="/images/figures/review-moderation-light.svg" alt="Behind, a review record. In front, the changes each workflow makes: updateReviewWorkflow moves status between pending, published, and rejected in any direction, respondReviewWorkflow sets seller_note once, and deleteReviewWorkflow sets deleted_at." />
20
+ <img className="hidden dark:block" src="/images/figures/review-moderation-dark.svg" alt="Behind, a review record. In front, the changes each workflow makes: updateReviewWorkflow moves status between pending, published, and rejected in any direction, respondReviewWorkflow sets seller_note once, and deleteReviewWorkflow sets deleted_at." />
21
+
22
+ The workflow enforces no order between statuses. An operator can publish a rejected review or move a published one back to `pending`. Who can set the status depends on the API:
23
+
24
+ - **Admin API**: `POST /admin/reviews/:id` sets `status`, `rating`, and `customer_note`
25
+ - **Vendor API**: a store can’t change `status` or `rating`; the respond route ignores both fields
26
+ - **Store API**: a customer can edit `rating` and `customer_note`, but not `status`
31
27
 
32
- A review is created as `pending`. Moderation moves it to `published` or
33
- `rejected` by updating the `status` field. See
34
- [Moderate a review](/platform/review/guides/moderate-a-review).
28
+ <Warning>
29
+ A customer edit doesn’t reset a published review to `pending`. If edited reviews should go back through moderation, wrap the Store route or `updateReviewWorkflow` and set the status yourself.
30
+ </Warning>
35
31
 
36
- ## Store responses
32
+ ## Store answers
37
33
 
38
- A store can attach a single public response to any of its reviews. The response
39
- lives in the `seller_note` field and is added through a dedicated respond flow,
40
- which refuses to overwrite an existing response.
34
+ A store answers a review by writing `seller_note` through `respondReviewWorkflow`. The workflow refuses a second answer: it fails when `seller_note` is already set, so the first answer stays until someone clears it with `updateReviewWorkflow`. Answering never touches `status`, so a store can answer a `pending` review before it’s published.
41
35
 
42
- <Note>
43
- Responding is separate from moderation. A store adds its `seller_note`, while
44
- the `status` transition (`published` / `rejected`) stays an operator decision.
45
- </Note>
36
+ Over HTTP, a store answers with `POST /vendor/reviews/:id`, which only accepts reviews linked to that store, so only seller reviews. An operator answers any review with `POST /admin/reviews/:id/respond`.
46
37
 
47
- ## Aggregate ratings
38
+ ## Deletion is soft
48
39
 
49
- The module service computes average ratings on demand rather than storing a
50
- denormalized column. `getAvgRating` returns the average for a single product or
51
- seller, and `getProductsWithRating` / `getSellersWithRating` return records with
52
- their average rating joined in for list views.
40
+ `deleteReviewWorkflow` soft deletes the review: it sets `deleted_at` and keeps the row and its links. `query.graph` and the APIs stop returning it, and the workflow restores it if a later step fails. Operators delete with `DELETE /admin/reviews/:id`, and customers delete their own reviews with `DELETE /store/reviews/:id`.
41
+
42
+ ## Average ratings are computed on read
43
+
44
+ The service’s rating helpers run an `AVG(rating)` query over the review table, joined through the product or seller link table. There’s no stored average to keep in sync. The helpers apply no filters beyond the target, which affects what counts:
45
+
46
+ - **Every status counts**: `pending` and `rejected` reviews are part of the average
47
+ - **Soft deleted reviews count**: the query doesn’t check `deleted_at`, and the link rows stay in place
48
+ - **No reviews**: `getAvgRating` returns `null`
53
49
 
54
50
  ```ts
55
- const service = container.resolve(MercurModules.REVIEW)
51
+ const reviewService = container.resolve(MercurModules.REVIEW)
56
52
 
57
- const avg = await service.getAvgRating("seller", "sel_123")
53
+ const average = await reviewService.getAvgRating(
54
+ "seller",
55
+ "sel_01JD8Q1234567"
56
+ )
58
57
  ```
59
58
 
60
- <Tip>
61
- Because averages are computed at query time, they always reflect the current
62
- set of reviews. There's no cache to invalidate when a review is added, removed,
63
- or moderated.
64
- </Tip>
59
+ This returns the store’s average as a numeric string, such as `"4.5000000000000000"`, or `null`. When only published reviews should count, compute the average from `query.graph` results instead, as shown in [Compute aggregate ratings](/platform/review/guides/compute-aggregate-ratings).
60
+
61
+ ## Related
62
+
63
+ - [Moderate a review from server code](/platform/review/guides/moderate-a-review)
64
+ - [Respond to a review](/platform/review/guides/respond-to-a-review)
65
+ - [Rating helper signatures](/platform/review/reference/service)
@@ -1,56 +1,63 @@
1
1
  ---
2
- title: "The review model"
2
+ title: "What a review stores"
3
3
  sidebarTitle: "The review model"
4
- description: "The single review entity, its rating, notes, and moderation status."
4
+ description: "The fields of a review: its target type, rating, the customer's note, the store's answer, and its status."
5
5
  ---
6
6
 
7
- This page covers the review record and the fields that make up a rating.
7
+ A review is one customer’s rating of one product or one store, earned by an order. Mercur stores it as a single `Review` record (table `review`, id prefix `rev`) in the Review module. Knowing its fields tells you what a review can hold, what a store can add to it, and what an operator changes when moderating it.
8
8
 
9
- ## Review
9
+ ## One record per rating
10
10
 
11
- A review is a customer's rating of a single target, either a product or a seller.
12
- It's represented by the `Review` data model (table `review`, id prefix `rev`). It
13
- holds the numeric rating, an optional customer note, an optional store response,
14
- and the moderation status.
11
+ Every review has the same shape, whatever it rates. The record holds the score and the text around it, while the product, store, order, and customer it belongs to live in module links rather than columns.
12
+
13
+ | Field | Holds |
14
+ | --- | --- |
15
+ | `id` | The review id, such as `rev_01JD8Q1234567` |
16
+ | `display_id` | An auto-incrementing number for people to read |
17
+ | `reference` | The target type: `product` or `seller` |
18
+ | `rating` | The score as an integer |
19
+ | `customer_note` | The customer’s text, or `null` |
20
+ | `seller_note` | The store’s answer, or `null` |
21
+ | `status` | `pending`, `published`, or `rejected` |
22
+
23
+ A review has no `product_id` or `seller_id` column. The `reference` value tells you which link to follow, as covered in [Product vs seller reviews](/platform/review/concepts/product-vs-seller-reviews).
24
+
25
+ ## Ratings are integers from 1 to 5 at the API
26
+
27
+ The `rating` column is an integer with no range check in the model itself. The Store and Admin API validators accept whole numbers from 1 to 5, so every review created over HTTP falls in that range:
28
+
29
+ - **Store API**: `rating` is required on create and update, and must be an integer from 1 to 5
30
+ - **Admin API**: `rating` is optional on update, with the same 1 to 5 range
31
+ - **Workflows**: `createReviewWorkflow` and `updateReviewWorkflow` store whatever number you pass
32
+
33
+ <Warning>
34
+ When you run the workflows from your own code, validate `rating` yourself. A value outside 1 to 5 is stored as is and skews every average that includes it.
35
+ </Warning>
36
+
37
+ ## Two notes, two authors
38
+
39
+ A review keeps the two sides of the conversation in separate fields. The customer writes `customer_note` when they submit or edit the review, and the store writes `seller_note` once, through `respondReviewWorkflow`. Both are nullable text fields marked searchable, so a `q` search in the Admin and Vendor APIs matches their content.
15
40
 
16
41
  ```ts
17
- const { result } = await createReviewWorkflow(container).run({
18
- input: {
19
- order_id: "order_123",
20
- reference: "product",
21
- reference_id: "prod_123",
22
- rating: 5,
23
- customer_note: "Exactly as described, fast shipping.",
24
- customer_id: "cus_123",
25
- },
26
- })
42
+ const review = {
43
+ id: "rev_01JD8Q1234567",
44
+ display_id: 1042,
45
+ reference: "product",
46
+ rating: 4,
47
+ customer_note: "Fits well, the colour is darker than shown.",
48
+ seller_note: "Thanks, we've updated the product photos.",
49
+ status: "published",
50
+ }
27
51
  ```
28
52
 
29
- Every review carries the same shape regardless of what it targets:
53
+ This is the shape `query.graph` returns for a review’s own fields. The Store API caps `customer_note` at 300 characters, the Admin API at 1000, and both APIs cap `seller_note` at 300.
30
54
 
31
- | Field | Purpose |
32
- | --- | --- |
33
- | `rating` | The numeric score the customer gave |
34
- | `reference` | Whether the review is about a `product` or a `seller` |
35
- | `customer_note` | The customer's optional free-text note |
36
- | `seller_note` | The store's optional public response |
37
- | `status` | The moderation state: `pending`, `published`, or `rejected` |
38
- | `display_id` | A human-readable auto-incrementing number |
39
-
40
- <Note>
41
- There is no separate table for product reviews and seller reviews. A single
42
- `Review` row is discriminated by its `reference` field. See
43
- [Product vs seller reviews](/platform/review/concepts/product-vs-seller-reviews).
44
- </Note>
45
-
46
- ## Notes
47
-
48
- A review separates the two sides of the conversation into two nullable text
49
- fields. `customer_note` is written by the customer when they submit the review;
50
- `seller_note` is the store's single response, added later through the respond
51
- flow. Both are searchable so operators can find reviews by their content.
52
-
53
- <Tip>
54
- A store can respond **once**. The respond flow refuses to overwrite an existing
55
- `seller_note`. To change a response, clear it first.
56
- </Tip>
55
+ ## Status starts at pending
56
+
57
+ Every new review gets `status: "pending"`, the model default. An operator then sets it to `published` or `rejected` with `updateReviewWorkflow`. Nothing in core hides a pending review from a storefront for you, since core has no public review listing: your own route decides which statuses to show. See [Ratings and moderation](/platform/review/concepts/ratings-and-moderation) for the full set of changes.
58
+
59
+ ## Related
60
+
61
+ - [How reviews pick their target](/platform/review/concepts/product-vs-seller-reviews)
62
+ - [Every field and index of the Review model](/platform/review/reference/data-models)
63
+ - [Create a review from a custom route](/platform/review/guides/create-a-review)