@labdigital/commercetools-mock 2.51.0 → 2.53.0
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/dist/index.d.ts +34 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +140 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/lib/product-review-statistics.test.ts +349 -0
- package/src/lib/productSearchFilter.test.ts +77 -0
- package/src/lib/review-statistics.ts +58 -0
- package/src/product-projection-search.ts +17 -2
- package/src/product-search-availability.test.ts +242 -0
- package/src/product-search.ts +22 -4
- package/src/repositories/as-associate.test.ts +126 -0
- package/src/repositories/attribute-group.test.ts +221 -0
- package/src/repositories/business-unit.test.ts +425 -0
- package/src/repositories/business-unit.ts +57 -1
- package/src/repositories/channel.test.ts +374 -0
- package/src/repositories/customer-group.test.ts +262 -0
- package/src/repositories/extension.test.ts +306 -0
- package/src/repositories/index.test.ts +17 -0
- package/src/repositories/product/index.ts +22 -1
- package/src/repositories/product-projection.ts +8 -2
- package/src/repositories/review.test.ts +636 -0
- package/src/repositories/review.ts +145 -4
- package/src/repositories/subscription.test.ts +207 -0
- package/src/repositories/zone.test.ts +278 -0
- package/src/services/as-associate-cart.test.ts +58 -0
- package/src/services/as-associate.test.ts +34 -0
- package/src/services/attribute-group.test.ts +114 -0
- package/src/services/channel.test.ts +90 -0
- package/src/services/customer-group.test.ts +85 -0
- package/src/services/discount-code.test.ts +120 -0
- package/src/services/extension.test.ts +130 -0
- package/src/services/my-business-unit.test.ts +113 -0
- package/src/services/my-business-unit.ts +6 -0
- package/src/services/my-customer.test.ts +24 -0
- package/src/services/order.test.ts +18 -0
- package/src/services/product-discount.test.ts +146 -0
- package/src/services/project.test.ts +17 -0
- package/src/services/reviews.test.ts +230 -0
- package/src/services/subscription.test.ts +151 -0
- package/src/services/type.test.ts +127 -0
- package/src/services/zone.test.ts +117 -0
package/package.json
CHANGED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Product,
|
|
3
|
+
ProductProjection,
|
|
4
|
+
Review,
|
|
5
|
+
} from "@commercetools/platform-sdk";
|
|
6
|
+
import supertest from "supertest";
|
|
7
|
+
import { beforeEach, describe, expect, test } from "vitest";
|
|
8
|
+
import { CommercetoolsMock } from "~src/index";
|
|
9
|
+
|
|
10
|
+
describe("Product Review Statistics", () => {
|
|
11
|
+
let ctMock: CommercetoolsMock;
|
|
12
|
+
let product: Product;
|
|
13
|
+
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
ctMock = new CommercetoolsMock();
|
|
16
|
+
|
|
17
|
+
// Create a product
|
|
18
|
+
const productResponse = await supertest(ctMock.app)
|
|
19
|
+
.post("/dummy/products")
|
|
20
|
+
.send({
|
|
21
|
+
name: { en: "Test Product" },
|
|
22
|
+
slug: { en: "test-product" },
|
|
23
|
+
productType: {
|
|
24
|
+
typeId: "product-type",
|
|
25
|
+
key: "dummy-product-type",
|
|
26
|
+
},
|
|
27
|
+
masterVariant: {
|
|
28
|
+
sku: "test-sku-1",
|
|
29
|
+
prices: [
|
|
30
|
+
{
|
|
31
|
+
value: {
|
|
32
|
+
currencyCode: "EUR",
|
|
33
|
+
centAmount: 1000,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
expect(productResponse.status).toBe(201);
|
|
40
|
+
product = productResponse.body;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("product has no review statistics when no reviews exist", async () => {
|
|
44
|
+
const response = await supertest(ctMock.app).get(
|
|
45
|
+
`/dummy/products/${product.id}`,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
expect(response.status).toBe(200);
|
|
49
|
+
expect(response.body.reviewRatingStatistics).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("product has review statistics when reviews exist", async () => {
|
|
53
|
+
// Create reviews for the product
|
|
54
|
+
await supertest(ctMock.app)
|
|
55
|
+
.post("/dummy/reviews")
|
|
56
|
+
.send({
|
|
57
|
+
authorName: "John Doe",
|
|
58
|
+
title: "Great product!",
|
|
59
|
+
text: "I really love this product.",
|
|
60
|
+
rating: 5,
|
|
61
|
+
target: {
|
|
62
|
+
typeId: "product",
|
|
63
|
+
id: product.id,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await supertest(ctMock.app)
|
|
68
|
+
.post("/dummy/reviews")
|
|
69
|
+
.send({
|
|
70
|
+
authorName: "Jane Smith",
|
|
71
|
+
title: "Good product",
|
|
72
|
+
text: "Pretty good overall.",
|
|
73
|
+
rating: 4,
|
|
74
|
+
target: {
|
|
75
|
+
typeId: "product",
|
|
76
|
+
id: product.id,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await supertest(ctMock.app)
|
|
81
|
+
.post("/dummy/reviews")
|
|
82
|
+
.send({
|
|
83
|
+
authorName: "Bob Wilson",
|
|
84
|
+
title: "Excellent!",
|
|
85
|
+
text: "Amazing quality.",
|
|
86
|
+
rating: 5,
|
|
87
|
+
target: {
|
|
88
|
+
typeId: "product",
|
|
89
|
+
id: product.id,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const response = await supertest(ctMock.app).get(
|
|
94
|
+
`/dummy/products/${product.id}`,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
expect(response.status).toBe(200);
|
|
98
|
+
expect(response.body.reviewRatingStatistics).toBeDefined();
|
|
99
|
+
expect(response.body.reviewRatingStatistics.count).toBe(3);
|
|
100
|
+
expect(response.body.reviewRatingStatistics.averageRating).toBe(4.66667);
|
|
101
|
+
expect(response.body.reviewRatingStatistics.highestRating).toBe(5);
|
|
102
|
+
expect(response.body.reviewRatingStatistics.lowestRating).toBe(4);
|
|
103
|
+
expect(response.body.reviewRatingStatistics.ratingsDistribution).toEqual({
|
|
104
|
+
"4": 1,
|
|
105
|
+
"5": 2,
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("product projection has review statistics", async () => {
|
|
110
|
+
// Create a review for the product
|
|
111
|
+
await supertest(ctMock.app)
|
|
112
|
+
.post("/dummy/reviews")
|
|
113
|
+
.send({
|
|
114
|
+
authorName: "Test User",
|
|
115
|
+
title: "Test Review",
|
|
116
|
+
text: "Test review text.",
|
|
117
|
+
rating: 3,
|
|
118
|
+
target: {
|
|
119
|
+
typeId: "product",
|
|
120
|
+
id: product.id,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const response = await supertest(ctMock.app).get(
|
|
125
|
+
`/dummy/product-projections/${product.id}`,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
expect(response.status).toBe(200);
|
|
129
|
+
expect(response.body.reviewRatingStatistics).toBeDefined();
|
|
130
|
+
expect(response.body.reviewRatingStatistics.count).toBe(1);
|
|
131
|
+
expect(response.body.reviewRatingStatistics.averageRating).toBe(3);
|
|
132
|
+
expect(response.body.reviewRatingStatistics.highestRating).toBe(3);
|
|
133
|
+
expect(response.body.reviewRatingStatistics.lowestRating).toBe(3);
|
|
134
|
+
expect(response.body.reviewRatingStatistics.ratingsDistribution).toEqual({
|
|
135
|
+
"3": 1,
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("product query includes review statistics", async () => {
|
|
140
|
+
// Create reviews for the product
|
|
141
|
+
await supertest(ctMock.app)
|
|
142
|
+
.post("/dummy/reviews")
|
|
143
|
+
.send({
|
|
144
|
+
authorName: "Reviewer 1",
|
|
145
|
+
rating: 2,
|
|
146
|
+
target: {
|
|
147
|
+
typeId: "product",
|
|
148
|
+
id: product.id,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await supertest(ctMock.app)
|
|
153
|
+
.post("/dummy/reviews")
|
|
154
|
+
.send({
|
|
155
|
+
authorName: "Reviewer 2",
|
|
156
|
+
rating: 4,
|
|
157
|
+
target: {
|
|
158
|
+
typeId: "product",
|
|
159
|
+
id: product.id,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const response = await supertest(ctMock.app).get("/dummy/products");
|
|
164
|
+
|
|
165
|
+
expect(response.status).toBe(200);
|
|
166
|
+
expect(response.body.results).toHaveLength(1);
|
|
167
|
+
expect(response.body.results[0].reviewRatingStatistics).toBeDefined();
|
|
168
|
+
expect(response.body.results[0].reviewRatingStatistics.count).toBe(2);
|
|
169
|
+
expect(response.body.results[0].reviewRatingStatistics.averageRating).toBe(
|
|
170
|
+
3,
|
|
171
|
+
);
|
|
172
|
+
expect(response.body.results[0].reviewRatingStatistics.highestRating).toBe(
|
|
173
|
+
4,
|
|
174
|
+
);
|
|
175
|
+
expect(response.body.results[0].reviewRatingStatistics.lowestRating).toBe(
|
|
176
|
+
2,
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("only reviews with includedInStatistics=true are counted", async () => {
|
|
181
|
+
// Create reviews - both will be included by default
|
|
182
|
+
const review1Response = await supertest(ctMock.app)
|
|
183
|
+
.post("/dummy/reviews")
|
|
184
|
+
.send({
|
|
185
|
+
authorName: "Reviewer 1",
|
|
186
|
+
rating: 5,
|
|
187
|
+
target: {
|
|
188
|
+
typeId: "product",
|
|
189
|
+
id: product.id,
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const review2Response = await supertest(ctMock.app)
|
|
194
|
+
.post("/dummy/reviews")
|
|
195
|
+
.send({
|
|
196
|
+
authorName: "Reviewer 2",
|
|
197
|
+
rating: 1,
|
|
198
|
+
target: {
|
|
199
|
+
typeId: "product",
|
|
200
|
+
id: product.id,
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Check that both reviews are included by default
|
|
205
|
+
const response = await supertest(ctMock.app).get(
|
|
206
|
+
`/dummy/products/${product.id}`,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
expect(response.status).toBe(200);
|
|
210
|
+
expect(response.body.reviewRatingStatistics).toBeDefined();
|
|
211
|
+
expect(response.body.reviewRatingStatistics.count).toBe(2);
|
|
212
|
+
expect(response.body.reviewRatingStatistics.averageRating).toBe(3);
|
|
213
|
+
|
|
214
|
+
// Now exclude one review from statistics by updating it
|
|
215
|
+
// (Note: In a real implementation, this would be done via state transitions,
|
|
216
|
+
// but for now we can test the filtering works with includedInStatistics directly)
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("reviews without ratings are not included in statistics", async () => {
|
|
220
|
+
// Create a review without rating
|
|
221
|
+
await supertest(ctMock.app)
|
|
222
|
+
.post("/dummy/reviews")
|
|
223
|
+
.send({
|
|
224
|
+
authorName: "No Rating User",
|
|
225
|
+
title: "No rating review",
|
|
226
|
+
text: "This review has no rating.",
|
|
227
|
+
target: {
|
|
228
|
+
typeId: "product",
|
|
229
|
+
id: product.id,
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Create a review with rating
|
|
234
|
+
await supertest(ctMock.app)
|
|
235
|
+
.post("/dummy/reviews")
|
|
236
|
+
.send({
|
|
237
|
+
authorName: "Rated User",
|
|
238
|
+
title: "Rated review",
|
|
239
|
+
rating: 4,
|
|
240
|
+
target: {
|
|
241
|
+
typeId: "product",
|
|
242
|
+
id: product.id,
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const response = await supertest(ctMock.app).get(
|
|
247
|
+
`/dummy/products/${product.id}`,
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
expect(response.status).toBe(200);
|
|
251
|
+
// Only the review with rating should be counted
|
|
252
|
+
expect(response.body.reviewRatingStatistics).toBeDefined();
|
|
253
|
+
expect(response.body.reviewRatingStatistics.count).toBe(1);
|
|
254
|
+
expect(response.body.reviewRatingStatistics.averageRating).toBe(4);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("reviews on other products are excluded from statistics", async () => {
|
|
258
|
+
// Create another product
|
|
259
|
+
const otherProductResponse = await supertest(ctMock.app)
|
|
260
|
+
.post("/dummy/products")
|
|
261
|
+
.send({
|
|
262
|
+
name: { en: "Other Product" },
|
|
263
|
+
slug: { en: "other-product" },
|
|
264
|
+
productType: {
|
|
265
|
+
typeId: "product-type",
|
|
266
|
+
key: "dummy-product-type",
|
|
267
|
+
},
|
|
268
|
+
masterVariant: {
|
|
269
|
+
sku: "other-sku",
|
|
270
|
+
prices: [
|
|
271
|
+
{
|
|
272
|
+
value: {
|
|
273
|
+
currencyCode: "EUR",
|
|
274
|
+
centAmount: 2000,
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
expect(otherProductResponse.status).toBe(201);
|
|
281
|
+
const otherProduct = otherProductResponse.body;
|
|
282
|
+
|
|
283
|
+
// Create reviews for both products
|
|
284
|
+
await supertest(ctMock.app)
|
|
285
|
+
.post("/dummy/reviews")
|
|
286
|
+
.send({
|
|
287
|
+
authorName: "User A",
|
|
288
|
+
title: "Review for first product",
|
|
289
|
+
rating: 5,
|
|
290
|
+
target: {
|
|
291
|
+
typeId: "product",
|
|
292
|
+
id: product.id,
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
await supertest(ctMock.app)
|
|
297
|
+
.post("/dummy/reviews")
|
|
298
|
+
.send({
|
|
299
|
+
authorName: "User B",
|
|
300
|
+
title: "Review for second product",
|
|
301
|
+
rating: 1,
|
|
302
|
+
target: {
|
|
303
|
+
typeId: "product",
|
|
304
|
+
id: otherProduct.id,
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
await supertest(ctMock.app)
|
|
309
|
+
.post("/dummy/reviews")
|
|
310
|
+
.send({
|
|
311
|
+
authorName: "User C",
|
|
312
|
+
title: "Another review for first product",
|
|
313
|
+
rating: 3,
|
|
314
|
+
target: {
|
|
315
|
+
typeId: "product",
|
|
316
|
+
id: product.id,
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Check statistics for the first product - should only include its own reviews
|
|
321
|
+
const response1 = await supertest(ctMock.app).get(
|
|
322
|
+
`/dummy/products/${product.id}`,
|
|
323
|
+
);
|
|
324
|
+
expect(response1.status).toBe(200);
|
|
325
|
+
expect(response1.body.reviewRatingStatistics).toBeDefined();
|
|
326
|
+
expect(response1.body.reviewRatingStatistics.count).toBe(2); // Only reviews for this product
|
|
327
|
+
expect(response1.body.reviewRatingStatistics.averageRating).toBe(4); // (5 + 3) / 2 = 4
|
|
328
|
+
expect(response1.body.reviewRatingStatistics.highestRating).toBe(5);
|
|
329
|
+
expect(response1.body.reviewRatingStatistics.lowestRating).toBe(3);
|
|
330
|
+
expect(response1.body.reviewRatingStatistics.ratingsDistribution).toEqual({
|
|
331
|
+
"3": 1,
|
|
332
|
+
"5": 1,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Check statistics for the second product - should only include its own review
|
|
336
|
+
const response2 = await supertest(ctMock.app).get(
|
|
337
|
+
`/dummy/products/${otherProduct.id}`,
|
|
338
|
+
);
|
|
339
|
+
expect(response2.status).toBe(200);
|
|
340
|
+
expect(response2.body.reviewRatingStatistics).toBeDefined();
|
|
341
|
+
expect(response2.body.reviewRatingStatistics.count).toBe(1); // Only reviews for this product
|
|
342
|
+
expect(response2.body.reviewRatingStatistics.averageRating).toBe(1);
|
|
343
|
+
expect(response2.body.reviewRatingStatistics.highestRating).toBe(1);
|
|
344
|
+
expect(response2.body.reviewRatingStatistics.lowestRating).toBe(1);
|
|
345
|
+
expect(response2.body.reviewRatingStatistics.ratingsDistribution).toEqual({
|
|
346
|
+
"1": 1,
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
});
|
|
@@ -346,4 +346,81 @@ describe("Product search filter", () => {
|
|
|
346
346
|
}).isMatch,
|
|
347
347
|
).toBeTruthy();
|
|
348
348
|
});
|
|
349
|
+
|
|
350
|
+
test("by availability.isOnStock", async () => {
|
|
351
|
+
const productWithAvailability: ProductProjection = {
|
|
352
|
+
...exampleProduct,
|
|
353
|
+
masterVariant: {
|
|
354
|
+
...exampleProduct.masterVariant,
|
|
355
|
+
availability: {
|
|
356
|
+
isOnStock: true,
|
|
357
|
+
availableQuantity: 10,
|
|
358
|
+
isOnStockForChannel: "test-channel",
|
|
359
|
+
} as any, // Cast to any since isOnStockForChannel is not in SDK type
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// This should pass - current behavior
|
|
364
|
+
const result1 = match(
|
|
365
|
+
{
|
|
366
|
+
exact: {
|
|
367
|
+
field: "variants.availability.isOnStock",
|
|
368
|
+
value: true,
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
productWithAvailability,
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
expect(result1.isMatch).toBeTruthy();
|
|
375
|
+
|
|
376
|
+
const result2 = match(
|
|
377
|
+
{
|
|
378
|
+
exact: {
|
|
379
|
+
field: "variants.availability.isOnStock",
|
|
380
|
+
value: false,
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
productWithAvailability,
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
expect(result2.isMatch).toBeFalsy();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("by availability.isOnStockForChannel", async () => {
|
|
390
|
+
const productWithAvailability: ProductProjection = {
|
|
391
|
+
...exampleProduct,
|
|
392
|
+
masterVariant: {
|
|
393
|
+
...exampleProduct.masterVariant,
|
|
394
|
+
availability: {
|
|
395
|
+
isOnStock: true,
|
|
396
|
+
availableQuantity: 10,
|
|
397
|
+
isOnStockForChannel: "test-channel",
|
|
398
|
+
} as any, // Cast to any since isOnStockForChannel is not in SDK type
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
expect(
|
|
403
|
+
match(
|
|
404
|
+
{
|
|
405
|
+
exact: {
|
|
406
|
+
field: "variants.availability.isOnStockForChannel",
|
|
407
|
+
value: "test-channel",
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
productWithAvailability,
|
|
411
|
+
).isMatch,
|
|
412
|
+
).toBeTruthy();
|
|
413
|
+
|
|
414
|
+
expect(
|
|
415
|
+
match(
|
|
416
|
+
{
|
|
417
|
+
exact: {
|
|
418
|
+
field: "variants.availability.isOnStockForChannel",
|
|
419
|
+
value: "other-channel",
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
productWithAvailability,
|
|
423
|
+
).isMatch,
|
|
424
|
+
).toBeFalsy();
|
|
425
|
+
});
|
|
349
426
|
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Review,
|
|
3
|
+
ReviewRatingStatistics,
|
|
4
|
+
} from "@commercetools/platform-sdk";
|
|
5
|
+
import type { AbstractStorage } from "../storage";
|
|
6
|
+
|
|
7
|
+
export class ReviewStatisticsService {
|
|
8
|
+
constructor(private _storage: AbstractStorage) {}
|
|
9
|
+
|
|
10
|
+
calculateProductReviewStatistics(
|
|
11
|
+
projectKey: string,
|
|
12
|
+
productId: string,
|
|
13
|
+
): ReviewRatingStatistics | undefined {
|
|
14
|
+
// Get all reviews for this product
|
|
15
|
+
const allReviews = this._storage.all(projectKey, "review") as Review[];
|
|
16
|
+
const productReviews = allReviews.filter(
|
|
17
|
+
(review) =>
|
|
18
|
+
review.target?.typeId === "product" &&
|
|
19
|
+
review.target?.id === productId &&
|
|
20
|
+
review.includedInStatistics &&
|
|
21
|
+
review.rating !== undefined,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
if (productReviews.length === 0) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ratings = productReviews
|
|
29
|
+
.map((review) => review.rating!)
|
|
30
|
+
.filter((rating) => rating !== undefined);
|
|
31
|
+
|
|
32
|
+
if (ratings.length === 0) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Calculate statistics
|
|
37
|
+
const count = ratings.length;
|
|
38
|
+
const sum = ratings.reduce((acc, rating) => acc + rating, 0);
|
|
39
|
+
const averageRating = Math.round((sum / count) * 100000) / 100000; // Round to 5 decimals
|
|
40
|
+
const highestRating = Math.max(...ratings);
|
|
41
|
+
const lowestRating = Math.min(...ratings);
|
|
42
|
+
|
|
43
|
+
// Calculate ratings distribution
|
|
44
|
+
const ratingsDistribution: { [key: string]: number } = {};
|
|
45
|
+
for (const rating of ratings) {
|
|
46
|
+
const key = rating.toString();
|
|
47
|
+
ratingsDistribution[key] = (ratingsDistribution[key] || 0) + 1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
averageRating,
|
|
52
|
+
highestRating,
|
|
53
|
+
lowestRating,
|
|
54
|
+
count,
|
|
55
|
+
ratingsDistribution,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
parseFilterExpression,
|
|
23
23
|
resolveVariantValue,
|
|
24
24
|
} from "./lib/projectionSearchFilter";
|
|
25
|
+
import { ReviewStatisticsService } from "./lib/review-statistics";
|
|
25
26
|
import { applyPriceSelector } from "./priceSelector";
|
|
26
27
|
import type { AbstractStorage } from "./storage";
|
|
27
28
|
import type { Writable } from "./types";
|
|
@@ -51,9 +52,11 @@ export type ProductProjectionSearchParams = {
|
|
|
51
52
|
|
|
52
53
|
export class ProductProjectionSearch {
|
|
53
54
|
protected _storage: AbstractStorage;
|
|
55
|
+
protected _reviewStatisticsService: ReviewStatisticsService;
|
|
54
56
|
|
|
55
57
|
constructor(config: Config) {
|
|
56
58
|
this._storage = config.storage;
|
|
59
|
+
this._reviewStatisticsService = new ReviewStatisticsService(config.storage);
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
search(
|
|
@@ -62,7 +65,7 @@ export class ProductProjectionSearch {
|
|
|
62
65
|
): ProductProjectionPagedSearchResponse {
|
|
63
66
|
let resources = this._storage
|
|
64
67
|
.all(projectKey, "product")
|
|
65
|
-
.map((r) => this.transform(r, params.staged ?? false))
|
|
68
|
+
.map((r) => this.transform(r, params.staged ?? false, projectKey))
|
|
66
69
|
.filter((p) => {
|
|
67
70
|
if (!(params.staged ?? false)) {
|
|
68
71
|
return p.published;
|
|
@@ -147,11 +150,22 @@ export class ProductProjectionSearch {
|
|
|
147
150
|
};
|
|
148
151
|
}
|
|
149
152
|
|
|
150
|
-
transform(
|
|
153
|
+
transform(
|
|
154
|
+
product: Product,
|
|
155
|
+
staged: boolean,
|
|
156
|
+
projectKey: string,
|
|
157
|
+
): ProductProjection {
|
|
151
158
|
const obj = !staged
|
|
152
159
|
? product.masterData.current
|
|
153
160
|
: product.masterData.staged;
|
|
154
161
|
|
|
162
|
+
// Calculate review statistics for this product
|
|
163
|
+
const reviewRatingStatistics =
|
|
164
|
+
this._reviewStatisticsService.calculateProductReviewStatistics(
|
|
165
|
+
projectKey,
|
|
166
|
+
product.id,
|
|
167
|
+
);
|
|
168
|
+
|
|
155
169
|
return {
|
|
156
170
|
id: product.id,
|
|
157
171
|
createdAt: product.createdAt,
|
|
@@ -168,6 +182,7 @@ export class ProductProjectionSearch {
|
|
|
168
182
|
productType: product.productType,
|
|
169
183
|
hasStagedChanges: product.masterData.hasStagedChanges,
|
|
170
184
|
published: product.masterData.published,
|
|
185
|
+
reviewRatingStatistics,
|
|
171
186
|
};
|
|
172
187
|
}
|
|
173
188
|
|