@fire-spark/core 0.1.2 → 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 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
- /** Root line to discount; omit to discount the whole order. */
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
- /** Steps + totals for a set of lines and redeems, keeping step order. */
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
@@ -1101,4 +1112,4 @@ declare const validateCardholderName: (value: string) => CardholderNameError | n
1101
1112
  */
1102
1113
  declare const cardPreviewGroups: (value: string) => CardPreviewSlot[][];
1103
1114
  //#endregion
1104
- 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, toOrderLines, toPaymentCardBrand, validateCardNumber, validateCardholderName, validateCvc, validateExpiry, validateOrderAgainstMenu, verifyWebhookSignature, 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 = (run) => {
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: left(run).toNumber(),
4460
- quantity: run.quantity
4465
+ value: total.toNumber(),
4466
+ quantity
4461
4467
  };
4462
- const b2 = { discount_amount: run.discount.toNumber() };
4463
- const b3 = { discount_rate: rateOf(run.discount, run.base) };
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
- const runningFor = (base, quantity) => {
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
- base: new Big(base),
4486
- discount: new Big(0),
4487
- quantity
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
- * totals. A discount never exceeds what is left on its target or the order.
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 computeRedemptionSteps = (lines, redeem, steps) => {
4495
- const sequence = sequenceRedeems(lines, redeem, steps);
4496
- if (sequence.length === 0) return [];
4497
- const roots = lines.filter((line) => line.parent_uid == null);
4498
- let orderBase = new Big(0);
4499
- const lineRuns = /* @__PURE__ */ new Map();
4500
- for (const root of roots) {
4501
- orderBase = orderBase.plus(root.group_total);
4502
- lineRuns.set(root.uid, runningFor(root.group_total, root.quantity));
4503
- }
4504
- const order = runningFor(orderBase, 1);
4505
- return sequence.map((item) => {
4506
- const line = item.target_id ? lineRuns.get(item.target_id) : void 0;
4507
- const before = snapshot(order);
4508
- const targetBefore = line ? snapshot(line) : void 0;
4509
- const orderLeft = left(order);
4510
- const targetLeft = line ? left(line) : orderLeft;
4511
- const raw = redeemDiscount(item.redeem, targetLeft);
4512
- const discount = minBig(minBig(raw, targetLeft), orderLeft);
4513
- order.discount = order.discount.plus(discount);
4514
- if (line) line.discount = line.discount.plus(discount);
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 = item.target_id ? { target_id: item.target_id } : {};
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(order)
4654
+ after: snapshot(roots, 1)
4530
4655
  };
4531
4656
  let b5 = {};
4532
- if (line) b5 = {
4657
+ if (root) b5 = {
4533
4658
  target_before: targetBefore,
4534
- target_after: snapshot(line)
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
- ...totals,
4557
- discount_rate,
4558
- discount_total,
4559
- total
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
- /** Steps + totals for a set of lines and redeems, keeping step order. */
4678
+ /** Discounted lines, steps and totals, keeping step order. */
4563
4679
  const recomputeRedemptions = (input) => {
4564
- const { lines, redeem, currency } = input;
4680
+ const { redeem, currency } = input;
4565
4681
  const steps = input.redemption_steps;
4566
- const redemption_steps = computeRedemptionSteps(lines, redeem, 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: applyRedemptionTotals(lineTotals, redemption_steps)
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 lines = structuredClone(source.lines);
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 lines = patch.lines ?? this.draft.lines;
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 expected = computeRedemptionSteps(input.lines, redeem, steps);
5907
- compareRedemptionSteps(steps, expected, issues);
5908
- return expected;
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 roots = input.lines.filter((line) => line.parent_uid == null);
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, input.lines, lookup, locale, lineIssues);
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 expectedSteps = validateRedemptions(input, issues, lineIssues);
5941
- const expectedTotals = applyRedemptionTotals(recomputeTotals(input.lines, input.totals.currency), expectedSteps);
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({
@@ -7857,4 +8004,4 @@ const cardPreviewGroups = (value) => {
7857
8004
  return groups;
7858
8005
  };
7859
8006
  //#endregion
7860
- 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, verifyWebhookSignature, 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.1.2",
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.1.2",
24
- "@fire-spark/types": "^0.1.2"
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",