@blamejs/blamejs-shop 0.0.54 → 0.0.57
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/addresses.js +430 -0
- package/lib/backorder.js +452 -0
- package/lib/bundles.js +587 -0
- package/lib/cart-abandonment.js +664 -0
- package/lib/currency-display.js +432 -0
- package/lib/email-suppressions.js +579 -0
- package/lib/fraud-screen.js +808 -0
- package/lib/index.js +22 -1
- package/lib/inventory-locations.js +774 -0
- package/lib/loyalty.js +496 -0
- package/lib/notifications.js +474 -0
- package/lib/order-export.js +724 -0
- package/lib/order-notes.js +563 -0
- package/lib/order-tracking.js +456 -0
- package/lib/payment-methods.js +522 -0
- package/lib/print-on-demand.js +709 -0
- package/lib/referrals.js +649 -0
- package/lib/returns.js +627 -0
- package/lib/save-for-later.js +667 -0
- package/lib/search-suggestions.js +528 -0
- package/lib/tax-exempt.js +519 -0
- package/lib/variants.js +726 -0
- package/lib/vendor/MANIFEST.json +1 -1
- package/package.json +1 -1
package/lib/backorder.js
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module shop.backorder
|
|
4
|
+
* @title Backorder — out-of-stock SKUs that can still be ordered
|
|
5
|
+
*
|
|
6
|
+
* @intro
|
|
7
|
+
* The catalog's inventory bucket says "we have N units on the
|
|
8
|
+
* shelf". A backorderable SKU says "we don't have it on the shelf,
|
|
9
|
+
* but the operator commits to ship by <date>" — and the storefront
|
|
10
|
+
* PDP renders "Ships by <date>" instead of "Out of stock" so the
|
|
11
|
+
* customer can still complete the purchase.
|
|
12
|
+
*
|
|
13
|
+
* Lifecycle:
|
|
14
|
+
*
|
|
15
|
+
* markBackorderable({ sku, max_backorder_quantity?,
|
|
16
|
+
* expected_ship_date, message? })
|
|
17
|
+
* → operator opts a SKU in. Upserts the per-SKU config row.
|
|
18
|
+
* `max_backorder_quantity: null` (or omitted) means
|
|
19
|
+
* unlimited; a positive integer caps the in-flight pending
|
|
20
|
+
* quantity so the operator doesn't commit beyond their
|
|
21
|
+
* supplier's capacity. `pending_quantity` is preserved across
|
|
22
|
+
* re-marks so re-marking the same SKU doesn't reset the
|
|
23
|
+
* counter.
|
|
24
|
+
*
|
|
25
|
+
* markNotBackorderable(sku)
|
|
26
|
+
* → flips `active` to 0. The row is preserved so historical
|
|
27
|
+
* lines still resolve their config; future `availabilityFor`
|
|
28
|
+
* calls return `out_of_stock` once stock_on_hand is also 0.
|
|
29
|
+
*
|
|
30
|
+
* recordBackorder({ order_id, sku, quantity, customer_id })
|
|
31
|
+
* → writes one `backorder_lines` row per backordered line at
|
|
32
|
+
* checkout time. Refuses if the SKU isn't currently
|
|
33
|
+
* backorderable, or if the requested quantity would push
|
|
34
|
+
* `pending_quantity` past the configured cap. Increments the
|
|
35
|
+
* per-SKU counter as part of the same logical operation.
|
|
36
|
+
* Idempotent on `(order_id, sku)` via the UNIQUE constraint —
|
|
37
|
+
* the same line replayed returns `{ status: "dedup" }` and
|
|
38
|
+
* does not double-increment the counter.
|
|
39
|
+
*
|
|
40
|
+
* fulfillBackorder({ order_id, sku }) /
|
|
41
|
+
* cancelBackorder({ order_id, sku, reason })
|
|
42
|
+
* → flip the row to `fulfilled` / `cancelled` and decrement
|
|
43
|
+
* the counter. Refuse on missing row or non-pending status
|
|
44
|
+
* so the counter can never under-flow.
|
|
45
|
+
*
|
|
46
|
+
* availabilityFor(sku) — pure read used by the PDP
|
|
47
|
+
* → returns one of:
|
|
48
|
+
* { status: "in_stock" }
|
|
49
|
+
* { status: "backorderable",
|
|
50
|
+
* expected_ship_date, message }
|
|
51
|
+
* { status: "out_of_stock" }
|
|
52
|
+
* Reads catalog inventory + the per-SKU backorder config;
|
|
53
|
+
* `in_stock` wins whenever stock_on_hand > 0 (the
|
|
54
|
+
* backorderable flag is irrelevant — fulfill from the shelf
|
|
55
|
+
* first).
|
|
56
|
+
*
|
|
57
|
+
* customerBackorders(customer_id) /
|
|
58
|
+
* pendingForSku(sku) /
|
|
59
|
+
* arrivalsThisWeek()
|
|
60
|
+
* → operator + customer dashboard reads.
|
|
61
|
+
*
|
|
62
|
+
* Composition:
|
|
63
|
+
* - b.guardUuid — every order_id / customer_id is UUID-shape
|
|
64
|
+
* validated at the entry point; SKU shape is validated via a
|
|
65
|
+
* local regex matching the rest of the shop primitives.
|
|
66
|
+
* - b.uuid.v7 — backorder_lines.id (sortable; customer dashboard
|
|
67
|
+
* reads sort by id desc to get newest-first without a second
|
|
68
|
+
* index).
|
|
69
|
+
* - catalog.inventory.get(sku) — the single source of truth for
|
|
70
|
+
* on-shelf stock. `availabilityFor` reads it but never mutates
|
|
71
|
+
* it; the catalog stays the owner of stock_on_hand.
|
|
72
|
+
*
|
|
73
|
+
* The factory accepts an optional `query` (defaults to
|
|
74
|
+
* b.externalDb.query) and a required `catalog` handle so the
|
|
75
|
+
* primitive stays decoupled from any specific catalog binding —
|
|
76
|
+
* tests inject an in-memory-SQLite-backed catalog; production wires
|
|
77
|
+
* the real catalog created at boot.
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
var bShop;
|
|
81
|
+
function _b() {
|
|
82
|
+
if (!bShop) bShop = require("./index");
|
|
83
|
+
return bShop.framework;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---- constants ----------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
var SKU_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
89
|
+
var MAX_MESSAGE_LEN = 280;
|
|
90
|
+
var MAX_REASON_LEN = 280;
|
|
91
|
+
var WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
92
|
+
|
|
93
|
+
var BACKORDER_STATUSES = Object.freeze(["pending", "fulfilled", "cancelled"]);
|
|
94
|
+
|
|
95
|
+
// ---- validators ---------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
function _uuid(s, label) {
|
|
98
|
+
try {
|
|
99
|
+
return _b().guardUuid.sanitize(s, { profile: "strict" });
|
|
100
|
+
} catch (e) {
|
|
101
|
+
throw new TypeError("backorder: " + label + " — " + (e && e.message || "invalid UUID"));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function _sku(s) {
|
|
106
|
+
if (typeof s !== "string" || !SKU_RE.test(s)) {
|
|
107
|
+
throw new TypeError("backorder: sku must match /^[A-Za-z0-9][A-Za-z0-9._-]*$/ (alnum + . _ -, ≤ 128 chars)");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function _shortText(s, label, max) {
|
|
112
|
+
if (s == null) return "";
|
|
113
|
+
if (typeof s !== "string" || s.length > max) {
|
|
114
|
+
throw new TypeError("backorder: " + label + " must be a string ≤ " + max + " chars");
|
|
115
|
+
}
|
|
116
|
+
return s;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function _positiveInt(n, label) {
|
|
120
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
121
|
+
throw new TypeError("backorder: " + label + " must be a positive integer");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function _nonNegIntOrNull(n, label) {
|
|
126
|
+
if (n === null || n === undefined) return null;
|
|
127
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
128
|
+
throw new TypeError("backorder: " + label + " must be a non-negative integer or null");
|
|
129
|
+
}
|
|
130
|
+
return n;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function _epochMs(n, label) {
|
|
134
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
135
|
+
throw new TypeError("backorder: " + label + " must be a non-negative integer (epoch ms)");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function _now() { return Date.now(); }
|
|
140
|
+
|
|
141
|
+
// ---- factory ------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
function create(opts) {
|
|
144
|
+
opts = opts || {};
|
|
145
|
+
if (!opts.catalog || !opts.catalog.inventory || typeof opts.catalog.inventory.get !== "function") {
|
|
146
|
+
throw new TypeError("backorder.create: opts.catalog with inventory.get(sku) required");
|
|
147
|
+
}
|
|
148
|
+
var catalog = opts.catalog;
|
|
149
|
+
var query = opts.query;
|
|
150
|
+
if (!query) {
|
|
151
|
+
query = function (sql, params) { return _b().externalDb.query(sql, params); };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Read the per-SKU config row. Returns null on miss so callers can
|
|
155
|
+
// map cleanly to the "not configured" branch without an exception
|
|
156
|
+
// round-trip.
|
|
157
|
+
async function _getConfig(sku) {
|
|
158
|
+
var r = await query("SELECT * FROM backorder_skus WHERE sku = ?1", [sku]);
|
|
159
|
+
return r.rows[0] || null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
// Operator opts a SKU in. Upserts the config row so re-marking
|
|
164
|
+
// updates expected_ship_date / message / cap without resetting
|
|
165
|
+
// the counter. `max_backorder_quantity: null` (or omitted) means
|
|
166
|
+
// unlimited — operators that want a strict cap pass a positive
|
|
167
|
+
// integer; passing 0 means "no new backorders accepted" (the
|
|
168
|
+
// existing pending rows remain).
|
|
169
|
+
markBackorderable: async function (input) {
|
|
170
|
+
if (!input || typeof input !== "object") {
|
|
171
|
+
throw new TypeError("backorder.markBackorderable: input object required");
|
|
172
|
+
}
|
|
173
|
+
_sku(input.sku);
|
|
174
|
+
var max = _nonNegIntOrNull(input.max_backorder_quantity, "max_backorder_quantity");
|
|
175
|
+
_epochMs(input.expected_ship_date, "expected_ship_date");
|
|
176
|
+
var message = _shortText(input.message, "message", MAX_MESSAGE_LEN);
|
|
177
|
+
var ts = _now();
|
|
178
|
+
var existing = await _getConfig(input.sku);
|
|
179
|
+
if (!existing) {
|
|
180
|
+
await query(
|
|
181
|
+
"INSERT INTO backorder_skus (sku, max_quantity, expected_ship_date, message, " +
|
|
182
|
+
"pending_quantity, active, created_at, updated_at) " +
|
|
183
|
+
"VALUES (?1, ?2, ?3, ?4, 0, 1, ?5, ?5)",
|
|
184
|
+
[input.sku, max, input.expected_ship_date, message, ts],
|
|
185
|
+
);
|
|
186
|
+
} else {
|
|
187
|
+
await query(
|
|
188
|
+
"UPDATE backorder_skus SET max_quantity = ?1, expected_ship_date = ?2, " +
|
|
189
|
+
"message = ?3, active = 1, updated_at = ?4 WHERE sku = ?5",
|
|
190
|
+
[max, input.expected_ship_date, message, ts, input.sku],
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
return await _getConfig(input.sku);
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
// Flip a SKU's backorder config off. The row is preserved so the
|
|
197
|
+
// pending_quantity counter + historical line resolution still
|
|
198
|
+
// work; future availabilityFor calls fall through to in_stock /
|
|
199
|
+
// out_of_stock without the backorderable branch.
|
|
200
|
+
markNotBackorderable: async function (sku) {
|
|
201
|
+
_sku(sku);
|
|
202
|
+
var ts = _now();
|
|
203
|
+
var r = await query(
|
|
204
|
+
"UPDATE backorder_skus SET active = 0, updated_at = ?1 WHERE sku = ?2",
|
|
205
|
+
[ts, sku],
|
|
206
|
+
);
|
|
207
|
+
if (r.rowCount === 0) return null;
|
|
208
|
+
return await _getConfig(sku);
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
getStatus: async function (sku) {
|
|
212
|
+
_sku(sku);
|
|
213
|
+
return await _getConfig(sku);
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
listBackorderable: async function (listOpts) {
|
|
217
|
+
listOpts = listOpts || {};
|
|
218
|
+
var activeOnly = listOpts.active_only === true;
|
|
219
|
+
var sql, params;
|
|
220
|
+
if (activeOnly) {
|
|
221
|
+
sql = "SELECT * FROM backorder_skus WHERE active = 1 ORDER BY expected_ship_date ASC, sku ASC";
|
|
222
|
+
params = [];
|
|
223
|
+
} else {
|
|
224
|
+
sql = "SELECT * FROM backorder_skus ORDER BY expected_ship_date ASC, sku ASC";
|
|
225
|
+
params = [];
|
|
226
|
+
}
|
|
227
|
+
var r = await query(sql, params);
|
|
228
|
+
return r.rows;
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
// Record one backorder line at checkout time. Refuses if:
|
|
232
|
+
// - the SKU isn't currently backorderable (no config / active=0)
|
|
233
|
+
// - the quantity would push pending_quantity past max_quantity
|
|
234
|
+
// Idempotent on (order_id, sku) via the UNIQUE constraint — the
|
|
235
|
+
// same line replayed returns { status: 'dedup' } and does not
|
|
236
|
+
// double-increment the counter.
|
|
237
|
+
recordBackorder: async function (input) {
|
|
238
|
+
if (!input || typeof input !== "object") {
|
|
239
|
+
throw new TypeError("backorder.recordBackorder: input object required");
|
|
240
|
+
}
|
|
241
|
+
_uuid(input.order_id, "order_id");
|
|
242
|
+
_uuid(input.customer_id, "customer_id");
|
|
243
|
+
_sku(input.sku);
|
|
244
|
+
_positiveInt(input.quantity, "quantity");
|
|
245
|
+
|
|
246
|
+
// Idempotency — same (order_id, sku) returns dedup without
|
|
247
|
+
// mutating the counter. The UNIQUE constraint backstops a race
|
|
248
|
+
// window where two callers raced past the SELECT below. Dedup
|
|
249
|
+
// is checked BEFORE the cap so a replay of an already-recorded
|
|
250
|
+
// line doesn't trip the cap refusal when the existing pending
|
|
251
|
+
// total is at the cap.
|
|
252
|
+
var dup = await query(
|
|
253
|
+
"SELECT id, status FROM backorder_lines WHERE order_id = ?1 AND sku = ?2 LIMIT 1",
|
|
254
|
+
[input.order_id, input.sku],
|
|
255
|
+
);
|
|
256
|
+
if (dup.rows.length) {
|
|
257
|
+
return { id: dup.rows[0].id, status: "dedup", line_status: dup.rows[0].status };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
var config = await _getConfig(input.sku);
|
|
261
|
+
if (!config || config.active !== 1) {
|
|
262
|
+
throw new TypeError("backorder.recordBackorder: sku " + JSON.stringify(input.sku) + " is not currently backorderable");
|
|
263
|
+
}
|
|
264
|
+
// Cap enforcement. NULL max_quantity = unlimited; a configured
|
|
265
|
+
// cap refuses any line that would push the in-flight pending
|
|
266
|
+
// total past it.
|
|
267
|
+
if (config.max_quantity !== null && config.max_quantity !== undefined) {
|
|
268
|
+
if (config.pending_quantity + input.quantity > config.max_quantity) {
|
|
269
|
+
throw new TypeError("backorder.recordBackorder: would exceed max_backorder_quantity " +
|
|
270
|
+
"(cap=" + config.max_quantity + ", pending=" + config.pending_quantity +
|
|
271
|
+
", requested=" + input.quantity + ")");
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
var id = _b().uuid.v7();
|
|
276
|
+
var ts = _now();
|
|
277
|
+
try {
|
|
278
|
+
await query(
|
|
279
|
+
"INSERT INTO backorder_lines (id, order_id, customer_id, sku, quantity, status, " +
|
|
280
|
+
"created_at) VALUES (?1, ?2, ?3, ?4, ?5, 'pending', ?6)",
|
|
281
|
+
[id, input.order_id, input.customer_id, input.sku, input.quantity, ts],
|
|
282
|
+
);
|
|
283
|
+
} catch (e) {
|
|
284
|
+
// Race: another caller landed the same (order_id, sku) tuple
|
|
285
|
+
// between the dup SELECT above and this INSERT. Re-read the
|
|
286
|
+
// existing row and return the dedup shape instead of
|
|
287
|
+
// bubbling a raw SQLITE_CONSTRAINT.
|
|
288
|
+
var redup = await query(
|
|
289
|
+
"SELECT id, status FROM backorder_lines WHERE order_id = ?1 AND sku = ?2 LIMIT 1",
|
|
290
|
+
[input.order_id, input.sku],
|
|
291
|
+
);
|
|
292
|
+
if (redup.rows.length) {
|
|
293
|
+
return { id: redup.rows[0].id, status: "dedup", line_status: redup.rows[0].status };
|
|
294
|
+
}
|
|
295
|
+
throw e;
|
|
296
|
+
}
|
|
297
|
+
await query(
|
|
298
|
+
"UPDATE backorder_skus SET pending_quantity = pending_quantity + ?1, updated_at = ?2 " +
|
|
299
|
+
"WHERE sku = ?3",
|
|
300
|
+
[input.quantity, ts, input.sku],
|
|
301
|
+
);
|
|
302
|
+
return { id: id, status: "recorded" };
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
// Flip a pending backorder line to fulfilled. Decrements the
|
|
306
|
+
// per-SKU counter. Refuses on missing row or non-pending status
|
|
307
|
+
// so the counter can never under-flow.
|
|
308
|
+
fulfillBackorder: async function (input) {
|
|
309
|
+
if (!input || typeof input !== "object") {
|
|
310
|
+
throw new TypeError("backorder.fulfillBackorder: input object required");
|
|
311
|
+
}
|
|
312
|
+
_uuid(input.order_id, "order_id");
|
|
313
|
+
_sku(input.sku);
|
|
314
|
+
var r = await query(
|
|
315
|
+
"SELECT id, quantity, status FROM backorder_lines WHERE order_id = ?1 AND sku = ?2 LIMIT 1",
|
|
316
|
+
[input.order_id, input.sku],
|
|
317
|
+
);
|
|
318
|
+
if (!r.rows.length) {
|
|
319
|
+
throw new TypeError("backorder.fulfillBackorder: no backorder line for order=" +
|
|
320
|
+
input.order_id + " sku=" + JSON.stringify(input.sku));
|
|
321
|
+
}
|
|
322
|
+
var line = r.rows[0];
|
|
323
|
+
if (line.status !== "pending") {
|
|
324
|
+
throw new TypeError("backorder.fulfillBackorder: line is " + line.status +
|
|
325
|
+
", only pending lines can be fulfilled");
|
|
326
|
+
}
|
|
327
|
+
var ts = _now();
|
|
328
|
+
await query(
|
|
329
|
+
"UPDATE backorder_lines SET status = 'fulfilled', fulfilled_at = ?1 WHERE id = ?2",
|
|
330
|
+
[ts, line.id],
|
|
331
|
+
);
|
|
332
|
+
// MAX(0, ...) clamps the counter so an out-of-band counter
|
|
333
|
+
// corruption (operator hand-edited a row) can't drive it
|
|
334
|
+
// negative. The CHECK constraint on pending_quantity would
|
|
335
|
+
// otherwise refuse the UPDATE outright.
|
|
336
|
+
await query(
|
|
337
|
+
"UPDATE backorder_skus SET pending_quantity = MAX(0, pending_quantity - ?1), " +
|
|
338
|
+
"updated_at = ?2 WHERE sku = ?3",
|
|
339
|
+
[line.quantity, ts, input.sku],
|
|
340
|
+
);
|
|
341
|
+
return { id: line.id, status: "fulfilled" };
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
cancelBackorder: async function (input) {
|
|
345
|
+
if (!input || typeof input !== "object") {
|
|
346
|
+
throw new TypeError("backorder.cancelBackorder: input object required");
|
|
347
|
+
}
|
|
348
|
+
_uuid(input.order_id, "order_id");
|
|
349
|
+
_sku(input.sku);
|
|
350
|
+
var reason = _shortText(input.reason, "reason", MAX_REASON_LEN);
|
|
351
|
+
var r = await query(
|
|
352
|
+
"SELECT id, quantity, status FROM backorder_lines WHERE order_id = ?1 AND sku = ?2 LIMIT 1",
|
|
353
|
+
[input.order_id, input.sku],
|
|
354
|
+
);
|
|
355
|
+
if (!r.rows.length) {
|
|
356
|
+
throw new TypeError("backorder.cancelBackorder: no backorder line for order=" +
|
|
357
|
+
input.order_id + " sku=" + JSON.stringify(input.sku));
|
|
358
|
+
}
|
|
359
|
+
var line = r.rows[0];
|
|
360
|
+
if (line.status !== "pending") {
|
|
361
|
+
throw new TypeError("backorder.cancelBackorder: line is " + line.status +
|
|
362
|
+
", only pending lines can be cancelled");
|
|
363
|
+
}
|
|
364
|
+
var ts = _now();
|
|
365
|
+
await query(
|
|
366
|
+
"UPDATE backorder_lines SET status = 'cancelled', reason = ?1, cancelled_at = ?2 " +
|
|
367
|
+
"WHERE id = ?3",
|
|
368
|
+
[reason, ts, line.id],
|
|
369
|
+
);
|
|
370
|
+
await query(
|
|
371
|
+
"UPDATE backorder_skus SET pending_quantity = MAX(0, pending_quantity - ?1), " +
|
|
372
|
+
"updated_at = ?2 WHERE sku = ?3",
|
|
373
|
+
[line.quantity, ts, input.sku],
|
|
374
|
+
);
|
|
375
|
+
return { id: line.id, status: "cancelled" };
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
// Pure read used by the PDP. Resolution rules:
|
|
379
|
+
// 1. stock_on_hand > 0 → in_stock (fulfill from shelf
|
|
380
|
+
// first; backorderable flag
|
|
381
|
+
// is irrelevant)
|
|
382
|
+
// 2. config active=1 + stock = 0 → backorderable + ship date +
|
|
383
|
+
// message
|
|
384
|
+
// 3. otherwise → out_of_stock
|
|
385
|
+
// The cap is not enforced here — availabilityFor returns the
|
|
386
|
+
// configured ship date even when the cap is full; the cap refusal
|
|
387
|
+
// surfaces at recordBackorder time so the customer sees the same
|
|
388
|
+
// PDP state until they actually try to commit.
|
|
389
|
+
availabilityFor: async function (sku) {
|
|
390
|
+
_sku(sku);
|
|
391
|
+
var inv = await catalog.inventory.get(sku);
|
|
392
|
+
var onHand = inv && inv.stock_on_hand != null ? inv.stock_on_hand : 0;
|
|
393
|
+
if (onHand > 0) {
|
|
394
|
+
return { status: "in_stock" };
|
|
395
|
+
}
|
|
396
|
+
var config = await _getConfig(sku);
|
|
397
|
+
if (config && config.active === 1) {
|
|
398
|
+
return {
|
|
399
|
+
status: "backorderable",
|
|
400
|
+
expected_ship_date: config.expected_ship_date,
|
|
401
|
+
message: config.message,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
return { status: "out_of_stock" };
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
// Newest first — the customer's account page renders the most
|
|
408
|
+
// recent backorder at the top. The v7-uuid PK sorts
|
|
409
|
+
// lexicographically by creation order so `ORDER BY id DESC` is
|
|
410
|
+
// equivalent to `ORDER BY created_at DESC` without needing the
|
|
411
|
+
// separate timestamp index.
|
|
412
|
+
customerBackorders: async function (customerId) {
|
|
413
|
+
_uuid(customerId, "customer_id");
|
|
414
|
+
var r = await query(
|
|
415
|
+
"SELECT * FROM backorder_lines WHERE customer_id = ?1 ORDER BY id DESC",
|
|
416
|
+
[customerId],
|
|
417
|
+
);
|
|
418
|
+
return r.rows;
|
|
419
|
+
},
|
|
420
|
+
|
|
421
|
+
pendingForSku: async function (sku) {
|
|
422
|
+
_sku(sku);
|
|
423
|
+
var r = await query(
|
|
424
|
+
"SELECT * FROM backorder_lines WHERE sku = ?1 AND status = 'pending' ORDER BY id ASC",
|
|
425
|
+
[sku],
|
|
426
|
+
);
|
|
427
|
+
return r.rows;
|
|
428
|
+
},
|
|
429
|
+
|
|
430
|
+
// Operator dashboard read — pending backorder lines whose
|
|
431
|
+
// configured expected_ship_date falls in the next 7 days
|
|
432
|
+
// (inclusive of now). The join pulls the per-SKU ship date so
|
|
433
|
+
// operators don't have to fan out one read per sku.
|
|
434
|
+
arrivalsThisWeek: async function () {
|
|
435
|
+
var now = _now();
|
|
436
|
+
var cutoff = now + WEEK_MS;
|
|
437
|
+
var r = await query(
|
|
438
|
+
"SELECT l.*, s.expected_ship_date AS expected_ship_date, s.message AS message " +
|
|
439
|
+
"FROM backorder_lines l JOIN backorder_skus s ON s.sku = l.sku " +
|
|
440
|
+
"WHERE l.status = 'pending' AND s.expected_ship_date <= ?1 " +
|
|
441
|
+
"ORDER BY s.expected_ship_date ASC, l.id ASC",
|
|
442
|
+
[cutoff],
|
|
443
|
+
);
|
|
444
|
+
return r.rows;
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
module.exports = {
|
|
450
|
+
create: create,
|
|
451
|
+
BACKORDER_STATUSES: BACKORDER_STATUSES,
|
|
452
|
+
};
|