@blamejs/blamejs-shop 0.0.72 → 0.0.76
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/CHANGELOG.md +8 -0
- package/lib/announcement-bar.js +753 -0
- package/lib/banner-ab-tests.js +806 -0
- package/lib/bin-locations.js +791 -0
- package/lib/blog-articles.js +1173 -0
- package/lib/carrier-accounts.js +805 -0
- package/lib/cart-recovery.js +1133 -0
- package/lib/category-navigation.js +934 -0
- package/lib/consent-ledger.js +539 -0
- package/lib/customer-impersonation.js +743 -0
- package/lib/customer-merge.js +879 -0
- package/lib/demand-forecast.js +1121 -0
- package/lib/dispute-resolution.js +886 -0
- package/lib/email-ab-tests.js +918 -0
- package/lib/email-engagement-score.js +649 -0
- package/lib/event-log.js +713 -0
- package/lib/fulfillment-sla.js +791 -0
- package/lib/index.js +41 -0
- package/lib/inventory-audits.js +852 -0
- package/lib/line-gift-wrap.js +430 -0
- package/lib/marketing-budget.js +792 -0
- package/lib/operator-activity-feed.js +977 -0
- package/lib/operator-approvals.js +942 -0
- package/lib/operator-help-center.js +1020 -0
- package/lib/operator-inbox.js +889 -0
- package/lib/operator-sessions.js +701 -0
- package/lib/order-exchanges.js +602 -0
- package/lib/product-compare.js +804 -0
- package/lib/pwa-manifest.js +1005 -0
- package/lib/referral-leaderboard.js +612 -0
- package/lib/sales-tax-filings.js +807 -0
- package/lib/search-ranking.js +859 -0
- package/lib/shipping-insurance.js +757 -0
- package/lib/shrinkage-report.js +1182 -0
- package/lib/sidebar-widgets.js +952 -0
- package/lib/smart-restocking.js +1048 -0
- package/lib/stock-receipts.js +834 -0
- package/lib/subscription-analytics.js +1032 -0
- package/lib/suggestion-box.js +921 -0
- package/lib/tax-remittance.js +625 -0
- package/lib/vendor-invoices.js +1021 -0
- package/lib/winback-campaigns.js +1350 -0
- package/lib/wishlist-digest.js +1133 -0
- 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
|
+
};
|