@fire-spark/core 0.12.4 → 0.13.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
@@ -216,8 +216,32 @@ type OrderCustomerSource = {
216
216
  email?: string | null;
217
217
  phone?: string | null;
218
218
  };
219
+ /**
220
+ * Stand-ins the order schema needs while a guest's contact data is unknown.
221
+ * Only valid when the merchant's `anonymous_customer` field is optional or
222
+ * hidden; the API rejects them on required fields (`CUSTOMER_FIELD_REQUIRED`).
223
+ */
224
+ declare const CUSTOMER_INFO_PLACEHOLDERS: {
225
+ readonly email: "unknown@example.com";
226
+ readonly phone: "+10000000000";
227
+ };
219
228
  /** Builds the order `customer` snapshot sent on create/update. */
220
229
  declare const buildCustomerSnapshot: (source: OrderCustomerSource) => Order$1["customer"];
230
+ /**
231
+ * True when `value` is a stand-in, not data the guest typed: the
232
+ * placeholders above, or a name equal to the anonymous customer id.
233
+ */
234
+ declare const isCustomerInfoPlaceholder: (key: string, value: unknown, customerId?: string | null) => boolean;
235
+ /** A `customer_info` field holds real (non-placeholder) guest data. */
236
+ declare const isCustomerInfoFieldFilled: (info: Record<string, unknown> | null | undefined, key: string, customerId?: string | null) => boolean;
237
+ /** Strips spaces, dashes, dots and parentheses: "+593 99-123" → "+59399123". */
238
+ declare const normalizeCustomerPhone: (value: string) => string;
239
+ /**
240
+ * Format check the order schema enforces on `customer_info`: email shape and
241
+ * E.164 phone (`+593991234567`). Empty values are valid (presence is checked
242
+ * against the merchant's `anonymous_customer` modes instead).
243
+ */
244
+ declare const isCustomerInfoValueValid: (key: string, value: string) => boolean;
221
245
  //#endregion
222
246
  //#region src/services/http.d.ts
223
247
  type ServiceHttpOptions = {
@@ -620,6 +644,8 @@ declare class Order {
620
644
  setCustomerDetails(details: {
621
645
  address_info?: Order$1["customer"]["address_info"];
622
646
  billing_info?: Order$1["customer"]["billing_info"];
647
+ /** Guest contact data (anonymous checkout); blanks keep the snapshot. */
648
+ customer_info?: Partial<Order$1["customer"]["customer_info"]>;
623
649
  }): void;
624
650
  cancelOrder(): Promise<void>;
625
651
  /**
@@ -824,7 +850,7 @@ declare const isOrderPaymentRequiredError: (error: unknown) => error is OrderPay
824
850
  //#endregion
825
851
  //#region src/order/validate.d.ts
826
852
  type OrderLineIssueCode = "PRODUCT_NOT_FOUND" | "HIDDEN" | "OUT_OF_STOCK" | "OUTSIDE_HOURS" | "MODIFIER_NOT_FOUND" | "MODIFIER_RULES_INVALID" | "VARIANT_BASE_NOT_ORDERABLE" | "VARIANT_NOT_OFFERED" | "PRICE_MISMATCH" | "TAX_MISMATCH" | "DISCOUNT_MISMATCH" | "QUANTITY_MISMATCH" | "QUANTITY_BELOW_MINIMUM" | "QUANTITY_ABOVE_MAXIMUM" | "STRUCTURE_MISMATCH" | "FEE_MISMATCH" | "REDEEM_EXPIRED" | RedeemIssueCode;
827
- type OrderIssueCode = "NO_MENU" | "MENU_CLOSED" | "SCOPE_INACTIVE" | "TOTALS_MISMATCH" | "REDEEM_EXPIRED" | RedeemIssueCode | "REDEMPTION_STEPS_MISMATCH" | "REDEMPTION_MISMATCH" | "EMPTY_ORDER" | "ITEMS_BELOW_MINIMUM" | "ITEMS_ABOVE_MAXIMUM" | "TOTAL_BELOW_MINIMUM" | "TOTAL_ABOVE_MAXIMUM" | "REDEEMS_ABOVE_MAXIMUM" | "REDEEMS_TOTAL_ABOVE_MAXIMUM" | "SHIPPING_REQUIRED" | "BILLING_REQUIRED" | "SHIPPING_FIELD_REQUIRED" | "BILLING_FIELD_REQUIRED" | "DAILY_ORDER_LIMIT_REACHED";
853
+ type OrderIssueCode = "NO_MENU" | "MENU_CLOSED" | "SCOPE_INACTIVE" | "TOTALS_MISMATCH" | "REDEEM_EXPIRED" | RedeemIssueCode | "REDEMPTION_STEPS_MISMATCH" | "REDEMPTION_MISMATCH" | "EMPTY_ORDER" | "ITEMS_BELOW_MINIMUM" | "ITEMS_ABOVE_MAXIMUM" | "TOTAL_BELOW_MINIMUM" | "TOTAL_ABOVE_MAXIMUM" | "REDEEMS_ABOVE_MAXIMUM" | "REDEEMS_TOTAL_ABOVE_MAXIMUM" | "SHIPPING_REQUIRED" | "BILLING_REQUIRED" | "SHIPPING_FIELD_REQUIRED" | "BILLING_FIELD_REQUIRED" | "CUSTOMER_FIELD_REQUIRED" | "DAILY_ORDER_LIMIT_REACHED";
828
854
  type OrderLineIssue = {
829
855
  code: OrderLineIssueCode;
830
856
  field?: string;
@@ -941,6 +967,7 @@ declare const capRedeems: <T extends CapOrder>(order: T) => T & {
941
967
  id: string;
942
968
  parent_uid: string | null;
943
969
  parent_id: string | null;
970
+ group_id?: string | undefined;
944
971
  name: string;
945
972
  image_url?: string | undefined;
946
973
  menu_id?: string | undefined;
@@ -1570,6 +1597,10 @@ interface CheckoutSection {
1570
1597
  interface CheckoutPreferences {
1571
1598
  shipping: CheckoutSection;
1572
1599
  billing: CheckoutSection;
1600
+ /** Missing on orders stamped before the setting existed → defaults. */
1601
+ anonymous_customer?: {
1602
+ fields: Record<string, CheckoutMode>;
1603
+ };
1573
1604
  }
1574
1605
  declare const isCheckoutFieldFilled: (info: Record<string, unknown> | null | undefined, key: string) => boolean;
1575
1606
  interface CheckoutOrderInput {
@@ -1674,4 +1705,4 @@ declare const validateCardholderName: (value: string) => CardholderNameError | n
1674
1705
  */
1675
1706
  declare const cardPreviewGroups: (value: string) => CardPreviewSlot[][];
1676
1707
  //#endregion
1677
- 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 DeunaCallbackUrls, type DeunaOrderStatus, type ExchangeOptions, type FailedCharge, type FlatMenuCategory, type GenOrderIdOptions, type GroupDisplayType, ID_HASH_FORMATS, type IdBuilder, type IdHashFormat, type ListMenusStreamOptions, type ListPaymentsOptions, type Menu, type MenuCategoryNode, type MenuLookup, type MilestoneSteps, type MilestoneTrackItem, type ModifierControlType, type ModifierCtx, type ModifierGroupCtx, type NestedMenuCategory, Order, type OrderCancelInput, type OrderCancelOptions, type OrderCancelResult, type OrderCancelType, type OrderCustomerSource, type OrderDeviceCredential, type OrderDraft, type OrderIssue, type OrderIssueCode, type OrderLineDiff, type OrderLineIssue, type OrderLineIssueCode, OrderPaymentRequiredError, type OrderScope, OrderSyncPendingError, OrderSyncRejectedError, OrderValidationError, type OrderValidationResult, type PayIntentOptions, type PayIntentResult, type PayOptions, type PaymentCardBrand, type PaymentProgressEvent, type PaymentProgressListener, type PaymentRunOptions, type PaymentSessionOptions, type PricingModifier, type Product, type ProductCtx, type QuantitiesByPath, type RecommendationType, RecurringDateCron, type RedeemPaymentRow, type RemoveRedeemInput, type ReorderOrderOptions, type ResetOptions, type ReversalOutcome, type ReverseAllOptions, RulesEngine, type ScheduleWindow, type SelectionStatus, type SelectionsByPath, type SerializedError, type ServiceHttpOptions, type SimpleTenderPayment, type Snapshot, type StorefrontAccessToken, type StorefrontErrorDetails, StorefrontHttpError, type TrackWorkflow, type ValidateOptions, type ValidateOrderAgainstMenuInput, type Variant, type VariantGroup, type VariantGroupCtx, type VariantOptionCtx, type VariantSelection, type VerifyWebhookSignatureInput, type VisitedState, addLines, auth, buildCustomerSnapshot, cancelTypeOf, capRedeems, cappedRedeems, cardBrandSpec, cardDigits, cardNumberGaps, cardNumberMaxLength, cardPreviewGroups, caretAfterDigits, collectOrderLineDiffs, computeStartingPrice, countCartProducts, coveredAmount, createDeunaWidgetApi, createSession, currentNodes, cvcMaxLength, defaultConditionError, detectCardBrand, fitRedeems, flattenMenuCategories, flushLogs, formatCardNumber, formatCvc, formatExpiry, fromOrderLines, genId, genOrderId, getLogContext, getScheduleWindow, indexMenu, isCardNumberComplete, isCardNumberDefinitelyInvalid, isCheckoutFieldFilled, isCvcComplete, isDeviceCharged, isDeviceNotLinked, isExpiryComplete, isGenId, isKioskUnauthorized, isMilestoneStepDone, isOnSchedule, isOrderPaymentRequiredError, isOrderValidationError, isPosDeviceNotLinked, isPosUnauthorized, isRealPaymentMethod, isStorefrontNotFound, isVariantBase, linePayableTotal, listMenusStream, localizePosError, log, logError, luhnCheck, matchAbsoluteDateOperator, matchBooleanOperator, matchCurrencyOperator, matchEnumOperator, matchNumberOperator, matchRecurringDateOperator, matchRelativeDateOperator, matchStringOperator, milestoneSteps, milestoneTrack, nestMenuCategories, normalizeCardholderName, orderPayableTotal, orderRedeemedTotal, orderRedeems, parseExpiry, paymentMatchesClientConditions, rebuildRootGroups, recomputeTotals, redeemedTotal, redemptionCap, redemptionSteps, remainingAmount, removeLine, replaceLine, resolveCheckout, resolveModifierImageUrl, reversePayment, runPaymentAttempt, selectablePayments, serializeError, services, setLineQuantity, storefrontJson, toOrderLines, toPaymentCardBrand, validateCardNumber, validateCardholderName, validateCvc, validateExpiry, validateOrderAgainstMenu, verifyWebhookSignature, visitedStates };
1708
+ export { type ApplyRedeemInput, type AuthConfigureOptions, CARD_BRANDS, CUSTOMER_INFO_PLACEHOLDERS, 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 DeunaCallbackUrls, type DeunaOrderStatus, type ExchangeOptions, type FailedCharge, type FlatMenuCategory, type GenOrderIdOptions, type GroupDisplayType, ID_HASH_FORMATS, type IdBuilder, type IdHashFormat, type ListMenusStreamOptions, type ListPaymentsOptions, type Menu, type MenuCategoryNode, type MenuLookup, type MilestoneSteps, type MilestoneTrackItem, type ModifierControlType, type ModifierCtx, type ModifierGroupCtx, type NestedMenuCategory, Order, type OrderCancelInput, type OrderCancelOptions, type OrderCancelResult, type OrderCancelType, type OrderCustomerSource, type OrderDeviceCredential, type OrderDraft, type OrderIssue, type OrderIssueCode, type OrderLineDiff, type OrderLineIssue, type OrderLineIssueCode, OrderPaymentRequiredError, type OrderScope, OrderSyncPendingError, OrderSyncRejectedError, OrderValidationError, type OrderValidationResult, type PayIntentOptions, type PayIntentResult, type PayOptions, type PaymentCardBrand, type PaymentProgressEvent, type PaymentProgressListener, type PaymentRunOptions, type PaymentSessionOptions, type PricingModifier, type Product, type ProductCtx, type QuantitiesByPath, type RecommendationType, RecurringDateCron, type RedeemPaymentRow, type RemoveRedeemInput, type ReorderOrderOptions, type ResetOptions, type ReversalOutcome, type ReverseAllOptions, RulesEngine, type ScheduleWindow, type SelectionStatus, type SelectionsByPath, type SerializedError, type ServiceHttpOptions, type SimpleTenderPayment, type Snapshot, type StorefrontAccessToken, type StorefrontErrorDetails, StorefrontHttpError, type TrackWorkflow, type ValidateOptions, type ValidateOrderAgainstMenuInput, type Variant, type VariantGroup, type VariantGroupCtx, type VariantOptionCtx, type VariantSelection, type VerifyWebhookSignatureInput, type VisitedState, addLines, auth, buildCustomerSnapshot, cancelTypeOf, capRedeems, cappedRedeems, cardBrandSpec, cardDigits, cardNumberGaps, cardNumberMaxLength, cardPreviewGroups, caretAfterDigits, collectOrderLineDiffs, computeStartingPrice, countCartProducts, coveredAmount, createDeunaWidgetApi, createSession, currentNodes, cvcMaxLength, defaultConditionError, detectCardBrand, fitRedeems, flattenMenuCategories, flushLogs, formatCardNumber, formatCvc, formatExpiry, fromOrderLines, genId, genOrderId, getLogContext, getScheduleWindow, indexMenu, isCardNumberComplete, isCardNumberDefinitelyInvalid, isCheckoutFieldFilled, isCustomerInfoFieldFilled, isCustomerInfoPlaceholder, isCustomerInfoValueValid, isCvcComplete, isDeviceCharged, isDeviceNotLinked, isExpiryComplete, isGenId, isKioskUnauthorized, isMilestoneStepDone, isOnSchedule, isOrderPaymentRequiredError, isOrderValidationError, isPosDeviceNotLinked, isPosUnauthorized, isRealPaymentMethod, isStorefrontNotFound, isVariantBase, linePayableTotal, listMenusStream, localizePosError, log, logError, luhnCheck, matchAbsoluteDateOperator, matchBooleanOperator, matchCurrencyOperator, matchEnumOperator, matchNumberOperator, matchRecurringDateOperator, matchRelativeDateOperator, matchStringOperator, milestoneSteps, milestoneTrack, nestMenuCategories, normalizeCardholderName, normalizeCustomerPhone, orderPayableTotal, orderRedeemedTotal, orderRedeems, parseExpiry, paymentMatchesClientConditions, rebuildRootGroups, recomputeTotals, redeemedTotal, redemptionCap, redemptionSteps, remainingAmount, removeLine, replaceLine, resolveCheckout, resolveModifierImageUrl, reversePayment, runPaymentAttempt, selectablePayments, serializeError, services, setLineQuantity, storefrontJson, toOrderLines, toPaymentCardBrand, validateCardNumber, validateCardholderName, validateCvc, validateExpiry, validateOrderAgainstMenu, verifyWebhookSignature, visitedStates };
package/dist/index.js CHANGED
@@ -1415,9 +1415,11 @@ const flattenMenuCategories = (categories, categoriesById, options) => {
1415
1415
  //#endregion
1416
1416
  //#region src/productCustomization/fromOrderLines.ts
1417
1417
  /** Group slot of a child line: listed directly, or as the variant of a base
1418
- * listed in one of the parent's modifier groups. */
1419
- const matchModifierLine = (lookup, parent, childId) => {
1418
+ * listed in one of the parent's modifier groups. With `onlyGroupId` (the
1419
+ * line's declared `group_id`) only that group of this parent is searched. */
1420
+ const matchModifierLine = (lookup, parent, childId, onlyGroupId) => {
1420
1421
  const groups = parent.pricing.modifier_group_ids.flatMap((groupId) => {
1422
+ if (onlyGroupId !== void 0 && groupId !== onlyGroupId) return [];
1421
1423
  const group = lookup.modifierGroupsById[groupId];
1422
1424
  return group ? [group] : [];
1423
1425
  });
@@ -1463,7 +1465,13 @@ const fromOrderLines = (root, lines, lookup) => {
1463
1465
  const parentProduct = lookup.productsById[parentLine.id];
1464
1466
  if (!parentProduct) return;
1465
1467
  for (const child of childrenByParent.get(parentLine.uid) ?? []) {
1466
- const match = matchModifierLine(lookup, parentProduct, child.id);
1468
+ const p = [
1469
+ lookup,
1470
+ parentProduct,
1471
+ child.id,
1472
+ child.group_id
1473
+ ];
1474
+ const match = matchModifierLine(...p);
1467
1475
  if (!match) continue;
1468
1476
  const { groupId, modifierId, selection } = match;
1469
1477
  const path = `${pathPrefix}/group/${groupId}/modifier/${modifierId}`;
@@ -1750,6 +1758,7 @@ const buildLine = (args) => {
1750
1758
  name: args.name
1751
1759
  };
1752
1760
  const r4 = {
1761
+ ...args.group_id ? { group_id: args.group_id } : {},
1753
1762
  ...imagePatch,
1754
1763
  ...catalog
1755
1764
  };
@@ -1835,7 +1844,8 @@ const appendModifierLine = (modifier, groupId, parent, locale) => {
1835
1844
  const line = buildLine({
1836
1845
  ...l1,
1837
1846
  ...l2,
1838
- ...l3
1847
+ ...l3,
1848
+ group_id: groupId
1839
1849
  });
1840
1850
  parent.children.push(line);
1841
1851
  if (modifier.product) appendGroupLines(modifier.product.groups, line, locale);
@@ -1955,6 +1965,15 @@ const auth = {
1955
1965
  };
1956
1966
  //#endregion
1957
1967
  //#region src/order/customer.ts
1968
+ /**
1969
+ * Stand-ins the order schema needs while a guest's contact data is unknown.
1970
+ * Only valid when the merchant's `anonymous_customer` field is optional or
1971
+ * hidden; the API rejects them on required fields (`CUSTOMER_FIELD_REQUIRED`).
1972
+ */
1973
+ const CUSTOMER_INFO_PLACEHOLDERS = {
1974
+ email: "unknown@example.com",
1975
+ phone: "+10000000000"
1976
+ };
1958
1977
  const splitName = (name) => {
1959
1978
  const parts = name.trim().split(/\s+/).filter(Boolean);
1960
1979
  const first_name = parts[0] ?? name;
@@ -1967,14 +1986,108 @@ const splitName = (name) => {
1967
1986
  /** Builds the order `customer` snapshot sent on create/update. */
1968
1987
  const buildCustomerSnapshot = (source) => {
1969
1988
  const names = splitName(source.name);
1989
+ const email = source.email || CUSTOMER_INFO_PLACEHOLDERS.email;
1990
+ const phone = source.phone || CUSTOMER_INFO_PLACEHOLDERS.phone;
1970
1991
  return { customer_info: {
1971
1992
  display_name: names.display_name,
1972
1993
  first_name: names.first_name,
1973
1994
  last_name: names.last_name,
1974
- email: source.email ?? "unknown@example.com",
1975
- phone: source.phone ?? "+10000000000"
1995
+ email,
1996
+ phone
1976
1997
  } };
1977
1998
  };
1999
+ /**
2000
+ * True when `value` is a stand-in, not data the guest typed: the
2001
+ * placeholders above, or a name equal to the anonymous customer id.
2002
+ */
2003
+ const isCustomerInfoPlaceholder = (key, value, customerId) => {
2004
+ if (typeof value !== "string") return false;
2005
+ const text = value.trim();
2006
+ if (key === "email") return text === CUSTOMER_INFO_PLACEHOLDERS.email;
2007
+ if (key === "phone") return text === CUSTOMER_INFO_PLACEHOLDERS.phone;
2008
+ if ((key === "first_name" || key === "last_name") && customerId) return text === customerId;
2009
+ return false;
2010
+ };
2011
+ /** A `customer_info` field holds real (non-placeholder) guest data. */
2012
+ const isCustomerInfoFieldFilled = (info, key, customerId) => {
2013
+ let value = info?.[key];
2014
+ const metadata = info?.metadata;
2015
+ if (value == null && metadata && typeof metadata === "object" && !Array.isArray(metadata)) value = metadata[key];
2016
+ if (value == null) return false;
2017
+ if (typeof value === "string" && !value.trim()) return false;
2018
+ return !isCustomerInfoPlaceholder(key, value, customerId);
2019
+ };
2020
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2021
+ const PHONE_RE = /^\+[1-9]\d{1,14}$/;
2022
+ /** Strips spaces, dashes, dots and parentheses: "+593 99-123" → "+59399123". */
2023
+ const normalizeCustomerPhone = (value) => {
2024
+ return value.trim().replace(/[\s\-().]/g, "");
2025
+ };
2026
+ /**
2027
+ * Format check the order schema enforces on `customer_info`: email shape and
2028
+ * E.164 phone (`+593991234567`). Empty values are valid (presence is checked
2029
+ * against the merchant's `anonymous_customer` modes instead).
2030
+ */
2031
+ const isCustomerInfoValueValid = (key, value) => {
2032
+ const text = value.trim();
2033
+ if (!text) return true;
2034
+ if (key === "email") return EMAIL_RE.test(text);
2035
+ if (key === "phone") return PHONE_RE.test(normalizeCustomerPhone(text));
2036
+ return text.length <= 100;
2037
+ };
2038
+ /**
2039
+ * Merges guest contact data into `customer_info`; empty or malformed values
2040
+ * fall back to the current snapshot (placeholders for optional/hidden fields)
2041
+ * so a half-typed field never breaks the order sync.
2042
+ */
2043
+ const mergeCustomerInfo = (current, patch) => {
2044
+ const clean = (value, key = "") => {
2045
+ let text = value?.trim();
2046
+ if (text && key === "phone") text = normalizeCustomerPhone(text);
2047
+ if (!text || !isCustomerInfoValueValid(key, text)) return void 0;
2048
+ return text;
2049
+ };
2050
+ const first_name = clean(patch.first_name) ?? current.first_name;
2051
+ const last_name = clean(patch.last_name) ?? current.last_name;
2052
+ const email = clean(patch.email, "email") ?? current.email;
2053
+ const phone = clean(patch.phone, "phone") ?? current.phone;
2054
+ const typedName = clean(patch.first_name) || clean(patch.last_name);
2055
+ let display_name = current.display_name;
2056
+ if (typedName) display_name = `${first_name} ${last_name}`.trim();
2057
+ const b1 = {
2058
+ display_name,
2059
+ first_name,
2060
+ last_name,
2061
+ email,
2062
+ phone
2063
+ };
2064
+ if (!patch.metadata) return {
2065
+ ...current,
2066
+ ...b1
2067
+ };
2068
+ const metadata = mergeCustomMetadata(current.metadata, patch.metadata);
2069
+ const { metadata: _drop, ...rest } = current;
2070
+ if (metadata === void 0) return {
2071
+ ...rest,
2072
+ ...b1
2073
+ };
2074
+ return {
2075
+ ...rest,
2076
+ ...b1,
2077
+ metadata
2078
+ };
2079
+ };
2080
+ /** Custom guest fields: non-empty text is set, blanks remove the key. */
2081
+ const mergeCustomMetadata = (current, patch) => {
2082
+ const next = { ...current };
2083
+ for (const [key, value] of Object.entries(patch)) {
2084
+ const text = typeof value === "string" ? value.trim() : value;
2085
+ const valid = typeof text !== "string" || isCustomerInfoValueValid(key, text);
2086
+ if (text == null || text === "" || !valid) delete next[key];
2087
+ else next[key] = text;
2088
+ }
2089
+ return Object.keys(next).length > 0 ? next : void 0;
2090
+ };
1978
2091
  //#endregion
1979
2092
  //#region src/logs/limits.ts
1980
2093
  /** Mirrors the API limits so oversized logs are trimmed, not rejected. */
@@ -3687,7 +3800,8 @@ const computeLineFingerprint = (root, lines) => {
3687
3800
  const keys = lines.filter((line) => {
3688
3801
  return line.parent_uid === parentUid;
3689
3802
  }).map((line) => {
3690
- return `${line.id}x${line.quantity}(${childKeys(line.uid)})`;
3803
+ const slot = line.group_id ? `@${line.group_id}` : "";
3804
+ return `${line.id}${slot}x${line.quantity}(${childKeys(line.uid)})`;
3691
3805
  });
3692
3806
  keys.sort((left, right) => left.localeCompare(right));
3693
3807
  return keys.join(",");
@@ -6000,15 +6114,21 @@ var Order = class Order {
6000
6114
  }
6001
6115
  setCustomerDetails(details) {
6002
6116
  if (this.deleted) return;
6117
+ let customer_info = this.draft.customer.customer_info;
6118
+ if (details.customer_info) customer_info = mergeCustomerInfo(customer_info, details.customer_info);
6003
6119
  const hasAddressInfo = details.address_info !== void 0;
6004
6120
  const hasBillingInfo = details.billing_info !== void 0;
6005
6121
  const address_info = !hasAddressInfo ? this.draft.customer.address_info : details.address_info;
6006
6122
  const billing_info = !hasBillingInfo ? this.draft.customer.billing_info : details.billing_info;
6007
- const customer = {
6008
- ...this.draft.customer,
6123
+ const c1 = {
6124
+ customer_info,
6009
6125
  address_info,
6010
6126
  billing_info
6011
6127
  };
6128
+ const customer = {
6129
+ ...this.draft.customer,
6130
+ ...c1
6131
+ };
6012
6132
  this.draft = {
6013
6133
  ...this.draft,
6014
6134
  customer
@@ -6643,7 +6763,8 @@ const selectionKey = (line, kidsOf, cache) => {
6643
6763
  return selectionKey(child, kidsOf, cache);
6644
6764
  });
6645
6765
  nested.sort((left, right) => left.localeCompare(right));
6646
- const key = `${line.id}x${line.quantity}(${nested.join(",")})`;
6766
+ const slot = line.group_id ? `@${line.group_id}` : "";
6767
+ const key = `${line.id}${slot}x${line.quantity}(${nested.join(",")})`;
6647
6768
  cache.set(line.uid, key);
6648
6769
  return key;
6649
6770
  };
@@ -6730,6 +6851,36 @@ const hasRepeatedProductSibling = (lines, rootUid) => {
6730
6851
  };
6731
6852
  return walk(rootUid);
6732
6853
  };
6854
+ /** A line whose product the parent's menu lists in more than one slot, at
6855
+ * any depth. Cart lines carry no group id, so the slot is chosen by
6856
+ * `placeCartLines`; with a single listing the session path is unambiguous. */
6857
+ const hasMultiSlotChild = (lines, rootUid, lookup) => {
6858
+ const kidsOf = /* @__PURE__ */ new Map();
6859
+ for (const line of lines) {
6860
+ if (line.parent_uid == null) continue;
6861
+ const list = kidsOf.get(line.parent_uid) ?? [];
6862
+ list.push(line);
6863
+ kidsOf.set(line.parent_uid, list);
6864
+ }
6865
+ const walk = (line) => {
6866
+ const parent = lookup.productsById[line.id];
6867
+ const groups = parent ? parentGroups(lookup, parent) : [];
6868
+ for (const kid of kidsOf.get(line.uid) ?? []) {
6869
+ const ids = listingsFor(lookup, groups, kid.id).map((row) => slotKey(row.group.id, row.modifierId));
6870
+ if (new Set(ids).size > 1) return true;
6871
+ if (walk(kid)) return true;
6872
+ }
6873
+ return false;
6874
+ };
6875
+ const root = lines.find((line) => line.uid === rootUid);
6876
+ return root ? walk(root) : false;
6877
+ };
6878
+ /** Cart shapes the slot search must place: a repeated sibling product, or
6879
+ * a product listed in several slots. Everything else keeps the session. */
6880
+ const needsSlotPlacement = (lines, rootUid, lookup) => {
6881
+ if (hasRepeatedProductSibling(lines, rootUid)) return true;
6882
+ return hasMultiSlotChild(lines, rootUid, lookup);
6883
+ };
6733
6884
  const noteRules = (state, broken) => {
6734
6885
  if (broken) state.rulesBroken = true;
6735
6886
  };
@@ -6897,13 +7048,21 @@ const greedyAssignment = (plans) => {
6897
7048
  overCapacity
6898
7049
  };
6899
7050
  };
7051
+ /** The groups a line may sit in under this parent: only its declared
7052
+ * `group_id` when it names one, else every group of the parent. A declared id
7053
+ * the parent does not own leaves none, so the line is never placed. */
7054
+ const groupsDeclared = (groups, line) => {
7055
+ if (line.group_id === void 0) return groups;
7056
+ return groups.filter((group) => group.id === line.group_id);
7057
+ };
6900
7058
  const planKids = (lookup, groups, byKey, kids, prefix, state) => {
6901
7059
  const plans = [];
6902
7060
  let previous = "";
6903
7061
  for (const child of kids) {
6904
7062
  if (pathVisits(prefix, child.id)) continue;
6905
7063
  const ordered = lookup.productsById[child.id];
6906
- const listings = listingsFor(lookup, groups, child.id);
7064
+ const scope = groupsDeclared(groups, child);
7065
+ const listings = listingsFor(lookup, scope, child.id);
6907
7066
  if (!ordered || listings.length === 0) continue;
6908
7067
  const listed = [];
6909
7068
  for (const item of listings) {
@@ -7123,14 +7282,17 @@ const emit = (node, parentUid, parentId, locale, out) => {
7123
7282
  type: "PRODUCT",
7124
7283
  uid: randomUUID()
7125
7284
  };
7285
+ const id = node.product.id;
7286
+ const groupPatch = node.group ? { group_id: node.group.id } : {};
7126
7287
  const r2 = {
7127
- id: node.product.id,
7288
+ id,
7128
7289
  parent_uid: parentUid,
7129
7290
  parent_id: parentId
7130
7291
  };
7131
7292
  const r3 = {
7132
7293
  name: displayName(node.product.name, locale),
7133
- quantity: node.quantity
7294
+ quantity: node.quantity,
7295
+ ...groupPatch
7134
7296
  };
7135
7297
  const r4 = {
7136
7298
  redeem: [],
@@ -7321,10 +7483,25 @@ const childrenOf = (lines, parentUid) => {
7321
7483
  if (byShape !== 0) return byShape;
7322
7484
  const bySubtotal = new Big(a.unit_subtotal).cmp(b.unit_subtotal);
7323
7485
  if (bySubtotal !== 0) return bySubtotal;
7324
- return new Big(a.unit_tax_total).cmp(b.unit_tax_total);
7486
+ const byTax = new Big(a.unit_tax_total).cmp(b.unit_tax_total);
7487
+ if (byTax !== 0) return byTax;
7488
+ return (a.group_id ?? "").localeCompare(b.group_id ?? "");
7325
7489
  });
7326
7490
  return children;
7327
7491
  };
7492
+ /** A declared `group_id` that the parent's menu does not back (unknown under
7493
+ * this parent, or it does not hold the product) names the field; a line that
7494
+ * is no modifier of its parent at all keeps the plain code. */
7495
+ const notFoundIssue = (lookup, parent, child) => {
7496
+ const code = "MODIFIER_NOT_FOUND";
7497
+ if (child.group_id === void 0) return { code };
7498
+ if (!matchModifierLine(lookup, parent, child.id)) return { code };
7499
+ return {
7500
+ code,
7501
+ field: "group_id",
7502
+ actual: child.group_id
7503
+ };
7504
+ };
7328
7505
  const collectUnmatchedModifiers = (lines, lookup, parentProductId, parentUid, out) => {
7329
7506
  const parent = lookup.productsById[parentProductId];
7330
7507
  if (!parent) return;
@@ -7336,9 +7513,14 @@ const collectUnmatchedModifiers = (lines, lookup, parentProductId, parentUid, ou
7336
7513
  pushLineIssue(out, child.uid, { code: "VARIANT_BASE_NOT_ORDERABLE" });
7337
7514
  continue;
7338
7515
  }
7339
- const match = matchModifierLine(lookup, parent, child.id);
7516
+ const match = matchModifierLine(...[
7517
+ lookup,
7518
+ parent,
7519
+ child.id,
7520
+ child.group_id
7521
+ ]);
7340
7522
  if (!match) {
7341
- pushLineIssue(out, child.uid, { code: "MODIFIER_NOT_FOUND" });
7523
+ pushLineIssue(out, child.uid, notFoundIssue(lookup, parent, child));
7342
7524
  continue;
7343
7525
  }
7344
7526
  const base = lookup.productsById[match.modifierId];
@@ -7481,6 +7663,13 @@ const compareTrees = (actualLines, expectedLines, actualUid, expectedUid, out) =
7481
7663
  });
7482
7664
  return;
7483
7665
  }
7666
+ const declared = actual.group_id;
7667
+ if (declared !== void 0 && declared !== expected.group_id) pushLineIssue(out, actual.uid, {
7668
+ code: "STRUCTURE_MISMATCH",
7669
+ field: "group_id",
7670
+ expected: expected.group_id,
7671
+ actual: declared
7672
+ });
7484
7673
  if (actual.quantity !== expected.quantity) pushLineIssue(out, actual.uid, {
7485
7674
  code: "QUANTITY_MISMATCH",
7486
7675
  field: "quantity",
@@ -7566,7 +7755,7 @@ const validateProductRoot = (root, lines, lookup, locale, out) => {
7566
7755
  if (!isOnSchedule(product.availability.schedules)) pushLineIssue(out, root.uid, { code: "OUTSIDE_HOURS" });
7567
7756
  collectUnmatchedModifiers(lines, lookup, root.id, root.uid, out);
7568
7757
  let expected;
7569
- if (hasRepeatedProductSibling(lines, root.uid)) {
7758
+ if (needsSlotPlacement(lines, root.uid, lookup)) {
7570
7759
  const placed = placeCartLines(root, lines, lookup, locale);
7571
7760
  if (placed.rulesBroken) pushLineIssue(out, root.uid, { code: "MODIFIER_RULES_INVALID" });
7572
7761
  expected = placed.lines;
@@ -7898,7 +8087,7 @@ const collectOrderLineDiffs = (input) => {
7898
8087
  continue;
7899
8088
  }
7900
8089
  try {
7901
- if (hasRepeatedProductSibling(input.lines, root.uid)) {
8090
+ if (needsSlotPlacement(input.lines, root.uid, lookup)) {
7902
8091
  const placed = placeCartLines(root, input.lines, lookup, locale);
7903
8092
  const next = placed.rulesBroken ? null : placed.lines;
7904
8093
  diffs.push({
@@ -9452,6 +9641,13 @@ const DEFAULT_BILLING_FIELDS = {
9452
9641
  tax_id: "required",
9453
9642
  tax_id_type: "required"
9454
9643
  };
9644
+ /** Guest contact fields → `customer.customer_info` (anonymous orders). */
9645
+ const DEFAULT_CUSTOMER_FIELDS = {
9646
+ email: "required",
9647
+ first_name: "required",
9648
+ last_name: "required",
9649
+ phone: "required"
9650
+ };
9455
9651
  const getCheckoutFieldValue = (info, key) => {
9456
9652
  if (!info) return void 0;
9457
9653
  const top = info[key];
@@ -9481,7 +9677,8 @@ const defaultCheckoutPreferences = {
9481
9677
  billing: {
9482
9678
  fields: DEFAULT_BILLING_FIELDS,
9483
9679
  rules: [fallbackRequired]
9484
- }
9680
+ },
9681
+ anonymous_customer: { fields: DEFAULT_CUSTOMER_FIELDS }
9485
9682
  };
9486
9683
  const factsFromOrder = (order) => {
9487
9684
  const brand_id = order.brand_id ?? void 0;
@@ -9536,13 +9733,15 @@ const resolveCheckout = (order, options) => {
9536
9733
  let shipping = "hidden";
9537
9734
  if (ships) shipping = resolveSection(checkout.shipping, engine);
9538
9735
  const billing = resolveSection(checkout.billing, engine);
9539
- const shippingFields = checkout.shipping.fields ?? DEFAULT_SHIPPING_FIELDS;
9540
- const billingFields = checkout.billing.fields ?? DEFAULT_BILLING_FIELDS;
9736
+ const f1 = {
9737
+ shippingFields: checkout.shipping.fields ?? DEFAULT_SHIPPING_FIELDS,
9738
+ billingFields: checkout.billing.fields ?? DEFAULT_BILLING_FIELDS,
9739
+ customerFields: checkout.anonymous_customer?.fields ?? DEFAULT_CUSTOMER_FIELDS
9740
+ };
9541
9741
  return {
9542
9742
  shipping,
9543
9743
  billing,
9544
- shippingFields,
9545
- billingFields
9744
+ ...f1
9546
9745
  };
9547
9746
  };
9548
9747
  //#endregion
@@ -9881,4 +10080,4 @@ const cardPreviewGroups = (value) => {
9881
10080
  return groups;
9882
10081
  };
9883
10082
  //#endregion
9884
- export { CARD_BRANDS, DEFAULT_RECOMMENDATION_TYPES, ID_HASH_FORMATS, Order, OrderPaymentRequiredError, OrderSyncPendingError, OrderSyncRejectedError, OrderValidationError, RecurringDateCron, RulesEngine, StorefrontHttpError, addLines, auth, buildCustomerSnapshot, cancelTypeOf, capRedeems, cappedRedeems, cardBrandSpec, cardDigits, cardNumberGaps, cardNumberMaxLength, cardPreviewGroups, caretAfterDigits, collectOrderLineDiffs, computeStartingPrice, countCartProducts, coveredAmount, createDeunaWidgetApi, createSession, currentNodes, cvcMaxLength, defaultConditionError, detectCardBrand, fitRedeems, flattenMenuCategories, flushLogs, formatCardNumber, formatCvc, formatExpiry, fromOrderLines, genId, genOrderId, getLogContext, getScheduleWindow, indexMenu, isCardNumberComplete, isCardNumberDefinitelyInvalid, isCheckoutFieldFilled, isCvcComplete, isDeviceCharged, isDeviceNotLinked, isExpiryComplete, isGenId, isKioskUnauthorized, isMilestoneStepDone, isOnSchedule, isOrderPaymentRequiredError, isOrderValidationError, isPosDeviceNotLinked, isPosUnauthorized, isRealPaymentMethod, isStorefrontNotFound, isVariantBase, linePayableTotal, listMenusStream, localizePosError, log, logError, luhnCheck, matchAbsoluteDateOperator, matchBooleanOperator, matchCurrencyOperator, matchEnumOperator, matchNumberOperator, matchRecurringDateOperator, matchRelativeDateOperator, matchStringOperator, milestoneSteps, milestoneTrack, nestMenuCategories, normalizeCardholderName, orderPayableTotal, orderRedeemedTotal, orderRedeems, parseExpiry, paymentMatchesClientConditions, rebuildRootGroups, recomputeTotals, redeemedTotal, redemptionCap, redemptionSteps, remainingAmount, removeLine, replaceLine, resolveCheckout, resolveModifierImageUrl, reversePayment, runPaymentAttempt, selectablePayments, serializeError, services, setLineQuantity, storefrontJson, toOrderLines, toPaymentCardBrand, validateCardNumber, validateCardholderName, validateCvc, validateExpiry, validateOrderAgainstMenu, verifyWebhookSignature, visitedStates };
10083
+ export { CARD_BRANDS, CUSTOMER_INFO_PLACEHOLDERS, DEFAULT_RECOMMENDATION_TYPES, ID_HASH_FORMATS, Order, OrderPaymentRequiredError, OrderSyncPendingError, OrderSyncRejectedError, OrderValidationError, RecurringDateCron, RulesEngine, StorefrontHttpError, addLines, auth, buildCustomerSnapshot, cancelTypeOf, capRedeems, cappedRedeems, cardBrandSpec, cardDigits, cardNumberGaps, cardNumberMaxLength, cardPreviewGroups, caretAfterDigits, collectOrderLineDiffs, computeStartingPrice, countCartProducts, coveredAmount, createDeunaWidgetApi, createSession, currentNodes, cvcMaxLength, defaultConditionError, detectCardBrand, fitRedeems, flattenMenuCategories, flushLogs, formatCardNumber, formatCvc, formatExpiry, fromOrderLines, genId, genOrderId, getLogContext, getScheduleWindow, indexMenu, isCardNumberComplete, isCardNumberDefinitelyInvalid, isCheckoutFieldFilled, isCustomerInfoFieldFilled, isCustomerInfoPlaceholder, isCustomerInfoValueValid, isCvcComplete, isDeviceCharged, isDeviceNotLinked, isExpiryComplete, isGenId, isKioskUnauthorized, isMilestoneStepDone, isOnSchedule, isOrderPaymentRequiredError, isOrderValidationError, isPosDeviceNotLinked, isPosUnauthorized, isRealPaymentMethod, isStorefrontNotFound, isVariantBase, linePayableTotal, listMenusStream, localizePosError, log, logError, luhnCheck, matchAbsoluteDateOperator, matchBooleanOperator, matchCurrencyOperator, matchEnumOperator, matchNumberOperator, matchRecurringDateOperator, matchRelativeDateOperator, matchStringOperator, milestoneSteps, milestoneTrack, nestMenuCategories, normalizeCardholderName, normalizeCustomerPhone, orderPayableTotal, orderRedeemedTotal, orderRedeems, parseExpiry, paymentMatchesClientConditions, rebuildRootGroups, recomputeTotals, redeemedTotal, redemptionCap, redemptionSteps, remainingAmount, removeLine, replaceLine, resolveCheckout, resolveModifierImageUrl, reversePayment, runPaymentAttempt, selectablePayments, serializeError, services, setLineQuantity, storefrontJson, 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.12.4",
3
+ "version": "0.13.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.12.4",
24
- "@fire-spark/types": "^0.12.4"
23
+ "@fire-spark/payments": "^0.13.0",
24
+ "@fire-spark/types": "^0.13.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "tsdown": "^0.22.14",