@labdigital/commercetools-mock 2.50.1 → 2.52.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 +121 -8
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/lib/product-review-statistics.test.ts +349 -0
- package/src/lib/review-statistics.ts +58 -0
- package/src/product-projection-search.ts +17 -2
- 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 +282 -0
- package/src/repositories/business-unit.ts +5 -1
- package/src/repositories/cart/index.test.ts +60 -0
- package/src/repositories/cart/index.ts +29 -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@labdigital/commercetools-mock",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.52.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Michael van Tellingen",
|
|
6
6
|
"type": "module",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"build": "tsdown",
|
|
61
61
|
"build:server": "esbuild src/server.ts --bundle --outfile=dist/server.js --platform=node",
|
|
62
62
|
"check": "biome check && tsc",
|
|
63
|
-
"format": "biome
|
|
63
|
+
"format": "biome check --fix",
|
|
64
64
|
"lint": "biome check",
|
|
65
65
|
"publish:ci": "pnpm build && pnpm changeset publish",
|
|
66
66
|
"publish:version": "pnpm changeset version && pnpm format",
|
|
@@ -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
|
+
});
|
|
@@ -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
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import type { Config } from "~src/config";
|
|
3
|
+
import { InMemoryStorage } from "~src/storage";
|
|
4
|
+
import {
|
|
5
|
+
AsAssociateCartRepository,
|
|
6
|
+
AsAssociateOrderRepository,
|
|
7
|
+
AsAssociateQuoteRequestRepository,
|
|
8
|
+
} from "./as-associate";
|
|
9
|
+
import { CustomerRepository } from "./customer";
|
|
10
|
+
|
|
11
|
+
describe("As Associate Repositories", () => {
|
|
12
|
+
const storage = new InMemoryStorage();
|
|
13
|
+
const config: Config = { storage, strict: false };
|
|
14
|
+
|
|
15
|
+
test("AsAssociateCartRepository can create and retrieve carts", () => {
|
|
16
|
+
const repository = new AsAssociateCartRepository(config);
|
|
17
|
+
const ctx = { projectKey: "test-project" };
|
|
18
|
+
|
|
19
|
+
const cartDraft = {
|
|
20
|
+
currency: "EUR",
|
|
21
|
+
inventoryMode: "None" as const,
|
|
22
|
+
taxMode: "Platform" as const,
|
|
23
|
+
taxRoundingMode: "HalfEven" as const,
|
|
24
|
+
taxCalculationMode: "UnitPriceLevel" as const,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const cart = repository.create(ctx, cartDraft);
|
|
28
|
+
expect(cart.id).toBeDefined();
|
|
29
|
+
expect(cart.version).toBe(1);
|
|
30
|
+
expect(cart.totalPrice.currencyCode).toBe("EUR");
|
|
31
|
+
|
|
32
|
+
// Test query
|
|
33
|
+
const result = repository.query(ctx);
|
|
34
|
+
expect(result.count).toBe(1);
|
|
35
|
+
expect(result.results[0].id).toBe(cart.id);
|
|
36
|
+
|
|
37
|
+
// Test get
|
|
38
|
+
const retrieved = repository.get(ctx, cart.id);
|
|
39
|
+
expect(retrieved).toBeDefined();
|
|
40
|
+
expect(retrieved?.id).toBe(cart.id);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("AsAssociateOrderRepository can create and retrieve orders", () => {
|
|
44
|
+
const repository = new AsAssociateOrderRepository(config);
|
|
45
|
+
const ctx = { projectKey: "test-project" };
|
|
46
|
+
|
|
47
|
+
// First create a cart to create an order from
|
|
48
|
+
const cartRepository = new AsAssociateCartRepository(config);
|
|
49
|
+
const cartDraft = {
|
|
50
|
+
currency: "EUR",
|
|
51
|
+
inventoryMode: "None" as const,
|
|
52
|
+
taxMode: "Platform" as const,
|
|
53
|
+
taxRoundingMode: "HalfEven" as const,
|
|
54
|
+
taxCalculationMode: "UnitPriceLevel" as const,
|
|
55
|
+
};
|
|
56
|
+
const cart = cartRepository.create(ctx, cartDraft);
|
|
57
|
+
|
|
58
|
+
const orderDraft = {
|
|
59
|
+
cart: {
|
|
60
|
+
id: cart.id,
|
|
61
|
+
typeId: "cart" as const,
|
|
62
|
+
},
|
|
63
|
+
version: cart.version,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const order = repository.create(ctx, orderDraft);
|
|
67
|
+
expect(order.id).toBeDefined();
|
|
68
|
+
expect(order.version).toBe(1);
|
|
69
|
+
expect(order.cart?.id).toBe(cart.id);
|
|
70
|
+
|
|
71
|
+
// Test query
|
|
72
|
+
const result = repository.query(ctx);
|
|
73
|
+
expect(result.count).toBe(1);
|
|
74
|
+
expect(result.results[0].id).toBe(order.id);
|
|
75
|
+
|
|
76
|
+
// Test get
|
|
77
|
+
const retrieved = repository.get(ctx, order.id);
|
|
78
|
+
expect(retrieved).toBeDefined();
|
|
79
|
+
expect(retrieved?.id).toBe(order.id);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("AsAssociateQuoteRequestRepository can create and retrieve quote requests", () => {
|
|
83
|
+
const repository = new AsAssociateQuoteRequestRepository(config);
|
|
84
|
+
const ctx = { projectKey: "test-project" };
|
|
85
|
+
|
|
86
|
+
// Create a customer using the customer repository
|
|
87
|
+
const customerRepository = new CustomerRepository(config);
|
|
88
|
+
const customer = customerRepository.create(ctx, {
|
|
89
|
+
email: "test@example.com",
|
|
90
|
+
password: "password123",
|
|
91
|
+
firstName: "John",
|
|
92
|
+
lastName: "Doe",
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// First create a cart to create a quote request from
|
|
96
|
+
const cartRepository = new AsAssociateCartRepository(config);
|
|
97
|
+
const cartDraft = {
|
|
98
|
+
currency: "EUR",
|
|
99
|
+
customerId: customer.id,
|
|
100
|
+
};
|
|
101
|
+
const cart = cartRepository.create(ctx, cartDraft);
|
|
102
|
+
|
|
103
|
+
const quoteRequestDraft = {
|
|
104
|
+
cart: {
|
|
105
|
+
id: cart.id,
|
|
106
|
+
typeId: "cart" as const,
|
|
107
|
+
},
|
|
108
|
+
cartVersion: cart.version,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const quoteRequest = repository.create(ctx, quoteRequestDraft);
|
|
112
|
+
expect(quoteRequest.id).toBeDefined();
|
|
113
|
+
expect(quoteRequest.version).toBe(1);
|
|
114
|
+
expect(quoteRequest.cart?.id).toBe(cart.id);
|
|
115
|
+
|
|
116
|
+
// Test query
|
|
117
|
+
const result = repository.query(ctx);
|
|
118
|
+
expect(result.count).toBe(1);
|
|
119
|
+
expect(result.results[0].id).toBe(quoteRequest.id);
|
|
120
|
+
|
|
121
|
+
// Test get
|
|
122
|
+
const retrieved = repository.get(ctx, quoteRequest.id);
|
|
123
|
+
expect(retrieved).toBeDefined();
|
|
124
|
+
expect(retrieved?.id).toBe(quoteRequest.id);
|
|
125
|
+
});
|
|
126
|
+
});
|