@fire-spark/core 0.4.1 → 0.5.0
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/dist/index.d.ts +211 -70
- package/dist/index.js +472 -462
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DIGITAL_COLLECT_AT, PARTIAL_APPROVAL_ERROR,
|
|
1
|
+
import { DIGITAL_COLLECT_AT, PARTIAL_APPROVAL_ERROR, REDEMPTION_METHOD, activeIntent, createAttemptReference, createPaymentError, currentIntent, defaultReversalFor, groupRedeems, hasRedeemPayment, isDeliveryFee, isPaymentError, isRealPaymentMethod, isRealPaymentMethod as isRealPaymentMethod$1, isTenderSettled, isTipFee, liveTenders, memoryPaymentStorage, missingDeviceFields, money, moneyMin, moneyNumber, moneyZero, orderRedeems, orderRedeems as orderRedeems$1, redeemsCoverTotal, resolvePaymentPolicy, resolveProviderEnvironment, scopedPaymentStorage, taxBreakdown, toChargeAmount, withTerminalLock } from "@fire-spark/payments";
|
|
2
2
|
import advancedFormat from "dayjs/plugin/advancedFormat.js";
|
|
3
3
|
import isBetween from "dayjs/plugin/isBetween.js";
|
|
4
4
|
import isSameOrAfter from "dayjs/plugin/isSameOrAfter.js";
|
|
@@ -3089,6 +3089,23 @@ const genOrderId = (storeId) => {
|
|
|
3089
3089
|
return genId().literal("ord").separator().literal(storeId, "nostore").separator().hash().build();
|
|
3090
3090
|
};
|
|
3091
3091
|
//#endregion
|
|
3092
|
+
//#region src/order/OrderPaymentRequiredError.ts
|
|
3093
|
+
/**
|
|
3094
|
+
* `placeOrder` has no real tender (no `payment`, no `setTenders`, no
|
|
3095
|
+
* `intent_id`) and the redeems do not cover the total. Same code as the
|
|
3096
|
+
* API's 422, so callers map both the same way.
|
|
3097
|
+
*/
|
|
3098
|
+
var OrderPaymentRequiredError = class extends Error {
|
|
3099
|
+
code = "PAYMENT_REQUIRED";
|
|
3100
|
+
constructor() {
|
|
3101
|
+
super("Place needs a payment method: the redeems do not cover the total");
|
|
3102
|
+
this.name = "OrderPaymentRequiredError";
|
|
3103
|
+
}
|
|
3104
|
+
};
|
|
3105
|
+
const isOrderPaymentRequiredError = (error) => {
|
|
3106
|
+
return error instanceof OrderPaymentRequiredError;
|
|
3107
|
+
};
|
|
3108
|
+
//#endregion
|
|
3092
3109
|
//#region src/order/retry.ts
|
|
3093
3110
|
const defaultSleep$1 = (ms) => {
|
|
3094
3111
|
return new Promise((resolve) => {
|
|
@@ -3118,17 +3135,31 @@ var OrderSyncPendingError = class extends Error {
|
|
|
3118
3135
|
this.name = "OrderSyncPendingError";
|
|
3119
3136
|
}
|
|
3120
3137
|
};
|
|
3138
|
+
/**
|
|
3139
|
+
* The server rejected an order write (POST/PATCH) and the draft was rolled
|
|
3140
|
+
* back to the last synced order. Keeps the API `status`, `code` and
|
|
3141
|
+
* `details`, so code checks on `StorefrontHttpError` still match.
|
|
3142
|
+
*/
|
|
3143
|
+
var OrderSyncRejectedError = class extends StorefrontHttpError {
|
|
3144
|
+
constructor(error) {
|
|
3145
|
+
const { message, status, code, details } = error;
|
|
3146
|
+
super(message, status, code, details);
|
|
3147
|
+
this.name = "OrderSyncRejectedError";
|
|
3148
|
+
}
|
|
3149
|
+
};
|
|
3121
3150
|
const createFlushController = (runtime) => {
|
|
3122
3151
|
let timer = null;
|
|
3123
3152
|
let inFlight = false;
|
|
3124
3153
|
let dirtyDuringFlight = false;
|
|
3125
3154
|
let current = null;
|
|
3126
3155
|
let pending = false;
|
|
3156
|
+
let rejected = null;
|
|
3127
3157
|
const cancel = () => {
|
|
3128
3158
|
if (timer) clearTimeout(timer);
|
|
3129
3159
|
timer = null;
|
|
3130
3160
|
dirtyDuringFlight = false;
|
|
3131
3161
|
pending = false;
|
|
3162
|
+
rejected = null;
|
|
3132
3163
|
};
|
|
3133
3164
|
const patchBody = (order) => {
|
|
3134
3165
|
const b1 = {
|
|
@@ -3211,11 +3242,20 @@ const createFlushController = (runtime) => {
|
|
|
3211
3242
|
await flushNow();
|
|
3212
3243
|
}
|
|
3213
3244
|
};
|
|
3245
|
+
const throwRejected = () => {
|
|
3246
|
+
if (!rejected) return;
|
|
3247
|
+
const error = rejected;
|
|
3248
|
+
rejected = null;
|
|
3249
|
+
throw error;
|
|
3250
|
+
};
|
|
3214
3251
|
const settle = async () => {
|
|
3215
3252
|
await drain();
|
|
3216
|
-
if (
|
|
3253
|
+
if (runtime.isDeleted()) return;
|
|
3254
|
+
throwRejected();
|
|
3255
|
+
if (!pending) return;
|
|
3217
3256
|
await flushNow();
|
|
3218
3257
|
await drain();
|
|
3258
|
+
throwRejected();
|
|
3219
3259
|
if (pending) throw new OrderSyncPendingError();
|
|
3220
3260
|
};
|
|
3221
3261
|
const resume = async () => {
|
|
@@ -3239,6 +3279,7 @@ const createFlushController = (runtime) => {
|
|
|
3239
3279
|
return services.orders.patch(...p1);
|
|
3240
3280
|
}, { sleep: runtime.sleep });
|
|
3241
3281
|
pending = false;
|
|
3282
|
+
rejected = null;
|
|
3242
3283
|
runtime.setPersisted(true);
|
|
3243
3284
|
runtime.setSynced(updated);
|
|
3244
3285
|
const reusedOpen = !wasPersisted && updated.id !== payload.id;
|
|
@@ -3273,6 +3314,7 @@ const createFlushController = (runtime) => {
|
|
|
3273
3314
|
return;
|
|
3274
3315
|
}
|
|
3275
3316
|
pending = false;
|
|
3317
|
+
rejected = new OrderSyncRejectedError(error);
|
|
3276
3318
|
runtime.setDraft(batchBase);
|
|
3277
3319
|
runtime.setSynced(batchBase);
|
|
3278
3320
|
runtime.setPersisted(wasPersisted);
|
|
@@ -3306,7 +3348,8 @@ const createFlushController = (runtime) => {
|
|
|
3306
3348
|
settle,
|
|
3307
3349
|
resume,
|
|
3308
3350
|
getInFlight: () => inFlight,
|
|
3309
|
-
getPending: () => pending
|
|
3351
|
+
getPending: () => pending,
|
|
3352
|
+
getRejected: () => rejected
|
|
3310
3353
|
};
|
|
3311
3354
|
};
|
|
3312
3355
|
//#endregion
|
|
@@ -3454,6 +3497,13 @@ const emptyLines = () => {
|
|
|
3454
3497
|
//#region src/order/tenders.ts
|
|
3455
3498
|
/** Catalog provider of `REDEMPTION` payments (§3.3). */
|
|
3456
3499
|
const DEFAULT_REDEMPTION_PROVIDER = "firespark";
|
|
3500
|
+
const isFixedFee = (line) => isTipFee(line) || isDeliveryFee(line);
|
|
3501
|
+
const positive = (value) => value.gt(0) ? value : moneyZero();
|
|
3502
|
+
/** What a redeem may cover on a root line: pre-tax value or with taxes. */
|
|
3503
|
+
const coverableOf = (line, beforeTaxes) => {
|
|
3504
|
+
const total = money(line.group_total);
|
|
3505
|
+
return positive(beforeTaxes ? total.minus(line.group_tax_total) : total);
|
|
3506
|
+
};
|
|
3457
3507
|
/**
|
|
3458
3508
|
* Digital tender the SDK charges (`DEVICE`, §7.3). `KIOSK`, `COUNTER`,
|
|
3459
3509
|
* `DOOR` and `OTHER` are in person: the POS, an operator or the courier
|
|
@@ -3462,42 +3512,53 @@ const DEFAULT_REDEMPTION_PROVIDER = "firespark";
|
|
|
3462
3512
|
const isDeviceCharged = (tender) => {
|
|
3463
3513
|
return DIGITAL_COLLECT_AT.includes(tender.collect_at);
|
|
3464
3514
|
};
|
|
3465
|
-
/** Most a REDEMPTION can cover: order total − tip − delivery (D18). */
|
|
3466
|
-
const redemptionCap = (order) => {
|
|
3467
|
-
const primaryOnly = order.lines.filter((line) => line.parent_uid == null).reduce((sum, line) => {
|
|
3468
|
-
if (!(isTipFee(line) || isDeliveryFee(line))) return sum;
|
|
3469
|
-
return sum.plus(line.group_total);
|
|
3470
|
-
}, moneyZero());
|
|
3471
|
-
const cap = money(order.totals.total).minus(primaryOnly);
|
|
3472
|
-
if (cap.lt(0)) return 0;
|
|
3473
|
-
return moneyNumber(cap);
|
|
3474
|
-
};
|
|
3475
3515
|
/**
|
|
3476
|
-
*
|
|
3477
|
-
*
|
|
3516
|
+
* Most the redeems can cover (D18): order total − tip − delivery. With
|
|
3517
|
+
* `beforeTaxes`, the taxes of the other roots are excluded too, so they
|
|
3518
|
+
* stay for the customer's method.
|
|
3478
3519
|
*/
|
|
3479
|
-
const
|
|
3480
|
-
const
|
|
3481
|
-
return sum.plus(
|
|
3520
|
+
const redemptionCap = (order, beforeTaxes = false) => {
|
|
3521
|
+
const excluded = order.lines.filter((line) => line.parent_uid == null).reduce((sum, line) => {
|
|
3522
|
+
if (isFixedFee(line)) return sum.plus(line.group_total);
|
|
3523
|
+
if (!beforeTaxes) return sum;
|
|
3524
|
+
return sum.plus(line.group_tax_total);
|
|
3482
3525
|
}, moneyZero());
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3526
|
+
return moneyNumber(positive(money(order.totals.total).minus(excluded)));
|
|
3527
|
+
};
|
|
3528
|
+
const fitRedeems = (order) => {
|
|
3529
|
+
const gross = money(redemptionCap(order));
|
|
3530
|
+
const net = money(redemptionCap(order, true));
|
|
3531
|
+
let used = moneyZero();
|
|
3532
|
+
const fit = (item, line, lineUsed) => {
|
|
3533
|
+
if (!hasRedeemPayment(item) || !(item.amount > 0)) return moneyZero();
|
|
3534
|
+
const beforeTaxes = item.before_taxes ?? true;
|
|
3535
|
+
let room = (beforeTaxes ? net : gross).minus(used);
|
|
3536
|
+
if (line) {
|
|
3537
|
+
const left = coverableOf(line, beforeTaxes).minus(lineUsed);
|
|
3538
|
+
room = moneyMin(room, left);
|
|
3539
|
+
}
|
|
3540
|
+
const fits = moneyMin(money(item.amount), positive(room));
|
|
3541
|
+
used = used.plus(fits);
|
|
3542
|
+
return fits;
|
|
3543
|
+
};
|
|
3544
|
+
return {
|
|
3545
|
+
lines: order.lines.map((line) => {
|
|
3546
|
+
if (line.parent_uid != null) return (line.redeem ?? []).map(moneyZero);
|
|
3547
|
+
let lineUsed = moneyZero();
|
|
3548
|
+
return (line.redeem ?? []).map((item) => {
|
|
3549
|
+
const fits = fit(item, line, lineUsed);
|
|
3550
|
+
lineUsed = lineUsed.plus(fits);
|
|
3551
|
+
return fits;
|
|
3552
|
+
});
|
|
3553
|
+
}),
|
|
3554
|
+
redeem: (order.redeem ?? []).map((item) => fit(item))
|
|
3555
|
+
};
|
|
3486
3556
|
};
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
* room remains, so the kept amounts sum to at most `cap`.
|
|
3491
|
-
*/
|
|
3492
|
-
const clampPaymentRedeems = (redeem, cap, _currency) => {
|
|
3493
|
-
let left = money(cap);
|
|
3494
|
-
return redeem.flatMap((item) => {
|
|
3495
|
-
if (!isPaymentRedeem(item)) return [item];
|
|
3496
|
-
const amount = money(item.amount ?? 0);
|
|
3497
|
-
const fits = amount.lt(left) ? amount : left;
|
|
3498
|
-
left = left.minus(fits);
|
|
3557
|
+
const keepFitted = (items, fitted) => {
|
|
3558
|
+
return items.flatMap((item, index) => {
|
|
3559
|
+
const fits = fitted[index] ?? moneyZero();
|
|
3499
3560
|
if (fits.lte(0)) return [];
|
|
3500
|
-
if (fits.eq(amount)) return [item];
|
|
3561
|
+
if (fits.eq(item.amount)) return [item];
|
|
3501
3562
|
return [{
|
|
3502
3563
|
...item,
|
|
3503
3564
|
amount: moneyNumber(fits)
|
|
@@ -3505,11 +3566,37 @@ const clampPaymentRedeems = (redeem, cap, _currency) => {
|
|
|
3505
3566
|
});
|
|
3506
3567
|
};
|
|
3507
3568
|
/**
|
|
3508
|
-
*
|
|
3509
|
-
*
|
|
3569
|
+
* Trims every redeem to what `fitRedeems` lets it cover; redeems with no
|
|
3570
|
+
* room left are dropped. Line and order money is never changed: the
|
|
3571
|
+
* REDEMPTION tender pays the kept `amount`.
|
|
3572
|
+
*/
|
|
3573
|
+
const capRedeems = (order) => {
|
|
3574
|
+
const fitted = fitRedeems(order);
|
|
3575
|
+
const lines = order.lines.map((line, index) => {
|
|
3576
|
+
if (!line.redeem?.length) return line;
|
|
3577
|
+
const redeem = keepFitted(line.redeem, fitted.lines[index] ?? []);
|
|
3578
|
+
return {
|
|
3579
|
+
...line,
|
|
3580
|
+
redeem
|
|
3581
|
+
};
|
|
3582
|
+
});
|
|
3583
|
+
const redeem = keepFitted(order.redeem ?? [], fitted.redeem);
|
|
3584
|
+
return {
|
|
3585
|
+
...order,
|
|
3586
|
+
lines,
|
|
3587
|
+
redeem
|
|
3588
|
+
};
|
|
3589
|
+
};
|
|
3590
|
+
/** Every redeem of the order after `capRedeems`, ready for tenders. */
|
|
3591
|
+
const cappedRedeems = (order) => {
|
|
3592
|
+
return orderRedeems(capRedeems(order));
|
|
3593
|
+
};
|
|
3594
|
+
/**
|
|
3595
|
+
* One REDEMPTION tender per `payment_id` (D15, D19); its
|
|
3596
|
+
* `details.rewards[]` lists every grouped redeem.
|
|
3510
3597
|
*/
|
|
3511
3598
|
const redemptionTenderInputs = (redeem, providers = /* @__PURE__ */ new Map()) => {
|
|
3512
|
-
return
|
|
3599
|
+
return groupRedeems(redeem).map(({ payment_id, details }) => {
|
|
3513
3600
|
const fallback = DEFAULT_REDEMPTION_PROVIDER;
|
|
3514
3601
|
const t1 = {
|
|
3515
3602
|
method: "REDEMPTION",
|
|
@@ -3915,8 +4002,7 @@ const partialOf = (result) => {
|
|
|
3915
4002
|
const isPartialApproval = (result, tender, ctx) => {
|
|
3916
4003
|
if (result.status !== "PAID") return false;
|
|
3917
4004
|
if (ctx.amount.currency !== tender.amount.currency) return false;
|
|
3918
|
-
|
|
3919
|
-
return result.amount + tolerance < tender.amount.total;
|
|
4005
|
+
return money(result.amount).lt(tender.amount.total);
|
|
3920
4006
|
};
|
|
3921
4007
|
const toFailure = (error) => {
|
|
3922
4008
|
const message = error instanceof Error ? error.message : "Terminal failed";
|
|
@@ -4660,109 +4746,38 @@ const collectRecommendations = (config) => {
|
|
|
4660
4746
|
return products;
|
|
4661
4747
|
};
|
|
4662
4748
|
//#endregion
|
|
4663
|
-
//#region src/order/totals.ts
|
|
4664
|
-
/** Sum root line group totals into order totals (Big.js). */
|
|
4665
|
-
const recomputeTotals = (lines, currency = "USD") => {
|
|
4666
|
-
let subtotal = new Big(0);
|
|
4667
|
-
let discountTotal = new Big(0);
|
|
4668
|
-
let taxTotal = new Big(0);
|
|
4669
|
-
let total = new Big(0);
|
|
4670
|
-
for (const line of lines) {
|
|
4671
|
-
if (line.parent_uid != null) continue;
|
|
4672
|
-
subtotal = subtotal.plus(line.group_subtotal);
|
|
4673
|
-
discountTotal = discountTotal.plus(line.group_discount);
|
|
4674
|
-
taxTotal = taxTotal.plus(line.group_tax_total);
|
|
4675
|
-
total = total.plus(line.group_total);
|
|
4676
|
-
}
|
|
4677
|
-
let taxRate = 0;
|
|
4678
|
-
let discountRate = 0;
|
|
4679
|
-
if (subtotal.gt(0)) {
|
|
4680
|
-
taxRate = taxTotal.div(subtotal).toNumber();
|
|
4681
|
-
discountRate = discountTotal.div(subtotal).toNumber();
|
|
4682
|
-
}
|
|
4683
|
-
const discount_total = discountTotal.toNumber();
|
|
4684
|
-
const tax_total = taxTotal.toNumber();
|
|
4685
|
-
const b1 = {
|
|
4686
|
-
currency,
|
|
4687
|
-
subtotal: subtotal.toNumber()
|
|
4688
|
-
};
|
|
4689
|
-
const b2 = {
|
|
4690
|
-
discount_rate: discountRate,
|
|
4691
|
-
discount_total
|
|
4692
|
-
};
|
|
4693
|
-
const b3 = {
|
|
4694
|
-
tax_rate: taxRate,
|
|
4695
|
-
tax_total,
|
|
4696
|
-
total: total.toNumber()
|
|
4697
|
-
};
|
|
4698
|
-
return {
|
|
4699
|
-
...b1,
|
|
4700
|
-
...b2,
|
|
4701
|
-
...b3
|
|
4702
|
-
};
|
|
4703
|
-
};
|
|
4704
|
-
const emptyTotals = (currency = "USD") => {
|
|
4705
|
-
const b1 = {
|
|
4706
|
-
currency,
|
|
4707
|
-
subtotal: 0,
|
|
4708
|
-
discount_rate: 0,
|
|
4709
|
-
discount_total: 0
|
|
4710
|
-
};
|
|
4711
|
-
const b2 = {
|
|
4712
|
-
tax_rate: 0,
|
|
4713
|
-
tax_total: 0,
|
|
4714
|
-
total: 0
|
|
4715
|
-
};
|
|
4716
|
-
return {
|
|
4717
|
-
...b1,
|
|
4718
|
-
...b2
|
|
4719
|
-
};
|
|
4720
|
-
};
|
|
4721
|
-
//#endregion
|
|
4722
4749
|
//#region src/order/redemptions.ts
|
|
4723
|
-
const ORDER_KEY = "ORDER";
|
|
4724
|
-
const buildPaymentRedeem = (input) => {
|
|
4725
|
-
const { reward_id, quantity, payment_id, amount } = input;
|
|
4726
|
-
if (amount === void 0 || !(amount > 0)) throw new Error("Redeem with payment_id requires amount > 0");
|
|
4727
|
-
const redeem = {
|
|
4728
|
-
reward_id,
|
|
4729
|
-
quantity,
|
|
4730
|
-
payment_id,
|
|
4731
|
-
amount
|
|
4732
|
-
};
|
|
4733
|
-
if (input.expires_at !== void 0) redeem.expires_at = input.expires_at;
|
|
4734
|
-
return redeem;
|
|
4735
|
-
};
|
|
4736
4750
|
/** Same rules as `vOrders.redeem()`; throws on invalid input. */
|
|
4737
4751
|
const buildRedeem = (input) => {
|
|
4738
|
-
const { reward_id, quantity,
|
|
4752
|
+
const { reward_id, quantity, payment_id, amount } = input;
|
|
4739
4753
|
const { before_taxes, expires_at } = input;
|
|
4740
|
-
if (!reward_id.trim()) throw new Error("Redeem requires reward_id");
|
|
4754
|
+
if (typeof reward_id !== "string" || !reward_id.trim()) throw new Error("Redeem requires reward_id");
|
|
4741
4755
|
if (!Number.isInteger(quantity) || quantity < 1) throw new Error("Redeem quantity must be an integer ≥ 1");
|
|
4756
|
+
if (typeof payment_id !== "string" || !payment_id.trim()) throw new Error("Redeem requires payment_id");
|
|
4757
|
+
if (typeof amount !== "number" || !(amount > 0)) throw new Error("Redeem requires amount > 0");
|
|
4742
4758
|
if (expires_at !== void 0 && Number.isNaN(Date.parse(expires_at))) throw new Error("Redeem expires_at must be an ISO datetime");
|
|
4743
|
-
if (input.payment_id) return buildPaymentRedeem(input);
|
|
4744
|
-
if (discount_rate === void 0 && discount_amount === void 0) throw new Error("Redeem requires discount_rate or discount_amount");
|
|
4745
|
-
const rate = discount_rate ?? 0;
|
|
4746
|
-
if (!(rate >= 0 && rate <= 1)) throw new Error("Redeem discount_rate must be between 0 and 1");
|
|
4747
|
-
if (discount_amount !== void 0 && !(discount_amount > 0)) throw new Error("Redeem discount_amount must be > 0");
|
|
4748
4759
|
const redeem = {
|
|
4749
4760
|
reward_id,
|
|
4750
|
-
quantity
|
|
4761
|
+
quantity,
|
|
4762
|
+
payment_id,
|
|
4763
|
+
amount
|
|
4751
4764
|
};
|
|
4752
|
-
if (discount_rate !== void 0) redeem.discount_rate = discount_rate;
|
|
4753
|
-
if (discount_amount !== void 0) redeem.discount_amount = discount_amount;
|
|
4754
4765
|
if (before_taxes !== void 0) redeem.before_taxes = before_taxes;
|
|
4755
4766
|
if (expires_at !== void 0) redeem.expires_at = expires_at;
|
|
4756
4767
|
return redeem;
|
|
4757
4768
|
};
|
|
4758
|
-
/**
|
|
4769
|
+
/**
|
|
4770
|
+
* `amount` covers every unit, so the kept units keep what the removed ones
|
|
4771
|
+
* did not take: `amount − amount × taken / quantity`, never rounded.
|
|
4772
|
+
*/
|
|
4759
4773
|
const keepUnits = (item, rest) => {
|
|
4760
|
-
if (
|
|
4774
|
+
if (rest === item.quantity) return {
|
|
4761
4775
|
...item,
|
|
4762
4776
|
quantity: rest
|
|
4763
4777
|
};
|
|
4764
|
-
const
|
|
4765
|
-
const
|
|
4778
|
+
const taken = item.quantity - rest;
|
|
4779
|
+
const share = new Big(item.amount).times(taken).div(item.quantity);
|
|
4780
|
+
const amount = new Big(item.amount).minus(share).toNumber();
|
|
4766
4781
|
return {
|
|
4767
4782
|
...item,
|
|
4768
4783
|
quantity: rest,
|
|
@@ -4770,8 +4785,8 @@ const keepUnits = (item, rest) => {
|
|
|
4770
4785
|
};
|
|
4771
4786
|
};
|
|
4772
4787
|
/**
|
|
4773
|
-
* Takes `quantity` units of `reward_id` off the latest entries first
|
|
4774
|
-
*
|
|
4788
|
+
* Takes `quantity` units of `reward_id` off the latest entries first.
|
|
4789
|
+
* Entries that reach 0 are removed.
|
|
4775
4790
|
*/
|
|
4776
4791
|
const subtractRedeem = (items, reward_id, quantity) => {
|
|
4777
4792
|
if (!Number.isInteger(quantity) || quantity < 1) throw new Error("Redeem quantity must be an integer ≥ 1");
|
|
@@ -4790,143 +4805,43 @@ const subtractRedeem = (items, reward_id, quantity) => {
|
|
|
4790
4805
|
}
|
|
4791
4806
|
return kept;
|
|
4792
4807
|
};
|
|
4793
|
-
const redemptionTargetKey = (target) => {
|
|
4794
|
-
if (target.target === "ORDER") return ORDER_KEY;
|
|
4795
|
-
return `LINE:${target.target_id ?? ""}`;
|
|
4796
|
-
};
|
|
4797
4808
|
/**
|
|
4798
|
-
*
|
|
4799
|
-
*
|
|
4809
|
+
* Money the redeems cover (Σ `amount`): the discount the customer sees.
|
|
4810
|
+
* It is paid by REDEMPTION tenders, so it is never subtracted from line
|
|
4811
|
+
* or order totals.
|
|
4800
4812
|
*/
|
|
4801
|
-
const
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
const pool = items.map((item) => ({
|
|
4811
|
-
...target,
|
|
4812
|
-
redeem: item
|
|
4813
|
-
}));
|
|
4814
|
-
pools.set(redemptionTargetKey(target), pool);
|
|
4815
|
-
}
|
|
4816
|
-
const order = { target: "ORDER" };
|
|
4817
|
-
const pool = discountRedeems(redeem).map((item) => ({
|
|
4818
|
-
...order,
|
|
4819
|
-
redeem: item
|
|
4820
|
-
}));
|
|
4821
|
-
pools.set(ORDER_KEY, pool);
|
|
4822
|
-
return pools;
|
|
4813
|
+
const redeemedTotal = (redeem = []) => {
|
|
4814
|
+
return (redeem ?? []).reduce((sum, item) => {
|
|
4815
|
+
if (!hasRedeemPayment(item)) return sum;
|
|
4816
|
+
return sum.plus(item.amount ?? 0);
|
|
4817
|
+
}, new Big(0)).toNumber();
|
|
4818
|
+
};
|
|
4819
|
+
/** Redeemed money of every redeem of the order (order + root lines). */
|
|
4820
|
+
const orderRedeemedTotal = (order) => {
|
|
4821
|
+
return redeemedTotal(orderRedeems$1(order));
|
|
4823
4822
|
};
|
|
4824
4823
|
/**
|
|
4825
|
-
*
|
|
4826
|
-
*
|
|
4827
|
-
* step are appended (lines first, then order).
|
|
4824
|
+
* What the customer pays with another method:
|
|
4825
|
+
* `totals.total − Σ redeem amounts` (order + root lines), never below 0.
|
|
4828
4826
|
*/
|
|
4829
|
-
const
|
|
4830
|
-
const
|
|
4831
|
-
|
|
4832
|
-
const sequence = [];
|
|
4833
|
-
for (const step of steps) {
|
|
4834
|
-
const key = redemptionTargetKey(step);
|
|
4835
|
-
const at = cursors.get(key) ?? 0;
|
|
4836
|
-
const next = pools.get(key)?.[at];
|
|
4837
|
-
if (!next || next.redeem.reward_id !== step.reward_id) continue;
|
|
4838
|
-
cursors.set(key, at + 1);
|
|
4839
|
-
sequence.push(next);
|
|
4840
|
-
}
|
|
4841
|
-
for (const [key, pool] of pools) sequence.push(...pool.slice(cursors.get(key) ?? 0));
|
|
4842
|
-
return sequence;
|
|
4843
|
-
};
|
|
4844
|
-
const minBig = (a, b) => {
|
|
4845
|
-
return a.lt(b) ? a : b;
|
|
4846
|
-
};
|
|
4847
|
-
const rateOf = (discount, base) => {
|
|
4848
|
-
if (base.lte(0)) return 0;
|
|
4849
|
-
return minBig(discount.div(base), new Big(1)).toNumber();
|
|
4850
|
-
};
|
|
4851
|
-
const snapshot = (roots, quantity) => {
|
|
4852
|
-
let subtotal = new Big(0);
|
|
4853
|
-
let total = new Big(0);
|
|
4854
|
-
let discount = new Big(0);
|
|
4855
|
-
for (const root of roots) {
|
|
4856
|
-
subtotal = subtotal.plus(root.subtotal);
|
|
4857
|
-
total = total.plus(root.total);
|
|
4858
|
-
discount = discount.plus(root.discount);
|
|
4859
|
-
}
|
|
4860
|
-
const b1 = {
|
|
4861
|
-
value: total.toNumber(),
|
|
4862
|
-
quantity
|
|
4863
|
-
};
|
|
4864
|
-
const b2 = { discount_amount: discount.toNumber() };
|
|
4865
|
-
const b3 = { discount_rate: rateOf(discount, subtotal) };
|
|
4866
|
-
return {
|
|
4867
|
-
...b1,
|
|
4868
|
-
...b2,
|
|
4869
|
-
...b3
|
|
4870
|
-
};
|
|
4871
|
-
};
|
|
4872
|
-
const netOf = (root) => {
|
|
4873
|
-
const net = root.total.minus(root.tax);
|
|
4874
|
-
return net.gt(0) ? net : new Big(0);
|
|
4875
|
-
};
|
|
4876
|
-
/** What a redeem can still discount on a root: pre-tax or with taxes. */
|
|
4877
|
-
const baseOf = (root, beforeTaxes) => {
|
|
4878
|
-
return beforeTaxes ? netOf(root) : root.total;
|
|
4879
|
-
};
|
|
4880
|
-
/** Before taxes, taxes shrink with the pre-tax value they were charged on. */
|
|
4881
|
-
const discountRoot = (root, amount, beforeTaxes) => {
|
|
4882
|
-
if (amount.lte(0)) return;
|
|
4883
|
-
root.discount = root.discount.plus(amount);
|
|
4884
|
-
if (!beforeTaxes) {
|
|
4885
|
-
root.total = root.total.minus(amount);
|
|
4886
|
-
return;
|
|
4887
|
-
}
|
|
4888
|
-
const net = netOf(root);
|
|
4889
|
-
const nextNet = net.minus(amount);
|
|
4890
|
-
root.tax = root.tax.times(nextNet).div(net);
|
|
4891
|
-
root.total = nextNet.plus(root.tax);
|
|
4892
|
-
};
|
|
4893
|
-
/** Splits an order discount across roots by what each can still discount. */
|
|
4894
|
-
const discountOrder = (roots, amount, beforeTaxes) => {
|
|
4895
|
-
const bases = roots.map((root) => baseOf(root, beforeTaxes));
|
|
4896
|
-
const sum = bases.reduce((acc, base) => acc.plus(base), new Big(0));
|
|
4897
|
-
if (sum.lte(0) || amount.lte(0)) return;
|
|
4898
|
-
let last = -1;
|
|
4899
|
-
bases.forEach((base, index) => {
|
|
4900
|
-
if (base.gt(0)) last = index;
|
|
4901
|
-
});
|
|
4902
|
-
let rest = amount;
|
|
4903
|
-
roots.forEach((root, index) => {
|
|
4904
|
-
const base = bases[index];
|
|
4905
|
-
if (base.lte(0)) return;
|
|
4906
|
-
const share = amount.times(base).div(sum);
|
|
4907
|
-
const part = minBig(index === last ? rest : share, base);
|
|
4908
|
-
rest = rest.minus(part);
|
|
4909
|
-
discountRoot(root, part, beforeTaxes);
|
|
4910
|
-
});
|
|
4827
|
+
const orderPayableTotal = (order) => {
|
|
4828
|
+
const rest = new Big(order.totals.total).minus(orderRedeemedTotal(order));
|
|
4829
|
+
return rest.gt(0) ? rest.toNumber() : 0;
|
|
4911
4830
|
};
|
|
4912
4831
|
/**
|
|
4913
|
-
*
|
|
4914
|
-
*
|
|
4832
|
+
* What the customer pays for a root line with another method:
|
|
4833
|
+
* `group_total − Σ line.redeem[].amount`, never below 0.
|
|
4915
4834
|
*/
|
|
4916
|
-
const
|
|
4917
|
-
const
|
|
4918
|
-
|
|
4919
|
-
if (discount_amount !== void 0) cap = new Big(discount_amount).times(quantity);
|
|
4920
|
-
if (discount_rate === void 0) return cap ?? new Big(0);
|
|
4921
|
-
const kept = new Big(1).minus(discount_rate).pow(quantity);
|
|
4922
|
-
const byRate = remaining.times(new Big(1).minus(kept));
|
|
4923
|
-
return cap ? minBig(byRate, cap) : byRate;
|
|
4835
|
+
const linePayableTotal = (line) => {
|
|
4836
|
+
const rest = new Big(line.group_total).minus(redeemedTotal(line.redeem));
|
|
4837
|
+
return rest.gt(0) ? rest.toNumber() : 0;
|
|
4924
4838
|
};
|
|
4925
4839
|
/**
|
|
4926
|
-
* Root group money rebuilt from the line itself plus its children
|
|
4927
|
-
*
|
|
4840
|
+
* Root group money rebuilt from the line itself plus its children. Legacy
|
|
4841
|
+
* orders carried their redeems in the root's group money; rebuilding drops
|
|
4842
|
+
* it so line money stays full.
|
|
4928
4843
|
*/
|
|
4929
|
-
const
|
|
4844
|
+
const rebuildRootGroups = (lines) => {
|
|
4930
4845
|
const children = /* @__PURE__ */ new Map();
|
|
4931
4846
|
for (const line of lines) {
|
|
4932
4847
|
if (line.parent_uid == null) continue;
|
|
@@ -4975,111 +4890,162 @@ const stripRedemptionLines = (lines) => {
|
|
|
4975
4890
|
};
|
|
4976
4891
|
});
|
|
4977
4892
|
};
|
|
4978
|
-
const
|
|
4979
|
-
const
|
|
4980
|
-
const
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
const
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4893
|
+
const stateOf = (target) => {
|
|
4894
|
+
const rest = target.total.minus(target.covered);
|
|
4895
|
+
const left = rest.gt(0) ? rest : moneyZero();
|
|
4896
|
+
let rate = moneyZero();
|
|
4897
|
+
if (target.total.gt(0)) rate = moneyMin(target.covered.div(target.total), money(1));
|
|
4898
|
+
const s1 = {
|
|
4899
|
+
value: moneyNumber(left),
|
|
4900
|
+
quantity: target.quantity
|
|
4901
|
+
};
|
|
4902
|
+
const s2 = { discount_amount: moneyNumber(target.covered) };
|
|
4987
4903
|
return {
|
|
4988
|
-
...
|
|
4989
|
-
...
|
|
4990
|
-
|
|
4991
|
-
...u3,
|
|
4992
|
-
...g1,
|
|
4993
|
-
...g2,
|
|
4994
|
-
...g3
|
|
4904
|
+
...s1,
|
|
4905
|
+
...s2,
|
|
4906
|
+
discount_rate: rate.toNumber()
|
|
4995
4907
|
};
|
|
4996
4908
|
};
|
|
4997
4909
|
/**
|
|
4998
|
-
*
|
|
4999
|
-
*
|
|
5000
|
-
*
|
|
4910
|
+
* One step per redeem with a `payment_id`, in the order `fitRedeems`
|
|
4911
|
+
* applies them: root-line redeems in line order, then order redeems.
|
|
4912
|
+
* `discount_amount` is the capped `amount`, so the steps of a `payment_id`
|
|
4913
|
+
* add up to its REDEMPTION tender and the line steps to the discount shown
|
|
4914
|
+
* on the line. Lines, taxes and totals keep their full value; `value` is
|
|
4915
|
+
* what the customer's method still pays.
|
|
5001
4916
|
*/
|
|
5002
|
-
const
|
|
5003
|
-
const
|
|
5004
|
-
const
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
const
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
...b4
|
|
5020
|
-
});
|
|
5021
|
-
}
|
|
5022
|
-
const roots = [...moneyByUid.values()];
|
|
5023
|
-
const redemption_steps = sequence.map((item) => {
|
|
5024
|
-
const id = item.target_id;
|
|
5025
|
-
const root = id ? moneyByUid.get(id) : void 0;
|
|
5026
|
-
const beforeTaxes = item.redeem.before_taxes ?? true;
|
|
5027
|
-
const before = snapshot(roots, 1);
|
|
5028
|
-
const targetBefore = root ? snapshot([root], root.quantity) : void 0;
|
|
5029
|
-
let left = new Big(0);
|
|
5030
|
-
if (root) left = baseOf(root, beforeTaxes);
|
|
5031
|
-
else for (const r of roots) left = left.plus(baseOf(r, beforeTaxes));
|
|
5032
|
-
const raw = redeemDiscount(item.redeem, left);
|
|
5033
|
-
const discount = minBig(raw, left);
|
|
5034
|
-
if (root) discountRoot(root, discount, beforeTaxes);
|
|
5035
|
-
else discountOrder(roots, discount, beforeTaxes);
|
|
5036
|
-
const action = discount.gt(0) ? "UPDATE" : "READ";
|
|
5037
|
-
const { reward_id, quantity } = item.redeem;
|
|
5038
|
-
const b1 = {
|
|
5039
|
-
reward_id,
|
|
5040
|
-
reward_quantity: quantity,
|
|
5041
|
-
target: item.target
|
|
4917
|
+
const redemptionSteps = (order) => {
|
|
4918
|
+
const fitted = fitRedeems(order);
|
|
4919
|
+
const whole = {
|
|
4920
|
+
total: money(order.totals.total),
|
|
4921
|
+
quantity: 1,
|
|
4922
|
+
covered: moneyZero()
|
|
4923
|
+
};
|
|
4924
|
+
const steps = [];
|
|
4925
|
+
const apply = (item, fits, line) => {
|
|
4926
|
+
const before = stateOf(whole);
|
|
4927
|
+
const targetBefore = line ? stateOf(line) : void 0;
|
|
4928
|
+
whole.covered = whole.covered.plus(fits);
|
|
4929
|
+
if (line) line.covered = line.covered.plus(fits);
|
|
4930
|
+
const action = fits.gt(0) ? "UPDATE" : "READ";
|
|
4931
|
+
const s1 = {
|
|
4932
|
+
reward_id: item.reward_id,
|
|
4933
|
+
reward_quantity: item.quantity
|
|
5042
4934
|
};
|
|
5043
|
-
const
|
|
5044
|
-
const
|
|
5045
|
-
|
|
5046
|
-
|
|
4935
|
+
const s2 = { payment_id: item.payment_id };
|
|
4936
|
+
const s3 = {
|
|
4937
|
+
before_taxes: item.before_taxes ?? true,
|
|
4938
|
+
action
|
|
5047
4939
|
};
|
|
5048
|
-
const
|
|
4940
|
+
const s4 = { discount_amount: moneyNumber(fits) };
|
|
4941
|
+
const s5 = {
|
|
5049
4942
|
before,
|
|
5050
|
-
after:
|
|
5051
|
-
};
|
|
5052
|
-
let b5 = {};
|
|
5053
|
-
if (root) b5 = {
|
|
5054
|
-
target_before: targetBefore,
|
|
5055
|
-
target_after: snapshot([root], root.quantity)
|
|
4943
|
+
after: stateOf(whole)
|
|
5056
4944
|
};
|
|
5057
4945
|
return {
|
|
5058
|
-
...
|
|
5059
|
-
...
|
|
5060
|
-
...
|
|
5061
|
-
...
|
|
5062
|
-
...
|
|
4946
|
+
...s1,
|
|
4947
|
+
...s2,
|
|
4948
|
+
...s3,
|
|
4949
|
+
...s4,
|
|
4950
|
+
...s5,
|
|
4951
|
+
targetBefore
|
|
5063
4952
|
};
|
|
4953
|
+
};
|
|
4954
|
+
order.lines.forEach((line, index) => {
|
|
4955
|
+
if (line.parent_uid != null) return;
|
|
4956
|
+
const covered = moneyZero();
|
|
4957
|
+
const target = {
|
|
4958
|
+
total: money(line.group_total),
|
|
4959
|
+
quantity: line.quantity,
|
|
4960
|
+
covered
|
|
4961
|
+
};
|
|
4962
|
+
(line.redeem ?? []).forEach((item, at) => {
|
|
4963
|
+
if (!hasRedeemPayment(item)) return;
|
|
4964
|
+
const fits = fitted.lines[index]?.[at] ?? moneyZero();
|
|
4965
|
+
const { targetBefore, ...step } = apply(item, fits, target);
|
|
4966
|
+
const l1 = {
|
|
4967
|
+
target: "LINE",
|
|
4968
|
+
target_id: line.uid
|
|
4969
|
+
};
|
|
4970
|
+
const l2 = {
|
|
4971
|
+
target_before: targetBefore,
|
|
4972
|
+
target_after: stateOf(target)
|
|
4973
|
+
};
|
|
4974
|
+
steps.push({
|
|
4975
|
+
...step,
|
|
4976
|
+
...l1,
|
|
4977
|
+
...l2
|
|
4978
|
+
});
|
|
4979
|
+
});
|
|
4980
|
+
});
|
|
4981
|
+
(order.redeem ?? []).forEach((item, at) => {
|
|
4982
|
+
if (!hasRedeemPayment(item)) return;
|
|
4983
|
+
const fits = fitted.redeem[at] ?? moneyZero();
|
|
4984
|
+
const { targetBefore: _, ...step } = apply(item, fits);
|
|
4985
|
+
steps.push({
|
|
4986
|
+
...step,
|
|
4987
|
+
target: "ORDER"
|
|
4988
|
+
});
|
|
5064
4989
|
});
|
|
4990
|
+
return steps;
|
|
4991
|
+
};
|
|
4992
|
+
//#endregion
|
|
4993
|
+
//#region src/order/totals.ts
|
|
4994
|
+
/** Sum root line group totals into order totals (Big.js). */
|
|
4995
|
+
const recomputeTotals = (lines, currency = "USD") => {
|
|
4996
|
+
let subtotal = new Big(0);
|
|
4997
|
+
let discountTotal = new Big(0);
|
|
4998
|
+
let taxTotal = new Big(0);
|
|
4999
|
+
let total = new Big(0);
|
|
5000
|
+
for (const line of lines) {
|
|
5001
|
+
if (line.parent_uid != null) continue;
|
|
5002
|
+
subtotal = subtotal.plus(line.group_subtotal);
|
|
5003
|
+
discountTotal = discountTotal.plus(line.group_discount);
|
|
5004
|
+
taxTotal = taxTotal.plus(line.group_tax_total);
|
|
5005
|
+
total = total.plus(line.group_total);
|
|
5006
|
+
}
|
|
5007
|
+
let taxRate = 0;
|
|
5008
|
+
let discountRate = 0;
|
|
5009
|
+
if (subtotal.gt(0)) {
|
|
5010
|
+
taxRate = taxTotal.div(subtotal).toNumber();
|
|
5011
|
+
discountRate = discountTotal.div(subtotal).toNumber();
|
|
5012
|
+
}
|
|
5013
|
+
const discount_total = discountTotal.toNumber();
|
|
5014
|
+
const tax_total = taxTotal.toNumber();
|
|
5015
|
+
const b1 = {
|
|
5016
|
+
currency,
|
|
5017
|
+
subtotal: subtotal.toNumber()
|
|
5018
|
+
};
|
|
5019
|
+
const b2 = {
|
|
5020
|
+
discount_rate: discountRate,
|
|
5021
|
+
discount_total
|
|
5022
|
+
};
|
|
5023
|
+
const b3 = {
|
|
5024
|
+
tax_rate: taxRate,
|
|
5025
|
+
tax_total,
|
|
5026
|
+
total: total.toNumber()
|
|
5027
|
+
};
|
|
5065
5028
|
return {
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
return withRootMoney(line, money);
|
|
5070
|
-
}),
|
|
5071
|
-
redemption_steps
|
|
5029
|
+
...b1,
|
|
5030
|
+
...b2,
|
|
5031
|
+
...b3
|
|
5072
5032
|
};
|
|
5073
5033
|
};
|
|
5074
|
-
|
|
5075
|
-
const
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5034
|
+
const emptyTotals = (currency = "USD") => {
|
|
5035
|
+
const b1 = {
|
|
5036
|
+
currency,
|
|
5037
|
+
subtotal: 0,
|
|
5038
|
+
discount_rate: 0,
|
|
5039
|
+
discount_total: 0
|
|
5040
|
+
};
|
|
5041
|
+
const b2 = {
|
|
5042
|
+
tax_rate: 0,
|
|
5043
|
+
tax_total: 0,
|
|
5044
|
+
total: 0
|
|
5045
|
+
};
|
|
5079
5046
|
return {
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
totals: recomputeTotals(lines, currency)
|
|
5047
|
+
...b1,
|
|
5048
|
+
...b2
|
|
5083
5049
|
};
|
|
5084
5050
|
};
|
|
5085
5051
|
//#endregion
|
|
@@ -5087,9 +5053,9 @@ const recomputeRedemptions = (input) => {
|
|
|
5087
5053
|
const cloneOrder = (order) => {
|
|
5088
5054
|
return structuredClone(order);
|
|
5089
5055
|
};
|
|
5090
|
-
const redeemSignature
|
|
5056
|
+
const redeemSignature = (items) => {
|
|
5091
5057
|
return items.map((item) => {
|
|
5092
|
-
return `${`${item.reward_id}:${item.quantity}:${item.payment_id ?? ""}`}:${`${item.amount
|
|
5058
|
+
return `${`${item.reward_id}:${item.quantity}:${item.payment_id ?? ""}`}:${`${item.amount}:${item.before_taxes ?? ""}`}`;
|
|
5093
5059
|
}).join("|");
|
|
5094
5060
|
};
|
|
5095
5061
|
const buildHttpOptions = (options) => {
|
|
@@ -5150,14 +5116,14 @@ const buildInsertFromSource = (options, source) => {
|
|
|
5150
5116
|
if (options.scope?.brand_id !== void 0) brand_id = optionalBrandId(options.scope.brand_id);
|
|
5151
5117
|
let customer = source.customer;
|
|
5152
5118
|
if (options.customer) customer = buildCustomerSnapshot(options.customer);
|
|
5153
|
-
const sourceLines = structuredClone(source.lines);
|
|
5154
5119
|
const is_anonymous = options.isAnonymous ?? source.is_anonymous;
|
|
5155
5120
|
const redeem = structuredClone(source.redeem ?? []);
|
|
5156
|
-
const
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5121
|
+
const lines = rebuildRootGroups(structuredClone(source.lines));
|
|
5122
|
+
const totals = recomputeTotals(lines, source.totals.currency);
|
|
5123
|
+
const redemption_steps = redemptionSteps({
|
|
5124
|
+
lines,
|
|
5125
|
+
totals,
|
|
5126
|
+
redeem
|
|
5161
5127
|
});
|
|
5162
5128
|
const b1 = {
|
|
5163
5129
|
id: orderId,
|
|
@@ -5503,20 +5469,20 @@ var Order = class Order {
|
|
|
5503
5469
|
});
|
|
5504
5470
|
}
|
|
5505
5471
|
/**
|
|
5506
|
-
*
|
|
5507
|
-
*
|
|
5508
|
-
*
|
|
5509
|
-
*
|
|
5510
|
-
*
|
|
5511
|
-
*
|
|
5512
|
-
*
|
|
5472
|
+
* Applies a redeem (points, voucher, free product…) to `line_uid` (root
|
|
5473
|
+
* product line) or to the order. `payment_id` and `amount` are required:
|
|
5474
|
+
* `amount` is what the redeem covers for the whole entry and is shown as
|
|
5475
|
+
* the line / order discount, while line and order totals keep their full
|
|
5476
|
+
* value. The REDEMPTION tender of `payment_id` pays `amount`; the
|
|
5477
|
+
* customer's method pays the rest (the taxes when `before_taxes`, the
|
|
5478
|
+
* default, keeps them out of the redeem). Redeems with the same
|
|
5479
|
+
* `payment_id` share one tender (D15, D18, D19).
|
|
5513
5480
|
*/
|
|
5514
5481
|
applyRedeem(input) {
|
|
5515
5482
|
if (this.deleted) return;
|
|
5516
5483
|
const { line_uid, provider, ...rest } = input;
|
|
5517
5484
|
const redeem = buildRedeem(rest);
|
|
5518
|
-
if (
|
|
5519
|
-
if (redeem.payment_id && provider) this.redemptionProviders.set(redeem.payment_id, provider);
|
|
5485
|
+
if (provider) this.redemptionProviders.set(redeem.payment_id, provider);
|
|
5520
5486
|
if (line_uid === void 0) {
|
|
5521
5487
|
this.commitLines({ redeem: [...this.appliedRedeem(), redeem] });
|
|
5522
5488
|
return;
|
|
@@ -5535,8 +5501,8 @@ var Order = class Order {
|
|
|
5535
5501
|
/**
|
|
5536
5502
|
* Removes `quantity` units of `reward_id` from `line_uid` or the order,
|
|
5537
5503
|
* latest applied first. Removing more units than applied clears them all
|
|
5538
|
-
* (and
|
|
5539
|
-
*
|
|
5504
|
+
* (and their REDEMPTION tender). A redeem that keeps some units keeps
|
|
5505
|
+
* their share of its `amount`.
|
|
5540
5506
|
*/
|
|
5541
5507
|
removeRedeem(input) {
|
|
5542
5508
|
if (this.deleted) return;
|
|
@@ -5692,10 +5658,10 @@ var Order = class Order {
|
|
|
5692
5658
|
lines: draft.lines,
|
|
5693
5659
|
redeem: draft.redeem ?? []
|
|
5694
5660
|
};
|
|
5695
|
-
const
|
|
5661
|
+
const redemption_steps = redemptionSteps(draft);
|
|
5696
5662
|
const b5 = {
|
|
5697
5663
|
totals: draft.totals,
|
|
5698
|
-
redemption_steps
|
|
5664
|
+
redemption_steps
|
|
5699
5665
|
};
|
|
5700
5666
|
const body = {
|
|
5701
5667
|
...b1,
|
|
@@ -5707,15 +5673,16 @@ var Order = class Order {
|
|
|
5707
5673
|
await services.orders.validate(body, this.http);
|
|
5708
5674
|
}
|
|
5709
5675
|
/**
|
|
5710
|
-
* Tenders of the next intent:
|
|
5711
|
-
*
|
|
5712
|
-
* cover the total, only
|
|
5676
|
+
* Tenders of the next intent: one REDEMPTION per redeem `payment_id`
|
|
5677
|
+
* (order and lines, capped) plus the customer's method for the rest,
|
|
5678
|
+
* e.g. the taxes. When the redeems cover the whole total, only
|
|
5679
|
+
* REDEMPTION tenders.
|
|
5713
5680
|
*/
|
|
5714
5681
|
getTenders(payment) {
|
|
5715
|
-
const redeem =
|
|
5682
|
+
const redeem = cappedRedeems(this.draft);
|
|
5716
5683
|
const providers = this.redemptionProviders;
|
|
5717
5684
|
const redemption = redemptionTenderInputs(redeem, providers);
|
|
5718
|
-
if (
|
|
5685
|
+
if (redeemsCoverTotal(redeem, this.draft.totals)) return redemption;
|
|
5719
5686
|
const simple = payment ? simpleTenderInput(payment) : null;
|
|
5720
5687
|
return [...simple ? [simple] : this.realTenders ?? [], ...redemption];
|
|
5721
5688
|
}
|
|
@@ -5738,6 +5705,13 @@ var Order = class Order {
|
|
|
5738
5705
|
this.applyServerOrder(next);
|
|
5739
5706
|
return this.getOrder();
|
|
5740
5707
|
}
|
|
5708
|
+
assertPayable(payment) {
|
|
5709
|
+
if (redeemsCoverTotal(cappedRedeems(this.draft), this.draft.totals)) return;
|
|
5710
|
+
const method = payment?.type;
|
|
5711
|
+
if (method && isRealPaymentMethod(method)) return;
|
|
5712
|
+
if (this.realTenders?.length) return;
|
|
5713
|
+
throw new OrderPaymentRequiredError();
|
|
5714
|
+
}
|
|
5741
5715
|
async syncPaymentIntent(tenders) {
|
|
5742
5716
|
await this.persist();
|
|
5743
5717
|
await this.flush.settle();
|
|
@@ -5754,7 +5728,7 @@ var Order = class Order {
|
|
|
5754
5728
|
}
|
|
5755
5729
|
/** Keeps the applied redeems while the server holds the same capped ones. */
|
|
5756
5730
|
replaceDraft(next) {
|
|
5757
|
-
if (redeemSignature
|
|
5731
|
+
if (redeemSignature(this.draft.redeem ?? []) !== redeemSignature(next.redeem ?? [])) this.requestedRedeem = null;
|
|
5758
5732
|
this.draft = next;
|
|
5759
5733
|
}
|
|
5760
5734
|
appliedRedeem() {
|
|
@@ -5764,10 +5738,14 @@ var Order = class Order {
|
|
|
5764
5738
|
* Places the order with a DRAFT intent: `intent_id` as is, else the
|
|
5765
5739
|
* tenders of `getTenders(input.payment)` saved first (simple flow: one
|
|
5766
5740
|
* real tender plus REDEMPTION). The intent is `LOCKED` afterwards (D25).
|
|
5741
|
+
* A rejected order sync is rethrown and nothing is placed; with no real
|
|
5742
|
+
* tender and redeems short of the total it throws
|
|
5743
|
+
* `OrderPaymentRequiredError` instead of placing.
|
|
5767
5744
|
*/
|
|
5768
5745
|
async placeOrder(input = {}) {
|
|
5769
5746
|
await this.persist();
|
|
5770
5747
|
await this.flush.settle();
|
|
5748
|
+
if (!input.intent_id) this.assertPayable(input.payment);
|
|
5771
5749
|
await this.validateOrder();
|
|
5772
5750
|
let body = input;
|
|
5773
5751
|
if (!input.intent_id) {
|
|
@@ -5882,29 +5860,28 @@ var Order = class Order {
|
|
|
5882
5860
|
runPaymentTerminal(payment, key) {
|
|
5883
5861
|
return this.payments.runTerminal(payment, key);
|
|
5884
5862
|
}
|
|
5885
|
-
/**
|
|
5863
|
+
/**
|
|
5864
|
+
* Applies lines/redeem, recomputes totals (redeems never change them),
|
|
5865
|
+
* trims every redeem to its `before_taxes` cap and rebuilds the steps.
|
|
5866
|
+
*/
|
|
5886
5867
|
commitLines(patch) {
|
|
5887
|
-
const patched = patch.lines ?? this.draft.lines;
|
|
5868
|
+
const patched = rebuildRootGroups(patch.lines ?? this.draft.lines);
|
|
5888
5869
|
const applied = patch.redeem ?? this.appliedRedeem();
|
|
5889
|
-
const
|
|
5890
|
-
const
|
|
5891
|
-
const { lines, redemption_steps, totals } = recomputeRedemptions({
|
|
5870
|
+
const totals = recomputeTotals(patched, this.draft.totals.currency);
|
|
5871
|
+
const capped = capRedeems({
|
|
5892
5872
|
lines: patched,
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
currency
|
|
5873
|
+
totals,
|
|
5874
|
+
redeem: applied
|
|
5896
5875
|
});
|
|
5897
|
-
const redeem =
|
|
5898
|
-
|
|
5899
|
-
totals
|
|
5900
|
-
}), currency);
|
|
5876
|
+
const { lines, redeem } = capped;
|
|
5877
|
+
const redemption_steps = redemptionSteps(capped);
|
|
5901
5878
|
this.requestedRedeem = applied;
|
|
5902
5879
|
this.draft = {
|
|
5903
5880
|
...this.draft,
|
|
5904
5881
|
lines,
|
|
5905
5882
|
redeem,
|
|
5906
|
-
|
|
5907
|
-
|
|
5883
|
+
totals,
|
|
5884
|
+
redemption_steps
|
|
5908
5885
|
};
|
|
5909
5886
|
this.scheduleAfterMutate();
|
|
5910
5887
|
}
|
|
@@ -6456,56 +6433,70 @@ const expiredIssues = (redeem, now) => {
|
|
|
6456
6433
|
});
|
|
6457
6434
|
return issues;
|
|
6458
6435
|
};
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6436
|
+
/** `NOT_FOUND`, `INACTIVE` or `NOT_REDEMPTION`; null when usable. */
|
|
6437
|
+
const paymentProblem = (payment_id, payments) => {
|
|
6438
|
+
const row = payments.find((item) => item.id === payment_id);
|
|
6439
|
+
if (!row) return "NOT_FOUND";
|
|
6440
|
+
if (row.status != null && row.status !== "ACTIVE") return "INACTIVE";
|
|
6441
|
+
if (row.method.method !== REDEMPTION_METHOD) return "NOT_REDEMPTION";
|
|
6442
|
+
return null;
|
|
6464
6443
|
};
|
|
6465
6444
|
/**
|
|
6466
|
-
* Every
|
|
6467
|
-
*
|
|
6445
|
+
* Every redeem needs `payment_id` and `amount > 0`; with `payments`, the
|
|
6446
|
+
* payment must exist, be active and be a REDEMPTION method. `amount` must
|
|
6447
|
+
* already be trimmed to its `before_taxes` cap (`fitted`).
|
|
6468
6448
|
*/
|
|
6469
|
-
const
|
|
6470
|
-
const
|
|
6471
|
-
|
|
6472
|
-
const
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
|
|
6449
|
+
const redeemIssues = (items, fitted, payments) => {
|
|
6450
|
+
const issues = [];
|
|
6451
|
+
items.forEach((item, index) => {
|
|
6452
|
+
const prefix = `redeem.${index}`;
|
|
6453
|
+
const payment_id = item.payment_id;
|
|
6454
|
+
if (typeof payment_id !== "string" || !payment_id) {
|
|
6455
|
+
issues.push({
|
|
6456
|
+
code: "REDEEM_PAYMENT_REQUIRED",
|
|
6457
|
+
field: `${prefix}.payment_id`
|
|
6458
|
+
});
|
|
6459
|
+
return;
|
|
6460
|
+
}
|
|
6461
|
+
if (!(typeof item.amount === "number" && item.amount > 0)) {
|
|
6462
|
+
issues.push({
|
|
6463
|
+
code: "REDEEM_PAYMENT_REQUIRED",
|
|
6464
|
+
field: `${prefix}.amount`
|
|
6465
|
+
});
|
|
6466
|
+
return;
|
|
6467
|
+
}
|
|
6468
|
+
const problem = payments ? paymentProblem(payment_id, payments) : null;
|
|
6469
|
+
if (problem) {
|
|
6470
|
+
const b1 = {
|
|
6471
|
+
code: "REDEEM_PAYMENT_METHOD_NOT_CONFIGURED",
|
|
6472
|
+
field: `${prefix}.payment_id`
|
|
6473
|
+
};
|
|
6474
|
+
issues.push({
|
|
6475
|
+
...b1,
|
|
6476
|
+
expected: REDEMPTION_METHOD,
|
|
6477
|
+
actual: problem
|
|
6478
|
+
});
|
|
6479
|
+
return;
|
|
6480
|
+
}
|
|
6481
|
+
const expected = fitted[index];
|
|
6482
|
+
if (expected && expected.eq(item.amount)) return;
|
|
6483
|
+
const b1 = {
|
|
6484
|
+
code: "REDEEM_AMOUNT_MISMATCH",
|
|
6485
|
+
field: `${prefix}.amount`
|
|
6502
6486
|
};
|
|
6487
|
+
const b2 = { expected: bigStr(expected?.toNumber() ?? 0) };
|
|
6503
6488
|
issues.push({
|
|
6504
6489
|
...b1,
|
|
6505
6490
|
...b2,
|
|
6506
|
-
actual:
|
|
6491
|
+
actual: bigStr(item.amount)
|
|
6507
6492
|
});
|
|
6508
|
-
}
|
|
6493
|
+
});
|
|
6494
|
+
return issues;
|
|
6495
|
+
};
|
|
6496
|
+
const stepSignature = (steps) => {
|
|
6497
|
+
return steps.map((step) => {
|
|
6498
|
+
return `${`${step.reward_id}:${step.reward_quantity}:${step.payment_id}`}:${step.target}:${step.target_id ?? ""}`;
|
|
6499
|
+
}).join(",");
|
|
6509
6500
|
};
|
|
6510
6501
|
const STATE_FIELDS = [
|
|
6511
6502
|
"value",
|
|
@@ -6523,37 +6514,39 @@ const flattenState = (prefix, state, out) => {
|
|
|
6523
6514
|
for (const field of STATE_FIELDS) out[`${prefix}.${field}`] = state ? bigStr(state[field]) : "";
|
|
6524
6515
|
};
|
|
6525
6516
|
const flattenStep = (step) => {
|
|
6526
|
-
const b1 = {
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
target: step.target,
|
|
6530
|
-
target_id: step.target_id ?? ""
|
|
6517
|
+
const b1 = {
|
|
6518
|
+
action: step.action,
|
|
6519
|
+
before_taxes: String(step.before_taxes)
|
|
6531
6520
|
};
|
|
6532
|
-
const
|
|
6533
|
-
const b5 = { discount_amount: bigStr(step.discount_amount) };
|
|
6521
|
+
const b2 = { discount_amount: bigStr(step.discount_amount) };
|
|
6534
6522
|
const out = {
|
|
6535
6523
|
...b1,
|
|
6536
|
-
...b2
|
|
6537
|
-
...b3,
|
|
6538
|
-
...b4,
|
|
6539
|
-
...b5
|
|
6524
|
+
...b2
|
|
6540
6525
|
};
|
|
6541
6526
|
for (const key of STATE_KEYS) flattenState(key, step[key], out);
|
|
6542
6527
|
return out;
|
|
6543
6528
|
};
|
|
6544
|
-
/**
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
|
|
6549
|
-
|
|
6529
|
+
/**
|
|
6530
|
+
* `REDEMPTION_STEPS_MISMATCH`: the steps do not list the redeems (reward,
|
|
6531
|
+
* units, payment, target) in application order. `REDEMPTION_MISMATCH`:
|
|
6532
|
+
* same redeems, but a step's money or state differs from the server's.
|
|
6533
|
+
*/
|
|
6534
|
+
const stepIssues = (submitted, expected) => {
|
|
6535
|
+
const issues = [];
|
|
6536
|
+
const wantedSignature = stepSignature(expected);
|
|
6537
|
+
const actualSignature = stepSignature(submitted);
|
|
6538
|
+
if (wantedSignature !== actualSignature) {
|
|
6539
|
+
const b1 = { code: "REDEMPTION_STEPS_MISMATCH" };
|
|
6540
|
+
const b2 = {
|
|
6541
|
+
field: "redemption_steps",
|
|
6542
|
+
expected: wantedSignature
|
|
6543
|
+
};
|
|
6550
6544
|
issues.push({
|
|
6551
6545
|
...b1,
|
|
6552
6546
|
...b2,
|
|
6553
|
-
|
|
6554
|
-
actual: String(submitted.length)
|
|
6547
|
+
actual: actualSignature
|
|
6555
6548
|
});
|
|
6556
|
-
return;
|
|
6549
|
+
return issues;
|
|
6557
6550
|
}
|
|
6558
6551
|
submitted.forEach((step, index) => {
|
|
6559
6552
|
const actual = flattenStep(step);
|
|
@@ -6573,22 +6566,36 @@ const compareRedemptionSteps = (submitted, expected, issues) => {
|
|
|
6573
6566
|
});
|
|
6574
6567
|
}
|
|
6575
6568
|
});
|
|
6569
|
+
return issues;
|
|
6576
6570
|
};
|
|
6577
6571
|
const validateRedemptions = (input, issues, lineIssues) => {
|
|
6578
6572
|
const redeem = input.redeem ?? [];
|
|
6579
|
-
const steps = input.redemptionSteps ?? [];
|
|
6580
6573
|
const now = input.now ?? Date.now();
|
|
6574
|
+
const p1 = {
|
|
6575
|
+
lines: input.lines,
|
|
6576
|
+
totals: input.totals,
|
|
6577
|
+
redeem
|
|
6578
|
+
};
|
|
6579
|
+
const fitted = fitRedeems(p1);
|
|
6581
6580
|
issues.push(...expiredIssues(redeem, now));
|
|
6582
|
-
|
|
6583
|
-
|
|
6581
|
+
issues.push(...redeemIssues(redeem, fitted.redeem, input.payments));
|
|
6582
|
+
input.lines.forEach((line, index) => {
|
|
6583
|
+
const items = line.redeem ?? [];
|
|
6584
|
+
const expired = expiredIssues(items, now);
|
|
6584
6585
|
for (const issue of expired) pushLineIssue(lineIssues, line.uid, issue);
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6586
|
+
if (line.parent_uid != null) return;
|
|
6587
|
+
const p2 = [
|
|
6588
|
+
items,
|
|
6589
|
+
fitted.lines[index] ?? [],
|
|
6590
|
+
input.payments
|
|
6591
|
+
];
|
|
6592
|
+
for (const issue of redeemIssues(...p2)) pushLineIssue(lineIssues, line.uid, issue);
|
|
6593
|
+
});
|
|
6594
|
+
if (input.redemptionSteps === void 0) return;
|
|
6595
|
+
const expected = redemptionSteps(p1);
|
|
6596
|
+
issues.push(...stepIssues(input.redemptionSteps, expected));
|
|
6590
6597
|
};
|
|
6591
|
-
const
|
|
6598
|
+
const ROOT_GROUP_FIELDS = [
|
|
6592
6599
|
["group_discount", "DISCOUNT_MISMATCH"],
|
|
6593
6600
|
["unit_group_discount", "DISCOUNT_MISMATCH"],
|
|
6594
6601
|
["group_tax_total", "TAX_MISMATCH"],
|
|
@@ -6596,14 +6603,17 @@ const REDEEMED_GROUP_FIELDS = [
|
|
|
6596
6603
|
["group_total", "PRICE_MISMATCH"],
|
|
6597
6604
|
["unit_group_total", "PRICE_MISMATCH"]
|
|
6598
6605
|
];
|
|
6599
|
-
/**
|
|
6600
|
-
|
|
6606
|
+
/**
|
|
6607
|
+
* Root group money is the line plus its children: a redeem never lowers
|
|
6608
|
+
* it (its REDEMPTION tender pays it), so a wiped line is rejected.
|
|
6609
|
+
*/
|
|
6610
|
+
const compareRootGroups = (submitted, expected, out) => {
|
|
6601
6611
|
const byUid = new Map(expected.map((line) => [line.uid, line]));
|
|
6602
6612
|
for (const line of submitted) {
|
|
6603
6613
|
if (line.parent_uid != null) continue;
|
|
6604
6614
|
const wanted = byUid.get(line.uid);
|
|
6605
6615
|
if (!wanted) continue;
|
|
6606
|
-
for (const [field, code] of
|
|
6616
|
+
for (const [field, code] of ROOT_GROUP_FIELDS) {
|
|
6607
6617
|
if (bigEq(line[field], wanted[field])) continue;
|
|
6608
6618
|
const b1 = {
|
|
6609
6619
|
code,
|
|
@@ -6628,7 +6638,7 @@ const validateOrderAgainstMenu = (input) => {
|
|
|
6628
6638
|
lines: {}
|
|
6629
6639
|
};
|
|
6630
6640
|
if (!input.menuActive) issues.push({ code: "MENU_CLOSED" });
|
|
6631
|
-
const menuLines =
|
|
6641
|
+
const menuLines = rebuildRootGroups(input.lines);
|
|
6632
6642
|
const roots = menuLines.filter((line) => line.parent_uid == null);
|
|
6633
6643
|
if (roots.length === 0) {
|
|
6634
6644
|
issues.push({ code: "EMPTY_ORDER" });
|
|
@@ -6648,9 +6658,9 @@ const validateOrderAgainstMenu = (input) => {
|
|
|
6648
6658
|
}
|
|
6649
6659
|
checkRootProductQuantities(roots, lookup, lineIssues);
|
|
6650
6660
|
if (input.expectedFeeLines) compareExpectedFeeLines(input.lines, input.expectedFeeLines, lineIssues);
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
const expectedTotals = recomputeTotals(
|
|
6661
|
+
validateRedemptions(input, issues, lineIssues);
|
|
6662
|
+
compareRootGroups(input.lines, menuLines, lineIssues);
|
|
6663
|
+
const expectedTotals = recomputeTotals(menuLines, input.totals.currency);
|
|
6654
6664
|
for (const field of TOTALS_FIELDS) {
|
|
6655
6665
|
if (bigEq(input.totals[field], expectedTotals[field])) continue;
|
|
6656
6666
|
issues.push({
|
|
@@ -8646,4 +8656,4 @@ const cardPreviewGroups = (value) => {
|
|
|
8646
8656
|
return groups;
|
|
8647
8657
|
};
|
|
8648
8658
|
//#endregion
|
|
8649
|
-
export { CARD_BRANDS, DEFAULT_RECOMMENDATION_TYPES, Order, OrderSyncPendingError, OrderValidationError, RecurringDateCron, RulesEngine, StorefrontHttpError, addLines, auth, buildCustomerSnapshot, cardBrandSpec, cardDigits, cardNumberGaps, cardNumberMaxLength, cardPreviewGroups, caretAfterDigits,
|
|
8659
|
+
export { CARD_BRANDS, DEFAULT_RECOMMENDATION_TYPES, Order, OrderPaymentRequiredError, OrderSyncPendingError, OrderSyncRejectedError, OrderValidationError, RecurringDateCron, RulesEngine, StorefrontHttpError, addLines, auth, buildCustomerSnapshot, capRedeems, cappedRedeems, cardBrandSpec, cardDigits, cardNumberGaps, cardNumberMaxLength, cardPreviewGroups, caretAfterDigits, collectOrderLineDiffs, computeStartingPrice, countCartProducts, coveredAmount, createSession, currentNodes, cvcMaxLength, defaultConditionError, detectCardBrand, fitRedeems, flattenMenuCategories, flushLogs, formatCardNumber, formatCvc, formatExpiry, fromOrderLines, genId, genOrderId, getLogContext, getScheduleWindow, indexMenu, isCardNumberComplete, isCardNumberDefinitelyInvalid, isCheckoutFieldFilled, isCvcComplete, isDeviceCharged, isDeviceNotLinked, isExpiryComplete, isKioskUnauthorized, isMilestoneStepDone, isOnSchedule, isOrderPaymentRequiredError, isOrderValidationError, isPosDeviceNotLinked, isPosUnauthorized, isRealPaymentMethod, isStorefrontNotFound, isVariantBase, linePayableTotal, listMenusStream, localizePosError, log, logError, luhnCheck, matchAbsoluteDateOperator, matchBooleanOperator, matchCurrencyOperator, matchEnumOperator, matchNumberOperator, matchRecurringDateOperator, matchRelativeDateOperator, matchStringOperator, milestoneSteps, milestoneTrack, nestMenuCategories, normalizeCardholderName, orderPayableTotal, orderRedeemedTotal, orderRedeems, parseExpiry, paymentMatchesClientConditions, rebuildRootGroups, recomputeTotals, redeemedTotal, redemptionCap, redemptionSteps, remainingAmount, removeLine, replaceLine, resolveCheckout, reversePayment, runPaymentAttempt, selectablePayments, serializeError, services, setLineQuantity, storefrontJson, toOrderLines, toPaymentCardBrand, validateCardNumber, validateCardholderName, validateCvc, validateExpiry, validateOrderAgainstMenu, verifyWebhookSignature, visitedStates };
|