@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.
- package/CHANGELOG.md +12 -0
- package/lib/assembly-instructions.js +777 -0
- package/lib/auto-replenish.js +933 -0
- package/lib/click-and-collect.js +711 -0
- package/lib/clickstream.js +713 -0
- package/lib/customer-activity.js +862 -0
- package/lib/customer-notes.js +712 -0
- package/lib/customer-risk-profile.js +593 -0
- package/lib/customer-surveys.js +1012 -0
- package/lib/damage-photos.js +473 -0
- package/lib/dropship-forwarding.js +645 -0
- package/lib/email-templates.js +817 -0
- package/lib/index.js +36 -0
- package/lib/inventory-allocations.js +559 -0
- package/lib/inventory-writeoffs.js +636 -0
- package/lib/knowledge-base.js +1104 -0
- package/lib/locale-router.js +1077 -0
- package/lib/loyalty-earn-rules.js +786 -0
- package/lib/operator-roles.js +768 -0
- package/lib/order-escalation.js +951 -0
- package/lib/order-ratings.js +495 -0
- package/lib/order-tags.js +944 -0
- package/lib/packing-slips.js +810 -0
- package/lib/pixel-events.js +995 -0
- package/lib/print-queue.js +681 -0
- package/lib/product-qa.js +749 -0
- package/lib/promo-bundles.js +835 -0
- package/lib/push-notifications.js +937 -0
- package/lib/refund-automation.js +853 -0
- package/lib/reorder-reminders.js +798 -0
- package/lib/robots-config.js +753 -0
- package/lib/seller-signup.js +1052 -0
- package/lib/sitemap-generator.js +717 -0
- package/lib/split-shipments.js +7 -1
- package/lib/subscription-gifts.js +710 -0
- package/lib/tax-cert-renewals.js +632 -0
- package/lib/tier-benefits.js +776 -0
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +2 -2
- package/lib/vendor/blamejs/lib/metrics.js +68 -4
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.12.5.json +40 -0
- package/lib/wishlist-alerts.js +842 -0
- package/lib/wishlist-sharing.js +718 -0
- 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
|
+
};
|