@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.
@@ -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, "&amp;")
374
+ .replace(/</g, "&lt;")
375
+ .replace(/>/g, "&gt;")
376
+ .replace(/"/g, "&quot;")
377
+ .replace(/'/g, "&#39;");
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
+ };