@fire-spark/core 0.4.0 → 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 +214 -65
- package/dist/index.js +965 -925
- 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,49 +3512,91 @@ 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
|
-
return isTipFee(line) || isDeliveryFee(line) ? sum + line.group_total : sum;
|
|
3469
|
-
}, 0);
|
|
3470
|
-
const decimals = currencyDecimals(order.totals.currency);
|
|
3471
|
-
return Math.max(roundTo(order.totals.total - primaryOnly, decimals), 0);
|
|
3472
|
-
};
|
|
3473
3515
|
/**
|
|
3474
|
-
*
|
|
3475
|
-
*
|
|
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.
|
|
3476
3519
|
*/
|
|
3477
|
-
const
|
|
3478
|
-
const
|
|
3479
|
-
|
|
3480
|
-
|
|
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);
|
|
3525
|
+
}, moneyZero());
|
|
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
|
+
};
|
|
3481
3556
|
};
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
const decimals = currencyDecimals(currency);
|
|
3488
|
-
let left = cap;
|
|
3489
|
-
return redeem.flatMap((item) => {
|
|
3490
|
-
if (!isPaymentRedeem(item)) return [item];
|
|
3491
|
-
const amount = item.amount ?? 0;
|
|
3492
|
-
const fits = Math.max(roundTo(Math.min(amount, left), decimals), 0);
|
|
3493
|
-
left = roundTo(left - fits, decimals);
|
|
3494
|
-
if (fits <= 0) return [];
|
|
3495
|
-
if (fits + amountTolerance(currency) >= amount) return [item];
|
|
3557
|
+
const keepFitted = (items, fitted) => {
|
|
3558
|
+
return items.flatMap((item, index) => {
|
|
3559
|
+
const fits = fitted[index] ?? moneyZero();
|
|
3560
|
+
if (fits.lte(0)) return [];
|
|
3561
|
+
if (fits.eq(item.amount)) return [item];
|
|
3496
3562
|
return [{
|
|
3497
3563
|
...item,
|
|
3498
|
-
amount: fits
|
|
3564
|
+
amount: moneyNumber(fits)
|
|
3499
3565
|
}];
|
|
3500
3566
|
});
|
|
3501
3567
|
};
|
|
3502
3568
|
/**
|
|
3503
|
-
*
|
|
3504
|
-
*
|
|
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.
|
|
3505
3597
|
*/
|
|
3506
3598
|
const redemptionTenderInputs = (redeem, providers = /* @__PURE__ */ new Map()) => {
|
|
3507
|
-
return
|
|
3599
|
+
return groupRedeems(redeem).map(({ payment_id, details }) => {
|
|
3508
3600
|
const fallback = DEFAULT_REDEMPTION_PROVIDER;
|
|
3509
3601
|
const t1 = {
|
|
3510
3602
|
method: "REDEMPTION",
|
|
@@ -3884,14 +3976,15 @@ const chargedTenders = (intents) => {
|
|
|
3884
3976
|
};
|
|
3885
3977
|
/** Settled tenders plus PENDING `confirm_after_inject`, order currency. */
|
|
3886
3978
|
const coveredAmount = (intents) => {
|
|
3887
|
-
return chargedTenders(intents).reduce((sum, tender) => {
|
|
3888
|
-
return sum
|
|
3889
|
-
},
|
|
3979
|
+
return moneyNumber(chargedTenders(intents).reduce((sum, tender) => {
|
|
3980
|
+
return sum.plus(tender.amount.total);
|
|
3981
|
+
}, moneyZero()));
|
|
3890
3982
|
};
|
|
3891
3983
|
const remainingAmount = (order) => {
|
|
3892
|
-
const
|
|
3893
|
-
const left = order.totals.total
|
|
3894
|
-
|
|
3984
|
+
const covered = money(coveredAmount(order.payment_intents));
|
|
3985
|
+
const left = money(order.totals.total).minus(covered);
|
|
3986
|
+
if (left.lt(0)) return 0;
|
|
3987
|
+
return moneyNumber(left);
|
|
3895
3988
|
};
|
|
3896
3989
|
const isPlaced = (order) => order.status !== "OPEN";
|
|
3897
3990
|
/** Orchestrator failures carry the charge-currency amount, not the order's. */
|
|
@@ -3909,8 +4002,7 @@ const partialOf = (result) => {
|
|
|
3909
4002
|
const isPartialApproval = (result, tender, ctx) => {
|
|
3910
4003
|
if (result.status !== "PAID") return false;
|
|
3911
4004
|
if (ctx.amount.currency !== tender.amount.currency) return false;
|
|
3912
|
-
|
|
3913
|
-
return result.amount + tolerance < tender.amount.total;
|
|
4005
|
+
return money(result.amount).lt(tender.amount.total);
|
|
3914
4006
|
};
|
|
3915
4007
|
const toFailure = (error) => {
|
|
3916
4008
|
const message = error instanceof Error ? error.message : "Terminal failed";
|
|
@@ -4021,12 +4113,9 @@ var PaymentSession = class {
|
|
|
4021
4113
|
const conversion = await this.chargeAmount(payment, amount, server);
|
|
4022
4114
|
const config = record(payment.config);
|
|
4023
4115
|
const methodConfig = record(payment.method.config);
|
|
4024
|
-
const
|
|
4025
|
-
const
|
|
4026
|
-
const taxes = taxBreakdown(order.lines, {
|
|
4027
|
-
share,
|
|
4028
|
-
decimals
|
|
4029
|
-
});
|
|
4116
|
+
const total = order.totals.total;
|
|
4117
|
+
const share = moneyNumber(total > 0 ? money(amount).div(total) : money(1));
|
|
4118
|
+
const taxes = taxBreakdown(order.lines, { share });
|
|
4030
4119
|
const info = order.customer.customer_info;
|
|
4031
4120
|
const customer = {
|
|
4032
4121
|
document: info.document_number ?? null,
|
|
@@ -4657,109 +4746,38 @@ const collectRecommendations = (config) => {
|
|
|
4657
4746
|
return products;
|
|
4658
4747
|
};
|
|
4659
4748
|
//#endregion
|
|
4660
|
-
//#region src/order/totals.ts
|
|
4661
|
-
/** Sum root line group totals into order totals (Big.js). */
|
|
4662
|
-
const recomputeTotals = (lines, currency = "USD") => {
|
|
4663
|
-
let subtotal = new Big(0);
|
|
4664
|
-
let discountTotal = new Big(0);
|
|
4665
|
-
let taxTotal = new Big(0);
|
|
4666
|
-
let total = new Big(0);
|
|
4667
|
-
for (const line of lines) {
|
|
4668
|
-
if (line.parent_uid != null) continue;
|
|
4669
|
-
subtotal = subtotal.plus(line.group_subtotal);
|
|
4670
|
-
discountTotal = discountTotal.plus(line.group_discount);
|
|
4671
|
-
taxTotal = taxTotal.plus(line.group_tax_total);
|
|
4672
|
-
total = total.plus(line.group_total);
|
|
4673
|
-
}
|
|
4674
|
-
let taxRate = 0;
|
|
4675
|
-
let discountRate = 0;
|
|
4676
|
-
if (subtotal.gt(0)) {
|
|
4677
|
-
taxRate = taxTotal.div(subtotal).toNumber();
|
|
4678
|
-
discountRate = discountTotal.div(subtotal).toNumber();
|
|
4679
|
-
}
|
|
4680
|
-
const discount_total = discountTotal.toNumber();
|
|
4681
|
-
const tax_total = taxTotal.toNumber();
|
|
4682
|
-
const b1 = {
|
|
4683
|
-
currency,
|
|
4684
|
-
subtotal: subtotal.toNumber()
|
|
4685
|
-
};
|
|
4686
|
-
const b2 = {
|
|
4687
|
-
discount_rate: discountRate,
|
|
4688
|
-
discount_total
|
|
4689
|
-
};
|
|
4690
|
-
const b3 = {
|
|
4691
|
-
tax_rate: taxRate,
|
|
4692
|
-
tax_total,
|
|
4693
|
-
total: total.toNumber()
|
|
4694
|
-
};
|
|
4695
|
-
return {
|
|
4696
|
-
...b1,
|
|
4697
|
-
...b2,
|
|
4698
|
-
...b3
|
|
4699
|
-
};
|
|
4700
|
-
};
|
|
4701
|
-
const emptyTotals = (currency = "USD") => {
|
|
4702
|
-
const b1 = {
|
|
4703
|
-
currency,
|
|
4704
|
-
subtotal: 0,
|
|
4705
|
-
discount_rate: 0,
|
|
4706
|
-
discount_total: 0
|
|
4707
|
-
};
|
|
4708
|
-
const b2 = {
|
|
4709
|
-
tax_rate: 0,
|
|
4710
|
-
tax_total: 0,
|
|
4711
|
-
total: 0
|
|
4712
|
-
};
|
|
4713
|
-
return {
|
|
4714
|
-
...b1,
|
|
4715
|
-
...b2
|
|
4716
|
-
};
|
|
4717
|
-
};
|
|
4718
|
-
//#endregion
|
|
4719
4749
|
//#region src/order/redemptions.ts
|
|
4720
|
-
const ORDER_KEY = "ORDER";
|
|
4721
|
-
const buildPaymentRedeem = (input) => {
|
|
4722
|
-
const { reward_id, quantity, payment_id, amount } = input;
|
|
4723
|
-
if (amount === void 0 || !(amount > 0)) throw new Error("Redeem with payment_id requires amount > 0");
|
|
4724
|
-
const redeem = {
|
|
4725
|
-
reward_id,
|
|
4726
|
-
quantity,
|
|
4727
|
-
payment_id,
|
|
4728
|
-
amount
|
|
4729
|
-
};
|
|
4730
|
-
if (input.expires_at !== void 0) redeem.expires_at = input.expires_at;
|
|
4731
|
-
return redeem;
|
|
4732
|
-
};
|
|
4733
4750
|
/** Same rules as `vOrders.redeem()`; throws on invalid input. */
|
|
4734
4751
|
const buildRedeem = (input) => {
|
|
4735
|
-
const { reward_id, quantity,
|
|
4752
|
+
const { reward_id, quantity, payment_id, amount } = input;
|
|
4736
4753
|
const { before_taxes, expires_at } = input;
|
|
4737
|
-
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");
|
|
4738
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");
|
|
4739
4758
|
if (expires_at !== void 0 && Number.isNaN(Date.parse(expires_at))) throw new Error("Redeem expires_at must be an ISO datetime");
|
|
4740
|
-
if (input.payment_id) return buildPaymentRedeem(input);
|
|
4741
|
-
if (discount_rate === void 0 && discount_amount === void 0) throw new Error("Redeem requires discount_rate or discount_amount");
|
|
4742
|
-
const rate = discount_rate ?? 0;
|
|
4743
|
-
if (!(rate >= 0 && rate <= 1)) throw new Error("Redeem discount_rate must be between 0 and 1");
|
|
4744
|
-
if (discount_amount !== void 0 && !(discount_amount > 0)) throw new Error("Redeem discount_amount must be > 0");
|
|
4745
4759
|
const redeem = {
|
|
4746
4760
|
reward_id,
|
|
4747
|
-
quantity
|
|
4761
|
+
quantity,
|
|
4762
|
+
payment_id,
|
|
4763
|
+
amount
|
|
4748
4764
|
};
|
|
4749
|
-
if (discount_rate !== void 0) redeem.discount_rate = discount_rate;
|
|
4750
|
-
if (discount_amount !== void 0) redeem.discount_amount = discount_amount;
|
|
4751
4765
|
if (before_taxes !== void 0) redeem.before_taxes = before_taxes;
|
|
4752
4766
|
if (expires_at !== void 0) redeem.expires_at = expires_at;
|
|
4753
4767
|
return redeem;
|
|
4754
4768
|
};
|
|
4755
|
-
/**
|
|
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
|
+
*/
|
|
4756
4773
|
const keepUnits = (item, rest) => {
|
|
4757
|
-
if (
|
|
4774
|
+
if (rest === item.quantity) return {
|
|
4758
4775
|
...item,
|
|
4759
4776
|
quantity: rest
|
|
4760
4777
|
};
|
|
4761
|
-
const
|
|
4762
|
-
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();
|
|
4763
4781
|
return {
|
|
4764
4782
|
...item,
|
|
4765
4783
|
quantity: rest,
|
|
@@ -4767,8 +4785,8 @@ const keepUnits = (item, rest) => {
|
|
|
4767
4785
|
};
|
|
4768
4786
|
};
|
|
4769
4787
|
/**
|
|
4770
|
-
* Takes `quantity` units of `reward_id` off the latest entries first
|
|
4771
|
-
*
|
|
4788
|
+
* Takes `quantity` units of `reward_id` off the latest entries first.
|
|
4789
|
+
* Entries that reach 0 are removed.
|
|
4772
4790
|
*/
|
|
4773
4791
|
const subtractRedeem = (items, reward_id, quantity) => {
|
|
4774
4792
|
if (!Number.isInteger(quantity) || quantity < 1) throw new Error("Redeem quantity must be an integer ≥ 1");
|
|
@@ -4787,143 +4805,43 @@ const subtractRedeem = (items, reward_id, quantity) => {
|
|
|
4787
4805
|
}
|
|
4788
4806
|
return kept;
|
|
4789
4807
|
};
|
|
4790
|
-
const redemptionTargetKey = (target) => {
|
|
4791
|
-
if (target.target === "ORDER") return ORDER_KEY;
|
|
4792
|
-
return `LINE:${target.target_id ?? ""}`;
|
|
4793
|
-
};
|
|
4794
4808
|
/**
|
|
4795
|
-
*
|
|
4796
|
-
*
|
|
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.
|
|
4797
4812
|
*/
|
|
4798
|
-
const
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
const pool = items.map((item) => ({
|
|
4808
|
-
...target,
|
|
4809
|
-
redeem: item
|
|
4810
|
-
}));
|
|
4811
|
-
pools.set(redemptionTargetKey(target), pool);
|
|
4812
|
-
}
|
|
4813
|
-
const order = { target: "ORDER" };
|
|
4814
|
-
const pool = discountRedeems(redeem).map((item) => ({
|
|
4815
|
-
...order,
|
|
4816
|
-
redeem: item
|
|
4817
|
-
}));
|
|
4818
|
-
pools.set(ORDER_KEY, pool);
|
|
4819
|
-
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));
|
|
4820
4822
|
};
|
|
4821
4823
|
/**
|
|
4822
|
-
*
|
|
4823
|
-
*
|
|
4824
|
-
* 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.
|
|
4825
4826
|
*/
|
|
4826
|
-
const
|
|
4827
|
-
const
|
|
4828
|
-
|
|
4829
|
-
const sequence = [];
|
|
4830
|
-
for (const step of steps) {
|
|
4831
|
-
const key = redemptionTargetKey(step);
|
|
4832
|
-
const at = cursors.get(key) ?? 0;
|
|
4833
|
-
const next = pools.get(key)?.[at];
|
|
4834
|
-
if (!next || next.redeem.reward_id !== step.reward_id) continue;
|
|
4835
|
-
cursors.set(key, at + 1);
|
|
4836
|
-
sequence.push(next);
|
|
4837
|
-
}
|
|
4838
|
-
for (const [key, pool] of pools) sequence.push(...pool.slice(cursors.get(key) ?? 0));
|
|
4839
|
-
return sequence;
|
|
4840
|
-
};
|
|
4841
|
-
const minBig = (a, b) => {
|
|
4842
|
-
return a.lt(b) ? a : b;
|
|
4843
|
-
};
|
|
4844
|
-
const rateOf = (discount, base) => {
|
|
4845
|
-
if (base.lte(0)) return 0;
|
|
4846
|
-
return minBig(discount.div(base), new Big(1)).toNumber();
|
|
4847
|
-
};
|
|
4848
|
-
const snapshot = (roots, quantity) => {
|
|
4849
|
-
let subtotal = new Big(0);
|
|
4850
|
-
let total = new Big(0);
|
|
4851
|
-
let discount = new Big(0);
|
|
4852
|
-
for (const root of roots) {
|
|
4853
|
-
subtotal = subtotal.plus(root.subtotal);
|
|
4854
|
-
total = total.plus(root.total);
|
|
4855
|
-
discount = discount.plus(root.discount);
|
|
4856
|
-
}
|
|
4857
|
-
const b1 = {
|
|
4858
|
-
value: total.toNumber(),
|
|
4859
|
-
quantity
|
|
4860
|
-
};
|
|
4861
|
-
const b2 = { discount_amount: discount.toNumber() };
|
|
4862
|
-
const b3 = { discount_rate: rateOf(discount, subtotal) };
|
|
4863
|
-
return {
|
|
4864
|
-
...b1,
|
|
4865
|
-
...b2,
|
|
4866
|
-
...b3
|
|
4867
|
-
};
|
|
4868
|
-
};
|
|
4869
|
-
const netOf = (root) => {
|
|
4870
|
-
const net = root.total.minus(root.tax);
|
|
4871
|
-
return net.gt(0) ? net : new Big(0);
|
|
4872
|
-
};
|
|
4873
|
-
/** What a redeem can still discount on a root: pre-tax or with taxes. */
|
|
4874
|
-
const baseOf = (root, beforeTaxes) => {
|
|
4875
|
-
return beforeTaxes ? netOf(root) : root.total;
|
|
4876
|
-
};
|
|
4877
|
-
/** Before taxes, taxes shrink with the pre-tax value they were charged on. */
|
|
4878
|
-
const discountRoot = (root, amount, beforeTaxes) => {
|
|
4879
|
-
if (amount.lte(0)) return;
|
|
4880
|
-
root.discount = root.discount.plus(amount);
|
|
4881
|
-
if (!beforeTaxes) {
|
|
4882
|
-
root.total = root.total.minus(amount);
|
|
4883
|
-
return;
|
|
4884
|
-
}
|
|
4885
|
-
const net = netOf(root);
|
|
4886
|
-
const nextNet = net.minus(amount);
|
|
4887
|
-
root.tax = root.tax.times(nextNet).div(net);
|
|
4888
|
-
root.total = nextNet.plus(root.tax);
|
|
4889
|
-
};
|
|
4890
|
-
/** Splits an order discount across roots by what each can still discount. */
|
|
4891
|
-
const discountOrder = (roots, amount, beforeTaxes) => {
|
|
4892
|
-
const bases = roots.map((root) => baseOf(root, beforeTaxes));
|
|
4893
|
-
const sum = bases.reduce((acc, base) => acc.plus(base), new Big(0));
|
|
4894
|
-
if (sum.lte(0) || amount.lte(0)) return;
|
|
4895
|
-
let last = -1;
|
|
4896
|
-
bases.forEach((base, index) => {
|
|
4897
|
-
if (base.gt(0)) last = index;
|
|
4898
|
-
});
|
|
4899
|
-
let rest = amount;
|
|
4900
|
-
roots.forEach((root, index) => {
|
|
4901
|
-
const base = bases[index];
|
|
4902
|
-
if (base.lte(0)) return;
|
|
4903
|
-
const share = amount.times(base).div(sum);
|
|
4904
|
-
const part = minBig(index === last ? rest : share, base);
|
|
4905
|
-
rest = rest.minus(part);
|
|
4906
|
-
discountRoot(root, part, beforeTaxes);
|
|
4907
|
-
});
|
|
4827
|
+
const orderPayableTotal = (order) => {
|
|
4828
|
+
const rest = new Big(order.totals.total).minus(orderRedeemedTotal(order));
|
|
4829
|
+
return rest.gt(0) ? rest.toNumber() : 0;
|
|
4908
4830
|
};
|
|
4909
4831
|
/**
|
|
4910
|
-
*
|
|
4911
|
-
*
|
|
4832
|
+
* What the customer pays for a root line with another method:
|
|
4833
|
+
* `group_total − Σ line.redeem[].amount`, never below 0.
|
|
4912
4834
|
*/
|
|
4913
|
-
const
|
|
4914
|
-
const
|
|
4915
|
-
|
|
4916
|
-
if (discount_amount !== void 0) cap = new Big(discount_amount).times(quantity);
|
|
4917
|
-
if (discount_rate === void 0) return cap ?? new Big(0);
|
|
4918
|
-
const kept = new Big(1).minus(discount_rate).pow(quantity);
|
|
4919
|
-
const byRate = remaining.times(new Big(1).minus(kept));
|
|
4920
|
-
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;
|
|
4921
4838
|
};
|
|
4922
4839
|
/**
|
|
4923
|
-
* Root group money rebuilt from the line itself plus its children
|
|
4924
|
-
*
|
|
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.
|
|
4925
4843
|
*/
|
|
4926
|
-
const
|
|
4844
|
+
const rebuildRootGroups = (lines) => {
|
|
4927
4845
|
const children = /* @__PURE__ */ new Map();
|
|
4928
4846
|
for (const line of lines) {
|
|
4929
4847
|
if (line.parent_uid == null) continue;
|
|
@@ -4972,111 +4890,162 @@ const stripRedemptionLines = (lines) => {
|
|
|
4972
4890
|
};
|
|
4973
4891
|
});
|
|
4974
4892
|
};
|
|
4975
|
-
const
|
|
4976
|
-
const
|
|
4977
|
-
const
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
const
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
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) };
|
|
4984
4903
|
return {
|
|
4985
|
-
...
|
|
4986
|
-
...
|
|
4987
|
-
|
|
4988
|
-
...u3,
|
|
4989
|
-
...g1,
|
|
4990
|
-
...g2,
|
|
4991
|
-
...g3
|
|
4904
|
+
...s1,
|
|
4905
|
+
...s2,
|
|
4906
|
+
discount_rate: rate.toNumber()
|
|
4992
4907
|
};
|
|
4993
4908
|
};
|
|
4994
4909
|
/**
|
|
4995
|
-
*
|
|
4996
|
-
*
|
|
4997
|
-
*
|
|
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.
|
|
4998
4916
|
*/
|
|
4999
|
-
const
|
|
5000
|
-
const
|
|
5001
|
-
const
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
const
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
...b4
|
|
5017
|
-
});
|
|
5018
|
-
}
|
|
5019
|
-
const roots = [...moneyByUid.values()];
|
|
5020
|
-
const redemption_steps = sequence.map((item) => {
|
|
5021
|
-
const id = item.target_id;
|
|
5022
|
-
const root = id ? moneyByUid.get(id) : void 0;
|
|
5023
|
-
const beforeTaxes = item.redeem.before_taxes ?? true;
|
|
5024
|
-
const before = snapshot(roots, 1);
|
|
5025
|
-
const targetBefore = root ? snapshot([root], root.quantity) : void 0;
|
|
5026
|
-
let left = new Big(0);
|
|
5027
|
-
if (root) left = baseOf(root, beforeTaxes);
|
|
5028
|
-
else for (const r of roots) left = left.plus(baseOf(r, beforeTaxes));
|
|
5029
|
-
const raw = redeemDiscount(item.redeem, left);
|
|
5030
|
-
const discount = minBig(raw, left);
|
|
5031
|
-
if (root) discountRoot(root, discount, beforeTaxes);
|
|
5032
|
-
else discountOrder(roots, discount, beforeTaxes);
|
|
5033
|
-
const action = discount.gt(0) ? "UPDATE" : "READ";
|
|
5034
|
-
const { reward_id, quantity } = item.redeem;
|
|
5035
|
-
const b1 = {
|
|
5036
|
-
reward_id,
|
|
5037
|
-
reward_quantity: quantity,
|
|
5038
|
-
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
|
|
5039
4934
|
};
|
|
5040
|
-
const
|
|
5041
|
-
const
|
|
5042
|
-
|
|
5043
|
-
|
|
4935
|
+
const s2 = { payment_id: item.payment_id };
|
|
4936
|
+
const s3 = {
|
|
4937
|
+
before_taxes: item.before_taxes ?? true,
|
|
4938
|
+
action
|
|
5044
4939
|
};
|
|
5045
|
-
const
|
|
4940
|
+
const s4 = { discount_amount: moneyNumber(fits) };
|
|
4941
|
+
const s5 = {
|
|
5046
4942
|
before,
|
|
5047
|
-
after:
|
|
5048
|
-
};
|
|
5049
|
-
let b5 = {};
|
|
5050
|
-
if (root) b5 = {
|
|
5051
|
-
target_before: targetBefore,
|
|
5052
|
-
target_after: snapshot([root], root.quantity)
|
|
4943
|
+
after: stateOf(whole)
|
|
5053
4944
|
};
|
|
5054
4945
|
return {
|
|
5055
|
-
...
|
|
5056
|
-
...
|
|
5057
|
-
...
|
|
5058
|
-
...
|
|
5059
|
-
...
|
|
4946
|
+
...s1,
|
|
4947
|
+
...s2,
|
|
4948
|
+
...s3,
|
|
4949
|
+
...s4,
|
|
4950
|
+
...s5,
|
|
4951
|
+
targetBefore
|
|
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
|
|
5060
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
|
+
});
|
|
5061
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
|
+
};
|
|
5062
5028
|
return {
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
return withRootMoney(line, money);
|
|
5067
|
-
}),
|
|
5068
|
-
redemption_steps
|
|
5029
|
+
...b1,
|
|
5030
|
+
...b2,
|
|
5031
|
+
...b3
|
|
5069
5032
|
};
|
|
5070
5033
|
};
|
|
5071
|
-
|
|
5072
|
-
const
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
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
|
+
};
|
|
5076
5046
|
return {
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
totals: recomputeTotals(lines, currency)
|
|
5047
|
+
...b1,
|
|
5048
|
+
...b2
|
|
5080
5049
|
};
|
|
5081
5050
|
};
|
|
5082
5051
|
//#endregion
|
|
@@ -5084,9 +5053,9 @@ const recomputeRedemptions = (input) => {
|
|
|
5084
5053
|
const cloneOrder = (order) => {
|
|
5085
5054
|
return structuredClone(order);
|
|
5086
5055
|
};
|
|
5087
|
-
const redeemSignature
|
|
5056
|
+
const redeemSignature = (items) => {
|
|
5088
5057
|
return items.map((item) => {
|
|
5089
|
-
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 ?? ""}`}`;
|
|
5090
5059
|
}).join("|");
|
|
5091
5060
|
};
|
|
5092
5061
|
const buildHttpOptions = (options) => {
|
|
@@ -5147,14 +5116,14 @@ const buildInsertFromSource = (options, source) => {
|
|
|
5147
5116
|
if (options.scope?.brand_id !== void 0) brand_id = optionalBrandId(options.scope.brand_id);
|
|
5148
5117
|
let customer = source.customer;
|
|
5149
5118
|
if (options.customer) customer = buildCustomerSnapshot(options.customer);
|
|
5150
|
-
const sourceLines = structuredClone(source.lines);
|
|
5151
5119
|
const is_anonymous = options.isAnonymous ?? source.is_anonymous;
|
|
5152
5120
|
const redeem = structuredClone(source.redeem ?? []);
|
|
5153
|
-
const
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
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
|
|
5158
5127
|
});
|
|
5159
5128
|
const b1 = {
|
|
5160
5129
|
id: orderId,
|
|
@@ -5500,20 +5469,20 @@ var Order = class Order {
|
|
|
5500
5469
|
});
|
|
5501
5470
|
}
|
|
5502
5471
|
/**
|
|
5503
|
-
*
|
|
5504
|
-
*
|
|
5505
|
-
*
|
|
5506
|
-
*
|
|
5507
|
-
*
|
|
5508
|
-
*
|
|
5509
|
-
*
|
|
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).
|
|
5510
5480
|
*/
|
|
5511
5481
|
applyRedeem(input) {
|
|
5512
5482
|
if (this.deleted) return;
|
|
5513
5483
|
const { line_uid, provider, ...rest } = input;
|
|
5514
5484
|
const redeem = buildRedeem(rest);
|
|
5515
|
-
if (
|
|
5516
|
-
if (redeem.payment_id && provider) this.redemptionProviders.set(redeem.payment_id, provider);
|
|
5485
|
+
if (provider) this.redemptionProviders.set(redeem.payment_id, provider);
|
|
5517
5486
|
if (line_uid === void 0) {
|
|
5518
5487
|
this.commitLines({ redeem: [...this.appliedRedeem(), redeem] });
|
|
5519
5488
|
return;
|
|
@@ -5532,8 +5501,8 @@ var Order = class Order {
|
|
|
5532
5501
|
/**
|
|
5533
5502
|
* Removes `quantity` units of `reward_id` from `line_uid` or the order,
|
|
5534
5503
|
* latest applied first. Removing more units than applied clears them all
|
|
5535
|
-
* (and
|
|
5536
|
-
*
|
|
5504
|
+
* (and their REDEMPTION tender). A redeem that keeps some units keeps
|
|
5505
|
+
* their share of its `amount`.
|
|
5537
5506
|
*/
|
|
5538
5507
|
removeRedeem(input) {
|
|
5539
5508
|
if (this.deleted) return;
|
|
@@ -5689,10 +5658,10 @@ var Order = class Order {
|
|
|
5689
5658
|
lines: draft.lines,
|
|
5690
5659
|
redeem: draft.redeem ?? []
|
|
5691
5660
|
};
|
|
5692
|
-
const
|
|
5661
|
+
const redemption_steps = redemptionSteps(draft);
|
|
5693
5662
|
const b5 = {
|
|
5694
5663
|
totals: draft.totals,
|
|
5695
|
-
redemption_steps
|
|
5664
|
+
redemption_steps
|
|
5696
5665
|
};
|
|
5697
5666
|
const body = {
|
|
5698
5667
|
...b1,
|
|
@@ -5704,15 +5673,16 @@ var Order = class Order {
|
|
|
5704
5673
|
await services.orders.validate(body, this.http);
|
|
5705
5674
|
}
|
|
5706
5675
|
/**
|
|
5707
|
-
* Tenders of the next intent:
|
|
5708
|
-
*
|
|
5709
|
-
* 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.
|
|
5710
5680
|
*/
|
|
5711
5681
|
getTenders(payment) {
|
|
5712
|
-
const redeem =
|
|
5682
|
+
const redeem = cappedRedeems(this.draft);
|
|
5713
5683
|
const providers = this.redemptionProviders;
|
|
5714
5684
|
const redemption = redemptionTenderInputs(redeem, providers);
|
|
5715
|
-
if (
|
|
5685
|
+
if (redeemsCoverTotal(redeem, this.draft.totals)) return redemption;
|
|
5716
5686
|
const simple = payment ? simpleTenderInput(payment) : null;
|
|
5717
5687
|
return [...simple ? [simple] : this.realTenders ?? [], ...redemption];
|
|
5718
5688
|
}
|
|
@@ -5735,6 +5705,13 @@ var Order = class Order {
|
|
|
5735
5705
|
this.applyServerOrder(next);
|
|
5736
5706
|
return this.getOrder();
|
|
5737
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
|
+
}
|
|
5738
5715
|
async syncPaymentIntent(tenders) {
|
|
5739
5716
|
await this.persist();
|
|
5740
5717
|
await this.flush.settle();
|
|
@@ -5751,7 +5728,7 @@ var Order = class Order {
|
|
|
5751
5728
|
}
|
|
5752
5729
|
/** Keeps the applied redeems while the server holds the same capped ones. */
|
|
5753
5730
|
replaceDraft(next) {
|
|
5754
|
-
if (redeemSignature
|
|
5731
|
+
if (redeemSignature(this.draft.redeem ?? []) !== redeemSignature(next.redeem ?? [])) this.requestedRedeem = null;
|
|
5755
5732
|
this.draft = next;
|
|
5756
5733
|
}
|
|
5757
5734
|
appliedRedeem() {
|
|
@@ -5761,10 +5738,14 @@ var Order = class Order {
|
|
|
5761
5738
|
* Places the order with a DRAFT intent: `intent_id` as is, else the
|
|
5762
5739
|
* tenders of `getTenders(input.payment)` saved first (simple flow: one
|
|
5763
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.
|
|
5764
5744
|
*/
|
|
5765
5745
|
async placeOrder(input = {}) {
|
|
5766
5746
|
await this.persist();
|
|
5767
5747
|
await this.flush.settle();
|
|
5748
|
+
if (!input.intent_id) this.assertPayable(input.payment);
|
|
5768
5749
|
await this.validateOrder();
|
|
5769
5750
|
let body = input;
|
|
5770
5751
|
if (!input.intent_id) {
|
|
@@ -5879,29 +5860,28 @@ var Order = class Order {
|
|
|
5879
5860
|
runPaymentTerminal(payment, key) {
|
|
5880
5861
|
return this.payments.runTerminal(payment, key);
|
|
5881
5862
|
}
|
|
5882
|
-
/**
|
|
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
|
+
*/
|
|
5883
5867
|
commitLines(patch) {
|
|
5884
|
-
const patched = patch.lines ?? this.draft.lines;
|
|
5868
|
+
const patched = rebuildRootGroups(patch.lines ?? this.draft.lines);
|
|
5885
5869
|
const applied = patch.redeem ?? this.appliedRedeem();
|
|
5886
|
-
const
|
|
5887
|
-
const
|
|
5888
|
-
const { lines, redemption_steps, totals } = recomputeRedemptions({
|
|
5870
|
+
const totals = recomputeTotals(patched, this.draft.totals.currency);
|
|
5871
|
+
const capped = capRedeems({
|
|
5889
5872
|
lines: patched,
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
currency
|
|
5873
|
+
totals,
|
|
5874
|
+
redeem: applied
|
|
5893
5875
|
});
|
|
5894
|
-
const redeem =
|
|
5895
|
-
|
|
5896
|
-
totals
|
|
5897
|
-
}), currency);
|
|
5876
|
+
const { lines, redeem } = capped;
|
|
5877
|
+
const redemption_steps = redemptionSteps(capped);
|
|
5898
5878
|
this.requestedRedeem = applied;
|
|
5899
5879
|
this.draft = {
|
|
5900
5880
|
...this.draft,
|
|
5901
5881
|
lines,
|
|
5902
5882
|
redeem,
|
|
5903
|
-
|
|
5904
|
-
|
|
5883
|
+
totals,
|
|
5884
|
+
redemption_steps
|
|
5905
5885
|
};
|
|
5906
5886
|
this.scheduleAfterMutate();
|
|
5907
5887
|
}
|
|
@@ -6453,56 +6433,70 @@ const expiredIssues = (redeem, now) => {
|
|
|
6453
6433
|
});
|
|
6454
6434
|
return issues;
|
|
6455
6435
|
};
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
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;
|
|
6461
6443
|
};
|
|
6462
6444
|
/**
|
|
6463
|
-
* Every
|
|
6464
|
-
*
|
|
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`).
|
|
6465
6448
|
*/
|
|
6466
|
-
const
|
|
6467
|
-
const
|
|
6468
|
-
|
|
6469
|
-
const
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
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`
|
|
6486
|
+
};
|
|
6487
|
+
const b2 = { expected: bigStr(expected?.toNumber() ?? 0) };
|
|
6488
|
+
issues.push({
|
|
6489
|
+
...b1,
|
|
6502
6490
|
...b2,
|
|
6503
|
-
actual:
|
|
6491
|
+
actual: bigStr(item.amount)
|
|
6504
6492
|
});
|
|
6505
|
-
}
|
|
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(",");
|
|
6506
6500
|
};
|
|
6507
6501
|
const STATE_FIELDS = [
|
|
6508
6502
|
"value",
|
|
@@ -6520,37 +6514,39 @@ const flattenState = (prefix, state, out) => {
|
|
|
6520
6514
|
for (const field of STATE_FIELDS) out[`${prefix}.${field}`] = state ? bigStr(state[field]) : "";
|
|
6521
6515
|
};
|
|
6522
6516
|
const flattenStep = (step) => {
|
|
6523
|
-
const b1 = {
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
target: step.target,
|
|
6527
|
-
target_id: step.target_id ?? ""
|
|
6517
|
+
const b1 = {
|
|
6518
|
+
action: step.action,
|
|
6519
|
+
before_taxes: String(step.before_taxes)
|
|
6528
6520
|
};
|
|
6529
|
-
const
|
|
6530
|
-
const b5 = { discount_amount: bigStr(step.discount_amount) };
|
|
6521
|
+
const b2 = { discount_amount: bigStr(step.discount_amount) };
|
|
6531
6522
|
const out = {
|
|
6532
6523
|
...b1,
|
|
6533
|
-
...b2
|
|
6534
|
-
...b3,
|
|
6535
|
-
...b4,
|
|
6536
|
-
...b5
|
|
6524
|
+
...b2
|
|
6537
6525
|
};
|
|
6538
6526
|
for (const key of STATE_KEYS) flattenState(key, step[key], out);
|
|
6539
6527
|
return out;
|
|
6540
6528
|
};
|
|
6541
|
-
/**
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
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
|
+
};
|
|
6547
6544
|
issues.push({
|
|
6548
6545
|
...b1,
|
|
6549
6546
|
...b2,
|
|
6550
|
-
|
|
6551
|
-
actual: String(submitted.length)
|
|
6547
|
+
actual: actualSignature
|
|
6552
6548
|
});
|
|
6553
|
-
return;
|
|
6549
|
+
return issues;
|
|
6554
6550
|
}
|
|
6555
6551
|
submitted.forEach((step, index) => {
|
|
6556
6552
|
const actual = flattenStep(step);
|
|
@@ -6570,22 +6566,36 @@ const compareRedemptionSteps = (submitted, expected, issues) => {
|
|
|
6570
6566
|
});
|
|
6571
6567
|
}
|
|
6572
6568
|
});
|
|
6569
|
+
return issues;
|
|
6573
6570
|
};
|
|
6574
6571
|
const validateRedemptions = (input, issues, lineIssues) => {
|
|
6575
6572
|
const redeem = input.redeem ?? [];
|
|
6576
|
-
const steps = input.redemptionSteps ?? [];
|
|
6577
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);
|
|
6578
6580
|
issues.push(...expiredIssues(redeem, now));
|
|
6579
|
-
|
|
6580
|
-
|
|
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);
|
|
6581
6585
|
for (const issue of expired) pushLineIssue(lineIssues, line.uid, issue);
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
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));
|
|
6587
6597
|
};
|
|
6588
|
-
const
|
|
6598
|
+
const ROOT_GROUP_FIELDS = [
|
|
6589
6599
|
["group_discount", "DISCOUNT_MISMATCH"],
|
|
6590
6600
|
["unit_group_discount", "DISCOUNT_MISMATCH"],
|
|
6591
6601
|
["group_tax_total", "TAX_MISMATCH"],
|
|
@@ -6593,14 +6603,17 @@ const REDEEMED_GROUP_FIELDS = [
|
|
|
6593
6603
|
["group_total", "PRICE_MISMATCH"],
|
|
6594
6604
|
["unit_group_total", "PRICE_MISMATCH"]
|
|
6595
6605
|
];
|
|
6596
|
-
/**
|
|
6597
|
-
|
|
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) => {
|
|
6598
6611
|
const byUid = new Map(expected.map((line) => [line.uid, line]));
|
|
6599
6612
|
for (const line of submitted) {
|
|
6600
6613
|
if (line.parent_uid != null) continue;
|
|
6601
6614
|
const wanted = byUid.get(line.uid);
|
|
6602
6615
|
if (!wanted) continue;
|
|
6603
|
-
for (const [field, code] of
|
|
6616
|
+
for (const [field, code] of ROOT_GROUP_FIELDS) {
|
|
6604
6617
|
if (bigEq(line[field], wanted[field])) continue;
|
|
6605
6618
|
const b1 = {
|
|
6606
6619
|
code,
|
|
@@ -6625,7 +6638,7 @@ const validateOrderAgainstMenu = (input) => {
|
|
|
6625
6638
|
lines: {}
|
|
6626
6639
|
};
|
|
6627
6640
|
if (!input.menuActive) issues.push({ code: "MENU_CLOSED" });
|
|
6628
|
-
const menuLines =
|
|
6641
|
+
const menuLines = rebuildRootGroups(input.lines);
|
|
6629
6642
|
const roots = menuLines.filter((line) => line.parent_uid == null);
|
|
6630
6643
|
if (roots.length === 0) {
|
|
6631
6644
|
issues.push({ code: "EMPTY_ORDER" });
|
|
@@ -6645,9 +6658,9 @@ const validateOrderAgainstMenu = (input) => {
|
|
|
6645
6658
|
}
|
|
6646
6659
|
checkRootProductQuantities(roots, lookup, lineIssues);
|
|
6647
6660
|
if (input.expectedFeeLines) compareExpectedFeeLines(input.lines, input.expectedFeeLines, lineIssues);
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
const expectedTotals = recomputeTotals(
|
|
6661
|
+
validateRedemptions(input, issues, lineIssues);
|
|
6662
|
+
compareRootGroups(input.lines, menuLines, lineIssues);
|
|
6663
|
+
const expectedTotals = recomputeTotals(menuLines, input.totals.currency);
|
|
6651
6664
|
for (const field of TOTALS_FIELDS) {
|
|
6652
6665
|
if (bigEq(input.totals[field], expectedTotals[field])) continue;
|
|
6653
6666
|
issues.push({
|
|
@@ -6959,391 +6972,8 @@ const verifyWebhookSignature = async (input) => {
|
|
|
6959
6972
|
return safeEqual(received, await hmacHex(secret, body));
|
|
6960
6973
|
};
|
|
6961
6974
|
//#endregion
|
|
6962
|
-
//#region
|
|
6963
|
-
|
|
6964
|
-
if (value) return;
|
|
6965
|
-
throw new Error(message);
|
|
6966
|
-
};
|
|
6967
|
-
//#endregion
|
|
6968
|
-
//#region src/rules/common.ts
|
|
6969
|
-
const primitiveError = (primitive) => {
|
|
6970
|
-
return `Condition primitive must be ${primitive}`;
|
|
6971
|
-
};
|
|
6972
|
-
const typeError = (type) => {
|
|
6973
|
-
return `Condition type must be ${type}`;
|
|
6974
|
-
};
|
|
6975
|
-
const defaultConditionError = "Condition was not met";
|
|
6976
|
-
const getLineByProductId = (lines, productId) => {
|
|
6977
|
-
if (!productId) return void 0;
|
|
6978
|
-
return lines?.find((line) => line.id === productId);
|
|
6979
|
-
};
|
|
6980
|
-
const countCartProducts = (lines, includeModifiers) => {
|
|
6981
|
-
if (!lines?.length) return 0;
|
|
6982
|
-
return lines.filter((line) => {
|
|
6983
|
-
if (line.type === "FEE") return false;
|
|
6984
|
-
if (includeModifiers) return true;
|
|
6985
|
-
if (!(line.parent_uid == null)) return false;
|
|
6986
|
-
return line.type === "PRODUCT" || line.type == null;
|
|
6987
|
-
}).reduce((sum, line) => sum + line.quantity, 0);
|
|
6988
|
-
};
|
|
6989
|
-
const getLineTagValues = (lines, tag) => {
|
|
6990
|
-
if (!tag) return [];
|
|
6991
|
-
const values = [];
|
|
6992
|
-
for (const line of lines ?? []) {
|
|
6993
|
-
const value = line.tags?.[tag];
|
|
6994
|
-
if (typeof value === "undefined") continue;
|
|
6995
|
-
values.push(value);
|
|
6996
|
-
}
|
|
6997
|
-
return values;
|
|
6998
|
-
};
|
|
6999
|
-
//#endregion
|
|
7000
|
-
//#region src/rules/booleanMatcher.ts
|
|
7001
|
-
const matchBooleanOperator = (condition, matcher) => {
|
|
7002
|
-
const { primitive, operator, value } = condition;
|
|
7003
|
-
assert(primitive === "boolean", primitiveError("boolean"));
|
|
7004
|
-
if (typeof matcher === "undefined") return false;
|
|
7005
|
-
assert(typeof value === "boolean", "Invalid boolean value");
|
|
7006
|
-
switch (operator) {
|
|
7007
|
-
case "is": return matcher === value;
|
|
7008
|
-
case "is not": return matcher !== value;
|
|
7009
|
-
default: throw new Error(`Operator ${operator} not supported in boolean matcher`);
|
|
7010
|
-
}
|
|
7011
|
-
};
|
|
7012
|
-
const isAnonymousCustomer = (condition, isAnonymous) => {
|
|
7013
|
-
const { type, primitive, value } = condition;
|
|
7014
|
-
const matcherType = "Customer is anonymous";
|
|
7015
|
-
assert(primitive === "boolean", primitiveError("boolean"));
|
|
7016
|
-
assert(type === matcherType, typeError(matcherType));
|
|
7017
|
-
assert(value != null, "Invalid value");
|
|
7018
|
-
return matchBooleanOperator(condition, isAnonymous ?? false);
|
|
7019
|
-
};
|
|
7020
|
-
const orderIncludesProductWithTag$3 = (condition, matcher) => {
|
|
7021
|
-
const { type, primitive, value } = condition;
|
|
7022
|
-
const matcherType = "Order includes product with tag";
|
|
7023
|
-
assert(primitive === "boolean", primitiveError("boolean"));
|
|
7024
|
-
assert(type === matcherType, typeError(matcherType));
|
|
7025
|
-
assert(value != null, "Invalid value");
|
|
7026
|
-
return matchBooleanOperator(condition, matcher);
|
|
7027
|
-
};
|
|
7028
|
-
const isAnonymousOrder = (condition, isAnonymous) => {
|
|
7029
|
-
const { type, primitive, value } = condition;
|
|
7030
|
-
const matcherType = "Is anonymous order";
|
|
7031
|
-
assert(primitive === "boolean", primitiveError("boolean"));
|
|
7032
|
-
assert(type === matcherType, typeError(matcherType));
|
|
7033
|
-
assert(value != null, "Invalid value");
|
|
7034
|
-
return matchBooleanOperator(condition, isAnonymous ?? false);
|
|
7035
|
-
};
|
|
7036
|
-
const paymentIsMixed = (condition, isMixed) => {
|
|
7037
|
-
const { type, primitive, value } = condition;
|
|
7038
|
-
const matcherType = "Payment is mixed";
|
|
7039
|
-
assert(primitive === "boolean", primitiveError("boolean"));
|
|
7040
|
-
assert(type === matcherType, typeError(matcherType));
|
|
7041
|
-
assert(value != null, "Invalid value");
|
|
7042
|
-
return matchBooleanOperator(condition, isMixed ?? false);
|
|
7043
|
-
};
|
|
7044
|
-
const paymentCaptured = (condition, captured) => {
|
|
7045
|
-
const { type, primitive, value } = condition;
|
|
7046
|
-
const matcherType = "Payment captured";
|
|
7047
|
-
assert(primitive === "boolean", primitiveError("boolean"));
|
|
7048
|
-
assert(type === matcherType, typeError(matcherType));
|
|
7049
|
-
assert(value != null, "Invalid value");
|
|
7050
|
-
return matchBooleanOperator(condition, captured ?? false);
|
|
7051
|
-
};
|
|
7052
|
-
//#endregion
|
|
7053
|
-
//#region src/rules/currencyMatcher.ts
|
|
7054
|
-
const matchCurrencyOperator = (condition, matcher) => {
|
|
7055
|
-
const { primitive, operator, value } = condition;
|
|
7056
|
-
assert(primitive === "currency", primitiveError("currency"));
|
|
7057
|
-
if (typeof matcher === "undefined") return false;
|
|
7058
|
-
assert(typeof value === "number", "Invalid currency value");
|
|
7059
|
-
switch (operator) {
|
|
7060
|
-
case "equal to": return matcher === value;
|
|
7061
|
-
case "not equal to": return matcher !== value;
|
|
7062
|
-
case "greater than": return matcher > value;
|
|
7063
|
-
case "greater than or equal to": return matcher >= value;
|
|
7064
|
-
case "less than": return matcher < value;
|
|
7065
|
-
case "less than or equal to": return matcher <= value;
|
|
7066
|
-
default: throw new Error(`Operator ${operator} not supported in currency matcher`);
|
|
7067
|
-
}
|
|
7068
|
-
};
|
|
7069
|
-
const orderIncludesProductWithTag$2 = (condition, matcher) => {
|
|
7070
|
-
const { type, primitive, value } = condition;
|
|
7071
|
-
const matcherType = "Order includes product with tag";
|
|
7072
|
-
assert(primitive === "currency", primitiveError("currency"));
|
|
7073
|
-
assert(type === matcherType, typeError(matcherType));
|
|
7074
|
-
assert(value != null, "Invalid value");
|
|
7075
|
-
return matchCurrencyOperator(condition, matcher);
|
|
7076
|
-
};
|
|
7077
|
-
const orderTotal = (condition, total) => {
|
|
7078
|
-
const { type, primitive, value } = condition;
|
|
7079
|
-
const matcherType = "Order total";
|
|
7080
|
-
assert(primitive === "currency", primitiveError("currency"));
|
|
7081
|
-
assert(type === matcherType, typeError(matcherType));
|
|
7082
|
-
assert(value != null, "Invalid value");
|
|
7083
|
-
return matchCurrencyOperator(condition, total);
|
|
7084
|
-
};
|
|
7085
|
-
//#endregion
|
|
7086
|
-
//#region src/rules/customMatcher.ts
|
|
7087
|
-
const MULTI_OPERATORS = [
|
|
7088
|
-
"is any of",
|
|
7089
|
-
"is none of",
|
|
7090
|
-
"is all of"
|
|
7091
|
-
];
|
|
7092
|
-
const isMultiOperator = (operator) => {
|
|
7093
|
-
return MULTI_OPERATORS.includes(operator);
|
|
7094
|
-
};
|
|
7095
|
-
const splitCsv = (value) => {
|
|
7096
|
-
return value.split(",").map((part) => part.trim()).filter(Boolean);
|
|
7097
|
-
};
|
|
7098
|
-
const matchCustomEquality = (condition, matcher) => {
|
|
7099
|
-
const { primitive, operator, value } = condition;
|
|
7100
|
-
assert(primitive === "custom", primitiveError("custom"));
|
|
7101
|
-
if (typeof matcher === "undefined") return false;
|
|
7102
|
-
assert(typeof value === "string", "Invalid custom value");
|
|
7103
|
-
switch (operator) {
|
|
7104
|
-
case "is": return matcher === value;
|
|
7105
|
-
case "is not": return matcher !== value;
|
|
7106
|
-
default: throw new Error(`Operator ${operator} not supported in custom matcher`);
|
|
7107
|
-
}
|
|
7108
|
-
};
|
|
7109
|
-
const matchCustomMulti = (condition, matcher) => {
|
|
7110
|
-
const { primitive, operator, value } = condition;
|
|
7111
|
-
assert(primitive === "custom", primitiveError("custom"));
|
|
7112
|
-
if (typeof matcher === "undefined") return false;
|
|
7113
|
-
assert(typeof value === "string", "Invalid custom value");
|
|
7114
|
-
const allowed = splitCsv(value);
|
|
7115
|
-
if (allowed.length === 0) return false;
|
|
7116
|
-
const candidates = typeof matcher === "object" ? matcher : [matcher];
|
|
7117
|
-
if (candidates.length === 0) return false;
|
|
7118
|
-
if (operator === "is any of") return allowed.some((id) => candidates.includes(id));
|
|
7119
|
-
if (operator === "is none of") return !allowed.some((id) => candidates.includes(id));
|
|
7120
|
-
if (operator === "is all of") return allowed.every((id) => candidates.includes(id));
|
|
7121
|
-
throw new Error(`Operator ${operator} not supported in custom matcher`);
|
|
7122
|
-
};
|
|
7123
|
-
/**
|
|
7124
|
-
* The operator picks the arity: `Payment` is single-select in order rules and
|
|
7125
|
-
* multi-select in fulfillment workflows. `matcher` may be a set (one id per
|
|
7126
|
-
* tender): `is` matches when some id matches, `is not` when none does.
|
|
7127
|
-
*/
|
|
7128
|
-
const matchCustom = (condition, matcher) => {
|
|
7129
|
-
if (isMultiOperator(condition.operator)) return matchCustomMulti(condition, matcher);
|
|
7130
|
-
if (typeof matcher === "undefined" || typeof matcher === "string") return matchCustomEquality(condition, matcher);
|
|
7131
|
-
if (matcher.length === 0) return false;
|
|
7132
|
-
const matches = (id) => matchCustomEquality(condition, id);
|
|
7133
|
-
if (condition.operator === "is not") return matcher.every(matches);
|
|
7134
|
-
return matcher.some(matches);
|
|
7135
|
-
};
|
|
7136
|
-
const assertCustom = (condition, order) => {
|
|
7137
|
-
const { type } = condition;
|
|
7138
|
-
switch (type) {
|
|
7139
|
-
case "Fulfillment":
|
|
7140
|
-
case "Fulfillments": return matchCustom(condition, order.fulfillment_id);
|
|
7141
|
-
case "Store":
|
|
7142
|
-
case "Stores": return matchCustom(condition, order.store_id);
|
|
7143
|
-
case "Channel":
|
|
7144
|
-
case "Channels": return matchCustom(condition, order.channel_id);
|
|
7145
|
-
case "Brand":
|
|
7146
|
-
case "Brands": return matchCustom(condition, order.brand_id);
|
|
7147
|
-
default: throw new Error(`Condition ${type} not supported in custom matcher`);
|
|
7148
|
-
}
|
|
7149
|
-
};
|
|
7150
|
-
//#endregion
|
|
7151
|
-
//#region src/rules/enumMatcher.ts
|
|
7152
|
-
const matchEnumOperator = (condition, matcher) => {
|
|
7153
|
-
const { primitive, operator, value } = condition;
|
|
7154
|
-
assert(primitive === "enum", primitiveError("enum"));
|
|
7155
|
-
if (typeof matcher === "undefined") return false;
|
|
7156
|
-
switch (operator) {
|
|
7157
|
-
case "is":
|
|
7158
|
-
assert(typeof value === "string", "Expected a single enum value");
|
|
7159
|
-
return matcher === value;
|
|
7160
|
-
case "is not":
|
|
7161
|
-
assert(typeof value === "string", "Expected a single enum value");
|
|
7162
|
-
return matcher !== value;
|
|
7163
|
-
case "is any of":
|
|
7164
|
-
assert(Array.isArray(value), "Expected a list of enum values");
|
|
7165
|
-
return value.includes(matcher);
|
|
7166
|
-
case "is none of":
|
|
7167
|
-
assert(Array.isArray(value), "Expected a list of enum values");
|
|
7168
|
-
return !value.includes(matcher);
|
|
7169
|
-
default: throw new Error(`Operator ${operator} not supported in enum matcher`);
|
|
7170
|
-
}
|
|
7171
|
-
};
|
|
7172
|
-
const NEGATIVE_ENUM_OPERATORS = ["is not", "is none of"];
|
|
7173
|
-
/**
|
|
7174
|
-
* Enum condition over a set (one value per tender): positive operators
|
|
7175
|
-
* match when some value matches, negative ones when none matches.
|
|
7176
|
-
*/
|
|
7177
|
-
const matchEnumSet = (condition, matcher) => {
|
|
7178
|
-
if (typeof matcher === "undefined" || typeof matcher === "string") return matchEnumOperator(condition, matcher);
|
|
7179
|
-
if (matcher.length === 0) return false;
|
|
7180
|
-
const matches = (value) => matchEnumOperator(condition, value);
|
|
7181
|
-
if (NEGATIVE_ENUM_OPERATORS.includes(condition.operator)) return matcher.every(matches);
|
|
7182
|
-
return matcher.some(matches);
|
|
7183
|
-
};
|
|
7184
|
-
const paymentCollectAt = (condition, collectAt) => {
|
|
7185
|
-
const matcherType = "Payment collect at";
|
|
7186
|
-
assert(condition.primitive === "enum", primitiveError("enum"));
|
|
7187
|
-
assert(condition.type === matcherType, typeError(matcherType));
|
|
7188
|
-
return matchEnumSet(condition, collectAt);
|
|
7189
|
-
};
|
|
7190
|
-
const paymentMethod = (condition, method) => {
|
|
7191
|
-
const matcherType = "Payment method";
|
|
7192
|
-
assert(condition.primitive === "enum", primitiveError("enum"));
|
|
7193
|
-
assert(condition.type === matcherType, typeError(matcherType));
|
|
7194
|
-
return matchEnumSet(condition, method);
|
|
7195
|
-
};
|
|
7196
|
-
const paymentStatus = (condition, status) => {
|
|
7197
|
-
const matcherType = "Payment status";
|
|
7198
|
-
assert(condition.primitive === "enum", primitiveError("enum"));
|
|
7199
|
-
assert(condition.type === matcherType, typeError(matcherType));
|
|
7200
|
-
return matchEnumOperator(condition, status);
|
|
7201
|
-
};
|
|
7202
|
-
const CONDITION_INTERPRETATION_DEFAULT = "server";
|
|
7203
|
-
const isInterpretationCompatible = (interpretation, runtime, fallback) => {
|
|
7204
|
-
if (!runtime) return true;
|
|
7205
|
-
const value = interpretation ?? fallback;
|
|
7206
|
-
if (value === "both") return true;
|
|
7207
|
-
return value === runtime;
|
|
7208
|
-
};
|
|
7209
|
-
//#endregion
|
|
7210
|
-
//#region src/rules/numberMatcher.ts
|
|
7211
|
-
const matchNumberOperator = (condition, matcher) => {
|
|
7212
|
-
const { primitive, operator, value } = condition;
|
|
7213
|
-
assert(primitive === "number", primitiveError("number"));
|
|
7214
|
-
if (typeof matcher === "undefined") return false;
|
|
7215
|
-
assert(typeof value === "number", "Invalid number value");
|
|
7216
|
-
switch (operator) {
|
|
7217
|
-
case "equal to": return matcher === value;
|
|
7218
|
-
case "not equal to": return matcher !== value;
|
|
7219
|
-
case "greater than": return matcher > value;
|
|
7220
|
-
case "greater than or equal to": return matcher >= value;
|
|
7221
|
-
case "less than": return matcher < value;
|
|
7222
|
-
case "less than or equal to": return matcher <= value;
|
|
7223
|
-
default: throw new Error(`Operator ${operator} not supported in number matcher`);
|
|
7224
|
-
}
|
|
7225
|
-
};
|
|
7226
|
-
const orderIncludesProduct = (condition, quantity) => {
|
|
7227
|
-
const { type, primitive, value } = condition;
|
|
7228
|
-
const matcherType = "Order includes product";
|
|
7229
|
-
assert(primitive === "number", primitiveError("number"));
|
|
7230
|
-
assert(type === matcherType, typeError(matcherType));
|
|
7231
|
-
assert(value != null, "Invalid value");
|
|
7232
|
-
return matchNumberOperator(condition, quantity ?? 0);
|
|
7233
|
-
};
|
|
7234
|
-
const cartProductCount = (condition, count) => {
|
|
7235
|
-
const { type, primitive, value } = condition;
|
|
7236
|
-
const matcherType = "Cart product count";
|
|
7237
|
-
assert(primitive === "number", primitiveError("number"));
|
|
7238
|
-
assert(type === matcherType, typeError(matcherType));
|
|
7239
|
-
assert(value != null, "Invalid value");
|
|
7240
|
-
return matchNumberOperator(condition, count);
|
|
7241
|
-
};
|
|
7242
|
-
const orderIncludesProductWithTag$1 = (condition, matcher) => {
|
|
7243
|
-
const { type, primitive, value } = condition;
|
|
7244
|
-
const matcherType = "Order includes product with tag";
|
|
7245
|
-
assert(primitive === "number", primitiveError("number"));
|
|
7246
|
-
assert(type === matcherType, typeError(matcherType));
|
|
7247
|
-
assert(value != null, "Invalid value");
|
|
7248
|
-
return matchNumberOperator(condition, matcher);
|
|
7249
|
-
};
|
|
7250
|
-
//#endregion
|
|
7251
|
-
//#region src/rules/stringCondition.utils.ts
|
|
7252
|
-
const getCaseInsensitive = (condition) => {
|
|
7253
|
-
return condition.caseInsensitive ?? false;
|
|
7254
|
-
};
|
|
7255
|
-
const normalizeStringForMatch = (value, caseInsensitive) => {
|
|
7256
|
-
if (caseInsensitive) return value.toLowerCase();
|
|
7257
|
-
return value;
|
|
7258
|
-
};
|
|
7259
|
-
const normalizePairForMatch = (matcher, expected, caseInsensitive) => {
|
|
7260
|
-
return {
|
|
7261
|
-
matcher: normalizeStringForMatch(matcher, caseInsensitive),
|
|
7262
|
-
expected: normalizeStringForMatch(expected, caseInsensitive)
|
|
7263
|
-
};
|
|
7264
|
-
};
|
|
7265
|
-
//#endregion
|
|
7266
|
-
//#region src/rules/stringMatcher.ts
|
|
7267
|
-
const matchStringOperator = (condition, matcher) => {
|
|
7268
|
-
const { primitive, operator, value } = condition;
|
|
7269
|
-
assert(primitive === "string", primitiveError("string"));
|
|
7270
|
-
if (typeof matcher === "undefined") return false;
|
|
7271
|
-
const caseInsensitive = getCaseInsensitive(condition);
|
|
7272
|
-
const pair = normalizePairForMatch(matcher, String(value), caseInsensitive);
|
|
7273
|
-
const normalizedMatcher = pair.matcher;
|
|
7274
|
-
const normalizedExpected = pair.expected;
|
|
7275
|
-
switch (operator) {
|
|
7276
|
-
case "equal to": return normalizedMatcher === normalizedExpected;
|
|
7277
|
-
case "not equal to": return normalizedMatcher !== normalizedExpected;
|
|
7278
|
-
case "starts with": return normalizedMatcher.startsWith(normalizedExpected);
|
|
7279
|
-
case "ends with": return normalizedMatcher.endsWith(normalizedExpected);
|
|
7280
|
-
case "contains": return normalizedMatcher.includes(normalizedExpected);
|
|
7281
|
-
default: throw new Error(`Operator ${operator} not supported in string matcher`);
|
|
7282
|
-
}
|
|
7283
|
-
};
|
|
7284
|
-
const orderIncludesProductWithTag = (condition, matcher) => {
|
|
7285
|
-
const { type, primitive, value } = condition;
|
|
7286
|
-
const matcherType = "Order includes product with tag";
|
|
7287
|
-
assert(primitive === "string", primitiveError("string"));
|
|
7288
|
-
assert(type === matcherType, typeError(matcherType));
|
|
7289
|
-
assert(value != null, "Invalid value");
|
|
7290
|
-
return matchStringOperator(condition, matcher);
|
|
7291
|
-
};
|
|
7292
|
-
//#endregion
|
|
7293
|
-
//#region src/rules/monitoringMatcher.ts
|
|
7294
|
-
const windowOf = (condition) => {
|
|
7295
|
-
if (!("window_minutes" in condition)) return void 0;
|
|
7296
|
-
return condition.window_minutes;
|
|
7297
|
-
};
|
|
7298
|
-
const accumulatedEvents = (condition, count) => {
|
|
7299
|
-
const { type, primitive, value } = condition;
|
|
7300
|
-
const matcherType = "Accumulated events";
|
|
7301
|
-
assert(primitive === "number", primitiveError("number"));
|
|
7302
|
-
assert(type === matcherType, typeError(matcherType));
|
|
7303
|
-
assert(value != null, "Invalid value");
|
|
7304
|
-
if (typeof count === "undefined") return false;
|
|
7305
|
-
return matchNumberOperator(condition, count);
|
|
7306
|
-
};
|
|
7307
|
-
const apiErrorRate = (condition, rate) => {
|
|
7308
|
-
const { type, primitive, value } = condition;
|
|
7309
|
-
const matcherType = "API error rate";
|
|
7310
|
-
assert(primitive === "number", primitiveError("number"));
|
|
7311
|
-
assert(type === matcherType, typeError(matcherType));
|
|
7312
|
-
assert(value != null, "Invalid value");
|
|
7313
|
-
if (typeof rate === "undefined") return false;
|
|
7314
|
-
return matchNumberOperator(condition, rate);
|
|
7315
|
-
};
|
|
7316
|
-
const httpStatus = (condition, status) => {
|
|
7317
|
-
const { type, primitive, value } = condition;
|
|
7318
|
-
const matcherType = "HTTP status";
|
|
7319
|
-
assert(primitive === "number", primitiveError("number"));
|
|
7320
|
-
assert(type === matcherType, typeError(matcherType));
|
|
7321
|
-
assert(value != null, "Invalid value");
|
|
7322
|
-
if (typeof status === "undefined") return false;
|
|
7323
|
-
return matchNumberOperator(condition, status);
|
|
7324
|
-
};
|
|
7325
|
-
const requestPath = (condition, path) => {
|
|
7326
|
-
const { type, primitive, value } = condition;
|
|
7327
|
-
const matcherType = "Request path";
|
|
7328
|
-
assert(primitive === "string", primitiveError("string"));
|
|
7329
|
-
assert(type === matcherType, typeError(matcherType));
|
|
7330
|
-
assert(value != null, "Invalid value");
|
|
7331
|
-
return matchStringOperator(condition, path);
|
|
7332
|
-
};
|
|
7333
|
-
const failureMessage = (condition, message) => {
|
|
7334
|
-
const { type, primitive, value } = condition;
|
|
7335
|
-
const matcherType = "Failure message";
|
|
7336
|
-
assert(primitive === "string", primitiveError("string"));
|
|
7337
|
-
assert(type === matcherType, typeError(matcherType));
|
|
7338
|
-
assert(value != null, "Invalid value");
|
|
7339
|
-
return matchStringOperator(condition, message);
|
|
7340
|
-
};
|
|
7341
|
-
const windowMinutes = (condition) => {
|
|
7342
|
-
return windowOf(condition);
|
|
7343
|
-
};
|
|
7344
|
-
//#endregion
|
|
7345
|
-
//#region src/rules/recurringDateCron.ts
|
|
7346
|
-
var import_dayjs_min = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
6975
|
+
//#region ../../node_modules/.pnpm/dayjs@1.11.9/node_modules/dayjs/dayjs.min.js
|
|
6976
|
+
var require_dayjs_min = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
7347
6977
|
(function(t, e) {
|
|
7348
6978
|
"object" == typeof exports && "undefined" != typeof module ? module.exports = e() : "function" == typeof define && define.amd ? define(e) : (t = "undefined" != typeof globalThis ? globalThis : t || self).dayjs = e();
|
|
7349
6979
|
})(exports, (function() {
|
|
@@ -7638,7 +7268,412 @@ var import_dayjs_min = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((
|
|
|
7638
7268
|
return w(1e3 * t);
|
|
7639
7269
|
}, w.en = D[g], w.Ls = D, w.p = {}, w;
|
|
7640
7270
|
}));
|
|
7641
|
-
}))
|
|
7271
|
+
}));
|
|
7272
|
+
//#endregion
|
|
7273
|
+
//#region src/rules/assert.ts
|
|
7274
|
+
const assert = (value, message) => {
|
|
7275
|
+
if (value) return;
|
|
7276
|
+
throw new Error(message);
|
|
7277
|
+
};
|
|
7278
|
+
//#endregion
|
|
7279
|
+
//#region src/rules/common.ts
|
|
7280
|
+
const primitiveError = (primitive) => {
|
|
7281
|
+
return `Condition primitive must be ${primitive}`;
|
|
7282
|
+
};
|
|
7283
|
+
const typeError = (type) => {
|
|
7284
|
+
return `Condition type must be ${type}`;
|
|
7285
|
+
};
|
|
7286
|
+
const defaultConditionError = "Condition was not met";
|
|
7287
|
+
const getLineByProductId = (lines, productId) => {
|
|
7288
|
+
if (!productId) return void 0;
|
|
7289
|
+
return lines?.find((line) => line.id === productId);
|
|
7290
|
+
};
|
|
7291
|
+
const countCartProducts = (lines, includeModifiers) => {
|
|
7292
|
+
if (!lines?.length) return 0;
|
|
7293
|
+
return lines.filter((line) => {
|
|
7294
|
+
if (line.type === "FEE") return false;
|
|
7295
|
+
if (includeModifiers) return true;
|
|
7296
|
+
if (!(line.parent_uid == null)) return false;
|
|
7297
|
+
return line.type === "PRODUCT" || line.type == null;
|
|
7298
|
+
}).reduce((sum, line) => sum + line.quantity, 0);
|
|
7299
|
+
};
|
|
7300
|
+
const getLineTagValues = (lines, tag) => {
|
|
7301
|
+
if (!tag) return [];
|
|
7302
|
+
const values = [];
|
|
7303
|
+
for (const line of lines ?? []) {
|
|
7304
|
+
const value = line.tags?.[tag];
|
|
7305
|
+
if (typeof value === "undefined") continue;
|
|
7306
|
+
values.push(value);
|
|
7307
|
+
}
|
|
7308
|
+
return values;
|
|
7309
|
+
};
|
|
7310
|
+
//#endregion
|
|
7311
|
+
//#region src/rules/absoluteDateMatcher.ts
|
|
7312
|
+
var import_dayjs_min = /* @__PURE__ */ __toESM(require_dayjs_min(), 1);
|
|
7313
|
+
const matchAbsoluteDateOperator = (condition, matcher) => {
|
|
7314
|
+
const { primitive, operator, value } = condition;
|
|
7315
|
+
assert(primitive === "absolute date", primitiveError("absolute date"));
|
|
7316
|
+
if (typeof matcher === "undefined") return false;
|
|
7317
|
+
const matcherDate = (0, import_dayjs_min.default)(matcher);
|
|
7318
|
+
const expectedDate = (0, import_dayjs_min.default)(value);
|
|
7319
|
+
switch (operator) {
|
|
7320
|
+
case "is": return matcherDate.isSame(expectedDate, "day");
|
|
7321
|
+
case "is not": return !matcherDate.isSame(expectedDate, "day");
|
|
7322
|
+
case "is before": return matcherDate.isBefore(expectedDate, "day");
|
|
7323
|
+
case "is after": return matcherDate.isAfter(expectedDate, "day");
|
|
7324
|
+
default: {
|
|
7325
|
+
const msg = `Operator ${operator} not supported in absolute date matcher`;
|
|
7326
|
+
throw new Error(msg);
|
|
7327
|
+
}
|
|
7328
|
+
}
|
|
7329
|
+
};
|
|
7330
|
+
//#endregion
|
|
7331
|
+
//#region src/rules/booleanMatcher.ts
|
|
7332
|
+
const matchBooleanOperator = (condition, matcher) => {
|
|
7333
|
+
const { primitive, operator, value } = condition;
|
|
7334
|
+
assert(primitive === "boolean", primitiveError("boolean"));
|
|
7335
|
+
if (typeof matcher === "undefined") return false;
|
|
7336
|
+
assert(typeof value === "boolean", "Invalid boolean value");
|
|
7337
|
+
switch (operator) {
|
|
7338
|
+
case "is": return matcher === value;
|
|
7339
|
+
case "is not": return matcher !== value;
|
|
7340
|
+
default: throw new Error(`Operator ${operator} not supported in boolean matcher`);
|
|
7341
|
+
}
|
|
7342
|
+
};
|
|
7343
|
+
const isAnonymousCustomer = (condition, isAnonymous) => {
|
|
7344
|
+
const { type, primitive, value } = condition;
|
|
7345
|
+
const matcherType = "Customer is anonymous";
|
|
7346
|
+
assert(primitive === "boolean", primitiveError("boolean"));
|
|
7347
|
+
assert(type === matcherType, typeError(matcherType));
|
|
7348
|
+
assert(value != null, "Invalid value");
|
|
7349
|
+
return matchBooleanOperator(condition, isAnonymous ?? false);
|
|
7350
|
+
};
|
|
7351
|
+
const orderIncludesProductWithTag$3 = (condition, matcher) => {
|
|
7352
|
+
const { type, primitive, value } = condition;
|
|
7353
|
+
const matcherType = "Order includes product with tag";
|
|
7354
|
+
assert(primitive === "boolean", primitiveError("boolean"));
|
|
7355
|
+
assert(type === matcherType, typeError(matcherType));
|
|
7356
|
+
assert(value != null, "Invalid value");
|
|
7357
|
+
return matchBooleanOperator(condition, matcher);
|
|
7358
|
+
};
|
|
7359
|
+
const isAnonymousOrder = (condition, isAnonymous) => {
|
|
7360
|
+
const { type, primitive, value } = condition;
|
|
7361
|
+
const matcherType = "Is anonymous order";
|
|
7362
|
+
assert(primitive === "boolean", primitiveError("boolean"));
|
|
7363
|
+
assert(type === matcherType, typeError(matcherType));
|
|
7364
|
+
assert(value != null, "Invalid value");
|
|
7365
|
+
return matchBooleanOperator(condition, isAnonymous ?? false);
|
|
7366
|
+
};
|
|
7367
|
+
const paymentIsMixed = (condition, isMixed) => {
|
|
7368
|
+
const { type, primitive, value } = condition;
|
|
7369
|
+
const matcherType = "Payment is mixed";
|
|
7370
|
+
assert(primitive === "boolean", primitiveError("boolean"));
|
|
7371
|
+
assert(type === matcherType, typeError(matcherType));
|
|
7372
|
+
assert(value != null, "Invalid value");
|
|
7373
|
+
return matchBooleanOperator(condition, isMixed ?? false);
|
|
7374
|
+
};
|
|
7375
|
+
const paymentCaptured = (condition, captured) => {
|
|
7376
|
+
const { type, primitive, value } = condition;
|
|
7377
|
+
const matcherType = "Payment captured";
|
|
7378
|
+
assert(primitive === "boolean", primitiveError("boolean"));
|
|
7379
|
+
assert(type === matcherType, typeError(matcherType));
|
|
7380
|
+
assert(value != null, "Invalid value");
|
|
7381
|
+
return matchBooleanOperator(condition, captured ?? false);
|
|
7382
|
+
};
|
|
7383
|
+
//#endregion
|
|
7384
|
+
//#region src/rules/currencyMatcher.ts
|
|
7385
|
+
const matchCurrencyOperator = (condition, matcher) => {
|
|
7386
|
+
const { primitive, operator, value } = condition;
|
|
7387
|
+
assert(primitive === "currency", primitiveError("currency"));
|
|
7388
|
+
if (typeof matcher === "undefined") return false;
|
|
7389
|
+
assert(typeof value === "number", "Invalid currency value");
|
|
7390
|
+
switch (operator) {
|
|
7391
|
+
case "equal to": return matcher === value;
|
|
7392
|
+
case "not equal to": return matcher !== value;
|
|
7393
|
+
case "greater than": return matcher > value;
|
|
7394
|
+
case "greater than or equal to": return matcher >= value;
|
|
7395
|
+
case "less than": return matcher < value;
|
|
7396
|
+
case "less than or equal to": return matcher <= value;
|
|
7397
|
+
default: throw new Error(`Operator ${operator} not supported in currency matcher`);
|
|
7398
|
+
}
|
|
7399
|
+
};
|
|
7400
|
+
const orderIncludesProductWithTag$2 = (condition, matcher) => {
|
|
7401
|
+
const { type, primitive, value } = condition;
|
|
7402
|
+
const matcherType = "Order includes product with tag";
|
|
7403
|
+
assert(primitive === "currency", primitiveError("currency"));
|
|
7404
|
+
assert(type === matcherType, typeError(matcherType));
|
|
7405
|
+
assert(value != null, "Invalid value");
|
|
7406
|
+
return matchCurrencyOperator(condition, matcher);
|
|
7407
|
+
};
|
|
7408
|
+
const orderTotal = (condition, total) => {
|
|
7409
|
+
const { type, primitive, value } = condition;
|
|
7410
|
+
const matcherType = "Order total";
|
|
7411
|
+
assert(primitive === "currency", primitiveError("currency"));
|
|
7412
|
+
assert(type === matcherType, typeError(matcherType));
|
|
7413
|
+
assert(value != null, "Invalid value");
|
|
7414
|
+
return matchCurrencyOperator(condition, total);
|
|
7415
|
+
};
|
|
7416
|
+
//#endregion
|
|
7417
|
+
//#region src/rules/customMatcher.ts
|
|
7418
|
+
const MULTI_OPERATORS = [
|
|
7419
|
+
"is any of",
|
|
7420
|
+
"is none of",
|
|
7421
|
+
"is all of"
|
|
7422
|
+
];
|
|
7423
|
+
const isMultiOperator = (operator) => {
|
|
7424
|
+
return MULTI_OPERATORS.includes(operator);
|
|
7425
|
+
};
|
|
7426
|
+
const splitCsv = (value) => {
|
|
7427
|
+
return value.split(",").map((part) => part.trim()).filter(Boolean);
|
|
7428
|
+
};
|
|
7429
|
+
const matchCustomEquality = (condition, matcher) => {
|
|
7430
|
+
const { primitive, operator, value } = condition;
|
|
7431
|
+
assert(primitive === "custom", primitiveError("custom"));
|
|
7432
|
+
if (typeof matcher === "undefined") return false;
|
|
7433
|
+
assert(typeof value === "string", "Invalid custom value");
|
|
7434
|
+
switch (operator) {
|
|
7435
|
+
case "is": return matcher === value;
|
|
7436
|
+
case "is not": return matcher !== value;
|
|
7437
|
+
default: throw new Error(`Operator ${operator} not supported in custom matcher`);
|
|
7438
|
+
}
|
|
7439
|
+
};
|
|
7440
|
+
const matchCustomMulti = (condition, matcher) => {
|
|
7441
|
+
const { primitive, operator, value } = condition;
|
|
7442
|
+
assert(primitive === "custom", primitiveError("custom"));
|
|
7443
|
+
if (typeof matcher === "undefined") return false;
|
|
7444
|
+
assert(typeof value === "string", "Invalid custom value");
|
|
7445
|
+
const allowed = splitCsv(value);
|
|
7446
|
+
if (allowed.length === 0) return false;
|
|
7447
|
+
const candidates = typeof matcher === "object" ? matcher : [matcher];
|
|
7448
|
+
if (candidates.length === 0) return false;
|
|
7449
|
+
if (operator === "is any of") return allowed.some((id) => candidates.includes(id));
|
|
7450
|
+
if (operator === "is none of") return !allowed.some((id) => candidates.includes(id));
|
|
7451
|
+
if (operator === "is all of") return allowed.every((id) => candidates.includes(id));
|
|
7452
|
+
throw new Error(`Operator ${operator} not supported in custom matcher`);
|
|
7453
|
+
};
|
|
7454
|
+
/**
|
|
7455
|
+
* The operator picks the arity: `Payment` is single-select in order rules and
|
|
7456
|
+
* multi-select in fulfillment workflows. `matcher` may be a set (one id per
|
|
7457
|
+
* tender): `is` matches when some id matches, `is not` when none does.
|
|
7458
|
+
*/
|
|
7459
|
+
const matchCustom = (condition, matcher) => {
|
|
7460
|
+
if (isMultiOperator(condition.operator)) return matchCustomMulti(condition, matcher);
|
|
7461
|
+
if (typeof matcher === "undefined" || typeof matcher === "string") return matchCustomEquality(condition, matcher);
|
|
7462
|
+
if (matcher.length === 0) return false;
|
|
7463
|
+
const matches = (id) => matchCustomEquality(condition, id);
|
|
7464
|
+
if (condition.operator === "is not") return matcher.every(matches);
|
|
7465
|
+
return matcher.some(matches);
|
|
7466
|
+
};
|
|
7467
|
+
const assertCustom = (condition, order) => {
|
|
7468
|
+
const { type } = condition;
|
|
7469
|
+
switch (type) {
|
|
7470
|
+
case "Fulfillment":
|
|
7471
|
+
case "Fulfillments": return matchCustom(condition, order.fulfillment_id);
|
|
7472
|
+
case "Store":
|
|
7473
|
+
case "Stores": return matchCustom(condition, order.store_id);
|
|
7474
|
+
case "Channel":
|
|
7475
|
+
case "Channels": return matchCustom(condition, order.channel_id);
|
|
7476
|
+
case "Brand":
|
|
7477
|
+
case "Brands": return matchCustom(condition, order.brand_id);
|
|
7478
|
+
default: throw new Error(`Condition ${type} not supported in custom matcher`);
|
|
7479
|
+
}
|
|
7480
|
+
};
|
|
7481
|
+
//#endregion
|
|
7482
|
+
//#region src/rules/enumMatcher.ts
|
|
7483
|
+
const matchEnumOperator = (condition, matcher) => {
|
|
7484
|
+
const { primitive, operator, value } = condition;
|
|
7485
|
+
assert(primitive === "enum", primitiveError("enum"));
|
|
7486
|
+
if (typeof matcher === "undefined") return false;
|
|
7487
|
+
switch (operator) {
|
|
7488
|
+
case "is":
|
|
7489
|
+
assert(typeof value === "string", "Expected a single enum value");
|
|
7490
|
+
return matcher === value;
|
|
7491
|
+
case "is not":
|
|
7492
|
+
assert(typeof value === "string", "Expected a single enum value");
|
|
7493
|
+
return matcher !== value;
|
|
7494
|
+
case "is any of":
|
|
7495
|
+
assert(Array.isArray(value), "Expected a list of enum values");
|
|
7496
|
+
return value.includes(matcher);
|
|
7497
|
+
case "is none of":
|
|
7498
|
+
assert(Array.isArray(value), "Expected a list of enum values");
|
|
7499
|
+
return !value.includes(matcher);
|
|
7500
|
+
default: throw new Error(`Operator ${operator} not supported in enum matcher`);
|
|
7501
|
+
}
|
|
7502
|
+
};
|
|
7503
|
+
const NEGATIVE_ENUM_OPERATORS = ["is not", "is none of"];
|
|
7504
|
+
/**
|
|
7505
|
+
* Enum condition over a set (one value per tender): positive operators
|
|
7506
|
+
* match when some value matches, negative ones when none matches.
|
|
7507
|
+
*/
|
|
7508
|
+
const matchEnumSet = (condition, matcher) => {
|
|
7509
|
+
if (typeof matcher === "undefined" || typeof matcher === "string") return matchEnumOperator(condition, matcher);
|
|
7510
|
+
if (matcher.length === 0) return false;
|
|
7511
|
+
const matches = (value) => matchEnumOperator(condition, value);
|
|
7512
|
+
if (NEGATIVE_ENUM_OPERATORS.includes(condition.operator)) return matcher.every(matches);
|
|
7513
|
+
return matcher.some(matches);
|
|
7514
|
+
};
|
|
7515
|
+
const paymentCollectAt = (condition, collectAt) => {
|
|
7516
|
+
const matcherType = "Payment collect at";
|
|
7517
|
+
assert(condition.primitive === "enum", primitiveError("enum"));
|
|
7518
|
+
assert(condition.type === matcherType, typeError(matcherType));
|
|
7519
|
+
return matchEnumSet(condition, collectAt);
|
|
7520
|
+
};
|
|
7521
|
+
const paymentMethod = (condition, method) => {
|
|
7522
|
+
const matcherType = "Payment method";
|
|
7523
|
+
assert(condition.primitive === "enum", primitiveError("enum"));
|
|
7524
|
+
assert(condition.type === matcherType, typeError(matcherType));
|
|
7525
|
+
return matchEnumSet(condition, method);
|
|
7526
|
+
};
|
|
7527
|
+
const paymentStatus = (condition, status) => {
|
|
7528
|
+
const matcherType = "Payment status";
|
|
7529
|
+
assert(condition.primitive === "enum", primitiveError("enum"));
|
|
7530
|
+
assert(condition.type === matcherType, typeError(matcherType));
|
|
7531
|
+
return matchEnumOperator(condition, status);
|
|
7532
|
+
};
|
|
7533
|
+
const CONDITION_INTERPRETATION_DEFAULT = "server";
|
|
7534
|
+
const isInterpretationCompatible = (interpretation, runtime, fallback) => {
|
|
7535
|
+
if (!runtime) return true;
|
|
7536
|
+
const value = interpretation ?? fallback;
|
|
7537
|
+
if (value === "both") return true;
|
|
7538
|
+
return value === runtime;
|
|
7539
|
+
};
|
|
7540
|
+
//#endregion
|
|
7541
|
+
//#region src/rules/numberMatcher.ts
|
|
7542
|
+
const matchNumberOperator = (condition, matcher) => {
|
|
7543
|
+
const { primitive, operator, value } = condition;
|
|
7544
|
+
assert(primitive === "number", primitiveError("number"));
|
|
7545
|
+
if (typeof matcher === "undefined") return false;
|
|
7546
|
+
assert(typeof value === "number", "Invalid number value");
|
|
7547
|
+
switch (operator) {
|
|
7548
|
+
case "equal to": return matcher === value;
|
|
7549
|
+
case "not equal to": return matcher !== value;
|
|
7550
|
+
case "greater than": return matcher > value;
|
|
7551
|
+
case "greater than or equal to": return matcher >= value;
|
|
7552
|
+
case "less than": return matcher < value;
|
|
7553
|
+
case "less than or equal to": return matcher <= value;
|
|
7554
|
+
default: throw new Error(`Operator ${operator} not supported in number matcher`);
|
|
7555
|
+
}
|
|
7556
|
+
};
|
|
7557
|
+
const orderIncludesProduct = (condition, quantity) => {
|
|
7558
|
+
const { type, primitive, value } = condition;
|
|
7559
|
+
const matcherType = "Order includes product";
|
|
7560
|
+
assert(primitive === "number", primitiveError("number"));
|
|
7561
|
+
assert(type === matcherType, typeError(matcherType));
|
|
7562
|
+
assert(value != null, "Invalid value");
|
|
7563
|
+
return matchNumberOperator(condition, quantity ?? 0);
|
|
7564
|
+
};
|
|
7565
|
+
const cartProductCount = (condition, count) => {
|
|
7566
|
+
const { type, primitive, value } = condition;
|
|
7567
|
+
const matcherType = "Cart product count";
|
|
7568
|
+
assert(primitive === "number", primitiveError("number"));
|
|
7569
|
+
assert(type === matcherType, typeError(matcherType));
|
|
7570
|
+
assert(value != null, "Invalid value");
|
|
7571
|
+
return matchNumberOperator(condition, count);
|
|
7572
|
+
};
|
|
7573
|
+
const orderIncludesProductWithTag$1 = (condition, matcher) => {
|
|
7574
|
+
const { type, primitive, value } = condition;
|
|
7575
|
+
const matcherType = "Order includes product with tag";
|
|
7576
|
+
assert(primitive === "number", primitiveError("number"));
|
|
7577
|
+
assert(type === matcherType, typeError(matcherType));
|
|
7578
|
+
assert(value != null, "Invalid value");
|
|
7579
|
+
return matchNumberOperator(condition, matcher);
|
|
7580
|
+
};
|
|
7581
|
+
//#endregion
|
|
7582
|
+
//#region src/rules/stringCondition.utils.ts
|
|
7583
|
+
const getCaseInsensitive = (condition) => {
|
|
7584
|
+
return condition.caseInsensitive ?? false;
|
|
7585
|
+
};
|
|
7586
|
+
const normalizeStringForMatch = (value, caseInsensitive) => {
|
|
7587
|
+
if (caseInsensitive) return value.toLowerCase();
|
|
7588
|
+
return value;
|
|
7589
|
+
};
|
|
7590
|
+
const normalizePairForMatch = (matcher, expected, caseInsensitive) => {
|
|
7591
|
+
return {
|
|
7592
|
+
matcher: normalizeStringForMatch(matcher, caseInsensitive),
|
|
7593
|
+
expected: normalizeStringForMatch(expected, caseInsensitive)
|
|
7594
|
+
};
|
|
7595
|
+
};
|
|
7596
|
+
//#endregion
|
|
7597
|
+
//#region src/rules/stringMatcher.ts
|
|
7598
|
+
const matchStringOperator = (condition, matcher) => {
|
|
7599
|
+
const { primitive, operator, value } = condition;
|
|
7600
|
+
assert(primitive === "string", primitiveError("string"));
|
|
7601
|
+
if (typeof matcher === "undefined") return false;
|
|
7602
|
+
const caseInsensitive = getCaseInsensitive(condition);
|
|
7603
|
+
const pair = normalizePairForMatch(matcher, String(value), caseInsensitive);
|
|
7604
|
+
const normalizedMatcher = pair.matcher;
|
|
7605
|
+
const normalizedExpected = pair.expected;
|
|
7606
|
+
switch (operator) {
|
|
7607
|
+
case "equal to": return normalizedMatcher === normalizedExpected;
|
|
7608
|
+
case "not equal to": return normalizedMatcher !== normalizedExpected;
|
|
7609
|
+
case "starts with": return normalizedMatcher.startsWith(normalizedExpected);
|
|
7610
|
+
case "ends with": return normalizedMatcher.endsWith(normalizedExpected);
|
|
7611
|
+
case "contains": return normalizedMatcher.includes(normalizedExpected);
|
|
7612
|
+
default: throw new Error(`Operator ${operator} not supported in string matcher`);
|
|
7613
|
+
}
|
|
7614
|
+
};
|
|
7615
|
+
const orderIncludesProductWithTag = (condition, matcher) => {
|
|
7616
|
+
const { type, primitive, value } = condition;
|
|
7617
|
+
const matcherType = "Order includes product with tag";
|
|
7618
|
+
assert(primitive === "string", primitiveError("string"));
|
|
7619
|
+
assert(type === matcherType, typeError(matcherType));
|
|
7620
|
+
assert(value != null, "Invalid value");
|
|
7621
|
+
return matchStringOperator(condition, matcher);
|
|
7622
|
+
};
|
|
7623
|
+
//#endregion
|
|
7624
|
+
//#region src/rules/monitoringMatcher.ts
|
|
7625
|
+
const windowOf = (condition) => {
|
|
7626
|
+
if (!("window_minutes" in condition)) return void 0;
|
|
7627
|
+
return condition.window_minutes;
|
|
7628
|
+
};
|
|
7629
|
+
const accumulatedEvents = (condition, count) => {
|
|
7630
|
+
const { type, primitive, value } = condition;
|
|
7631
|
+
const matcherType = "Accumulated events";
|
|
7632
|
+
assert(primitive === "number", primitiveError("number"));
|
|
7633
|
+
assert(type === matcherType, typeError(matcherType));
|
|
7634
|
+
assert(value != null, "Invalid value");
|
|
7635
|
+
if (typeof count === "undefined") return false;
|
|
7636
|
+
return matchNumberOperator(condition, count);
|
|
7637
|
+
};
|
|
7638
|
+
const apiErrorRate = (condition, rate) => {
|
|
7639
|
+
const { type, primitive, value } = condition;
|
|
7640
|
+
const matcherType = "API error rate";
|
|
7641
|
+
assert(primitive === "number", primitiveError("number"));
|
|
7642
|
+
assert(type === matcherType, typeError(matcherType));
|
|
7643
|
+
assert(value != null, "Invalid value");
|
|
7644
|
+
if (typeof rate === "undefined") return false;
|
|
7645
|
+
return matchNumberOperator(condition, rate);
|
|
7646
|
+
};
|
|
7647
|
+
const httpStatus = (condition, status) => {
|
|
7648
|
+
const { type, primitive, value } = condition;
|
|
7649
|
+
const matcherType = "HTTP status";
|
|
7650
|
+
assert(primitive === "number", primitiveError("number"));
|
|
7651
|
+
assert(type === matcherType, typeError(matcherType));
|
|
7652
|
+
assert(value != null, "Invalid value");
|
|
7653
|
+
if (typeof status === "undefined") return false;
|
|
7654
|
+
return matchNumberOperator(condition, status);
|
|
7655
|
+
};
|
|
7656
|
+
const requestPath = (condition, path) => {
|
|
7657
|
+
const { type, primitive, value } = condition;
|
|
7658
|
+
const matcherType = "Request path";
|
|
7659
|
+
assert(primitive === "string", primitiveError("string"));
|
|
7660
|
+
assert(type === matcherType, typeError(matcherType));
|
|
7661
|
+
assert(value != null, "Invalid value");
|
|
7662
|
+
return matchStringOperator(condition, path);
|
|
7663
|
+
};
|
|
7664
|
+
const failureMessage = (condition, message) => {
|
|
7665
|
+
const { type, primitive, value } = condition;
|
|
7666
|
+
const matcherType = "Failure message";
|
|
7667
|
+
assert(primitive === "string", primitiveError("string"));
|
|
7668
|
+
assert(type === matcherType, typeError(matcherType));
|
|
7669
|
+
assert(value != null, "Invalid value");
|
|
7670
|
+
return matchStringOperator(condition, message);
|
|
7671
|
+
};
|
|
7672
|
+
const windowMinutes = (condition) => {
|
|
7673
|
+
return windowOf(condition);
|
|
7674
|
+
};
|
|
7675
|
+
//#endregion
|
|
7676
|
+
//#region src/rules/recurringDateCron.ts
|
|
7642
7677
|
import_dayjs_min.default.extend(utc);
|
|
7643
7678
|
import_dayjs_min.default.extend(timezone);
|
|
7644
7679
|
import_dayjs_min.default.extend(isBetween);
|
|
@@ -7835,6 +7870,47 @@ const currentDate = (condition, referenceDate) => {
|
|
|
7835
7870
|
return matchRecurringDateOperator(condition, current);
|
|
7836
7871
|
};
|
|
7837
7872
|
//#endregion
|
|
7873
|
+
//#region src/rules/relativeDateMatcher.ts
|
|
7874
|
+
const matchRelativeDateOperator = (condition, matcher, referenceDate) => {
|
|
7875
|
+
const { primitive, operator, value } = condition;
|
|
7876
|
+
assert(primitive === "relative date", primitiveError("relative date"));
|
|
7877
|
+
if (typeof matcher === "undefined") return false;
|
|
7878
|
+
assert(typeof value === "string", "Invalid relative date value");
|
|
7879
|
+
const [amountString, unitRaw] = value.split("/");
|
|
7880
|
+
assert(amountString && unitRaw, "Amount & Unit required");
|
|
7881
|
+
const amount = parseInt(amountString, 10);
|
|
7882
|
+
const unit = unitRaw;
|
|
7883
|
+
const issueDate = (0, import_dayjs_min.default)(referenceDate ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
7884
|
+
const matcherDate = (0, import_dayjs_min.default)(matcher);
|
|
7885
|
+
switch (operator) {
|
|
7886
|
+
case "occurred": {
|
|
7887
|
+
const endDate = issueDate.subtract(amount, unit).endOf(unit);
|
|
7888
|
+
const startDate = endDate.startOf(unit);
|
|
7889
|
+
const sameStart = matcherDate.isSame(startDate, unit);
|
|
7890
|
+
const afterStart = matcherDate.isAfter(startDate);
|
|
7891
|
+
const sameEnd = matcherDate.isSame(endDate, unit);
|
|
7892
|
+
const beforeEnd = matcherDate.isBefore(endDate);
|
|
7893
|
+
return (sameStart || afterStart) && (sameEnd || beforeEnd);
|
|
7894
|
+
}
|
|
7895
|
+
case "occurred in the last": {
|
|
7896
|
+
const targetDate = issueDate.subtract(amount, unit);
|
|
7897
|
+
const sameTarget = matcherDate.isSame(targetDate, unit);
|
|
7898
|
+
const afterTarget = matcherDate.isAfter(targetDate);
|
|
7899
|
+
const sameIssue = matcherDate.isSame(issueDate, unit);
|
|
7900
|
+
const beforeIssue = matcherDate.isBefore(issueDate);
|
|
7901
|
+
return (sameTarget || afterTarget) && (sameIssue || beforeIssue);
|
|
7902
|
+
}
|
|
7903
|
+
case "occurred more than": {
|
|
7904
|
+
const targetDate = issueDate.subtract(amount, unit);
|
|
7905
|
+
return matcherDate.isBefore(targetDate, unit);
|
|
7906
|
+
}
|
|
7907
|
+
default: {
|
|
7908
|
+
const msg = `Operator ${operator} not supported in relative date matcher`;
|
|
7909
|
+
throw new Error(msg);
|
|
7910
|
+
}
|
|
7911
|
+
}
|
|
7912
|
+
};
|
|
7913
|
+
//#endregion
|
|
7838
7914
|
//#region src/rules/RulesEngine.ts
|
|
7839
7915
|
var RulesEngine = class {
|
|
7840
7916
|
order;
|
|
@@ -7932,6 +8008,7 @@ var RulesEngine = class {
|
|
|
7932
8008
|
const monitoring = this.getDependency("monitoring", type);
|
|
7933
8009
|
return httpStatus(condition, monitoring.http_status);
|
|
7934
8010
|
}
|
|
8011
|
+
if (type === "Customer age") return matchNumberOperator(condition, this.customerAge());
|
|
7935
8012
|
throw new Error(`Condition ${type} not supported in number matcher`);
|
|
7936
8013
|
};
|
|
7937
8014
|
assertBoolean = (condition) => {
|
|
@@ -7989,17 +8066,40 @@ var RulesEngine = class {
|
|
|
7989
8066
|
default: throw new Error(`Condition ${type} not supported in enum matcher`);
|
|
7990
8067
|
}
|
|
7991
8068
|
};
|
|
8069
|
+
customerAge = () => {
|
|
8070
|
+
const dob = this.customer?.dob;
|
|
8071
|
+
if (!dob) return void 0;
|
|
8072
|
+
const born = new Date(dob);
|
|
8073
|
+
const now = new Date(this.referenceDate ?? Date.now());
|
|
8074
|
+
if (Number.isNaN(born.getTime()) || Number.isNaN(now.getTime())) return;
|
|
8075
|
+
let age = now.getUTCFullYear() - born.getUTCFullYear();
|
|
8076
|
+
const month = now.getUTCMonth() - born.getUTCMonth();
|
|
8077
|
+
const beforeBirthday = now.getUTCDate() < born.getUTCDate();
|
|
8078
|
+
if (month < 0 || month === 0 && beforeBirthday) age -= 1;
|
|
8079
|
+
return age;
|
|
8080
|
+
};
|
|
8081
|
+
customerDate = (type) => {
|
|
8082
|
+
const customer = this.customer;
|
|
8083
|
+
if (type === "Birthday") return customer?.dob;
|
|
8084
|
+
if (type === "Registration") return customer?.registered_at;
|
|
8085
|
+
if (type === "Customer last visit") return customer?.last_visit_at;
|
|
8086
|
+
if (type === "Customer last session") return customer?.last_session_at;
|
|
8087
|
+
};
|
|
7992
8088
|
assertAbsoluteDate = (condition) => {
|
|
7993
8089
|
const { primitive, type } = condition;
|
|
7994
8090
|
assert(primitive === "absolute date", primitiveError("absolute date"));
|
|
7995
|
-
|
|
7996
|
-
|
|
8091
|
+
if (type !== "Today") {
|
|
8092
|
+
const msg = `Condition ${type} not supported in absolute date matcher`;
|
|
8093
|
+
throw new Error(msg);
|
|
8094
|
+
}
|
|
8095
|
+
const today = this.referenceDate ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
8096
|
+
return matchAbsoluteDateOperator(condition, today);
|
|
7997
8097
|
};
|
|
7998
8098
|
assertRelativeDate = (condition) => {
|
|
7999
8099
|
const { primitive, type } = condition;
|
|
8000
8100
|
assert(primitive === "relative date", primitiveError("relative date"));
|
|
8001
|
-
const
|
|
8002
|
-
|
|
8101
|
+
const fact = this.customerDate(type);
|
|
8102
|
+
return matchRelativeDateOperator(condition, fact, this.referenceDate);
|
|
8003
8103
|
};
|
|
8004
8104
|
assertRecurringDate = (condition) => {
|
|
8005
8105
|
const { primitive, type } = condition;
|
|
@@ -8104,66 +8204,6 @@ var RulesEngine = class {
|
|
|
8104
8204
|
};
|
|
8105
8205
|
};
|
|
8106
8206
|
//#endregion
|
|
8107
|
-
//#region src/rules/absoluteDateMatcher.ts
|
|
8108
|
-
const matchAbsoluteDateOperator = (condition, matcher) => {
|
|
8109
|
-
const { primitive, operator, value } = condition;
|
|
8110
|
-
assert(primitive === "absolute date", primitiveError("absolute date"));
|
|
8111
|
-
if (typeof matcher === "undefined") return false;
|
|
8112
|
-
const matcherDate = (0, import_dayjs_min.default)(matcher);
|
|
8113
|
-
const expectedDate = (0, import_dayjs_min.default)(value);
|
|
8114
|
-
switch (operator) {
|
|
8115
|
-
case "is": return matcherDate.isSame(expectedDate, "day");
|
|
8116
|
-
case "is not": return !matcherDate.isSame(expectedDate, "day");
|
|
8117
|
-
case "is before": return matcherDate.isBefore(expectedDate, "day");
|
|
8118
|
-
case "is after": return matcherDate.isAfter(expectedDate, "day");
|
|
8119
|
-
default: {
|
|
8120
|
-
const msg = `Operator ${operator} not supported in absolute date matcher`;
|
|
8121
|
-
throw new Error(msg);
|
|
8122
|
-
}
|
|
8123
|
-
}
|
|
8124
|
-
};
|
|
8125
|
-
//#endregion
|
|
8126
|
-
//#region src/rules/relativeDateMatcher.ts
|
|
8127
|
-
const matchRelativeDateOperator = (condition, matcher, referenceDate) => {
|
|
8128
|
-
const { primitive, operator, value } = condition;
|
|
8129
|
-
assert(primitive === "relative date", primitiveError("relative date"));
|
|
8130
|
-
if (typeof matcher === "undefined") return false;
|
|
8131
|
-
assert(typeof value === "string", "Invalid relative date value");
|
|
8132
|
-
const [amountString, unitRaw] = value.split("/");
|
|
8133
|
-
assert(amountString && unitRaw, "Amount & Unit required");
|
|
8134
|
-
const amount = parseInt(amountString, 10);
|
|
8135
|
-
const unit = unitRaw;
|
|
8136
|
-
const issueDate = (0, import_dayjs_min.default)(referenceDate ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
8137
|
-
const matcherDate = (0, import_dayjs_min.default)(matcher);
|
|
8138
|
-
switch (operator) {
|
|
8139
|
-
case "occurred": {
|
|
8140
|
-
const endDate = issueDate.subtract(amount, unit).endOf(unit);
|
|
8141
|
-
const startDate = endDate.startOf(unit);
|
|
8142
|
-
const sameStart = matcherDate.isSame(startDate, unit);
|
|
8143
|
-
const afterStart = matcherDate.isAfter(startDate);
|
|
8144
|
-
const sameEnd = matcherDate.isSame(endDate, unit);
|
|
8145
|
-
const beforeEnd = matcherDate.isBefore(endDate);
|
|
8146
|
-
return (sameStart || afterStart) && (sameEnd || beforeEnd);
|
|
8147
|
-
}
|
|
8148
|
-
case "occurred in the last": {
|
|
8149
|
-
const targetDate = issueDate.subtract(amount, unit);
|
|
8150
|
-
const sameTarget = matcherDate.isSame(targetDate, unit);
|
|
8151
|
-
const afterTarget = matcherDate.isAfter(targetDate);
|
|
8152
|
-
const sameIssue = matcherDate.isSame(issueDate, unit);
|
|
8153
|
-
const beforeIssue = matcherDate.isBefore(issueDate);
|
|
8154
|
-
return (sameTarget || afterTarget) && (sameIssue || beforeIssue);
|
|
8155
|
-
}
|
|
8156
|
-
case "occurred more than": {
|
|
8157
|
-
const targetDate = issueDate.subtract(amount, unit);
|
|
8158
|
-
return matcherDate.isBefore(targetDate, unit);
|
|
8159
|
-
}
|
|
8160
|
-
default: {
|
|
8161
|
-
const msg = `Operator ${operator} not supported in relative date matcher`;
|
|
8162
|
-
throw new Error(msg);
|
|
8163
|
-
}
|
|
8164
|
-
}
|
|
8165
|
-
};
|
|
8166
|
-
//#endregion
|
|
8167
8207
|
//#region src/rules/checkout.ts
|
|
8168
8208
|
const DEFAULT_SHIPPING_FIELDS = {
|
|
8169
8209
|
address_line1: "required",
|
|
@@ -8616,4 +8656,4 @@ const cardPreviewGroups = (value) => {
|
|
|
8616
8656
|
return groups;
|
|
8617
8657
|
};
|
|
8618
8658
|
//#endregion
|
|
8619
|
-
export { CARD_BRANDS, DEFAULT_RECOMMENDATION_TYPES, Order, OrderSyncPendingError, OrderValidationError, RecurringDateCron, RulesEngine, StorefrontHttpError, addLines, auth, buildCustomerSnapshot, cardBrandSpec, cardDigits, cardNumberGaps, cardNumberMaxLength, cardPreviewGroups, caretAfterDigits, collectOrderLineDiffs, computeStartingPrice, countCartProducts, coveredAmount, createSession, currentNodes, cvcMaxLength, defaultConditionError, detectCardBrand, flattenMenuCategories, flushLogs, formatCardNumber, formatCvc, formatExpiry, fromOrderLines, genId, genOrderId, getLogContext, getScheduleWindow, indexMenu, isCardNumberComplete, isCardNumberDefinitelyInvalid, isCheckoutFieldFilled, isCvcComplete, isDeviceCharged, isDeviceNotLinked, isExpiryComplete, isKioskUnauthorized, isMilestoneStepDone, isOnSchedule, isOrderValidationError, isPosDeviceNotLinked, isPosUnauthorized, isRealPaymentMethod, isStorefrontNotFound, isVariantBase, listMenusStream, localizePosError, log, logError, luhnCheck, matchAbsoluteDateOperator, matchBooleanOperator, matchCurrencyOperator, matchEnumOperator, matchNumberOperator, matchRecurringDateOperator, matchRelativeDateOperator, matchStringOperator, milestoneSteps, milestoneTrack, nestMenuCategories, normalizeCardholderName,
|
|
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 };
|