@blamejs/blamejs-shop 0.0.66 → 0.0.72

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 (46) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/lib/assembly-instructions.js +777 -0
  3. package/lib/auto-replenish.js +933 -0
  4. package/lib/click-and-collect.js +711 -0
  5. package/lib/clickstream.js +713 -0
  6. package/lib/customer-activity.js +862 -0
  7. package/lib/customer-notes.js +712 -0
  8. package/lib/customer-risk-profile.js +593 -0
  9. package/lib/customer-surveys.js +1012 -0
  10. package/lib/damage-photos.js +473 -0
  11. package/lib/dropship-forwarding.js +645 -0
  12. package/lib/email-templates.js +817 -0
  13. package/lib/index.js +36 -0
  14. package/lib/inventory-allocations.js +559 -0
  15. package/lib/inventory-writeoffs.js +636 -0
  16. package/lib/knowledge-base.js +1104 -0
  17. package/lib/locale-router.js +1077 -0
  18. package/lib/loyalty-earn-rules.js +786 -0
  19. package/lib/operator-roles.js +768 -0
  20. package/lib/order-escalation.js +951 -0
  21. package/lib/order-ratings.js +495 -0
  22. package/lib/order-tags.js +944 -0
  23. package/lib/packing-slips.js +810 -0
  24. package/lib/pixel-events.js +995 -0
  25. package/lib/print-queue.js +681 -0
  26. package/lib/product-qa.js +749 -0
  27. package/lib/promo-bundles.js +835 -0
  28. package/lib/push-notifications.js +937 -0
  29. package/lib/refund-automation.js +853 -0
  30. package/lib/reorder-reminders.js +798 -0
  31. package/lib/robots-config.js +753 -0
  32. package/lib/seller-signup.js +1052 -0
  33. package/lib/sitemap-generator.js +717 -0
  34. package/lib/split-shipments.js +7 -1
  35. package/lib/subscription-gifts.js +710 -0
  36. package/lib/tax-cert-renewals.js +632 -0
  37. package/lib/tier-benefits.js +776 -0
  38. package/lib/vendor/MANIFEST.json +2 -2
  39. package/lib/vendor/blamejs/CHANGELOG.md +2 -0
  40. package/lib/vendor/blamejs/api-snapshot.json +2 -2
  41. package/lib/vendor/blamejs/lib/metrics.js +68 -4
  42. package/lib/vendor/blamejs/package.json +1 -1
  43. package/lib/vendor/blamejs/release-notes/v0.12.5.json +40 -0
  44. package/lib/wishlist-alerts.js +842 -0
  45. package/lib/wishlist-sharing.js +718 -0
  46. package/package.json +1 -1
@@ -0,0 +1,810 @@
1
+ "use strict";
2
+ /**
3
+ * @module shop.packingSlips
4
+ * @title Packing slips — warehouse pick-verification paperwork
5
+ *
6
+ * @intro
7
+ * The third post-sale paper artifact, distinct from the other two:
8
+ *
9
+ * printReceipts — customer-facing receipt (thermal counter
10
+ * slip, email body, A4 PDF receipt). Lives
11
+ * outside the box.
12
+ * invoiceRenderer — formal accounting invoice with a
13
+ * sequential number + tax breakdown + payment
14
+ * terms. Travels separately to the buyer's
15
+ * AP department.
16
+ * packingSlips — warehouse pick-verification slip. Ships
17
+ * INSIDE the box. Lists order contents, qty
18
+ * per line, an optional gift message +
19
+ * recipient name, and a Code-128 barcode of
20
+ * the order_id the picker scans against the
21
+ * workstation before the seal goes on.
22
+ *
23
+ * The three surfaces never share render bytes — the slip never
24
+ * carries the payment intent id, the invoice number, or the
25
+ * customer's full account history. A gift recipient unboxing the
26
+ * parcel reads the slip, not the receipt; an operator hardening
27
+ * gift-order privacy toggles `gift_options.hide_prices` on the
28
+ * order and the renderer strips every monetary column from the
29
+ * slip output.
30
+ *
31
+ * Surface:
32
+ *
33
+ * renderHtml({ order_id, locale? })
34
+ * — returns the packing-slip HTML string (self-contained,
35
+ * inlined CSS, no external assets) suitable for direct
36
+ * printing or PDF conversion downstream. Locale defaults to
37
+ * "en"; the catalog ships en / es / de inline with the
38
+ * standard fallback chain (region falls back to primary
39
+ * subtag, unknown locales fall back to en).
40
+ *
41
+ * renderPdfPayload({ order_id, locale?, paper_size? })
42
+ * — same HTML body as `renderHtml` but wrapped in a `@page`
43
+ * rule sized for the requested paper. `paper_size` is one of
44
+ * "letter" (8.5x11in / US warehouse default) or "a4"
45
+ * (210x297mm / EU + APAC default); defaults to "letter".
46
+ * Operators with bespoke paper (4x6 thermal labels, A5) wrap
47
+ * this primitive with their own renderer — the two common
48
+ * sizes ship here.
49
+ *
50
+ * recordPrint({ order_id, printer_name, sha3_512, byte_size })
51
+ * — appends one row to `packing_slip_prints` recording who
52
+ * printed which slip at which physical printer. The caller
53
+ * passes pre-computed `sha3_512` + `byte_size` of the bytes
54
+ * actually handed to the printer (not the renderer output)
55
+ * so an operator reconciling a "did the right slip print?"
56
+ * dispute can hash the captured spool and compare against
57
+ * this row. `printer_name` is required (unlike the receipt
58
+ * primitive's nullable column) — a slip print without a
59
+ * named printer is a smoke-test artifact, not a real
60
+ * warehouse event.
61
+ *
62
+ * printsForOrder(order_id)
63
+ * — reads the audit log newest-first. Returns
64
+ * `[{ id, order_id, printer_name, sha3_512, byte_size,
65
+ * occurred_at }]`.
66
+ *
67
+ * enqueueForLocation({ order_id, location_code })
68
+ * — registers an order as awaiting print at a named
69
+ * fulfillment location. The pick-list-complete handler is
70
+ * the natural caller; the row idempotency guard (PRIMARY
71
+ * KEY on `(order_id, location_code)`) means a double-call
72
+ * refuses rather than duplicating the slip in the next
73
+ * batch.
74
+ *
75
+ * dequeueForLocation({ order_id, location_code })
76
+ * — removes the queue entry after the warehouse workstation
77
+ * confirms the slip printed. Returns
78
+ * `{ dequeued: boolean }`.
79
+ *
80
+ * bulkRenderForLocation({ location_code, limit? })
81
+ * — returns up to `limit` (default 50, max 500) oldest-queued
82
+ * `{ order_id, html }` pairs for the named location. The
83
+ * queue rows are NOT auto-dequeued — the operator's
84
+ * workstation calls `dequeueForLocation` after the printer
85
+ * confirms each slip so a transient printer failure
86
+ * re-batches the same slips on the next call rather than
87
+ * dropping them.
88
+ *
89
+ * Every operator + customer-input field passes through
90
+ * `b.template.escapeHtml` — operator-input fields (SKU strings,
91
+ * address lines) and customer-input fields (ship-to, gift
92
+ * message, recipient name) are equally at risk when the render
93
+ * surface is HTML; the primitive does not distinguish trust
94
+ * levels.
95
+ *
96
+ * @related b.template.escapeHtml, b.crypto.sha3Hash, b.guardUuid, b.uuid.v7
97
+ */
98
+
99
+ var bShop;
100
+ function _b() {
101
+ if (!bShop) bShop = require("./index");
102
+ return bShop.framework;
103
+ }
104
+
105
+ // ---- constants ----------------------------------------------------------
106
+
107
+ var DEFAULT_PAPER_SIZE = "letter";
108
+ var VALID_PAPER_SIZES = { "letter": true, "a4": true };
109
+
110
+ var DEFAULT_BULK_LIMIT = 50;
111
+ var MAX_BULK_LIMIT = 500;
112
+
113
+ // BCP-47 shape: 2-3 alpha primary subtag, optional region/script
114
+ // subtags. Mirrors the receipt + invoice surface so the three
115
+ // primitives agree on locale shape.
116
+ var BCP47_RE = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
117
+
118
+ // Location code shape mirrors the inventory-locations primitive's
119
+ // own CHECK constraint (1..64 chars, free-form text the operator
120
+ // assigns to a warehouse / retail / dropship endpoint).
121
+ var LOCATION_CODE_RE = /^[A-Za-z0-9_.-]{1,64}$/;
122
+
123
+ // Zero-decimal currencies (rendered as bare integers). Matches the
124
+ // receipt + invoice catalogs — the three render surfaces agree.
125
+ var ZERO_DECIMAL_CURRENCIES = { "JPY": true, "KRW": true, "VND": true, "CLP": true, "ISK": true };
126
+
127
+ // Per-locale labels for the warehouse-facing slip header / columns.
128
+ // Stable narrow vocabulary; operators wanting a fuller catalog wrap
129
+ // this primitive with their own label map.
130
+ var LOCALE_LABELS = {
131
+ "en": {
132
+ packing_slip: "Packing slip",
133
+ order: "Order",
134
+ placed: "Placed",
135
+ ship_to: "Ship to",
136
+ item: "Item",
137
+ sku: "SKU",
138
+ qty: "Qty",
139
+ price: "Price",
140
+ line_total: "Total",
141
+ subtotal: "Subtotal",
142
+ discount: "Discount",
143
+ tax: "Tax",
144
+ shipping: "Shipping",
145
+ grand_total: "Grand total",
146
+ gift_message: "Gift message",
147
+ gift_to: "To",
148
+ scan_to_verify: "Scan to verify",
149
+ },
150
+ "es": {
151
+ packing_slip: "Albaran",
152
+ order: "Pedido",
153
+ placed: "Realizado",
154
+ ship_to: "Enviar a",
155
+ item: "Articulo",
156
+ sku: "SKU",
157
+ qty: "Cant",
158
+ price: "Precio",
159
+ line_total: "Total",
160
+ subtotal: "Subtotal",
161
+ discount: "Descuento",
162
+ tax: "Impuesto",
163
+ shipping: "Envio",
164
+ grand_total: "Total general",
165
+ gift_message: "Mensaje de regalo",
166
+ gift_to: "Para",
167
+ scan_to_verify: "Escanear para verificar",
168
+ },
169
+ "de": {
170
+ packing_slip: "Lieferschein",
171
+ order: "Bestellung",
172
+ placed: "Aufgegeben",
173
+ ship_to: "Versand an",
174
+ item: "Artikel",
175
+ sku: "Artikelnr.",
176
+ qty: "Menge",
177
+ price: "Preis",
178
+ line_total: "Summe",
179
+ subtotal: "Zwischensumme",
180
+ discount: "Rabatt",
181
+ tax: "Steuer",
182
+ shipping: "Versand",
183
+ grand_total: "Gesamtbetrag",
184
+ gift_message: "Geschenknachricht",
185
+ gift_to: "An",
186
+ scan_to_verify: "Zur Verifikation scannen",
187
+ },
188
+ };
189
+
190
+ // ---- monotonic clock ---------------------------------------------------
191
+ //
192
+ // Two `recordPrint` / `enqueueForLocation` calls landing in the same
193
+ // millisecond on a fast machine would otherwise share an
194
+ // `occurred_at` / `queued_at` value, ambiguating the audit-log sort
195
+ // (`printsForOrder`) and the FIFO queue read
196
+ // (`bulkRenderForLocation`). Bumping the timestamp by 1ms on a tie
197
+ // keeps the timeline strictly increasing so a sort-by-timestamp
198
+ // read returns the events in the order they were issued.
199
+
200
+ var _lastTs = 0;
201
+ function _now() {
202
+ var t = Date.now();
203
+ if (t <= _lastTs) { t = _lastTs + 1; }
204
+ _lastTs = t;
205
+ return t;
206
+ }
207
+
208
+ // ---- validators --------------------------------------------------------
209
+
210
+ function _uuid(s, label) {
211
+ try { return _b().guardUuid.sanitize(s, { profile: "strict" }); }
212
+ catch (e) { throw new TypeError("packingSlips: " + label + " — " + (e && e.message || "invalid UUID")); }
213
+ }
214
+
215
+ function _locale(s) {
216
+ if (s == null) return "en";
217
+ if (typeof s !== "string" || !BCP47_RE.test(s)) {
218
+ throw new TypeError("packingSlips: locale must be a BCP-47-shape string (e.g. 'en-US')");
219
+ }
220
+ return s;
221
+ }
222
+
223
+ function _paperSize(s) {
224
+ if (s == null) return DEFAULT_PAPER_SIZE;
225
+ if (typeof s !== "string" || !VALID_PAPER_SIZES[s]) {
226
+ throw new TypeError("packingSlips: paper_size must be one of 'letter', 'a4'");
227
+ }
228
+ return s;
229
+ }
230
+
231
+ function _printerName(s) {
232
+ if (typeof s !== "string" || s.length < 1 || s.length > 256) {
233
+ throw new TypeError("packingSlips: printer_name must be a 1..256 char string");
234
+ }
235
+ return s;
236
+ }
237
+
238
+ function _byteSize(n) {
239
+ if (!Number.isInteger(n) || n < 0) {
240
+ throw new TypeError("packingSlips: byte_size must be a non-negative integer");
241
+ }
242
+ return n;
243
+ }
244
+
245
+ function _sha3(s) {
246
+ if (typeof s !== "string" || s.length !== 128 || !/^[0-9a-f]{128}$/.test(s)) {
247
+ throw new TypeError("packingSlips: sha3_512 must be a 128-char lowercase hex string");
248
+ }
249
+ return s;
250
+ }
251
+
252
+ function _locationCode(s) {
253
+ if (typeof s !== "string" || !LOCATION_CODE_RE.test(s)) {
254
+ throw new TypeError("packingSlips: location_code must match /^[A-Za-z0-9_.-]{1,64}$/");
255
+ }
256
+ return s;
257
+ }
258
+
259
+ function _bulkLimit(n) {
260
+ if (n == null) return DEFAULT_BULK_LIMIT;
261
+ if (!Number.isInteger(n) || n < 1 || n > MAX_BULK_LIMIT) {
262
+ throw new TypeError("packingSlips: limit must be an integer 1.." + MAX_BULK_LIMIT);
263
+ }
264
+ return n;
265
+ }
266
+
267
+ function _resolveLocaleLabels(locale) {
268
+ var lc = locale.toLowerCase();
269
+ if (LOCALE_LABELS[lc]) return LOCALE_LABELS[lc];
270
+ var primary = lc.split("-")[0];
271
+ if (LOCALE_LABELS[primary]) return LOCALE_LABELS[primary];
272
+ return LOCALE_LABELS.en;
273
+ }
274
+
275
+ // ---- shared rendering helpers ------------------------------------------
276
+
277
+ function _formatMoney(minor, currency) {
278
+ if (ZERO_DECIMAL_CURRENCIES[currency]) {
279
+ return String(minor) + " " + currency;
280
+ }
281
+ var major = Math.floor(minor / 100);
282
+ var cents = Math.abs(minor % 100);
283
+ var centsStr = cents < 10 ? "0" + cents : String(cents);
284
+ return major + "." + centsStr + " " + currency;
285
+ }
286
+
287
+ function _isoDate(epochMs) {
288
+ // YYYY-MM-DD HH:MM UTC — stable across runners, never resorts to
289
+ // locale-aware Date formatting which would shift across host
290
+ // timezones.
291
+ var d = new Date(epochMs);
292
+ function _pad(n) { return n < 10 ? "0" + n : String(n); }
293
+ return d.getUTCFullYear() + "-" + _pad(d.getUTCMonth() + 1) + "-" + _pad(d.getUTCDate()) +
294
+ " " + _pad(d.getUTCHours()) + ":" + _pad(d.getUTCMinutes()) + " UTC";
295
+ }
296
+
297
+ function _shipToLines(shipTo) {
298
+ if (!shipTo || typeof shipTo !== "object") return [];
299
+ var lines = [];
300
+ if (shipTo.name) lines.push(String(shipTo.name));
301
+ if (shipTo.line1) lines.push(String(shipTo.line1));
302
+ if (shipTo.line2) lines.push(String(shipTo.line2));
303
+ var cityLine = [];
304
+ if (shipTo.city) cityLine.push(String(shipTo.city));
305
+ if (shipTo.region) cityLine.push(String(shipTo.region));
306
+ if (shipTo.postal_code) cityLine.push(String(shipTo.postal_code));
307
+ if (cityLine.length) lines.push(cityLine.join(", "));
308
+ if (shipTo.country) lines.push(String(shipTo.country));
309
+ return lines;
310
+ }
311
+
312
+ // ---- Code-128 barcode renderer -----------------------------------------
313
+ //
314
+ // Self-contained Code-128 Set B renderer. The slip embeds an inline
315
+ // SVG of the order_id so the picker can scan against the workstation
316
+ // to verify the right slip joined the right box. Lives here rather
317
+ // than reaching into `lib/barcodes.js` because the barcode primitive
318
+ // is a SKU registry — assigning + minting barcodes — and the slip
319
+ // renders an ad-hoc code over a UUID that never enters that registry.
320
+ //
321
+ // The SVG is structural-only: no <script>, no <foreignObject>, no
322
+ // xlink:href. Safe to embed directly in an HTML / PDF pipeline.
323
+
324
+ // 0..106 symbol-width patterns. Each pattern is a digit-string of
325
+ // alternating bar/space module widths (bar first). Position 106 is
326
+ // the STOP symbol (7 widths instead of 6).
327
+ var CODE128_PATTERNS = [
328
+ "212222","222122","222221","121223","121322","131222","122213","122312","132212","221213",
329
+ "221312","231212","112232","122132","122231","113222","123122","123221","223211","221132",
330
+ "221231","213212","223112","312131","311222","321122","321221","312212","322112","322211",
331
+ "212123","212321","232121","111323","131123","131321","112313","132113","132311","211313",
332
+ "231113","231311","112133","112331","132131","113123","113321","133121","313121","211331",
333
+ "231131","213113","213311","213131","311123","311321","331121","312113","312311","332111",
334
+ "314111","221411","431111","111224","111422","121124","121421","141122","141221","112214",
335
+ "112412","122114","122411","142112","142211","241211","221114","413111","241112","134111",
336
+ "111242","121142","121241","114212","124112","124211","411212","421112","421211","212141",
337
+ "214121","412121","111143","111341","131141","114113","114311","411113","411311","113141",
338
+ "114131","311141","411131","211412","211214","211232","2331112",
339
+ ];
340
+
341
+ // Code-128 Set B starts at ASCII 0x20 (space → value 0).
342
+ function _code128ValueB(ch) { return ch.charCodeAt(0) - 32; }
343
+
344
+ // Build the module-width bit string for a printable-ASCII payload
345
+ // using Set B. Order UUIDs are 36-char [0-9a-f-] strings — well
346
+ // inside Set B's printable-ASCII envelope.
347
+ function _code128Modules(payload) {
348
+ var symbols = [104]; // Start B
349
+ for (var i = 0; i < payload.length; i += 1) {
350
+ symbols.push(_code128ValueB(payload.charAt(i)));
351
+ }
352
+ var sum = symbols[0];
353
+ for (var j = 1; j < symbols.length; j += 1) {
354
+ sum += symbols[j] * j;
355
+ }
356
+ symbols.push(sum % 103); // Check
357
+ symbols.push(106); // Stop
358
+ var modules = "";
359
+ for (var s = 0; s < symbols.length; s += 1) {
360
+ var pat = CODE128_PATTERNS[symbols[s]];
361
+ var bar = true;
362
+ for (var c = 0; c < pat.length; c += 1) {
363
+ var w = parseInt(pat.charAt(c), 10);
364
+ modules += (bar ? "1" : "0").repeat(w);
365
+ bar = !bar;
366
+ }
367
+ }
368
+ return modules;
369
+ }
370
+
371
+ // Paint the module bit-string into an inline <svg>. Pre-escapes the
372
+ // human-readable label (the order_id is UUID-shape so no XML metacharacters
373
+ // appear, but the escape costs nothing and keeps the renderer composable
374
+ // with future payload shapes).
375
+ function _renderBarcodeSvg(payload) {
376
+ var modules = _code128Modules(payload);
377
+ var moduleW = 2;
378
+ var heightPx = 60;
379
+ var barH = heightPx - 12;
380
+ var totalW = modules.length * moduleW;
381
+ var bars = "";
382
+ var i = 0;
383
+ while (i < modules.length) {
384
+ if (modules.charAt(i) === "1") {
385
+ var run = 1;
386
+ while (i + run < modules.length && modules.charAt(i + run) === "1") run += 1;
387
+ bars += "<rect x=\"" + (i * moduleW).toFixed(3) + "\" y=\"0\" width=\"" +
388
+ (run * moduleW).toFixed(3) + "\" height=\"" + barH + "\" fill=\"#000\"/>";
389
+ i += run;
390
+ } else {
391
+ i += 1;
392
+ }
393
+ }
394
+ var safe = _b().template.escapeHtml(payload);
395
+ var labelXml = "<text x=\"" + (totalW / 2).toFixed(3) + "\" y=\"" + (heightPx - 2) +
396
+ "\" font-family=\"monospace\" font-size=\"10\" text-anchor=\"middle\" fill=\"#000\">" +
397
+ safe + "</text>";
398
+ return "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"" + totalW + "\" height=\"" + heightPx +
399
+ "\" viewBox=\"0 0 " + totalW + " " + heightPx +
400
+ "\" role=\"img\" aria-label=\"barcode\">" +
401
+ "<rect x=\"0\" y=\"0\" width=\"" + totalW + "\" height=\"" + heightPx +
402
+ "\" fill=\"#fff\"/>" + bars + labelXml + "</svg>";
403
+ }
404
+
405
+ // ---- factory ------------------------------------------------------------
406
+
407
+ function create(opts) {
408
+ opts = opts || {};
409
+ var query = opts.query;
410
+ if (!query) {
411
+ query = function (sql, params) { return _b().externalDb.query(sql, params); };
412
+ }
413
+ if (!opts.order || typeof opts.order.get !== "function") {
414
+ throw new TypeError("packingSlips.create: opts.order primitive is required");
415
+ }
416
+ var orderPrim = opts.order;
417
+
418
+ // gift-options is optional — operators without the gift-options
419
+ // primitive wired still get a working slip (no gift message
420
+ // block, no hide-prices toggle). When present, the renderer reads
421
+ // the per-order row via `getForOrder` and applies the toggles.
422
+ var giftOptionsPrim = opts.giftOptions || null;
423
+ if (giftOptionsPrim != null && typeof giftOptionsPrim.getForOrder !== "function") {
424
+ throw new TypeError("packingSlips.create: opts.giftOptions must expose getForOrder when provided");
425
+ }
426
+
427
+ async function _loadOrder(orderId) {
428
+ orderId = _uuid(orderId, "order_id");
429
+ var order = await orderPrim.get(orderId);
430
+ if (!order) {
431
+ throw new TypeError("packingSlips: order " + orderId + " not found");
432
+ }
433
+ return order;
434
+ }
435
+
436
+ async function _loadGiftOptions(orderId) {
437
+ if (!giftOptionsPrim) return null;
438
+ try {
439
+ var row = await giftOptionsPrim.getForOrder(orderId);
440
+ return row || null;
441
+ } catch (_e) {
442
+ // A wrap_sku referenced by the order may have been archived
443
+ // since the order shipped; the slip render should not break
444
+ // on a stale lookup. Treat any read failure as "no gift
445
+ // options" — the slip degrades to the plain shape.
446
+ return null;
447
+ }
448
+ }
449
+
450
+ // ---- render --------------------------------------------------------
451
+
452
+ function _renderHtmlBody(order, gift, locale) {
453
+ var labels = _resolveLocaleLabels(locale);
454
+ var escapeHtml = _b().template.escapeHtml;
455
+ var hidePrices = !!(gift && gift.hide_prices);
456
+
457
+ var shipLinesHtml = _shipToLines(order.ship_to).map(function (line) {
458
+ return "<div>" + escapeHtml(line) + "</div>";
459
+ }).join("");
460
+
461
+ // Gift-message block. Customer-authored prose is split on LF so
462
+ // the slip emits one <div> per line; the gift-options primitive
463
+ // already refuses control bytes + zero-width characters at
464
+ // setForOrder time, but the escape pass here is the
465
+ // defense-in-depth layer in case the row was inserted by an
466
+ // operator-side migration that bypassed the validator.
467
+ var giftBlockHtml = "";
468
+ if (gift) {
469
+ var messageLinesHtml = "";
470
+ if (gift.gift_message) {
471
+ var raw = String(gift.gift_message).replace(/\r\n/g, "\n").split("\n");
472
+ while (raw.length && raw[raw.length - 1] === "") raw.pop();
473
+ messageLinesHtml = raw.map(function (line) {
474
+ return "<div>" + escapeHtml(line) + "</div>";
475
+ }).join("");
476
+ }
477
+ var recipientHtml = "";
478
+ if (gift.recipient_name) {
479
+ recipientHtml = "<div><strong>" + escapeHtml(labels.gift_to) + ":</strong> " +
480
+ escapeHtml(String(gift.recipient_name)) + "</div>";
481
+ }
482
+ if (messageLinesHtml || recipientHtml) {
483
+ giftBlockHtml =
484
+ "<div class=\"gift-block\">\n" +
485
+ "<strong class=\"label\">" + escapeHtml(labels.gift_message) + ":</strong>\n" +
486
+ recipientHtml +
487
+ "<div class=\"gift-message\">" + messageLinesHtml + "</div>\n" +
488
+ "</div>\n";
489
+ }
490
+ }
491
+
492
+ // Line items. `hide_prices` strips the Price + Total columns
493
+ // (the gift-receipt pattern); SKU + Item title + Qty always
494
+ // render so the picker can verify against the pick list.
495
+ var rowsHtml = "";
496
+ var orderLines = order.lines || [];
497
+ for (var i = 0; i < orderLines.length; i += 1) {
498
+ var l = orderLines[i];
499
+ var sku = escapeHtml(l.sku || "");
500
+ var qty = escapeHtml(String(l.qty));
501
+ if (hidePrices) {
502
+ rowsHtml +=
503
+ "<tr>" +
504
+ "<td>" + sku + "</td>" +
505
+ "<td class=\"num\">" + qty + "</td>" +
506
+ "</tr>";
507
+ } else {
508
+ var unit = _formatMoney(Number(l.unit_amount_minor || 0), l.unit_currency || order.currency);
509
+ var lineTotal = _formatMoney(Number(l.line_total_minor || 0), l.unit_currency || order.currency);
510
+ rowsHtml +=
511
+ "<tr>" +
512
+ "<td>" + sku + "</td>" +
513
+ "<td class=\"num\">" + qty + "</td>" +
514
+ "<td class=\"num\">" + escapeHtml(unit) + "</td>" +
515
+ "<td class=\"num\">" + escapeHtml(lineTotal) + "</td>" +
516
+ "</tr>";
517
+ }
518
+ }
519
+
520
+ var headerCols = hidePrices
521
+ ? ("<th>" + escapeHtml(labels.sku) + "</th>" +
522
+ "<th class=\"num\">" + escapeHtml(labels.qty) + "</th>")
523
+ : ("<th>" + escapeHtml(labels.sku) + "</th>" +
524
+ "<th class=\"num\">" + escapeHtml(labels.qty) + "</th>" +
525
+ "<th class=\"num\">" + escapeHtml(labels.price) + "</th>" +
526
+ "<th class=\"num\">" + escapeHtml(labels.line_total) + "</th>");
527
+
528
+ var totalsHtml = "";
529
+ if (!hidePrices) {
530
+ var discountRow = Number(order.discount_minor || 0) > 0
531
+ ? ("<tr><th>" + escapeHtml(labels.discount) + "</th>" +
532
+ "<td class=\"num\">-" + escapeHtml(_formatMoney(Number(order.discount_minor || 0), order.currency)) + "</td></tr>")
533
+ : "";
534
+ totalsHtml =
535
+ "<table class=\"totals\">\n" +
536
+ "<tr><th>" + escapeHtml(labels.subtotal) + "</th>" +
537
+ "<td class=\"num\">" + escapeHtml(_formatMoney(Number(order.subtotal_minor || 0), order.currency)) + "</td></tr>\n" +
538
+ discountRow +
539
+ "<tr><th>" + escapeHtml(labels.tax) + "</th>" +
540
+ "<td class=\"num\">" + escapeHtml(_formatMoney(Number(order.tax_minor || 0), order.currency)) + "</td></tr>\n" +
541
+ "<tr><th>" + escapeHtml(labels.shipping) + "</th>" +
542
+ "<td class=\"num\">" + escapeHtml(_formatMoney(Number(order.shipping_minor || 0), order.currency)) + "</td></tr>\n" +
543
+ "<tr class=\"grand\"><th>" + escapeHtml(labels.grand_total) + "</th>" +
544
+ "<td class=\"num\">" + escapeHtml(_formatMoney(Number(order.grand_total_minor || 0), order.currency)) + "</td></tr>\n" +
545
+ "</table>\n";
546
+ }
547
+
548
+ var barcodeSvg = _renderBarcodeSvg(order.id);
549
+
550
+ return "<h1>" + escapeHtml(labels.packing_slip) + "</h1>\n" +
551
+ "<div class=\"meta\">\n" +
552
+ "<div><strong>" + escapeHtml(labels.order) + ":</strong> " + escapeHtml(order.id) + "</div>\n" +
553
+ "<div><strong>" + escapeHtml(labels.placed) + ":</strong> " + escapeHtml(_isoDate(Number(order.created_at))) + "</div>\n" +
554
+ "</div>\n" +
555
+ "<div class=\"ship-to\">\n" +
556
+ "<strong class=\"label\">" + escapeHtml(labels.ship_to) + ":</strong>\n" +
557
+ shipLinesHtml +
558
+ "</div>\n" +
559
+ giftBlockHtml +
560
+ "<table class=\"items\">\n" +
561
+ "<thead><tr>" + headerCols + "</tr></thead>\n" +
562
+ "<tbody>" + rowsHtml + "</tbody>\n" +
563
+ "</table>\n" +
564
+ totalsHtml +
565
+ "<div class=\"barcode\">\n" +
566
+ "<div class=\"barcode-label\">" + escapeHtml(labels.scan_to_verify) + ":</div>\n" +
567
+ barcodeSvg +
568
+ "</div>\n";
569
+ }
570
+
571
+ function _wrapDocument(bodyHtml, locale, paperSize) {
572
+ var escapeHtml = _b().template.escapeHtml;
573
+ var pageRule = paperSize === "a4"
574
+ ? "@page { size: A4; margin: 18mm; }"
575
+ : "@page { size: letter; margin: 0.75in; }";
576
+ return "<!doctype html>\n" +
577
+ "<html lang=\"" + escapeHtml(locale) + "\">\n" +
578
+ "<head>\n" +
579
+ "<meta charset=\"utf-8\">\n" +
580
+ "<title>" + escapeHtml(_resolveLocaleLabels(locale).packing_slip) + "</title>\n" +
581
+ "<style>\n" +
582
+ pageRule + "\n" +
583
+ "body { font-family: system-ui, sans-serif; color: #111; font-size: 11pt; }\n" +
584
+ "h1 { font-size: 20pt; margin: 0 0 6mm 0; }\n" +
585
+ ".meta { margin-bottom: 6mm; }\n" +
586
+ ".meta div { margin-bottom: 1mm; }\n" +
587
+ ".ship-to { margin-bottom: 6mm; }\n" +
588
+ ".ship-to strong.label { display: block; margin-bottom: 2mm; }\n" +
589
+ ".gift-block { border: 1px dashed #999; padding: 4mm; margin-bottom: 6mm; }\n" +
590
+ ".gift-block strong.label { display: block; margin-bottom: 2mm; }\n" +
591
+ ".gift-message { margin-top: 2mm; font-style: italic; }\n" +
592
+ "table.items { width: 100%; border-collapse: collapse; margin-bottom: 6mm; }\n" +
593
+ "table.items th, table.items td { padding: 2mm 1mm; border-bottom: 1px solid #ddd; text-align: left; }\n" +
594
+ "td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }\n" +
595
+ ".totals { width: auto; margin-left: auto; border-collapse: collapse; margin-bottom: 8mm; }\n" +
596
+ ".totals th { text-align: left; font-weight: normal; padding: 1mm 3mm 1mm 0; }\n" +
597
+ ".totals td { text-align: right; padding: 1mm 0; font-variant-numeric: tabular-nums; }\n" +
598
+ ".totals tr.grand th, .totals tr.grand td { font-weight: bold; border-top: 2px solid #111; }\n" +
599
+ ".barcode { margin-top: 10mm; text-align: center; }\n" +
600
+ ".barcode-label { margin-bottom: 2mm; font-size: 9pt; color: #555; }\n" +
601
+ "</style>\n" +
602
+ "</head>\n" +
603
+ "<body>\n" +
604
+ bodyHtml +
605
+ "</body>\n" +
606
+ "</html>\n";
607
+ }
608
+
609
+ // ---- public surface --------------------------------------------------
610
+
611
+ return {
612
+ VALID_PAPER_SIZES: Object.freeze(Object.keys(VALID_PAPER_SIZES)),
613
+ DEFAULT_PAPER_SIZE: DEFAULT_PAPER_SIZE,
614
+ DEFAULT_BULK_LIMIT: DEFAULT_BULK_LIMIT,
615
+ MAX_BULK_LIMIT: MAX_BULK_LIMIT,
616
+
617
+ renderHtml: async function (input) {
618
+ if (!input || typeof input !== "object") {
619
+ throw new TypeError("packingSlips.renderHtml: input object required");
620
+ }
621
+ var orderId = _uuid(input.order_id, "order_id");
622
+ var locale = _locale(input.locale);
623
+ var order = await _loadOrder(orderId);
624
+ var gift = await _loadGiftOptions(orderId);
625
+ var body = _renderHtmlBody(order, gift, locale);
626
+ // Default to letter paper for the bare `renderHtml` call —
627
+ // `renderPdfPayload` is the explicit-paper variant; callers
628
+ // wanting a4 use that surface.
629
+ return _wrapDocument(body, locale, DEFAULT_PAPER_SIZE);
630
+ },
631
+
632
+ renderPdfPayload: async function (input) {
633
+ if (!input || typeof input !== "object") {
634
+ throw new TypeError("packingSlips.renderPdfPayload: input object required");
635
+ }
636
+ var orderId = _uuid(input.order_id, "order_id");
637
+ var locale = _locale(input.locale);
638
+ var paperSize = _paperSize(input.paper_size);
639
+ var order = await _loadOrder(orderId);
640
+ var gift = await _loadGiftOptions(orderId);
641
+ var body = _renderHtmlBody(order, gift, locale);
642
+ return {
643
+ order_id: orderId,
644
+ paper_size: paperSize,
645
+ locale: locale,
646
+ html: _wrapDocument(body, locale, paperSize),
647
+ };
648
+ },
649
+
650
+ recordPrint: async function (input) {
651
+ if (!input || typeof input !== "object") {
652
+ throw new TypeError("packingSlips.recordPrint: input object required");
653
+ }
654
+ var orderId = _uuid(input.order_id, "order_id");
655
+ var printerName = _printerName(input.printer_name);
656
+ var sha3 = _sha3(input.sha3_512);
657
+ var byteSize = _byteSize(input.byte_size);
658
+
659
+ // Verify the order exists upfront — the FK would catch this
660
+ // at INSERT time too, but the upfront read returns a readable
661
+ // error rather than the engine's opaque constraint message.
662
+ await _loadOrder(orderId);
663
+
664
+ var id = _b().uuid.v7();
665
+ var occurredAt = _now();
666
+ await query(
667
+ "INSERT INTO packing_slip_prints (id, order_id, printer_name, byte_size, " +
668
+ "sha3_512, occurred_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
669
+ [id, orderId, printerName, byteSize, sha3, occurredAt],
670
+ );
671
+ return {
672
+ id: id,
673
+ order_id: orderId,
674
+ printer_name: printerName,
675
+ byte_size: byteSize,
676
+ sha3_512: sha3,
677
+ occurred_at: occurredAt,
678
+ };
679
+ },
680
+
681
+ printsForOrder: async function (orderId) {
682
+ orderId = _uuid(orderId, "order_id");
683
+ var rows = (await query(
684
+ "SELECT id, order_id, printer_name, byte_size, sha3_512, occurred_at " +
685
+ "FROM packing_slip_prints WHERE order_id = ?1 " +
686
+ "ORDER BY occurred_at DESC, id DESC",
687
+ [orderId],
688
+ )).rows;
689
+ return rows.map(function (r) {
690
+ return {
691
+ id: r.id,
692
+ order_id: r.order_id,
693
+ printer_name: r.printer_name,
694
+ byte_size: Number(r.byte_size),
695
+ sha3_512: r.sha3_512,
696
+ occurred_at: Number(r.occurred_at),
697
+ };
698
+ });
699
+ },
700
+
701
+ enqueueForLocation: async function (input) {
702
+ if (!input || typeof input !== "object") {
703
+ throw new TypeError("packingSlips.enqueueForLocation: input object required");
704
+ }
705
+ var orderId = _uuid(input.order_id, "order_id");
706
+ var locationCode = _locationCode(input.location_code);
707
+ await _loadOrder(orderId);
708
+ var queuedAt = _now();
709
+ try {
710
+ await query(
711
+ "INSERT INTO packing_slip_queue (order_id, location_code, queued_at) " +
712
+ "VALUES (?1, ?2, ?3)",
713
+ [orderId, locationCode, queuedAt],
714
+ );
715
+ } catch (e) {
716
+ // Idempotency guard: enqueueing the same (order_id,
717
+ // location_code) twice refuses with a readable message
718
+ // rather than the engine's opaque UNIQUE-violation string.
719
+ if (/UNIQUE|PRIMARY/i.test(String(e && e.message))) {
720
+ throw new TypeError(
721
+ "packingSlips: order " + orderId + " already queued at location " +
722
+ JSON.stringify(locationCode),
723
+ );
724
+ }
725
+ throw e;
726
+ }
727
+ return {
728
+ order_id: orderId,
729
+ location_code: locationCode,
730
+ queued_at: queuedAt,
731
+ };
732
+ },
733
+
734
+ dequeueForLocation: async function (input) {
735
+ if (!input || typeof input !== "object") {
736
+ throw new TypeError("packingSlips.dequeueForLocation: input object required");
737
+ }
738
+ var orderId = _uuid(input.order_id, "order_id");
739
+ var locationCode = _locationCode(input.location_code);
740
+ var r = await query(
741
+ "DELETE FROM packing_slip_queue WHERE order_id = ?1 AND location_code = ?2",
742
+ [orderId, locationCode],
743
+ );
744
+ return { dequeued: Number(r.rowCount || 0) > 0 };
745
+ },
746
+
747
+ bulkRenderForLocation: async function (input) {
748
+ if (!input || typeof input !== "object") {
749
+ throw new TypeError("packingSlips.bulkRenderForLocation: input object required");
750
+ }
751
+ var locationCode = _locationCode(input.location_code);
752
+ var limit = _bulkLimit(input.limit);
753
+
754
+ var rows = (await query(
755
+ "SELECT order_id FROM packing_slip_queue WHERE location_code = ?1 " +
756
+ "ORDER BY queued_at ASC, order_id ASC LIMIT ?2",
757
+ [locationCode, limit],
758
+ )).rows;
759
+
760
+ var locale = _locale(input.locale);
761
+ var out = [];
762
+ for (var i = 0; i < rows.length; i += 1) {
763
+ var oid = rows[i].order_id;
764
+ // Defensive: if the order was deleted after the row was
765
+ // queued, skip rather than throw — the operator's batch
766
+ // shouldn't fail on a single orphan. The queue row stays
767
+ // until dequeueForLocation is called so the operator can
768
+ // reconcile.
769
+ var order;
770
+ try { order = await orderPrim.get(oid); }
771
+ catch (_e) { order = null; }
772
+ if (!order) continue;
773
+ var gift = await _loadGiftOptions(oid);
774
+ var body = _renderHtmlBody(order, gift, locale);
775
+ out.push({
776
+ order_id: oid,
777
+ html: _wrapDocument(body, locale, DEFAULT_PAPER_SIZE),
778
+ });
779
+ }
780
+ return out;
781
+ },
782
+
783
+ run: async function () {
784
+ // No-op lifecycle hook. The primitive has no background
785
+ // workers, retries, or sweepers — every surface is request-
786
+ // driven. The async `run()` export exists so a framework-wide
787
+ // boot orchestrator can iterate every primitive uniformly
788
+ // without a per-primitive special case.
789
+ return { ok: true };
790
+ },
791
+ };
792
+ }
793
+
794
+ // Module-level `run()` — same lifecycle hook signature as the
795
+ // factory's `run()`. Operators wiring the primitive into a startup
796
+ // orchestrator that doesn't yet have a query handle (no DB
797
+ // available pre-migrate) can still call this and get a clean
798
+ // resolution.
799
+ async function run() {
800
+ return { ok: true };
801
+ }
802
+
803
+ module.exports = {
804
+ create: create,
805
+ run: run,
806
+ VALID_PAPER_SIZES: Object.freeze(Object.keys(VALID_PAPER_SIZES)),
807
+ DEFAULT_PAPER_SIZE: DEFAULT_PAPER_SIZE,
808
+ DEFAULT_BULK_LIMIT: DEFAULT_BULK_LIMIT,
809
+ MAX_BULK_LIMIT: MAX_BULK_LIMIT,
810
+ };