@blamejs/blamejs-shop 0.0.65 → 0.0.70

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 (54) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/lib/assembly-instructions.js +777 -0
  3. package/lib/auto-replenish.js +933 -0
  4. package/lib/business-hours.js +980 -0
  5. package/lib/click-and-collect.js +711 -0
  6. package/lib/clickstream.js +713 -0
  7. package/lib/cost-layers.js +774 -0
  8. package/lib/credit-limits.js +752 -0
  9. package/lib/currency-rounding.js +525 -0
  10. package/lib/customer-activity.js +862 -0
  11. package/lib/customer-notes.js +712 -0
  12. package/lib/customer-risk-profile.js +593 -0
  13. package/lib/customer-surveys.js +1012 -0
  14. package/lib/damage-photos.js +473 -0
  15. package/lib/discount-allocation.js +557 -0
  16. package/lib/dropship-forwarding.js +645 -0
  17. package/lib/email-templates.js +817 -0
  18. package/lib/index.js +45 -0
  19. package/lib/inventory-allocations.js +559 -0
  20. package/lib/inventory-writeoffs.js +636 -0
  21. package/lib/knowledge-base.js +1104 -0
  22. package/lib/locale-router.js +1077 -0
  23. package/lib/operator-roles.js +768 -0
  24. package/lib/order-escalation.js +951 -0
  25. package/lib/order-ratings.js +495 -0
  26. package/lib/order-tags.js +944 -0
  27. package/lib/packing-slips.js +810 -0
  28. package/lib/payment-retries.js +816 -0
  29. package/lib/pick-lists.js +639 -0
  30. package/lib/pixel-events.js +995 -0
  31. package/lib/preorder.js +595 -0
  32. package/lib/print-queue.js +681 -0
  33. package/lib/product-qa.js +749 -0
  34. package/lib/promo-bundles.js +835 -0
  35. package/lib/push-notifications.js +937 -0
  36. package/lib/refund-automation.js +853 -0
  37. package/lib/reorder-reminders.js +798 -0
  38. package/lib/robots-config.js +753 -0
  39. package/lib/seller-signup.js +1052 -0
  40. package/lib/site-redirects.js +690 -0
  41. package/lib/sitemap-generator.js +717 -0
  42. package/lib/subscription-gifts.js +710 -0
  43. package/lib/tax-cert-renewals.js +632 -0
  44. package/lib/theme-assets.js +711 -0
  45. package/lib/tier-benefits.js +776 -0
  46. package/lib/vendor/MANIFEST.json +2 -2
  47. package/lib/vendor/blamejs/CHANGELOG.md +2 -0
  48. package/lib/vendor/blamejs/api-snapshot.json +2 -2
  49. package/lib/vendor/blamejs/lib/metrics.js +68 -4
  50. package/lib/vendor/blamejs/package.json +1 -1
  51. package/lib/vendor/blamejs/release-notes/v0.12.5.json +40 -0
  52. package/lib/wishlist-alerts.js +842 -0
  53. package/lib/wishlist-sharing.js +718 -0
  54. package/package.json +1 -1
@@ -0,0 +1,495 @@
1
+ "use strict";
2
+ /**
3
+ * @module shop.orderRatings
4
+ * @title Per-order rating primitive — shipping / packaging /
5
+ * recommend-to-friend feedback against a single order.
6
+ *
7
+ * @intro
8
+ * A focused feedback row tied to one order. Distinct from `reviews`
9
+ * (per-product star ratings) and from `customerSurveys` (NPS / CSAT
10
+ * / CES survey instruments with multi-question shapes). The
11
+ * primitive answers one question per order:
12
+ *
13
+ * "How did the customer rate the shipping, packaging, and
14
+ * likelihood to recommend us — and did they leave a comment that
15
+ * needs moderation or a public operator reply?"
16
+ *
17
+ * Surface:
18
+ * - `submitRating({ order_id, customer_id, shipping_rating,
19
+ * packaging_rating, recommend_rating, comment? })`
20
+ * Creates the rating row. Refuses ratings outside [1, 5],
21
+ * refuses control bytes in the comment, refuses a duplicate
22
+ * submission for the same order_id (one rating per order;
23
+ * enforced both by the UNIQUE constraint and by a primitive-
24
+ * layer pre-insert check so the error surfaces with a stable
25
+ * shape).
26
+ * - `getRating({ order_id })`
27
+ * Returns the rating row keyed by order_id, or null when the
28
+ * order has no rating yet. Renders the comment HTML-escaped
29
+ * via `b.template.escapeHtml` so callers that splice the
30
+ * rendered field into a template can't introduce script
31
+ * content from the customer-supplied string.
32
+ * - `ratingsForCustomer({ customer_id })`
33
+ * Lists every rating a customer has left, newest first.
34
+ * - `aggregateForPeriod({ from, to })`
35
+ * Returns the per-rating-axis mean + count + distribution
36
+ * buckets across the [from, to] window. Empty window returns
37
+ * zeroed buckets so the operator can distinguish "no
38
+ * ratings" from "low ratings."
39
+ * - `flagComment({ rating_id, reason, flagged_by })`
40
+ * Operator-side moderation: marks a comment as flagged so
41
+ * storefront renderers can suppress it. Refused when the
42
+ * comment is already flagged (operators clear-and-re-flag
43
+ * when the reason changes; the primitive doesn't silently
44
+ * overwrite). Refused when the rating row has no comment in
45
+ * the first place — flagging an empty string is a smell.
46
+ * - `responseToCustomer({ rating_id, response, responded_by })`
47
+ * Operator's public reply to the rating. One reply per
48
+ * rating; refused on second call. The reply renders HTML-
49
+ * escaped alongside the comment.
50
+ * - `topPositiveRatings({ from, to, limit })`
51
+ * Highest-scoring ratings (sum of the three axes) in the
52
+ * window, ordered (score DESC, occurred_at DESC, id DESC).
53
+ * - `topNegativeRatings({ from, to, limit })`
54
+ * Inverse of topPositive — lowest-scoring ratings first.
55
+ *
56
+ * Comment rendering: `getRating` exposes both the raw `comment`
57
+ * field and `comment_html` (the HTML-escaped form). Templates
58
+ * splicing the rendered field don't have to remember to escape;
59
+ * templates that need the raw text (export, analytics) read
60
+ * `comment`. Same shape for `response_text` / `response_html`.
61
+ *
62
+ * Composes:
63
+ * - `b.guardUuid` — UUID-shape validation for
64
+ * order_id / customer_id / actor /
65
+ * rating_id.
66
+ * - `b.uuid.v7` — row id (lexicographic monotonic so
67
+ * audit reads sort cleanly).
68
+ * - `b.template.escapeHtml` — comment + response render layer.
69
+ *
70
+ * Storage: `migrations-d1/0151_order_ratings.sql`.
71
+ *
72
+ * @primitive orderRatings
73
+ * @related reviews, customerSurveys, b.template.escapeHtml
74
+ */
75
+
76
+ var bShop;
77
+ function _b() {
78
+ if (!bShop) bShop = require("./index");
79
+ return bShop.framework;
80
+ }
81
+
82
+ // ---- constants ----------------------------------------------------------
83
+
84
+ var MIN_RATING = 1;
85
+ var MAX_RATING = 5;
86
+ var RATING_AXES = Object.freeze(["shipping", "packaging", "recommend"]);
87
+ var MAX_COMMENT_LEN = 2000;
88
+ var MAX_FLAG_REASON_LEN = 500;
89
+ var MAX_RESPONSE_LEN = 2000;
90
+ var DEFAULT_LIST_LIMIT = 50;
91
+ var MAX_LIST_LIMIT = 500;
92
+ var DEFAULT_TOP_LIMIT = 10;
93
+ var MAX_TOP_LIMIT = 100;
94
+
95
+ var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
96
+
97
+ // ---- monotonic clock ----------------------------------------------------
98
+ //
99
+ // Submissions persist epoch-ms timestamps. Two same-millisecond
100
+ // `_now()` calls produce distinct integers so the row-ordering on
101
+ // `occurred_at` is deterministic without an extra tiebreaker column.
102
+ // Tests that submit ratings in tight loops rely on this for ordering
103
+ // assertions (topPositive / topNegative tiebreakers fall back to
104
+ // occurred_at DESC, then id DESC).
105
+ var _lastTs = 0;
106
+ function _now() {
107
+ var t = Date.now();
108
+ if (t <= _lastTs) { t = _lastTs + 1; }
109
+ _lastTs = t;
110
+ return t;
111
+ }
112
+
113
+ // ---- validators ---------------------------------------------------------
114
+
115
+ function _uuid(s, label) {
116
+ try { return _b().guardUuid.sanitize(s, { profile: "strict" }); }
117
+ catch (e) { throw new TypeError("orderRatings: " + label + " — " + (e && e.message || "invalid UUID")); }
118
+ }
119
+
120
+ function _rating(n, label) {
121
+ if (typeof n !== "number" || !Number.isInteger(n) || n < MIN_RATING || n > MAX_RATING) {
122
+ throw new TypeError("orderRatings: " + label +
123
+ " must be an integer in [" + MIN_RATING + ", " + MAX_RATING + "]");
124
+ }
125
+ return n;
126
+ }
127
+
128
+ function _comment(s) {
129
+ if (s == null) return null;
130
+ if (typeof s !== "string") {
131
+ throw new TypeError("orderRatings: comment must be a string when provided");
132
+ }
133
+ if (!s.length) return null;
134
+ if (s.length > MAX_COMMENT_LEN) {
135
+ throw new TypeError("orderRatings: comment must be <= " + MAX_COMMENT_LEN + " chars");
136
+ }
137
+ if (CONTROL_BYTE_RE.test(s)) {
138
+ throw new TypeError("orderRatings: comment must not contain control bytes");
139
+ }
140
+ return s;
141
+ }
142
+
143
+ function _flagReason(s) {
144
+ if (typeof s !== "string" || !s.length || s.length > MAX_FLAG_REASON_LEN) {
145
+ throw new TypeError("orderRatings: reason must be a non-empty string <= " + MAX_FLAG_REASON_LEN + " chars");
146
+ }
147
+ if (CONTROL_BYTE_RE.test(s)) {
148
+ throw new TypeError("orderRatings: reason must not contain control bytes");
149
+ }
150
+ return s;
151
+ }
152
+
153
+ function _responseText(s) {
154
+ if (typeof s !== "string" || !s.length || s.length > MAX_RESPONSE_LEN) {
155
+ throw new TypeError("orderRatings: response must be a non-empty string <= " + MAX_RESPONSE_LEN + " chars");
156
+ }
157
+ if (CONTROL_BYTE_RE.test(s)) {
158
+ throw new TypeError("orderRatings: response must not contain control bytes");
159
+ }
160
+ return s;
161
+ }
162
+
163
+ function _epoch(n, label) {
164
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 0) {
165
+ throw new TypeError("orderRatings: " + label + " must be a non-negative integer (ms epoch)");
166
+ }
167
+ return n;
168
+ }
169
+
170
+ function _limit(n, defaultN, maxN, label) {
171
+ if (n == null) return defaultN;
172
+ if (!Number.isInteger(n) || n <= 0 || n > maxN) {
173
+ throw new TypeError("orderRatings: " + label + " must be an integer in [1, " + maxN + "]");
174
+ }
175
+ return n;
176
+ }
177
+
178
+ // ---- render helpers -----------------------------------------------------
179
+
180
+ function _esc(s) {
181
+ if (s == null) return null;
182
+ return _b().template.escapeHtml(s);
183
+ }
184
+
185
+ // ---- factory ------------------------------------------------------------
186
+
187
+ function create(opts) {
188
+ opts = opts || {};
189
+ var query = opts.query;
190
+ if (!query) {
191
+ query = function (sql, params) { return _b().externalDb.query(sql, params); };
192
+ }
193
+
194
+ function _decode(row) {
195
+ if (!row) return null;
196
+ return {
197
+ id: row.id,
198
+ order_id: row.order_id,
199
+ customer_id: row.customer_id,
200
+ shipping_rating: Number(row.shipping_rating),
201
+ packaging_rating: Number(row.packaging_rating),
202
+ recommend_rating: Number(row.recommend_rating),
203
+ comment: row.comment == null ? null : row.comment,
204
+ comment_html: row.comment == null ? null : _esc(row.comment),
205
+ comment_flagged: row.comment_flagged === 1 || row.comment_flagged === true,
206
+ flag_reason: row.flag_reason == null ? null : row.flag_reason,
207
+ flag_actor: row.flag_actor == null ? null : row.flag_actor,
208
+ response_text: row.response_text == null ? null : row.response_text,
209
+ response_html: row.response_text == null ? null : _esc(row.response_text),
210
+ response_actor: row.response_actor == null ? null : row.response_actor,
211
+ response_at: row.response_at == null ? null : Number(row.response_at),
212
+ occurred_at: Number(row.occurred_at),
213
+ };
214
+ }
215
+
216
+ async function _rowById(id) {
217
+ var r = await query("SELECT * FROM order_ratings WHERE id = ?1", [id]);
218
+ return r.rows[0] || null;
219
+ }
220
+
221
+ async function _rowByOrder(orderId) {
222
+ var r = await query("SELECT * FROM order_ratings WHERE order_id = ?1", [orderId]);
223
+ return r.rows[0] || null;
224
+ }
225
+
226
+ // ---- submitRating ----------------------------------------------------
227
+
228
+ async function submitRating(input) {
229
+ if (!input || typeof input !== "object") {
230
+ throw new TypeError("orderRatings.submitRating: input object required");
231
+ }
232
+ var orderId = _uuid(input.order_id, "order_id");
233
+ var customerId = _uuid(input.customer_id, "customer_id");
234
+ var shipping = _rating(input.shipping_rating, "shipping_rating");
235
+ var packaging = _rating(input.packaging_rating, "packaging_rating");
236
+ var recommend = _rating(input.recommend_rating, "recommend_rating");
237
+ var comment = _comment(input.comment);
238
+
239
+ // Pre-insert duplicate check so a second submission surfaces a
240
+ // typed error with a stable code rather than a UNIQUE-constraint
241
+ // SQLite error message that varies across drivers. The schema
242
+ // UNIQUE is the second line of defense — a race that slips past
243
+ // this check still trips the constraint and the caller gets the
244
+ // driver-level error.
245
+ var existing = await _rowByOrder(orderId);
246
+ if (existing) {
247
+ var dupe = new Error("orderRatings.submitRating: order " + JSON.stringify(orderId) +
248
+ " already has a rating");
249
+ dupe.code = "ORDER_RATING_ALREADY_EXISTS";
250
+ throw dupe;
251
+ }
252
+
253
+ var id = _b().uuid.v7();
254
+ var ts = _now();
255
+ await query(
256
+ "INSERT INTO order_ratings " +
257
+ "(id, order_id, customer_id, shipping_rating, packaging_rating, recommend_rating, " +
258
+ " comment, comment_flagged, flag_reason, flag_actor, " +
259
+ " response_text, response_actor, response_at, occurred_at) " +
260
+ "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, 0, NULL, NULL, NULL, NULL, NULL, ?8)",
261
+ [id, orderId, customerId, shipping, packaging, recommend, comment, ts],
262
+ );
263
+ return _decode(await _rowById(id));
264
+ }
265
+
266
+ // ---- getRating -------------------------------------------------------
267
+
268
+ async function getRating(input) {
269
+ if (!input || typeof input !== "object") {
270
+ throw new TypeError("orderRatings.getRating: input object required");
271
+ }
272
+ var orderId = _uuid(input.order_id, "order_id");
273
+ return _decode(await _rowByOrder(orderId));
274
+ }
275
+
276
+ // ---- ratingsForCustomer ----------------------------------------------
277
+
278
+ async function ratingsForCustomer(input) {
279
+ if (!input || typeof input !== "object") {
280
+ throw new TypeError("orderRatings.ratingsForCustomer: input object required");
281
+ }
282
+ var customerId = _uuid(input.customer_id, "customer_id");
283
+ var limit = _limit(input.limit, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT, "limit");
284
+ var r = await query(
285
+ "SELECT * FROM order_ratings WHERE customer_id = ?1 " +
286
+ "ORDER BY occurred_at DESC, id DESC LIMIT ?2",
287
+ [customerId, limit],
288
+ );
289
+ var out = [];
290
+ for (var i = 0; i < r.rows.length; i += 1) out.push(_decode(r.rows[i]));
291
+ return out;
292
+ }
293
+
294
+ // ---- aggregateForPeriod ----------------------------------------------
295
+
296
+ async function aggregateForPeriod(input) {
297
+ if (!input || typeof input !== "object") {
298
+ throw new TypeError("orderRatings.aggregateForPeriod: input object required");
299
+ }
300
+ var from = _epoch(input.from, "from");
301
+ var to = _epoch(input.to, "to");
302
+ if (from > to) {
303
+ throw new TypeError("orderRatings.aggregateForPeriod: from must be <= to");
304
+ }
305
+
306
+ var r = await query(
307
+ "SELECT shipping_rating, packaging_rating, recommend_rating " +
308
+ "FROM order_ratings WHERE occurred_at >= ?1 AND occurred_at <= ?2",
309
+ [from, to],
310
+ );
311
+
312
+ // Seed bucket shapes from RATING_AXES so the rollup always carries
313
+ // every axis even when the window is empty — operators rendering
314
+ // a dashboard get the same column list regardless of traffic.
315
+ var axes = {};
316
+ for (var a = 0; a < RATING_AXES.length; a += 1) {
317
+ var axisLabel = RATING_AXES[a];
318
+ var dist = {};
319
+ for (var v = MIN_RATING; v <= MAX_RATING; v += 1) dist[String(v)] = 0;
320
+ axes[axisLabel] = { count: 0, sum: 0, mean: 0, distribution: dist };
321
+ }
322
+
323
+ for (var i = 0; i < r.rows.length; i += 1) {
324
+ var row = r.rows[i];
325
+ _accumulate(axes.shipping, Number(row.shipping_rating));
326
+ _accumulate(axes.packaging, Number(row.packaging_rating));
327
+ _accumulate(axes.recommend, Number(row.recommend_rating));
328
+ }
329
+
330
+ for (var b = 0; b < RATING_AXES.length; b += 1) {
331
+ var bucket = axes[RATING_AXES[b]];
332
+ if (bucket.count > 0) {
333
+ // Mean rounded to 2 decimals — operator-facing display
334
+ // precision. Sum + count are also exposed so a caller that
335
+ // wants more precision can recompute.
336
+ bucket.mean = Math.round((bucket.sum / bucket.count) * 100) / 100;
337
+ }
338
+ }
339
+
340
+ return {
341
+ from: from,
342
+ to: to,
343
+ response_count: r.rows.length,
344
+ shipping: axes.shipping,
345
+ packaging: axes.packaging,
346
+ recommend: axes.recommend,
347
+ };
348
+ }
349
+
350
+ // ---- flagComment -----------------------------------------------------
351
+
352
+ async function flagComment(input) {
353
+ if (!input || typeof input !== "object") {
354
+ throw new TypeError("orderRatings.flagComment: input object required");
355
+ }
356
+ var ratingId = _uuid(input.rating_id, "rating_id");
357
+ var reason = _flagReason(input.reason);
358
+ var flaggedBy = _uuid(input.flagged_by, "flagged_by");
359
+
360
+ var row = await _rowById(ratingId);
361
+ if (!row) {
362
+ var miss = new Error("orderRatings.flagComment: rating not found");
363
+ miss.code = "ORDER_RATING_NOT_FOUND";
364
+ throw miss;
365
+ }
366
+ if (row.comment == null) {
367
+ // Flagging an empty-comment rating is a smell — operators flag
368
+ // text, not the absence of text. Refuse so a UI bug that calls
369
+ // flagComment without checking the row first surfaces here.
370
+ var noComment = new Error("orderRatings.flagComment: rating has no comment to flag");
371
+ noComment.code = "ORDER_RATING_NO_COMMENT";
372
+ throw noComment;
373
+ }
374
+ if (row.comment_flagged === 1 || row.comment_flagged === true) {
375
+ var already = new Error("orderRatings.flagComment: comment already flagged");
376
+ already.code = "ORDER_RATING_ALREADY_FLAGGED";
377
+ throw already;
378
+ }
379
+ await query(
380
+ "UPDATE order_ratings SET comment_flagged = 1, flag_reason = ?1, flag_actor = ?2 " +
381
+ "WHERE id = ?3",
382
+ [reason, flaggedBy, ratingId],
383
+ );
384
+ return _decode(await _rowById(ratingId));
385
+ }
386
+
387
+ // ---- responseToCustomer ----------------------------------------------
388
+
389
+ async function responseToCustomer(input) {
390
+ if (!input || typeof input !== "object") {
391
+ throw new TypeError("orderRatings.responseToCustomer: input object required");
392
+ }
393
+ var ratingId = _uuid(input.rating_id, "rating_id");
394
+ var response = _responseText(input.response);
395
+ var respondedBy = _uuid(input.responded_by, "responded_by");
396
+
397
+ var row = await _rowById(ratingId);
398
+ if (!row) {
399
+ var miss = new Error("orderRatings.responseToCustomer: rating not found");
400
+ miss.code = "ORDER_RATING_NOT_FOUND";
401
+ throw miss;
402
+ }
403
+ if (row.response_text != null) {
404
+ // One operator reply per rating. A second call could be an
405
+ // edit; the primitive doesn't expose that path because the
406
+ // public reply is part of the storefront-visible audit trail
407
+ // and silent rewrites would obscure it. An operator who needs
408
+ // to revise composes a clear-and-rewrite path through the
409
+ // admin surface.
410
+ var already = new Error("orderRatings.responseToCustomer: rating already has a response");
411
+ already.code = "ORDER_RATING_ALREADY_RESPONDED";
412
+ throw already;
413
+ }
414
+ var ts = _now();
415
+ await query(
416
+ "UPDATE order_ratings SET response_text = ?1, response_actor = ?2, response_at = ?3 " +
417
+ "WHERE id = ?4",
418
+ [response, respondedBy, ts, ratingId],
419
+ );
420
+ return _decode(await _rowById(ratingId));
421
+ }
422
+
423
+ // ---- topPositive / topNegative ---------------------------------------
424
+
425
+ async function _topRatings(input, label, direction) {
426
+ if (!input || typeof input !== "object") {
427
+ throw new TypeError("orderRatings." + label + ": input object required");
428
+ }
429
+ var from = _epoch(input.from, "from");
430
+ var to = _epoch(input.to, "to");
431
+ if (from > to) {
432
+ throw new TypeError("orderRatings." + label + ": from must be <= to");
433
+ }
434
+ var limit = _limit(input.limit, DEFAULT_TOP_LIMIT, MAX_TOP_LIMIT, "limit");
435
+
436
+ // Sum of the three axes is the score. DESC for top-positive,
437
+ // ASC for top-negative. Secondary order on occurred_at DESC so
438
+ // the most recent rating wins ties; tertiary on id DESC for total
439
+ // ordering across same-millisecond submissions.
440
+ var orderClause = direction === "positive"
441
+ ? "ORDER BY (shipping_rating + packaging_rating + recommend_rating) DESC, occurred_at DESC, id DESC"
442
+ : "ORDER BY (shipping_rating + packaging_rating + recommend_rating) ASC, occurred_at DESC, id DESC";
443
+
444
+ var r = await query(
445
+ "SELECT * FROM order_ratings WHERE occurred_at >= ?1 AND occurred_at <= ?2 " +
446
+ orderClause + " LIMIT ?3",
447
+ [from, to, limit],
448
+ );
449
+ var out = [];
450
+ for (var i = 0; i < r.rows.length; i += 1) out.push(_decode(r.rows[i]));
451
+ return out;
452
+ }
453
+
454
+ async function topPositiveRatings(input) {
455
+ return _topRatings(input, "topPositiveRatings", "positive");
456
+ }
457
+
458
+ async function topNegativeRatings(input) {
459
+ return _topRatings(input, "topNegativeRatings", "negative");
460
+ }
461
+
462
+ return {
463
+ RATING_AXES: RATING_AXES.slice(),
464
+ MIN_RATING: MIN_RATING,
465
+ MAX_RATING: MAX_RATING,
466
+ MAX_COMMENT_LEN: MAX_COMMENT_LEN,
467
+ MAX_FLAG_REASON_LEN: MAX_FLAG_REASON_LEN,
468
+ MAX_RESPONSE_LEN: MAX_RESPONSE_LEN,
469
+
470
+ submitRating: submitRating,
471
+ getRating: getRating,
472
+ ratingsForCustomer: ratingsForCustomer,
473
+ aggregateForPeriod: aggregateForPeriod,
474
+ flagComment: flagComment,
475
+ responseToCustomer: responseToCustomer,
476
+ topPositiveRatings: topPositiveRatings,
477
+ topNegativeRatings: topNegativeRatings,
478
+ };
479
+ }
480
+
481
+ function _accumulate(bucket, val) {
482
+ bucket.count += 1;
483
+ bucket.sum += val;
484
+ bucket.distribution[String(val)] = (bucket.distribution[String(val)] || 0) + 1;
485
+ }
486
+
487
+ module.exports = {
488
+ create: create,
489
+ RATING_AXES: RATING_AXES,
490
+ MIN_RATING: MIN_RATING,
491
+ MAX_RATING: MAX_RATING,
492
+ MAX_COMMENT_LEN: MAX_COMMENT_LEN,
493
+ MAX_FLAG_REASON_LEN: MAX_FLAG_REASON_LEN,
494
+ MAX_RESPONSE_LEN: MAX_RESPONSE_LEN,
495
+ };