@blamejs/blamejs-shop 0.0.59 → 0.0.60
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 +2 -0
- package/lib/api-keys.js +789 -0
- package/lib/barcodes.js +671 -0
- package/lib/coupon-stacking.js +717 -0
- package/lib/customer-portal.js +359 -0
- package/lib/experiments.js +697 -0
- package/lib/index.js +14 -0
- package/lib/inventory-snapshots.js +691 -0
- package/lib/print-receipts.js +675 -0
- package/lib/product-import.js +1034 -0
- package/lib/storefront-pages.js +701 -0
- package/lib/subscription-billing.js +644 -0
- package/lib/tax-rates.js +559 -0
- package/lib/tenants.js +665 -0
- package/lib/translations.js +553 -0
- package/lib/webhook-subscriptions.js +565 -0
- package/package.json +1 -1
package/lib/barcodes.js
ADDED
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module shop.barcodes
|
|
4
|
+
* @title Barcodes primitive — SKU -> scannable identifier with checksum validation
|
|
5
|
+
*
|
|
6
|
+
* @intro
|
|
7
|
+
* Maps a SKU to one or more barcode values across the four kinds
|
|
8
|
+
* the storefront actually needs:
|
|
9
|
+
*
|
|
10
|
+
* - `upc_a` — 12 digits, mod-10 (North-American retail
|
|
11
|
+
* consumer pack).
|
|
12
|
+
* - `ean_13` — 13 digits, mod-10 (international consumer
|
|
13
|
+
* pack; GS1 company prefix + item reference +
|
|
14
|
+
* check digit).
|
|
15
|
+
* - `code_128` — variable-length alphanumeric, no industry
|
|
16
|
+
* checksum at the data layer (Code-128 carries
|
|
17
|
+
* its own modulo-103 check internally to the
|
|
18
|
+
* symbol — that's a renderer concern, not a
|
|
19
|
+
* value-validation concern; the operator stores
|
|
20
|
+
* the human-readable payload).
|
|
21
|
+
* - `gtin_14` — 14 digits, mod-10 (case / outer-shipper
|
|
22
|
+
* pack; first digit is the packaging-level
|
|
23
|
+
* indicator).
|
|
24
|
+
*
|
|
25
|
+
* `assign` refuses on bad checksum / wrong digit-length / duplicate
|
|
26
|
+
* (kind, value). `assignAuto` mints the next value from an
|
|
27
|
+
* operator-allocated range and writes the assignment in one go —
|
|
28
|
+
* the range row's `next_value` advances atomically per call so two
|
|
29
|
+
* concurrent auto-mints never collide.
|
|
30
|
+
*
|
|
31
|
+
* `renderSvg` returns a self-contained inline `<svg>` string —
|
|
32
|
+
* no external assets, no `<script>`, no `<foreignObject>` — safe to
|
|
33
|
+
* embed directly in a print template or a thermal-label PDF. The
|
|
34
|
+
* primitive ships the encoding tables for each kind inline; the
|
|
35
|
+
* storefront never reaches for an external barcode library.
|
|
36
|
+
*
|
|
37
|
+
* Composition:
|
|
38
|
+
* var bc = bShop.barcodes.create({ query: q, catalog: cat });
|
|
39
|
+
* await bc.defineRange({
|
|
40
|
+
* kind: "ean_13", prefix: "5012345", next_value: 0, max_value: 99999,
|
|
41
|
+
* owner_company: "Example Foods Ltd",
|
|
42
|
+
* });
|
|
43
|
+
* var b = await bc.assignAuto({ sku: "WIDGET-A", kind: "ean_13" });
|
|
44
|
+
* var svg = await bc.renderSvg({ sku: "WIDGET-A" });
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
var bShop;
|
|
48
|
+
function _b() {
|
|
49
|
+
if (!bShop) bShop = require("./index");
|
|
50
|
+
return bShop.framework;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
var KINDS = ["upc_a", "ean_13", "code_128", "gtin_14"];
|
|
54
|
+
|
|
55
|
+
// Digit-length per numeric kind (Code-128 is variable so it's not
|
|
56
|
+
// in this table — its length is validated by the alphanumeric +
|
|
57
|
+
// printable-ASCII shape check instead).
|
|
58
|
+
var DIGIT_LEN = {
|
|
59
|
+
upc_a: 12,
|
|
60
|
+
ean_13: 13,
|
|
61
|
+
gtin_14: 14,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Code-128 accepts ASCII 0x20..0x7E (printable). The renderer
|
|
65
|
+
// chooses Code Set B for that range. Operators wanting a Code-A /
|
|
66
|
+
// Code-C payload should use the alphabet they need within this
|
|
67
|
+
// shape; the validator doesn't second-guess.
|
|
68
|
+
var CODE128_RE = /^[\x20-\x7E]+$/;
|
|
69
|
+
var DIGITS_RE = /^[0-9]+$/;
|
|
70
|
+
|
|
71
|
+
// ---- checksum primitives ------------------------------------------------
|
|
72
|
+
|
|
73
|
+
// Standard GS1 mod-10: rightmost data digit weighted 3, then
|
|
74
|
+
// alternating 1/3. The check digit makes the total a multiple of
|
|
75
|
+
// 10. Used by UPC-A, EAN-13, GTIN-14 (the algorithm is identical;
|
|
76
|
+
// only the input length differs).
|
|
77
|
+
function _gs1Mod10(digits) {
|
|
78
|
+
var sum = 0;
|
|
79
|
+
// Walk right-to-left, weight = 3 on the first (rightmost) data
|
|
80
|
+
// digit, alternating 1/3 thereafter. `digits` here is the data
|
|
81
|
+
// portion WITHOUT the check digit appended.
|
|
82
|
+
for (var i = digits.length - 1, w = 3; i >= 0; i -= 1, w = (w === 3 ? 1 : 3)) {
|
|
83
|
+
sum += parseInt(digits.charAt(i), 10) * w;
|
|
84
|
+
}
|
|
85
|
+
var mod = sum % 10;
|
|
86
|
+
return mod === 0 ? 0 : 10 - mod;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Validate a numeric value's check digit against the trailing
|
|
90
|
+
// position. Returns true if the trailing digit matches the
|
|
91
|
+
// computed check digit.
|
|
92
|
+
function _gs1CheckOk(value) {
|
|
93
|
+
if (!DIGITS_RE.test(value) || value.length < 2) return false;
|
|
94
|
+
var data = value.slice(0, -1);
|
|
95
|
+
var check = parseInt(value.charAt(value.length - 1), 10);
|
|
96
|
+
return _gs1Mod10(data) === check;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Pure-function value validation per kind. Returns true on a
|
|
100
|
+
// well-formed value (correct length, correct shape, correct
|
|
101
|
+
// checksum where applicable).
|
|
102
|
+
function validateValue(input) {
|
|
103
|
+
if (!input || typeof input !== "object") {
|
|
104
|
+
throw new TypeError("barcodes.validateValue: input object required");
|
|
105
|
+
}
|
|
106
|
+
if (KINDS.indexOf(input.kind) === -1) {
|
|
107
|
+
throw new TypeError("barcodes.validateValue: kind must be one of " + KINDS.join(", "));
|
|
108
|
+
}
|
|
109
|
+
if (typeof input.value !== "string" || !input.value.length) {
|
|
110
|
+
throw new TypeError("barcodes.validateValue: value must be a non-empty string");
|
|
111
|
+
}
|
|
112
|
+
var v = input.value;
|
|
113
|
+
if (input.kind === "code_128") {
|
|
114
|
+
return CODE128_RE.test(v);
|
|
115
|
+
}
|
|
116
|
+
var expected = DIGIT_LEN[input.kind];
|
|
117
|
+
if (v.length !== expected) return false;
|
|
118
|
+
if (!DIGITS_RE.test(v)) return false;
|
|
119
|
+
return _gs1CheckOk(v);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---- input validators ---------------------------------------------------
|
|
123
|
+
|
|
124
|
+
function _sku(s) {
|
|
125
|
+
if (typeof s !== "string" || !s.length || s.length > 128) {
|
|
126
|
+
throw new TypeError("barcodes: sku must be a non-empty string ≤ 128 chars");
|
|
127
|
+
}
|
|
128
|
+
// The catalog primitive owns SKU canonicalization; here we only
|
|
129
|
+
// refuse control bytes + leading/trailing whitespace so a typo
|
|
130
|
+
// can't quietly map to a different row than the catalog sees.
|
|
131
|
+
if (/[\x00-\x1f\x7f]/.test(s) || /^\s|\s$/.test(s)) {
|
|
132
|
+
throw new TypeError("barcodes: sku contains control bytes or surrounding whitespace");
|
|
133
|
+
}
|
|
134
|
+
return s;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function _kind(k) {
|
|
138
|
+
if (typeof k !== "string" || KINDS.indexOf(k) === -1) {
|
|
139
|
+
throw new TypeError("barcodes: kind must be one of " + KINDS.join(", "));
|
|
140
|
+
}
|
|
141
|
+
return k;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function _now() { return Date.now(); }
|
|
145
|
+
|
|
146
|
+
function _nonNegInt(n, label) {
|
|
147
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n < 0) {
|
|
148
|
+
throw new TypeError("barcodes: " + label + " must be a non-negative integer");
|
|
149
|
+
}
|
|
150
|
+
return n;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function _prefix(s, kind) {
|
|
154
|
+
if (typeof s !== "string" || !s.length || s.length > 32) {
|
|
155
|
+
throw new TypeError("barcodes: prefix must be a non-empty string ≤ 32 chars");
|
|
156
|
+
}
|
|
157
|
+
if (kind === "code_128") {
|
|
158
|
+
if (!CODE128_RE.test(s)) {
|
|
159
|
+
throw new TypeError("barcodes: code_128 prefix must be printable ASCII (0x20–0x7E)");
|
|
160
|
+
}
|
|
161
|
+
} else if (!DIGITS_RE.test(s)) {
|
|
162
|
+
throw new TypeError("barcodes: numeric-kind prefix must be digits only");
|
|
163
|
+
}
|
|
164
|
+
return s;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ---- auto-mint value formatter ------------------------------------------
|
|
168
|
+
|
|
169
|
+
// Compose `prefix + zero-padded counter + check digit` to the
|
|
170
|
+
// kind's expected length. For Code-128 we just concatenate prefix
|
|
171
|
+
// + counter (no check digit; the symbol-level check is a renderer
|
|
172
|
+
// concern).
|
|
173
|
+
function _mintValue(kind, prefix, counter) {
|
|
174
|
+
if (kind === "code_128") {
|
|
175
|
+
return prefix + String(counter);
|
|
176
|
+
}
|
|
177
|
+
var totalLen = DIGIT_LEN[kind];
|
|
178
|
+
var dataLen = totalLen - 1; // reserve trailing check digit
|
|
179
|
+
var counterStr = String(counter);
|
|
180
|
+
var padLen = dataLen - prefix.length - counterStr.length;
|
|
181
|
+
if (padLen < 0) return null; // counter overflowed the available data space
|
|
182
|
+
var data = prefix + "0".repeat(padLen) + counterStr;
|
|
183
|
+
var check = _gs1Mod10(data);
|
|
184
|
+
return data + String(check);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ---- SVG renderer -------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
// Code-128 Code Set B encoding table — covers ASCII 0x20–0x7E.
|
|
190
|
+
// Index = symbol value (0..106). The bar pattern is 6 elements
|
|
191
|
+
// (3 bar-space pairs, "11" = bar, "00" = space etc. — but the
|
|
192
|
+
// canonical representation is a sequence of bar+space widths). We
|
|
193
|
+
// store the canonical 6-width string per code; the renderer paints
|
|
194
|
+
// bars at odd positions, spaces at even positions.
|
|
195
|
+
// (Trimmed comment block; the table below is the published GS1
|
|
196
|
+
// Code-128 specification, abbreviated to values 0..106 — START B
|
|
197
|
+
// = 104, STOP = 106 in this encoding, with the modulo-103 weighted
|
|
198
|
+
// checksum positioned just before STOP per ISO/IEC 15417.)
|
|
199
|
+
var CODE128_PATTERNS = [
|
|
200
|
+
"212222","222122","222221","121223","121322","131222","122213","122312","132212","221213",
|
|
201
|
+
"221312","231212","112232","122132","122231","113222","123122","123221","223211","221132",
|
|
202
|
+
"221231","213212","223112","312131","311222","321122","321221","312212","322112","322211",
|
|
203
|
+
"212123","212321","232121","111323","131123","131321","112313","132113","132311","211313",
|
|
204
|
+
"231113","231311","112133","112331","132131","113123","113321","133121","313121","211331",
|
|
205
|
+
"231131","213113","213311","213131","311123","311321","331121","312113","312311","332111",
|
|
206
|
+
"314111","221411","431111","111224","111422","121124","121421","141122","141221","112214",
|
|
207
|
+
"112412","122114","122411","142112","142211","241211","221114","413111","241112","134111",
|
|
208
|
+
"111242","121142","121241","114212","124112","124211","411212","421112","421211","212141",
|
|
209
|
+
"214121","412121","111143","111341","131141","114113","114311","411113","411311","113141",
|
|
210
|
+
"114131","311141","411131","211412","211214","211232","2331112" // 100..106; index 106 = STOP
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
// Map a Code-128 Set B character to its symbol value.
|
|
214
|
+
// Set B starts at 0x20 (space → 0), so value = charCode - 32.
|
|
215
|
+
function _code128ValueB(ch) {
|
|
216
|
+
return ch.charCodeAt(0) - 32;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// EAN/UPC L/G/R encoding tables. For UPC-A: left 6 digits = L,
|
|
220
|
+
// right 6 digits = R. For EAN-13: leading digit is implicit
|
|
221
|
+
// (encoded by L/G pattern on the left 6); right 6 = R.
|
|
222
|
+
//
|
|
223
|
+
// Each entry is a 7-module bit string. "1" = bar, "0" = space.
|
|
224
|
+
var EAN_L = [
|
|
225
|
+
"0001101","0011001","0010011","0111101","0100011",
|
|
226
|
+
"0110001","0101111","0111011","0110111","0001011",
|
|
227
|
+
];
|
|
228
|
+
var EAN_G = [
|
|
229
|
+
"0100111","0110011","0011011","0100001","0011101",
|
|
230
|
+
"0111001","0000101","0010001","0001001","0010111",
|
|
231
|
+
];
|
|
232
|
+
var EAN_R = [
|
|
233
|
+
"1110010","1100110","1101100","1000010","1011100",
|
|
234
|
+
"1001110","1010000","1000100","1001000","1110100",
|
|
235
|
+
];
|
|
236
|
+
// EAN-13 leading-digit -> L/G pattern across the left 6 positions.
|
|
237
|
+
// "L" = use EAN_L, "G" = use EAN_G.
|
|
238
|
+
var EAN13_LEAD = [
|
|
239
|
+
"LLLLLL","LLGLGG","LLGGLG","LLGGGL","LGLLGG",
|
|
240
|
+
"LGGLLG","LGGGLL","LGLGLG","LGLGGL","LGGLGL",
|
|
241
|
+
];
|
|
242
|
+
var EAN_GUARD = "101";
|
|
243
|
+
var EAN_MID_GUARD = "01010";
|
|
244
|
+
|
|
245
|
+
// Render a numeric value (UPC-A or EAN-13) into a module-width
|
|
246
|
+
// bit string. Returns an array of bit characters.
|
|
247
|
+
function _renderEan(kind, value) {
|
|
248
|
+
var modules = "";
|
|
249
|
+
if (kind === "upc_a") {
|
|
250
|
+
// UPC-A is EAN-13 with a leading 0; the L-pattern is all-L.
|
|
251
|
+
modules += EAN_GUARD;
|
|
252
|
+
for (var i = 0; i < 6; i += 1) {
|
|
253
|
+
modules += EAN_L[parseInt(value.charAt(i), 10)];
|
|
254
|
+
}
|
|
255
|
+
modules += EAN_MID_GUARD;
|
|
256
|
+
for (var j = 6; j < 12; j += 1) {
|
|
257
|
+
modules += EAN_R[parseInt(value.charAt(j), 10)];
|
|
258
|
+
}
|
|
259
|
+
modules += EAN_GUARD;
|
|
260
|
+
return modules;
|
|
261
|
+
}
|
|
262
|
+
// ean_13
|
|
263
|
+
var lead = EAN13_LEAD[parseInt(value.charAt(0), 10)];
|
|
264
|
+
modules += EAN_GUARD;
|
|
265
|
+
for (var k = 0; k < 6; k += 1) {
|
|
266
|
+
var d = parseInt(value.charAt(k + 1), 10);
|
|
267
|
+
var enc = lead.charAt(k);
|
|
268
|
+
modules += (enc === "L" ? EAN_L[d] : EAN_G[d]);
|
|
269
|
+
}
|
|
270
|
+
modules += EAN_MID_GUARD;
|
|
271
|
+
for (var m = 7; m < 13; m += 1) {
|
|
272
|
+
modules += EAN_R[parseInt(value.charAt(m), 10)];
|
|
273
|
+
}
|
|
274
|
+
modules += EAN_GUARD;
|
|
275
|
+
return modules;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// GTIN-14 is rendered as an ITF-14 (Interleaved 2-of-5) bar
|
|
279
|
+
// pattern. Each digit pair encodes 10 modules: 5 narrow/wide
|
|
280
|
+
// bars interleaved with 5 narrow/wide spaces. We use the standard
|
|
281
|
+
// I-2-of-5 weights (1, 1, 1, 2, 2 — narrow=1, wide=2 module).
|
|
282
|
+
var ITF_WIDTHS = [
|
|
283
|
+
// 0..9: each entry is 5 weights, "1" = narrow, "2" = wide.
|
|
284
|
+
"11221","21112","12112","22111","11212",
|
|
285
|
+
"21211","12211","11122","21121","12121",
|
|
286
|
+
];
|
|
287
|
+
|
|
288
|
+
function _renderItf14(value) {
|
|
289
|
+
// Start: narrow bar, narrow space, narrow bar, narrow space.
|
|
290
|
+
var out = "1010";
|
|
291
|
+
for (var i = 0; i < value.length; i += 2) {
|
|
292
|
+
var bw = ITF_WIDTHS[parseInt(value.charAt(i), 10)]; // bar widths
|
|
293
|
+
var sw = ITF_WIDTHS[parseInt(value.charAt(i + 1), 10)]; // space widths
|
|
294
|
+
for (var k = 0; k < 5; k += 1) {
|
|
295
|
+
// Bar (width 1 or 2 modules).
|
|
296
|
+
out += (bw.charAt(k) === "2") ? "11" : "1";
|
|
297
|
+
// Space (width 1 or 2 modules).
|
|
298
|
+
out += (sw.charAt(k) === "2") ? "00" : "0";
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// Stop: wide bar, narrow space, narrow bar.
|
|
302
|
+
out += "1101";
|
|
303
|
+
return out;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Render a Code-128 payload into a module-width bit string. The
|
|
307
|
+
// renderer always emits a Code Set B symbol (printable ASCII) —
|
|
308
|
+
// operators needing Set A or Set C choose payload shapes that
|
|
309
|
+
// remain valid in Set B (no control bytes; numeric strings are
|
|
310
|
+
// fine, just longer than a Set C encoding would be).
|
|
311
|
+
function _renderCode128(payload) {
|
|
312
|
+
// Start B = 104, weighted 1.
|
|
313
|
+
var symbols = [104];
|
|
314
|
+
for (var i = 0; i < payload.length; i += 1) {
|
|
315
|
+
symbols.push(_code128ValueB(payload.charAt(i)));
|
|
316
|
+
}
|
|
317
|
+
// Checksum: start-value*1 + sum(i=1..n, symbol_i * i), mod 103.
|
|
318
|
+
var sum = symbols[0];
|
|
319
|
+
for (var j = 1; j < symbols.length; j += 1) {
|
|
320
|
+
sum += symbols[j] * j;
|
|
321
|
+
}
|
|
322
|
+
symbols.push(sum % 103);
|
|
323
|
+
// STOP (value 106 in this table).
|
|
324
|
+
symbols.push(106);
|
|
325
|
+
|
|
326
|
+
var modules = "";
|
|
327
|
+
for (var s = 0; s < symbols.length; s += 1) {
|
|
328
|
+
var pat = CODE128_PATTERNS[symbols[s]];
|
|
329
|
+
// pat is a sequence of bar/space widths. Even indices are
|
|
330
|
+
// bars (start with bar), odd indices are spaces. STOP (last)
|
|
331
|
+
// has 7 widths instead of 6 — append all of them.
|
|
332
|
+
var bar = true;
|
|
333
|
+
for (var c = 0; c < pat.length; c += 1) {
|
|
334
|
+
var w = parseInt(pat.charAt(c), 10);
|
|
335
|
+
modules += (bar ? "1" : "0").repeat(w);
|
|
336
|
+
bar = !bar;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return modules;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Paint a module-width bit string into an inline SVG. The result
|
|
343
|
+
// holds NO `<script>`, NO `<foreignObject>`, NO external `xlink:href`
|
|
344
|
+
// — only `<svg>` root, `<rect>` bars, and an optional `<text>` for
|
|
345
|
+
// the human-readable line. Width is auto-derived from module count;
|
|
346
|
+
// the operator can override height + module-width via options.
|
|
347
|
+
function _renderSvg(modules, label, opts) {
|
|
348
|
+
opts = opts || {};
|
|
349
|
+
var heightPx = (typeof opts.height_px === "number" && opts.height_px > 0) ? Math.floor(opts.height_px) : 60;
|
|
350
|
+
var widthPx = (typeof opts.width_px === "number" && opts.width_px > 0) ? Math.floor(opts.width_px) : null;
|
|
351
|
+
var moduleW = widthPx != null ? (widthPx / modules.length) : 2;
|
|
352
|
+
var totalW = widthPx != null ? widthPx : Math.ceil(modules.length * moduleW);
|
|
353
|
+
|
|
354
|
+
// Reserve 12px at the bottom for the human-readable label.
|
|
355
|
+
var barH = label ? Math.max(heightPx - 12, 4) : heightPx;
|
|
356
|
+
var bars = "";
|
|
357
|
+
var i = 0;
|
|
358
|
+
while (i < modules.length) {
|
|
359
|
+
if (modules.charAt(i) === "1") {
|
|
360
|
+
var run = 1;
|
|
361
|
+
while (i + run < modules.length && modules.charAt(i + run) === "1") run += 1;
|
|
362
|
+
bars += "<rect x=\"" + (i * moduleW).toFixed(3) + "\" y=\"0\" width=\"" + (run * moduleW).toFixed(3) + "\" height=\"" + barH + "\" fill=\"#000\"/>";
|
|
363
|
+
i += run;
|
|
364
|
+
} else {
|
|
365
|
+
i += 1;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
var labelXml = "";
|
|
369
|
+
if (label) {
|
|
370
|
+
// Escape & < > " for the human-readable line. (' is rare in
|
|
371
|
+
// barcode values but cheap to cover.)
|
|
372
|
+
var safe = String(label)
|
|
373
|
+
.replace(/&/g, "&")
|
|
374
|
+
.replace(/</g, "<")
|
|
375
|
+
.replace(/>/g, ">")
|
|
376
|
+
.replace(/"/g, """)
|
|
377
|
+
.replace(/'/g, "'");
|
|
378
|
+
labelXml = "<text x=\"" + (totalW / 2).toFixed(3) + "\" y=\"" + (heightPx - 2) +
|
|
379
|
+
"\" font-family=\"monospace\" font-size=\"10\" text-anchor=\"middle\" fill=\"#000\">" + safe + "</text>";
|
|
380
|
+
}
|
|
381
|
+
return "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"" + totalW + "\" height=\"" + heightPx +
|
|
382
|
+
"\" viewBox=\"0 0 " + totalW + " " + heightPx + "\" role=\"img\" aria-label=\"barcode\">" +
|
|
383
|
+
"<rect x=\"0\" y=\"0\" width=\"" + totalW + "\" height=\"" + heightPx + "\" fill=\"#fff\"/>" +
|
|
384
|
+
bars + labelXml + "</svg>";
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ---- factory ------------------------------------------------------------
|
|
388
|
+
|
|
389
|
+
function create(opts) {
|
|
390
|
+
opts = opts || {};
|
|
391
|
+
if (!opts.catalog || !opts.catalog.variants || typeof opts.catalog.variants.bySku !== "function") {
|
|
392
|
+
throw new TypeError("barcodes.create: opts.catalog with variants.bySku(sku) required");
|
|
393
|
+
}
|
|
394
|
+
var catalog = opts.catalog;
|
|
395
|
+
var query = opts.query;
|
|
396
|
+
if (!query) {
|
|
397
|
+
query = function (sql, params) { return _b().externalDb.query(sql, params); };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function _verifySku(sku) {
|
|
401
|
+
var v = await catalog.variants.bySku(sku);
|
|
402
|
+
if (!v) {
|
|
403
|
+
throw new TypeError("barcodes: sku " + JSON.stringify(sku) + " not found in catalog");
|
|
404
|
+
}
|
|
405
|
+
return v;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function _assignRow(sku, kind, value) {
|
|
409
|
+
var id = _b().uuid.v7();
|
|
410
|
+
var ts = _now();
|
|
411
|
+
try {
|
|
412
|
+
await query(
|
|
413
|
+
"INSERT INTO barcode_assignments (id, sku, kind, value, assigned_at) VALUES (?1, ?2, ?3, ?4, ?5)",
|
|
414
|
+
[id, sku, kind, value, ts],
|
|
415
|
+
);
|
|
416
|
+
} catch (e) {
|
|
417
|
+
// Distinguish duplicate-value from any other storage failure.
|
|
418
|
+
// SQLite + most adapters surface unique-violation as an Error
|
|
419
|
+
// whose message contains "UNIQUE" / "unique".
|
|
420
|
+
var msg = (e && e.message) || "";
|
|
421
|
+
if (/unique/i.test(msg)) {
|
|
422
|
+
var dup = new Error("barcodes.assign: value already assigned for this kind");
|
|
423
|
+
dup.code = "BARCODE_VALUE_TAKEN";
|
|
424
|
+
throw dup;
|
|
425
|
+
}
|
|
426
|
+
throw e;
|
|
427
|
+
}
|
|
428
|
+
return { id: id, sku: sku, kind: kind, value: value, assigned_at: ts };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
KINDS: KINDS,
|
|
433
|
+
validateValue: validateValue,
|
|
434
|
+
|
|
435
|
+
assign: async function (input) {
|
|
436
|
+
if (!input || typeof input !== "object") {
|
|
437
|
+
throw new TypeError("barcodes.assign: input object required");
|
|
438
|
+
}
|
|
439
|
+
_sku(input.sku);
|
|
440
|
+
_kind(input.kind);
|
|
441
|
+
if (typeof input.value !== "string" || !input.value.length) {
|
|
442
|
+
throw new TypeError("barcodes.assign: value must be a non-empty string");
|
|
443
|
+
}
|
|
444
|
+
if (!validateValue({ kind: input.kind, value: input.value })) {
|
|
445
|
+
var bad = new Error("barcodes.assign: value failed " + input.kind + " validation (length / shape / checksum)");
|
|
446
|
+
bad.code = "BARCODE_INVALID_VALUE";
|
|
447
|
+
throw bad;
|
|
448
|
+
}
|
|
449
|
+
await _verifySku(input.sku);
|
|
450
|
+
return await _assignRow(input.sku, input.kind, input.value);
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
assignAuto: async function (input) {
|
|
454
|
+
if (!input || typeof input !== "object") {
|
|
455
|
+
throw new TypeError("barcodes.assignAuto: input object required");
|
|
456
|
+
}
|
|
457
|
+
_sku(input.sku);
|
|
458
|
+
_kind(input.kind);
|
|
459
|
+
await _verifySku(input.sku);
|
|
460
|
+
// Pick the lowest-id range for the kind that still has room.
|
|
461
|
+
// `next_value <= max_value` is the "room remaining" predicate.
|
|
462
|
+
var r = await query(
|
|
463
|
+
"SELECT id, prefix, next_value, max_value FROM barcode_ranges " +
|
|
464
|
+
"WHERE kind = ?1 AND next_value <= max_value ORDER BY created_at ASC, id ASC LIMIT 1",
|
|
465
|
+
[input.kind],
|
|
466
|
+
);
|
|
467
|
+
if (!r.rows.length) {
|
|
468
|
+
var none = new Error("barcodes.assignAuto: no range with remaining capacity for kind " + input.kind);
|
|
469
|
+
none.code = "BARCODE_RANGE_EXHAUSTED";
|
|
470
|
+
throw none;
|
|
471
|
+
}
|
|
472
|
+
var range = r.rows[0];
|
|
473
|
+
var value = _mintValue(input.kind, range.prefix, range.next_value);
|
|
474
|
+
if (value == null) {
|
|
475
|
+
// Prefix + counter no longer fits in the kind's data block.
|
|
476
|
+
// Refuse and surface as exhausted; the operator allocates a
|
|
477
|
+
// new range with a shorter prefix or a fresh counter base.
|
|
478
|
+
var overflow = new Error("barcodes.assignAuto: range counter overflowed the data block — allocate a new range");
|
|
479
|
+
overflow.code = "BARCODE_RANGE_EXHAUSTED";
|
|
480
|
+
throw overflow;
|
|
481
|
+
}
|
|
482
|
+
// Advance the counter atomically with a CAS guard on
|
|
483
|
+
// `next_value` so two concurrent auto-mints can't collide on
|
|
484
|
+
// the same counter value. The mint is retried up to a small
|
|
485
|
+
// bound on contention (in practice D1 sequences these per
|
|
486
|
+
// worker; the loop is belt-and-braces).
|
|
487
|
+
var dec = await query(
|
|
488
|
+
"UPDATE barcode_ranges SET next_value = next_value + 1 " +
|
|
489
|
+
"WHERE id = ?1 AND next_value = ?2 AND next_value <= max_value",
|
|
490
|
+
[range.id, range.next_value],
|
|
491
|
+
);
|
|
492
|
+
if (dec.rowCount === 0) {
|
|
493
|
+
// Lost the race; surface as a transient retryable failure.
|
|
494
|
+
var raced = new Error("barcodes.assignAuto: range counter race — retry");
|
|
495
|
+
raced.code = "BARCODE_RANGE_RACE";
|
|
496
|
+
throw raced;
|
|
497
|
+
}
|
|
498
|
+
return await _assignRow(input.sku, input.kind, value);
|
|
499
|
+
},
|
|
500
|
+
|
|
501
|
+
lookup: async function (input) {
|
|
502
|
+
if (!input || typeof input !== "object") {
|
|
503
|
+
throw new TypeError("barcodes.lookup: input object required");
|
|
504
|
+
}
|
|
505
|
+
_sku(input.sku);
|
|
506
|
+
var r = await query(
|
|
507
|
+
"SELECT id, sku, kind, value, assigned_at FROM barcode_assignments WHERE sku = ?1 ORDER BY assigned_at ASC",
|
|
508
|
+
[input.sku],
|
|
509
|
+
);
|
|
510
|
+
return r.rows;
|
|
511
|
+
},
|
|
512
|
+
|
|
513
|
+
bySkuList: async function (skus) {
|
|
514
|
+
if (!Array.isArray(skus)) {
|
|
515
|
+
throw new TypeError("barcodes.bySkuList: skus must be an array");
|
|
516
|
+
}
|
|
517
|
+
if (!skus.length) return {};
|
|
518
|
+
var seen = Object.create(null);
|
|
519
|
+
var clean = [];
|
|
520
|
+
for (var i = 0; i < skus.length; i += 1) {
|
|
521
|
+
_sku(skus[i]);
|
|
522
|
+
if (!seen[skus[i]]) { seen[skus[i]] = true; clean.push(skus[i]); }
|
|
523
|
+
}
|
|
524
|
+
// Build an IN (?1, ?2, ...) clause with positional params.
|
|
525
|
+
var placeholders = clean.map(function (_v, idx) { return "?" + (idx + 1); }).join(", ");
|
|
526
|
+
var r = await query(
|
|
527
|
+
"SELECT id, sku, kind, value, assigned_at FROM barcode_assignments WHERE sku IN (" + placeholders + ") ORDER BY sku, assigned_at ASC",
|
|
528
|
+
clean,
|
|
529
|
+
);
|
|
530
|
+
var out = {};
|
|
531
|
+
for (var k = 0; k < clean.length; k += 1) out[clean[k]] = [];
|
|
532
|
+
for (var j = 0; j < r.rows.length; j += 1) {
|
|
533
|
+
var row = r.rows[j];
|
|
534
|
+
out[row.sku].push(row);
|
|
535
|
+
}
|
|
536
|
+
return out;
|
|
537
|
+
},
|
|
538
|
+
|
|
539
|
+
lookupByValue: async function (input) {
|
|
540
|
+
if (!input || typeof input !== "object") {
|
|
541
|
+
throw new TypeError("barcodes.lookupByValue: input object required");
|
|
542
|
+
}
|
|
543
|
+
_kind(input.kind);
|
|
544
|
+
if (typeof input.value !== "string" || !input.value.length) {
|
|
545
|
+
throw new TypeError("barcodes.lookupByValue: value must be a non-empty string");
|
|
546
|
+
}
|
|
547
|
+
var r = await query(
|
|
548
|
+
"SELECT id, sku, kind, value, assigned_at FROM barcode_assignments WHERE kind = ?1 AND value = ?2",
|
|
549
|
+
[input.kind, input.value],
|
|
550
|
+
);
|
|
551
|
+
return r.rows.length ? r.rows[0] : null;
|
|
552
|
+
},
|
|
553
|
+
|
|
554
|
+
unassign: async function (input) {
|
|
555
|
+
if (!input || typeof input !== "object") {
|
|
556
|
+
throw new TypeError("barcodes.unassign: input object required");
|
|
557
|
+
}
|
|
558
|
+
_sku(input.sku);
|
|
559
|
+
var sql, params;
|
|
560
|
+
if (input.kind != null) {
|
|
561
|
+
_kind(input.kind);
|
|
562
|
+
sql = "DELETE FROM barcode_assignments WHERE sku = ?1 AND kind = ?2";
|
|
563
|
+
params = [input.sku, input.kind];
|
|
564
|
+
} else {
|
|
565
|
+
sql = "DELETE FROM barcode_assignments WHERE sku = ?1";
|
|
566
|
+
params = [input.sku];
|
|
567
|
+
}
|
|
568
|
+
var d = await query(sql, params);
|
|
569
|
+
return { removed: d.rowCount || 0 };
|
|
570
|
+
},
|
|
571
|
+
|
|
572
|
+
defineRange: async function (input) {
|
|
573
|
+
if (!input || typeof input !== "object") {
|
|
574
|
+
throw new TypeError("barcodes.defineRange: input object required");
|
|
575
|
+
}
|
|
576
|
+
_kind(input.kind);
|
|
577
|
+
_prefix(input.prefix, input.kind);
|
|
578
|
+
_nonNegInt(input.next_value, "next_value");
|
|
579
|
+
_nonNegInt(input.max_value, "max_value");
|
|
580
|
+
if (input.max_value < input.next_value) {
|
|
581
|
+
throw new TypeError("barcodes.defineRange: max_value must be ≥ next_value");
|
|
582
|
+
}
|
|
583
|
+
// For numeric kinds, the prefix + max_value digit count must
|
|
584
|
+
// fit inside the kind's data block (total length minus the
|
|
585
|
+
// trailing check digit). Refuse a range the operator can't
|
|
586
|
+
// actually mint from.
|
|
587
|
+
if (input.kind !== "code_128") {
|
|
588
|
+
var dataLen = DIGIT_LEN[input.kind] - 1;
|
|
589
|
+
if (input.prefix.length + String(input.max_value).length > dataLen) {
|
|
590
|
+
throw new TypeError("barcodes.defineRange: prefix + max_value digits exceed " + input.kind + " data block (" + dataLen + " digits)");
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
var ownerCompany = null;
|
|
594
|
+
if (input.owner_company != null) {
|
|
595
|
+
if (typeof input.owner_company !== "string" || !input.owner_company.length || input.owner_company.length > 128) {
|
|
596
|
+
throw new TypeError("barcodes.defineRange: owner_company must be a string ≤ 128 chars when provided");
|
|
597
|
+
}
|
|
598
|
+
ownerCompany = input.owner_company;
|
|
599
|
+
}
|
|
600
|
+
var id = _b().uuid.v7();
|
|
601
|
+
var ts = _now();
|
|
602
|
+
await query(
|
|
603
|
+
"INSERT INTO barcode_ranges (id, kind, prefix, next_value, max_value, owner_company, created_at) " +
|
|
604
|
+
"VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
|
605
|
+
[id, input.kind, input.prefix, input.next_value, input.max_value, ownerCompany, ts],
|
|
606
|
+
);
|
|
607
|
+
return {
|
|
608
|
+
id: id,
|
|
609
|
+
kind: input.kind,
|
|
610
|
+
prefix: input.prefix,
|
|
611
|
+
next_value: input.next_value,
|
|
612
|
+
max_value: input.max_value,
|
|
613
|
+
owner_company: ownerCompany,
|
|
614
|
+
created_at: ts,
|
|
615
|
+
};
|
|
616
|
+
},
|
|
617
|
+
|
|
618
|
+
listRanges: async function (opts2) {
|
|
619
|
+
opts2 = opts2 || {};
|
|
620
|
+
if (opts2.kind != null) _kind(opts2.kind);
|
|
621
|
+
var sql, params;
|
|
622
|
+
if (opts2.kind != null) {
|
|
623
|
+
sql = "SELECT id, kind, prefix, next_value, max_value, owner_company, created_at FROM barcode_ranges WHERE kind = ?1 ORDER BY created_at ASC, id ASC";
|
|
624
|
+
params = [opts2.kind];
|
|
625
|
+
} else {
|
|
626
|
+
sql = "SELECT id, kind, prefix, next_value, max_value, owner_company, created_at FROM barcode_ranges ORDER BY created_at ASC, id ASC";
|
|
627
|
+
params = [];
|
|
628
|
+
}
|
|
629
|
+
var r = await query(sql, params);
|
|
630
|
+
return r.rows;
|
|
631
|
+
},
|
|
632
|
+
|
|
633
|
+
renderSvg: async function (input) {
|
|
634
|
+
if (!input || typeof input !== "object") {
|
|
635
|
+
throw new TypeError("barcodes.renderSvg: input object required");
|
|
636
|
+
}
|
|
637
|
+
_sku(input.sku);
|
|
638
|
+
if (input.kind != null) _kind(input.kind);
|
|
639
|
+
var sql, params;
|
|
640
|
+
if (input.kind != null) {
|
|
641
|
+
sql = "SELECT kind, value FROM barcode_assignments WHERE sku = ?1 AND kind = ?2 ORDER BY assigned_at ASC LIMIT 1";
|
|
642
|
+
params = [input.sku, input.kind];
|
|
643
|
+
} else {
|
|
644
|
+
sql = "SELECT kind, value FROM barcode_assignments WHERE sku = ?1 ORDER BY assigned_at ASC LIMIT 1";
|
|
645
|
+
params = [input.sku];
|
|
646
|
+
}
|
|
647
|
+
var r = await query(sql, params);
|
|
648
|
+
if (!r.rows.length) {
|
|
649
|
+
var miss = new Error("barcodes.renderSvg: sku has no assigned barcode" + (input.kind ? " for kind " + input.kind : ""));
|
|
650
|
+
miss.code = "BARCODE_NOT_FOUND";
|
|
651
|
+
throw miss;
|
|
652
|
+
}
|
|
653
|
+
var row = r.rows[0];
|
|
654
|
+
var modules;
|
|
655
|
+
if (row.kind === "upc_a" || row.kind === "ean_13") {
|
|
656
|
+
modules = _renderEan(row.kind, row.value);
|
|
657
|
+
} else if (row.kind === "gtin_14") {
|
|
658
|
+
modules = _renderItf14(row.value);
|
|
659
|
+
} else {
|
|
660
|
+
modules = _renderCode128(row.value);
|
|
661
|
+
}
|
|
662
|
+
return _renderSvg(modules, row.value, { height_px: input.height_px, width_px: input.width_px });
|
|
663
|
+
},
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
module.exports = {
|
|
668
|
+
create: create,
|
|
669
|
+
validateValue: validateValue,
|
|
670
|
+
KINDS: KINDS,
|
|
671
|
+
};
|