@86d-app/reviews 0.0.4 → 0.0.13

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 (104) hide show
  1. package/.turbo/turbo-build.log +1 -0
  2. package/AGENTS.md +54 -18
  3. package/README.md +150 -41
  4. package/dist/__tests__/controllers.test.d.ts +2 -0
  5. package/dist/__tests__/controllers.test.d.ts.map +1 -0
  6. package/dist/__tests__/endpoint-security.test.d.ts +2 -0
  7. package/dist/__tests__/endpoint-security.test.d.ts.map +1 -0
  8. package/dist/__tests__/new-features.test.d.ts +2 -0
  9. package/dist/__tests__/new-features.test.d.ts.map +1 -0
  10. package/dist/__tests__/service-impl.test.d.ts +2 -0
  11. package/dist/__tests__/service-impl.test.d.ts.map +1 -0
  12. package/dist/admin/components/index.d.ts +4 -0
  13. package/dist/admin/components/index.d.ts.map +1 -0
  14. package/dist/admin/components/review-analytics.d.ts +2 -0
  15. package/dist/admin/components/review-analytics.d.ts.map +1 -0
  16. package/dist/admin/components/review-list.d.ts +2 -0
  17. package/dist/admin/components/review-list.d.ts.map +1 -0
  18. package/dist/admin/components/review-moderation.d.ts +6 -0
  19. package/dist/admin/components/review-moderation.d.ts.map +1 -0
  20. package/dist/admin/endpoints/approve-review.d.ts +16 -0
  21. package/dist/admin/endpoints/approve-review.d.ts.map +1 -0
  22. package/dist/admin/endpoints/delete-review.d.ts +16 -0
  23. package/dist/admin/endpoints/delete-review.d.ts.map +1 -0
  24. package/dist/admin/endpoints/get-review.d.ts +16 -0
  25. package/dist/admin/endpoints/get-review.d.ts.map +1 -0
  26. package/dist/admin/endpoints/index.d.ts +167 -0
  27. package/dist/admin/endpoints/index.d.ts.map +1 -0
  28. package/dist/admin/endpoints/list-reports.d.ts +17 -0
  29. package/dist/admin/endpoints/list-reports.d.ts.map +1 -0
  30. package/dist/admin/endpoints/list-review-requests.d.ts +11 -0
  31. package/dist/admin/endpoints/list-review-requests.d.ts.map +1 -0
  32. package/dist/admin/endpoints/list-reviews.d.ts +18 -0
  33. package/dist/admin/endpoints/list-reviews.d.ts.map +1 -0
  34. package/dist/admin/endpoints/reject-review.d.ts +16 -0
  35. package/dist/admin/endpoints/reject-review.d.ts.map +1 -0
  36. package/dist/admin/endpoints/respond-review.d.ts +19 -0
  37. package/dist/admin/endpoints/respond-review.d.ts.map +1 -0
  38. package/dist/admin/endpoints/review-analytics.d.ts +6 -0
  39. package/dist/admin/endpoints/review-analytics.d.ts.map +1 -0
  40. package/dist/admin/endpoints/review-request-stats.d.ts +6 -0
  41. package/dist/admin/endpoints/review-request-stats.d.ts.map +1 -0
  42. package/dist/admin/endpoints/send-review-request.d.ts +23 -0
  43. package/dist/admin/endpoints/send-review-request.d.ts.map +1 -0
  44. package/dist/admin/endpoints/update-report.d.ts +22 -0
  45. package/dist/admin/endpoints/update-report.d.ts.map +1 -0
  46. package/dist/index.d.ts +8 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/schema.d.ts +136 -0
  49. package/dist/schema.d.ts.map +1 -0
  50. package/dist/service-impl.d.ts +6 -0
  51. package/dist/service-impl.d.ts.map +1 -0
  52. package/dist/service.d.ts +149 -0
  53. package/dist/service.d.ts.map +1 -0
  54. package/dist/store/components/_hooks.d.ts +6 -0
  55. package/dist/store/components/_hooks.d.ts.map +1 -0
  56. package/dist/store/components/_utils.d.ts +3 -0
  57. package/dist/store/components/_utils.d.ts.map +1 -0
  58. package/dist/store/components/distribution-bars.d.ts +5 -0
  59. package/dist/store/components/distribution-bars.d.ts.map +1 -0
  60. package/dist/store/components/index.d.ts +18 -0
  61. package/dist/store/components/index.d.ts.map +1 -0
  62. package/dist/store/components/product-reviews.d.ts +5 -0
  63. package/dist/store/components/product-reviews.d.ts.map +1 -0
  64. package/dist/store/components/review-card.d.ts +18 -0
  65. package/dist/store/components/review-card.d.ts.map +1 -0
  66. package/dist/store/components/review-form.d.ts +5 -0
  67. package/dist/store/components/review-form.d.ts.map +1 -0
  68. package/dist/store/components/reviews-summary.d.ts +4 -0
  69. package/dist/store/components/reviews-summary.d.ts.map +1 -0
  70. package/dist/store/components/star-display.d.ts +5 -0
  71. package/dist/store/components/star-display.d.ts.map +1 -0
  72. package/dist/store/components/star-picker.d.ts +5 -0
  73. package/dist/store/components/star-picker.d.ts.map +1 -0
  74. package/dist/store/endpoints/index.d.ts +116 -0
  75. package/dist/store/endpoints/index.d.ts.map +1 -0
  76. package/dist/store/endpoints/list-my-reviews.d.ts +30 -0
  77. package/dist/store/endpoints/list-my-reviews.d.ts.map +1 -0
  78. package/dist/store/endpoints/list-product-reviews.d.ts +23 -0
  79. package/dist/store/endpoints/list-product-reviews.d.ts.map +1 -0
  80. package/dist/store/endpoints/mark-helpful.d.ts +18 -0
  81. package/dist/store/endpoints/mark-helpful.d.ts.map +1 -0
  82. package/dist/store/endpoints/report-review.d.ts +27 -0
  83. package/dist/store/endpoints/report-review.d.ts.map +1 -0
  84. package/dist/store/endpoints/submit-review.d.ts +25 -0
  85. package/dist/store/endpoints/submit-review.d.ts.map +1 -0
  86. package/package.json +3 -3
  87. package/src/__tests__/controllers.test.ts +1074 -0
  88. package/src/__tests__/endpoint-security.test.ts +559 -0
  89. package/src/__tests__/new-features.test.ts +618 -0
  90. package/src/__tests__/service-impl.test.ts +1 -1
  91. package/src/admin/endpoints/index.ts +4 -0
  92. package/src/admin/endpoints/list-reports.ts +25 -0
  93. package/src/admin/endpoints/update-report.ts +22 -0
  94. package/src/index.ts +7 -1
  95. package/src/schema.ts +28 -0
  96. package/src/service-impl.ts +151 -3
  97. package/src/service.ts +61 -0
  98. package/src/store/endpoints/index.ts +2 -0
  99. package/src/store/endpoints/list-my-reviews.ts +1 -1
  100. package/src/store/endpoints/list-product-reviews.ts +6 -2
  101. package/src/store/endpoints/mark-helpful.ts +21 -2
  102. package/src/store/endpoints/report-review.ts +38 -0
  103. package/src/store/endpoints/submit-review.ts +33 -7
  104. package/COMPONENTS.md +0 -34
@@ -0,0 +1,1074 @@
1
+ import { createMockDataService } from "@86d-app/core/test-utils";
2
+ import { beforeEach, describe, expect, it } from "vitest";
3
+ import { createReviewController } from "../service-impl";
4
+
5
+ describe("review controller edge cases", () => {
6
+ let mockData: ReturnType<typeof createMockDataService>;
7
+ let controller: ReturnType<typeof createReviewController>;
8
+
9
+ beforeEach(() => {
10
+ mockData = createMockDataService();
11
+ controller = createReviewController(mockData);
12
+ });
13
+
14
+ // ── createReview edge cases ────────────────────────────────────────
15
+
16
+ describe("createReview edge cases", () => {
17
+ it("defaults to pending status when no options provided", async () => {
18
+ const review = await controller.createReview({
19
+ productId: "prod_1",
20
+ authorName: "Alice",
21
+ authorEmail: "alice@example.com",
22
+ rating: 5,
23
+ body: "Great product!",
24
+ });
25
+ expect(review.status).toBe("pending");
26
+ expect(review.helpfulCount).toBe(0);
27
+ expect(review.isVerifiedPurchase).toBe(false);
28
+ });
29
+
30
+ it("sets approved status when autoApprove option is true", async () => {
31
+ const autoCtrl = createReviewController(mockData, {
32
+ autoApprove: true,
33
+ });
34
+ const review = await autoCtrl.createReview({
35
+ productId: "prod_1",
36
+ authorName: "Bob",
37
+ authorEmail: "bob@example.com",
38
+ rating: 4,
39
+ body: "Solid product.",
40
+ });
41
+ expect(review.status).toBe("approved");
42
+ });
43
+
44
+ it("keeps pending status when autoApprove is explicitly false", async () => {
45
+ const ctrl = createReviewController(mockData, { autoApprove: false });
46
+ const review = await ctrl.createReview({
47
+ productId: "prod_1",
48
+ authorName: "Carol",
49
+ authorEmail: "carol@example.com",
50
+ rating: 3,
51
+ body: "Average.",
52
+ });
53
+ expect(review.status).toBe("pending");
54
+ });
55
+
56
+ it("each created review gets a unique id", async () => {
57
+ const ids = new Set<string>();
58
+ for (let i = 0; i < 20; i++) {
59
+ const review = await controller.createReview({
60
+ productId: `prod_${i}`,
61
+ authorName: `Author ${i}`,
62
+ authorEmail: `author${i}@example.com`,
63
+ rating: (i % 5) + 1,
64
+ body: `Review body ${i}`,
65
+ });
66
+ ids.add(review.id);
67
+ }
68
+ expect(ids.size).toBe(20);
69
+ });
70
+
71
+ it("preserves optional fields like title, customerId, and isVerifiedPurchase", async () => {
72
+ const review = await controller.createReview({
73
+ productId: "prod_1",
74
+ authorName: "Dave",
75
+ authorEmail: "dave@example.com",
76
+ rating: 5,
77
+ title: "Amazing!",
78
+ body: "Best purchase ever.",
79
+ customerId: "cust_1",
80
+ isVerifiedPurchase: true,
81
+ });
82
+ expect(review.title).toBe("Amazing!");
83
+ expect(review.customerId).toBe("cust_1");
84
+ expect(review.isVerifiedPurchase).toBe(true);
85
+ });
86
+
87
+ it("createdAt and updatedAt are set to approximately current time", async () => {
88
+ const before = new Date();
89
+ const review = await controller.createReview({
90
+ productId: "prod_1",
91
+ authorName: "Eve",
92
+ authorEmail: "eve@example.com",
93
+ rating: 4,
94
+ body: "Good.",
95
+ });
96
+ const after = new Date();
97
+ expect(review.createdAt.getTime()).toBeGreaterThanOrEqual(
98
+ before.getTime(),
99
+ );
100
+ expect(review.createdAt.getTime()).toBeLessThanOrEqual(after.getTime());
101
+ expect(review.updatedAt.getTime()).toBeGreaterThanOrEqual(
102
+ before.getTime(),
103
+ );
104
+ expect(review.updatedAt.getTime()).toBeLessThanOrEqual(after.getTime());
105
+ });
106
+ });
107
+
108
+ // ── getReview edge cases ───────────────────────────────────────────
109
+
110
+ describe("getReview edge cases", () => {
111
+ it("returns the review by id", async () => {
112
+ const created = await controller.createReview({
113
+ productId: "prod_1",
114
+ authorName: "Alice",
115
+ authorEmail: "alice@example.com",
116
+ rating: 5,
117
+ body: "Great!",
118
+ });
119
+ const fetched = await controller.getReview(created.id);
120
+ expect(fetched).not.toBeNull();
121
+ expect(fetched?.id).toBe(created.id);
122
+ expect(fetched?.authorName).toBe("Alice");
123
+ });
124
+
125
+ it("returns null for non-existent id", async () => {
126
+ const fetched = await controller.getReview("non-existent-id");
127
+ expect(fetched).toBeNull();
128
+ });
129
+ });
130
+
131
+ // ── updateReviewStatus edge cases ──────────────────────────────────
132
+
133
+ describe("updateReviewStatus edge cases", () => {
134
+ it("updates status from pending to approved", async () => {
135
+ const review = await controller.createReview({
136
+ productId: "prod_1",
137
+ authorName: "Alice",
138
+ authorEmail: "alice@example.com",
139
+ rating: 5,
140
+ body: "Great!",
141
+ });
142
+ const updated = await controller.updateReviewStatus(
143
+ review.id,
144
+ "approved",
145
+ );
146
+ expect(updated?.status).toBe("approved");
147
+ });
148
+
149
+ it("attaches moderation note when provided", async () => {
150
+ const review = await controller.createReview({
151
+ productId: "prod_1",
152
+ authorName: "Spammer",
153
+ authorEmail: "spam@example.com",
154
+ rating: 1,
155
+ body: "Buy my product!",
156
+ });
157
+ const updated = await controller.updateReviewStatus(
158
+ review.id,
159
+ "rejected",
160
+ "Spam content detected",
161
+ );
162
+ expect(updated?.status).toBe("rejected");
163
+ expect(updated?.moderationNote).toBe("Spam content detected");
164
+ });
165
+
166
+ it("returns null for non-existent review", async () => {
167
+ const result = await controller.updateReviewStatus(
168
+ "non-existent",
169
+ "approved",
170
+ );
171
+ expect(result).toBeNull();
172
+ });
173
+
174
+ it("updates updatedAt timestamp on status change", async () => {
175
+ const review = await controller.createReview({
176
+ productId: "prod_1",
177
+ authorName: "Alice",
178
+ authorEmail: "alice@example.com",
179
+ rating: 4,
180
+ body: "Good.",
181
+ });
182
+ const updated = await controller.updateReviewStatus(
183
+ review.id,
184
+ "approved",
185
+ );
186
+ expect(updated?.updatedAt.getTime()).toBeGreaterThanOrEqual(
187
+ review.updatedAt.getTime(),
188
+ );
189
+ });
190
+ });
191
+
192
+ // ── deleteReview edge cases ────────────────────────────────────────
193
+
194
+ describe("deleteReview edge cases", () => {
195
+ it("returns true when review exists", async () => {
196
+ const review = await controller.createReview({
197
+ productId: "prod_1",
198
+ authorName: "Alice",
199
+ authorEmail: "alice@example.com",
200
+ rating: 5,
201
+ body: "Great!",
202
+ });
203
+ expect(await controller.deleteReview(review.id)).toBe(true);
204
+ });
205
+
206
+ it("returns false for non-existent review", async () => {
207
+ expect(await controller.deleteReview("non-existent")).toBe(false);
208
+ });
209
+
210
+ it("double deletion returns false on second attempt", async () => {
211
+ const review = await controller.createReview({
212
+ productId: "prod_1",
213
+ authorName: "Alice",
214
+ authorEmail: "alice@example.com",
215
+ rating: 5,
216
+ body: "Great!",
217
+ });
218
+ expect(await controller.deleteReview(review.id)).toBe(true);
219
+ expect(await controller.deleteReview(review.id)).toBe(false);
220
+ });
221
+
222
+ it("getReview returns null after deletion", async () => {
223
+ const review = await controller.createReview({
224
+ productId: "prod_1",
225
+ authorName: "Alice",
226
+ authorEmail: "alice@example.com",
227
+ rating: 3,
228
+ body: "OK.",
229
+ });
230
+ await controller.deleteReview(review.id);
231
+ expect(await controller.getReview(review.id)).toBeNull();
232
+ });
233
+ });
234
+
235
+ // ── addMerchantResponse edge cases ─────────────────────────────────
236
+
237
+ describe("addMerchantResponse edge cases", () => {
238
+ it("adds merchant response and sets merchantResponseAt", async () => {
239
+ const review = await controller.createReview({
240
+ productId: "prod_1",
241
+ authorName: "Alice",
242
+ authorEmail: "alice@example.com",
243
+ rating: 2,
244
+ body: "Not great.",
245
+ });
246
+ const before = new Date();
247
+ const updated = await controller.addMerchantResponse(
248
+ review.id,
249
+ "Sorry to hear that! Please contact support.",
250
+ );
251
+ const after = new Date();
252
+ expect(updated?.merchantResponse).toBe(
253
+ "Sorry to hear that! Please contact support.",
254
+ );
255
+ expect(updated?.merchantResponseAt).toBeInstanceOf(Date);
256
+ expect(updated?.merchantResponseAt?.getTime()).toBeGreaterThanOrEqual(
257
+ before.getTime(),
258
+ );
259
+ expect(updated?.merchantResponseAt?.getTime()).toBeLessThanOrEqual(
260
+ after.getTime(),
261
+ );
262
+ });
263
+
264
+ it("returns null for non-existent review", async () => {
265
+ const result = await controller.addMerchantResponse(
266
+ "non-existent",
267
+ "Response text",
268
+ );
269
+ expect(result).toBeNull();
270
+ });
271
+
272
+ it("overwrites previous merchant response", async () => {
273
+ const review = await controller.createReview({
274
+ productId: "prod_1",
275
+ authorName: "Bob",
276
+ authorEmail: "bob@example.com",
277
+ rating: 3,
278
+ body: "Meh.",
279
+ });
280
+ await controller.addMerchantResponse(review.id, "First response");
281
+ const updated = await controller.addMerchantResponse(
282
+ review.id,
283
+ "Updated response",
284
+ );
285
+ expect(updated?.merchantResponse).toBe("Updated response");
286
+ });
287
+ });
288
+
289
+ // ── markHelpful edge cases ─────────────────────────────────────────
290
+
291
+ describe("markHelpful edge cases", () => {
292
+ it("increments helpfulCount by 1", async () => {
293
+ const review = await controller.createReview({
294
+ productId: "prod_1",
295
+ authorName: "Alice",
296
+ authorEmail: "alice@example.com",
297
+ rating: 5,
298
+ body: "Helpful review.",
299
+ });
300
+ expect(review.helpfulCount).toBe(0);
301
+ const first = await controller.markHelpful(review.id);
302
+ expect(first?.helpfulCount).toBe(1);
303
+ });
304
+
305
+ it("increments helpfulCount multiple times correctly", async () => {
306
+ const review = await controller.createReview({
307
+ productId: "prod_1",
308
+ authorName: "Bob",
309
+ authorEmail: "bob@example.com",
310
+ rating: 4,
311
+ body: "Very useful.",
312
+ });
313
+ await controller.markHelpful(review.id);
314
+ await controller.markHelpful(review.id);
315
+ const third = await controller.markHelpful(review.id);
316
+ expect(third?.helpfulCount).toBe(3);
317
+ });
318
+
319
+ it("returns null for non-existent review", async () => {
320
+ const result = await controller.markHelpful("non-existent");
321
+ expect(result).toBeNull();
322
+ });
323
+ });
324
+
325
+ // ── getReviewAnalytics edge cases ──────────────────────────────────
326
+
327
+ describe("getReviewAnalytics edge cases", () => {
328
+ it("returns zeroes for empty store", async () => {
329
+ const analytics = await controller.getReviewAnalytics();
330
+ expect(analytics.totalReviews).toBe(0);
331
+ expect(analytics.pendingCount).toBe(0);
332
+ expect(analytics.approvedCount).toBe(0);
333
+ expect(analytics.rejectedCount).toBe(0);
334
+ expect(analytics.averageRating).toBe(0);
335
+ expect(analytics.withMerchantResponse).toBe(0);
336
+ expect(analytics.ratingsDistribution).toEqual({
337
+ "1": 0,
338
+ "2": 0,
339
+ "3": 0,
340
+ "4": 0,
341
+ "5": 0,
342
+ });
343
+ });
344
+
345
+ it("counts statuses and ratings correctly with multiple reviews", async () => {
346
+ // Create reviews with various ratings and statuses
347
+ const r1 = await controller.createReview({
348
+ productId: "prod_1",
349
+ authorName: "A",
350
+ authorEmail: "a@example.com",
351
+ rating: 5,
352
+ body: "Perfect.",
353
+ });
354
+ await controller.createReview({
355
+ productId: "prod_1",
356
+ authorName: "B",
357
+ authorEmail: "b@example.com",
358
+ rating: 4,
359
+ body: "Good.",
360
+ });
361
+ const r3 = await controller.createReview({
362
+ productId: "prod_2",
363
+ authorName: "C",
364
+ authorEmail: "c@example.com",
365
+ rating: 3,
366
+ body: "OK.",
367
+ });
368
+
369
+ // Approve r1, reject r3, leave r2 pending
370
+ await controller.updateReviewStatus(r1.id, "approved");
371
+ await controller.updateReviewStatus(r3.id, "rejected");
372
+
373
+ // Add merchant response to r1
374
+ await controller.addMerchantResponse(r1.id, "Thank you!");
375
+
376
+ const analytics = await controller.getReviewAnalytics();
377
+ expect(analytics.totalReviews).toBe(3);
378
+ expect(analytics.pendingCount).toBe(1);
379
+ expect(analytics.approvedCount).toBe(1);
380
+ expect(analytics.rejectedCount).toBe(1);
381
+ expect(analytics.withMerchantResponse).toBe(1);
382
+ // Average: (5+4+3)/3 = 4.0
383
+ expect(analytics.averageRating).toBe(4);
384
+ expect(analytics.ratingsDistribution).toEqual({
385
+ "1": 0,
386
+ "2": 0,
387
+ "3": 1,
388
+ "4": 1,
389
+ "5": 1,
390
+ });
391
+ });
392
+
393
+ it("averageRating rounds to 1 decimal place", async () => {
394
+ // Create 3 reviews: ratings 5, 4, 4 => average = 13/3 = 4.333... => 4.3
395
+ await controller.createReview({
396
+ productId: "prod_1",
397
+ authorName: "A",
398
+ authorEmail: "a@example.com",
399
+ rating: 5,
400
+ body: "Great.",
401
+ });
402
+ await controller.createReview({
403
+ productId: "prod_1",
404
+ authorName: "B",
405
+ authorEmail: "b@example.com",
406
+ rating: 4,
407
+ body: "Good.",
408
+ });
409
+ await controller.createReview({
410
+ productId: "prod_1",
411
+ authorName: "C",
412
+ authorEmail: "c@example.com",
413
+ rating: 4,
414
+ body: "Decent.",
415
+ });
416
+ const analytics = await controller.getReviewAnalytics();
417
+ expect(analytics.averageRating).toBe(4.3);
418
+ });
419
+
420
+ it("handles all 5-star ratings distribution", async () => {
421
+ for (let i = 0; i < 5; i++) {
422
+ await controller.createReview({
423
+ productId: "prod_1",
424
+ authorName: `User ${i}`,
425
+ authorEmail: `user${i}@example.com`,
426
+ rating: 5,
427
+ body: "Perfect!",
428
+ });
429
+ }
430
+ const analytics = await controller.getReviewAnalytics();
431
+ expect(analytics.averageRating).toBe(5);
432
+ expect(analytics.ratingsDistribution).toEqual({
433
+ "1": 0,
434
+ "2": 0,
435
+ "3": 0,
436
+ "4": 0,
437
+ "5": 5,
438
+ });
439
+ });
440
+ });
441
+
442
+ // ── getProductRatingSummary edge cases ──────────────────────────────
443
+
444
+ describe("getProductRatingSummary edge cases", () => {
445
+ it("returns zero summary for product with no reviews", async () => {
446
+ const summary = await controller.getProductRatingSummary("prod_none");
447
+ expect(summary.average).toBe(0);
448
+ expect(summary.count).toBe(0);
449
+ expect(summary.distribution).toEqual({
450
+ "1": 0,
451
+ "2": 0,
452
+ "3": 0,
453
+ "4": 0,
454
+ "5": 0,
455
+ });
456
+ });
457
+
458
+ it("only counts approved reviews", async () => {
459
+ const r1 = await controller.createReview({
460
+ productId: "prod_1",
461
+ authorName: "A",
462
+ authorEmail: "a@example.com",
463
+ rating: 5,
464
+ body: "Great.",
465
+ });
466
+ const r2 = await controller.createReview({
467
+ productId: "prod_1",
468
+ authorName: "B",
469
+ authorEmail: "b@example.com",
470
+ rating: 1,
471
+ body: "Terrible.",
472
+ });
473
+ await controller.createReview({
474
+ productId: "prod_1",
475
+ authorName: "C",
476
+ authorEmail: "c@example.com",
477
+ rating: 3,
478
+ body: "Meh.",
479
+ });
480
+
481
+ // Approve only r1
482
+ await controller.updateReviewStatus(r1.id, "approved");
483
+ // Reject r2
484
+ await controller.updateReviewStatus(r2.id, "rejected");
485
+ // r3 stays pending
486
+
487
+ const summary = await controller.getProductRatingSummary("prod_1");
488
+ expect(summary.count).toBe(1);
489
+ expect(summary.average).toBe(5);
490
+ expect(summary.distribution["5"]).toBe(1);
491
+ expect(summary.distribution["1"]).toBe(0);
492
+ expect(summary.distribution["3"]).toBe(0);
493
+ });
494
+
495
+ it("calculates accurate distribution with mixed approved ratings", async () => {
496
+ const autoCtrl = createReviewController(mockData, {
497
+ autoApprove: true,
498
+ });
499
+
500
+ await autoCtrl.createReview({
501
+ productId: "prod_1",
502
+ authorName: "A",
503
+ authorEmail: "a@example.com",
504
+ rating: 5,
505
+ body: "Perfect.",
506
+ });
507
+ await autoCtrl.createReview({
508
+ productId: "prod_1",
509
+ authorName: "B",
510
+ authorEmail: "b@example.com",
511
+ rating: 5,
512
+ body: "Love it.",
513
+ });
514
+ await autoCtrl.createReview({
515
+ productId: "prod_1",
516
+ authorName: "C",
517
+ authorEmail: "c@example.com",
518
+ rating: 3,
519
+ body: "Average.",
520
+ });
521
+ await autoCtrl.createReview({
522
+ productId: "prod_1",
523
+ authorName: "D",
524
+ authorEmail: "d@example.com",
525
+ rating: 1,
526
+ body: "Bad.",
527
+ });
528
+
529
+ const summary = await autoCtrl.getProductRatingSummary("prod_1");
530
+ expect(summary.count).toBe(4);
531
+ // (5+5+3+1)/4 = 14/4 = 3.5
532
+ expect(summary.average).toBe(3.5);
533
+ expect(summary.distribution).toEqual({
534
+ "1": 1,
535
+ "2": 0,
536
+ "3": 1,
537
+ "4": 0,
538
+ "5": 2,
539
+ });
540
+ });
541
+
542
+ it("ignores reviews from other products", async () => {
543
+ const autoCtrl = createReviewController(mockData, {
544
+ autoApprove: true,
545
+ });
546
+ await autoCtrl.createReview({
547
+ productId: "prod_1",
548
+ authorName: "A",
549
+ authorEmail: "a@example.com",
550
+ rating: 5,
551
+ body: "Great product 1.",
552
+ });
553
+ await autoCtrl.createReview({
554
+ productId: "prod_2",
555
+ authorName: "B",
556
+ authorEmail: "b@example.com",
557
+ rating: 1,
558
+ body: "Bad product 2.",
559
+ });
560
+
561
+ const summary = await autoCtrl.getProductRatingSummary("prod_1");
562
+ expect(summary.count).toBe(1);
563
+ expect(summary.average).toBe(5);
564
+ });
565
+ });
566
+
567
+ // ── listReviewsByProduct edge cases ─────────────────────────────────
568
+
569
+ describe("listReviewsByProduct edge cases", () => {
570
+ it("returns all reviews for a product without filters", async () => {
571
+ await controller.createReview({
572
+ productId: "prod_1",
573
+ authorName: "A",
574
+ authorEmail: "a@example.com",
575
+ rating: 5,
576
+ body: "Great.",
577
+ });
578
+ await controller.createReview({
579
+ productId: "prod_1",
580
+ authorName: "B",
581
+ authorEmail: "b@example.com",
582
+ rating: 3,
583
+ body: "OK.",
584
+ });
585
+ await controller.createReview({
586
+ productId: "prod_2",
587
+ authorName: "C",
588
+ authorEmail: "c@example.com",
589
+ rating: 4,
590
+ body: "Good.",
591
+ });
592
+
593
+ const reviews = await controller.listReviewsByProduct("prod_1");
594
+ expect(reviews).toHaveLength(2);
595
+ for (const r of reviews) {
596
+ expect(r.productId).toBe("prod_1");
597
+ }
598
+ });
599
+
600
+ it("filters by approvedOnly when set", async () => {
601
+ const r1 = await controller.createReview({
602
+ productId: "prod_1",
603
+ authorName: "A",
604
+ authorEmail: "a@example.com",
605
+ rating: 5,
606
+ body: "Great.",
607
+ });
608
+ await controller.createReview({
609
+ productId: "prod_1",
610
+ authorName: "B",
611
+ authorEmail: "b@example.com",
612
+ rating: 3,
613
+ body: "OK.",
614
+ });
615
+
616
+ await controller.updateReviewStatus(r1.id, "approved");
617
+
618
+ const approved = await controller.listReviewsByProduct("prod_1", {
619
+ approvedOnly: true,
620
+ });
621
+ expect(approved).toHaveLength(1);
622
+ expect(approved[0].status).toBe("approved");
623
+ });
624
+
625
+ it("supports pagination with take and skip", async () => {
626
+ for (let i = 0; i < 5; i++) {
627
+ await controller.createReview({
628
+ productId: "prod_1",
629
+ authorName: `Author ${i}`,
630
+ authorEmail: `author${i}@example.com`,
631
+ rating: 4,
632
+ body: `Body ${i}`,
633
+ });
634
+ }
635
+ const page1 = await controller.listReviewsByProduct("prod_1", {
636
+ take: 2,
637
+ skip: 0,
638
+ });
639
+ const page2 = await controller.listReviewsByProduct("prod_1", {
640
+ take: 2,
641
+ skip: 2,
642
+ });
643
+ const page3 = await controller.listReviewsByProduct("prod_1", {
644
+ take: 2,
645
+ skip: 4,
646
+ });
647
+ expect(page1).toHaveLength(2);
648
+ expect(page2).toHaveLength(2);
649
+ expect(page3).toHaveLength(1);
650
+ });
651
+ });
652
+
653
+ // ── listReviews edge cases ─────────────────────────────────────────
654
+
655
+ describe("listReviews edge cases", () => {
656
+ it("returns all reviews without params", async () => {
657
+ await controller.createReview({
658
+ productId: "prod_1",
659
+ authorName: "A",
660
+ authorEmail: "a@example.com",
661
+ rating: 5,
662
+ body: "Great.",
663
+ });
664
+ await controller.createReview({
665
+ productId: "prod_2",
666
+ authorName: "B",
667
+ authorEmail: "b@example.com",
668
+ rating: 3,
669
+ body: "OK.",
670
+ });
671
+ const all = await controller.listReviews();
672
+ expect(all).toHaveLength(2);
673
+ });
674
+
675
+ it("filters by status", async () => {
676
+ const r1 = await controller.createReview({
677
+ productId: "prod_1",
678
+ authorName: "A",
679
+ authorEmail: "a@example.com",
680
+ rating: 5,
681
+ body: "Great.",
682
+ });
683
+ await controller.createReview({
684
+ productId: "prod_2",
685
+ authorName: "B",
686
+ authorEmail: "b@example.com",
687
+ rating: 2,
688
+ body: "Meh.",
689
+ });
690
+
691
+ await controller.updateReviewStatus(r1.id, "approved");
692
+
693
+ const approved = await controller.listReviews({ status: "approved" });
694
+ expect(approved).toHaveLength(1);
695
+ expect(approved[0].id).toBe(r1.id);
696
+ });
697
+
698
+ it("filters by productId", async () => {
699
+ await controller.createReview({
700
+ productId: "prod_1",
701
+ authorName: "A",
702
+ authorEmail: "a@example.com",
703
+ rating: 5,
704
+ body: "Great.",
705
+ });
706
+ await controller.createReview({
707
+ productId: "prod_2",
708
+ authorName: "B",
709
+ authorEmail: "b@example.com",
710
+ rating: 3,
711
+ body: "OK.",
712
+ });
713
+ const filtered = await controller.listReviews({
714
+ productId: "prod_1",
715
+ });
716
+ expect(filtered).toHaveLength(1);
717
+ expect(filtered[0].productId).toBe("prod_1");
718
+ });
719
+ });
720
+
721
+ // ── listReviewsByCustomer edge cases ────────────────────────────────
722
+
723
+ describe("listReviewsByCustomer edge cases", () => {
724
+ it("returns reviews and total for a customer", async () => {
725
+ await controller.createReview({
726
+ productId: "prod_1",
727
+ authorName: "Alice",
728
+ authorEmail: "alice@example.com",
729
+ rating: 5,
730
+ body: "Great.",
731
+ customerId: "cust_1",
732
+ });
733
+ await controller.createReview({
734
+ productId: "prod_2",
735
+ authorName: "Alice",
736
+ authorEmail: "alice@example.com",
737
+ rating: 4,
738
+ body: "Good.",
739
+ customerId: "cust_1",
740
+ });
741
+ await controller.createReview({
742
+ productId: "prod_1",
743
+ authorName: "Bob",
744
+ authorEmail: "bob@example.com",
745
+ rating: 3,
746
+ body: "OK.",
747
+ customerId: "cust_2",
748
+ });
749
+
750
+ const result = await controller.listReviewsByCustomer("cust_1");
751
+ expect(result.reviews).toHaveLength(2);
752
+ expect(result.total).toBe(2);
753
+ });
754
+
755
+ it("filters by status for a customer", async () => {
756
+ const r1 = await controller.createReview({
757
+ productId: "prod_1",
758
+ authorName: "Alice",
759
+ authorEmail: "alice@example.com",
760
+ rating: 5,
761
+ body: "Great.",
762
+ customerId: "cust_1",
763
+ });
764
+ await controller.createReview({
765
+ productId: "prod_2",
766
+ authorName: "Alice",
767
+ authorEmail: "alice@example.com",
768
+ rating: 4,
769
+ body: "Good.",
770
+ customerId: "cust_1",
771
+ });
772
+
773
+ await controller.updateReviewStatus(r1.id, "approved");
774
+
775
+ const result = await controller.listReviewsByCustomer("cust_1", {
776
+ status: "approved",
777
+ });
778
+ expect(result.reviews).toHaveLength(1);
779
+ expect(result.reviews[0].status).toBe("approved");
780
+ expect(result.total).toBe(1);
781
+ });
782
+
783
+ it("paginates correctly with take and skip", async () => {
784
+ for (let i = 0; i < 5; i++) {
785
+ await controller.createReview({
786
+ productId: `prod_${i}`,
787
+ authorName: "Alice",
788
+ authorEmail: "alice@example.com",
789
+ rating: 4,
790
+ body: `Review ${i}`,
791
+ customerId: "cust_1",
792
+ });
793
+ }
794
+
795
+ const page1 = await controller.listReviewsByCustomer("cust_1", {
796
+ take: 2,
797
+ skip: 0,
798
+ });
799
+ expect(page1.reviews).toHaveLength(2);
800
+ expect(page1.total).toBe(5);
801
+
802
+ const page2 = await controller.listReviewsByCustomer("cust_1", {
803
+ take: 2,
804
+ skip: 2,
805
+ });
806
+ expect(page2.reviews).toHaveLength(2);
807
+ expect(page2.total).toBe(5);
808
+
809
+ const page3 = await controller.listReviewsByCustomer("cust_1", {
810
+ take: 2,
811
+ skip: 4,
812
+ });
813
+ expect(page3.reviews).toHaveLength(1);
814
+ expect(page3.total).toBe(5);
815
+ });
816
+
817
+ it("returns empty for customer with no reviews", async () => {
818
+ const result = await controller.listReviewsByCustomer("cust_none");
819
+ expect(result.reviews).toHaveLength(0);
820
+ expect(result.total).toBe(0);
821
+ });
822
+ });
823
+
824
+ // ── createReviewRequest edge cases ──────────────────────────────────
825
+
826
+ describe("createReviewRequest edge cases", () => {
827
+ it("creates a review request with all fields", async () => {
828
+ const request = await controller.createReviewRequest({
829
+ orderId: "order_1",
830
+ orderNumber: "ORD-001",
831
+ email: "customer@example.com",
832
+ customerName: "Alice",
833
+ items: [
834
+ { productId: "prod_1", name: "Widget A" },
835
+ { productId: "prod_2", name: "Widget B" },
836
+ ],
837
+ });
838
+ expect(request.id).toBeDefined();
839
+ expect(request.orderId).toBe("order_1");
840
+ expect(request.orderNumber).toBe("ORD-001");
841
+ expect(request.email).toBe("customer@example.com");
842
+ expect(request.customerName).toBe("Alice");
843
+ expect(request.items).toHaveLength(2);
844
+ expect(request.sentAt).toBeInstanceOf(Date);
845
+ });
846
+ });
847
+
848
+ // ── getReviewRequest edge cases ─────────────────────────────────────
849
+
850
+ describe("getReviewRequest edge cases", () => {
851
+ it("finds review request by orderId", async () => {
852
+ await controller.createReviewRequest({
853
+ orderId: "order_1",
854
+ orderNumber: "ORD-001",
855
+ email: "customer@example.com",
856
+ customerName: "Alice",
857
+ items: [{ productId: "prod_1", name: "Widget" }],
858
+ });
859
+ const found = await controller.getReviewRequest("order_1");
860
+ expect(found).not.toBeNull();
861
+ expect(found?.orderId).toBe("order_1");
862
+ });
863
+
864
+ it("returns null for non-existent orderId", async () => {
865
+ const found = await controller.getReviewRequest("non-existent");
866
+ expect(found).toBeNull();
867
+ });
868
+ });
869
+
870
+ // ── listReviewRequests edge cases ───────────────────────────────────
871
+
872
+ describe("listReviewRequests edge cases", () => {
873
+ it("returns all requests without params", async () => {
874
+ await controller.createReviewRequest({
875
+ orderId: "order_1",
876
+ orderNumber: "ORD-001",
877
+ email: "a@example.com",
878
+ customerName: "A",
879
+ items: [{ productId: "prod_1", name: "Widget" }],
880
+ });
881
+ await controller.createReviewRequest({
882
+ orderId: "order_2",
883
+ orderNumber: "ORD-002",
884
+ email: "b@example.com",
885
+ customerName: "B",
886
+ items: [{ productId: "prod_2", name: "Gadget" }],
887
+ });
888
+ const all = await controller.listReviewRequests();
889
+ expect(all).toHaveLength(2);
890
+ });
891
+
892
+ it("supports pagination", async () => {
893
+ for (let i = 0; i < 5; i++) {
894
+ await controller.createReviewRequest({
895
+ orderId: `order_${i}`,
896
+ orderNumber: `ORD-00${i}`,
897
+ email: `user${i}@example.com`,
898
+ customerName: `User ${i}`,
899
+ items: [{ productId: `prod_${i}`, name: `Product ${i}` }],
900
+ });
901
+ }
902
+ const page = await controller.listReviewRequests({
903
+ take: 2,
904
+ skip: 0,
905
+ });
906
+ expect(page).toHaveLength(2);
907
+ });
908
+ });
909
+
910
+ // ── getReviewRequestStats edge cases ────────────────────────────────
911
+
912
+ describe("getReviewRequestStats edge cases", () => {
913
+ it("returns zeroes for empty store", async () => {
914
+ const stats = await controller.getReviewRequestStats();
915
+ expect(stats.totalSent).toBe(0);
916
+ expect(stats.uniqueOrders).toBe(0);
917
+ });
918
+
919
+ it("counts unique orders correctly with duplicate orderId requests", async () => {
920
+ await controller.createReviewRequest({
921
+ orderId: "order_1",
922
+ orderNumber: "ORD-001",
923
+ email: "a@example.com",
924
+ customerName: "A",
925
+ items: [{ productId: "prod_1", name: "Widget" }],
926
+ });
927
+ await controller.createReviewRequest({
928
+ orderId: "order_1",
929
+ orderNumber: "ORD-001",
930
+ email: "a@example.com",
931
+ customerName: "A",
932
+ items: [{ productId: "prod_2", name: "Gadget" }],
933
+ });
934
+ await controller.createReviewRequest({
935
+ orderId: "order_2",
936
+ orderNumber: "ORD-002",
937
+ email: "b@example.com",
938
+ customerName: "B",
939
+ items: [{ productId: "prod_3", name: "Doohickey" }],
940
+ });
941
+
942
+ const stats = await controller.getReviewRequestStats();
943
+ expect(stats.totalSent).toBe(3);
944
+ expect(stats.uniqueOrders).toBe(2);
945
+ });
946
+ });
947
+
948
+ // ── full lifecycle scenarios ────────────────────────────────────────
949
+
950
+ describe("full lifecycle scenarios", () => {
951
+ it("create, approve, add response, mark helpful, verify analytics", async () => {
952
+ const review = await controller.createReview({
953
+ productId: "prod_1",
954
+ authorName: "Alice",
955
+ authorEmail: "alice@example.com",
956
+ rating: 5,
957
+ body: "Absolutely love this product!",
958
+ customerId: "cust_1",
959
+ isVerifiedPurchase: true,
960
+ });
961
+
962
+ // Approve
963
+ await controller.updateReviewStatus(review.id, "approved");
964
+
965
+ // Add merchant response
966
+ await controller.addMerchantResponse(
967
+ review.id,
968
+ "Thank you for your kind words!",
969
+ );
970
+
971
+ // Mark helpful multiple times
972
+ await controller.markHelpful(review.id);
973
+ await controller.markHelpful(review.id);
974
+
975
+ // Verify final state
976
+ const final = await controller.getReview(review.id);
977
+ expect(final?.status).toBe("approved");
978
+ expect(final?.merchantResponse).toBe("Thank you for your kind words!");
979
+ expect(final?.helpfulCount).toBe(2);
980
+ expect(final?.isVerifiedPurchase).toBe(true);
981
+
982
+ // Verify analytics
983
+ const analytics = await controller.getReviewAnalytics();
984
+ expect(analytics.totalReviews).toBe(1);
985
+ expect(analytics.approvedCount).toBe(1);
986
+ expect(analytics.withMerchantResponse).toBe(1);
987
+ expect(analytics.averageRating).toBe(5);
988
+
989
+ // Verify product summary
990
+ const summary = await controller.getProductRatingSummary("prod_1");
991
+ expect(summary.count).toBe(1);
992
+ expect(summary.average).toBe(5);
993
+ });
994
+
995
+ it("analytics reflect deletions", async () => {
996
+ const r1 = await controller.createReview({
997
+ productId: "prod_1",
998
+ authorName: "A",
999
+ authorEmail: "a@example.com",
1000
+ rating: 5,
1001
+ body: "Great.",
1002
+ });
1003
+ await controller.createReview({
1004
+ productId: "prod_1",
1005
+ authorName: "B",
1006
+ authorEmail: "b@example.com",
1007
+ rating: 3,
1008
+ body: "OK.",
1009
+ });
1010
+
1011
+ let analytics = await controller.getReviewAnalytics();
1012
+ expect(analytics.totalReviews).toBe(2);
1013
+
1014
+ await controller.deleteReview(r1.id);
1015
+
1016
+ analytics = await controller.getReviewAnalytics();
1017
+ expect(analytics.totalReviews).toBe(1);
1018
+ expect(analytics.averageRating).toBe(3);
1019
+ });
1020
+ });
1021
+
1022
+ // ── data store consistency ──────────────────────────────────────────
1023
+
1024
+ describe("data store consistency", () => {
1025
+ it("review store size matches expected items", async () => {
1026
+ await controller.createReview({
1027
+ productId: "prod_1",
1028
+ authorName: "A",
1029
+ authorEmail: "a@example.com",
1030
+ rating: 5,
1031
+ body: "Great.",
1032
+ });
1033
+ await controller.createReview({
1034
+ productId: "prod_2",
1035
+ authorName: "B",
1036
+ authorEmail: "b@example.com",
1037
+ rating: 3,
1038
+ body: "OK.",
1039
+ });
1040
+ expect(mockData.size("review")).toBe(2);
1041
+ });
1042
+
1043
+ it("review request store size matches expected items", async () => {
1044
+ await controller.createReviewRequest({
1045
+ orderId: "order_1",
1046
+ orderNumber: "ORD-001",
1047
+ email: "a@example.com",
1048
+ customerName: "A",
1049
+ items: [{ productId: "prod_1", name: "Widget" }],
1050
+ });
1051
+ expect(mockData.size("reviewRequest")).toBe(1);
1052
+ });
1053
+
1054
+ it("store is empty after deleting all reviews", async () => {
1055
+ const r1 = await controller.createReview({
1056
+ productId: "prod_1",
1057
+ authorName: "A",
1058
+ authorEmail: "a@example.com",
1059
+ rating: 5,
1060
+ body: "Great.",
1061
+ });
1062
+ const r2 = await controller.createReview({
1063
+ productId: "prod_2",
1064
+ authorName: "B",
1065
+ authorEmail: "b@example.com",
1066
+ rating: 3,
1067
+ body: "OK.",
1068
+ });
1069
+ await controller.deleteReview(r1.id);
1070
+ await controller.deleteReview(r2.id);
1071
+ expect(mockData.size("review")).toBe(0);
1072
+ });
1073
+ });
1074
+ });