@blamejs/blamejs-shop 0.0.72 → 0.0.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/lib/announcement-bar.js +753 -0
  3. package/lib/banner-ab-tests.js +806 -0
  4. package/lib/bin-locations.js +791 -0
  5. package/lib/blog-articles.js +1173 -0
  6. package/lib/carrier-accounts.js +805 -0
  7. package/lib/cart-recovery.js +1133 -0
  8. package/lib/category-navigation.js +934 -0
  9. package/lib/consent-ledger.js +539 -0
  10. package/lib/customer-impersonation.js +743 -0
  11. package/lib/customer-merge.js +879 -0
  12. package/lib/demand-forecast.js +1121 -0
  13. package/lib/dispute-resolution.js +886 -0
  14. package/lib/email-ab-tests.js +918 -0
  15. package/lib/email-engagement-score.js +649 -0
  16. package/lib/event-log.js +713 -0
  17. package/lib/fulfillment-sla.js +791 -0
  18. package/lib/index.js +41 -0
  19. package/lib/inventory-audits.js +852 -0
  20. package/lib/line-gift-wrap.js +430 -0
  21. package/lib/marketing-budget.js +792 -0
  22. package/lib/operator-activity-feed.js +977 -0
  23. package/lib/operator-approvals.js +942 -0
  24. package/lib/operator-help-center.js +1020 -0
  25. package/lib/operator-inbox.js +889 -0
  26. package/lib/operator-sessions.js +701 -0
  27. package/lib/order-exchanges.js +602 -0
  28. package/lib/product-compare.js +804 -0
  29. package/lib/pwa-manifest.js +1005 -0
  30. package/lib/referral-leaderboard.js +612 -0
  31. package/lib/sales-tax-filings.js +807 -0
  32. package/lib/search-ranking.js +859 -0
  33. package/lib/shipping-insurance.js +757 -0
  34. package/lib/shrinkage-report.js +1182 -0
  35. package/lib/sidebar-widgets.js +952 -0
  36. package/lib/smart-restocking.js +1048 -0
  37. package/lib/stock-receipts.js +834 -0
  38. package/lib/subscription-analytics.js +1032 -0
  39. package/lib/suggestion-box.js +921 -0
  40. package/lib/tax-remittance.js +625 -0
  41. package/lib/vendor-invoices.js +1021 -0
  42. package/lib/winback-campaigns.js +1350 -0
  43. package/lib/wishlist-digest.js +1133 -0
  44. package/package.json +1 -1
@@ -0,0 +1,791 @@
1
+ "use strict";
2
+ /**
3
+ * @module shop.binLocations
4
+ * @title Bin locations — per-SKU warehouse bin/aisle/shelf placement
5
+ *
6
+ * @intro
7
+ * Multi-location operators need to know where, physically, a given
8
+ * SKU lives inside a warehouse so the picker walks the floor in a
9
+ * minimum-distance pattern instead of zig-zagging between aisles.
10
+ * This primitive owns that addressing layer:
11
+ *
12
+ * - Assign a SKU to a (location, bin, aisle, shelf, level)
13
+ * tuple — and, when the same SKU lives across several bins at
14
+ * the same location, flag one of them as `primary` so the
15
+ * picker has an unambiguous first-look bin.
16
+ * - Translate a flat list of SKUs into an aisle-ordered walk
17
+ * path so a pick list reaches the picker pre-sorted.
18
+ * - Track each bin's physical condition (clean / needs_audit /
19
+ * damaged / unusable) so the warehouse-floor dashboard can
20
+ * surface bins that need a cleaning pass or are unusable until
21
+ * a repair lands.
22
+ * - Record bin-content reconciliations (`recordBinAudit`) with
23
+ * the variance between expected SKUs and the SKUs the auditor
24
+ * actually found — the audit row stays append-only so the
25
+ * operator can prove the reconciliation history when a stock
26
+ * adjustment lands downstream.
27
+ *
28
+ * Composes:
29
+ * - `b.uuid.v7` — assignment / audit row ids
30
+ * (lexicographic + monotonic so ties on assigned_at sort
31
+ * deterministically).
32
+ * - `inventoryLocations` (optional) — when wired, the
33
+ * `location_code` on every assign / unassign / audit / condition
34
+ * call is checked against `inventoryLocations.getLocation(code)`
35
+ * and a missing/inactive location fails the write at the
36
+ * boundary; absent the dep, the primitive accepts every well-
37
+ * shaped code (the operator's downstream tooling is expected to
38
+ * validate the linkage).
39
+ * - `catalog` (optional) — when wired, every SKU on `assignBin`
40
+ * / `bulkAssign` is checked against `catalog.get(sku)` and an
41
+ * unknown SKU fails the write; absent, every well-shaped SKU
42
+ * string passes the boundary.
43
+ *
44
+ * Picker-path discipline:
45
+ * `pickPathSort({ location_code, skus })` returns the input SKU
46
+ * list sorted by (aisle ASC, shelf ASC, level ASC) using the
47
+ * PRIMARY bin assignment at the location. A SKU with no
48
+ * assignment lands at the END of the path under a synthetic
49
+ * `(zzz, zzz, zzz)` sort key — the picker still gets the SKU on
50
+ * the list but knows to handle it specially (find-and-fetch
51
+ * rather than walk-to-bin). The function is stable: duplicate
52
+ * SKUs keep their relative order; SKUs sharing the same
53
+ * coordinates sort lexicographically among themselves.
54
+ *
55
+ * Audit variance:
56
+ * `recordBinAudit({ expected_skus, actual_skus })` writes the
57
+ * variance object directly to the audit row:
58
+ * {
59
+ * missing: [...sku strings that were expected but absent],
60
+ * extra: [...sku strings that were present but not expected],
61
+ * }
62
+ * Both lists are JSON-serialised and sorted lexicographically for
63
+ * deterministic round-trips.
64
+ *
65
+ * Three-tier input validation (use the discipline; don't write
66
+ * the labels in shipped artifacts):
67
+ * - Config-time / boot: factory `create()` THROWS on bad
68
+ * optional-dep shapes (catalog without `get`, inventoryLocations
69
+ * without `getLocation`).
70
+ * - Hot-path read (`binForSku`, `binsForSku`, `skusInBin`,
71
+ * `searchBinsByAisle`, `pickPathSort`, `listBinsWithCondition`):
72
+ * RETURNS DEFAULTS / empty arrays on a missing row, never
73
+ * throws — the picker / operator dashboard tolerate a transient
74
+ * miss while a re-assign is in flight.
75
+ * - Write path (`assignBin`, `unassignBin`, `bulkAssign`,
76
+ * `bulkUnassign`, `recordBinAudit`, `binCondition`): THROWS on
77
+ * bad input. The operator's boot-time wiring catches every
78
+ * typo on the first call.
79
+ *
80
+ * @primitive binLocations
81
+ * @related b.uuid.v7, inventoryLocations, catalog
82
+ */
83
+
84
+ // ---- constants ---------------------------------------------------------
85
+
86
+ var SKU_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
87
+ var LOC_CODE_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
88
+ var BIN_LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
89
+ var AISLE_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,31}$/;
90
+ var SHELF_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,31}$/;
91
+ var LEVEL_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,31}$/;
92
+ var AUDITOR_RE = /^[A-Za-z0-9][A-Za-z0-9._@:-]{0,127}$/;
93
+
94
+ var MAX_LIST_LIMIT = 500;
95
+ var MAX_BULK_ROWS = 1000;
96
+ var MAX_AUDIT_SKUS = 5000;
97
+
98
+ var CONDITIONS = Object.freeze([
99
+ "clean", "needs_audit", "damaged", "unusable",
100
+ ]);
101
+
102
+ // Synthetic sort key for SKUs missing an assignment in pickPathSort —
103
+ // a string that lexicographically follows every shape-valid aisle /
104
+ // shelf / level value. AISLE_RE etc. require an alphanumeric leading
105
+ // byte, so a `zzz` prefix sorts AFTER any real coordinate when the
106
+ // operator has not used an `{` or beyond as a leading byte.
107
+ var NO_ASSIGN_SORT_KEY = "￿";
108
+
109
+ var bShop;
110
+ function _b() {
111
+ if (!bShop) bShop = require("./index");
112
+ return bShop.framework;
113
+ }
114
+
115
+ // ---- monotonic clock ---------------------------------------------------
116
+ //
117
+ // Operator-driven writes can land in the same millisecond on fast
118
+ // machines (bulkAssign loops, immediate assign-then-unassign tests).
119
+ // Bumping by 1ms on a tie keeps assigned_at / occurred_at / updated_at
120
+ // strictly increasing so a sort-by-timestamp read returns the events
121
+ // in the order they were issued.
122
+
123
+ var _lastTs = 0;
124
+ function _now() {
125
+ var t = Date.now();
126
+ if (t <= _lastTs) { t = _lastTs + 1; }
127
+ _lastTs = t;
128
+ return t;
129
+ }
130
+
131
+ // ---- validators --------------------------------------------------------
132
+
133
+ function _sku(s, label) {
134
+ if (typeof s !== "string" || !SKU_RE.test(s)) {
135
+ throw new TypeError("bin-locations: " + (label || "sku") +
136
+ " must match /^[A-Za-z0-9][A-Za-z0-9._-]*$/ (alnum + . _ -, 1..128 chars)");
137
+ }
138
+ return s;
139
+ }
140
+
141
+ function _locCode(s) {
142
+ if (typeof s !== "string" || !LOC_CODE_RE.test(s)) {
143
+ throw new TypeError("bin-locations: location_code must match " +
144
+ "/^[A-Za-z0-9][A-Za-z0-9._-]*$/ (alnum + . _ -, 1..64 chars)");
145
+ }
146
+ return s;
147
+ }
148
+
149
+ function _binLabel(s) {
150
+ if (typeof s !== "string" || !BIN_LABEL_RE.test(s)) {
151
+ throw new TypeError("bin-locations: bin_label must match " +
152
+ "/^[A-Za-z0-9][A-Za-z0-9._-]*$/ (alnum + . _ -, 1..64 chars)");
153
+ }
154
+ return s;
155
+ }
156
+
157
+ function _aisle(s) {
158
+ if (typeof s !== "string" || !AISLE_RE.test(s)) {
159
+ throw new TypeError("bin-locations: aisle must match " +
160
+ "/^[A-Za-z0-9][A-Za-z0-9._-]*$/ (alnum + . _ -, 1..32 chars)");
161
+ }
162
+ return s;
163
+ }
164
+
165
+ function _shelf(s) {
166
+ if (typeof s !== "string" || !SHELF_RE.test(s)) {
167
+ throw new TypeError("bin-locations: shelf must match " +
168
+ "/^[A-Za-z0-9][A-Za-z0-9._-]*$/ (alnum + . _ -, 1..32 chars)");
169
+ }
170
+ return s;
171
+ }
172
+
173
+ function _level(s) {
174
+ if (typeof s !== "string" || !LEVEL_RE.test(s)) {
175
+ throw new TypeError("bin-locations: level must match " +
176
+ "/^[A-Za-z0-9][A-Za-z0-9._-]*$/ (alnum + . _ -, 1..32 chars)");
177
+ }
178
+ return s;
179
+ }
180
+
181
+ function _auditor(s) {
182
+ if (typeof s !== "string" || !AUDITOR_RE.test(s)) {
183
+ throw new TypeError("bin-locations: audited_by must match " +
184
+ "/^[A-Za-z0-9][A-Za-z0-9._@:-]*$/ (alnum + . _ @ : -, 1..128 chars)");
185
+ }
186
+ return s;
187
+ }
188
+
189
+ function _condition(s) {
190
+ if (typeof s !== "string" || CONDITIONS.indexOf(s) === -1) {
191
+ throw new TypeError("bin-locations: condition must be one of " +
192
+ CONDITIONS.join(", ") + ", got " + JSON.stringify(s));
193
+ }
194
+ return s;
195
+ }
196
+
197
+ function _limit(n) {
198
+ if (!Number.isInteger(n) || n <= 0 || n > MAX_LIST_LIMIT) {
199
+ throw new TypeError("bin-locations: limit must be an integer in 1..." + MAX_LIST_LIMIT);
200
+ }
201
+ return n;
202
+ }
203
+
204
+ function _skuListForAudit(v, label) {
205
+ if (!Array.isArray(v)) {
206
+ throw new TypeError("bin-locations: " + label + " must be an array of sku strings");
207
+ }
208
+ if (v.length > MAX_AUDIT_SKUS) {
209
+ throw new TypeError("bin-locations: " + label + " must contain <= " +
210
+ MAX_AUDIT_SKUS + " sku entries");
211
+ }
212
+ for (var i = 0; i < v.length; i += 1) _sku(v[i], label + "[" + i + "]");
213
+ return v;
214
+ }
215
+
216
+ // ---- row hydration ------------------------------------------------------
217
+
218
+ function _hydrateAssignment(r) {
219
+ if (!r) return null;
220
+ return {
221
+ id: r.id,
222
+ sku: r.sku,
223
+ location_code: r.location_code,
224
+ bin_label: r.bin_label,
225
+ aisle: r.aisle,
226
+ shelf: r.shelf,
227
+ level: r.level,
228
+ is_primary: Number(r.is_primary) === 1,
229
+ assigned_at: Number(r.assigned_at),
230
+ archived_at: r.archived_at == null ? null : Number(r.archived_at),
231
+ };
232
+ }
233
+
234
+ function _hydrateAudit(r) {
235
+ if (!r) return null;
236
+ return {
237
+ id: r.id,
238
+ location_code: r.location_code,
239
+ bin_label: r.bin_label,
240
+ audited_by: r.audited_by,
241
+ expected_skus: JSON.parse(r.expected_skus_json),
242
+ actual_skus: JSON.parse(r.actual_skus_json),
243
+ variance: JSON.parse(r.variance_json),
244
+ occurred_at: Number(r.occurred_at),
245
+ };
246
+ }
247
+
248
+ function _hydrateCondition(r) {
249
+ if (!r) return null;
250
+ return {
251
+ location_code: r.location_code,
252
+ bin_label: r.bin_label,
253
+ condition: r.condition,
254
+ updated_at: Number(r.updated_at),
255
+ };
256
+ }
257
+
258
+ // Compute the {missing, extra} variance between expected and actual
259
+ // sku sets. Both lists are sorted lexicographically inside the result
260
+ // so the JSON round-trip is deterministic.
261
+ function _computeVariance(expected, actual) {
262
+ var expSet = Object.create(null);
263
+ var actSet = Object.create(null);
264
+ for (var i = 0; i < expected.length; i += 1) expSet[expected[i]] = true;
265
+ for (var j = 0; j < actual.length; j += 1) actSet[actual[j]] = true;
266
+
267
+ var missing = [];
268
+ var extra = [];
269
+ var k;
270
+ for (k in expSet) if (!actSet[k]) missing.push(k);
271
+ for (k in actSet) if (!expSet[k]) extra.push(k);
272
+ missing.sort();
273
+ extra.sort();
274
+ return { missing: missing, extra: extra };
275
+ }
276
+
277
+ // ---- factory -----------------------------------------------------------
278
+
279
+ function create(opts) {
280
+ opts = opts || {};
281
+ var query = opts.query;
282
+ if (!query) {
283
+ query = function (sql, params) { return _b().externalDb.query(sql, params); };
284
+ }
285
+
286
+ // inventoryLocations is optional — when wired, every assign /
287
+ // unassign / audit / condition call validates the location_code
288
+ // against the registered set. Absent, every well-shaped code passes
289
+ // the boundary.
290
+ var invLocations = opts.inventoryLocations || null;
291
+ if (invLocations && typeof invLocations.getLocation !== "function") {
292
+ throw new TypeError("bin-locations.create: opts.inventoryLocations must expose a getLocation(code) method");
293
+ }
294
+
295
+ // catalog is optional — when wired, every assignBin / bulkAssign
296
+ // checks the SKU against catalog.get(sku) and refuses unknown SKUs.
297
+ // Absent, every well-shaped SKU string passes the boundary.
298
+ var catalog = opts.catalog || null;
299
+ if (catalog && typeof catalog.get !== "function") {
300
+ throw new TypeError("bin-locations.create: opts.catalog must expose a get(sku) method");
301
+ }
302
+
303
+ async function _checkLocation(code) {
304
+ if (!invLocations) return;
305
+ var loc = await invLocations.getLocation(code);
306
+ if (!loc) {
307
+ throw new TypeError("bin-locations: location_code " + JSON.stringify(code) +
308
+ " is not registered with the wired inventoryLocations");
309
+ }
310
+ }
311
+
312
+ async function _checkSku(sku) {
313
+ if (!catalog) return;
314
+ var row = await catalog.get(sku);
315
+ if (!row) {
316
+ throw new TypeError("bin-locations: sku " + JSON.stringify(sku) +
317
+ " is not registered with the wired catalog");
318
+ }
319
+ }
320
+
321
+ async function _getActiveAssignment(sku, locationCode, binLabel) {
322
+ var r = await query(
323
+ "SELECT * FROM bin_assignments WHERE sku = ?1 AND location_code = ?2 " +
324
+ "AND bin_label = ?3 AND archived_at IS NULL LIMIT 1",
325
+ [sku, locationCode, binLabel],
326
+ );
327
+ return r.rows.length ? r.rows[0] : null;
328
+ }
329
+
330
+ async function _existsPrimaryForSkuLocation(sku, locationCode, excludeBinLabel) {
331
+ var sql = "SELECT COUNT(*) AS n FROM bin_assignments WHERE sku = ?1 " +
332
+ "AND location_code = ?2 AND archived_at IS NULL AND is_primary = 1";
333
+ var params = [sku, locationCode];
334
+ if (excludeBinLabel != null) {
335
+ sql += " AND bin_label != ?3";
336
+ params.push(excludeBinLabel);
337
+ }
338
+ var r = await query(sql, params);
339
+ return Number(r.rows[0].n) > 0;
340
+ }
341
+
342
+ async function _assignBinInner(input) {
343
+ if (!input || typeof input !== "object") {
344
+ throw new TypeError("bin-locations.assignBin: input object required");
345
+ }
346
+ var sku = _sku(input.sku, "sku");
347
+ var locCode = _locCode(input.location_code);
348
+ var binLabel = _binLabel(input.bin_label);
349
+ var aisle = _aisle(input.aisle);
350
+ var shelf = _shelf(input.shelf);
351
+ var level = _level(input.level);
352
+ var explicitPrimary = false;
353
+ var requestedPrimary = false;
354
+ if (input.is_primary !== undefined) {
355
+ if (typeof input.is_primary !== "boolean") {
356
+ throw new TypeError("bin-locations.assignBin: is_primary must be a boolean when provided");
357
+ }
358
+ explicitPrimary = true;
359
+ requestedPrimary = input.is_primary;
360
+ }
361
+
362
+ await _checkLocation(locCode);
363
+ await _checkSku(sku);
364
+
365
+ // Primary-flag policy: when the caller hasn't requested one
366
+ // explicitly, the assignment becomes primary IFF no other active
367
+ // assignment for the same (sku, location) already holds the flag.
368
+ // When the caller did request `is_primary: true`, every other
369
+ // active assignment for the same (sku, location) gets demoted in
370
+ // the same write — there is always exactly one primary per
371
+ // (sku, location).
372
+ var existingPrimary = await _existsPrimaryForSkuLocation(sku, locCode, binLabel);
373
+ var isPrimary;
374
+ if (explicitPrimary) {
375
+ isPrimary = requestedPrimary;
376
+ } else {
377
+ isPrimary = !existingPrimary;
378
+ }
379
+
380
+ var now = _now();
381
+ var existing = await _getActiveAssignment(sku, locCode, binLabel);
382
+ if (existing) {
383
+ // Re-assigning the same triple updates the coordinates in place
384
+ // rather than throwing the UNIQUE error. Operators correct a
385
+ // mis-typed aisle by re-running assignBin with the right values.
386
+ await query(
387
+ "UPDATE bin_assignments SET aisle = ?1, shelf = ?2, level = ?3, " +
388
+ "is_primary = ?4, assigned_at = ?5 WHERE id = ?6",
389
+ [aisle, shelf, level, isPrimary ? 1 : 0, now, existing.id],
390
+ );
391
+ } else {
392
+ try {
393
+ await query(
394
+ "INSERT INTO bin_assignments (id, sku, location_code, bin_label, " +
395
+ "aisle, shelf, level, is_primary, assigned_at, archived_at) " +
396
+ "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, NULL)",
397
+ [_b().uuid.v7(), sku, locCode, binLabel,
398
+ aisle, shelf, level, isPrimary ? 1 : 0, now],
399
+ );
400
+ } catch (e) {
401
+ if (/UNIQUE/i.test(String(e && e.message))) {
402
+ throw new TypeError("bin-locations.assignBin: assignment for sku " +
403
+ JSON.stringify(sku) + " at " + JSON.stringify(locCode) + "/" +
404
+ JSON.stringify(binLabel) + " already exists");
405
+ }
406
+ throw e;
407
+ }
408
+ }
409
+
410
+ // When the new row is primary, demote every other active
411
+ // assignment for the same (sku, location).
412
+ if (isPrimary) {
413
+ await query(
414
+ "UPDATE bin_assignments SET is_primary = 0 WHERE sku = ?1 " +
415
+ "AND location_code = ?2 AND bin_label != ?3 AND archived_at IS NULL",
416
+ [sku, locCode, binLabel],
417
+ );
418
+ }
419
+
420
+ return _hydrateAssignment(await _getActiveAssignment(sku, locCode, binLabel));
421
+ }
422
+
423
+ async function _unassignBinInner(input) {
424
+ if (!input || typeof input !== "object") {
425
+ throw new TypeError("bin-locations.unassignBin: input object required");
426
+ }
427
+ var sku = _sku(input.sku, "sku");
428
+ var locCode = _locCode(input.location_code);
429
+ var binLabel = _binLabel(input.bin_label);
430
+
431
+ var existing = await _getActiveAssignment(sku, locCode, binLabel);
432
+ if (!existing) {
433
+ throw new TypeError("bin-locations.unassignBin: no active assignment for sku " +
434
+ JSON.stringify(sku) + " at " + JSON.stringify(locCode) + "/" +
435
+ JSON.stringify(binLabel));
436
+ }
437
+ var now = _now();
438
+ await query(
439
+ "UPDATE bin_assignments SET archived_at = ?1, is_primary = 0 WHERE id = ?2",
440
+ [now, existing.id],
441
+ );
442
+
443
+ // If the archived row was the primary, promote any remaining
444
+ // active assignment for the same (sku, location) to primary.
445
+ // Stable ordering by assigned_at ASC, bin_label ASC picks a
446
+ // deterministic successor.
447
+ if (Number(existing.is_primary) === 1) {
448
+ var successors = (await query(
449
+ "SELECT id FROM bin_assignments WHERE sku = ?1 AND location_code = ?2 " +
450
+ "AND archived_at IS NULL ORDER BY assigned_at ASC, bin_label ASC LIMIT 1",
451
+ [sku, locCode],
452
+ )).rows;
453
+ if (successors.length) {
454
+ await query(
455
+ "UPDATE bin_assignments SET is_primary = 1 WHERE id = ?1",
456
+ [successors[0].id],
457
+ );
458
+ }
459
+ }
460
+
461
+ return { sku: sku, location_code: locCode, bin_label: binLabel,
462
+ archived_at: now };
463
+ }
464
+
465
+ return {
466
+
467
+ CONDITIONS: CONDITIONS,
468
+
469
+ // Assign a SKU to a (location, bin, aisle, shelf, level) tuple.
470
+ // Re-assigning the same triple updates the coordinates in place;
471
+ // a brand-new triple inserts. The is_primary flag is auto-set
472
+ // when the caller doesn't request one explicitly — the first
473
+ // active assignment for a (sku, location) becomes primary; later
474
+ // assignments default to secondary. Operators override by
475
+ // passing `is_primary: true` on the call they want promoted; the
476
+ // primitive demotes every other active assignment in the same
477
+ // write.
478
+ assignBin: _assignBinInner,
479
+
480
+ // Soft-delete an assignment. The row stays in the table so the
481
+ // audit history of "where this SKU used to live" survives.
482
+ // When the archived row was the primary, the next-oldest active
483
+ // assignment for the same (sku, location) is promoted.
484
+ unassignBin: _unassignBinInner,
485
+
486
+ // Primary-bin read. Returns the active assignment row flagged
487
+ // is_primary at the (sku, location_code), or null when the SKU
488
+ // has no active assignments at that location.
489
+ binForSku: async function (input) {
490
+ if (!input || typeof input !== "object") {
491
+ throw new TypeError("bin-locations.binForSku: input object required");
492
+ }
493
+ var sku = _sku(input.sku, "sku");
494
+ var locCode = _locCode(input.location_code);
495
+ var r = await query(
496
+ "SELECT * FROM bin_assignments WHERE sku = ?1 AND location_code = ?2 " +
497
+ "AND archived_at IS NULL ORDER BY is_primary DESC, assigned_at ASC, " +
498
+ "bin_label ASC LIMIT 1",
499
+ [sku, locCode],
500
+ );
501
+ return r.rows.length ? _hydrateAssignment(r.rows[0]) : null;
502
+ },
503
+
504
+ // Every active assignment across every location for a SKU.
505
+ // Sorted (location_code ASC, is_primary DESC, bin_label ASC) so
506
+ // each location's primary bin lands first within its group.
507
+ binsForSku: async function (sku) {
508
+ _sku(sku, "sku");
509
+ var r = await query(
510
+ "SELECT * FROM bin_assignments WHERE sku = ?1 AND archived_at IS NULL " +
511
+ "ORDER BY location_code ASC, is_primary DESC, bin_label ASC",
512
+ [sku],
513
+ );
514
+ var out = [];
515
+ for (var i = 0; i < r.rows.length; i += 1) out.push(_hydrateAssignment(r.rows[i]));
516
+ return out;
517
+ },
518
+
519
+ // Every SKU residing at a (location, bin). Operator's bin-audit
520
+ // screen reads this to render the "what does this bin hold" list.
521
+ skusInBin: async function (input) {
522
+ if (!input || typeof input !== "object") {
523
+ throw new TypeError("bin-locations.skusInBin: input object required");
524
+ }
525
+ var locCode = _locCode(input.location_code);
526
+ var binLabel = _binLabel(input.bin_label);
527
+ var r = await query(
528
+ "SELECT * FROM bin_assignments WHERE location_code = ?1 AND bin_label = ?2 " +
529
+ "AND archived_at IS NULL ORDER BY sku ASC",
530
+ [locCode, binLabel],
531
+ );
532
+ var out = [];
533
+ for (var i = 0; i < r.rows.length; i += 1) out.push(_hydrateAssignment(r.rows[i]));
534
+ return out;
535
+ },
536
+
537
+ // Aisle-scoped read for the operator's walk-the-floor view.
538
+ // Sorts by (shelf ASC, level ASC, bin_label ASC, sku ASC) so the
539
+ // result reads top-to-bottom, left-to-right along the aisle.
540
+ searchBinsByAisle: async function (input) {
541
+ if (!input || typeof input !== "object") {
542
+ throw new TypeError("bin-locations.searchBinsByAisle: input object required");
543
+ }
544
+ var locCode = _locCode(input.location_code);
545
+ var aisle = _aisle(input.aisle);
546
+ var limit = input.limit == null ? 100 : input.limit;
547
+ _limit(limit);
548
+ var r = await query(
549
+ "SELECT * FROM bin_assignments WHERE location_code = ?1 AND aisle = ?2 " +
550
+ "AND archived_at IS NULL " +
551
+ "ORDER BY shelf ASC, level ASC, bin_label ASC, sku ASC LIMIT ?3",
552
+ [locCode, aisle, limit],
553
+ );
554
+ var out = [];
555
+ for (var i = 0; i < r.rows.length; i += 1) out.push(_hydrateAssignment(r.rows[i]));
556
+ return out;
557
+ },
558
+
559
+ // Translate a flat list of SKUs into an aisle-ordered walk path
560
+ // at the given location. SKUs with no active assignment land at
561
+ // the END of the path so the picker still gets them on the list
562
+ // but knows to handle them specially. The sort is stable for
563
+ // duplicates and lexicographic across coordinate ties.
564
+ pickPathSort: async function (input) {
565
+ if (!input || typeof input !== "object") {
566
+ throw new TypeError("bin-locations.pickPathSort: input object required");
567
+ }
568
+ var locCode = _locCode(input.location_code);
569
+ var skus = input.skus;
570
+ if (!Array.isArray(skus)) {
571
+ throw new TypeError("bin-locations.pickPathSort: skus must be an array");
572
+ }
573
+ if (skus.length === 0) return [];
574
+ if (skus.length > MAX_LIST_LIMIT) {
575
+ throw new TypeError("bin-locations.pickPathSort: skus must contain <= " +
576
+ MAX_LIST_LIMIT + " entries");
577
+ }
578
+ for (var k = 0; k < skus.length; k += 1) _sku(skus[k], "skus[" + k + "]");
579
+
580
+ // Pull every PRIMARY (or sole-active) assignment for the input
581
+ // SKUs at this location in one SQL round-trip. `is_primary DESC`
582
+ // ensures the primary lands first when a SKU has multiple
583
+ // active assignments; the GROUP BY on `sku` keeps one row per
584
+ // SKU.
585
+ var placeholders = [];
586
+ var params = [locCode];
587
+ for (var i = 0; i < skus.length; i += 1) {
588
+ placeholders.push("?" + (i + 2));
589
+ params.push(skus[i]);
590
+ }
591
+ var r = await query(
592
+ "SELECT sku, aisle, shelf, level FROM bin_assignments WHERE " +
593
+ "location_code = ?1 AND archived_at IS NULL AND sku IN (" +
594
+ placeholders.join(", ") + ") " +
595
+ "ORDER BY sku ASC, is_primary DESC, assigned_at ASC, bin_label ASC",
596
+ params,
597
+ );
598
+ // Build the per-sku coordinate index — first row per sku wins
599
+ // (the ORDER BY already sorted primaries to the front of each
600
+ // sku's group).
601
+ var coordsBySku = Object.create(null);
602
+ for (var j = 0; j < r.rows.length; j += 1) {
603
+ var row = r.rows[j];
604
+ if (coordsBySku[row.sku] != null) continue;
605
+ coordsBySku[row.sku] = {
606
+ aisle: row.aisle, shelf: row.shelf, level: row.level,
607
+ };
608
+ }
609
+ // Build the decorated list preserving original index for
610
+ // stability when coordinates tie.
611
+ var decorated = [];
612
+ for (var m = 0; m < skus.length; m += 1) {
613
+ var sku = skus[m];
614
+ var c = coordsBySku[sku];
615
+ decorated.push({
616
+ sku: sku,
617
+ aisle: c ? c.aisle : NO_ASSIGN_SORT_KEY,
618
+ shelf: c ? c.shelf : NO_ASSIGN_SORT_KEY,
619
+ level: c ? c.level : NO_ASSIGN_SORT_KEY,
620
+ idx: m,
621
+ });
622
+ }
623
+ decorated.sort(function (a, b) {
624
+ if (a.aisle !== b.aisle) return a.aisle < b.aisle ? -1 : 1;
625
+ if (a.shelf !== b.shelf) return a.shelf < b.shelf ? -1 : 1;
626
+ if (a.level !== b.level) return a.level < b.level ? -1 : 1;
627
+ if (a.sku !== b.sku) return a.sku < b.sku ? -1 : 1;
628
+ return a.idx - b.idx;
629
+ });
630
+ var sorted = [];
631
+ for (var p = 0; p < decorated.length; p += 1) sorted.push(decorated[p].sku);
632
+ return sorted;
633
+ },
634
+
635
+ // Bulk assign — N rows, same shape as `assignBin`. Refuses the
636
+ // whole batch on the first malformed row (the write-time
637
+ // validators throw before any row touches the table); rows
638
+ // already valid pass through one-at-a-time so each is_primary
639
+ // promotion sees the prior writes. Returns the per-row hydrated
640
+ // result list.
641
+ bulkAssign: async function (rows) {
642
+ if (!Array.isArray(rows)) {
643
+ throw new TypeError("bin-locations.bulkAssign: rows must be an array");
644
+ }
645
+ if (rows.length === 0) return [];
646
+ if (rows.length > MAX_BULK_ROWS) {
647
+ throw new TypeError("bin-locations.bulkAssign: rows must contain <= " +
648
+ MAX_BULK_ROWS + " entries");
649
+ }
650
+ var out = [];
651
+ for (var i = 0; i < rows.length; i += 1) {
652
+ out.push(await _assignBinInner(rows[i]));
653
+ }
654
+ return out;
655
+ },
656
+
657
+ // Bulk unassign — N rows, same shape as `unassignBin`. Refuses
658
+ // the whole batch on the first malformed row.
659
+ bulkUnassign: async function (rows) {
660
+ if (!Array.isArray(rows)) {
661
+ throw new TypeError("bin-locations.bulkUnassign: rows must be an array");
662
+ }
663
+ if (rows.length === 0) return [];
664
+ if (rows.length > MAX_BULK_ROWS) {
665
+ throw new TypeError("bin-locations.bulkUnassign: rows must contain <= " +
666
+ MAX_BULK_ROWS + " entries");
667
+ }
668
+ var out = [];
669
+ for (var i = 0; i < rows.length; i += 1) {
670
+ out.push(await _unassignBinInner(rows[i]));
671
+ }
672
+ return out;
673
+ },
674
+
675
+ // Append a bin-audit row. Computes the variance (missing /
676
+ // extra) between expected and actual SKU sets and persists the
677
+ // resulting object on the audit row. The operator's
678
+ // reconciliation worker reads `variance` to decide whether to
679
+ // adjust stock, file a damage claim, or escalate to a recount.
680
+ recordBinAudit: async function (input) {
681
+ if (!input || typeof input !== "object") {
682
+ throw new TypeError("bin-locations.recordBinAudit: input object required");
683
+ }
684
+ var locCode = _locCode(input.location_code);
685
+ var binLabel = _binLabel(input.bin_label);
686
+ var auditor = _auditor(input.audited_by);
687
+ var expected = _skuListForAudit(input.expected_skus, "expected_skus");
688
+ var actual = _skuListForAudit(input.actual_skus, "actual_skus");
689
+ var occurredAt;
690
+ if (input.occurred_at == null) {
691
+ occurredAt = _now();
692
+ } else {
693
+ if (!Number.isInteger(input.occurred_at) || input.occurred_at <= 0) {
694
+ throw new TypeError("bin-locations.recordBinAudit: occurred_at must be a positive integer (epoch ms) when provided");
695
+ }
696
+ occurredAt = input.occurred_at;
697
+ }
698
+
699
+ await _checkLocation(locCode);
700
+
701
+ var variance = _computeVariance(expected, actual);
702
+ // Sort the expected/actual lists for deterministic storage —
703
+ // the audit row round-trips the same JSON bytes regardless of
704
+ // input order.
705
+ var expectedSorted = expected.slice().sort();
706
+ var actualSorted = actual.slice().sort();
707
+ var id = _b().uuid.v7();
708
+ await query(
709
+ "INSERT INTO bin_audits (id, location_code, bin_label, audited_by, " +
710
+ "expected_skus_json, actual_skus_json, variance_json, occurred_at) " +
711
+ "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
712
+ [id, locCode, binLabel, auditor,
713
+ JSON.stringify(expectedSorted),
714
+ JSON.stringify(actualSorted),
715
+ JSON.stringify(variance),
716
+ occurredAt],
717
+ );
718
+ var r = await query("SELECT * FROM bin_audits WHERE id = ?1", [id]);
719
+ return _hydrateAudit(r.rows[0]);
720
+ },
721
+
722
+ // Upsert a bin's condition flag. The operator's warehouse-floor
723
+ // dashboard reads `listBinsWithCondition({ condition })` to
724
+ // surface bins that need a cleaning pass or are unusable.
725
+ binCondition: async function (input) {
726
+ if (!input || typeof input !== "object") {
727
+ throw new TypeError("bin-locations.binCondition: input object required");
728
+ }
729
+ var locCode = _locCode(input.location_code);
730
+ var binLabel = _binLabel(input.bin_label);
731
+ var condition = _condition(input.condition);
732
+
733
+ await _checkLocation(locCode);
734
+
735
+ var now = _now();
736
+ var existing = (await query(
737
+ "SELECT * FROM bin_conditions WHERE location_code = ?1 AND bin_label = ?2",
738
+ [locCode, binLabel],
739
+ )).rows;
740
+ if (existing.length) {
741
+ await query(
742
+ "UPDATE bin_conditions SET condition = ?1, updated_at = ?2 " +
743
+ "WHERE location_code = ?3 AND bin_label = ?4",
744
+ [condition, now, locCode, binLabel],
745
+ );
746
+ } else {
747
+ await query(
748
+ "INSERT INTO bin_conditions (location_code, bin_label, condition, updated_at) " +
749
+ "VALUES (?1, ?2, ?3, ?4)",
750
+ [locCode, binLabel, condition, now],
751
+ );
752
+ }
753
+ var r = await query(
754
+ "SELECT * FROM bin_conditions WHERE location_code = ?1 AND bin_label = ?2",
755
+ [locCode, binLabel],
756
+ );
757
+ return _hydrateCondition(r.rows[0]);
758
+ },
759
+
760
+ // List bins flagged with a given condition. When location_code
761
+ // is provided, restricts to that location; absent, returns
762
+ // every flagged bin across every location ordered by
763
+ // (location_code, bin_label).
764
+ listBinsWithCondition: async function (input) {
765
+ if (!input || typeof input !== "object") {
766
+ throw new TypeError("bin-locations.listBinsWithCondition: input object required");
767
+ }
768
+ var condition = _condition(input.condition);
769
+ var sql, params;
770
+ if (input.location_code != null) {
771
+ var locCode = _locCode(input.location_code);
772
+ sql = "SELECT * FROM bin_conditions WHERE condition = ?1 AND location_code = ?2 " +
773
+ "ORDER BY bin_label ASC";
774
+ params = [condition, locCode];
775
+ } else {
776
+ sql = "SELECT * FROM bin_conditions WHERE condition = ?1 " +
777
+ "ORDER BY location_code ASC, bin_label ASC";
778
+ params = [condition];
779
+ }
780
+ var r = await query(sql, params);
781
+ var out = [];
782
+ for (var i = 0; i < r.rows.length; i += 1) out.push(_hydrateCondition(r.rows[i]));
783
+ return out;
784
+ },
785
+ };
786
+ }
787
+
788
+ module.exports = {
789
+ create: create,
790
+ CONDITIONS: CONDITIONS,
791
+ };