@blamejs/blamejs-shop 0.0.53 → 0.0.56
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/analytics.js +400 -0
- package/lib/cart-abandonment.js +664 -0
- package/lib/currency-display.js +432 -0
- package/lib/email-suppressions.js +579 -0
- package/lib/email.js +264 -0
- package/lib/index.js +14 -0
- package/lib/inventory-receive.js +494 -0
- package/lib/loyalty.js +496 -0
- package/lib/newsletter.js +176 -12
- package/lib/notifications.js +474 -0
- package/lib/order-tracking.js +456 -0
- package/lib/payment.js +193 -13
- package/lib/referrals.js +649 -0
- package/lib/returns.js +627 -0
- package/lib/reviews.js +412 -0
- package/lib/search-suggestions.js +528 -0
- package/lib/tax-exempt.js +519 -0
- package/lib/tax.js +391 -3
- package/lib/vendor/MANIFEST.json +1 -1
- package/lib/webhooks.js +293 -16
- package/lib/wishlist.js +269 -0
- package/package.json +1 -1
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module shop.orderTracking
|
|
4
|
+
* @title Order tracking — shipment + carrier-event ledger
|
|
5
|
+
*
|
|
6
|
+
* @intro
|
|
7
|
+
* Once an order is paid + fulfilling, the operator hands the package
|
|
8
|
+
* to a carrier. The tracking primitive is the post-handoff ledger:
|
|
9
|
+
* one `shipments` row per parcel (split shipments allowed — a single
|
|
10
|
+
* order can produce multiple shipments when items ship from
|
|
11
|
+
* different warehouses or one item backorders), and one
|
|
12
|
+
* `shipment_events` row per carrier scan / operator-recorded status
|
|
13
|
+
* change.
|
|
14
|
+
*
|
|
15
|
+
* Lifecycle (see migration 0021_shipments.sql for the storage CHECK):
|
|
16
|
+
*
|
|
17
|
+
* pending — row created, no carrier label yet
|
|
18
|
+
* label-created — operator generated the shipping label
|
|
19
|
+
* in-transit — carrier picked up
|
|
20
|
+
* out-for-delivery — carrier final-mile underway
|
|
21
|
+
* delivered — confirmed delivery
|
|
22
|
+
* exception — carrier-reported problem
|
|
23
|
+
* returned — package en route back / received back
|
|
24
|
+
*
|
|
25
|
+
* `recordEvent` is the operator-facing append-and-update — it adds
|
|
26
|
+
* a row to `shipment_events` and updates `shipments.status` to the
|
|
27
|
+
* event's status. Idempotent on duplicate `(shipment_id, status,
|
|
28
|
+
* occurred_at)` triples so a carrier webhook that fires twice for
|
|
29
|
+
* the same scan doesn't double-write.
|
|
30
|
+
*
|
|
31
|
+
* `markShipped` + `markDelivered` are the two thin convenience
|
|
32
|
+
* wrappers around `recordEvent` that the checkout / fulfillment
|
|
33
|
+
* flow uses. When `order` is wired into the factory, `markDelivered`
|
|
34
|
+
* ALSO drives the parent order through `order.transition(...,
|
|
35
|
+
* "mark_delivered")` so the order FSM reflects fulfillment without
|
|
36
|
+
* a second operator call.
|
|
37
|
+
*
|
|
38
|
+
* `trackingUrl` is a pure helper — given a carrier code and tracking
|
|
39
|
+
* number, return the well-known carrier tracking URL or null. The
|
|
40
|
+
* templates are public-knowledge carrier URLs (UPS / FedEx / USPS /
|
|
41
|
+
* DHL / Royal Mail / Canada Post / Australia Post). 'other' returns
|
|
42
|
+
* null — the operator surfaces a carrier-specific URL out of band.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
var bShop;
|
|
46
|
+
function _b() {
|
|
47
|
+
if (!bShop) bShop = require("./index");
|
|
48
|
+
return bShop.framework;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---- constants ----------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
var CARRIERS = Object.freeze([
|
|
54
|
+
"ups",
|
|
55
|
+
"fedex",
|
|
56
|
+
"usps",
|
|
57
|
+
"dhl",
|
|
58
|
+
"royal-mail",
|
|
59
|
+
"canada-post",
|
|
60
|
+
"australia-post",
|
|
61
|
+
"other",
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
var STATUSES = Object.freeze([
|
|
65
|
+
"pending",
|
|
66
|
+
"label-created",
|
|
67
|
+
"in-transit",
|
|
68
|
+
"out-for-delivery",
|
|
69
|
+
"delivered",
|
|
70
|
+
"exception",
|
|
71
|
+
"returned",
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
var MAX_TRACKING_LEN = 64;
|
|
75
|
+
var MAX_LOCATION_LEN = 128;
|
|
76
|
+
var MAX_DETAIL_LEN = 512;
|
|
77
|
+
var MAX_NOTES_LEN = 2048;
|
|
78
|
+
var MAX_CARRIER_OTHER_LEN = 64;
|
|
79
|
+
|
|
80
|
+
// Carrier-specific tracking URL templates. Each function returns the
|
|
81
|
+
// well-known public URL for the given tracking number, already
|
|
82
|
+
// encoded. Kept as plain functions (not a `${...}` template lookup)
|
|
83
|
+
// so the templates that need a different query-param shape — Royal
|
|
84
|
+
// Mail uses `trackNumber`, USPS uses `qtc_tLabels1`, Canada Post uses
|
|
85
|
+
// `trackingNumber` — stay easy to read in source.
|
|
86
|
+
var CARRIER_URL = {
|
|
87
|
+
"ups": function (n) {
|
|
88
|
+
return "https://www.ups.com/track?tracknum=" + encodeURIComponent(n);
|
|
89
|
+
},
|
|
90
|
+
"fedex": function (n) {
|
|
91
|
+
return "https://www.fedex.com/fedextrack/?trknbr=" + encodeURIComponent(n);
|
|
92
|
+
},
|
|
93
|
+
"usps": function (n) {
|
|
94
|
+
return "https://tools.usps.com/go/TrackConfirmAction?qtc_tLabels1=" + encodeURIComponent(n);
|
|
95
|
+
},
|
|
96
|
+
"dhl": function (n) {
|
|
97
|
+
return "https://www.dhl.com/global-en/home/tracking.html?tracking-id=" + encodeURIComponent(n);
|
|
98
|
+
},
|
|
99
|
+
"royal-mail": function (n) {
|
|
100
|
+
return "https://www.royalmail.com/track-your-item#/tracking-results/" + encodeURIComponent(n);
|
|
101
|
+
},
|
|
102
|
+
"canada-post": function (n) {
|
|
103
|
+
return "https://www.canadapost-postescanada.ca/track-reperage/en#/details/" + encodeURIComponent(n);
|
|
104
|
+
},
|
|
105
|
+
"australia-post": function (n) {
|
|
106
|
+
return "https://auspost.com.au/mypost/track/#/details/" + encodeURIComponent(n);
|
|
107
|
+
},
|
|
108
|
+
"other": function () { return null; },
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// ---- validators ---------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
function _uuid(s, label) {
|
|
114
|
+
try { return _b().guardUuid.sanitize(s, { profile: "strict" }); }
|
|
115
|
+
catch (e) { throw new TypeError("order-tracking: " + label + " — " + (e && e.message || "invalid UUID")); }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function _carrier(c) {
|
|
119
|
+
if (typeof c !== "string" || CARRIERS.indexOf(c) === -1) {
|
|
120
|
+
throw new TypeError("order-tracking: carrier must be one of " + CARRIERS.join(", "));
|
|
121
|
+
}
|
|
122
|
+
return c;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function _status(s) {
|
|
126
|
+
if (typeof s !== "string" || STATUSES.indexOf(s) === -1) {
|
|
127
|
+
throw new TypeError("order-tracking: status must be one of " + STATUSES.join(", "));
|
|
128
|
+
}
|
|
129
|
+
return s;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function _nonNegInt(n, label) {
|
|
133
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
134
|
+
throw new TypeError("order-tracking: " + label + " must be a non-negative integer");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function _currency(c) {
|
|
139
|
+
if (typeof c !== "string" || !/^[A-Z]{3}$/.test(c)) {
|
|
140
|
+
throw new TypeError("order-tracking: cost_currency must be 3-letter uppercase ISO 4217");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Codepoint scan refuses every C0 control byte (0x00..0x1f) + DEL
|
|
145
|
+
// (0x7f). Implemented as a `charCodeAt` walk rather than a regex
|
|
146
|
+
// literal so the `no-control-regex` ESLint rule stays clean — same
|
|
147
|
+
// approach blamejs uses in `lib/auth/ciba.js` + `lib/auth/step-up.js`.
|
|
148
|
+
function _hasStrictControlByte(s) {
|
|
149
|
+
for (var i = 0; i < s.length; i += 1) {
|
|
150
|
+
var cc = s.charCodeAt(i);
|
|
151
|
+
if (cc <= 0x1f || cc === 0x7f) return true;
|
|
152
|
+
}
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Same scan but tolerant of tab / LF / CR — operator notes legitimately
|
|
157
|
+
// span lines, and a paste from a carrier portal that lands a `\r\n`
|
|
158
|
+
// pair shouldn't trip the refusal.
|
|
159
|
+
function _hasFreeTextControlByte(s) {
|
|
160
|
+
for (var i = 0; i < s.length; i += 1) {
|
|
161
|
+
var cc = s.charCodeAt(i);
|
|
162
|
+
if (cc === 0x09 || cc === 0x0a || cc === 0x0d) continue;
|
|
163
|
+
if (cc <= 0x1f || cc === 0x7f) return true;
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function _trackingNumber(n) {
|
|
169
|
+
// Tracking numbers are operator-supplied free-form strings — carriers
|
|
170
|
+
// use everything from raw decimal digits (USPS 22-digit) to mixed
|
|
171
|
+
// alphanumeric (UPS 1Z...) to internal SKU-style separators. Refuse
|
|
172
|
+
// control bytes + cap length so a pathological input can't blow out
|
|
173
|
+
// storage; otherwise let the operator pass through whatever the
|
|
174
|
+
// carrier prints on the label.
|
|
175
|
+
if (typeof n !== "string" || !n.length) {
|
|
176
|
+
throw new TypeError("order-tracking: tracking_number must be a non-empty string when provided");
|
|
177
|
+
}
|
|
178
|
+
if (n.length > MAX_TRACKING_LEN) {
|
|
179
|
+
throw new TypeError("order-tracking: tracking_number must be <= " + MAX_TRACKING_LEN + " chars");
|
|
180
|
+
}
|
|
181
|
+
if (_hasStrictControlByte(n)) {
|
|
182
|
+
throw new TypeError("order-tracking: tracking_number must not contain control characters");
|
|
183
|
+
}
|
|
184
|
+
return n;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function _freeText(s, label, maxLen) {
|
|
188
|
+
if (s == null || s === "") return "";
|
|
189
|
+
if (typeof s !== "string") {
|
|
190
|
+
throw new TypeError("order-tracking: " + label + " must be a string");
|
|
191
|
+
}
|
|
192
|
+
if (s.length > maxLen) {
|
|
193
|
+
throw new TypeError("order-tracking: " + label + " must be <= " + maxLen + " chars");
|
|
194
|
+
}
|
|
195
|
+
if (_hasFreeTextControlByte(s)) {
|
|
196
|
+
throw new TypeError("order-tracking: " + label + " must not contain control characters");
|
|
197
|
+
}
|
|
198
|
+
return s;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function _epochMs(ts, label) {
|
|
202
|
+
if (ts == null) return null;
|
|
203
|
+
if (!Number.isInteger(ts) || ts <= 0) {
|
|
204
|
+
throw new TypeError("order-tracking: " + label + " must be a positive integer epoch-ms");
|
|
205
|
+
}
|
|
206
|
+
return ts;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function _now() { return Date.now(); }
|
|
210
|
+
|
|
211
|
+
// ---- pure URL helper (exported standalone too) -------------------------
|
|
212
|
+
|
|
213
|
+
function trackingUrl(carrier, trackingNumber) {
|
|
214
|
+
if (typeof carrier !== "string" || CARRIERS.indexOf(carrier) === -1) return null;
|
|
215
|
+
if (typeof trackingNumber !== "string" || !trackingNumber.length) return null;
|
|
216
|
+
var build = CARRIER_URL[carrier];
|
|
217
|
+
if (typeof build !== "function") return null;
|
|
218
|
+
return build(trackingNumber);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---- factory ------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
function create(opts) {
|
|
224
|
+
opts = opts || {};
|
|
225
|
+
var query = opts.query;
|
|
226
|
+
if (!query) {
|
|
227
|
+
query = function (sql, params) { return _b().externalDb.query(sql, params); };
|
|
228
|
+
}
|
|
229
|
+
// Optional order primitive wiring — when wired, `markDelivered`
|
|
230
|
+
// also drives the parent order through `order.transition(...,
|
|
231
|
+
// "mark_delivered")` so the order FSM reflects delivery without a
|
|
232
|
+
// second operator call. Kept optional so tests + standalone
|
|
233
|
+
// operators can use the shipment ledger without the order FSM.
|
|
234
|
+
var orderPrim = opts.order || null;
|
|
235
|
+
if (orderPrim && typeof orderPrim.transition !== "function") {
|
|
236
|
+
throw new TypeError("order-tracking.create: opts.order must expose a transition(id, event, opts) method");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Internal: append an event row + update shipments.status. Idempotent
|
|
240
|
+
// on `(shipment_id, status, occurred_at)` — a duplicate carrier
|
|
241
|
+
// webhook fire-twice doesn't double-write.
|
|
242
|
+
async function _appendEvent(shipmentId, status, location, detail, occurredAt) {
|
|
243
|
+
var ts = _now();
|
|
244
|
+
var dup = await query(
|
|
245
|
+
"SELECT id FROM shipment_events WHERE shipment_id = ?1 AND status = ?2 AND occurred_at = ?3 LIMIT 1",
|
|
246
|
+
[shipmentId, status, occurredAt],
|
|
247
|
+
);
|
|
248
|
+
if (dup.rows.length) {
|
|
249
|
+
return { id: dup.rows[0].id, duplicate: true };
|
|
250
|
+
}
|
|
251
|
+
var id = _b().uuid.v7();
|
|
252
|
+
await query(
|
|
253
|
+
"INSERT INTO shipment_events (id, shipment_id, status, location, detail, occurred_at, recorded_at) " +
|
|
254
|
+
"VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
|
255
|
+
[id, shipmentId, status, location, detail, occurredAt, ts],
|
|
256
|
+
);
|
|
257
|
+
await query(
|
|
258
|
+
"UPDATE shipments SET status = ?1, updated_at = ?2 WHERE id = ?3",
|
|
259
|
+
[status, ts, shipmentId],
|
|
260
|
+
);
|
|
261
|
+
return { id: id, duplicate: false };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function _getShipmentRow(shipmentId) {
|
|
265
|
+
var r = await query("SELECT * FROM shipments WHERE id = ?1", [shipmentId]);
|
|
266
|
+
return r.rows[0] || null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
CARRIERS: CARRIERS,
|
|
271
|
+
STATUSES: STATUSES,
|
|
272
|
+
|
|
273
|
+
// Pure helper, also exposed on the instance for ergonomics.
|
|
274
|
+
trackingUrl: trackingUrl,
|
|
275
|
+
|
|
276
|
+
createShipment: async function (input) {
|
|
277
|
+
if (!input || typeof input !== "object") {
|
|
278
|
+
throw new TypeError("order-tracking.createShipment: input object required");
|
|
279
|
+
}
|
|
280
|
+
_uuid(input.order_id, "order_id");
|
|
281
|
+
_carrier(input.carrier);
|
|
282
|
+
|
|
283
|
+
var trackingNumber = null;
|
|
284
|
+
if (input.tracking_number != null && input.tracking_number !== "") {
|
|
285
|
+
trackingNumber = _trackingNumber(input.tracking_number);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
var carrierOther = null;
|
|
289
|
+
if (input.carrier === "other") {
|
|
290
|
+
if (typeof input.carrier_other_name !== "string" || !input.carrier_other_name.length) {
|
|
291
|
+
throw new TypeError("order-tracking.createShipment: carrier_other_name required when carrier='other'");
|
|
292
|
+
}
|
|
293
|
+
carrierOther = _freeText(input.carrier_other_name, "carrier_other_name", MAX_CARRIER_OTHER_LEN);
|
|
294
|
+
} else if (input.carrier_other_name != null) {
|
|
295
|
+
// Defensive — surface the contradiction rather than silently
|
|
296
|
+
// dropping operator intent.
|
|
297
|
+
throw new TypeError("order-tracking.createShipment: carrier_other_name only valid when carrier='other'");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
var weight = input.weight_grams == null ? 0 : input.weight_grams;
|
|
301
|
+
_nonNegInt(weight, "weight_grams");
|
|
302
|
+
|
|
303
|
+
var costMinor = input.cost_minor == null ? 0 : input.cost_minor;
|
|
304
|
+
_nonNegInt(costMinor, "cost_minor");
|
|
305
|
+
|
|
306
|
+
var costCurrency = input.cost_currency == null ? "USD" : input.cost_currency;
|
|
307
|
+
_currency(costCurrency);
|
|
308
|
+
|
|
309
|
+
var notes = _freeText(input.notes, "notes", MAX_NOTES_LEN);
|
|
310
|
+
|
|
311
|
+
// Verify the parent order exists. The SQL FK enforces this too,
|
|
312
|
+
// but a TypeError up-front gives a clearer operator-facing
|
|
313
|
+
// message than the raw SQLITE_CONSTRAINT bubble.
|
|
314
|
+
var parent = await query("SELECT id FROM orders WHERE id = ?1", [input.order_id]);
|
|
315
|
+
if (!parent.rows.length) {
|
|
316
|
+
throw new TypeError("order-tracking.createShipment: order " + input.order_id + " not found");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
var id = _b().uuid.v7();
|
|
320
|
+
var ts = _now();
|
|
321
|
+
await query(
|
|
322
|
+
"INSERT INTO shipments (id, order_id, tracking_number, carrier, carrier_other_name, " +
|
|
323
|
+
"status, shipped_at, delivered_at, estimated_delivery_at, weight_grams, cost_minor, " +
|
|
324
|
+
"cost_currency, notes, created_at, updated_at) " +
|
|
325
|
+
"VALUES (?1, ?2, ?3, ?4, ?5, 'pending', NULL, NULL, NULL, ?6, ?7, ?8, ?9, ?10, ?10)",
|
|
326
|
+
[
|
|
327
|
+
id, input.order_id, trackingNumber, input.carrier, carrierOther,
|
|
328
|
+
weight, costMinor, costCurrency, notes, ts,
|
|
329
|
+
],
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
id: id,
|
|
334
|
+
tracking_url: trackingUrl(input.carrier, trackingNumber),
|
|
335
|
+
};
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
recordEvent: async function (input) {
|
|
339
|
+
if (!input || typeof input !== "object") {
|
|
340
|
+
throw new TypeError("order-tracking.recordEvent: input object required");
|
|
341
|
+
}
|
|
342
|
+
_uuid(input.shipment_id, "shipment_id");
|
|
343
|
+
_status(input.status);
|
|
344
|
+
var location = _freeText(input.location, "location", MAX_LOCATION_LEN);
|
|
345
|
+
var detail = _freeText(input.detail, "detail", MAX_DETAIL_LEN);
|
|
346
|
+
var occurred = input.occurred_at == null ? _now() : _epochMs(input.occurred_at, "occurred_at");
|
|
347
|
+
|
|
348
|
+
var row = await _getShipmentRow(input.shipment_id);
|
|
349
|
+
if (!row) {
|
|
350
|
+
throw new TypeError("order-tracking.recordEvent: shipment " + input.shipment_id + " not found");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
var r = await _appendEvent(input.shipment_id, input.status, location, detail, occurred);
|
|
354
|
+
return {
|
|
355
|
+
id: r.id,
|
|
356
|
+
shipment_id: input.shipment_id,
|
|
357
|
+
status: input.status,
|
|
358
|
+
occurred_at: occurred,
|
|
359
|
+
duplicate: r.duplicate,
|
|
360
|
+
};
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
markShipped: async function (shipmentId, opts2) {
|
|
364
|
+
_uuid(shipmentId, "shipment_id");
|
|
365
|
+
opts2 = opts2 || {};
|
|
366
|
+
var shippedAt = opts2.shipped_at == null ? _now() : _epochMs(opts2.shipped_at, "shipped_at");
|
|
367
|
+
var eta = _epochMs(opts2.estimated_delivery_at == null ? null : opts2.estimated_delivery_at, "estimated_delivery_at");
|
|
368
|
+
|
|
369
|
+
var row = await _getShipmentRow(shipmentId);
|
|
370
|
+
if (!row) {
|
|
371
|
+
throw new TypeError("order-tracking.markShipped: shipment " + shipmentId + " not found");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
await _appendEvent(shipmentId, "in-transit", "", "", shippedAt);
|
|
375
|
+
var ts = _now();
|
|
376
|
+
await query(
|
|
377
|
+
"UPDATE shipments SET shipped_at = ?1, estimated_delivery_at = ?2, updated_at = ?3 WHERE id = ?4",
|
|
378
|
+
[shippedAt, eta, ts, shipmentId],
|
|
379
|
+
);
|
|
380
|
+
return await this.getShipment(shipmentId);
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
markDelivered: async function (shipmentId, opts2) {
|
|
384
|
+
_uuid(shipmentId, "shipment_id");
|
|
385
|
+
opts2 = opts2 || {};
|
|
386
|
+
var deliveredAt = opts2.delivered_at == null ? _now() : _epochMs(opts2.delivered_at, "delivered_at");
|
|
387
|
+
|
|
388
|
+
var row = await _getShipmentRow(shipmentId);
|
|
389
|
+
if (!row) {
|
|
390
|
+
throw new TypeError("order-tracking.markDelivered: shipment " + shipmentId + " not found");
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
await _appendEvent(shipmentId, "delivered", "", "", deliveredAt);
|
|
394
|
+
var ts = _now();
|
|
395
|
+
await query(
|
|
396
|
+
"UPDATE shipments SET delivered_at = ?1, updated_at = ?2 WHERE id = ?3",
|
|
397
|
+
[deliveredAt, ts, shipmentId],
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
// Drive the parent order's FSM to `delivered` when the order
|
|
401
|
+
// primitive is wired. The transition is guarded by the FSM —
|
|
402
|
+
// re-firing from an already-delivered order is refused with
|
|
403
|
+
// `fsm/illegal-transition`, which we swallow so a double
|
|
404
|
+
// markDelivered (idempotent at the shipment layer) doesn't
|
|
405
|
+
// spuriously fail at the order layer. Other refusal codes
|
|
406
|
+
// surface to the caller.
|
|
407
|
+
if (orderPrim) {
|
|
408
|
+
try {
|
|
409
|
+
await orderPrim.transition(row.order_id, "mark_delivered", {
|
|
410
|
+
reason: "shipment_delivered",
|
|
411
|
+
metadata: { shipment_id: shipmentId, delivered_at: deliveredAt },
|
|
412
|
+
});
|
|
413
|
+
} catch (e) {
|
|
414
|
+
var code = e && e.code;
|
|
415
|
+
if (code !== "fsm/illegal-transition") throw e;
|
|
416
|
+
// drop-silent on fsm/illegal-transition — the order is
|
|
417
|
+
// already in a state where mark_delivered is a no-op
|
|
418
|
+
// (delivered / refunded / cancelled).
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return await this.getShipment(shipmentId);
|
|
422
|
+
},
|
|
423
|
+
|
|
424
|
+
getShipment: async function (shipmentId) {
|
|
425
|
+
_uuid(shipmentId, "shipment_id");
|
|
426
|
+
var row = await _getShipmentRow(shipmentId);
|
|
427
|
+
if (!row) return null;
|
|
428
|
+
var events = (await query(
|
|
429
|
+
"SELECT * FROM shipment_events WHERE shipment_id = ?1 ORDER BY occurred_at ASC, recorded_at ASC",
|
|
430
|
+
[shipmentId],
|
|
431
|
+
)).rows;
|
|
432
|
+
row.events = events;
|
|
433
|
+
row.tracking_url = trackingUrl(row.carrier, row.tracking_number);
|
|
434
|
+
return row;
|
|
435
|
+
},
|
|
436
|
+
|
|
437
|
+
listForOrder: async function (orderId) {
|
|
438
|
+
_uuid(orderId, "order_id");
|
|
439
|
+
var rows = (await query(
|
|
440
|
+
"SELECT * FROM shipments WHERE order_id = ?1 ORDER BY created_at ASC",
|
|
441
|
+
[orderId],
|
|
442
|
+
)).rows;
|
|
443
|
+
for (var i = 0; i < rows.length; i += 1) {
|
|
444
|
+
rows[i].tracking_url = trackingUrl(rows[i].carrier, rows[i].tracking_number);
|
|
445
|
+
}
|
|
446
|
+
return rows;
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
module.exports = {
|
|
452
|
+
create: create,
|
|
453
|
+
trackingUrl: trackingUrl,
|
|
454
|
+
CARRIERS: CARRIERS,
|
|
455
|
+
STATUSES: STATUSES,
|
|
456
|
+
};
|