@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,834 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module shop.stockReceipts
|
|
4
|
+
* @title Stock receipts — customer-facing proof-of-receipt via QR
|
|
5
|
+
*
|
|
6
|
+
* @intro
|
|
7
|
+
* Every shipped order's packing slip prints a QR code that resolves
|
|
8
|
+
* to a single-use plaintext token. When the customer scans the QR on
|
|
9
|
+
* arrival, the storefront walks them through a checklist of the
|
|
10
|
+
* order's line items: confirm received, flag damaged, or leave a
|
|
11
|
+
* line pending until they finish unpacking. The primitive records
|
|
12
|
+
* every scan event (audit trail), tracks per-line state with
|
|
13
|
+
* partial-quantity support, and on `completeReceipt` composes the
|
|
14
|
+
* optional `loyaltyEarnRules` handle to award goods-received points.
|
|
15
|
+
*
|
|
16
|
+
* FSM (receipt token):
|
|
17
|
+
*
|
|
18
|
+
* issued --recordReceiptScan--> scanned --completeReceipt--> completed
|
|
19
|
+
* \ ^
|
|
20
|
+
* \--(expires_at < now)--> expired |
|
|
21
|
+
*
|
|
22
|
+
* `completed` and `expired` are terminal. The first scan flips the
|
|
23
|
+
* row to `scanned`; subsequent scans append to the event log
|
|
24
|
+
* without re-flipping the FSM. `completeReceipt` refuses on
|
|
25
|
+
* `issued` (the customer must scan first) and on `expired` /
|
|
26
|
+
* `completed`.
|
|
27
|
+
*
|
|
28
|
+
* FSM (per-line state):
|
|
29
|
+
*
|
|
30
|
+
* pending --markLineReceived--> received
|
|
31
|
+
* \--markLineDamaged --> damaged
|
|
32
|
+
* \--mixed quantities--> partial (when received > 0 AND damaged > 0)
|
|
33
|
+
*
|
|
34
|
+
* Token plaintext:
|
|
35
|
+
* 32 random bytes from `b.crypto.generateBytes`, rendered as 43-
|
|
36
|
+
* char base64url (no padding). Returned EXACTLY ONCE from
|
|
37
|
+
* `issueReceiptToken`; only the SHA3-512 namespace-hash lands on
|
|
38
|
+
* `stock_receipt_tokens.token_hash`. `recordReceiptScan` re-hashes
|
|
39
|
+
* the presented token and looks up — wrong tokens surface as
|
|
40
|
+
* not-found rather than leaking timing information.
|
|
41
|
+
*
|
|
42
|
+
* Composes:
|
|
43
|
+
* - `b.crypto.generateBytes` — uniform 32-byte plaintext draw.
|
|
44
|
+
* - `b.crypto.namespaceHash` — SHA3-512 hash under the
|
|
45
|
+
* `stock-receipt-token` namespace,
|
|
46
|
+
* and per-scan UA / IP hashing so
|
|
47
|
+
* audit reads don't carry raw PII.
|
|
48
|
+
* - `b.crypto.timingSafeEqual` — constant-time hex compare on
|
|
49
|
+
* recordReceiptScan.
|
|
50
|
+
* - `b.guardUuid` — strict UUID gate on order_id /
|
|
51
|
+
* receipt id reads.
|
|
52
|
+
* - `b.uuid.v7` — receipt token + scan row PKs
|
|
53
|
+
* (lexicographic + monotonic so
|
|
54
|
+
* audit reads sort cleanly).
|
|
55
|
+
* - `loyaltyEarnRules` (optional) — when wired, `completeReceipt`
|
|
56
|
+
* calls `evaluateForEvent({
|
|
57
|
+
* trigger: "per_purchase", ... })`
|
|
58
|
+
* once the line checklist is
|
|
59
|
+
* closed. Failures are drop-silent
|
|
60
|
+
* — the receipt is complete
|
|
61
|
+
* regardless of whether the points
|
|
62
|
+
* landed.
|
|
63
|
+
*
|
|
64
|
+
* Storage: `migrations-d1/0177_stock_receipts.sql` —
|
|
65
|
+
* `stock_receipt_tokens` + `stock_receipt_scans` (FK CASCADE) +
|
|
66
|
+
* `stock_receipt_line_states` (FK CASCADE).
|
|
67
|
+
*
|
|
68
|
+
* @primitive stockReceipts
|
|
69
|
+
* @related b.crypto, b.uuid, b.guardUuid, loyaltyEarnRules
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
var bShop;
|
|
73
|
+
function _b() {
|
|
74
|
+
if (!bShop) bShop = require("./index");
|
|
75
|
+
return bShop.framework;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---- constants ----------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
var TOKEN_NAMESPACE = "stock-receipt-token";
|
|
81
|
+
var UA_NAMESPACE = "stock-receipt-user-agent";
|
|
82
|
+
var IP_NAMESPACE = "stock-receipt-client-ip";
|
|
83
|
+
|
|
84
|
+
var TOKEN_BYTE_LEN = 32;
|
|
85
|
+
var TOKEN_PLAINTEXT_LEN = 43;
|
|
86
|
+
var TOKEN_PLAINTEXT_RE = /^[A-Za-z0-9_-]{43}$/;
|
|
87
|
+
|
|
88
|
+
var SKU_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
89
|
+
var MAX_REASON_LEN = 500;
|
|
90
|
+
var MAX_UA_LEN = 1024;
|
|
91
|
+
var MAX_IP_LEN = 64;
|
|
92
|
+
var MAX_LIST_LIMIT = 500;
|
|
93
|
+
var MAX_LINES = 200;
|
|
94
|
+
|
|
95
|
+
var DEFAULT_EXPIRES_HOURS = 24 * 30; // 30 days
|
|
96
|
+
var MIN_EXPIRES_HOURS = 1;
|
|
97
|
+
var MAX_EXPIRES_HOURS = 24 * 365; // 1 year
|
|
98
|
+
|
|
99
|
+
var RECEIPT_STATUSES = Object.freeze([
|
|
100
|
+
"issued", "scanned", "completed", "expired",
|
|
101
|
+
]);
|
|
102
|
+
var LINE_STATES = Object.freeze([
|
|
103
|
+
"pending", "received", "damaged", "partial",
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
var CONTROL_BYTE_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
107
|
+
|
|
108
|
+
// ---- monotonic clock ----------------------------------------------------
|
|
109
|
+
//
|
|
110
|
+
// FSM transitions + scan events land on epoch-ms timestamps. The
|
|
111
|
+
// receipt's first scan can land in the same millisecond as a follow-
|
|
112
|
+
// up scan from a re-opened tab; strict-monotonic ordering guarantees
|
|
113
|
+
// `scanned_at` is distinct row-by-row so a chronological dashboard
|
|
114
|
+
// sort returns events in the order they were issued.
|
|
115
|
+
var _lastTs = 0;
|
|
116
|
+
function _now() {
|
|
117
|
+
var t = Date.now();
|
|
118
|
+
if (t <= _lastTs) { t = _lastTs + 1; }
|
|
119
|
+
_lastTs = t;
|
|
120
|
+
return t;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---- validators ---------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
function _orderId(s) {
|
|
126
|
+
try { return _b().guardUuid.sanitize(s, { profile: "strict" }); }
|
|
127
|
+
catch (e) { throw new TypeError("stockReceipts: order_id — " + (e && e.message || "invalid UUID")); }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function _uuid(s, label) {
|
|
131
|
+
try { return _b().guardUuid.sanitize(s, { profile: "strict" }); }
|
|
132
|
+
catch (e) { throw new TypeError("stockReceipts: " + label + " — " + (e && e.message || "invalid UUID")); }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function _sku(s) {
|
|
136
|
+
if (typeof s !== "string" || !SKU_RE.test(s)) {
|
|
137
|
+
throw new TypeError("stockReceipts: sku must match /^[A-Za-z0-9][A-Za-z0-9._-]*$/ (alnum + . _ -, 1..128 chars)");
|
|
138
|
+
}
|
|
139
|
+
return s;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function _positiveInt(n, label) {
|
|
143
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
144
|
+
throw new TypeError("stockReceipts: " + label + " must be a positive integer");
|
|
145
|
+
}
|
|
146
|
+
return n;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function _nonNegInt(n, label) {
|
|
150
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
151
|
+
throw new TypeError("stockReceipts: " + label + " must be a non-negative integer");
|
|
152
|
+
}
|
|
153
|
+
return n;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function _expiresInHours(n) {
|
|
157
|
+
if (n == null) return DEFAULT_EXPIRES_HOURS;
|
|
158
|
+
if (!Number.isInteger(n) || n < MIN_EXPIRES_HOURS || n > MAX_EXPIRES_HOURS) {
|
|
159
|
+
throw new TypeError("stockReceipts: expires_in_hours must be an integer in [" +
|
|
160
|
+
MIN_EXPIRES_HOURS + ", " + MAX_EXPIRES_HOURS + "]");
|
|
161
|
+
}
|
|
162
|
+
return n;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function _limit(n) {
|
|
166
|
+
if (n == null) return 50;
|
|
167
|
+
if (!Number.isInteger(n) || n <= 0 || n > MAX_LIST_LIMIT) {
|
|
168
|
+
throw new TypeError("stockReceipts: limit must be an integer in [1, " + MAX_LIST_LIMIT + "]");
|
|
169
|
+
}
|
|
170
|
+
return n;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function _reason(s) {
|
|
174
|
+
if (typeof s !== "string" || !s.length || s.length > MAX_REASON_LEN) {
|
|
175
|
+
throw new TypeError("stockReceipts: reason must be a non-empty string <= " + MAX_REASON_LEN + " chars");
|
|
176
|
+
}
|
|
177
|
+
if (CONTROL_BYTE_RE.test(s)) {
|
|
178
|
+
throw new TypeError("stockReceipts: reason must not contain control bytes");
|
|
179
|
+
}
|
|
180
|
+
return s;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function _canonicalToken(input) {
|
|
184
|
+
if (typeof input !== "string" || !input.length) {
|
|
185
|
+
throw new TypeError("stockReceipts: token must be a non-empty string");
|
|
186
|
+
}
|
|
187
|
+
if (!TOKEN_PLAINTEXT_RE.test(input)) {
|
|
188
|
+
throw new TypeError("stockReceipts: token must be 43 base64url characters");
|
|
189
|
+
}
|
|
190
|
+
return input;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function _uaOpt(s) {
|
|
194
|
+
if (s == null) return null;
|
|
195
|
+
if (typeof s !== "string" || s.length > MAX_UA_LEN) {
|
|
196
|
+
throw new TypeError("stockReceipts: user_agent must be a string <= " + MAX_UA_LEN + " chars");
|
|
197
|
+
}
|
|
198
|
+
if (CONTROL_BYTE_RE.test(s)) {
|
|
199
|
+
throw new TypeError("stockReceipts: user_agent must not contain control bytes");
|
|
200
|
+
}
|
|
201
|
+
return s;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function _ipOpt(s) {
|
|
205
|
+
if (s == null) return null;
|
|
206
|
+
if (typeof s !== "string" || s.length > MAX_IP_LEN) {
|
|
207
|
+
throw new TypeError("stockReceipts: client_ip must be a string <= " + MAX_IP_LEN + " chars");
|
|
208
|
+
}
|
|
209
|
+
if (CONTROL_BYTE_RE.test(s)) {
|
|
210
|
+
throw new TypeError("stockReceipts: client_ip must not contain control bytes");
|
|
211
|
+
}
|
|
212
|
+
return s;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function _linesArray(arr) {
|
|
216
|
+
if (!Array.isArray(arr) || arr.length === 0) {
|
|
217
|
+
throw new TypeError("stockReceipts: lines must be a non-empty array");
|
|
218
|
+
}
|
|
219
|
+
if (arr.length > MAX_LINES) {
|
|
220
|
+
throw new TypeError("stockReceipts: lines must contain <= " + MAX_LINES + " entries");
|
|
221
|
+
}
|
|
222
|
+
var seen = Object.create(null);
|
|
223
|
+
var out = [];
|
|
224
|
+
for (var i = 0; i < arr.length; i += 1) {
|
|
225
|
+
var ln = arr[i];
|
|
226
|
+
if (!ln || typeof ln !== "object") {
|
|
227
|
+
throw new TypeError("stockReceipts: lines[" + i + "] must be an object");
|
|
228
|
+
}
|
|
229
|
+
var sku = _sku(ln.sku);
|
|
230
|
+
if (seen[sku]) {
|
|
231
|
+
throw new TypeError("stockReceipts: lines[" + i + "].sku duplicates a previous entry");
|
|
232
|
+
}
|
|
233
|
+
seen[sku] = true;
|
|
234
|
+
var qty = _positiveInt(ln.quantity_expected, "lines[" + i + "].quantity_expected");
|
|
235
|
+
out.push({ sku: sku, quantity_expected: qty });
|
|
236
|
+
}
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ---- token generation + hashing -----------------------------------------
|
|
241
|
+
|
|
242
|
+
function _generateToken() {
|
|
243
|
+
var buf = _b().crypto.generateBytes(TOKEN_BYTE_LEN);
|
|
244
|
+
return buf.toString("base64")
|
|
245
|
+
.replace(/\+/g, "-")
|
|
246
|
+
.replace(/\//g, "_")
|
|
247
|
+
.replace(/=+$/, "");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function _hashToken(canonical) {
|
|
251
|
+
return _b().crypto.namespaceHash(TOKEN_NAMESPACE, canonical);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ---- factory ------------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
function create(opts) {
|
|
257
|
+
opts = opts || {};
|
|
258
|
+
var query = opts.query;
|
|
259
|
+
if (!query) {
|
|
260
|
+
query = function (sql, params) { return _b().externalDb.query(sql, params); };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// order is optional — when wired, issueReceiptToken can look up the
|
|
264
|
+
// expected line items from order.getById so the operator doesn't
|
|
265
|
+
// have to re-supply them inline. Absent, the caller passes `lines`
|
|
266
|
+
// explicitly.
|
|
267
|
+
var orderPrim = opts.order || null;
|
|
268
|
+
if (orderPrim && typeof orderPrim.getById !== "function") {
|
|
269
|
+
throw new TypeError("stockReceipts.create: opts.order must expose a getById(id) method");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// loyaltyEarnRules is optional — when wired, completeReceipt fires
|
|
273
|
+
// a per_purchase award once the customer closes the checklist.
|
|
274
|
+
// Failures are drop-silent (the receipt is complete regardless of
|
|
275
|
+
// whether the loyalty award landed).
|
|
276
|
+
var loyaltyEarnRules = opts.loyaltyEarnRules || null;
|
|
277
|
+
if (loyaltyEarnRules && typeof loyaltyEarnRules.evaluateForEvent !== "function") {
|
|
278
|
+
throw new TypeError("stockReceipts.create: opts.loyaltyEarnRules must expose an evaluateForEvent(input) method");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ---- internal helpers -------------------------------------------------
|
|
282
|
+
|
|
283
|
+
async function _receiptRowById(id) {
|
|
284
|
+
var r = await query("SELECT * FROM stock_receipt_tokens WHERE id = ?1", [id]);
|
|
285
|
+
return r.rows[0] || null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function _receiptRowByOrder(orderId) {
|
|
289
|
+
var r = await query(
|
|
290
|
+
"SELECT * FROM stock_receipt_tokens WHERE order_id = ?1",
|
|
291
|
+
[orderId],
|
|
292
|
+
);
|
|
293
|
+
return r.rows[0] || null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function _receiptRowByTokenHash(hash) {
|
|
297
|
+
var r = await query(
|
|
298
|
+
"SELECT * FROM stock_receipt_tokens WHERE token_hash = ?1",
|
|
299
|
+
[hash],
|
|
300
|
+
);
|
|
301
|
+
return r.rows[0] || null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function _linesForReceipt(receiptId) {
|
|
305
|
+
var r = await query(
|
|
306
|
+
"SELECT * FROM stock_receipt_line_states WHERE receipt_id = ?1 ORDER BY sku ASC",
|
|
307
|
+
[receiptId],
|
|
308
|
+
);
|
|
309
|
+
return r.rows;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function _decodeReceipt(row, lines) {
|
|
313
|
+
if (!row) return null;
|
|
314
|
+
return {
|
|
315
|
+
id: row.id,
|
|
316
|
+
order_id: row.order_id,
|
|
317
|
+
status: row.status,
|
|
318
|
+
expires_at: Number(row.expires_at),
|
|
319
|
+
issued_at: Number(row.issued_at),
|
|
320
|
+
first_scanned_at: row.first_scanned_at != null ? Number(row.first_scanned_at) : null,
|
|
321
|
+
completed_at: row.completed_at != null ? Number(row.completed_at) : null,
|
|
322
|
+
created_at: Number(row.created_at),
|
|
323
|
+
updated_at: Number(row.updated_at),
|
|
324
|
+
lines: lines || [],
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function _decodeLine(row) {
|
|
329
|
+
return {
|
|
330
|
+
sku: row.sku,
|
|
331
|
+
quantity_expected: Number(row.quantity_expected),
|
|
332
|
+
quantity_received: Number(row.quantity_received),
|
|
333
|
+
quantity_damaged: Number(row.quantity_damaged),
|
|
334
|
+
state: row.state,
|
|
335
|
+
damage_reason: row.damage_reason,
|
|
336
|
+
updated_at: Number(row.updated_at),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function _decodeScan(row) {
|
|
341
|
+
return {
|
|
342
|
+
id: row.id,
|
|
343
|
+
receipt_id: row.receipt_id,
|
|
344
|
+
scanned_at: Number(row.scanned_at),
|
|
345
|
+
user_agent_hash: row.user_agent_hash,
|
|
346
|
+
client_ip_hash: row.client_ip_hash,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function _hydrate(row) {
|
|
351
|
+
if (!row) return null;
|
|
352
|
+
var lines = await _linesForReceipt(row.id);
|
|
353
|
+
return _decodeReceipt(row, lines.map(_decodeLine));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Per-row terminal-state guard. A `scanned` row whose expires_at has
|
|
357
|
+
// passed reads as terminal (refuse markLineReceived, etc.). The FSM
|
|
358
|
+
// never auto-flips the row to `expired` mid-call — that's a job for
|
|
359
|
+
// a future sweep — but the read-side guard prevents post-expiry
|
|
360
|
+
// mutations.
|
|
361
|
+
function _isLive(row, nowTs) {
|
|
362
|
+
if (row.status === "completed" || row.status === "expired") return false;
|
|
363
|
+
if (Number(row.expires_at) < nowTs) return false;
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ---- issueReceiptToken -----------------------------------------------
|
|
368
|
+
|
|
369
|
+
async function issueReceiptToken(input) {
|
|
370
|
+
if (!input || typeof input !== "object") {
|
|
371
|
+
throw new TypeError("stockReceipts.issueReceiptToken: input object required");
|
|
372
|
+
}
|
|
373
|
+
var orderId = _orderId(input.order_id);
|
|
374
|
+
var lines = _linesArray(input.lines);
|
|
375
|
+
var expiresHours = _expiresInHours(input.expires_in_hours);
|
|
376
|
+
|
|
377
|
+
// Re-issuance: an existing row for this order is overwritten in
|
|
378
|
+
// place (the prior token_hash is replaced atomically, the scan
|
|
379
|
+
// log persists). Refuse if the row is already `completed` — the
|
|
380
|
+
// customer's audit trail is closed.
|
|
381
|
+
var existing = await _receiptRowByOrder(orderId);
|
|
382
|
+
if (existing && existing.status === "completed") {
|
|
383
|
+
throw new TypeError("stockReceipts.issueReceiptToken: order " + orderId +
|
|
384
|
+
" has a completed receipt; no re-issuance");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
var id = existing ? existing.id : _b().uuid.v7();
|
|
388
|
+
var plaintext = _generateToken();
|
|
389
|
+
var tokenHash = _hashToken(plaintext);
|
|
390
|
+
var nowTs = _now();
|
|
391
|
+
var expiresAt = nowTs + (expiresHours * 60 * 60 * 1000);
|
|
392
|
+
|
|
393
|
+
if (existing) {
|
|
394
|
+
await query(
|
|
395
|
+
"UPDATE stock_receipt_tokens " +
|
|
396
|
+
"SET token_hash = ?1, status = 'issued', expires_at = ?2, " +
|
|
397
|
+
"first_scanned_at = NULL, completed_at = NULL, updated_at = ?3 " +
|
|
398
|
+
"WHERE id = ?4",
|
|
399
|
+
[tokenHash, expiresAt, nowTs, id],
|
|
400
|
+
);
|
|
401
|
+
// Replace the line states atomically — operators who re-issue
|
|
402
|
+
// after editing the packing slip get a fresh checklist.
|
|
403
|
+
await query("DELETE FROM stock_receipt_line_states WHERE receipt_id = ?1", [id]);
|
|
404
|
+
} else {
|
|
405
|
+
await query(
|
|
406
|
+
"INSERT INTO stock_receipt_tokens " +
|
|
407
|
+
"(id, order_id, token_hash, status, expires_at, issued_at, " +
|
|
408
|
+
" first_scanned_at, completed_at, created_at, updated_at) " +
|
|
409
|
+
"VALUES (?1, ?2, ?3, 'issued', ?4, ?5, NULL, NULL, ?5, ?5)",
|
|
410
|
+
[id, orderId, tokenHash, expiresAt, nowTs],
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
for (var i = 0; i < lines.length; i += 1) {
|
|
415
|
+
var ln = lines[i];
|
|
416
|
+
await query(
|
|
417
|
+
"INSERT INTO stock_receipt_line_states " +
|
|
418
|
+
"(receipt_id, sku, quantity_expected, quantity_received, quantity_damaged, " +
|
|
419
|
+
" state, damage_reason, updated_at) " +
|
|
420
|
+
"VALUES (?1, ?2, ?3, 0, 0, 'pending', NULL, ?4)",
|
|
421
|
+
[id, ln.sku, ln.quantity_expected, nowTs],
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Plaintext token is returned EXACTLY ONCE here. Subsequent reads
|
|
426
|
+
// of the receipt row never see it again — the storage column
|
|
427
|
+
// carries only the SHA3-512 namespace-hash. The picker embeds the
|
|
428
|
+
// plaintext in the QR rendered on the packing slip and discards it.
|
|
429
|
+
return {
|
|
430
|
+
receipt_id: id,
|
|
431
|
+
order_id: orderId,
|
|
432
|
+
plaintext_token: plaintext,
|
|
433
|
+
status: "issued",
|
|
434
|
+
expires_at: expiresAt,
|
|
435
|
+
issued_at: nowTs,
|
|
436
|
+
lines: lines.map(function (l) {
|
|
437
|
+
return {
|
|
438
|
+
sku: l.sku,
|
|
439
|
+
quantity_expected: l.quantity_expected,
|
|
440
|
+
quantity_received: 0,
|
|
441
|
+
quantity_damaged: 0,
|
|
442
|
+
state: "pending",
|
|
443
|
+
};
|
|
444
|
+
}),
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ---- recordReceiptScan -----------------------------------------------
|
|
449
|
+
|
|
450
|
+
async function recordReceiptScan(input) {
|
|
451
|
+
if (!input || typeof input !== "object") {
|
|
452
|
+
throw new TypeError("stockReceipts.recordReceiptScan: input object required");
|
|
453
|
+
}
|
|
454
|
+
var token = _canonicalToken(input.token);
|
|
455
|
+
var ua = _uaOpt(input.user_agent);
|
|
456
|
+
var ip = _ipOpt(input.client_ip);
|
|
457
|
+
|
|
458
|
+
var hash = _hashToken(token);
|
|
459
|
+
var receipt = await _receiptRowByTokenHash(hash);
|
|
460
|
+
if (!receipt) {
|
|
461
|
+
var miss = new Error("stockReceipts.recordReceiptScan: receipt not found");
|
|
462
|
+
miss.code = "STOCK_RECEIPT_NOT_FOUND";
|
|
463
|
+
throw miss;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Constant-time hex compare on the matched row's hash — belt-
|
|
467
|
+
// and-braces over the SQL = match.
|
|
468
|
+
if (!_b().crypto.timingSafeEqual(receipt.token_hash, hash)) {
|
|
469
|
+
var mismatch = new Error("stockReceipts.recordReceiptScan: receipt not found");
|
|
470
|
+
mismatch.code = "STOCK_RECEIPT_NOT_FOUND";
|
|
471
|
+
throw mismatch;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
var nowTs = _now();
|
|
475
|
+
if (receipt.status === "completed") {
|
|
476
|
+
var done = new Error("stockReceipts.recordReceiptScan: receipt already completed");
|
|
477
|
+
done.code = "STOCK_RECEIPT_COMPLETED";
|
|
478
|
+
throw done;
|
|
479
|
+
}
|
|
480
|
+
if (receipt.status === "expired" || Number(receipt.expires_at) < nowTs) {
|
|
481
|
+
var expired = new Error("stockReceipts.recordReceiptScan: receipt has expired");
|
|
482
|
+
expired.code = "STOCK_RECEIPT_EXPIRED";
|
|
483
|
+
throw expired;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
var scanId = _b().uuid.v7();
|
|
487
|
+
var uaHash = ua != null ? _b().crypto.namespaceHash(UA_NAMESPACE, ua) : null;
|
|
488
|
+
var ipHash = ip != null ? _b().crypto.namespaceHash(IP_NAMESPACE, ip) : null;
|
|
489
|
+
|
|
490
|
+
await query(
|
|
491
|
+
"INSERT INTO stock_receipt_scans " +
|
|
492
|
+
"(id, receipt_id, scanned_at, user_agent_hash, client_ip_hash) " +
|
|
493
|
+
"VALUES (?1, ?2, ?3, ?4, ?5)",
|
|
494
|
+
[scanId, receipt.id, nowTs, uaHash, ipHash],
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
// First scan flips the receipt FSM `issued -> scanned`. Subsequent
|
|
498
|
+
// scans append the event without re-flipping (the timestamp on
|
|
499
|
+
// `first_scanned_at` is sticky for audit purposes).
|
|
500
|
+
if (receipt.status === "issued") {
|
|
501
|
+
await query(
|
|
502
|
+
"UPDATE stock_receipt_tokens " +
|
|
503
|
+
"SET status = 'scanned', first_scanned_at = ?1, updated_at = ?1 " +
|
|
504
|
+
"WHERE id = ?2",
|
|
505
|
+
[nowTs, receipt.id],
|
|
506
|
+
);
|
|
507
|
+
} else {
|
|
508
|
+
await query(
|
|
509
|
+
"UPDATE stock_receipt_tokens SET updated_at = ?1 WHERE id = ?2",
|
|
510
|
+
[nowTs, receipt.id],
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
var fresh = await _hydrate(await _receiptRowById(receipt.id));
|
|
515
|
+
return {
|
|
516
|
+
scan_id: scanId,
|
|
517
|
+
receipt: fresh,
|
|
518
|
+
scanned_at: nowTs,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ---- markLineReceived ------------------------------------------------
|
|
523
|
+
|
|
524
|
+
async function markLineReceived(input) {
|
|
525
|
+
if (!input || typeof input !== "object") {
|
|
526
|
+
throw new TypeError("stockReceipts.markLineReceived: input object required");
|
|
527
|
+
}
|
|
528
|
+
var receiptId = _uuid(input.receipt_id, "receipt_id");
|
|
529
|
+
var sku = _sku(input.sku);
|
|
530
|
+
var qty = input.quantity_received == null
|
|
531
|
+
? null
|
|
532
|
+
: _nonNegInt(input.quantity_received, "quantity_received");
|
|
533
|
+
|
|
534
|
+
var nowTs = _now();
|
|
535
|
+
var receipt = await _receiptRowById(receiptId);
|
|
536
|
+
if (!receipt) {
|
|
537
|
+
throw new TypeError("stockReceipts.markLineReceived: receipt " + receiptId + " not found");
|
|
538
|
+
}
|
|
539
|
+
if (!_isLive(receipt, nowTs)) {
|
|
540
|
+
throw new TypeError("stockReceipts.markLineReceived: receipt status is " +
|
|
541
|
+
receipt.status + " (or expired); only scanned receipts can mark lines");
|
|
542
|
+
}
|
|
543
|
+
if (receipt.status !== "scanned") {
|
|
544
|
+
throw new TypeError("stockReceipts.markLineReceived: receipt status is " +
|
|
545
|
+
receipt.status + "; customer must scan the QR before marking lines");
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
var lineRow = (await query(
|
|
549
|
+
"SELECT * FROM stock_receipt_line_states WHERE receipt_id = ?1 AND sku = ?2",
|
|
550
|
+
[receiptId, sku],
|
|
551
|
+
)).rows[0];
|
|
552
|
+
if (!lineRow) {
|
|
553
|
+
throw new TypeError("stockReceipts.markLineReceived: sku " + JSON.stringify(sku) +
|
|
554
|
+
" is not in the receipt's line set");
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
var expected = Number(lineRow.quantity_expected);
|
|
558
|
+
var received = qty == null ? expected : qty;
|
|
559
|
+
if (received > expected) {
|
|
560
|
+
throw new TypeError("stockReceipts.markLineReceived: quantity_received " + received +
|
|
561
|
+
" exceeds quantity_expected " + expected + " for sku " + JSON.stringify(sku));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Mixed state: if the line already has damaged quantity recorded,
|
|
565
|
+
// received + damaged must not exceed expected, and the line state
|
|
566
|
+
// becomes `partial` rather than `received` (the line isn't fully
|
|
567
|
+
// received because some quantity was damaged).
|
|
568
|
+
var damaged = Number(lineRow.quantity_damaged);
|
|
569
|
+
if (received + damaged > expected) {
|
|
570
|
+
throw new TypeError("stockReceipts.markLineReceived: received " + received +
|
|
571
|
+
" + damaged " + damaged + " exceeds quantity_expected " + expected +
|
|
572
|
+
" for sku " + JSON.stringify(sku));
|
|
573
|
+
}
|
|
574
|
+
var newState;
|
|
575
|
+
if (damaged > 0 && received > 0) {
|
|
576
|
+
newState = "partial";
|
|
577
|
+
} else if (damaged > 0) {
|
|
578
|
+
// received == 0 and damaged > 0 — keep the line damaged
|
|
579
|
+
newState = "damaged";
|
|
580
|
+
} else {
|
|
581
|
+
newState = received === expected ? "received" : "partial";
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
await query(
|
|
585
|
+
"UPDATE stock_receipt_line_states " +
|
|
586
|
+
"SET quantity_received = ?1, state = ?2, updated_at = ?3 " +
|
|
587
|
+
"WHERE receipt_id = ?4 AND sku = ?5",
|
|
588
|
+
[received, newState, nowTs, receiptId, sku],
|
|
589
|
+
);
|
|
590
|
+
await query(
|
|
591
|
+
"UPDATE stock_receipt_tokens SET updated_at = ?1 WHERE id = ?2",
|
|
592
|
+
[nowTs, receiptId],
|
|
593
|
+
);
|
|
594
|
+
return await _hydrate(await _receiptRowById(receiptId));
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// ---- markLineDamaged -------------------------------------------------
|
|
598
|
+
|
|
599
|
+
async function markLineDamaged(input) {
|
|
600
|
+
if (!input || typeof input !== "object") {
|
|
601
|
+
throw new TypeError("stockReceipts.markLineDamaged: input object required");
|
|
602
|
+
}
|
|
603
|
+
var receiptId = _uuid(input.receipt_id, "receipt_id");
|
|
604
|
+
var sku = _sku(input.sku);
|
|
605
|
+
var qty = input.quantity_damaged == null
|
|
606
|
+
? null
|
|
607
|
+
: _nonNegInt(input.quantity_damaged, "quantity_damaged");
|
|
608
|
+
var reason = _reason(input.reason);
|
|
609
|
+
|
|
610
|
+
var nowTs = _now();
|
|
611
|
+
var receipt = await _receiptRowById(receiptId);
|
|
612
|
+
if (!receipt) {
|
|
613
|
+
throw new TypeError("stockReceipts.markLineDamaged: receipt " + receiptId + " not found");
|
|
614
|
+
}
|
|
615
|
+
if (!_isLive(receipt, nowTs)) {
|
|
616
|
+
throw new TypeError("stockReceipts.markLineDamaged: receipt status is " +
|
|
617
|
+
receipt.status + " (or expired); only scanned receipts can mark lines");
|
|
618
|
+
}
|
|
619
|
+
if (receipt.status !== "scanned") {
|
|
620
|
+
throw new TypeError("stockReceipts.markLineDamaged: receipt status is " +
|
|
621
|
+
receipt.status + "; customer must scan the QR before marking lines");
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
var lineRow = (await query(
|
|
625
|
+
"SELECT * FROM stock_receipt_line_states WHERE receipt_id = ?1 AND sku = ?2",
|
|
626
|
+
[receiptId, sku],
|
|
627
|
+
)).rows[0];
|
|
628
|
+
if (!lineRow) {
|
|
629
|
+
throw new TypeError("stockReceipts.markLineDamaged: sku " + JSON.stringify(sku) +
|
|
630
|
+
" is not in the receipt's line set");
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
var expected = Number(lineRow.quantity_expected);
|
|
634
|
+
var damaged = qty == null ? expected : qty;
|
|
635
|
+
if (damaged > expected) {
|
|
636
|
+
throw new TypeError("stockReceipts.markLineDamaged: quantity_damaged " + damaged +
|
|
637
|
+
" exceeds quantity_expected " + expected + " for sku " + JSON.stringify(sku));
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
var received = Number(lineRow.quantity_received);
|
|
641
|
+
if (received + damaged > expected) {
|
|
642
|
+
throw new TypeError("stockReceipts.markLineDamaged: received " + received +
|
|
643
|
+
" + damaged " + damaged + " exceeds quantity_expected " + expected +
|
|
644
|
+
" for sku " + JSON.stringify(sku));
|
|
645
|
+
}
|
|
646
|
+
var newState;
|
|
647
|
+
if (received > 0 && damaged > 0) {
|
|
648
|
+
newState = "partial";
|
|
649
|
+
} else if (received > 0) {
|
|
650
|
+
// damaged == 0 — keep the line received
|
|
651
|
+
newState = received === expected ? "received" : "partial";
|
|
652
|
+
} else {
|
|
653
|
+
newState = damaged === expected ? "damaged" : "partial";
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
await query(
|
|
657
|
+
"UPDATE stock_receipt_line_states " +
|
|
658
|
+
"SET quantity_damaged = ?1, damage_reason = ?2, state = ?3, updated_at = ?4 " +
|
|
659
|
+
"WHERE receipt_id = ?5 AND sku = ?6",
|
|
660
|
+
[damaged, reason, newState, nowTs, receiptId, sku],
|
|
661
|
+
);
|
|
662
|
+
await query(
|
|
663
|
+
"UPDATE stock_receipt_tokens SET updated_at = ?1 WHERE id = ?2",
|
|
664
|
+
[nowTs, receiptId],
|
|
665
|
+
);
|
|
666
|
+
return await _hydrate(await _receiptRowById(receiptId));
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// ---- completeReceipt -------------------------------------------------
|
|
670
|
+
|
|
671
|
+
async function completeReceipt(input) {
|
|
672
|
+
if (!input || typeof input !== "object") {
|
|
673
|
+
throw new TypeError("stockReceipts.completeReceipt: input object required");
|
|
674
|
+
}
|
|
675
|
+
var receiptId = _uuid(input.receipt_id, "receipt_id");
|
|
676
|
+
var customerId = input.customer_id != null
|
|
677
|
+
? _uuid(input.customer_id, "customer_id")
|
|
678
|
+
: null;
|
|
679
|
+
|
|
680
|
+
var nowTs = _now();
|
|
681
|
+
var receipt = await _receiptRowById(receiptId);
|
|
682
|
+
if (!receipt) {
|
|
683
|
+
throw new TypeError("stockReceipts.completeReceipt: receipt " + receiptId + " not found");
|
|
684
|
+
}
|
|
685
|
+
if (receipt.status === "completed") {
|
|
686
|
+
var done = new Error("stockReceipts.completeReceipt: receipt already completed");
|
|
687
|
+
done.code = "STOCK_RECEIPT_COMPLETED";
|
|
688
|
+
throw done;
|
|
689
|
+
}
|
|
690
|
+
if (receipt.status === "expired" || Number(receipt.expires_at) < nowTs) {
|
|
691
|
+
var expired = new Error("stockReceipts.completeReceipt: receipt has expired");
|
|
692
|
+
expired.code = "STOCK_RECEIPT_EXPIRED";
|
|
693
|
+
throw expired;
|
|
694
|
+
}
|
|
695
|
+
if (receipt.status !== "scanned") {
|
|
696
|
+
throw new TypeError("stockReceipts.completeReceipt: receipt status is " +
|
|
697
|
+
receipt.status + "; customer must scan before completing");
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
var lines = (await _linesForReceipt(receiptId)).map(_decodeLine);
|
|
701
|
+
|
|
702
|
+
await query(
|
|
703
|
+
"UPDATE stock_receipt_tokens " +
|
|
704
|
+
"SET status = 'completed', completed_at = ?1, updated_at = ?1 " +
|
|
705
|
+
"WHERE id = ?2",
|
|
706
|
+
[nowTs, receiptId],
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
var summary = {
|
|
710
|
+
total_lines: lines.length,
|
|
711
|
+
received_lines: 0,
|
|
712
|
+
damaged_lines: 0,
|
|
713
|
+
partial_lines: 0,
|
|
714
|
+
pending_lines: 0,
|
|
715
|
+
total_quantity_received: 0,
|
|
716
|
+
total_quantity_damaged: 0,
|
|
717
|
+
};
|
|
718
|
+
for (var i = 0; i < lines.length; i += 1) {
|
|
719
|
+
var ln = lines[i];
|
|
720
|
+
if (ln.state === "received") summary.received_lines += 1;
|
|
721
|
+
else if (ln.state === "damaged") summary.damaged_lines += 1;
|
|
722
|
+
else if (ln.state === "partial") summary.partial_lines += 1;
|
|
723
|
+
else summary.pending_lines += 1;
|
|
724
|
+
summary.total_quantity_received += ln.quantity_received;
|
|
725
|
+
summary.total_quantity_damaged += ln.quantity_damaged;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Compose loyaltyEarnRules — drop-silent on failure. The
|
|
729
|
+
// receipt is complete regardless of whether the loyalty award
|
|
730
|
+
// landed; an operator audit reads the loyalty_earn_log to find
|
|
731
|
+
// misses. We fire `per_purchase` keyed by the order_id so the
|
|
732
|
+
// dedup UNIQUE on (rule_slug, customer_id, trigger_event_ref)
|
|
733
|
+
// protects against re-completion (which can't happen anyway —
|
|
734
|
+
// completed is terminal — but defends a future re-completion
|
|
735
|
+
// path against double-award).
|
|
736
|
+
var loyaltyResult = null;
|
|
737
|
+
if (loyaltyEarnRules && customerId) {
|
|
738
|
+
try {
|
|
739
|
+
loyaltyResult = await loyaltyEarnRules.evaluateForEvent({
|
|
740
|
+
trigger: "per_purchase",
|
|
741
|
+
customer_id: customerId,
|
|
742
|
+
trigger_event_ref: "stock-receipt:" + receipt.order_id,
|
|
743
|
+
occurred_at: nowTs,
|
|
744
|
+
metadata: {
|
|
745
|
+
order_id: receipt.order_id,
|
|
746
|
+
receipt_id: receiptId,
|
|
747
|
+
received_lines: summary.received_lines,
|
|
748
|
+
damaged_lines: summary.damaged_lines,
|
|
749
|
+
total_quantity_received: summary.total_quantity_received,
|
|
750
|
+
},
|
|
751
|
+
});
|
|
752
|
+
} catch (_e) { /* drop-silent — loyalty failure must not roll back completion */ }
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
var fresh = await _hydrate(await _receiptRowById(receiptId));
|
|
756
|
+
return {
|
|
757
|
+
receipt: fresh,
|
|
758
|
+
summary: summary,
|
|
759
|
+
loyalty_result: loyaltyResult,
|
|
760
|
+
completed_at: nowTs,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// ---- getReceiptByToken -----------------------------------------------
|
|
765
|
+
|
|
766
|
+
async function getReceiptByToken(token) {
|
|
767
|
+
var canonical = _canonicalToken(token);
|
|
768
|
+
var hash = _hashToken(canonical);
|
|
769
|
+
var row = await _receiptRowByTokenHash(hash);
|
|
770
|
+
if (!row) return null;
|
|
771
|
+
if (!_b().crypto.timingSafeEqual(row.token_hash, hash)) return null;
|
|
772
|
+
return await _hydrate(row);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// ---- receiptsForOrder ------------------------------------------------
|
|
776
|
+
|
|
777
|
+
async function receiptsForOrder(orderId) {
|
|
778
|
+
var id = _orderId(orderId);
|
|
779
|
+
var row = await _receiptRowByOrder(id);
|
|
780
|
+
if (!row) return [];
|
|
781
|
+
return [await _hydrate(row)];
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// ---- recentScans -----------------------------------------------------
|
|
785
|
+
|
|
786
|
+
async function recentScans(listOpts) {
|
|
787
|
+
listOpts = listOpts || {};
|
|
788
|
+
var limit = _limit(listOpts.limit);
|
|
789
|
+
var rows;
|
|
790
|
+
if (listOpts.receipt_id != null) {
|
|
791
|
+
var rid = _uuid(listOpts.receipt_id, "receipt_id");
|
|
792
|
+
rows = (await query(
|
|
793
|
+
"SELECT * FROM stock_receipt_scans WHERE receipt_id = ?1 " +
|
|
794
|
+
"ORDER BY scanned_at DESC, id DESC LIMIT ?2",
|
|
795
|
+
[rid, limit],
|
|
796
|
+
)).rows;
|
|
797
|
+
} else {
|
|
798
|
+
rows = (await query(
|
|
799
|
+
"SELECT * FROM stock_receipt_scans " +
|
|
800
|
+
"ORDER BY scanned_at DESC, id DESC LIMIT ?1",
|
|
801
|
+
[limit],
|
|
802
|
+
)).rows;
|
|
803
|
+
}
|
|
804
|
+
var out = [];
|
|
805
|
+
for (var i = 0; i < rows.length; i += 1) out.push(_decodeScan(rows[i]));
|
|
806
|
+
return out;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
return {
|
|
810
|
+
RECEIPT_STATUSES: RECEIPT_STATUSES.slice(),
|
|
811
|
+
LINE_STATES: LINE_STATES.slice(),
|
|
812
|
+
TOKEN_NAMESPACE: TOKEN_NAMESPACE,
|
|
813
|
+
TOKEN_PLAINTEXT_LEN: TOKEN_PLAINTEXT_LEN,
|
|
814
|
+
DEFAULT_EXPIRES_HOURS: DEFAULT_EXPIRES_HOURS,
|
|
815
|
+
|
|
816
|
+
issueReceiptToken: issueReceiptToken,
|
|
817
|
+
recordReceiptScan: recordReceiptScan,
|
|
818
|
+
markLineReceived: markLineReceived,
|
|
819
|
+
markLineDamaged: markLineDamaged,
|
|
820
|
+
completeReceipt: completeReceipt,
|
|
821
|
+
getReceiptByToken: getReceiptByToken,
|
|
822
|
+
receiptsForOrder: receiptsForOrder,
|
|
823
|
+
recentScans: recentScans,
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
module.exports = {
|
|
828
|
+
create: create,
|
|
829
|
+
RECEIPT_STATUSES: RECEIPT_STATUSES,
|
|
830
|
+
LINE_STATES: LINE_STATES,
|
|
831
|
+
TOKEN_NAMESPACE: TOKEN_NAMESPACE,
|
|
832
|
+
TOKEN_PLAINTEXT_LEN: TOKEN_PLAINTEXT_LEN,
|
|
833
|
+
DEFAULT_EXPIRES_HOURS: DEFAULT_EXPIRES_HOURS,
|
|
834
|
+
};
|