@blamejs/blamejs-shop 0.0.65 → 0.0.66

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,557 @@
1
+ "use strict";
2
+ /**
3
+ * @module shop.discountAllocation
4
+ * @title Discount-allocation primitive — distribute order-level
5
+ * discounts across order lines for accounting + refund precision.
6
+ *
7
+ * @intro
8
+ * An order carries an order-level discount: "$20 off the whole
9
+ * order", "15% off everything after the threshold". The lines
10
+ * underneath the order each carry their own subtotal + quantity.
11
+ * For accounting + refund precision the operator needs to know,
12
+ * for any one line, what slice of the order-level discount belongs
13
+ * to it — a partial refund of one line must remove that line's
14
+ * share of the discount, not the whole-order amount.
15
+ *
16
+ * This primitive answers two questions:
17
+ *
18
+ * 1. Given lines + a discount + a kind, compute the per-line
19
+ * breakdown such that `sum(allocated_minor) === discount_minor`
20
+ * by construction. Rounding is half-even via the framework's
21
+ * Money class; the rounding remainder lands on the
22
+ * highest-subtotal line (deterministic, operator-readable —
23
+ * "the biggest line absorbs the cent").
24
+ *
25
+ * 2. Given a recorded allocation + a refund amount, walk the
26
+ * breakdown in reverse to compute the per-line refund shares
27
+ * proportional to the original allocation. The reverse
28
+ * breakdown is the audit trail proving the refund math came
29
+ * from the recorded breakdown — not a recomputation that
30
+ * could drift if the underlying subtotals were modified after
31
+ * the fact.
32
+ *
33
+ * Surface:
34
+ *
35
+ * - `allocate({ lines, discount_minor, kind })`
36
+ * Pure function. `lines` is `[{ line_id, subtotal_minor, quantity }]`.
37
+ * `kind` is one of `proportional` / `equal` / `by_subtotal` /
38
+ * `by_quantity`. Returns
39
+ * `[{ line_id, allocated_minor, remaining_minor }]` where the
40
+ * allocated values sum exactly to `discount_minor`.
41
+ *
42
+ * - `recordAllocation({ order_id, discount_source, kind,
43
+ * breakdown, total_minor, applied_at? })`
44
+ * Persists the breakdown as an audit row. Returns the
45
+ * hydrated row including the assigned id + applied_at.
46
+ *
47
+ * - `allocationsForOrder(order_id)`
48
+ * Returns every recorded allocation against an order, newest
49
+ * first. Breakdown JSON is parsed.
50
+ *
51
+ * - `reverseAllocation({ order_id, source, refund_minor,
52
+ * occurred_at? })`
53
+ * Looks up the allocation row matching (order_id, source),
54
+ * walks its breakdown in reverse, computes per-line refund
55
+ * shares proportional to the original allocated_minor values,
56
+ * persists the reversal row, and returns the reverse
57
+ * breakdown. Rounding is half-even; the rounding remainder
58
+ * lands on the line with the largest original allocation.
59
+ *
60
+ * - `metricsForKind({ kind, from, to })`
61
+ * Aggregates allocation totals + counts by kind across a
62
+ * time window. Returns `{ kind, count, sum_minor }`.
63
+ *
64
+ * Composition:
65
+ * b.money.fromMinorUnits + .multiply([num, den]) carry the
66
+ * half-even rounding semantics. The primitive computes share
67
+ * numerators (line weight) + denominators (sum of weights) and
68
+ * delegates the rounded multiply to Money; the remainder is
69
+ * applied to the highest-subtotal line so the sum invariant
70
+ * holds without re-running the math.
71
+ *
72
+ * Storage:
73
+ * - `discount_allocations` + `discount_reversals` (migration
74
+ * 0129_discount_allocation.sql).
75
+ *
76
+ * @primitive discountAllocation
77
+ * @related b.money, b.uuid.v7, shop.refundPolicy, shop.couponStacking
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 KINDS = Object.freeze([
89
+ "proportional",
90
+ "equal",
91
+ "by_subtotal",
92
+ "by_quantity",
93
+ ]);
94
+
95
+ var MAX_LINES = 5000;
96
+ var MAX_SOURCE_LEN = 200;
97
+ var MAX_LINE_ID_LEN = 200;
98
+
99
+ // Source / line_id are operator-supplied correlation handles — refuse
100
+ // all control bytes (including CR/LF and tab) so a log-injection
101
+ // payload can't land in the audit-trail breakdown JSON.
102
+ var PRINTABLE_RE = /^[^\x00-\x1f\x7f]*$/;
103
+
104
+ // The rounding currency is a placeholder; the math here is integer
105
+ // minor units and the half-even bump only depends on the rational
106
+ // remainder, not on the currency exponent. Picking a fixed code keeps
107
+ // the Money class from refusing the composition.
108
+ var ROUNDING_CURRENCY = "USD";
109
+
110
+ // ---- validators ---------------------------------------------------------
111
+
112
+ function _kind(k) {
113
+ if (typeof k !== "string" || KINDS.indexOf(k) === -1) {
114
+ throw new TypeError("discountAllocation: kind must be one of " + KINDS.join(", "));
115
+ }
116
+ return k;
117
+ }
118
+
119
+ function _positiveInt(n, label) {
120
+ if (typeof n !== "number" || !Number.isInteger(n) || n <= 0) {
121
+ throw new TypeError("discountAllocation: " + label + " must be a positive integer (minor units)");
122
+ }
123
+ return n;
124
+ }
125
+
126
+ function _nonNegInt(n, label) {
127
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 0) {
128
+ throw new TypeError("discountAllocation: " + label + " must be a non-negative integer");
129
+ }
130
+ return n;
131
+ }
132
+
133
+ function _shortString(s, label, max) {
134
+ if (typeof s !== "string" || s.length === 0) {
135
+ throw new TypeError("discountAllocation: " + label + " must be a non-empty string");
136
+ }
137
+ if (s.length > max) {
138
+ throw new TypeError("discountAllocation: " + label + " must be <= " + max + " chars");
139
+ }
140
+ if (!PRINTABLE_RE.test(s)) {
141
+ throw new TypeError("discountAllocation: " + label + " must not contain control bytes");
142
+ }
143
+ return s;
144
+ }
145
+
146
+ function _epochMs(ts, label) {
147
+ if (ts == null) return null;
148
+ if (typeof ts !== "number" || !Number.isInteger(ts) || ts < 0) {
149
+ throw new TypeError("discountAllocation: " + label + " must be a non-negative integer epoch-ms");
150
+ }
151
+ return ts;
152
+ }
153
+
154
+ function _validateLines(lines) {
155
+ if (!Array.isArray(lines)) {
156
+ throw new TypeError("discountAllocation: lines must be an array");
157
+ }
158
+ if (lines.length === 0) {
159
+ throw new TypeError("discountAllocation: lines must be a non-empty array");
160
+ }
161
+ if (lines.length > MAX_LINES) {
162
+ throw new TypeError("discountAllocation: lines must be <= " + MAX_LINES + " entries");
163
+ }
164
+ var seen = Object.create(null);
165
+ var out = [];
166
+ for (var i = 0; i < lines.length; i += 1) {
167
+ var L = lines[i];
168
+ if (!L || typeof L !== "object") {
169
+ throw new TypeError("discountAllocation: lines[" + i + "] must be an object");
170
+ }
171
+ var lineId = _shortString(L.line_id, "lines[" + i + "].line_id", MAX_LINE_ID_LEN);
172
+ var subtotal = _nonNegInt(L.subtotal_minor, "lines[" + i + "].subtotal_minor");
173
+ var quantity = L.quantity;
174
+ if (typeof quantity !== "number" || !Number.isInteger(quantity) || quantity <= 0) {
175
+ throw new TypeError("discountAllocation: lines[" + i + "].quantity must be a positive integer");
176
+ }
177
+ if (Object.prototype.hasOwnProperty.call(seen, lineId)) {
178
+ throw new TypeError("discountAllocation: lines[" + i + "].line_id duplicates an earlier line");
179
+ }
180
+ seen[lineId] = true;
181
+ out.push({ line_id: lineId, subtotal_minor: subtotal, quantity: quantity, _idx: i });
182
+ }
183
+ return out;
184
+ }
185
+
186
+ // ---- allocation math (pure) --------------------------------------------
187
+
188
+ // Compute the per-line share via b.money's half-even rounded multiply.
189
+ // Given total `T`, weights `[w0, w1, ...]` summing to `W`, each share
190
+ // is `floor_half_even(T * wi / W)`. The shares may sum to slightly
191
+ // less than `T` (rounding gives back fractions); the remainder is
192
+ // applied to the line with the highest subtotal so the audit row's
193
+ // `allocated_minor` values sum exactly to `T`.
194
+ //
195
+ // "By quantity" weights use quantity; "equal" weights every line by
196
+ // `1` (so the share is `T / N` rounded); "by_subtotal" /
197
+ // "proportional" weight by subtotal. The remainder placement is
198
+ // always by highest subtotal — the operator's "the biggest line
199
+ // absorbs the cent" mental model holds for every kind.
200
+ function _shares(total, lines, kind) {
201
+ var weights = new Array(lines.length);
202
+ var sumWeight = 0n;
203
+ for (var i = 0; i < lines.length; i += 1) {
204
+ var w;
205
+ if (kind === "equal") {
206
+ w = 1;
207
+ } else if (kind === "by_quantity") {
208
+ w = lines[i].quantity;
209
+ } else {
210
+ // proportional / by_subtotal
211
+ w = lines[i].subtotal_minor;
212
+ }
213
+ weights[i] = w;
214
+ sumWeight += BigInt(w);
215
+ }
216
+ if (sumWeight === 0n) {
217
+ // Every weight is zero — for by_subtotal/proportional this means
218
+ // every line subtotal is zero. Fall back to equal-by-line so the
219
+ // operator still gets a complete breakdown rather than a refusal
220
+ // they have to special-case.
221
+ for (var k = 0; k < weights.length; k += 1) {
222
+ weights[k] = 1;
223
+ sumWeight += 1n;
224
+ }
225
+ }
226
+
227
+ var money = _b().money;
228
+ var totalMoney = money.fromMinorUnits(BigInt(total), ROUNDING_CURRENCY);
229
+ var shares = new Array(lines.length);
230
+ var allocated = 0n;
231
+ for (var j = 0; j < lines.length; j += 1) {
232
+ var share = totalMoney
233
+ .multiply([BigInt(weights[j]), sumWeight])
234
+ .toMinorUnits();
235
+ shares[j] = share;
236
+ allocated += share;
237
+ }
238
+
239
+ // Place the rounding remainder (positive or negative) on the line
240
+ // with the highest subtotal_minor. Ties broken by original index so
241
+ // the placement is deterministic across runtimes.
242
+ var remainder = BigInt(total) - allocated;
243
+ if (remainder !== 0n) {
244
+ var bestIdx = 0;
245
+ var bestSubtotal = lines[0].subtotal_minor;
246
+ for (var m = 1; m < lines.length; m += 1) {
247
+ if (lines[m].subtotal_minor > bestSubtotal) {
248
+ bestSubtotal = lines[m].subtotal_minor;
249
+ bestIdx = m;
250
+ }
251
+ }
252
+ shares[bestIdx] = shares[bestIdx] + remainder;
253
+ }
254
+ return shares;
255
+ }
256
+
257
+ // Public allocate — composes _shares + emits the integer-shape
258
+ // breakdown the caller persists. Refuses negative remaining lines
259
+ // (the discount exceeds the line subtotal); the operator surfaces
260
+ // that as a refusal upstream when the discount value is too large
261
+ // for the order subtotal. Clamps to zero rather than going negative
262
+ // because the receipt-line "remaining" column shouldn't expose
263
+ // negative minor units to the storefront layer.
264
+ function _allocate(input) {
265
+ if (!input || typeof input !== "object") {
266
+ throw new TypeError("discountAllocation.allocate: input object required");
267
+ }
268
+ var lines = _validateLines(input.lines);
269
+ var discount = _positiveInt(input.discount_minor, "discount_minor");
270
+ var kind = _kind(input.kind);
271
+
272
+ var shares = _shares(discount, lines, kind);
273
+ var out = new Array(lines.length);
274
+ for (var i = 0; i < lines.length; i += 1) {
275
+ var allocated = Number(shares[i]);
276
+ var remaining = lines[i].subtotal_minor - allocated;
277
+ if (remaining < 0) remaining = 0;
278
+ out[i] = {
279
+ line_id: lines[i].line_id,
280
+ allocated_minor: allocated,
281
+ remaining_minor: remaining,
282
+ };
283
+ }
284
+ return out;
285
+ }
286
+
287
+ // ---- factory ------------------------------------------------------------
288
+
289
+ function create(opts) {
290
+ opts = opts || {};
291
+ var query = opts.query;
292
+ if (!query) {
293
+ query = function (sql, params) { return _b().externalDb.query(sql, params); };
294
+ }
295
+
296
+ // Two writes against the same order in the same millisecond would
297
+ // tie on `applied_at` and make `allocationsForOrder` ordering
298
+ // ambiguous. Bump the requested timestamp to `prior + 1` when it
299
+ // would collide (or land older than the prior row, which an
300
+ // out-of-order operator write could trigger). The result is a
301
+ // strictly-monotonic per-order `applied_at` sequence.
302
+ function _resolveAppliedAt(requestedTs, latestTs) {
303
+ if (latestTs == null) return requestedTs;
304
+ if (requestedTs > latestTs) return requestedTs;
305
+ return latestTs + 1;
306
+ }
307
+
308
+ async function _latestAppliedAt(orderId) {
309
+ var r = await query(
310
+ "SELECT applied_at FROM discount_allocations " +
311
+ "WHERE order_id = ?1 ORDER BY applied_at DESC LIMIT 1",
312
+ [orderId],
313
+ );
314
+ if (!r.rows.length) return null;
315
+ return r.rows[0].applied_at;
316
+ }
317
+
318
+ async function _latestReversalAt(allocationId) {
319
+ var r = await query(
320
+ "SELECT occurred_at FROM discount_reversals " +
321
+ "WHERE allocation_id = ?1 ORDER BY occurred_at DESC LIMIT 1",
322
+ [allocationId],
323
+ );
324
+ if (!r.rows.length) return null;
325
+ return r.rows[0].occurred_at;
326
+ }
327
+
328
+ async function recordAllocation(input) {
329
+ if (!input || typeof input !== "object") {
330
+ throw new TypeError("discountAllocation.recordAllocation: input object required");
331
+ }
332
+ var orderId = _shortString(input.order_id, "order_id", MAX_LINE_ID_LEN);
333
+ var source = _shortString(input.discount_source, "discount_source", MAX_SOURCE_LEN);
334
+ var kind = _kind(input.kind);
335
+ var total = _positiveInt(input.total_minor, "total_minor");
336
+ var breakdown = input.breakdown;
337
+ if (!Array.isArray(breakdown) || breakdown.length === 0) {
338
+ throw new TypeError("discountAllocation.recordAllocation: breakdown must be a non-empty array");
339
+ }
340
+ // Validate breakdown shape + the sum invariant. The persisted
341
+ // row's audit value is only meaningful if `sum(allocated_minor)`
342
+ // equals `total_minor` exactly; refuse the write rather than
343
+ // silently store an inconsistent breakdown.
344
+ var sum = 0;
345
+ var seenIds = Object.create(null);
346
+ for (var i = 0; i < breakdown.length; i += 1) {
347
+ var row = breakdown[i];
348
+ if (!row || typeof row !== "object") {
349
+ throw new TypeError("discountAllocation.recordAllocation: breakdown[" + i + "] must be an object");
350
+ }
351
+ _shortString(row.line_id, "breakdown[" + i + "].line_id", MAX_LINE_ID_LEN);
352
+ _nonNegInt(row.allocated_minor, "breakdown[" + i + "].allocated_minor");
353
+ _nonNegInt(row.remaining_minor, "breakdown[" + i + "].remaining_minor");
354
+ if (Object.prototype.hasOwnProperty.call(seenIds, row.line_id)) {
355
+ throw new TypeError("discountAllocation.recordAllocation: breakdown[" + i + "].line_id duplicates an earlier row");
356
+ }
357
+ seenIds[row.line_id] = true;
358
+ sum += row.allocated_minor;
359
+ }
360
+ if (sum !== total) {
361
+ throw new TypeError(
362
+ "discountAllocation.recordAllocation: breakdown sum (" + sum +
363
+ ") does not equal total_minor (" + total + ")",
364
+ );
365
+ }
366
+ var requested = _epochMs(input.applied_at, "applied_at");
367
+ if (requested == null) requested = Date.now();
368
+ var latest = await _latestAppliedAt(orderId);
369
+ var ts = _resolveAppliedAt(requested, latest);
370
+
371
+ var id = _b().uuid.v7();
372
+ await query(
373
+ "INSERT INTO discount_allocations " +
374
+ "(id, order_id, source, kind, total_minor, breakdown_json, applied_at) " +
375
+ "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
376
+ [id, orderId, source, kind, total, JSON.stringify(breakdown), ts],
377
+ );
378
+ return {
379
+ id: id,
380
+ order_id: orderId,
381
+ source: source,
382
+ kind: kind,
383
+ total_minor: total,
384
+ breakdown: breakdown.slice(),
385
+ applied_at: ts,
386
+ };
387
+ }
388
+
389
+ async function allocationsForOrder(orderId) {
390
+ _shortString(orderId, "order_id", MAX_LINE_ID_LEN);
391
+ var r = await query(
392
+ "SELECT id, order_id, source, kind, total_minor, breakdown_json, applied_at " +
393
+ "FROM discount_allocations WHERE order_id = ?1 " +
394
+ "ORDER BY applied_at DESC, id DESC",
395
+ [orderId],
396
+ );
397
+ var rows = r.rows;
398
+ var out = new Array(rows.length);
399
+ for (var i = 0; i < rows.length; i += 1) {
400
+ var row = rows[i];
401
+ out[i] = {
402
+ id: row.id,
403
+ order_id: row.order_id,
404
+ source: row.source,
405
+ kind: row.kind,
406
+ total_minor: row.total_minor,
407
+ breakdown: JSON.parse(row.breakdown_json),
408
+ applied_at: row.applied_at,
409
+ };
410
+ }
411
+ return out;
412
+ }
413
+
414
+ // Reverse-walk an allocation: given a refund amount, distribute it
415
+ // across the original breakdown proportional to each line's
416
+ // `allocated_minor`. Half-even rounding via b.money; the rounding
417
+ // remainder lands on the line with the largest original allocation
418
+ // (the same "biggest line absorbs the cent" rule applied to the
419
+ // reverse direction).
420
+ function _reverseShares(refund, breakdown) {
421
+ var weights = new Array(breakdown.length);
422
+ var sumWeight = 0n;
423
+ for (var i = 0; i < breakdown.length; i += 1) {
424
+ weights[i] = breakdown[i].allocated_minor;
425
+ sumWeight += BigInt(breakdown[i].allocated_minor);
426
+ }
427
+ if (sumWeight === 0n) {
428
+ // Every original share was zero (shouldn't be possible because
429
+ // recordAllocation refuses a sum-zero breakdown when total > 0,
430
+ // but defensive). Spread evenly so the refund still produces a
431
+ // complete breakdown.
432
+ for (var k = 0; k < weights.length; k += 1) {
433
+ weights[k] = 1;
434
+ sumWeight += 1n;
435
+ }
436
+ }
437
+ var money = _b().money;
438
+ var refundMoney = money.fromMinorUnits(BigInt(refund), ROUNDING_CURRENCY);
439
+ var shares = new Array(breakdown.length);
440
+ var allocated = 0n;
441
+ for (var j = 0; j < breakdown.length; j += 1) {
442
+ var share = refundMoney
443
+ .multiply([BigInt(weights[j]), sumWeight])
444
+ .toMinorUnits();
445
+ shares[j] = share;
446
+ allocated += share;
447
+ }
448
+ var remainder = BigInt(refund) - allocated;
449
+ if (remainder !== 0n) {
450
+ var bestIdx = 0;
451
+ var bestAlloc = breakdown[0].allocated_minor;
452
+ for (var m = 1; m < breakdown.length; m += 1) {
453
+ if (breakdown[m].allocated_minor > bestAlloc) {
454
+ bestAlloc = breakdown[m].allocated_minor;
455
+ bestIdx = m;
456
+ }
457
+ }
458
+ shares[bestIdx] = shares[bestIdx] + remainder;
459
+ }
460
+ return shares;
461
+ }
462
+
463
+ async function reverseAllocation(input) {
464
+ if (!input || typeof input !== "object") {
465
+ throw new TypeError("discountAllocation.reverseAllocation: input object required");
466
+ }
467
+ var orderId = _shortString(input.order_id, "order_id", MAX_LINE_ID_LEN);
468
+ var source = _shortString(input.source, "source", MAX_SOURCE_LEN);
469
+ var refund = _positiveInt(input.refund_minor, "refund_minor");
470
+
471
+ var r = await query(
472
+ "SELECT id, breakdown_json, total_minor FROM discount_allocations " +
473
+ "WHERE order_id = ?1 AND source = ?2 " +
474
+ "ORDER BY applied_at DESC, id DESC LIMIT 1",
475
+ [orderId, source],
476
+ );
477
+ if (!r.rows.length) {
478
+ var err = new Error("discountAllocation.reverseAllocation: no allocation found for order_id + source");
479
+ err.code = "DISCOUNT_ALLOCATION_NOT_FOUND";
480
+ throw err;
481
+ }
482
+ var allocationId = r.rows[0].id;
483
+ var breakdown = JSON.parse(r.rows[0].breakdown_json);
484
+
485
+ var shares = _reverseShares(refund, breakdown);
486
+ var reverseBreakdown = new Array(breakdown.length);
487
+ for (var i = 0; i < breakdown.length; i += 1) {
488
+ reverseBreakdown[i] = {
489
+ line_id: breakdown[i].line_id,
490
+ refund_minor: Number(shares[i]),
491
+ };
492
+ }
493
+
494
+ var requested = _epochMs(input.occurred_at, "occurred_at");
495
+ if (requested == null) requested = Date.now();
496
+ var latest = await _latestReversalAt(allocationId);
497
+ var ts = _resolveAppliedAt(requested, latest);
498
+
499
+ var id = _b().uuid.v7();
500
+ await query(
501
+ "INSERT INTO discount_reversals " +
502
+ "(id, allocation_id, refund_minor, reverse_breakdown_json, occurred_at) " +
503
+ "VALUES (?1, ?2, ?3, ?4, ?5)",
504
+ [id, allocationId, refund, JSON.stringify(reverseBreakdown), ts],
505
+ );
506
+ return {
507
+ id: id,
508
+ allocation_id: allocationId,
509
+ refund_minor: refund,
510
+ reverse_breakdown: reverseBreakdown,
511
+ occurred_at: ts,
512
+ };
513
+ }
514
+
515
+ async function metricsForKind(input) {
516
+ if (!input || typeof input !== "object") {
517
+ throw new TypeError("discountAllocation.metricsForKind: input object required");
518
+ }
519
+ var kind = _kind(input.kind);
520
+ var from = _epochMs(input.from, "from");
521
+ var to = _epochMs(input.to, "to");
522
+ if (from == null || to == null) {
523
+ throw new TypeError("discountAllocation.metricsForKind: from + to required");
524
+ }
525
+ if (to < from) {
526
+ throw new TypeError("discountAllocation.metricsForKind: to must be >= from");
527
+ }
528
+ var r = await query(
529
+ "SELECT COUNT(*) AS cnt, COALESCE(SUM(total_minor), 0) AS total " +
530
+ "FROM discount_allocations " +
531
+ "WHERE kind = ?1 AND applied_at >= ?2 AND applied_at <= ?3",
532
+ [kind, from, to],
533
+ );
534
+ var row = r.rows[0] || { cnt: 0, total: 0 };
535
+ return {
536
+ kind: kind,
537
+ from: from,
538
+ to: to,
539
+ count: Number(row.cnt),
540
+ sum_minor: Number(row.total),
541
+ };
542
+ }
543
+
544
+ return {
545
+ KINDS: KINDS.slice(),
546
+ allocate: function (input) { return _allocate(input); },
547
+ recordAllocation: recordAllocation,
548
+ allocationsForOrder: allocationsForOrder,
549
+ reverseAllocation: reverseAllocation,
550
+ metricsForKind: metricsForKind,
551
+ };
552
+ }
553
+
554
+ module.exports = {
555
+ create: create,
556
+ KINDS: KINDS,
557
+ };
package/lib/index.js CHANGED
@@ -155,4 +155,14 @@ module.exports = {
155
155
  splitShipments: require("./split-shipments"),
156
156
  trustBadges: require("./trust-badges"),
157
157
  webhookReceiver: require("./webhook-receiver"),
158
+ siteRedirects: require("./site-redirects"),
159
+ costLayers: require("./cost-layers"),
160
+ paymentRetries: require("./payment-retries"),
161
+ pickLists: require("./pick-lists"),
162
+ preorder: require("./preorder"),
163
+ discountAllocation: require("./discount-allocation"),
164
+ currencyRounding: require("./currency-rounding"),
165
+ creditLimits: require("./credit-limits"),
166
+ themeAssets: require("./theme-assets"),
167
+ businessHours: require("./business-hours"),
158
168
  };