@fire-spark/core 0.1.1 → 0.2.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 +29 -4
- package/dist/index.js +246 -73
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -331,7 +331,12 @@ type ApplyRedeemInput = {
|
|
|
331
331
|
discount_rate?: number;
|
|
332
332
|
/** > 0, per unit; caps the rate discount when both are set. */
|
|
333
333
|
discount_amount?: number;
|
|
334
|
-
/**
|
|
334
|
+
/**
|
|
335
|
+
* Default `true`: discounts the pre-tax value and recomputes taxes on it.
|
|
336
|
+
* `false` discounts the value with taxes and leaves taxes unchanged.
|
|
337
|
+
*/
|
|
338
|
+
before_taxes?: boolean;
|
|
339
|
+
/** Root line to discount; omit to discount the whole order (prorated). */
|
|
335
340
|
line_uid?: string;
|
|
336
341
|
/** ISO datetime with offset; validation fails once it has passed. */
|
|
337
342
|
expires_at?: string;
|
|
@@ -568,15 +573,21 @@ declare class OrderValidationError extends Error {
|
|
|
568
573
|
declare const isOrderValidationError: (error: unknown) => error is OrderValidationError;
|
|
569
574
|
//#endregion
|
|
570
575
|
//#region src/order/redemptions.d.ts
|
|
571
|
-
/**
|
|
576
|
+
/**
|
|
577
|
+
* Root group money rebuilt from the line itself plus its children, i.e. the
|
|
578
|
+
* menu price before any redemption. Child lines are never discounted.
|
|
579
|
+
*/
|
|
580
|
+
declare const stripRedemptionLines: (lines: OrderLine[]) => OrderLine[];
|
|
581
|
+
/** Discounted lines, steps and totals, keeping step order. */
|
|
572
582
|
declare const recomputeRedemptions: (input: {
|
|
573
583
|
lines: OrderLine[];
|
|
574
584
|
redeem: OrderRedeem[];
|
|
575
585
|
redemption_steps: OrderRedemptionStep[];
|
|
576
586
|
currency: string;
|
|
577
587
|
}) => {
|
|
588
|
+
lines: OrderLine[];
|
|
578
589
|
redemption_steps: OrderRedemptionStep[];
|
|
579
|
-
totals: OrderTotals;
|
|
590
|
+
totals: import("@fire-spark/types").OrderTotals;
|
|
580
591
|
};
|
|
581
592
|
//#endregion
|
|
582
593
|
//#region src/order/totals.d.ts
|
|
@@ -871,6 +882,20 @@ type SerializedError = {
|
|
|
871
882
|
/** Accepts anything a `catch` can receive. */
|
|
872
883
|
declare const serializeError: (value: unknown) => SerializedError;
|
|
873
884
|
//#endregion
|
|
885
|
+
//#region src/webhooks/webhooks.types.d.ts
|
|
886
|
+
interface VerifyWebhookSignatureInput {
|
|
887
|
+
/** Webhook secret configured in Fire Spark. */
|
|
888
|
+
secret: string;
|
|
889
|
+
/** Raw request body, exactly as received. */
|
|
890
|
+
body: string;
|
|
891
|
+
/** `X-Firespark-Signature` header value (`sha256=<hex>`). */
|
|
892
|
+
signature: string | null | undefined;
|
|
893
|
+
}
|
|
894
|
+
//#endregion
|
|
895
|
+
//#region src/webhooks/verifyWebhookSignature.d.ts
|
|
896
|
+
/** Checks a Fire Spark webhook/callback signature in constant time. */
|
|
897
|
+
declare const verifyWebhookSignature: (input: VerifyWebhookSignatureInput) => Promise<boolean>;
|
|
898
|
+
//#endregion
|
|
874
899
|
//#region src/shared/genId.d.ts
|
|
875
900
|
type IdBuilder = {
|
|
876
901
|
/**
|
|
@@ -1087,4 +1112,4 @@ declare const validateCardholderName: (value: string) => CardholderNameError | n
|
|
|
1087
1112
|
*/
|
|
1088
1113
|
declare const cardPreviewGroups: (value: string) => CardPreviewSlot[][];
|
|
1089
1114
|
//#endregion
|
|
1090
|
-
export { type ApplyRedeemInput, type AuthConfigureOptions, CARD_BRANDS, type CardBrand, type CardBrandSpec, type CardCvcError, type CardExpiry, type CardExpiryError, type CardNumberError, type CardPreviewSlot, type CardholderNameError, type CreateOrderOptions, type CreateSessionOptions, type CustomizationPath, type CustomizationSession, DEFAULT_RECOMMENDATION_TYPES, type ExchangeOptions, type FlatMenuCategory, type GroupDisplayType, type IdBuilder, type ListMenusStreamOptions, type Menu, type MenuCategoryNode, type MenuLookup, type MilestoneSteps, type MilestoneTrackItem, type ModifierControlType, type ModifierCtx, type ModifierGroupCtx, type NestedMenuCategory, Order, type OrderCustomerSource, type OrderDraft, type OrderIssue, type OrderIssueCode, type OrderLineDiff, type OrderLineIssue, type OrderLineIssueCode, type OrderScope, OrderSyncPendingError, OrderValidationError, type OrderValidationResult, type PayOptions, type PaymentCardBrand, type PaymentRunOptions, type PaymentSessionOptions, type PricingModifier, type Product, type ProductCtx, type QuantitiesByPath, type RecommendationType, RecurringDateCron, type RemoveRedeemInput, type ReorderOrderOptions, type ResetOptions, type ReversalOutcome, RulesEngine, type ScheduleWindow, type SelectionStatus, type SelectionsByPath, type SerializedError, type ServiceHttpOptions, type Snapshot, type StorefrontAccessToken, StorefrontHttpError, type TrackWorkflow, type ValidateOptions, type ValidateOrderAgainstMenuInput, type Variant, type VariantGroup, type VariantGroupCtx, type VariantOptionCtx, type VariantSelection, type VisitedState, 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, isDeviceNotLinked, isExpiryComplete, isKioskUnauthorized, isMilestoneStepDone, isOnSchedule, isOrderValidationError, isPosDeviceNotLinked, isPosUnauthorized, isStorefrontNotFound, isVariantBase, listMenusStream, localizePosError, log, logError, luhnCheck, matchAbsoluteDateOperator, matchBooleanOperator, matchCurrencyOperator, matchEnumOperator, matchNumberOperator, matchRecurringDateOperator, matchRelativeDateOperator, matchStringOperator, milestoneSteps, milestoneTrack, nestMenuCategories, normalizeCardholderName, parseExpiry, paymentMatchesClientConditions, recomputeRedemptions, recomputeTotals, remainingAmount, removeLine, replaceLine, resolveCheckout, reversePayment, runPaymentAttempt, serializeError, services, setLineQuantity, storefrontJson, toOrderLines, toPaymentCardBrand, validateCardNumber, validateCardholderName, validateCvc, validateExpiry, validateOrderAgainstMenu, visitedStates };
|
|
1115
|
+
export { type ApplyRedeemInput, type AuthConfigureOptions, CARD_BRANDS, type CardBrand, type CardBrandSpec, type CardCvcError, type CardExpiry, type CardExpiryError, type CardNumberError, type CardPreviewSlot, type CardholderNameError, type CreateOrderOptions, type CreateSessionOptions, type CustomizationPath, type CustomizationSession, DEFAULT_RECOMMENDATION_TYPES, type ExchangeOptions, type FlatMenuCategory, type GroupDisplayType, type IdBuilder, type ListMenusStreamOptions, type Menu, type MenuCategoryNode, type MenuLookup, type MilestoneSteps, type MilestoneTrackItem, type ModifierControlType, type ModifierCtx, type ModifierGroupCtx, type NestedMenuCategory, Order, type OrderCustomerSource, type OrderDraft, type OrderIssue, type OrderIssueCode, type OrderLineDiff, type OrderLineIssue, type OrderLineIssueCode, type OrderScope, OrderSyncPendingError, OrderValidationError, type OrderValidationResult, type PayOptions, type PaymentCardBrand, type PaymentRunOptions, type PaymentSessionOptions, type PricingModifier, type Product, type ProductCtx, type QuantitiesByPath, type RecommendationType, RecurringDateCron, type RemoveRedeemInput, type ReorderOrderOptions, type ResetOptions, type ReversalOutcome, RulesEngine, type ScheduleWindow, type SelectionStatus, type SelectionsByPath, type SerializedError, type ServiceHttpOptions, type Snapshot, type StorefrontAccessToken, StorefrontHttpError, type TrackWorkflow, type ValidateOptions, type ValidateOrderAgainstMenuInput, type Variant, type VariantGroup, type VariantGroupCtx, type VariantOptionCtx, type VariantSelection, type VerifyWebhookSignatureInput, type VisitedState, 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, isDeviceNotLinked, isExpiryComplete, isKioskUnauthorized, isMilestoneStepDone, isOnSchedule, isOrderValidationError, isPosDeviceNotLinked, isPosUnauthorized, isStorefrontNotFound, isVariantBase, listMenusStream, localizePosError, log, logError, luhnCheck, matchAbsoluteDateOperator, matchBooleanOperator, matchCurrencyOperator, matchEnumOperator, matchNumberOperator, matchRecurringDateOperator, matchRelativeDateOperator, matchStringOperator, milestoneSteps, milestoneTrack, nestMenuCategories, normalizeCardholderName, parseExpiry, paymentMatchesClientConditions, recomputeRedemptions, recomputeTotals, remainingAmount, removeLine, replaceLine, resolveCheckout, reversePayment, runPaymentAttempt, serializeError, services, setLineQuantity, storefrontJson, stripRedemptionLines, toOrderLines, toPaymentCardBrand, validateCardNumber, validateCardholderName, validateCvc, validateExpiry, validateOrderAgainstMenu, verifyWebhookSignature, visitedStates };
|
package/dist/index.js
CHANGED
|
@@ -4355,7 +4355,7 @@ const ORDER_KEY = "ORDER";
|
|
|
4355
4355
|
/** Same rules as `vOrders.redeem()`; throws on invalid input. */
|
|
4356
4356
|
const buildRedeem = (input) => {
|
|
4357
4357
|
const { reward_id, quantity, discount_rate, discount_amount } = input;
|
|
4358
|
-
const { expires_at } = input;
|
|
4358
|
+
const { before_taxes, expires_at } = input;
|
|
4359
4359
|
if (!reward_id.trim()) throw new Error("Redeem requires reward_id");
|
|
4360
4360
|
if (!Number.isInteger(quantity) || quantity < 1) throw new Error("Redeem quantity must be an integer ≥ 1");
|
|
4361
4361
|
if (discount_rate === void 0 && discount_amount === void 0) throw new Error("Redeem requires discount_rate or discount_amount");
|
|
@@ -4369,6 +4369,7 @@ const buildRedeem = (input) => {
|
|
|
4369
4369
|
};
|
|
4370
4370
|
if (discount_rate !== void 0) redeem.discount_rate = discount_rate;
|
|
4371
4371
|
if (discount_amount !== void 0) redeem.discount_amount = discount_amount;
|
|
4372
|
+
if (before_taxes !== void 0) redeem.before_taxes = before_taxes;
|
|
4372
4373
|
if (expires_at !== void 0) redeem.expires_at = expires_at;
|
|
4373
4374
|
return redeem;
|
|
4374
4375
|
};
|
|
@@ -4447,26 +4448,71 @@ const sequenceRedeems = (lines, redeem, steps) => {
|
|
|
4447
4448
|
const minBig = (a, b) => {
|
|
4448
4449
|
return a.lt(b) ? a : b;
|
|
4449
4450
|
};
|
|
4450
|
-
const left = (run) => {
|
|
4451
|
-
return run.base.minus(run.discount);
|
|
4452
|
-
};
|
|
4453
4451
|
const rateOf = (discount, base) => {
|
|
4454
4452
|
if (base.lte(0)) return 0;
|
|
4455
4453
|
return minBig(discount.div(base), new Big(1)).toNumber();
|
|
4456
4454
|
};
|
|
4457
|
-
const snapshot = (
|
|
4455
|
+
const snapshot = (roots, quantity) => {
|
|
4456
|
+
let subtotal = new Big(0);
|
|
4457
|
+
let total = new Big(0);
|
|
4458
|
+
let discount = new Big(0);
|
|
4459
|
+
for (const root of roots) {
|
|
4460
|
+
subtotal = subtotal.plus(root.subtotal);
|
|
4461
|
+
total = total.plus(root.total);
|
|
4462
|
+
discount = discount.plus(root.discount);
|
|
4463
|
+
}
|
|
4458
4464
|
const b1 = {
|
|
4459
|
-
value:
|
|
4460
|
-
quantity
|
|
4465
|
+
value: total.toNumber(),
|
|
4466
|
+
quantity
|
|
4461
4467
|
};
|
|
4462
|
-
const b2 = { discount_amount:
|
|
4463
|
-
const b3 = { discount_rate: rateOf(
|
|
4468
|
+
const b2 = { discount_amount: discount.toNumber() };
|
|
4469
|
+
const b3 = { discount_rate: rateOf(discount, subtotal) };
|
|
4464
4470
|
return {
|
|
4465
4471
|
...b1,
|
|
4466
4472
|
...b2,
|
|
4467
4473
|
...b3
|
|
4468
4474
|
};
|
|
4469
4475
|
};
|
|
4476
|
+
const netOf = (root) => {
|
|
4477
|
+
const net = root.total.minus(root.tax);
|
|
4478
|
+
return net.gt(0) ? net : new Big(0);
|
|
4479
|
+
};
|
|
4480
|
+
/** What a redeem can still discount on a root: pre-tax or with taxes. */
|
|
4481
|
+
const baseOf = (root, beforeTaxes) => {
|
|
4482
|
+
return beforeTaxes ? netOf(root) : root.total;
|
|
4483
|
+
};
|
|
4484
|
+
/** Before taxes, taxes shrink with the pre-tax value they were charged on. */
|
|
4485
|
+
const discountRoot = (root, amount, beforeTaxes) => {
|
|
4486
|
+
if (amount.lte(0)) return;
|
|
4487
|
+
root.discount = root.discount.plus(amount);
|
|
4488
|
+
if (!beforeTaxes) {
|
|
4489
|
+
root.total = root.total.minus(amount);
|
|
4490
|
+
return;
|
|
4491
|
+
}
|
|
4492
|
+
const net = netOf(root);
|
|
4493
|
+
const nextNet = net.minus(amount);
|
|
4494
|
+
root.tax = root.tax.times(nextNet).div(net);
|
|
4495
|
+
root.total = nextNet.plus(root.tax);
|
|
4496
|
+
};
|
|
4497
|
+
/** Splits an order discount across roots by what each can still discount. */
|
|
4498
|
+
const discountOrder = (roots, amount, beforeTaxes) => {
|
|
4499
|
+
const bases = roots.map((root) => baseOf(root, beforeTaxes));
|
|
4500
|
+
const sum = bases.reduce((acc, base) => acc.plus(base), new Big(0));
|
|
4501
|
+
if (sum.lte(0) || amount.lte(0)) return;
|
|
4502
|
+
let last = -1;
|
|
4503
|
+
bases.forEach((base, index) => {
|
|
4504
|
+
if (base.gt(0)) last = index;
|
|
4505
|
+
});
|
|
4506
|
+
let rest = amount;
|
|
4507
|
+
roots.forEach((root, index) => {
|
|
4508
|
+
const base = bases[index];
|
|
4509
|
+
if (base.lte(0)) return;
|
|
4510
|
+
const share = amount.times(base).div(sum);
|
|
4511
|
+
const part = minBig(index === last ? rest : share, base);
|
|
4512
|
+
rest = rest.minus(part);
|
|
4513
|
+
discountRoot(root, part, beforeTaxes);
|
|
4514
|
+
});
|
|
4515
|
+
};
|
|
4470
4516
|
/**
|
|
4471
4517
|
* Rate compounds once per reward unit over the remaining value; amount is
|
|
4472
4518
|
* per unit. With both, the rate discount is capped at `amount × quantity`.
|
|
@@ -4480,38 +4526,117 @@ const redeemDiscount = (redeem, remaining) => {
|
|
|
4480
4526
|
const byRate = remaining.times(new Big(1).minus(kept));
|
|
4481
4527
|
return cap ? minBig(byRate, cap) : byRate;
|
|
4482
4528
|
};
|
|
4483
|
-
|
|
4529
|
+
/**
|
|
4530
|
+
* Root group money rebuilt from the line itself plus its children, i.e. the
|
|
4531
|
+
* menu price before any redemption. Child lines are never discounted.
|
|
4532
|
+
*/
|
|
4533
|
+
const stripRedemptionLines = (lines) => {
|
|
4534
|
+
const children = /* @__PURE__ */ new Map();
|
|
4535
|
+
for (const line of lines) {
|
|
4536
|
+
if (line.parent_uid == null) continue;
|
|
4537
|
+
const list = children.get(line.parent_uid) ?? [];
|
|
4538
|
+
list.push(line);
|
|
4539
|
+
children.set(line.parent_uid, list);
|
|
4540
|
+
}
|
|
4541
|
+
return lines.map((line) => {
|
|
4542
|
+
if (line.parent_uid != null) return line;
|
|
4543
|
+
const qty = new Big(line.quantity);
|
|
4544
|
+
let sub = new Big(0);
|
|
4545
|
+
let disc = new Big(0);
|
|
4546
|
+
let tax = new Big(0);
|
|
4547
|
+
let tot = new Big(0);
|
|
4548
|
+
for (const child of children.get(line.uid) ?? []) {
|
|
4549
|
+
sub = sub.plus(child.group_subtotal);
|
|
4550
|
+
disc = disc.plus(child.group_discount);
|
|
4551
|
+
tax = tax.plus(child.group_tax_total);
|
|
4552
|
+
tot = tot.plus(child.group_total);
|
|
4553
|
+
}
|
|
4554
|
+
const gSub = new Big(line.subtotal).plus(sub.times(qty));
|
|
4555
|
+
const gDisc = new Big(line.discount_total).plus(disc.times(qty));
|
|
4556
|
+
const gTax = new Big(line.tax_total).plus(tax.times(qty));
|
|
4557
|
+
const gTot = new Big(line.total).plus(tot.times(qty));
|
|
4558
|
+
const u1 = { unit_group_subtotal: gSub.div(qty).toNumber() };
|
|
4559
|
+
const u2 = { unit_group_discount: gDisc.div(qty).toNumber() };
|
|
4560
|
+
const u3 = { unit_group_tax_total: gTax.div(qty).toNumber() };
|
|
4561
|
+
const u4 = { unit_group_total: gTot.div(qty).toNumber() };
|
|
4562
|
+
const g1 = { group_subtotal: gSub.toNumber() };
|
|
4563
|
+
const g2 = { group_discount: gDisc.toNumber() };
|
|
4564
|
+
const g3 = { group_tax_total: gTax.toNumber() };
|
|
4565
|
+
const g4 = { group_total: gTot.toNumber() };
|
|
4566
|
+
const p1 = {
|
|
4567
|
+
...u1,
|
|
4568
|
+
...u2,
|
|
4569
|
+
...u3,
|
|
4570
|
+
...u4
|
|
4571
|
+
};
|
|
4572
|
+
return {
|
|
4573
|
+
...line,
|
|
4574
|
+
...p1,
|
|
4575
|
+
...g1,
|
|
4576
|
+
...g2,
|
|
4577
|
+
...g3,
|
|
4578
|
+
...g4
|
|
4579
|
+
};
|
|
4580
|
+
});
|
|
4581
|
+
};
|
|
4582
|
+
const withRootMoney = (line, money) => {
|
|
4583
|
+
const qty = new Big(line.quantity);
|
|
4584
|
+
const discount = new Big(line.group_discount).plus(money.discount);
|
|
4585
|
+
const u1 = { unit_group_discount: discount.div(qty).toNumber() };
|
|
4586
|
+
const u2 = { unit_group_tax_total: money.tax.div(qty).toNumber() };
|
|
4587
|
+
const u3 = { unit_group_total: money.total.div(qty).toNumber() };
|
|
4588
|
+
const g1 = { group_discount: discount.toNumber() };
|
|
4589
|
+
const g2 = { group_tax_total: money.tax.toNumber() };
|
|
4590
|
+
const g3 = { group_total: money.total.toNumber() };
|
|
4484
4591
|
return {
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4592
|
+
...line,
|
|
4593
|
+
...u1,
|
|
4594
|
+
...u2,
|
|
4595
|
+
...u3,
|
|
4596
|
+
...g1,
|
|
4597
|
+
...g2,
|
|
4598
|
+
...g3
|
|
4488
4599
|
};
|
|
4489
4600
|
};
|
|
4490
4601
|
/**
|
|
4491
4602
|
* Replays every redeem in persisted step order over the root lines' group
|
|
4492
|
-
*
|
|
4603
|
+
* money. Line redeems discount their root; order redeems are prorated over
|
|
4604
|
+
* every root. A discount never exceeds what is left on its target.
|
|
4493
4605
|
*/
|
|
4494
|
-
const
|
|
4495
|
-
const
|
|
4496
|
-
|
|
4497
|
-
const
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4606
|
+
const replayRedemptions = (lines, redeem, steps) => {
|
|
4607
|
+
const base = stripRedemptionLines(lines);
|
|
4608
|
+
const sequence = sequenceRedeems(base, redeem, steps);
|
|
4609
|
+
const moneyByUid = /* @__PURE__ */ new Map();
|
|
4610
|
+
for (const line of base) {
|
|
4611
|
+
if (line.parent_uid != null) continue;
|
|
4612
|
+
const b1 = { quantity: line.quantity };
|
|
4613
|
+
const b2 = { subtotal: new Big(line.group_subtotal) };
|
|
4614
|
+
const b3 = { tax: new Big(line.group_tax_total) };
|
|
4615
|
+
const b4 = {
|
|
4616
|
+
total: new Big(line.group_total),
|
|
4617
|
+
discount: new Big(0)
|
|
4618
|
+
};
|
|
4619
|
+
moneyByUid.set(line.uid, {
|
|
4620
|
+
...b1,
|
|
4621
|
+
...b2,
|
|
4622
|
+
...b3,
|
|
4623
|
+
...b4
|
|
4624
|
+
});
|
|
4625
|
+
}
|
|
4626
|
+
const roots = [...moneyByUid.values()];
|
|
4627
|
+
const redemption_steps = sequence.map((item) => {
|
|
4628
|
+
const id = item.target_id;
|
|
4629
|
+
const root = id ? moneyByUid.get(id) : void 0;
|
|
4630
|
+
const beforeTaxes = item.redeem.before_taxes ?? true;
|
|
4631
|
+
const before = snapshot(roots, 1);
|
|
4632
|
+
const targetBefore = root ? snapshot([root], root.quantity) : void 0;
|
|
4633
|
+
let left = new Big(0);
|
|
4634
|
+
if (root) left = baseOf(root, beforeTaxes);
|
|
4635
|
+
else for (const r of roots) left = left.plus(baseOf(r, beforeTaxes));
|
|
4636
|
+
const raw = redeemDiscount(item.redeem, left);
|
|
4637
|
+
const discount = minBig(raw, left);
|
|
4638
|
+
if (root) discountRoot(root, discount, beforeTaxes);
|
|
4639
|
+
else discountOrder(roots, discount, beforeTaxes);
|
|
4515
4640
|
const action = discount.gt(0) ? "UPDATE" : "READ";
|
|
4516
4641
|
const { reward_id, quantity } = item.redeem;
|
|
4517
4642
|
const b1 = {
|
|
@@ -4519,19 +4644,19 @@ const computeRedemptionSteps = (lines, redeem, steps) => {
|
|
|
4519
4644
|
reward_quantity: quantity,
|
|
4520
4645
|
target: item.target
|
|
4521
4646
|
};
|
|
4522
|
-
const b2 =
|
|
4647
|
+
const b2 = id ? { target_id: id } : {};
|
|
4523
4648
|
const b3 = {
|
|
4524
4649
|
action,
|
|
4525
4650
|
discount_amount: discount.toNumber()
|
|
4526
4651
|
};
|
|
4527
4652
|
const b4 = {
|
|
4528
4653
|
before,
|
|
4529
|
-
after: snapshot(
|
|
4654
|
+
after: snapshot(roots, 1)
|
|
4530
4655
|
};
|
|
4531
4656
|
let b5 = {};
|
|
4532
|
-
if (
|
|
4657
|
+
if (root) b5 = {
|
|
4533
4658
|
target_before: targetBefore,
|
|
4534
|
-
target_after: snapshot(
|
|
4659
|
+
target_after: snapshot([root], root.quantity)
|
|
4535
4660
|
};
|
|
4536
4661
|
return {
|
|
4537
4662
|
...b1,
|
|
@@ -4541,33 +4666,24 @@ const computeRedemptionSteps = (lines, redeem, steps) => {
|
|
|
4541
4666
|
...b5
|
|
4542
4667
|
};
|
|
4543
4668
|
});
|
|
4544
|
-
};
|
|
4545
|
-
/** Subtract redemption discounts from line-based order totals. */
|
|
4546
|
-
const applyRedemptionTotals = (totals, steps) => {
|
|
4547
|
-
if (steps.length === 0) return totals;
|
|
4548
|
-
let redeemed = new Big(0);
|
|
4549
|
-
for (const step of steps) redeemed = redeemed.plus(step.discount_amount);
|
|
4550
|
-
const discountTotal = new Big(totals.discount_total).plus(redeemed);
|
|
4551
|
-
const subtotal = new Big(totals.subtotal);
|
|
4552
|
-
const discount_rate = rateOf(discountTotal, subtotal);
|
|
4553
|
-
const discount_total = discountTotal.toNumber();
|
|
4554
|
-
const total = new Big(totals.total).minus(redeemed).toNumber();
|
|
4555
4669
|
return {
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4670
|
+
lines: base.map((line) => {
|
|
4671
|
+
const money = moneyByUid.get(line.uid);
|
|
4672
|
+
if (!money || money.discount.eq(0)) return line;
|
|
4673
|
+
return withRootMoney(line, money);
|
|
4674
|
+
}),
|
|
4675
|
+
redemption_steps
|
|
4560
4676
|
};
|
|
4561
4677
|
};
|
|
4562
|
-
/**
|
|
4678
|
+
/** Discounted lines, steps and totals, keeping step order. */
|
|
4563
4679
|
const recomputeRedemptions = (input) => {
|
|
4564
|
-
const {
|
|
4680
|
+
const { redeem, currency } = input;
|
|
4565
4681
|
const steps = input.redemption_steps;
|
|
4566
|
-
const redemption_steps =
|
|
4567
|
-
const lineTotals = recomputeTotals(lines, currency);
|
|
4682
|
+
const { lines, redemption_steps } = replayRedemptions(input.lines, redeem, steps);
|
|
4568
4683
|
return {
|
|
4684
|
+
lines,
|
|
4569
4685
|
redemption_steps,
|
|
4570
|
-
totals:
|
|
4686
|
+
totals: recomputeTotals(lines, currency)
|
|
4571
4687
|
};
|
|
4572
4688
|
};
|
|
4573
4689
|
//#endregion
|
|
@@ -4633,11 +4749,11 @@ const buildInsertFromSource = (options, source) => {
|
|
|
4633
4749
|
if (options.scope?.brand_id !== void 0) brand_id = optionalBrandId(options.scope.brand_id);
|
|
4634
4750
|
let customer = source.customer;
|
|
4635
4751
|
if (options.customer) customer = buildCustomerSnapshot(options.customer);
|
|
4636
|
-
const
|
|
4752
|
+
const sourceLines = structuredClone(source.lines);
|
|
4637
4753
|
const is_anonymous = options.isAnonymous ?? source.is_anonymous;
|
|
4638
4754
|
const redeem = structuredClone(source.redeem ?? []);
|
|
4639
|
-
const { redemption_steps, totals } = recomputeRedemptions({
|
|
4640
|
-
lines,
|
|
4755
|
+
const { lines, redemption_steps, totals } = recomputeRedemptions({
|
|
4756
|
+
lines: sourceLines,
|
|
4641
4757
|
redeem,
|
|
4642
4758
|
redemption_steps: source.redemption_steps ?? [],
|
|
4643
4759
|
currency: source.totals.currency
|
|
@@ -5213,10 +5329,10 @@ var Order = class Order {
|
|
|
5213
5329
|
}
|
|
5214
5330
|
/** Applies lines/redeem, replaying redemption steps and totals. */
|
|
5215
5331
|
commitLines(patch) {
|
|
5216
|
-
const
|
|
5332
|
+
const patched = patch.lines ?? this.draft.lines;
|
|
5217
5333
|
const redeem = patch.redeem ?? this.draft.redeem ?? [];
|
|
5218
|
-
const { redemption_steps, totals } = recomputeRedemptions({
|
|
5219
|
-
lines,
|
|
5334
|
+
const { lines, redemption_steps, totals } = recomputeRedemptions({
|
|
5335
|
+
lines: patched,
|
|
5220
5336
|
redeem,
|
|
5221
5337
|
redemption_steps: this.draft.redemption_steps ?? [],
|
|
5222
5338
|
currency: this.draft.totals.currency
|
|
@@ -5903,9 +6019,38 @@ const validateRedemptions = (input, issues, lineIssues) => {
|
|
|
5903
6019
|
for (const issue of expired) pushLineIssue(lineIssues, line.uid, issue);
|
|
5904
6020
|
}
|
|
5905
6021
|
checkRedemptionTargets(input.lines, redeem, steps, issues, lineIssues);
|
|
5906
|
-
const
|
|
5907
|
-
compareRedemptionSteps(steps,
|
|
5908
|
-
return
|
|
6022
|
+
const replayed = replayRedemptions(input.lines, redeem, steps);
|
|
6023
|
+
compareRedemptionSteps(steps, replayed.redemption_steps, issues);
|
|
6024
|
+
return replayed.lines;
|
|
6025
|
+
};
|
|
6026
|
+
const REDEEMED_GROUP_FIELDS = [
|
|
6027
|
+
["group_discount", "DISCOUNT_MISMATCH"],
|
|
6028
|
+
["unit_group_discount", "DISCOUNT_MISMATCH"],
|
|
6029
|
+
["group_tax_total", "TAX_MISMATCH"],
|
|
6030
|
+
["unit_group_tax_total", "TAX_MISMATCH"],
|
|
6031
|
+
["group_total", "PRICE_MISMATCH"],
|
|
6032
|
+
["unit_group_total", "PRICE_MISMATCH"]
|
|
6033
|
+
];
|
|
6034
|
+
/** Root group money must carry exactly the replayed redemption discounts. */
|
|
6035
|
+
const compareRedeemedRoots = (submitted, expected, out) => {
|
|
6036
|
+
const byUid = new Map(expected.map((line) => [line.uid, line]));
|
|
6037
|
+
for (const line of submitted) {
|
|
6038
|
+
if (line.parent_uid != null) continue;
|
|
6039
|
+
const wanted = byUid.get(line.uid);
|
|
6040
|
+
if (!wanted) continue;
|
|
6041
|
+
for (const [field, code] of REDEEMED_GROUP_FIELDS) {
|
|
6042
|
+
if (bigEq(line[field], wanted[field])) continue;
|
|
6043
|
+
const b1 = {
|
|
6044
|
+
code,
|
|
6045
|
+
field,
|
|
6046
|
+
expected: bigStr(wanted[field])
|
|
6047
|
+
};
|
|
6048
|
+
pushLineIssue(out, line.uid, {
|
|
6049
|
+
...b1,
|
|
6050
|
+
actual: bigStr(line[field])
|
|
6051
|
+
});
|
|
6052
|
+
}
|
|
6053
|
+
}
|
|
5909
6054
|
};
|
|
5910
6055
|
/** Compare cart lines/totals against a fresh menu (exact Big.js equality). */
|
|
5911
6056
|
const validateOrderAgainstMenu = (input) => {
|
|
@@ -5918,7 +6063,8 @@ const validateOrderAgainstMenu = (input) => {
|
|
|
5918
6063
|
lines: {}
|
|
5919
6064
|
};
|
|
5920
6065
|
if (!input.menuActive) issues.push({ code: "MENU_CLOSED" });
|
|
5921
|
-
const
|
|
6066
|
+
const menuLines = stripRedemptionLines(input.lines);
|
|
6067
|
+
const roots = menuLines.filter((line) => line.parent_uid == null);
|
|
5922
6068
|
if (roots.length === 0) {
|
|
5923
6069
|
issues.push({ code: "EMPTY_ORDER" });
|
|
5924
6070
|
return {
|
|
@@ -5933,12 +6079,13 @@ const validateOrderAgainstMenu = (input) => {
|
|
|
5933
6079
|
checkFeeSelfConsistency(root, lineIssues);
|
|
5934
6080
|
continue;
|
|
5935
6081
|
}
|
|
5936
|
-
validateProductRoot(root,
|
|
6082
|
+
validateProductRoot(root, menuLines, lookup, locale, lineIssues);
|
|
5937
6083
|
}
|
|
5938
6084
|
checkRootProductQuantities(roots, lookup, lineIssues);
|
|
5939
6085
|
if (input.expectedFeeLines) compareExpectedFeeLines(input.lines, input.expectedFeeLines, lineIssues);
|
|
5940
|
-
const
|
|
5941
|
-
|
|
6086
|
+
const redeemed = validateRedemptions(input, issues, lineIssues);
|
|
6087
|
+
compareRedeemedRoots(input.lines, redeemed, lineIssues);
|
|
6088
|
+
const expectedTotals = recomputeTotals(redeemed, input.totals.currency);
|
|
5942
6089
|
for (const field of TOTALS_FIELDS) {
|
|
5943
6090
|
if (bigEq(input.totals[field], expectedTotals[field])) continue;
|
|
5944
6091
|
issues.push({
|
|
@@ -6224,6 +6371,32 @@ const logError = (error, options = {}) => {
|
|
|
6224
6371
|
return enqueue(entry);
|
|
6225
6372
|
};
|
|
6226
6373
|
//#endregion
|
|
6374
|
+
//#region src/webhooks/verifyWebhookSignature.ts
|
|
6375
|
+
const SIGNATURE_PREFIX = "sha256=";
|
|
6376
|
+
const hmacHex = async (secret, body) => {
|
|
6377
|
+
const raw = new TextEncoder().encode(secret);
|
|
6378
|
+
const key = await crypto.subtle.importKey("raw", raw, {
|
|
6379
|
+
name: "HMAC",
|
|
6380
|
+
hash: "SHA-256"
|
|
6381
|
+
}, false, ["sign"]);
|
|
6382
|
+
const data = new TextEncoder().encode(body);
|
|
6383
|
+
const sig = new Uint8Array(await crypto.subtle.sign("HMAC", key, data));
|
|
6384
|
+
return Array.from(sig, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
6385
|
+
};
|
|
6386
|
+
const safeEqual = (left, right) => {
|
|
6387
|
+
if (left.length !== right.length) return false;
|
|
6388
|
+
let diff = 0;
|
|
6389
|
+
for (let i = 0; i < left.length; i += 1) diff |= left.charCodeAt(i) ^ right.charCodeAt(i);
|
|
6390
|
+
return diff === 0;
|
|
6391
|
+
};
|
|
6392
|
+
/** Checks a Fire Spark webhook/callback signature in constant time. */
|
|
6393
|
+
const verifyWebhookSignature = async (input) => {
|
|
6394
|
+
const { secret, body, signature } = input;
|
|
6395
|
+
if (!secret || !signature?.startsWith(SIGNATURE_PREFIX)) return false;
|
|
6396
|
+
const received = signature.slice(7).toLowerCase();
|
|
6397
|
+
return safeEqual(received, await hmacHex(secret, body));
|
|
6398
|
+
};
|
|
6399
|
+
//#endregion
|
|
6227
6400
|
//#region src/rules/assert.ts
|
|
6228
6401
|
const assert = (value, message) => {
|
|
6229
6402
|
if (value) return;
|
|
@@ -7831,4 +8004,4 @@ const cardPreviewGroups = (value) => {
|
|
|
7831
8004
|
return groups;
|
|
7832
8005
|
};
|
|
7833
8006
|
//#endregion
|
|
7834
|
-
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, isDeviceNotLinked, isExpiryComplete, isKioskUnauthorized, isMilestoneStepDone, isOnSchedule, isOrderValidationError, isPosDeviceNotLinked, isPosUnauthorized, isStorefrontNotFound, isVariantBase, listMenusStream, localizePosError, log, logError, luhnCheck, matchAbsoluteDateOperator, matchBooleanOperator, matchCurrencyOperator, matchEnumOperator, matchNumberOperator, matchRecurringDateOperator, matchRelativeDateOperator, matchStringOperator, milestoneSteps, milestoneTrack, nestMenuCategories, normalizeCardholderName, parseExpiry, paymentMatchesClientConditions, recomputeRedemptions, recomputeTotals, remainingAmount, removeLine, replaceLine, resolveCheckout, reversePayment, runPaymentAttempt, serializeError, services, setLineQuantity, storefrontJson, toOrderLines, toPaymentCardBrand, validateCardNumber, validateCardholderName, validateCvc, validateExpiry, validateOrderAgainstMenu, visitedStates };
|
|
8007
|
+
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, isDeviceNotLinked, isExpiryComplete, isKioskUnauthorized, isMilestoneStepDone, isOnSchedule, isOrderValidationError, isPosDeviceNotLinked, isPosUnauthorized, isStorefrontNotFound, isVariantBase, listMenusStream, localizePosError, log, logError, luhnCheck, matchAbsoluteDateOperator, matchBooleanOperator, matchCurrencyOperator, matchEnumOperator, matchNumberOperator, matchRecurringDateOperator, matchRelativeDateOperator, matchStringOperator, milestoneSteps, milestoneTrack, nestMenuCategories, normalizeCardholderName, parseExpiry, paymentMatchesClientConditions, recomputeRedemptions, recomputeTotals, remainingAmount, removeLine, replaceLine, resolveCheckout, reversePayment, runPaymentAttempt, serializeError, services, setLineQuantity, storefrontJson, stripRedemptionLines, toOrderLines, toPaymentCardBrand, validateCardNumber, validateCardholderName, validateCvc, validateExpiry, validateOrderAgainstMenu, verifyWebhookSignature, visitedStates };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fire-spark/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Fire Spark headless SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"big.js": "6.2.1",
|
|
21
21
|
"dayjs": "1.11.9",
|
|
22
22
|
"@types/big.js": "6.2.2",
|
|
23
|
-
"@fire-spark/payments": "^0.
|
|
24
|
-
"@fire-spark/types": "^0.
|
|
23
|
+
"@fire-spark/payments": "^0.2.0",
|
|
24
|
+
"@fire-spark/types": "^0.2.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"tsdown": "^0.22.14",
|