@blamejs/blamejs-shop 0.0.60 → 0.0.61

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.
@@ -0,0 +1,683 @@
1
+ "use strict";
2
+ /**
3
+ * @module shop.carrierRates
4
+ * @title Carrier rates — at-checkout shipping rate shopping
5
+ *
6
+ * @intro
7
+ * Operator registers N rate sources (UPS / FedEx / USPS / DHL / a
8
+ * flat-rate fallback). For a (cart + ship-to) tuple the primitive
9
+ * returns sorted-by-cost rates with delivery-window estimates.
10
+ *
11
+ * The framework does NOT call carrier APIs directly. Each carrier
12
+ * has its own auth + endpoint shape; the operator's worker drains a
13
+ * rate request, calls the carrier, and posts each quote back via
14
+ * `recordRateQuote`. The primitive caches every quote until the
15
+ * carrier-declared `valid_until` expires, then `ratesForShipment`
16
+ * returns the live cached rates sorted ascending by cost. When no
17
+ * live carrier rate covers the request, the primitive composes a
18
+ * synthetic flat-rate fallback from the registered `flat_rate`
19
+ * carrier (if one exists) so the checkout never stalls.
20
+ *
21
+ * Distinct from sibling primitives:
22
+ * - `shipping` — per-order cost math at the till (the rate
23
+ * the till charges; this primitive shops
24
+ * the carrier menu that feeds it).
25
+ * - `shippingLabels` — post-checkout broker-purchased label
26
+ * artefact (tracking + PDF).
27
+ *
28
+ * Composes:
29
+ * - `b.guardUuid` — UUID-shape validation on every id input
30
+ * - `b.uuid.v7` — quote row PK
31
+ * - `b.safeUrl.parse` — https-only `rate_endpoint` validation
32
+ * - `b.pagination` — HMAC-tagged cursor for `recentQuotes`
33
+ *
34
+ * Surface:
35
+ * registerCarrier({ slug, carrier, service_levels, rate_endpoint?, active })
36
+ * recordRateQuote({ carrier_slug, service_code, origin_postal,
37
+ * dest_postal, weight_grams, dimensions,
38
+ * rate_minor, currency, valid_until })
39
+ * ratesForShipment({ origin_postal, dest_postal, weight_grams,
40
+ * dimensions })
41
+ * cleanupExpiredQuotes()
42
+ * recentQuotes({ carrier_slug?, from, to, limit, cursor? })
43
+ * metricsForCarrier({ slug, from, to })
44
+ *
45
+ * Storage:
46
+ * - `carriers` (migration `0080_carrier_rates.sql`)
47
+ * - `carrier_rate_quotes` (migration `0080_carrier_rates.sql`)
48
+ *
49
+ * @primitive carrierRates
50
+ * @related b.guardUuid, b.uuid, b.safeUrl, b.pagination, shop.shipping
51
+ */
52
+
53
+ var CARRIERS = Object.freeze(["ups", "fedex", "usps", "dhl", "flat_rate"]);
54
+
55
+ var MAX_SLUG_LEN = 64;
56
+ var SLUG_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
57
+
58
+ var MAX_SERVICE_CODE_LEN = 64;
59
+ var SERVICE_CODE_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
60
+
61
+ var MAX_SERVICE_LABEL_LEN = 128;
62
+ var MAX_SERVICE_LEVELS = 32;
63
+
64
+ var MAX_POSTAL_LEN = 16;
65
+ var POSTAL_RE = /^[A-Za-z0-9][A-Za-z0-9 -]{0,15}$/;
66
+
67
+ var MAX_ENDPOINT_URL_LEN = 2048;
68
+
69
+ var DEFAULT_RECENT_LIMIT = 25;
70
+ var MAX_RECENT_LIMIT = 200;
71
+
72
+ var MAX_DIMENSION_MM = 5000; // 5 m — generous, catches absurd input
73
+ var MAX_WEIGHT_GRAMS = 1000 * 1000; // 1 t — generous, catches absurd input
74
+
75
+ // Lazy framework handle — matches the pattern every other shop
76
+ // primitive uses; avoids the require cycle that would arise from
77
+ // importing `./index` at module-eval time.
78
+ var bShop;
79
+ function _b() {
80
+ if (!bShop) bShop = require("./index");
81
+ return bShop.framework;
82
+ }
83
+
84
+ // ---- validators ---------------------------------------------------------
85
+
86
+ function _hasControlByte(s) {
87
+ for (var i = 0; i < s.length; i += 1) {
88
+ var cc = s.charCodeAt(i);
89
+ if (cc <= 0x1f || cc === 0x7f) return true;
90
+ }
91
+ return false;
92
+ }
93
+
94
+ function _slug(s) {
95
+ if (typeof s !== "string" || !s.length) {
96
+ throw new TypeError("carrierRates: slug must be a non-empty string");
97
+ }
98
+ if (s.length > MAX_SLUG_LEN) {
99
+ throw new TypeError("carrierRates: slug must be <= " + MAX_SLUG_LEN + " characters");
100
+ }
101
+ if (!SLUG_RE.test(s)) {
102
+ throw new TypeError(
103
+ "carrierRates: slug must match /^[a-z0-9][a-z0-9_-]{0,63}$/ — " +
104
+ "lowercase alphanumerics with `_`/`-`, must not start with separator"
105
+ );
106
+ }
107
+ return s;
108
+ }
109
+
110
+ function _carrier(c) {
111
+ if (typeof c !== "string" || CARRIERS.indexOf(c) === -1) {
112
+ throw new TypeError("carrierRates: carrier must be one of " + CARRIERS.join(", "));
113
+ }
114
+ return c;
115
+ }
116
+
117
+ function _serviceCode(c, label) {
118
+ if (typeof c !== "string" || !c.length) {
119
+ throw new TypeError("carrierRates: " + label + " must be a non-empty string");
120
+ }
121
+ if (c.length > MAX_SERVICE_CODE_LEN) {
122
+ throw new TypeError("carrierRates: " + label + " must be <= " + MAX_SERVICE_CODE_LEN + " characters");
123
+ }
124
+ if (!SERVICE_CODE_RE.test(c)) {
125
+ throw new TypeError(
126
+ "carrierRates: " + label + " must match /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/"
127
+ );
128
+ }
129
+ return c;
130
+ }
131
+
132
+ function _serviceLevels(arr) {
133
+ if (!Array.isArray(arr) || arr.length === 0) {
134
+ throw new TypeError("carrierRates: service_levels must be a non-empty array");
135
+ }
136
+ if (arr.length > MAX_SERVICE_LEVELS) {
137
+ throw new TypeError("carrierRates: service_levels must be <= " + MAX_SERVICE_LEVELS + " entries");
138
+ }
139
+ var seen = {};
140
+ var out = [];
141
+ for (var i = 0; i < arr.length; i += 1) {
142
+ var lvl = arr[i];
143
+ if (!lvl || typeof lvl !== "object" || Array.isArray(lvl)) {
144
+ throw new TypeError("carrierRates: service_levels[" + i + "] must be an object");
145
+ }
146
+ var code = _serviceCode(lvl.code, "service_levels[" + i + "].code");
147
+ if (Object.prototype.hasOwnProperty.call(seen, code)) {
148
+ throw new TypeError("carrierRates: service_levels[" + i + "].code duplicates an earlier entry");
149
+ }
150
+ seen[code] = true;
151
+ if (typeof lvl.label !== "string" || !lvl.label.length) {
152
+ throw new TypeError("carrierRates: service_levels[" + i + "].label must be a non-empty string");
153
+ }
154
+ if (lvl.label.length > MAX_SERVICE_LABEL_LEN) {
155
+ throw new TypeError("carrierRates: service_levels[" + i + "].label must be <= " + MAX_SERVICE_LABEL_LEN + " characters");
156
+ }
157
+ if (_hasControlByte(lvl.label)) {
158
+ throw new TypeError("carrierRates: service_levels[" + i + "].label must not contain control characters");
159
+ }
160
+ if (!Number.isInteger(lvl.sla_days) || lvl.sla_days < 0 || lvl.sla_days > 365) {
161
+ throw new TypeError("carrierRates: service_levels[" + i + "].sla_days must be an integer 0..365");
162
+ }
163
+ out.push({ code: code, label: lvl.label, sla_days: lvl.sla_days });
164
+ }
165
+ return out;
166
+ }
167
+
168
+ function _rateEndpoint(url) {
169
+ if (url == null || url === "") return null;
170
+ if (typeof url !== "string") {
171
+ throw new TypeError("carrierRates: rate_endpoint must be a string when provided");
172
+ }
173
+ if (url.length > MAX_ENDPOINT_URL_LEN) {
174
+ throw new TypeError("carrierRates: rate_endpoint must be <= " + MAX_ENDPOINT_URL_LEN + " characters");
175
+ }
176
+ if (_hasControlByte(url)) {
177
+ throw new TypeError("carrierRates: rate_endpoint must not contain control characters");
178
+ }
179
+ // safeUrl.parse defaults to ALLOW_HTTP_TLS (https only). The framework
180
+ // refuses http:// + non-http schemes + user:pass@ userinfo + bracketed
181
+ // raw-IP forms at this gate — operators forwarding rate requests over
182
+ // cleartext would leak the shopping pattern; refuse at the door.
183
+ try {
184
+ _b().safeUrl.parse(url, { allowedProtocols: ["https:"] });
185
+ } catch (e) {
186
+ throw new TypeError("carrierRates: rate_endpoint — " + (e && e.message || "must be a valid https:// URL"));
187
+ }
188
+ return url;
189
+ }
190
+
191
+ function _active(a) {
192
+ if (typeof a !== "boolean") {
193
+ throw new TypeError("carrierRates: active must be a boolean");
194
+ }
195
+ return a ? 1 : 0;
196
+ }
197
+
198
+ function _postal(s, label) {
199
+ if (typeof s !== "string" || !s.length) {
200
+ throw new TypeError("carrierRates: " + label + " must be a non-empty string");
201
+ }
202
+ if (s.length > MAX_POSTAL_LEN) {
203
+ throw new TypeError("carrierRates: " + label + " must be <= " + MAX_POSTAL_LEN + " characters");
204
+ }
205
+ if (!POSTAL_RE.test(s)) {
206
+ throw new TypeError(
207
+ "carrierRates: " + label + " must match /^[A-Za-z0-9][A-Za-z0-9 -]{0,15}$/ " +
208
+ "(alphanumerics with embedded space or `-`)"
209
+ );
210
+ }
211
+ return s;
212
+ }
213
+
214
+ function _weightGrams(n) {
215
+ if (!Number.isInteger(n) || n <= 0 || n > MAX_WEIGHT_GRAMS) {
216
+ throw new TypeError(
217
+ "carrierRates: weight_grams must be a positive integer <= " + MAX_WEIGHT_GRAMS
218
+ );
219
+ }
220
+ return n;
221
+ }
222
+
223
+ function _dimensions(d) {
224
+ if (!d || typeof d !== "object" || Array.isArray(d)) {
225
+ throw new TypeError("carrierRates: dimensions must be an object {length_mm, width_mm, height_mm}");
226
+ }
227
+ var keys = ["length_mm", "width_mm", "height_mm"];
228
+ for (var i = 0; i < keys.length; i += 1) {
229
+ var k = keys[i];
230
+ var v = d[k];
231
+ if (!Number.isInteger(v) || v <= 0 || v > MAX_DIMENSION_MM) {
232
+ throw new TypeError(
233
+ "carrierRates: dimensions." + k + " must be a positive integer <= " + MAX_DIMENSION_MM
234
+ );
235
+ }
236
+ }
237
+ return {
238
+ length_mm: d.length_mm,
239
+ width_mm: d.width_mm,
240
+ height_mm: d.height_mm,
241
+ };
242
+ }
243
+
244
+ function _rateMinor(n) {
245
+ if (!Number.isInteger(n) || n < 0) {
246
+ throw new TypeError("carrierRates: rate_minor must be a non-negative integer");
247
+ }
248
+ return n;
249
+ }
250
+
251
+ function _currency(c) {
252
+ if (typeof c !== "string" || !/^[A-Z]{3}$/.test(c)) {
253
+ throw new TypeError("carrierRates: currency must be 3-letter uppercase ISO 4217");
254
+ }
255
+ return c;
256
+ }
257
+
258
+ function _epochMs(n, label) {
259
+ if (!Number.isInteger(n) || n <= 0) {
260
+ throw new TypeError("carrierRates: " + label + " must be a positive integer epoch-ms");
261
+ }
262
+ return n;
263
+ }
264
+
265
+ function _limit(n) {
266
+ if (n == null) return DEFAULT_RECENT_LIMIT;
267
+ if (!Number.isInteger(n) || n <= 0 || n > MAX_RECENT_LIMIT) {
268
+ throw new TypeError("carrierRates: limit must be an integer 1..." + MAX_RECENT_LIMIT);
269
+ }
270
+ return n;
271
+ }
272
+
273
+ function _now() { return Date.now(); }
274
+
275
+ // ---- factory ------------------------------------------------------------
276
+
277
+ function create(opts) {
278
+ opts = opts || {};
279
+ var query = opts.query;
280
+ if (!query) {
281
+ query = function (sql, params) { return _b().externalDb.query(sql, params); };
282
+ }
283
+ var cursorSecret = opts.cursorSecret || null;
284
+ if (cursorSecret != null && (typeof cursorSecret !== "string" || !cursorSecret.length)) {
285
+ throw new TypeError("carrierRates.create: opts.cursorSecret must be a non-empty string when provided");
286
+ }
287
+
288
+ function _hydrateCarrier(row) {
289
+ if (!row) return null;
290
+ var levels = [];
291
+ try { levels = JSON.parse(row.service_levels_json || "[]"); }
292
+ catch (_e) { levels = []; }
293
+ return {
294
+ slug: row.slug,
295
+ carrier: row.carrier,
296
+ service_levels: levels,
297
+ rate_endpoint: row.rate_endpoint == null ? null : row.rate_endpoint,
298
+ active: Number(row.active) === 1,
299
+ created_at: Number(row.created_at),
300
+ updated_at: Number(row.updated_at),
301
+ };
302
+ }
303
+
304
+ function _hydrateQuote(row) {
305
+ if (!row) return null;
306
+ var dims = { length_mm: 0, width_mm: 0, height_mm: 0 };
307
+ try { dims = JSON.parse(row.dimensions_json || "{}"); }
308
+ catch (_e) { /* drop-silent — by design; row content is operator-supplied JSON */ }
309
+ return {
310
+ id: row.id,
311
+ carrier_slug: row.carrier_slug,
312
+ service_code: row.service_code,
313
+ origin_postal: row.origin_postal,
314
+ dest_postal: row.dest_postal,
315
+ weight_grams: Number(row.weight_grams),
316
+ dimensions: dims,
317
+ rate_minor: Number(row.rate_minor),
318
+ currency: row.currency,
319
+ valid_until: Number(row.valid_until),
320
+ queried_at: Number(row.queried_at),
321
+ };
322
+ }
323
+
324
+ async function _getCarrierRow(slug) {
325
+ var r = await query("SELECT * FROM carriers WHERE slug = ?1", [slug]);
326
+ return r.rows[0] || null;
327
+ }
328
+
329
+ return {
330
+ CARRIERS: CARRIERS,
331
+ MAX_SERVICE_LEVELS: MAX_SERVICE_LEVELS,
332
+ MAX_RECENT_LIMIT: MAX_RECENT_LIMIT,
333
+
334
+ // Register (or update) an operator's rate source. Upsert on slug
335
+ // — repeating with the same slug replaces the carrier shape +
336
+ // service-level menu in place so an operator paused-then-rotated
337
+ // an account doesn't accumulate stale rows.
338
+ registerCarrier: async function (input) {
339
+ if (!input || typeof input !== "object") {
340
+ throw new TypeError("carrierRates.registerCarrier: input object required");
341
+ }
342
+ var slug = _slug(input.slug);
343
+ var carrier = _carrier(input.carrier);
344
+ var serviceLevels = _serviceLevels(input.service_levels);
345
+ var rateEndpoint = _rateEndpoint(input.rate_endpoint);
346
+ var active = _active(input.active);
347
+
348
+ var ts = _now();
349
+ var existing = await _getCarrierRow(slug);
350
+ var createdAt = existing ? Number(existing.created_at) : ts;
351
+
352
+ await query(
353
+ "INSERT INTO carriers " +
354
+ "(slug, carrier, service_levels_json, rate_endpoint, active, created_at, updated_at) " +
355
+ "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) " +
356
+ "ON CONFLICT(slug) DO UPDATE SET " +
357
+ " carrier = excluded.carrier, " +
358
+ " service_levels_json = excluded.service_levels_json, " +
359
+ " rate_endpoint = excluded.rate_endpoint, " +
360
+ " active = excluded.active, " +
361
+ " updated_at = excluded.updated_at",
362
+ [
363
+ slug, carrier, JSON.stringify(serviceLevels), rateEndpoint,
364
+ active, createdAt, ts,
365
+ ],
366
+ );
367
+ return _hydrateCarrier(await _getCarrierRow(slug));
368
+ },
369
+
370
+ // Cache one carrier-minted quote. The UNIQUE
371
+ // (carrier_slug, service_code, origin_postal, dest_postal,
372
+ // weight_grams) key dedups — a fresh quote with the same shape
373
+ // refreshes rate + valid_until in place, never stacks.
374
+ recordRateQuote: async function (input) {
375
+ if (!input || typeof input !== "object") {
376
+ throw new TypeError("carrierRates.recordRateQuote: input object required");
377
+ }
378
+ var slug = _slug(input.carrier_slug);
379
+ var serviceCode = _serviceCode(input.service_code, "service_code");
380
+ var originPostal = _postal(input.origin_postal, "origin_postal");
381
+ var destPostal = _postal(input.dest_postal, "dest_postal");
382
+ var weightGrams = _weightGrams(input.weight_grams);
383
+ var dimensions = _dimensions(input.dimensions);
384
+ var rateMinor = _rateMinor(input.rate_minor);
385
+ var currency = _currency(input.currency);
386
+ var validUntil = _epochMs(input.valid_until, "valid_until");
387
+
388
+ var carrierRow = await _getCarrierRow(slug);
389
+ if (!carrierRow) {
390
+ throw new TypeError("carrierRates.recordRateQuote: carrier '" + slug + "' not registered");
391
+ }
392
+ // Service code must appear in the carrier's registered menu.
393
+ // Operators rotating service-level offerings re-call
394
+ // `registerCarrier` first; a quote for an unmenued code is the
395
+ // worker drifting against the operator's intent — refuse loud.
396
+ var menu;
397
+ try { menu = JSON.parse(carrierRow.service_levels_json || "[]"); }
398
+ catch (_e) { menu = []; }
399
+ var menued = false;
400
+ for (var i = 0; i < menu.length; i += 1) {
401
+ if (menu[i] && menu[i].code === serviceCode) { menued = true; break; }
402
+ }
403
+ if (!menued) {
404
+ throw new TypeError(
405
+ "carrierRates.recordRateQuote: service_code '" + serviceCode +
406
+ "' not in carrier '" + slug + "' service_levels menu"
407
+ );
408
+ }
409
+
410
+ var ts = _now();
411
+ // Pre-resolve the existing row id (if any) so the upsert keeps a
412
+ // stable id across refreshes — the audit trail in `recentQuotes`
413
+ // stays threaded by id rather than churning a new uuid per
414
+ // refresh. INSERT OR REPLACE without the lookup would re-mint.
415
+ var existing = (await query(
416
+ "SELECT id FROM carrier_rate_quotes " +
417
+ "WHERE carrier_slug = ?1 AND service_code = ?2 " +
418
+ " AND origin_postal = ?3 AND dest_postal = ?4 AND weight_grams = ?5",
419
+ [slug, serviceCode, originPostal, destPostal, weightGrams],
420
+ )).rows[0];
421
+ var id = existing ? existing.id : _b().uuid.v7();
422
+
423
+ await query(
424
+ "INSERT INTO carrier_rate_quotes " +
425
+ "(id, carrier_slug, service_code, origin_postal, dest_postal, " +
426
+ " weight_grams, dimensions_json, rate_minor, currency, " +
427
+ " valid_until, queried_at) " +
428
+ "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11) " +
429
+ "ON CONFLICT(carrier_slug, service_code, origin_postal, dest_postal, weight_grams) " +
430
+ "DO UPDATE SET " +
431
+ " dimensions_json = excluded.dimensions_json, " +
432
+ " rate_minor = excluded.rate_minor, " +
433
+ " currency = excluded.currency, " +
434
+ " valid_until = excluded.valid_until, " +
435
+ " queried_at = excluded.queried_at",
436
+ [
437
+ id, slug, serviceCode, originPostal, destPostal,
438
+ weightGrams, JSON.stringify(dimensions), rateMinor, currency,
439
+ validUntil, ts,
440
+ ],
441
+ );
442
+
443
+ var r = await query("SELECT * FROM carrier_rate_quotes WHERE id = ?1", [id]);
444
+ return _hydrateQuote(r.rows[0]);
445
+ },
446
+
447
+ // Read live cached rates for a shipment. ORDER BY rate_minor ASC
448
+ // so the cheapest live option surfaces first; the operator
449
+ // checkout typically renders the top N. The `dimensions` input is
450
+ // operator-reported for transparency but not part of the lookup
451
+ // key (operators packing a 30cm + 60cm box for the same address
452
+ // typically run two recordRateQuote calls with different weights;
453
+ // dimensions are stored on each quote so the checkout can pick
454
+ // the right one out of band when needed).
455
+ //
456
+ // When no live carrier rate exists, the primitive composes a
457
+ // synthetic fallback row per registered `flat_rate` carrier so
458
+ // the checkout never stalls on an empty rate pool. The synthetic
459
+ // row uses each flat_rate carrier's first service level + a
460
+ // documented FLAT_RATE_FALLBACK rate_minor of 0 (the operator
461
+ // overrides via a stored quote when they care about a non-zero
462
+ // baseline). Synthetic rows carry `synthetic: true` so the
463
+ // checkout can label them distinctly.
464
+ ratesForShipment: async function (input) {
465
+ if (!input || typeof input !== "object") {
466
+ throw new TypeError("carrierRates.ratesForShipment: input object required");
467
+ }
468
+ var originPostal = _postal(input.origin_postal, "origin_postal");
469
+ var destPostal = _postal(input.dest_postal, "dest_postal");
470
+ var weightGrams = _weightGrams(input.weight_grams);
471
+ _dimensions(input.dimensions);
472
+
473
+ var now = _now();
474
+ // Join in carrier_slug -> carriers.active so a paused carrier
475
+ // drops out of the rate pool without operator scrubbing the
476
+ // cached quotes (resuming the carrier surfaces the cache again).
477
+ var rows = (await query(
478
+ "SELECT q.*, c.active AS carrier_active, c.service_levels_json AS carrier_levels_json " +
479
+ "FROM carrier_rate_quotes q " +
480
+ "JOIN carriers c ON c.slug = q.carrier_slug " +
481
+ "WHERE q.origin_postal = ?1 AND q.dest_postal = ?2 " +
482
+ " AND q.weight_grams = ?3 AND q.valid_until > ?4 " +
483
+ " AND c.active = 1 " +
484
+ "ORDER BY q.rate_minor ASC, q.queried_at DESC, q.id ASC",
485
+ [originPostal, destPostal, weightGrams, now],
486
+ )).rows;
487
+
488
+ var out = [];
489
+ for (var i = 0; i < rows.length; i += 1) {
490
+ var hydrated = _hydrateQuote(rows[i]);
491
+ // Decorate with the matching service_level entry so the
492
+ // checkout can render label + sla_days without a second
493
+ // lookup.
494
+ var levels = [];
495
+ try { levels = JSON.parse(rows[i].carrier_levels_json || "[]"); }
496
+ catch (_e) { levels = []; }
497
+ var serviceLevel = null;
498
+ for (var j = 0; j < levels.length; j += 1) {
499
+ if (levels[j] && levels[j].code === hydrated.service_code) {
500
+ serviceLevel = levels[j];
501
+ break;
502
+ }
503
+ }
504
+ hydrated.service_label = serviceLevel ? serviceLevel.label : null;
505
+ hydrated.sla_days = serviceLevel ? serviceLevel.sla_days : null;
506
+ hydrated.synthetic = false;
507
+ out.push(hydrated);
508
+ }
509
+
510
+ if (out.length > 0) return out;
511
+
512
+ // Empty pool — compose synthetic flat-rate fallbacks. The
513
+ // operator registers a `flat_rate` carrier with at least one
514
+ // service level; the synthetic row borrows that menu so the
515
+ // checkout sees a consistent shape (label + sla_days + service
516
+ // code). The synthetic rate_minor is 0 — operators wanting a
517
+ // non-zero baseline post a real recordRateQuote against the
518
+ // flat_rate slug with the desired rate.
519
+ var flatRows = (await query(
520
+ "SELECT * FROM carriers WHERE carrier = 'flat_rate' AND active = 1 " +
521
+ "ORDER BY slug ASC",
522
+ [],
523
+ )).rows;
524
+ for (var k = 0; k < flatRows.length; k += 1) {
525
+ var flat = _hydrateCarrier(flatRows[k]);
526
+ if (!flat.service_levels.length) continue;
527
+ var first = flat.service_levels[0];
528
+ out.push({
529
+ id: null,
530
+ carrier_slug: flat.slug,
531
+ service_code: first.code,
532
+ service_label: first.label,
533
+ sla_days: first.sla_days,
534
+ origin_postal: originPostal,
535
+ dest_postal: destPostal,
536
+ weight_grams: weightGrams,
537
+ dimensions: _dimensions(input.dimensions),
538
+ rate_minor: 0,
539
+ currency: null,
540
+ valid_until: null,
541
+ queried_at: null,
542
+ synthetic: true,
543
+ });
544
+ }
545
+ return out;
546
+ },
547
+
548
+ // Prune every row whose carrier-declared `valid_until` has
549
+ // already passed. Operator-side housekeeping — the rate-lookup
550
+ // path filters expired rows out at read time, so this is a disk-
551
+ // reclaim hint, not a correctness gate. Returns the count pruned.
552
+ cleanupExpiredQuotes: async function () {
553
+ var now = _now();
554
+ var r = await query(
555
+ "DELETE FROM carrier_rate_quotes WHERE valid_until <= ?1",
556
+ [now],
557
+ );
558
+ return { pruned: Number(r.rowCount || 0) };
559
+ },
560
+
561
+ // Operator dashboard feed — paginated by (queried_at DESC, id
562
+ // DESC). Cursor is HMAC-tagged via b.pagination so an operator
563
+ // can't hand-craft one to skip past a hidden row. Filters by
564
+ // `carrier_slug` when supplied + the [from, to] queried_at
565
+ // window (inclusive on both ends).
566
+ recentQuotes: async function (listOpts) {
567
+ if (!listOpts || typeof listOpts !== "object") {
568
+ throw new TypeError("carrierRates.recentQuotes: opts object required");
569
+ }
570
+ var from = _epochMs(listOpts.from, "from");
571
+ var to = _epochMs(listOpts.to, "to");
572
+ if (to < from) {
573
+ throw new TypeError("carrierRates.recentQuotes: to must be >= from");
574
+ }
575
+ var limit = _limit(listOpts.limit);
576
+ var carrierFilter = null;
577
+ if (listOpts.carrier_slug != null) {
578
+ carrierFilter = _slug(listOpts.carrier_slug);
579
+ }
580
+ var orderKey = ["queried_at:desc", "id:desc"];
581
+ var cursorVals = null;
582
+ if (listOpts.cursor != null) {
583
+ if (typeof listOpts.cursor !== "string") {
584
+ throw new TypeError("carrierRates.recentQuotes: cursor must be an opaque string or null");
585
+ }
586
+ try {
587
+ var state = _b().pagination.decodeCursor(listOpts.cursor, cursorSecret);
588
+ if (JSON.stringify(state.orderKey) !== JSON.stringify(orderKey)) {
589
+ throw new TypeError("carrierRates.recentQuotes: cursor orderKey mismatch");
590
+ }
591
+ cursorVals = state.vals;
592
+ } catch (e) {
593
+ if (e instanceof TypeError) throw e;
594
+ throw new TypeError("carrierRates.recentQuotes: cursor — " + (e && e.message || "malformed"));
595
+ }
596
+ }
597
+
598
+ var where = ["queried_at >= ?1", "queried_at <= ?2"];
599
+ var params = [from, to];
600
+ var idx = 3;
601
+ if (carrierFilter) {
602
+ where.push("carrier_slug = ?" + idx);
603
+ params.push(carrierFilter);
604
+ idx += 1;
605
+ }
606
+ if (cursorVals) {
607
+ where.push("(queried_at < ?" + idx + " OR (queried_at = ?" + idx + " AND id < ?" + (idx + 1) + "))");
608
+ params.push(cursorVals[0]);
609
+ params.push(cursorVals[1]);
610
+ idx += 2;
611
+ }
612
+ var sql =
613
+ "SELECT * FROM carrier_rate_quotes WHERE " + where.join(" AND ") +
614
+ " ORDER BY queried_at DESC, id DESC LIMIT ?" + idx;
615
+ params.push(limit);
616
+
617
+ var rows = (await query(sql, params)).rows;
618
+ var out = [];
619
+ for (var i = 0; i < rows.length; i += 1) out.push(_hydrateQuote(rows[i]));
620
+ var last = out[out.length - 1];
621
+ var next = null;
622
+ if (last && out.length === limit) {
623
+ next = _b().pagination.encodeCursor({
624
+ orderKey: orderKey,
625
+ vals: [last.queried_at, last.id],
626
+ forward: true,
627
+ }, cursorSecret);
628
+ }
629
+ return { rows: out, next_cursor: next };
630
+ },
631
+
632
+ // Per-carrier aggregate over a queried_at window. Counts the
633
+ // quotes recorded + reports min/max/avg rate_minor per currency
634
+ // (mixing USD + EUR into a single avg is silently lossy, so the
635
+ // group key includes currency). Operators use the report to spot
636
+ // a carrier whose rates drifted up week-over-week or a
637
+ // service code that's silently degenerating.
638
+ metricsForCarrier: async function (input) {
639
+ if (!input || typeof input !== "object") {
640
+ throw new TypeError("carrierRates.metricsForCarrier: input object required");
641
+ }
642
+ var slug = _slug(input.slug);
643
+ var from = _epochMs(input.from, "from");
644
+ var to = _epochMs(input.to, "to");
645
+ if (to < from) {
646
+ throw new TypeError("carrierRates.metricsForCarrier: to must be >= from");
647
+ }
648
+ var rows = (await query(
649
+ "SELECT service_code, currency, " +
650
+ " COUNT(*) AS quote_count, " +
651
+ " MIN(rate_minor) AS min_minor, " +
652
+ " MAX(rate_minor) AS max_minor, " +
653
+ " AVG(rate_minor) AS avg_minor " +
654
+ "FROM carrier_rate_quotes " +
655
+ "WHERE carrier_slug = ?1 AND queried_at >= ?2 AND queried_at <= ?3 " +
656
+ "GROUP BY service_code, currency " +
657
+ "ORDER BY service_code ASC, currency ASC",
658
+ [slug, from, to],
659
+ )).rows;
660
+ var out = [];
661
+ for (var i = 0; i < rows.length; i += 1) {
662
+ var r = rows[i];
663
+ out.push({
664
+ carrier_slug: slug,
665
+ service_code: r.service_code,
666
+ currency: r.currency,
667
+ quote_count: Number(r.quote_count),
668
+ min_minor: Number(r.min_minor),
669
+ max_minor: Number(r.max_minor),
670
+ avg_minor: Number(r.avg_minor),
671
+ });
672
+ }
673
+ return out;
674
+ },
675
+ };
676
+ }
677
+
678
+ module.exports = {
679
+ create: create,
680
+ CARRIERS: CARRIERS,
681
+ MAX_SERVICE_LEVELS: MAX_SERVICE_LEVELS,
682
+ MAX_RECENT_LIMIT: MAX_RECENT_LIMIT,
683
+ };