@blamejs/blamejs-shop 0.0.72 → 0.0.75

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 (44) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/lib/announcement-bar.js +753 -0
  3. package/lib/banner-ab-tests.js +806 -0
  4. package/lib/bin-locations.js +791 -0
  5. package/lib/blog-articles.js +1173 -0
  6. package/lib/carrier-accounts.js +805 -0
  7. package/lib/cart-recovery.js +1133 -0
  8. package/lib/category-navigation.js +934 -0
  9. package/lib/consent-ledger.js +539 -0
  10. package/lib/customer-impersonation.js +743 -0
  11. package/lib/customer-merge.js +879 -0
  12. package/lib/demand-forecast.js +1121 -0
  13. package/lib/dispute-resolution.js +886 -0
  14. package/lib/email-ab-tests.js +918 -0
  15. package/lib/email-engagement-score.js +649 -0
  16. package/lib/event-log.js +713 -0
  17. package/lib/fulfillment-sla.js +791 -0
  18. package/lib/index.js +41 -0
  19. package/lib/inventory-audits.js +852 -0
  20. package/lib/line-gift-wrap.js +430 -0
  21. package/lib/marketing-budget.js +792 -0
  22. package/lib/operator-activity-feed.js +977 -0
  23. package/lib/operator-approvals.js +942 -0
  24. package/lib/operator-help-center.js +1020 -0
  25. package/lib/operator-inbox.js +889 -0
  26. package/lib/operator-sessions.js +701 -0
  27. package/lib/order-exchanges.js +602 -0
  28. package/lib/product-compare.js +804 -0
  29. package/lib/pwa-manifest.js +1005 -0
  30. package/lib/referral-leaderboard.js +612 -0
  31. package/lib/sales-tax-filings.js +807 -0
  32. package/lib/search-ranking.js +859 -0
  33. package/lib/shipping-insurance.js +757 -0
  34. package/lib/shrinkage-report.js +1182 -0
  35. package/lib/sidebar-widgets.js +952 -0
  36. package/lib/smart-restocking.js +1048 -0
  37. package/lib/stock-receipts.js +834 -0
  38. package/lib/subscription-analytics.js +1032 -0
  39. package/lib/suggestion-box.js +921 -0
  40. package/lib/tax-remittance.js +625 -0
  41. package/lib/vendor-invoices.js +1021 -0
  42. package/lib/winback-campaigns.js +1350 -0
  43. package/lib/wishlist-digest.js +1133 -0
  44. package/package.json +1 -1
@@ -0,0 +1,430 @@
1
+ "use strict";
2
+ /**
3
+ * @module shop.lineGiftWrap
4
+ * @title Per-line gift wrap — distinct wrap_sku per order line
5
+ *
6
+ * @intro
7
+ * The sibling `giftOptions` primitive carries one wrap_sku at the
8
+ * ORDER level. That shape collapses when a single order ships gifts
9
+ * to multiple recipients: "the necklace goes to my sister in floral
10
+ * paper; the watch goes to my dad in kraft paper" can't be
11
+ * expressed when there's only one wrap slot for the whole order.
12
+ *
13
+ * This primitive lets each `(order_id, line_id)` carry its own
14
+ * wrap_sku + gift_message + recipient_name. The two primitives are
15
+ * complementary, not competing — `giftOptions` still owns the
16
+ * order-level concerns (hide_prices on the slip, the per-order
17
+ * recipient field that survives when no per-line override exists).
18
+ * Operators who don't need per-line granularity stay on
19
+ * `giftOptions`; operators who do compose both.
20
+ *
21
+ * The wrap catalog itself lives on `giftOptions.defineWrap(...)`
22
+ * — this primitive does NOT duplicate the wrap registry. When a
23
+ * `giftOptions` handle is provided at create time,
24
+ * `feeForOrder({ order_id })` sums every per-line wrap fee by
25
+ * reading `giftOptions.getWrap(wrap_sku).fee_minor`. Absent the
26
+ * handle, `feeForOrder` refuses (the fee data simply isn't
27
+ * reachable without it).
28
+ *
29
+ * Composition:
30
+ *
31
+ * var lgw = bShop.lineGiftWrap.create({
32
+ * query: q,
33
+ * giftOptions: bShop.giftOptions.create({ query: q, catalog: cat }),
34
+ * });
35
+ *
36
+ * await lgw.setLineWrap({
37
+ * order_id: "...",
38
+ * line_id: "...",
39
+ * wrap_sku: "WRAP-FLORAL",
40
+ * gift_message: "Happy birthday, sis!",
41
+ * recipient_name: "Alice",
42
+ * });
43
+ *
44
+ * Surface:
45
+ *
46
+ * - `setLineWrap({ order_id, line_id, wrap_sku, gift_message?,
47
+ * recipient_name? })` — UPSERT against
48
+ * UNIQUE(order_id, line_id).
49
+ * - `getLineWrap({ order_id, line_id })` — hydrated row or null.
50
+ * - `wrapsForOrder({ order_id })` — every per-line wrap on the
51
+ * order in stable line_id order.
52
+ * - `clearLineWrap({ order_id, line_id })` — drop one row.
53
+ * - `feeForOrder({ order_id })` — sum of fee_minor across every
54
+ * per-line wrap on the order. Refuses unless
55
+ * `giftOptions` was wired at create time.
56
+ * - `renderPackingSlipLines({ order_id, locale })` — per-line
57
+ * render data with HTML-escaped strings.
58
+ * - `analytics({ from, to })` — per-wrap-sku usage counts in the
59
+ * window.
60
+ *
61
+ * Storage:
62
+ * - `line_gift_wraps` (migration `0202_line_gift_wrap.sql`).
63
+ *
64
+ * @primitive lineGiftWrap
65
+ * @related shop.giftOptions, b.guardUuid, b.uuid.v7,
66
+ * b.template.escapeHtml
67
+ */
68
+
69
+ // ---- constants ----------------------------------------------------------
70
+
71
+ var MAX_MESSAGE_LEN = 500;
72
+ var MAX_RECIPIENT_LEN = 120;
73
+ var MAX_FROM_TO_SPAN = 366 * 24 * 3600 * 1000; // analytics window cap
74
+
75
+ // SKU shape mirrors catalog.js + gift-options.js — alnum + . _ -, ≤
76
+ // 128 chars, leading char must be alnum so a wrap_sku can never
77
+ // start with a hyphen / dot (sidesteps shell-arg-style ambiguity in
78
+ // downstream CSV exports + the "looks like a flag" class of operator
79
+ // slips).
80
+ var SKU_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
81
+
82
+ // Refuse C0 control bytes + DEL. The gift message + recipient name
83
+ // render onto a packing slip and (potentially) a printer queue;
84
+ // embedded control bytes have caused header-injection-class slips in
85
+ // adjacent ecosystems. Newlines are allowed in gift_message (people
86
+ // write multi-line messages); the recipient name is a single line
87
+ // and refuses LF / CR too.
88
+ var CONTROL_BYTE_MSG_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
89
+ var CONTROL_BYTE_NAME_RE = /[\x00-\x1f\x7f]/;
90
+
91
+ // Zero-width / direction-override family — mirrors the gift-options
92
+ // primitive's catalogue: ZWSP/ZWNJ/ZWJ (U+200B-200D), LRM/RLM
93
+ // (U+200E/U+200F), the bidi-formatting block (U+202A-U+202E), the
94
+ // invisible-math block (U+2060-U+2064), the LRI/RLI/FSI/PDI block
95
+ // (U+2066-U+2069), the BOM (U+FEFF), and the Arabic letter mark
96
+ // (U+061C). Spelled with \u-escapes so ESLint's
97
+ // no-irregular-whitespace stays happy.
98
+ var ZERO_WIDTH_RE = new RegExp(
99
+ "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
100
+ );
101
+
102
+ var BCP47_RE = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
103
+
104
+ var bShop;
105
+ function _b() {
106
+ if (!bShop) bShop = require("./index");
107
+ return bShop.framework;
108
+ }
109
+
110
+ // ---- monotonic clock ---------------------------------------------------
111
+ //
112
+ // Per-line wrap UPSERTs frequently arrive in tight bursts (the
113
+ // storefront's gift-wrap picker fires a setLineWrap per row when the
114
+ // customer hits "save"). Two same-millisecond writes would otherwise
115
+ // share a `set_at` timestamp and a sort-by-set_at read would lose
116
+ // the operator's actual mutation order. Bumping by 1ms on a tie
117
+ // keeps the timeline strictly increasing.
118
+
119
+ var _lastTs = 0;
120
+ function _now() {
121
+ var t = Date.now();
122
+ if (t <= _lastTs) { t = _lastTs + 1; }
123
+ _lastTs = t;
124
+ return t;
125
+ }
126
+
127
+ // ---- validators --------------------------------------------------------
128
+
129
+ function _orderId(s) {
130
+ try {
131
+ return _b().guardUuid.sanitize(s, { profile: "strict" });
132
+ } catch (e) {
133
+ throw new TypeError("lineGiftWrap: order_id — " + (e && e.message || "invalid UUID"));
134
+ }
135
+ }
136
+
137
+ function _lineId(s) {
138
+ try {
139
+ return _b().guardUuid.sanitize(s, { profile: "strict" });
140
+ } catch (e) {
141
+ throw new TypeError("lineGiftWrap: line_id — " + (e && e.message || "invalid UUID"));
142
+ }
143
+ }
144
+
145
+ function _sku(s) {
146
+ if (typeof s !== "string" || !SKU_RE.test(s)) {
147
+ throw new TypeError("lineGiftWrap: wrap_sku must match /^[A-Za-z0-9][A-Za-z0-9._-]*$/ (alnum + . _ -, 1..128 chars)");
148
+ }
149
+ return s;
150
+ }
151
+
152
+ function _giftMessage(s) {
153
+ if (s == null) return null;
154
+ if (typeof s !== "string") {
155
+ throw new TypeError("lineGiftWrap: gift_message must be a string");
156
+ }
157
+ if (s.length > MAX_MESSAGE_LEN) {
158
+ throw new TypeError("lineGiftWrap: gift_message must be ≤ " + MAX_MESSAGE_LEN + " chars");
159
+ }
160
+ if (CONTROL_BYTE_MSG_RE.test(s)) {
161
+ throw new TypeError("lineGiftWrap: gift_message contains control bytes");
162
+ }
163
+ if (ZERO_WIDTH_RE.test(s)) {
164
+ throw new TypeError("lineGiftWrap: gift_message contains zero-width / direction-override characters");
165
+ }
166
+ return s;
167
+ }
168
+
169
+ function _recipientName(s) {
170
+ if (s == null) return null;
171
+ if (typeof s !== "string") {
172
+ throw new TypeError("lineGiftWrap: recipient_name must be a string");
173
+ }
174
+ if (s.length > MAX_RECIPIENT_LEN) {
175
+ throw new TypeError("lineGiftWrap: recipient_name must be ≤ " + MAX_RECIPIENT_LEN + " chars");
176
+ }
177
+ if (CONTROL_BYTE_NAME_RE.test(s)) {
178
+ throw new TypeError("lineGiftWrap: recipient_name contains control bytes (incl. CR/LF)");
179
+ }
180
+ if (ZERO_WIDTH_RE.test(s)) {
181
+ throw new TypeError("lineGiftWrap: recipient_name contains zero-width / direction-override characters");
182
+ }
183
+ return s;
184
+ }
185
+
186
+ function _epochMs(n, label) {
187
+ if (!Number.isInteger(n) || n < 0) {
188
+ throw new TypeError("lineGiftWrap: " + label + " must be a non-negative integer (epoch ms)");
189
+ }
190
+ return n;
191
+ }
192
+
193
+ function _locale(s) {
194
+ if (typeof s !== "string" || !BCP47_RE.test(s)) {
195
+ throw new TypeError("lineGiftWrap: locale must be a BCP-47-shape string (e.g. 'en-US')");
196
+ }
197
+ return s;
198
+ }
199
+
200
+ function _hydrateRow(r) {
201
+ if (!r) return null;
202
+ return {
203
+ id: r.id,
204
+ order_id: r.order_id,
205
+ line_id: r.line_id,
206
+ wrap_sku: r.wrap_sku,
207
+ gift_message: r.gift_message == null ? null : String(r.gift_message),
208
+ recipient_name: r.recipient_name == null ? null : String(r.recipient_name),
209
+ set_at: Number(r.set_at),
210
+ updated_at: Number(r.updated_at),
211
+ };
212
+ }
213
+
214
+ // ---- factory -----------------------------------------------------------
215
+
216
+ function create(opts) {
217
+ opts = opts || {};
218
+ var query = opts.query;
219
+ if (!query) {
220
+ query = function (sql, params) { return _b().externalDb.query(sql, params); };
221
+ }
222
+
223
+ // giftOptions is optional — when wired, `feeForOrder` sums per-line
224
+ // wrap fees by calling `giftOptions.getWrap(wrap_sku)`. Absent the
225
+ // handle, `feeForOrder` refuses loudly (the fee data simply isn't
226
+ // reachable without the wrap catalog). The factory verifies the
227
+ // shape at boot so a typo in the wiring fails loud, not when
228
+ // `feeForOrder` is first invoked.
229
+ var giftOpts = opts.giftOptions || null;
230
+ if (giftOpts && typeof giftOpts.getWrap !== "function") {
231
+ throw new TypeError("lineGiftWrap.create: opts.giftOptions must expose a getWrap(wrap_sku) method");
232
+ }
233
+
234
+ async function _getRow(orderId, lineId) {
235
+ var r = await query(
236
+ "SELECT * FROM line_gift_wraps WHERE order_id = ?1 AND line_id = ?2 LIMIT 1",
237
+ [orderId, lineId],
238
+ );
239
+ return r.rows.length ? r.rows[0] : null;
240
+ }
241
+
242
+ async function setLineWrap(input) {
243
+ if (!input || typeof input !== "object") {
244
+ throw new TypeError("lineGiftWrap.setLineWrap: input object required");
245
+ }
246
+ var orderId = _orderId(input.order_id);
247
+ var lineId = _lineId(input.line_id);
248
+ var wrapSku = _sku(input.wrap_sku);
249
+ var giftMessage = _giftMessage(input.gift_message);
250
+ var recipientName = _recipientName(input.recipient_name);
251
+
252
+ var ts = _now();
253
+ var existing = await _getRow(orderId, lineId);
254
+ if (existing) {
255
+ // UPSERT against UNIQUE(order_id, line_id). Re-running with
256
+ // different inputs replaces every column (the storefront UI
257
+ // re-submits the full state when the customer edits a line's
258
+ // wrap, so a partial update would silently retain stale
259
+ // gift_message / recipient_name fields).
260
+ await query(
261
+ "UPDATE line_gift_wraps SET wrap_sku = ?1, gift_message = ?2, " +
262
+ "recipient_name = ?3, updated_at = ?4 WHERE order_id = ?5 AND line_id = ?6",
263
+ [wrapSku, giftMessage, recipientName, ts, orderId, lineId],
264
+ );
265
+ } else {
266
+ await query(
267
+ "INSERT INTO line_gift_wraps (id, order_id, line_id, wrap_sku, " +
268
+ "gift_message, recipient_name, set_at, updated_at) " +
269
+ "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?7)",
270
+ [_b().uuid.v7(), orderId, lineId, wrapSku, giftMessage, recipientName, ts],
271
+ );
272
+ }
273
+ return _hydrateRow(await _getRow(orderId, lineId));
274
+ }
275
+
276
+ async function getLineWrap(input) {
277
+ if (!input || typeof input !== "object") {
278
+ throw new TypeError("lineGiftWrap.getLineWrap: input object required");
279
+ }
280
+ var orderId = _orderId(input.order_id);
281
+ var lineId = _lineId(input.line_id);
282
+ return _hydrateRow(await _getRow(orderId, lineId));
283
+ }
284
+
285
+ async function wrapsForOrder(input) {
286
+ if (!input || typeof input !== "object") {
287
+ throw new TypeError("lineGiftWrap.wrapsForOrder: input object required");
288
+ }
289
+ var orderId = _orderId(input.order_id);
290
+ var rows = (await query(
291
+ "SELECT * FROM line_gift_wraps WHERE order_id = ?1 ORDER BY line_id ASC",
292
+ [orderId],
293
+ )).rows;
294
+ return rows.map(_hydrateRow);
295
+ }
296
+
297
+ async function clearLineWrap(input) {
298
+ if (!input || typeof input !== "object") {
299
+ throw new TypeError("lineGiftWrap.clearLineWrap: input object required");
300
+ }
301
+ var orderId = _orderId(input.order_id);
302
+ var lineId = _lineId(input.line_id);
303
+ var r = await query(
304
+ "DELETE FROM line_gift_wraps WHERE order_id = ?1 AND line_id = ?2",
305
+ [orderId, lineId],
306
+ );
307
+ return { cleared: Number(r.rowCount || 0) > 0 };
308
+ }
309
+
310
+ async function feeForOrder(input) {
311
+ if (!input || typeof input !== "object") {
312
+ throw new TypeError("lineGiftWrap.feeForOrder: input object required");
313
+ }
314
+ if (!giftOpts) {
315
+ throw new TypeError("lineGiftWrap.feeForOrder: opts.giftOptions must be wired (the wrap catalog + fee_minor live on the giftOptions primitive)");
316
+ }
317
+ var orderId = _orderId(input.order_id);
318
+ var rows = (await query(
319
+ "SELECT wrap_sku FROM line_gift_wraps WHERE order_id = ?1",
320
+ [orderId],
321
+ )).rows;
322
+ var total = 0;
323
+ for (var i = 0; i < rows.length; i += 1) {
324
+ var wrap = await giftOpts.getWrap(rows[i].wrap_sku);
325
+ if (wrap && Number.isFinite(Number(wrap.fee_minor))) {
326
+ total += Number(wrap.fee_minor);
327
+ }
328
+ }
329
+ return total;
330
+ }
331
+
332
+ async function renderPackingSlipLines(input) {
333
+ if (!input || typeof input !== "object") {
334
+ throw new TypeError("lineGiftWrap.renderPackingSlipLines: input object required");
335
+ }
336
+ var orderId = _orderId(input.order_id);
337
+ var locale = _locale(input.locale);
338
+
339
+ var rows = (await query(
340
+ "SELECT * FROM line_gift_wraps WHERE order_id = ?1 ORDER BY line_id ASC",
341
+ [orderId],
342
+ )).rows;
343
+
344
+ var escapeHtml = _b().template.escapeHtml;
345
+
346
+ // Per-line render shape:
347
+ // { line_id, wrap_sku, message_lines: [<html-escaped>],
348
+ // recipient_name: <html-escaped|null>, locale }
349
+ // The wrap_sku passes through verbatim (it's already shape-
350
+ // constrained at write time), but every customer-authored string
351
+ // (gift_message, recipient_name) is HTML-escaped before reaching
352
+ // the slip template. Multi-line gift_message values split on LF
353
+ // (handling rare CRLF) and trailing empty lines are dropped so
354
+ // the slip doesn't grow a stray blank row when the customer
355
+ // typed an extra newline.
356
+ return rows.map(function (r) {
357
+ var messageLines = [];
358
+ if (r.gift_message) {
359
+ var raw = String(r.gift_message).replace(/\r\n/g, "\n").split("\n");
360
+ while (raw.length && raw[raw.length - 1] === "") raw.pop();
361
+ messageLines = raw.map(function (line) { return escapeHtml(line); });
362
+ }
363
+ return {
364
+ line_id: r.line_id,
365
+ wrap_sku: r.wrap_sku,
366
+ message_lines: messageLines,
367
+ recipient_name: r.recipient_name ? escapeHtml(String(r.recipient_name)) : null,
368
+ locale: locale,
369
+ };
370
+ });
371
+ }
372
+
373
+ async function analytics(input) {
374
+ if (!input || typeof input !== "object") {
375
+ throw new TypeError("lineGiftWrap.analytics: input object required");
376
+ }
377
+ var from = _epochMs(input.from, "from");
378
+ var to = _epochMs(input.to, "to");
379
+ if (to < from) {
380
+ throw new TypeError("lineGiftWrap.analytics: to must be >= from");
381
+ }
382
+ if (to - from > MAX_FROM_TO_SPAN) {
383
+ throw new TypeError("lineGiftWrap.analytics: window must be ≤ 366d (the operator dashboard pages by year)");
384
+ }
385
+
386
+ // Top wrap_skus by usage count. Capped at 50 because the operator
387
+ // dashboard doesn't need an unbounded list; the wrap catalog
388
+ // itself is rarely more than a handful of SKUs.
389
+ var rows = (await query(
390
+ "SELECT wrap_sku, COUNT(*) AS n FROM line_gift_wraps " +
391
+ "WHERE set_at >= ?1 AND set_at < ?2 " +
392
+ "GROUP BY wrap_sku ORDER BY n DESC, wrap_sku ASC LIMIT 50",
393
+ [from, to],
394
+ )).rows;
395
+
396
+ var totalRow = (await query(
397
+ "SELECT COUNT(*) AS n FROM line_gift_wraps WHERE set_at >= ?1 AND set_at < ?2",
398
+ [from, to],
399
+ )).rows[0];
400
+ var totalLines = Number((totalRow || {}).n || 0);
401
+
402
+ return {
403
+ from: from,
404
+ to: to,
405
+ total_lines: totalLines,
406
+ by_wrap_sku: rows.map(function (r) {
407
+ return { wrap_sku: r.wrap_sku, count: Number(r.n) };
408
+ }),
409
+ };
410
+ }
411
+
412
+ return {
413
+ MAX_MESSAGE_LEN: MAX_MESSAGE_LEN,
414
+ MAX_RECIPIENT_LEN: MAX_RECIPIENT_LEN,
415
+
416
+ setLineWrap: setLineWrap,
417
+ getLineWrap: getLineWrap,
418
+ wrapsForOrder: wrapsForOrder,
419
+ clearLineWrap: clearLineWrap,
420
+ feeForOrder: feeForOrder,
421
+ renderPackingSlipLines: renderPackingSlipLines,
422
+ analytics: analytics,
423
+ };
424
+ }
425
+
426
+ module.exports = {
427
+ create: create,
428
+ MAX_MESSAGE_LEN: MAX_MESSAGE_LEN,
429
+ MAX_RECIPIENT_LEN: MAX_RECIPIENT_LEN,
430
+ };