@fire-spark/core 0.11.1 → 0.12.1

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
@@ -1,4 +1,4 @@
1
- import { Money, PaymentAdapter, PaymentCollectAt, PaymentContext, PaymentResult, PaymentStorage, PaymentUI, RedeemLike, RedeemSource, isRealPaymentMethod, orderRedeems } from "@fire-spark/payments";
1
+ import { Money, PaymentAdapter, PaymentCollectAt, PaymentContext, PaymentResult, PaymentReversal, PaymentStorage, PaymentUI, RedeemLike, RedeemSource, isRealPaymentMethod, orderRedeems } from "@fire-spark/payments";
2
2
  import { AndConditions, CheckoutMode, CheckoutResolved, CmsBlock, Condition, Customer, CustomerIdentifyInput, CustomerRegisterInput, CustomerUpdateInput, Fulfillment, FulfillmentListInput, GroupDisplayType, KioskDeviceConfig, KioskDeviceOverrides, KioskHealthBatchInput, KioskHealthBatchResult, KioskHeartbeatInput, KioskHeartbeatResult, KioskRealtimeToken, MatchResult, MatchableRule, Menu, Menu as Menu$1, MenuLookup, MenuLookup as MenuLookup$1, MerchantPayment, ModifierControlType, ModifierGroup, OrConditions, Order as Order$1, Order as Order$2, OrderCancelInput, OrderCancelInput as OrderCancelInput$1, OrderCancelType, OrderCancelType as OrderCancelType$1, OrderDeviceListInput, OrderDeviceListResult, OrderFulfillment, OrderInsertInput, OrderLine, OrderListInput, OrderRecordPaymentInput, OrderRedeem, OrderRedemptionStep, OrderReorderInput, OrderTotals, OrderUpdateInput, OrderValidateInput, OrderValidateOutput, PaymentCollectAt as PaymentCollectAt$1, PaymentIntentPutInput, PaymentMethod, PaymentProvider, PaymentTender, PaymentTenderInput, PlaceOrderResult, PosCatalog, PosFloorInput, PosFloorOccupancy, PosFloorPlan, PosFloorRealtimeToken, PosHealthBatchInput, PosHealthBatchResult, PosHeartbeatInput, PosHeartbeatResult, PosKioskOrder, PosKioskSettleInput, PosKioskSettleResult, PosOrderInput, PosOrderResult, PosTableAttachOrderInput, PosTableCloseInput, PosTableJoinInput, PosTableSeatInput, PosTableSeatReservationInput, PosTableSplitInput, PricingModifier, Product, Product as Product$1, Recommendation, RecurringDateCronResult, RecurringDateCronValues, RulesEngineOrderLine, RulesEngineParams, RulesEngineRuntime, Schedule, SdkLogContext, SdkLogErrorInfo, SdkLogLevel, SdkLogOptions, Store, StoreGetInput, StoreListInput, StoreNearbyInput, StorefrontAvailability, StorefrontMenu, StorefrontMenuScope, StorefrontMenuStreamOptions, StorefrontPaymentsListInput, StorefrontPreferences, TenderRefundInput, TenderReplaceInput, TenderVoidInput, Variant, VariantGroup } from "@fire-spark/types";
3
3
  import { Big } from "big.js";
4
4
  //#region src/productCustomization/catalog.utils.d.ts
@@ -167,6 +167,11 @@ type AuthConfigureBase = {
167
167
  /** Default: exchange an IdP id_token (Firebase, …) for a storefront token. */
168
168
  type AuthConfigureSubjectOptions = AuthConfigureBase & {
169
169
  getSubjectToken: () => Promise<string>;
170
+ /**
171
+ * OAuth app alias (RFC 8693 `audience`). Required when several merchants
172
+ * share the identity provider (same issuer and audience).
173
+ */
174
+ audience?: string;
170
175
  getAccessToken?: never;
171
176
  };
172
177
  /**
@@ -176,11 +181,14 @@ type AuthConfigureSubjectOptions = AuthConfigureBase & {
176
181
  type AuthConfigureAccessTokenOptions = AuthConfigureBase & {
177
182
  getAccessToken: () => Promise<StorefrontAccessToken>;
178
183
  getSubjectToken?: never;
184
+ audience?: never;
179
185
  };
180
186
  type AuthConfigureOptions = AuthConfigureSubjectOptions | AuthConfigureAccessTokenOptions;
181
187
  type ExchangeOptions = {
182
188
  baseUrl?: string;
183
189
  fetchImpl?: typeof fetch;
190
+ /** OAuth app alias (RFC 8693 `audience`) when the provider is shared. */
191
+ audience?: string;
184
192
  };
185
193
  //#endregion
186
194
  //#region src/auth/auth.d.ts
@@ -313,7 +321,7 @@ type PaymentRunOptions = {
313
321
  */
314
322
  declare const runPaymentAttempt: (adapter: PaymentAdapter, ctx: PaymentContext, options?: PaymentRunOptions) => Promise<PaymentResult>;
315
323
  type ReversalOutcome = {
316
- kind: "cancel" | "refund" | "manual";
324
+ kind: PaymentReversal;
317
325
  result: PaymentResult;
318
326
  };
319
327
  /** Undo a charge after a failed injection or an incomplete split payment. */
@@ -422,7 +430,7 @@ type OrderCancelResult = {
422
430
  path: "cancel" | "refund";
423
431
  /** What the device did with each charge it voided. */
424
432
  outcomes: ReversalOutcome[];
425
- /** Tender ids the device could not void yet (`reversal_pending`). */
433
+ /** Tender ids neither the device nor the server reversed yet. */
426
434
  pending: string[];
427
435
  };
428
436
  //#endregion
@@ -1129,6 +1137,8 @@ type DeunaOrderStatus = {
1129
1137
  order_token: string;
1130
1138
  status: string;
1131
1139
  transaction_id: string | null;
1140
+ /** Processor DEUNA charged with, when it reports one. */
1141
+ processor?: string | null;
1132
1142
  error: {
1133
1143
  code: string | null;
1134
1144
  message: string | null;
package/dist/index.js CHANGED
@@ -1649,11 +1649,11 @@ const randomUUID = () => {
1649
1649
  };
1650
1650
  //#endregion
1651
1651
  //#region src/productCustomization/toOrderLines.ts
1652
- const displayName = (text, locale) => {
1652
+ const displayName$1 = (text, locale) => {
1653
1653
  if (text[locale]) return text[locale];
1654
1654
  return Object.values(text)[0] ?? "";
1655
1655
  };
1656
- const applyUnitMoney = (config) => {
1656
+ const applyUnitMoney$1 = (config) => {
1657
1657
  const qty = new Big(config.quantity);
1658
1658
  const unitDiscount = config.unitSubtotal.times(config.discountRate);
1659
1659
  const net = config.unitSubtotal.minus(unitDiscount);
@@ -1732,7 +1732,7 @@ const buildLine = (args) => {
1732
1732
  ...b2,
1733
1733
  ...b3
1734
1734
  };
1735
- const money = applyUnitMoney(moneyArgs);
1735
+ const money = applyUnitMoney$1(moneyArgs);
1736
1736
  const imageUrl = args.product.image_url ?? void 0;
1737
1737
  const imagePatch = imageUrl ? { image_url: imageUrl } : {};
1738
1738
  const catalog = lineCatalogFields(args.product);
@@ -1820,7 +1820,7 @@ const appendModifierLine = (modifier, groupId, parent, locale) => {
1820
1820
  const unitSubtotal = new Big(modifier.linePrice).div(modifier.quantity);
1821
1821
  const l1 = {
1822
1822
  product,
1823
- name: displayName(product.name, locale),
1823
+ name: displayName$1(product.name, locale),
1824
1824
  quantity: modifier.quantity,
1825
1825
  unitSubtotal
1826
1826
  };
@@ -1844,7 +1844,7 @@ const appendModifierLine = (modifier, groupId, parent, locale) => {
1844
1844
  const toOrderLines = (root, locale) => {
1845
1845
  if (root.quantity <= 0) return [];
1846
1846
  if (isVariantBase(root.data)) return [];
1847
- const name = displayName(root.data.name, locale);
1847
+ const name = displayName$1(root.data.name, locale);
1848
1848
  const unitSubtotal = new Big(root.data.pricing.price);
1849
1849
  const t1 = {
1850
1850
  product: root.data,
@@ -1888,7 +1888,8 @@ const exchangeSubjectToken = async (subjectToken, options = {}) => {
1888
1888
  body: JSON.stringify({
1889
1889
  grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
1890
1890
  subject_token: subjectToken,
1891
- subject_token_type: "urn:ietf:params:oauth:token-type:id_token"
1891
+ subject_token_type: "urn:ietf:params:oauth:token-type:id_token",
1892
+ ...options.audience ? { audience: options.audience } : {}
1892
1893
  })
1893
1894
  });
1894
1895
  if (!response.ok) {
@@ -1907,6 +1908,7 @@ const configure = (options) => {
1907
1908
  state = {
1908
1909
  getSubjectToken: options.getSubjectToken ?? null,
1909
1910
  getDirectToken: options.getAccessToken ?? null,
1911
+ audience: options.audience,
1910
1912
  baseUrl: resolveBaseUrl(options.baseUrl),
1911
1913
  fetchImpl: options.fetchImpl ?? fetch,
1912
1914
  tokenSkewMs: options.tokenSkewMs ?? DEFAULT_TOKEN_SKEW_MS,
@@ -1928,7 +1930,8 @@ const issueToken = async (current) => {
1928
1930
  if (!current.getSubjectToken) throw new Error("auth.configure() needs getSubjectToken or getAccessToken");
1929
1931
  return exchangeSubjectToken(await current.getSubjectToken(), {
1930
1932
  baseUrl: current.baseUrl,
1931
- fetchImpl: current.fetchImpl
1933
+ fetchImpl: current.fetchImpl,
1934
+ audience: current.audience
1932
1935
  });
1933
1936
  };
1934
1937
  const getAccessToken = async () => {
@@ -3390,6 +3393,14 @@ const REVERSED_RESULTS = ["REFUNDED", "CANCELLED"];
3390
3393
  const isDeviceReversed = (reversal) => {
3391
3394
  return REVERSED_RESULTS.includes(reversal.outcome.result.status);
3392
3395
  };
3396
+ /**
3397
+ * The server ended the tender (`CANCELLED` / `REFUNDED`) in its answer:
3398
+ * a charge it reversed itself (`server` kind) is not pending.
3399
+ */
3400
+ const isTenderSettled$1 = (order, tenderId) => {
3401
+ const tender = liveTenders(currentIntent(order.payment_intents)).find((item) => item.id === tenderId);
3402
+ return Boolean(tender && REVERSED_RESULTS.includes(tender.status));
3403
+ };
3393
3404
  /** Body `tenders` entry for one device reversal. */
3394
3405
  const toReportedTender = (reversal) => {
3395
3406
  const { tender, outcome } = reversal;
@@ -4232,6 +4243,10 @@ const reversePayment = async (adapter, ctx, result, options = {}) => {
4232
4243
  const clock = options.clock ?? defaultClock;
4233
4244
  const now = options.now ?? new Date(clock.now());
4234
4245
  let kind = (adapter.reversalFor ?? defaultReversalFor)(result, now);
4246
+ if (kind === "server") return {
4247
+ kind,
4248
+ result
4249
+ };
4235
4250
  const operation = kind === "cancel" ? adapter.cancel : adapter.refund;
4236
4251
  if (kind !== "manual" && !operation) kind = "manual";
4237
4252
  if (kind === "manual" || !operation) {
@@ -6306,7 +6321,10 @@ var Order = class Order {
6306
6321
  this.deleted = true;
6307
6322
  this.notify();
6308
6323
  const outcomes = reversals.map((reversal) => reversal.outcome);
6309
- const pending = reversals.filter((reversal) => !isDeviceReversed(reversal)).map((reversal) => reversal.tender.id);
6324
+ const pending = reversals.filter((reversal) => {
6325
+ if (isDeviceReversed(reversal)) return false;
6326
+ return !isTenderSettled$1(next, reversal.tender.id);
6327
+ }).map((reversal) => reversal.tender.id);
6310
6328
  return {
6311
6329
  order: this.getOrder(),
6312
6330
  type,
@@ -6603,6 +6621,608 @@ const milestoneSteps = (workflow, currentNodeIds) => {
6603
6621
  };
6604
6622
  };
6605
6623
  //#endregion
6624
+ //#region src/order/placeCartLines.ts
6625
+ const selectionKey = (line, kidsOf, cache) => {
6626
+ const cached = cache.get(line.uid);
6627
+ if (cached) return cached;
6628
+ const nested = (kidsOf.get(line.uid) ?? []).map((child) => {
6629
+ return selectionKey(child, kidsOf, cache);
6630
+ });
6631
+ nested.sort((left, right) => left.localeCompare(right));
6632
+ const key = `${line.id}x${line.quantity}(${nested.join(",")})`;
6633
+ cache.set(line.uid, key);
6634
+ return key;
6635
+ };
6636
+ const parentGroups = (lookup, product) => {
6637
+ return product.pricing.modifier_group_ids.flatMap((groupId) => {
6638
+ const group = lookup.modifierGroupsById[groupId];
6639
+ return group ? [group] : [];
6640
+ });
6641
+ };
6642
+ const buildSlots = (lookup, groups) => {
6643
+ const slots = [];
6644
+ for (const group of groups) {
6645
+ const seen = /* @__PURE__ */ new Set();
6646
+ for (const modifierId of group.modifier_ids) {
6647
+ if (seen.has(modifierId)) continue;
6648
+ seen.add(modifierId);
6649
+ const product = lookup.productsById[modifierId];
6650
+ const entry = product ? resolvePricingModifier(product, group.id) : null;
6651
+ const modifierMax = entry?.maximum_quantity ?? group.maximum_quantity;
6652
+ const row = {
6653
+ group,
6654
+ modifierId,
6655
+ modifierMin: entry?.minimum_quantity ?? 0
6656
+ };
6657
+ const counts = {
6658
+ modifierMax,
6659
+ instances: 0,
6660
+ next: 2
6661
+ };
6662
+ slots.push({
6663
+ ...row,
6664
+ ...counts
6665
+ });
6666
+ }
6667
+ }
6668
+ return slots;
6669
+ };
6670
+ const listingsFor = (lookup, groups, childId) => {
6671
+ const direct = [];
6672
+ for (const group of groups) {
6673
+ if (!group.modifier_ids.includes(childId)) continue;
6674
+ direct.push({
6675
+ group,
6676
+ modifierId: childId
6677
+ });
6678
+ }
6679
+ if (direct.length > 0) return direct;
6680
+ const child = lookup.productsById[childId];
6681
+ const viaBase = [];
6682
+ for (const entry of child?.pricing.variants ?? []) {
6683
+ const base = lookup.productsById[entry.product_id];
6684
+ if (!child || !base) continue;
6685
+ if (!selectionForVariant(lookup, base, child)) continue;
6686
+ for (const group of groups) {
6687
+ if (!group.modifier_ids.includes(base.id)) continue;
6688
+ viaBase.push({
6689
+ group,
6690
+ modifierId: base.id
6691
+ });
6692
+ }
6693
+ }
6694
+ return viaBase;
6695
+ };
6696
+ const slotKey = (groupId, modifierId) => {
6697
+ return `${groupId}/${modifierId}`;
6698
+ };
6699
+ /** Same product id more than once under one parent, at any depth. */
6700
+ const hasRepeatedProductSibling = (lines, rootUid) => {
6701
+ const kidsOf = /* @__PURE__ */ new Map();
6702
+ for (const line of lines) {
6703
+ if (line.parent_uid == null) continue;
6704
+ const list = kidsOf.get(line.parent_uid) ?? [];
6705
+ list.push(line);
6706
+ kidsOf.set(line.parent_uid, list);
6707
+ }
6708
+ const walk = (uid) => {
6709
+ const seen = /* @__PURE__ */ new Set();
6710
+ for (const child of kidsOf.get(uid) ?? []) {
6711
+ if (seen.has(child.id)) return true;
6712
+ seen.add(child.id);
6713
+ if (walk(child.uid)) return true;
6714
+ }
6715
+ return false;
6716
+ };
6717
+ return walk(rootUid);
6718
+ };
6719
+ const noteRules = (state, broken) => {
6720
+ if (broken) state.rulesBroken = true;
6721
+ };
6722
+ /** Search steps per parent. Far above any real menu. A cart that needs more
6723
+ * is rejected: a partial search never accepts anything. */
6724
+ const SEARCH_BUDGET = 4096;
6725
+ const OWN_MONEY = [
6726
+ "unit_subtotal",
6727
+ "subtotal",
6728
+ "unit_discount",
6729
+ "discount_total",
6730
+ "unit_tax_total",
6731
+ "tax_total",
6732
+ "unit_total",
6733
+ "total",
6734
+ "discount_rate"
6735
+ ];
6736
+ const emptyUsage = () => {
6737
+ return {
6738
+ bySlot: /* @__PURE__ */ new Map(),
6739
+ byGroup: /* @__PURE__ */ new Map()
6740
+ };
6741
+ };
6742
+ const addUsage = (usage, slot, quantity) => {
6743
+ const own = usage.bySlot.get(slot) ?? 0;
6744
+ usage.bySlot.set(slot, own + quantity);
6745
+ const group = usage.byGroup.get(slot.group.id) ?? 0;
6746
+ usage.byGroup.set(slot.group.id, group + quantity);
6747
+ };
6748
+ const hasRoom = (usage, slot, quantity) => {
6749
+ const own = usage.bySlot.get(slot) ?? 0;
6750
+ if ((usage.byGroup.get(slot.group.id) ?? 0) + quantity > slot.group.maximum_quantity) return false;
6751
+ return own + quantity <= slot.modifierMax;
6752
+ };
6753
+ /** Every instance counts: usage is the sum over all sibling lines. A slot's
6754
+ * own minimum binds once the slot is used, as in the customization session.
6755
+ * An unused slot owes nothing; the group minimum is what requires a pick. */
6756
+ const limitsBroken = (groups, slots, usage) => {
6757
+ for (const group of groups) {
6758
+ const used = usage.byGroup.get(group.id) ?? 0;
6759
+ if (used > group.maximum_quantity) return true;
6760
+ if (used < group.minimum_quantity) return true;
6761
+ }
6762
+ for (const slot of slots) {
6763
+ const used = usage.bySlot.get(slot) ?? 0;
6764
+ if (used > slot.modifierMax) return true;
6765
+ if (used > 0 && used < slot.modifierMin) return true;
6766
+ }
6767
+ return false;
6768
+ };
6769
+ const claimKey = (line) => {
6770
+ const subtotal = new Big(line.unit_subtotal).toString();
6771
+ if (line.tax_rate == null) return `${subtotal}|null`;
6772
+ return `${subtotal}|${new Big(line.tax_rate).toString()}`;
6773
+ };
6774
+ const sameOwnMoney = (line, money) => {
6775
+ for (const field of OWN_MONEY) if (!new Big(line[field]).eq(money[field])) return false;
6776
+ if (line.tax_rate == null) return false;
6777
+ return new Big(line.tax_rate).eq(money.tax_rate);
6778
+ };
6779
+ /** True when the line's own money is exactly what the menu yields for this
6780
+ * slot: the slot's price and tax, with 0 to `quantity` charged units
6781
+ * (charge_above shares free units between siblings). It only narrows which
6782
+ * slots a line may take. Nothing from the cart is ever copied into the
6783
+ * expected tree: the final comparison is still exact. */
6784
+ const slotYieldsLine = (lookup, line, ordered, slot) => {
6785
+ const quantity = line.quantity;
6786
+ if (quantity <= 0) return false;
6787
+ if (quantity > slot.modifierMax) return false;
6788
+ if (quantity > slot.group.maximum_quantity) return false;
6789
+ const listed = lookup.productsById[slot.modifierId] ?? ordered;
6790
+ const pricing = modifierUnitPricing(listed, ordered, slot.group.id);
6791
+ const free = (resolvePricingModifier(listed, slot.group.id)?.charge_above_quantity ?? 0) + (slot.group.charge_above_quantity ?? 0);
6792
+ const unit = new Big(pricing.price);
6793
+ const config = {
6794
+ quantity,
6795
+ taxRate: pricing.taxRate
6796
+ };
6797
+ const inclusive = { taxInclusive: pricing.isTaxInclusive };
6798
+ const fewest = Math.max(0, quantity - free);
6799
+ for (let charged = fewest; charged <= quantity; charged++) {
6800
+ const unitSubtotal = unit.times(charged).div(quantity);
6801
+ const money = applyUnitMoney({
6802
+ ...config,
6803
+ ...inclusive,
6804
+ unitSubtotal
6805
+ });
6806
+ if (sameOwnMoney(line, money)) return true;
6807
+ }
6808
+ return false;
6809
+ };
6810
+ /** Slots the line may take: those whose menu price it matches, else every
6811
+ * listed slot (the comparison then reports the price it cannot match). */
6812
+ const optionsFor = (lookup, line, ordered, listed) => {
6813
+ if (listed.length < 2) return listed;
6814
+ const yielding = listed.filter((slot) => {
6815
+ return slotYieldsLine(lookup, line, ordered, slot);
6816
+ });
6817
+ if (yielding.length > 0) return yielding;
6818
+ return listed;
6819
+ };
6820
+ /** Depth-first assignment of sibling lines to slots, in a fixed order, within
6821
+ * capacity, until every group and modifier limit holds. Twins (same shape and
6822
+ * same claimed price) are interchangeable, so they are forced into
6823
+ * non-decreasing option order. At most SEARCH_BUDGET steps of O(slots). */
6824
+ const searchAssignment = (plans, groups, slots) => {
6825
+ const total = plans.length;
6826
+ const usage = emptyUsage();
6827
+ const chosen = [];
6828
+ const picked = [];
6829
+ const cursor = [0];
6830
+ let depth = 0;
6831
+ let steps = 0;
6832
+ while (depth >= 0) {
6833
+ steps += 1;
6834
+ if (steps > SEARCH_BUDGET) return null;
6835
+ if (depth === total) {
6836
+ if (!limitsBroken(groups, slots, usage)) return chosen;
6837
+ depth -= 1;
6838
+ addUsage(usage, chosen[depth], -plans[depth].line.quantity);
6839
+ continue;
6840
+ }
6841
+ const plan = plans[depth];
6842
+ const quantity = plan.line.quantity;
6843
+ let found = -1;
6844
+ for (let next = cursor[depth]; next < plan.options.length; next++) {
6845
+ if (!hasRoom(usage, plan.options[next], quantity)) continue;
6846
+ found = next;
6847
+ break;
6848
+ }
6849
+ if (found < 0) {
6850
+ depth -= 1;
6851
+ if (depth < 0) break;
6852
+ addUsage(usage, chosen[depth], -plans[depth].line.quantity);
6853
+ continue;
6854
+ }
6855
+ cursor[depth] = found + 1;
6856
+ picked[depth] = found;
6857
+ chosen[depth] = plan.options[found];
6858
+ addUsage(usage, chosen[depth], quantity);
6859
+ depth += 1;
6860
+ cursor[depth] = plans[depth]?.twin === true ? picked[depth - 1] : 0;
6861
+ }
6862
+ return null;
6863
+ };
6864
+ /** Used when no assignment satisfies every limit: first slot with room, so the
6865
+ * repaired tree and the broken-rules flag still come out deterministic. */
6866
+ const greedyAssignment = (plans) => {
6867
+ const usage = emptyUsage();
6868
+ const chosen = [];
6869
+ let overCapacity = false;
6870
+ for (const plan of plans) {
6871
+ const quantity = plan.line.quantity;
6872
+ const fits = (slot) => hasRoom(usage, slot, quantity);
6873
+ let slot = plan.options.find(fits) ?? plan.listed.find(fits);
6874
+ if (!slot) {
6875
+ slot = plan.listed[0];
6876
+ overCapacity = true;
6877
+ }
6878
+ addUsage(usage, slot, quantity);
6879
+ chosen.push(slot);
6880
+ }
6881
+ return {
6882
+ chosen,
6883
+ overCapacity
6884
+ };
6885
+ };
6886
+ const planKids = (lookup, groups, byKey, kids, prefix, state) => {
6887
+ const plans = [];
6888
+ let previous = "";
6889
+ for (const child of kids) {
6890
+ if (pathVisits(prefix, child.id)) continue;
6891
+ const ordered = lookup.productsById[child.id];
6892
+ const listings = listingsFor(lookup, groups, child.id);
6893
+ if (!ordered || listings.length === 0) continue;
6894
+ const listed = [];
6895
+ for (const item of listings) {
6896
+ const slot = byKey.get(slotKey(item.group.id, item.modifierId));
6897
+ if (slot) listed.push(slot);
6898
+ }
6899
+ if (listed.length === 0) continue;
6900
+ const signature = `${selectionKey(child, state.childrenByParent, state.keyCache)}|${claimKey(child)}`;
6901
+ const options = optionsFor(lookup, child, ordered, listed);
6902
+ const twin = signature === previous;
6903
+ plans.push({
6904
+ line: child,
6905
+ ordered,
6906
+ listed,
6907
+ options,
6908
+ twin
6909
+ });
6910
+ previous = signature;
6911
+ }
6912
+ return plans;
6913
+ };
6914
+ const placeChildren = (lookup, parent, parentLine, prefix, state) => {
6915
+ const groups = parentGroups(lookup, parent);
6916
+ const slots = buildSlots(lookup, groups);
6917
+ const byKey = /* @__PURE__ */ new Map();
6918
+ for (const slot of slots) byKey.set(slotKey(slot.group.id, slot.modifierId), slot);
6919
+ const kids = (state.childrenByParent.get(parentLine.uid) ?? []).slice();
6920
+ kids.sort((left, right) => {
6921
+ const a = selectionKey(left, state.childrenByParent, state.keyCache);
6922
+ const b = selectionKey(right, state.childrenByParent, state.keyCache);
6923
+ const byShape = a.localeCompare(b);
6924
+ if (byShape !== 0) return byShape;
6925
+ return claimKey(left).localeCompare(claimKey(right));
6926
+ });
6927
+ const plans = planKids(lookup, groups, byKey, kids, prefix, state);
6928
+ let assignment = [];
6929
+ if (plans.length > 0) assignment = searchAssignment(plans, groups, slots);
6930
+ if (!assignment) {
6931
+ const fallback = greedyAssignment(plans);
6932
+ assignment = fallback.chosen;
6933
+ if (fallback.overCapacity) state.overCapacity = true;
6934
+ }
6935
+ const usage = emptyUsage();
6936
+ const placed = [];
6937
+ for (let index = 0; index < plans.length; index++) {
6938
+ const plan = plans[index];
6939
+ const chosen = assignment[index];
6940
+ const child = plan.line;
6941
+ const ordered = plan.ordered;
6942
+ const base = `${`${prefix}/group/${chosen.group.id}`}/modifier/${chosen.modifierId}`;
6943
+ let path = base;
6944
+ if (chosen.instances > 0) {
6945
+ path = `${base}/instance/${chosen.next}`;
6946
+ chosen.next += 1;
6947
+ }
6948
+ chosen.instances += 1;
6949
+ addUsage(usage, chosen, child.quantity);
6950
+ state.paths[path] = child.quantity;
6951
+ const listed = lookup.productsById[chosen.modifierId] ?? ordered;
6952
+ const pricing = modifierUnitPricing(listed, ordered, chosen.group.id);
6953
+ const n1 = {
6954
+ product: ordered,
6955
+ quantity: child.quantity,
6956
+ path
6957
+ };
6958
+ const n2 = {
6959
+ children: [],
6960
+ listed,
6961
+ group: chosen.group
6962
+ };
6963
+ const n3 = {
6964
+ unitPrice: new Big(0),
6965
+ taxRate: pricing.taxRate
6966
+ };
6967
+ const n4 = { taxInclusive: pricing.isTaxInclusive };
6968
+ const node = {
6969
+ ...n1,
6970
+ ...n2,
6971
+ ...n3,
6972
+ ...n4
6973
+ };
6974
+ node.children = placeChildren(lookup, ordered, child, path, state);
6975
+ placed.push(node);
6976
+ }
6977
+ noteRules(state, limitsBroken(groups, slots, usage));
6978
+ billGroups(placed);
6979
+ return placed;
6980
+ };
6981
+ const billGroups = (nodes) => {
6982
+ const byGroup = /* @__PURE__ */ new Map();
6983
+ for (const node of nodes) {
6984
+ if (!node.group) continue;
6985
+ const list = byGroup.get(node.group.id) ?? [];
6986
+ list.push(node);
6987
+ byGroup.set(node.group.id, list);
6988
+ }
6989
+ for (const list of byGroup.values()) {
6990
+ const group = list[0]?.group;
6991
+ if (group) billGroup(group, list);
6992
+ }
6993
+ };
6994
+ /** Menu order, then instance order. One modifier's free units are shared,
6995
+ * so a second sibling line is not free just because it is its own line. */
6996
+ const billGroup = (group, nodes) => {
6997
+ const groupChargeAbove = group.charge_above_quantity ?? 0;
6998
+ let chargedCount = 0;
6999
+ const consumed = /* @__PURE__ */ new Map();
7000
+ const ordered = [];
7001
+ for (const modifierId of group.modifier_ids) for (const node of nodes) if (node.listed?.id === modifierId) ordered.push(node);
7002
+ for (const node of ordered) {
7003
+ if (node.quantity <= 0 || !node.listed || !node.group) continue;
7004
+ const entry = resolvePricingModifier(node.listed, node.group.id);
7005
+ const unit = new Big(modifierUnitPricing(...[
7006
+ node.listed,
7007
+ node.product,
7008
+ node.group.id
7009
+ ]).price);
7010
+ const allowance0 = entry?.charge_above_quantity ?? 0;
7011
+ const id = node.listed.id;
7012
+ const taken = consumed.get(id) ?? 0;
7013
+ const allowance = Math.max(0, allowance0 - taken);
7014
+ let remaining = Math.max(0, node.quantity - allowance);
7015
+ consumed.set(id, taken + node.quantity);
7016
+ let linePrice = new Big(0);
7017
+ while (remaining > 0) {
7018
+ if (chargedCount >= groupChargeAbove) linePrice = linePrice.plus(unit);
7019
+ chargedCount += 1;
7020
+ remaining -= 1;
7021
+ }
7022
+ node.unitPrice = node.quantity > 0 ? linePrice.div(node.quantity) : unit;
7023
+ }
7024
+ };
7025
+ const applyUnitMoney = (config) => {
7026
+ const qty = new Big(config.quantity);
7027
+ const net = config.unitSubtotal;
7028
+ let unitTax = net.times(config.taxRate);
7029
+ let unitTotal = net.plus(unitTax);
7030
+ if (config.taxInclusive) {
7031
+ const divisor = new Big(1).plus(config.taxRate);
7032
+ unitTax = net.times(config.taxRate).div(divisor);
7033
+ unitTotal = net;
7034
+ }
7035
+ const r1 = {
7036
+ tax_rate: config.taxRate,
7037
+ discount_rate: 0
7038
+ };
7039
+ const r2 = { unit_subtotal: config.unitSubtotal.toNumber() };
7040
+ const r3 = {
7041
+ unit_discount: 0,
7042
+ unit_tax_total: unitTax.toNumber()
7043
+ };
7044
+ const r4 = { unit_total: unitTotal.toNumber() };
7045
+ const r5 = { subtotal: config.unitSubtotal.times(qty).toNumber() };
7046
+ const r6 = {
7047
+ discount_total: 0,
7048
+ tax_total: unitTax.times(qty).toNumber()
7049
+ };
7050
+ const r7 = { total: unitTotal.times(qty).toNumber() };
7051
+ return {
7052
+ ...r1,
7053
+ ...r2,
7054
+ ...r3,
7055
+ ...r4,
7056
+ ...r5,
7057
+ ...r6,
7058
+ ...r7
7059
+ };
7060
+ };
7061
+ const emptyGroup = () => {
7062
+ const g1 = {
7063
+ unit_group_subtotal: 0,
7064
+ unit_group_discount: 0
7065
+ };
7066
+ const g2 = {
7067
+ unit_group_tax_total: 0,
7068
+ unit_group_total: 0
7069
+ };
7070
+ const g3 = {
7071
+ group_subtotal: 0,
7072
+ group_discount: 0
7073
+ };
7074
+ const g4 = {
7075
+ group_tax_total: 0,
7076
+ group_total: 0
7077
+ };
7078
+ return {
7079
+ ...g1,
7080
+ ...g2,
7081
+ ...g3,
7082
+ ...g4
7083
+ };
7084
+ };
7085
+ const displayName = (text, locale) => {
7086
+ if (text[locale]) return text[locale];
7087
+ return Object.values(text)[0] ?? "";
7088
+ };
7089
+ const emit = (node, parentUid, parentId, locale, out) => {
7090
+ const mIn1 = {
7091
+ unitSubtotal: node.unitPrice,
7092
+ quantity: node.quantity
7093
+ };
7094
+ const mIn2 = {
7095
+ taxRate: node.taxRate,
7096
+ taxInclusive: node.taxInclusive
7097
+ };
7098
+ const money = applyUnitMoney({
7099
+ ...mIn1,
7100
+ ...mIn2
7101
+ });
7102
+ const imageUrl = node.product.image_url ?? void 0;
7103
+ const imagePatch = imageUrl ? { image_url: imageUrl } : {};
7104
+ const catalog = {
7105
+ metadata: { ...node.product.metadata ?? {} },
7106
+ bundled_items: [...node.product.bundled_items ?? []]
7107
+ };
7108
+ const r1 = {
7109
+ type: "PRODUCT",
7110
+ uid: randomUUID()
7111
+ };
7112
+ const r2 = {
7113
+ id: node.product.id,
7114
+ parent_uid: parentUid,
7115
+ parent_id: parentId
7116
+ };
7117
+ const r3 = {
7118
+ name: displayName(node.product.name, locale),
7119
+ quantity: node.quantity
7120
+ };
7121
+ const r4 = {
7122
+ redeem: [],
7123
+ ...imagePatch,
7124
+ ...catalog,
7125
+ ...money
7126
+ };
7127
+ const line = {
7128
+ ...r1,
7129
+ ...r2,
7130
+ ...r3,
7131
+ ...r4,
7132
+ ...emptyGroup()
7133
+ };
7134
+ out.push(line);
7135
+ for (const child of node.children) emit(child, line.uid, line.id, locale, out);
7136
+ let sub = new Big(line.subtotal);
7137
+ let tax = new Big(line.tax_total);
7138
+ let tot = new Big(line.total);
7139
+ let childSub = new Big(0);
7140
+ let childTax = new Big(0);
7141
+ let childTot = new Big(0);
7142
+ for (const child of out) {
7143
+ if (child.parent_uid !== line.uid) continue;
7144
+ childSub = childSub.plus(child.group_subtotal);
7145
+ childTax = childTax.plus(child.group_tax_total);
7146
+ childTot = childTot.plus(child.group_total);
7147
+ }
7148
+ const qty = new Big(line.quantity);
7149
+ sub = sub.plus(childSub.times(qty));
7150
+ tax = tax.plus(childTax.times(qty));
7151
+ tot = tot.plus(childTot.times(qty));
7152
+ line.group_subtotal = sub.toNumber();
7153
+ line.group_tax_total = tax.toNumber();
7154
+ line.group_total = tot.toNumber();
7155
+ line.group_discount = 0;
7156
+ line.unit_group_subtotal = sub.div(qty).toNumber();
7157
+ line.unit_group_tax_total = tax.div(qty).toNumber();
7158
+ line.unit_group_total = tot.div(qty).toNumber();
7159
+ line.unit_group_discount = 0;
7160
+ };
7161
+ const rootNode = (lookup, root, state) => {
7162
+ const product = lookup.productsById[root.id];
7163
+ if (!product) return null;
7164
+ const max = product.pricing.maximum_quantity;
7165
+ const min = product.pricing.minimum_quantity;
7166
+ noteRules(state, root.quantity > max || root.quantity < min);
7167
+ const path = `/${product.id}`;
7168
+ state.paths[path] = root.quantity;
7169
+ const children = placeChildren(lookup, product, root, path, state);
7170
+ const r1 = {
7171
+ product,
7172
+ quantity: root.quantity,
7173
+ path,
7174
+ children
7175
+ };
7176
+ const r2 = {
7177
+ listed: null,
7178
+ group: null
7179
+ };
7180
+ const r3 = { unitPrice: new Big(product.pricing.price) };
7181
+ const r4 = { taxRate: product.pricing.tax_rate };
7182
+ const r5 = { taxInclusive: product.pricing.is_tax_inclusive };
7183
+ return {
7184
+ ...r1,
7185
+ ...r2,
7186
+ ...r3,
7187
+ ...r4,
7188
+ ...r5
7189
+ };
7190
+ };
7191
+ /** Place cart lines on menu slots and price them from the menu.
7192
+ * The tree is built from product id, parent, and quantity. A line's claimed
7193
+ * own price is read for one purpose: to rank which listed slots it may take
7194
+ * (see slotYieldsLine). The expected money is always the menu's for the
7195
+ * chosen slot, so a claim the menu cannot yield stays a mismatch.
7196
+ * A quantity above 1 stays one line. Extra sibling lines reuse a slot
7197
+ * only while its max still has room, at `/instance/2`, `/instance/3`, ….
7198
+ * Indices are assigned here. A forged index in the cart is not an input. */
7199
+ const placeCartLines = (root, lines, lookup, locale) => {
7200
+ const childrenByParent = /* @__PURE__ */ new Map();
7201
+ for (const line of lines) {
7202
+ if (line.parent_uid == null) continue;
7203
+ const list = childrenByParent.get(line.parent_uid) ?? [];
7204
+ list.push(line);
7205
+ childrenByParent.set(line.parent_uid, list);
7206
+ }
7207
+ const state = {
7208
+ paths: {},
7209
+ overCapacity: false,
7210
+ rulesBroken: false,
7211
+ childrenByParent,
7212
+ keyCache: /* @__PURE__ */ new Map()
7213
+ };
7214
+ const node = rootNode(lookup, root, state);
7215
+ const out = [];
7216
+ if (node) emit(node, null, null, locale, out);
7217
+ if (state.overCapacity) state.rulesBroken = true;
7218
+ return {
7219
+ paths: state.paths,
7220
+ overCapacity: state.overCapacity,
7221
+ rulesBroken: state.rulesBroken,
7222
+ lines: out
7223
+ };
7224
+ };
7225
+ //#endregion
6606
7226
  //#region src/order/validate.ts
6607
7227
  const PRICE_FIELDS = [
6608
7228
  "unit_subtotal",
@@ -6661,11 +7281,34 @@ const rateStr = (n) => {
6661
7281
  if (n == null) return "null";
6662
7282
  return bigStr(n);
6663
7283
  };
7284
+ const structureKey = (lines, line) => {
7285
+ const nested = lines.filter((child) => child.parent_uid === line.uid).map((child) => structureKey(lines, child));
7286
+ nested.sort((left, right) => left.localeCompare(right));
7287
+ return `${line.id}x${line.quantity}(${nested.join(",")})`;
7288
+ };
7289
+ /** Sorted by id as before. Only same-id siblings are told apart further:
7290
+ * by nested shape, then unit price, so each pairs with its own expectation. */
6664
7291
  const childrenOf = (lines, parentUid) => {
6665
7292
  const children = lines.filter((line) => {
6666
7293
  return line.parent_uid === parentUid;
6667
7294
  });
6668
- children.sort((a, b) => a.id.localeCompare(b.id));
7295
+ const shapes = /* @__PURE__ */ new Map();
7296
+ const shapeOf = (line) => {
7297
+ const cached = shapes.get(line.uid);
7298
+ if (cached !== void 0) return cached;
7299
+ const shape = structureKey(lines, line);
7300
+ shapes.set(line.uid, shape);
7301
+ return shape;
7302
+ };
7303
+ children.sort((a, b) => {
7304
+ const byId = a.id.localeCompare(b.id);
7305
+ if (byId !== 0) return byId;
7306
+ const byShape = shapeOf(a).localeCompare(shapeOf(b));
7307
+ if (byShape !== 0) return byShape;
7308
+ const bySubtotal = new Big(a.unit_subtotal).cmp(b.unit_subtotal);
7309
+ if (bySubtotal !== 0) return bySubtotal;
7310
+ return new Big(a.unit_tax_total).cmp(b.unit_tax_total);
7311
+ });
6669
7312
  return children;
6670
7313
  };
6671
7314
  const collectUnmatchedModifiers = (lines, lookup, parentProductId, parentUid, out) => {
@@ -6908,10 +7551,17 @@ const validateProductRoot = (root, lines, lookup, locale, out) => {
6908
7551
  if (status === "OUT_OF_STOCK") pushLineIssue(out, root.uid, { code: "OUT_OF_STOCK" });
6909
7552
  if (!isOnSchedule(product.availability.schedules)) pushLineIssue(out, root.uid, { code: "OUTSIDE_HOURS" });
6910
7553
  collectUnmatchedModifiers(lines, lookup, root.id, root.uid, out);
6911
- const snapshot = fromOrderLines(root, lines, lookup);
6912
- const session = createSession(lookup, root.id, { snapshot });
6913
- if (session.validate() !== "VALID") pushLineIssue(out, root.uid, { code: "MODIFIER_RULES_INVALID" });
6914
- const expected = toOrderLines(session.root, locale);
7554
+ let expected;
7555
+ if (hasRepeatedProductSibling(lines, root.uid)) {
7556
+ const placed = placeCartLines(root, lines, lookup, locale);
7557
+ if (placed.rulesBroken) pushLineIssue(out, root.uid, { code: "MODIFIER_RULES_INVALID" });
7558
+ expected = placed.lines;
7559
+ } else {
7560
+ const snapshot = fromOrderLines(root, lines, lookup);
7561
+ const session = createSession(lookup, root.id, { snapshot });
7562
+ if (session.validate() !== "VALID") pushLineIssue(out, root.uid, { code: "MODIFIER_RULES_INVALID" });
7563
+ expected = toOrderLines(session.root, locale);
7564
+ }
6915
7565
  const expectedRoot = expected.find((line) => line.parent_uid == null);
6916
7566
  if (!expectedRoot) {
6917
7567
  pushLineIssue(out, root.uid, { code: "STRUCTURE_MISMATCH" });
@@ -7234,6 +7884,17 @@ const collectOrderLineDiffs = (input) => {
7234
7884
  continue;
7235
7885
  }
7236
7886
  try {
7887
+ if (hasRepeatedProductSibling(input.lines, root.uid)) {
7888
+ const placed = placeCartLines(root, input.lines, lookup, locale);
7889
+ const next = placed.rulesBroken ? null : placed.lines;
7890
+ diffs.push({
7891
+ uid: root.uid,
7892
+ previous,
7893
+ next,
7894
+ issues
7895
+ });
7896
+ continue;
7897
+ }
7237
7898
  const snapshot = fromOrderLines(root, input.lines, lookup);
7238
7899
  const next = toOrderLines(createSession(lookup, root.id, { snapshot }).root, locale);
7239
7900
  diffs.push({
@@ -7800,16 +8461,6 @@ const countCartProducts = (lines, includeModifiers) => {
7800
8461
  return line.type === "PRODUCT" || line.type == null;
7801
8462
  }).reduce((sum, line) => sum + line.quantity, 0);
7802
8463
  };
7803
- const getLineTagValues = (lines, tag) => {
7804
- if (!tag) return [];
7805
- const values = [];
7806
- for (const line of lines ?? []) {
7807
- const value = line.tags?.[tag];
7808
- if (typeof value === "undefined") continue;
7809
- values.push(value);
7810
- }
7811
- return values;
7812
- };
7813
8464
  //#endregion
7814
8465
  //#region src/rules/absoluteDateMatcher.ts
7815
8466
  var import_dayjs_min = /* @__PURE__ */ __toESM(require_dayjs_min(), 1);
@@ -8414,6 +9065,72 @@ const matchRelativeDateOperator = (condition, matcher, referenceDate) => {
8414
9065
  }
8415
9066
  };
8416
9067
  //#endregion
9068
+ //#region src/rules/tagMatcher.ts
9069
+ const NEGATED_TO_POSITIVE = {
9070
+ "not equal to": "equal to",
9071
+ "is not": "is"
9072
+ };
9073
+ const SET_OPERATORS = [
9074
+ "is any of",
9075
+ "is all of",
9076
+ "is none of"
9077
+ ];
9078
+ const isComparable = (kind, value) => {
9079
+ if (kind === "string") return typeof value === "string";
9080
+ if (kind === "boolean") return typeof value === "boolean";
9081
+ return typeof value === "number";
9082
+ };
9083
+ const normalize = (value, caseInsensitive) => {
9084
+ if (typeof value === "string" && caseInsensitive) return value.toLowerCase();
9085
+ return value;
9086
+ };
9087
+ const matchSet = (condition, kind, elements) => {
9088
+ const { operator, value } = condition;
9089
+ if (!Array.isArray(value)) throw new Error("Invalid tag set value");
9090
+ const caseInsensitive = getCaseInsensitive(condition);
9091
+ const wanted = new Set(value.map((item) => normalize(item, caseInsensitive)));
9092
+ const present = new Set(elements.map((item) => normalize(item, caseInsensitive)));
9093
+ const hasAny = elements.length > 0;
9094
+ switch (operator) {
9095
+ case "is any of": return [...present].some((item) => wanted.has(item));
9096
+ case "is all of": return [...wanted].every((item) => present.has(item));
9097
+ case "is none of": return !hasAny || ![...present].some((item) => wanted.has(item));
9098
+ default: throw new Error(`Operator ${operator} not supported in ${kind} tags`);
9099
+ }
9100
+ };
9101
+ /**
9102
+ * Evaluates one line's tag value against a tag condition.
9103
+ *
9104
+ * - Scalars use the primitive's matcher unchanged. A value of another type
9105
+ * never matches.
9106
+ * - Lists use "any element matches" for positive operators and "no element
9107
+ * matches the positive form" for negated ones. An empty list matches
9108
+ * negated operators and set operator "is none of", never the others.
9109
+ * - Set operators treat a scalar as a one-element set.
9110
+ */
9111
+ const matchTagLine = (condition, kind, raw, matchScalar) => {
9112
+ const { operator } = condition;
9113
+ const isList = Array.isArray(raw);
9114
+ const items = isList ? raw : [raw];
9115
+ const comparable = items.filter((item) => isComparable(kind, item));
9116
+ if (SET_OPERATORS.includes(operator)) {
9117
+ if (kind === "boolean" || kind === "currency") return false;
9118
+ if (items.length > 0 && comparable.length === 0) return false;
9119
+ return matchSet(condition, kind, comparable);
9120
+ }
9121
+ const match = matchScalar;
9122
+ if (!isList) return comparable.length > 0 ? match(condition, raw) : false;
9123
+ const positive = NEGATED_TO_POSITIVE[operator];
9124
+ if (items.length === 0) return typeof positive === "string";
9125
+ if (comparable.length === 0) return false;
9126
+ if (typeof positive === "undefined") return comparable.some((item) => match(condition, item));
9127
+ const positiveCondition = {
9128
+ ...condition,
9129
+ operator: positive
9130
+ };
9131
+ return !comparable.some((item) => match(positiveCondition, item));
9132
+ };
9133
+ //#endregion
8417
9134
  //#region src/rules/RulesEngine.ts
8418
9135
  var RulesEngine = class {
8419
9136
  order;
@@ -8450,20 +9167,20 @@ var RulesEngine = class {
8450
9167
  if (!("tag" in condition) || !condition.tag) throw new Error("Invalid tag");
8451
9168
  return condition.tag;
8452
9169
  };
8453
- matchOrderProductTag = (condition, match) => {
9170
+ matchOrderProductTag = (condition, kind, matchScalar) => {
8454
9171
  const order = this.getDependency("order", condition.type);
8455
9172
  const tag = this.getProductTag(condition);
8456
- const values = getLineTagValues(order.lines, tag);
8457
- for (const value of values) if (match(value)) return true;
9173
+ for (const line of order.lines ?? []) {
9174
+ const raw = line.tags?.[tag];
9175
+ if (typeof raw === "undefined") continue;
9176
+ if (matchTagLine(condition, kind, raw, matchScalar)) return true;
9177
+ }
8458
9178
  return false;
8459
9179
  };
8460
9180
  assertString = (condition) => {
8461
9181
  const { primitive, type } = condition;
8462
9182
  assert(primitive === "string", primitiveError("string"));
8463
- if (type === "Order includes product with tag") return this.matchOrderProductTag(condition, (value) => {
8464
- if (typeof value !== "string") return false;
8465
- return orderIncludesProductWithTag(condition, value);
8466
- });
9183
+ if (type === "Order includes product with tag") return this.matchOrderProductTag(condition, "string", orderIncludesProductWithTag);
8467
9184
  if (type === "Request path") {
8468
9185
  const monitoring = this.getDependency("monitoring", type);
8469
9186
  return requestPath(condition, monitoring.path);
@@ -8489,10 +9206,7 @@ var RulesEngine = class {
8489
9206
  const line = getLineByProductId(order.lines, productId);
8490
9207
  return orderIncludesProduct(condition, line?.quantity);
8491
9208
  }
8492
- if (type === "Order includes product with tag") return this.matchOrderProductTag(condition, (value) => {
8493
- if (typeof value !== "number") return false;
8494
- return orderIncludesProductWithTag$1(condition, value);
8495
- });
9209
+ if (type === "Order includes product with tag") return this.matchOrderProductTag(condition, "number", orderIncludesProductWithTag$1);
8496
9210
  if (type === "Accumulated events") {
8497
9211
  const monitoring = this.getDependency("monitoring", type);
8498
9212
  const window = windowMinutes(condition);
@@ -8525,10 +9239,7 @@ var RulesEngine = class {
8525
9239
  const isAnonymous = this.getDependency("customer", type).is_anonymous;
8526
9240
  return isAnonymousCustomer(condition, isAnonymous);
8527
9241
  }
8528
- if (type === "Order includes product with tag") return this.matchOrderProductTag(condition, (value) => {
8529
- if (typeof value !== "boolean") return false;
8530
- return orderIncludesProductWithTag$3(condition, value);
8531
- });
9242
+ if (type === "Order includes product with tag") return this.matchOrderProductTag(condition, "boolean", orderIncludesProductWithTag$3);
8532
9243
  if (type === "Payment is mixed") {
8533
9244
  const payment = this.getDependency("payment", type);
8534
9245
  return paymentIsMixed(condition, payment.is_mixed);
@@ -8546,10 +9257,7 @@ var RulesEngine = class {
8546
9257
  const order = this.getDependency("order", type);
8547
9258
  return orderTotal(condition, order.total);
8548
9259
  }
8549
- if (type === "Order includes product with tag") return this.matchOrderProductTag(condition, (value) => {
8550
- if (typeof value !== "number") return false;
8551
- return orderIncludesProductWithTag$2(condition, value);
8552
- });
9260
+ if (type === "Order includes product with tag") return this.matchOrderProductTag(condition, "currency", orderIncludesProductWithTag$2);
8553
9261
  throw new Error(`Condition ${type} not supported in currency matcher`);
8554
9262
  };
8555
9263
  assertEnum = (condition) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fire-spark/core",
3
- "version": "0.11.1",
3
+ "version": "0.12.1",
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.11.1",
24
- "@fire-spark/types": "^0.11.1"
23
+ "@fire-spark/payments": "^0.12.1",
24
+ "@fire-spark/types": "^0.12.1"
25
25
  },
26
26
  "devDependencies": {
27
27
  "tsdown": "^0.22.14",