@mercurjs/docs 2.3.8-canary.2 → 2.3.8-canary.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/content/learn/architecture.mdx +166 -263
- package/content/platform/attribute/concepts/attribute-types.mdx +73 -46
- package/content/platform/attribute/concepts/global-vs-inline.mdx +54 -32
- package/content/platform/attribute/concepts/variant-axes.mdx +54 -32
- package/content/platform/attribute/guides/attach-attributes-to-a-product.mdx +96 -53
- package/content/platform/attribute/guides/create-a-variant-axis.mdx +106 -57
- package/content/platform/attribute/guides/create-an-attribute.mdx +105 -56
- package/content/platform/attribute/overview.mdx +37 -33
- package/content/platform/attribute/reference/data-models.mdx +53 -38
- package/content/platform/attribute/reference/events.mdx +47 -24
- package/content/platform/attribute/reference/links.mdx +98 -27
- package/content/platform/attribute/reference/service.mdx +71 -32
- package/content/platform/attribute/reference/workflows.mdx +158 -28
- package/content/platform/catalog/concepts/master-products.mdx +39 -37
- package/content/platform/catalog/concepts/product-seller-allowlist.mdx +56 -35
- package/content/platform/catalog/concepts/status-lifecycle.mdx +55 -49
- package/content/platform/catalog/concepts/variants-categories-collections.mdx +55 -35
- package/content/platform/catalog/guides/allowlist-stores.mdx +100 -37
- package/content/platform/catalog/guides/create-a-master-product.mdx +124 -48
- package/content/platform/catalog/guides/publish-or-reject-a-product.mdx +120 -55
- package/content/platform/catalog/overview.mdx +39 -35
- package/content/platform/catalog/reference/data-models.mdx +154 -43
- package/content/platform/catalog/reference/events.mdx +41 -25
- package/content/platform/catalog/reference/links.mdx +196 -28
- package/content/platform/catalog/reference/service.mdx +57 -35
- package/content/platform/catalog/reference/workflows.mdx +124 -30
- package/content/platform/commission/concepts/order-commission-lines.mdx +54 -52
- package/content/platform/commission/concepts/rule-matching.mdx +53 -63
- package/content/platform/commission/concepts/rules-and-rates.mdx +61 -59
- package/content/platform/commission/guides/batch-update-rules.mdx +69 -41
- package/content/platform/commission/guides/create-a-rate.mdx +101 -53
- package/content/platform/commission/guides/refresh-order-commission-lines.mdx +104 -35
- package/content/platform/commission/overview.mdx +43 -35
- package/content/platform/commission/reference/data-models.mdx +59 -45
- package/content/platform/commission/reference/events.mdx +28 -34
- package/content/platform/commission/reference/links.mdx +42 -33
- package/content/platform/commission/reference/service.mdx +103 -28
- package/content/platform/commission/reference/workflows.mdx +101 -27
- package/content/platform/offer/concepts/pricing-and-inventory.mdx +63 -49
- package/content/platform/offer/concepts/shipping.mdx +28 -29
- package/content/platform/offer/concepts/what-is-an-offer.mdx +48 -45
- package/content/platform/offer/guides/bulk-create-offers.mdx +97 -61
- package/content/platform/offer/guides/create-an-offer.mdx +112 -60
- package/content/platform/offer/guides/manage-offer-inventory.mdx +78 -38
- package/content/platform/offer/overview.mdx +39 -32
- package/content/platform/offer/reference/data-models.mdx +77 -47
- package/content/platform/offer/reference/events.mdx +44 -18
- package/content/platform/offer/reference/links.mdx +92 -40
- package/content/platform/offer/reference/service.mdx +66 -31
- package/content/platform/offer/reference/workflows.mdx +146 -37
- package/content/platform/order-group/concepts/computed-totals.mdx +40 -37
- package/content/platform/order-group/concepts/order-splitting.mdx +41 -39
- package/content/platform/order-group/concepts/the-order-group.mdx +39 -32
- package/content/platform/order-group/guides/list-order-groups.mdx +96 -56
- package/content/platform/order-group/guides/retrieve-an-order-group.mdx +93 -42
- package/content/platform/order-group/guides/split-a-cart.mdx +115 -40
- package/content/platform/order-group/overview.mdx +42 -32
- package/content/platform/order-group/reference/data-models.mdx +48 -31
- package/content/platform/order-group/reference/events.mdx +29 -17
- package/content/platform/order-group/reference/links.mdx +90 -29
- package/content/platform/order-group/reference/service.mdx +55 -31
- package/content/platform/order-group/reference/workflows.mdx +83 -24
- package/content/platform/payout/concepts/account-lifecycle.mdx +64 -67
- package/content/platform/payout/concepts/accounts-and-onboarding.mdx +50 -40
- package/content/platform/payout/concepts/payout-pipeline.mdx +70 -73
- package/content/platform/payout/guides/create-a-payout-account.mdx +65 -41
- package/content/platform/payout/guides/process-a-provider-webhook.mdx +84 -38
- package/content/platform/payout/guides/start-provider-onboarding.mdx +90 -46
- package/content/platform/payout/overview.mdx +43 -34
- package/content/platform/payout/reference/data-models.mdx +63 -36
- package/content/platform/payout/reference/events.mdx +56 -30
- package/content/platform/payout/reference/links.mdx +52 -22
- package/content/platform/payout/reference/service.mdx +72 -33
- package/content/platform/payout/reference/workflows.mdx +80 -19
- package/content/platform/product-edit/concepts/change-actions.mdx +52 -45
- package/content/platform/product-edit/concepts/change-pipeline.mdx +60 -52
- package/content/platform/product-edit/concepts/status-and-auto-confirm.mdx +43 -46
- package/content/platform/product-edit/guides/confirm-or-decline-a-change.mdx +104 -56
- package/content/platform/product-edit/guides/edit-a-product.mdx +126 -46
- package/content/platform/product-edit/guides/request-a-revision.mdx +99 -39
- package/content/platform/product-edit/overview.mdx +41 -34
- package/content/platform/product-edit/reference/data-models.mdx +73 -47
- package/content/platform/product-edit/reference/events.mdx +39 -29
- package/content/platform/product-edit/reference/links.mdx +32 -24
- package/content/platform/product-edit/reference/service.mdx +45 -26
- package/content/platform/product-edit/reference/workflows.mdx +173 -31
- package/content/platform/review/concepts/product-vs-seller-reviews.mdx +42 -41
- package/content/platform/review/concepts/ratings-and-moderation.mdx +45 -44
- package/content/platform/review/concepts/the-review-model.mdx +52 -45
- package/content/platform/review/guides/compute-aggregate-ratings.mdx +62 -37
- package/content/platform/review/guides/create-a-review.mdx +109 -39
- package/content/platform/review/guides/moderate-a-review.mdx +83 -37
- package/content/platform/review/guides/respond-to-a-review.mdx +85 -40
- package/content/platform/review/overview.mdx +43 -33
- package/content/platform/review/reference/data-models.mdx +41 -27
- package/content/platform/review/reference/events.mdx +55 -37
- package/content/platform/review/reference/links.mdx +80 -22
- package/content/platform/review/reference/service.mdx +61 -33
- package/content/platform/review/reference/workflows.mdx +80 -24
- package/content/platform/store/concepts/lifecycle.mdx +68 -44
- package/content/platform/store/concepts/store-entity.mdx +45 -27
- package/content/platform/store/concepts/team.mdx +56 -35
- package/content/platform/store/guides/create-a-store.mdx +102 -35
- package/content/platform/store/guides/manage-the-team.mdx +122 -32
- package/content/platform/store/guides/moderate-a-store.mdx +107 -38
- package/content/platform/store/overview.mdx +41 -34
- package/content/platform/store/reference/data-models.mdx +132 -57
- package/content/platform/store/reference/events.mdx +42 -17
- package/content/platform/store/reference/links.mdx +67 -49
- package/content/platform/store/reference/service.mdx +105 -33
- package/content/platform/store/reference/workflows.mdx +304 -24
- package/content/references/panel-extensions/layouts.mdx +169 -0
- package/llms.txt +112 -111
- package/package.json +1 -1
|
@@ -1,57 +1,82 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: "
|
|
2
|
+
title: "Show average ratings on your storefront"
|
|
3
3
|
sidebarTitle: "Aggregate ratings"
|
|
4
|
-
description: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11
|
+
Pick the approach that matches what should count toward the number:
|
|
16
12
|
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
24
|
+
export async function GET(req: MedusaRequest, res: MedusaResponse) {
|
|
25
|
+
const reviewService = req.scope.resolve(MercurModules.REVIEW)
|
|
27
26
|
|
|
28
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
|
70
|
+
const reviewService = container.resolve(MercurModules.REVIEW)
|
|
42
71
|
|
|
43
|
-
const sellers = await
|
|
44
|
-
|
|
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
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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: "
|
|
4
|
+
description: "Run createReviewWorkflow from a custom API route, an import script, or a seed."
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11
|
+
You need the following:
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
119
|
+
When either check fails, nothing is written. If a later step fails, the workflow deletes the review it created.
|
|
46
120
|
|
|
47
|
-
|
|
48
|
-
[Moderate a review](/platform/review/guides/moderate-a-review) to publish or
|
|
49
|
-
reject it.
|
|
121
|
+
## Next steps
|
|
50
122
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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: "
|
|
2
|
+
title: "Publish, reject, and delete reviews in code"
|
|
3
3
|
sidebarTitle: "Moderate a review"
|
|
4
|
-
description: "
|
|
4
|
+
description: "Set a review's status with updateReviewWorkflow and remove it with deleteReviewWorkflow."
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
9
|
+
<Steps>
|
|
10
|
+
<Step title="Set the status">
|
|
11
|
+
Run `updateReviewWorkflow` with the review id and the new status:
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
-
##
|
|
84
|
+
## Correct a review’s content
|
|
37
85
|
|
|
38
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
89
|
+
await updateReviewWorkflow(container).run({
|
|
90
|
+
input: { id: "rev_01JD8Q1234567", customer_note: null },
|
|
91
|
+
})
|
|
45
92
|
```
|
|
46
93
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
##
|
|
100
|
+
## Next steps
|
|
54
101
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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: "
|
|
2
|
+
title: "Answer a review as a store"
|
|
3
3
|
sidebarTitle: "Respond to a review"
|
|
4
|
-
description: "Attach a store's public
|
|
4
|
+
description: "Attach a store's public answer to a review with respondReviewWorkflow."
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
##
|
|
9
|
+
## Before you begin
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
Check which reviews a store may answer before you build on this workflow:
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
37
|
+
return data.length > 0
|
|
38
|
+
}
|
|
39
|
+
```
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
} from "@
|
|
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: "
|
|
89
|
+
input: { id: "rev_01JD8Q1234567", seller_note: null },
|
|
50
90
|
})
|
|
51
91
|
|
|
52
92
|
await respondReviewWorkflow(container).run({
|
|
53
|
-
input: {
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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)
|