@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,57 +1,82 @@
1
1
  ---
2
- title: "Compute aggregate ratings"
2
+ title: "Show average ratings on your storefront"
3
3
  sidebarTitle: "Aggregate ratings"
4
- description: "Compute average ratings for products and sellers from server code."
4
+ description: "Serve average ratings for products and stores from a custom route, with the service helpers or from published reviews only."
5
5
  ---
6
6
 
7
- In this guide, you'll learn how to compute average ratings for products and
8
- sellers from your own server code. This is useful when surfacing a rating on a
9
- storefront page or in a custom API route.
7
+ Add a Store API route that returns a product’s or a store’s average rating, because core ships no public rating endpoint. You can call the Review module service’s rating helpers, which average every linked review, or compute the average from published reviews with `query.graph`.
10
8
 
11
- The Review module service computes averages on demand instead of storing a
12
- denormalized column, so the numbers always reflect the current set of reviews.
13
- Resolve the service from the container to use its rating helpers.
9
+ ## Before you begin
14
10
 
15
- ## Average for one target
11
+ Pick the approach that matches what should count toward the number:
16
12
 
17
- `getAvgRating` returns the average rating for a single product or seller:
13
+ - **Rating helpers**: one SQL query per call; counts `pending`, `rejected`, and soft deleted reviews too
14
+ - **Published only**: loads the target’s reviews with `query.graph` and averages the `published` ones
18
15
 
19
- ```ts title="src/api/custom/route.ts"
20
- import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
21
- import { MercurModules } from "@mercurjs/types"
16
+ <Steps>
17
+ <Step title="Return a store's average from the service">
18
+ Resolve the service with `MercurModules.REVIEW` and call `getAvgRating`:
22
19
 
23
- export async function GET(req: MedusaRequest, res: MedusaResponse) {
24
- const reviewModuleService = req.scope.resolve(MercurModules.REVIEW)
20
+ ```ts title="src/api/store/sellers/[id]/rating/route.ts"
21
+ import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
22
+ import { MercurModules } from "@mercurjs/types"
25
23
 
26
- const rating = await reviewModuleService.getAvgRating("seller", "sel_123")
24
+ export async function GET(req: MedusaRequest, res: MedusaResponse) {
25
+ const reviewService = req.scope.resolve(MercurModules.REVIEW)
27
26
 
28
- res.json({ rating })
29
- }
30
- ```
27
+ const average = await reviewService.getAvgRating("seller", req.params.id)
28
+
29
+ res.json({ rating: average === null ? null : Number(average) })
30
+ }
31
+ ```
32
+
33
+ The first argument is `"product"` or `"seller"`, the second the target’s id. The method returns a numeric string, or `null` when the target has no reviews, so the route converts it to a number.
34
+ </Step>
35
+ <Step title="Average published reviews only">
36
+ Load the product’s linked reviews with `query.graph` and average the published ones:
37
+
38
+ ```ts title="src/api/store/products/[id]/rating/route.ts"
39
+ import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
40
+ import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
31
41
 
32
- The first argument is the target type (`"product"` or `"seller"`), the second is
33
- its id. The method returns `null` when the target has no reviews yet.
42
+ export async function GET(req: MedusaRequest, res: MedusaResponse) {
43
+ const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
44
+ const { data: [product] } = await query.graph({
45
+ entity: "product",
46
+ fields: ["id", "reviews.rating", "reviews.status"],
47
+ filters: { id: req.params.id },
48
+ })
34
49
 
35
- ## Ratings for a list
50
+ const ratings = (product?.reviews ?? [])
51
+ .filter((review) => review?.status === "published")
52
+ .map((review) => review!.rating)
53
+ const rating = ratings.length
54
+ ? ratings.reduce((sum, value) => sum + value, 0) / ratings.length
55
+ : null
36
56
 
37
- For list views, `getProductsWithRating` and `getSellersWithRating` return records
38
- with their average rating joined in, so you don't fan out a call per row:
57
+ res.json({ rating, count: ratings.length })
58
+ }
59
+ ```
60
+
61
+ `reviews` is the alias the product to review link adds to a product. Soft deleted reviews don’t come back from `query.graph`, so they never count.
62
+ </Step>
63
+ </Steps>
64
+
65
+ ## Rate every product or store in one query
66
+
67
+ For list pages, `getProductsWithRating` and `getSellersWithRating` return every row of the `product` or `seller` table with its average joined in:
39
68
 
40
69
  ```ts
41
- const reviewModuleService = container.resolve(MercurModules.REVIEW)
70
+ const reviewService = container.resolve(MercurModules.REVIEW)
42
71
 
43
- const sellers = await reviewModuleService.getSellersWithRating([
44
- "id",
45
- "name",
46
- ])
47
- // => [{ id, name, rating }, ...]
72
+ const sellers = await reviewService.getSellersWithRating(["id", "name"])
73
+ // [{ id: "sel_01JD8Q1234567", name: "ACME", rating: "4.5000000000000000" }]
48
74
  ```
49
75
 
50
- Pass the fields you want selected from the target table; each returned record
51
- gains a `rating` field with the average.
76
+ Pass column names of the target table. Each row gains a `rating` string, or `null` for rows without reviews. The helpers take no filters or pagination and include soft deleted products and sellers, so filter the result before you return it.
77
+
78
+ ## Next steps
52
79
 
53
- <Tip>
54
- These helpers average across a target's linked reviews. Combine them with your
55
- own `status` filter if you only want `published` reviews to count toward the
56
- public number.
57
- </Tip>
80
+ - [Learn what the rating helpers count](/platform/review/concepts/ratings-and-moderation)
81
+ - [See every service method](/platform/review/reference/service)
82
+ - [Query reviews through their links](/platform/review/reference/links)
@@ -1,55 +1,125 @@
1
1
  ---
2
- title: "Create a review"
2
+ title: "Create a review from your own route"
3
3
  sidebarTitle: "Create a review"
4
- description: "Create a review programmatically with createReviewWorkflow."
4
+ description: "Run createReviewWorkflow from a custom API route, an import script, or a seed."
5
5
  ---
6
6
 
7
- In this guide, you'll learn how to create a review from your own server code. This
8
- is useful in a custom storefront route, an import script, or a seed.
7
+ Run `createReviewWorkflow` to create a review outside the built-in `POST /store/reviews` route, for example in a custom checkout follow-up, an import script, or a seed. The workflow validates the order, creates the review as `pending`, and writes its four links.
9
8
 
10
- Mercur exposes a `createReviewWorkflow` that validates the submission, creates the
11
- `Review` record, and links it to its target, order, and customer in one step. Run
12
- it from any place that has access to the Medusa container.
9
+ ## Before you begin
13
10
 
14
- ## Run the workflow
11
+ You need the following:
15
12
 
16
- ```ts title="src/api/custom/route.ts"
17
- import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
18
- import { createReviewWorkflow } from "@mercurjs/core/workflows"
13
+ - **A customer and an order**: the order’s `customer_id` must match the customer you pass
14
+ - **A target id**: a product id for a product review, or a seller id for a store review
15
+ - **Mercur core installed**: the Review module ships in `@mercurjs/core`, so there’s nothing else to register
19
16
 
20
- export async function POST(req: MedusaRequest, res: MedusaResponse) {
21
- const { result } = await createReviewWorkflow(req.scope).run({
22
- input: {
23
- order_id: "order_123",
24
- reference: "product",
25
- reference_id: "prod_123",
26
- rating: 5,
27
- customer_note: "Exactly as described.",
28
- customer_id: "cus_123",
29
- },
30
- })
17
+ <Steps>
18
+ <Step title="Add the route">
19
+ Create a route that reads the signed-in customer and runs the workflow:
31
20
 
32
- res.status(201).json({ review: result })
21
+ ```ts title="src/api/store/order-reviews/route.ts" {14-22}
22
+ import type {
23
+ AuthenticatedMedusaRequest,
24
+ MedusaResponse,
25
+ } from "@medusajs/framework/http"
26
+ import { createReviewWorkflow } from "@mercurjs/core/workflows"
27
+
28
+ type Body = { order_id: string; product_id: string; rating: number }
29
+
30
+ export async function POST(
31
+ req: AuthenticatedMedusaRequest<Body>,
32
+ res: MedusaResponse
33
+ ) {
34
+ const { order_id, product_id, rating } = req.body
35
+ const { result } = await createReviewWorkflow(req.scope).run({
36
+ input: {
37
+ order_id,
38
+ reference: "product",
39
+ reference_id: product_id,
40
+ rating,
41
+ customer_id: req.auth_context.actor_id,
42
+ },
43
+ })
44
+
45
+ res.status(201).json({ review: result })
46
+ }
47
+ ```
48
+
49
+ The route always creates a product review. Pass `reference: "seller"` with a seller id in `reference_id` to rate a store instead. `customer_note` is optional and defaults to `null`.
50
+ </Step>
51
+ <Step title="Require a signed-in customer">
52
+ Custom `/store` routes aren’t authenticated by default. Add the `authenticate` middleware so `auth_context.actor_id` holds the customer id:
53
+
54
+ ```ts title="src/api/middlewares.ts"
55
+ import { authenticate, defineMiddlewares } from "@medusajs/framework/http"
56
+
57
+ export default defineMiddlewares({
58
+ routes: [
59
+ {
60
+ method: ["POST"],
61
+ matcher: "/store/order-reviews",
62
+ middlewares: [authenticate("customer", ["session", "bearer"])],
63
+ },
64
+ ],
65
+ })
66
+ ```
67
+
68
+ This is the same middleware the built-in Store review routes use.
69
+ </Step>
70
+ <Step title="Call the route">
71
+ Send a request as the customer who placed the order:
72
+
73
+ ```bash
74
+ curl -X POST http://localhost:9000/store/order-reviews \
75
+ -H "Authorization: Bearer your_customer_token" \
76
+ -H "x-publishable-api-key: your_publishable_key" \
77
+ -H "Content-Type: application/json" \
78
+ -d '{
79
+ "order_id": "order_01JD8Q1234567",
80
+ "product_id": "prod_01JD8Q1234568",
81
+ "rating": 5
82
+ }'
83
+ ```
84
+
85
+ The response holds the new review with `status: "pending"`.
86
+ </Step>
87
+ </Steps>
88
+
89
+ ## Validate the rating and the target
90
+
91
+ `createReviewWorkflow` checks only that the order belongs to the customer and that the order hasn’t reviewed this target yet. It stores any `rating` you pass and doesn’t check that the product or seller was part of the order. Validate both before you run it:
92
+
93
+ ```ts title="src/api/store/order-reviews/route.ts"
94
+ if (!Number.isInteger(rating) || rating < 1 || rating > 5) {
95
+ throw new MedusaError(MedusaError.Types.INVALID_DATA, "Invalid rating")
96
+ }
97
+
98
+ const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
99
+ const { data: [order] } = await query.graph({
100
+ entity: "order",
101
+ fields: ["items.product_id"],
102
+ filters: { id: order_id },
103
+ })
104
+
105
+ if (!order?.items?.some((item) => item?.product_id === product_id)) {
106
+ throw new MedusaError(MedusaError.Types.NOT_ALLOWED, "Not in order")
33
107
  }
34
108
  ```
35
109
 
36
- The `reference` field selects the target: pass `"product"` with a product id, or
37
- `"seller"` with a seller id, in `reference_id`.
110
+ Import `MedusaError` and `ContainerRegistrationKeys` from `@medusajs/framework/utils`. Put this block before the workflow call.
111
+
112
+ ## Handle a rejected submission
113
+
114
+ The workflow throws a `MedusaError` with type `INVALID_DATA` in two cases, and Medusa returns them as `400` responses:
38
115
 
39
- <Note>
40
- The workflow validates that the `order_id` belongs to `customer_id` and that
41
- the customer hasn't already reviewed the same target on that order. Either
42
- check failing raises an error and no review is created.
43
- </Note>
116
+ - **“Order not found!”**: the order doesn’t exist or belongs to another customer
117
+ - **“Review already exists”**: this order already has a review with the same `reference` and target id
44
118
 
45
- ## Statuses on creation
119
+ When either check fails, nothing is written. If a later step fails, the workflow deletes the review it created.
46
120
 
47
- A new review is created as `pending` and stays hidden until it's moderated. See
48
- [Moderate a review](/platform/review/guides/moderate-a-review) to publish or
49
- reject it.
121
+ ## Next steps
50
122
 
51
- <Tip>
52
- A single order can back several reviews, one per distinct target. Create the
53
- product review and the seller review as two separate calls with different
54
- `reference` / `reference_id` values.
55
- </Tip>
123
+ - [Moderate the new review](/platform/review/guides/moderate-a-review)
124
+ - [Check createReviewWorkflow’s steps and input](/platform/review/reference/workflows)
125
+ - [Learn how reference picks the target](/platform/review/concepts/product-vs-seller-reviews)
@@ -1,58 +1,104 @@
1
1
  ---
2
- title: "Moderate a review"
2
+ title: "Publish, reject, and delete reviews in code"
3
3
  sidebarTitle: "Moderate a review"
4
- description: "Publish, reject, and delete reviews from server code."
4
+ description: "Set a review's status with updateReviewWorkflow and remove it with deleteReviewWorkflow."
5
5
  ---
6
6
 
7
- In this guide, you'll learn how to moderate reviews from your own server code.
8
- Moderation is a change to the review's `status`; removal is a soft delete that
9
- can be undone.
7
+ Moderate reviews from your own code when the Admin API doesn’t fit, for example to auto-publish high ratings or to reject reviews flagged by a spam filter. Moderation is an `updateReviewWorkflow` run that sets `status`, and removal is a `deleteReviewWorkflow` run that soft deletes the review.
10
8
 
11
- ## Publish or reject
9
+ <Steps>
10
+ <Step title="Set the status">
11
+ Run `updateReviewWorkflow` with the review id and the new status:
12
12
 
13
- Move a `pending` review to `published` or `rejected` with `updateReviewWorkflow`,
14
- setting the `status` field:
13
+ ```ts title="src/api/admin/review-moderation/[id]/route.ts" {13-15}
14
+ import type {
15
+ AuthenticatedMedusaRequest,
16
+ MedusaResponse,
17
+ } from "@medusajs/framework/http"
18
+ import { updateReviewWorkflow } from "@mercurjs/core/workflows"
15
19
 
16
- ```ts title="src/api/custom/moderate/route.ts"
17
- import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
20
+ type Body = { status: "published" | "rejected" }
21
+
22
+ export async function POST(
23
+ req: AuthenticatedMedusaRequest<Body>,
24
+ res: MedusaResponse
25
+ ) {
26
+ const { result } = await updateReviewWorkflow(req.scope).run({
27
+ input: { id: req.params.id, status: req.body.status },
28
+ })
29
+
30
+ res.json({ review: result })
31
+ }
32
+ ```
33
+
34
+ Routes under `/admin` require an authenticated admin user by default. The workflow returns the updated review.
35
+ </Step>
36
+ <Step title="Delete a review">
37
+ Add a `DELETE` handler to the same file that runs `deleteReviewWorkflow` with the review id as its whole input:
38
+
39
+ ```ts title="src/api/admin/review-moderation/[id]/route.ts"
40
+ import { deleteReviewWorkflow } from "@mercurjs/core/workflows"
41
+
42
+ export async function DELETE(
43
+ req: AuthenticatedMedusaRequest,
44
+ res: MedusaResponse
45
+ ) {
46
+ await deleteReviewWorkflow(req.scope).run({ input: req.params.id })
47
+
48
+ res.json({ id: req.params.id, object: "review", deleted: true })
49
+ }
50
+ ```
51
+
52
+ The review gets a `deleted_at` timestamp. Its row and its four links stay in the database.
53
+ </Step>
54
+ </Steps>
55
+
56
+ ## Publish reviews automatically
57
+
58
+ Run the moderation from a subscriber to publish reviews without an operator. The module emits `review.review.created` with the new review’s id, so you can load the review and decide:
59
+
60
+ ```ts title="src/subscribers/auto-publish-reviews.ts"
61
+ import type { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
62
+ import { MercurModules } from "@mercurjs/types"
18
63
  import { updateReviewWorkflow } from "@mercurjs/core/workflows"
19
64
 
20
- export async function POST(req: MedusaRequest, res: MedusaResponse) {
21
- await updateReviewWorkflow(req.scope).run({
22
- input: {
23
- id: req.params.id,
24
- status: "published",
25
- },
26
- })
65
+ export default async function autoPublish({
66
+ event,
67
+ container,
68
+ }: SubscriberArgs<{ id: string }>) {
69
+ const reviewService = container.resolve(MercurModules.REVIEW)
70
+ const review = await reviewService.retrieveReview(event.data.id)
27
71
 
28
- res.sendStatus(200)
72
+ if (review.rating >= 4) {
73
+ await updateReviewWorkflow(container).run({
74
+ input: { id: review.id, status: "published" },
75
+ })
76
+ }
29
77
  }
78
+
79
+ export const config: SubscriberConfig = { event: "review.review.created" }
30
80
  ```
31
81
 
32
- `updateReviewWorkflow` also accepts `rating`, `customer_note`, and `seller_note`,
33
- so the same workflow is used for any correction to a review. It captures the
34
- previous values for compensation, so a failure downstream rolls the record back.
82
+ This publishes every review rated 4 or 5 and leaves the rest `pending` for an operator. See the [Events reference](/platform/review/reference/events) for the payload.
35
83
 
36
- ## Delete a review
84
+ ## Correct a review’s content
37
85
 
38
- Remove a review with `deleteReviewWorkflow`. This is a **soft delete**. The row
39
- is retained and restored automatically if the workflow is rolled back:
86
+ `updateReviewWorkflow` also accepts `rating`, `customer_note`, and `seller_note`. Use it to fix a rating, redact a note, or clear a store’s answer:
40
87
 
41
88
  ```ts
42
- import { deleteReviewWorkflow } from "@mercurjs/core/workflows"
43
-
44
- await deleteReviewWorkflow(container).run({ input: "rev_123" })
89
+ await updateReviewWorkflow(container).run({
90
+ input: { id: "rev_01JD8Q1234567", customer_note: null },
91
+ })
45
92
  ```
46
93
 
47
- <Warning>
48
- A soft-deleted review stops counting toward aggregate ratings, but its row (and
49
- its links) are kept. Use a hard delete through the service only if you need the
50
- record gone permanently.
51
- </Warning>
94
+ The workflow saves the previous `rating`, `customer_note`, `seller_note`, and `status` before it writes, and restores them if a later step fails.
95
+
96
+ ## Account for soft deleted reviews in averages
97
+
98
+ A soft deleted review disappears from `query.graph` and the APIs, but the service’s rating helpers still count it, because their query doesn’t check `deleted_at`. Compute averages with `query.graph` if deletions must change the number, as shown in [Compute aggregate ratings](/platform/review/guides/compute-aggregate-ratings).
52
99
 
53
- ## React to moderation
100
+ ## Next steps
54
101
 
55
- To run your own side effects when a review is published or rejected, wrap
56
- `updateReviewWorkflow` in a route that also runs your follow-up logic, or add a
57
- hook. See the [Events reference](/platform/review/reference/events) for the
58
- current state of review-domain events.
102
+ - [Let the store answer the review](/platform/review/guides/respond-to-a-review)
103
+ - [Learn which status changes are allowed](/platform/review/concepts/ratings-and-moderation)
104
+ - [Check updateReviewWorkflow’s input](/platform/review/reference/workflows)
@@ -1,61 +1,106 @@
1
1
  ---
2
- title: "Respond to a review"
2
+ title: "Answer a review as a store"
3
3
  sidebarTitle: "Respond to a review"
4
- description: "Attach a store's public response with respondReviewWorkflow."
4
+ description: "Attach a store's public answer to a review with respondReviewWorkflow."
5
5
  ---
6
6
 
7
- In this guide, you'll learn how to attach a store's response to a review from your
8
- own server code. A response is a single public note the store adds to a review it
9
- received.
7
+ Run `respondReviewWorkflow` to attach a store’s answer to a review, for example from a custom vendor route or a support tool. The workflow writes `seller_note` once and refuses to overwrite an existing answer.
10
8
 
11
- ## Run the workflow
9
+ ## Before you begin
12
10
 
13
- `respondReviewWorkflow` writes the `seller_note` on an existing review:
11
+ Check which reviews a store may answer before you build on this workflow:
14
12
 
15
- ```ts title="src/api/custom/respond/route.ts"
16
- import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
17
- import { respondReviewWorkflow } from "@mercurjs/core/workflows"
13
+ - **Seller reviews only**: product reviews have no seller link, so the built-in Vendor API can’t reach them
14
+ - **One answer per review**: a second call fails until `seller_note` is cleared
15
+ - **No status change**: answering leaves `status` as it is
18
16
 
19
- export async function POST(req: MedusaRequest, res: MedusaResponse) {
20
- await respondReviewWorkflow(req.scope).run({
21
- input: {
22
- id: req.params.id,
23
- seller_note: "Thanks for the feedback, glad it arrived quickly!",
24
- },
25
- })
17
+ <Steps>
18
+ <Step title="Check that the review belongs to the store">
19
+ Look up the seller link before you write. The built-in Vendor route runs the same check:
26
20
 
27
- res.sendStatus(200)
28
- }
29
- ```
21
+ ```ts title="src/api/vendor/review-answers/[id]/helpers.ts"
22
+ import { MedusaContainer } from "@medusajs/framework"
23
+ import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
30
24
 
31
- <Note>
32
- A store can respond **once**. The workflow throws if the review already has a
33
- `seller_note`, and it throws `NOT_FOUND` if the review id doesn't exist. It
34
- captures the previous value so a rollback clears the response again.
35
- </Note>
25
+ export async function storeOwnsReview(
26
+ scope: MedusaContainer,
27
+ sellerId: string,
28
+ reviewId: string
29
+ ) {
30
+ const query = scope.resolve(ContainerRegistrationKeys.QUERY)
31
+ const { data } = await query.graph({
32
+ entity: "seller_seller_review_review",
33
+ fields: ["review_id"],
34
+ filters: { seller_id: sellerId, review_id: reviewId },
35
+ })
36
36
 
37
- ## Clearing a response
37
+ return data.length > 0
38
+ }
39
+ ```
38
40
 
39
- The respond flow won't overwrite an existing note. To replace a response, first
40
- clear it with `updateReviewWorkflow`, then respond again:
41
+ `seller_seller_review_review` is the link table between sellers and reviews.
42
+ </Step>
43
+ <Step title="Run the workflow">
44
+ Call `respondReviewWorkflow` with the review id and the answer:
41
45
 
42
- ```ts
43
- import {
44
- updateReviewWorkflow,
45
- respondReviewWorkflow,
46
- } from "@mercurjs/core/workflows"
46
+ ```ts title="src/api/vendor/review-answers/[id]/route.ts" {16-18}
47
+ import type {
48
+ AuthenticatedMedusaRequest,
49
+ MedusaResponse,
50
+ } from "@medusajs/framework/http"
51
+ import { MedusaError } from "@medusajs/framework/utils"
52
+ import { respondReviewWorkflow } from "@mercurjs/core/workflows"
53
+ import { storeOwnsReview } from "./helpers"
54
+
55
+ export async function POST(req: AuthenticatedMedusaRequest<{
56
+ seller_note: string
57
+ }>, res: MedusaResponse) {
58
+ const sellerId = req.seller_context!.seller_id
59
+ if (!(await storeOwnsReview(req.scope, sellerId, req.params.id))) {
60
+ throw new MedusaError(MedusaError.Types.NOT_FOUND, "Review not found")
61
+ }
62
+ const { result } = await respondReviewWorkflow(req.scope).run({
63
+ input: { id: req.params.id, seller_note: req.body.seller_note },
64
+ })
65
+
66
+ res.json({ review: result })
67
+ }
68
+ ```
69
+
70
+ Mercur fills `req.seller_context` on `/vendor` routes with the signed-in member’s current store.
71
+ </Step>
72
+ </Steps>
73
+
74
+ ## Handle a second answer
75
+
76
+ `respondReviewWorkflow` throws in two cases:
47
77
 
78
+ - **`NOT_FOUND`, “Review not found”**: no review has this id
79
+ - **`NOT_ALLOWED`, “This review already has a response”**: `seller_note` is already set
80
+
81
+ The built-in routes return `400` for the second case. If a later step fails after the write, the workflow sets `seller_note` back to `null`.
82
+
83
+ ## Replace an existing answer
84
+
85
+ The respond workflow never overwrites. To replace an answer, clear it with `updateReviewWorkflow`, then respond again:
86
+
87
+ ```ts
48
88
  await updateReviewWorkflow(container).run({
49
- input: { id: "rev_123", seller_note: null },
89
+ input: { id: "rev_01JD8Q1234567", seller_note: null },
50
90
  })
51
91
 
52
92
  await respondReviewWorkflow(container).run({
53
- input: { id: "rev_123", seller_note: "Updated response." },
93
+ input: {
94
+ id: "rev_01JD8Q1234567",
95
+ seller_note: "Sorry for the delay, a replacement is on its way.",
96
+ },
54
97
  })
55
98
  ```
56
99
 
57
- <Tip>
58
- Responding never changes the review's `status`. A store can respond to a
59
- `pending` review, but the response only becomes public once the review is
60
- [published](/platform/review/guides/moderate-a-review).
61
- </Tip>
100
+ Import both workflows from `@mercurjs/core/workflows`. The built-in APIs cap `seller_note` at 300 characters, while the workflow itself doesn’t.
101
+
102
+ ## Next steps
103
+
104
+ - [Publish the review so the answer is shown](/platform/review/guides/moderate-a-review)
105
+ - [Learn why stores see only seller reviews](/platform/review/concepts/product-vs-seller-reviews)
106
+ - [Check respondReviewWorkflow’s input](/platform/review/reference/workflows)