@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.
- package/CHANGELOG.md +6 -0
- package/lib/announcement-bar.js +753 -0
- package/lib/banner-ab-tests.js +806 -0
- package/lib/bin-locations.js +791 -0
- package/lib/blog-articles.js +1173 -0
- package/lib/carrier-accounts.js +805 -0
- package/lib/cart-recovery.js +1133 -0
- package/lib/category-navigation.js +934 -0
- package/lib/consent-ledger.js +539 -0
- package/lib/customer-impersonation.js +743 -0
- package/lib/customer-merge.js +879 -0
- package/lib/demand-forecast.js +1121 -0
- package/lib/dispute-resolution.js +886 -0
- package/lib/email-ab-tests.js +918 -0
- package/lib/email-engagement-score.js +649 -0
- package/lib/event-log.js +713 -0
- package/lib/fulfillment-sla.js +791 -0
- package/lib/index.js +41 -0
- package/lib/inventory-audits.js +852 -0
- package/lib/line-gift-wrap.js +430 -0
- package/lib/marketing-budget.js +792 -0
- package/lib/operator-activity-feed.js +977 -0
- package/lib/operator-approvals.js +942 -0
- package/lib/operator-help-center.js +1020 -0
- package/lib/operator-inbox.js +889 -0
- package/lib/operator-sessions.js +701 -0
- package/lib/order-exchanges.js +602 -0
- package/lib/product-compare.js +804 -0
- package/lib/pwa-manifest.js +1005 -0
- package/lib/referral-leaderboard.js +612 -0
- package/lib/sales-tax-filings.js +807 -0
- package/lib/search-ranking.js +859 -0
- package/lib/shipping-insurance.js +757 -0
- package/lib/shrinkage-report.js +1182 -0
- package/lib/sidebar-widgets.js +952 -0
- package/lib/smart-restocking.js +1048 -0
- package/lib/stock-receipts.js +834 -0
- package/lib/subscription-analytics.js +1032 -0
- package/lib/suggestion-box.js +921 -0
- package/lib/tax-remittance.js +625 -0
- package/lib/vendor-invoices.js +1021 -0
- package/lib/winback-campaigns.js +1350 -0
- package/lib/wishlist-digest.js +1133 -0
- 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
|
+
};
|