@fire-spark/core 0.15.3 → 0.16.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +190 -7
  2. package/dist/index.js +1385 -291
  3. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -1889,6 +1889,34 @@ const resolveBaseUrl = (baseUrl) => {
1889
1889
  return (baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
1890
1890
  };
1891
1891
  //#endregion
1892
+ //#region src/auth/DelegatedSessionExpiredError.ts
1893
+ /**
1894
+ * A delegated customer session is still active, but its access token was
1895
+ * rejected or has expired. The anonymous subject token is not exchanged.
1896
+ */
1897
+ var DelegatedSessionExpiredError = class extends Error {
1898
+ code = "DELEGATED_SESSION_EXPIRED";
1899
+ constructor() {
1900
+ super("Delegated customer session expired");
1901
+ this.name = "DelegatedSessionExpiredError";
1902
+ }
1903
+ };
1904
+ const isDelegatedSessionExpiredError = (error) => {
1905
+ return error instanceof DelegatedSessionExpiredError;
1906
+ };
1907
+ //#endregion
1908
+ //#region src/auth/OrderAuthPinnedError.ts
1909
+ /**
1910
+ * `Order` was built with an explicit `accessToken`. It cannot follow
1911
+ * `auth.switchCustomer`, which swaps the shared auth session.
1912
+ */
1913
+ var OrderAuthPinnedError = class extends Error {
1914
+ constructor() {
1915
+ super("This order uses a fixed accessToken and cannot follow auth.switchCustomer");
1916
+ this.name = "OrderAuthPinnedError";
1917
+ }
1918
+ };
1919
+ //#endregion
1892
1920
  //#region src/auth/exchange.ts
1893
1921
  const exchangeSubjectToken = async (subjectToken, options = {}) => {
1894
1922
  const base = resolveBaseUrl(options.baseUrl);
@@ -1910,20 +1938,105 @@ const exchangeSubjectToken = async (subjectToken, options = {}) => {
1910
1938
  if (!json.data?.access_token) throw new Error("Token exchange response missing access_token");
1911
1939
  return json.data;
1912
1940
  };
1941
+ const GRANT = "urn:ietf:params:oauth:grant-type:token-exchange";
1942
+ const switchCustomerToken = async (body, options = {}) => {
1943
+ const base = resolveBaseUrl(options.baseUrl);
1944
+ const fetchImpl = options.fetchImpl ?? fetch;
1945
+ const url = `${base}/api/storefront/v1/oauth/exchange`;
1946
+ const payload = {
1947
+ grant_type: GRANT,
1948
+ ...body
1949
+ };
1950
+ const response = await fetchImpl(url, {
1951
+ method: "POST",
1952
+ headers: { "Content-Type": "application/json" },
1953
+ body: JSON.stringify(payload)
1954
+ });
1955
+ if (!response.ok) {
1956
+ const message = await response.text();
1957
+ throw new Error(message || `Token exchange failed (${response.status})`);
1958
+ }
1959
+ const data = (await response.json()).data;
1960
+ if (!data?.access_token || !data.customer_id || !data.order) throw new Error("Token exchange response missing access_token");
1961
+ const order = data.order;
1962
+ return {
1963
+ ...data,
1964
+ token_type: "Bearer",
1965
+ order
1966
+ };
1967
+ };
1913
1968
  //#endregion
1914
1969
  //#region src/auth/auth.ts
1915
1970
  const DEFAULT_TOKEN_SKEW_MS = 3e4;
1971
+ const ID_TOKEN = "urn:ietf:params:oauth:token-type:id_token";
1972
+ const PROOF = "urn:fire-spark:params:oauth:token-type:customer-switch-proof";
1973
+ const ACCESS_TOKEN = "urn:ietf:params:oauth:token-type:access_token";
1916
1974
  let state = null;
1975
+ const signatureOf = (options) => {
1976
+ return `${options.getAccessToken ? "direct" : "subject"}\n${resolveBaseUrl(options.baseUrl)}\n${options.audience ?? ""}`;
1977
+ };
1978
+ const decodeJwtPart = (token) => {
1979
+ const part = token.split(".")[1];
1980
+ if (!part) return null;
1981
+ try {
1982
+ const b64 = part.replace(/-/g, "+").replace(/_/g, "/");
1983
+ const pad = b64 + "=".repeat((4 - b64.length % 4) % 4);
1984
+ return JSON.parse(atob(pad));
1985
+ } catch {
1986
+ return null;
1987
+ }
1988
+ };
1989
+ const decodeJwtSub = (token) => {
1990
+ const payload = decodeJwtPart(token);
1991
+ if (typeof payload?.sub !== "string" || !payload.sub) return null;
1992
+ return payload.sub;
1993
+ };
1994
+ const emit$1 = (customerId) => {
1995
+ if (!state) return;
1996
+ for (const listener of state.listeners) listener(customerId);
1997
+ };
1998
+ const dropDelegated = () => {
1999
+ if (!state?.delegated) return false;
2000
+ state.delegated = null;
2001
+ state.accessToken = null;
2002
+ state.expiresAt = 0;
2003
+ return true;
2004
+ };
1917
2005
  const configure = (options) => {
2006
+ const signature = signatureOf(options);
2007
+ const current = state;
2008
+ const sameSig = current?.signature === signature;
2009
+ const keep = Boolean(current && sameSig && current.delegated);
2010
+ const listeners = current?.listeners ?? /* @__PURE__ */ new Set();
2011
+ const delegated = keep && current ? current.delegated : null;
2012
+ const accessToken = keep && current ? current.accessToken : null;
2013
+ const expiresAt = keep && current ? current.expiresAt : 0;
2014
+ const getSubjectToken = options.getSubjectToken ?? null;
2015
+ const getDirectToken = options.getAccessToken ?? null;
2016
+ const baseUrl = resolveBaseUrl(options.baseUrl);
2017
+ const fetchImpl = options.fetchImpl ?? fetch;
2018
+ const tokenSkewMs = options.tokenSkewMs ?? DEFAULT_TOKEN_SKEW_MS;
2019
+ const b1 = {
2020
+ signature,
2021
+ getSubjectToken,
2022
+ getDirectToken,
2023
+ audience: options.audience
2024
+ };
2025
+ const b2 = {
2026
+ baseUrl,
2027
+ fetchImpl,
2028
+ tokenSkewMs,
2029
+ accessToken
2030
+ };
2031
+ const b3 = {
2032
+ expiresAt,
2033
+ delegated,
2034
+ listeners
2035
+ };
1918
2036
  state = {
1919
- getSubjectToken: options.getSubjectToken ?? null,
1920
- getDirectToken: options.getAccessToken ?? null,
1921
- audience: options.audience,
1922
- baseUrl: resolveBaseUrl(options.baseUrl),
1923
- fetchImpl: options.fetchImpl ?? fetch,
1924
- tokenSkewMs: options.tokenSkewMs ?? DEFAULT_TOKEN_SKEW_MS,
1925
- accessToken: null,
1926
- expiresAt: 0
2037
+ ...b1,
2038
+ ...b2,
2039
+ ...b3
1927
2040
  };
1928
2041
  };
1929
2042
  const getBaseUrl = () => {
@@ -1932,8 +2045,53 @@ const getBaseUrl = () => {
1932
2045
  };
1933
2046
  const clear = () => {
1934
2047
  if (!state) return;
2048
+ const had = dropDelegated();
1935
2049
  state.accessToken = null;
1936
2050
  state.expiresAt = 0;
2051
+ if (had) emit$1(null);
2052
+ };
2053
+ const isDelegatedSession = () => {
2054
+ return state?.delegated !== null && state?.delegated !== void 0;
2055
+ };
2056
+ const onCustomerChange = (listener) => {
2057
+ if (!state) {
2058
+ const baseUrl = resolveBaseUrl();
2059
+ const c1 = {
2060
+ signature: "",
2061
+ getSubjectToken: null,
2062
+ getDirectToken: null
2063
+ };
2064
+ const c2 = {
2065
+ audience: void 0,
2066
+ baseUrl,
2067
+ fetchImpl: fetch
2068
+ };
2069
+ const c3 = {
2070
+ tokenSkewMs: DEFAULT_TOKEN_SKEW_MS,
2071
+ accessToken: null
2072
+ };
2073
+ const c4 = {
2074
+ expiresAt: 0,
2075
+ delegated: null,
2076
+ listeners: /* @__PURE__ */ new Set()
2077
+ };
2078
+ state = {
2079
+ ...c1,
2080
+ ...c2,
2081
+ ...c3,
2082
+ ...c4
2083
+ };
2084
+ }
2085
+ state.listeners.add(listener);
2086
+ return () => {
2087
+ state?.listeners.delete(listener);
2088
+ };
2089
+ };
2090
+ /** Drops the delegated token when that order is no longer OPEN. */
2091
+ const releaseDelegatedOrder = (orderId) => {
2092
+ if (!state?.delegated || state.delegated.orderId !== orderId) return;
2093
+ dropDelegated();
2094
+ emit$1(null);
1937
2095
  };
1938
2096
  const issueToken = async (current) => {
1939
2097
  if (current.getDirectToken) return current.getDirectToken();
@@ -1944,15 +2102,100 @@ const issueToken = async (current) => {
1944
2102
  audience: current.audience
1945
2103
  });
1946
2104
  };
2105
+ const subjectRotated = async (current) => {
2106
+ const actorSub = current.delegated?.actorSub;
2107
+ if (!actorSub || !current.getSubjectToken) return false;
2108
+ const raw = await current.getSubjectToken();
2109
+ const sub = decodeJwtSub(raw);
2110
+ return Boolean(sub && sub !== actorSub);
2111
+ };
1947
2112
  const getAccessToken = async () => {
1948
2113
  if (!state) throw new Error("auth.configure() must be called before getAccessToken()");
1949
2114
  const now = Date.now();
2115
+ let rotated = false;
2116
+ if (state.delegated) {
2117
+ rotated = await subjectRotated(state);
2118
+ if (rotated) dropDelegated();
2119
+ else if (state.delegated.expiresAt > now + state.tokenSkewMs) return state.delegated.accessToken;
2120
+ else throw new DelegatedSessionExpiredError();
2121
+ }
1950
2122
  if (state.accessToken && state.expiresAt > now + state.tokenSkewMs) return state.accessToken;
1951
2123
  const issued = await issueToken(state);
1952
2124
  state.accessToken = issued.access_token;
1953
2125
  state.expiresAt = now + issued.expires_in * 1e3;
2126
+ if (rotated) emit$1(null);
1954
2127
  return state.accessToken;
1955
2128
  };
2129
+ const orderIdFromProof = (token) => {
2130
+ const payload = decodeJwtPart(token);
2131
+ if (typeof payload?.order_id !== "string" || !payload.order_id) throw new Error("Customer switch proof is invalid");
2132
+ return payload.order_id;
2133
+ };
2134
+ const isIdToken = (input) => {
2135
+ if (typeof input === "string" || !("type" in input)) return false;
2136
+ return input.type === "id_token";
2137
+ };
2138
+ const proofOf = (input) => {
2139
+ if (typeof input === "string") return {
2140
+ token: input,
2141
+ orderId: orderIdFromProof(input)
2142
+ };
2143
+ if (isIdToken(input)) return null;
2144
+ const token = input.customer_switch_proof;
2145
+ if (!token) throw new Error("Masivo verify did not return a customer switch proof");
2146
+ return {
2147
+ token,
2148
+ orderId: orderIdFromProof(token)
2149
+ };
2150
+ };
2151
+ const switchCustomer = async (input) => {
2152
+ if (!state) throw new Error("auth.configure() must be called before switchCustomer()");
2153
+ const actorToken = await getAccessToken();
2154
+ let actorSub = null;
2155
+ if (state.getSubjectToken) {
2156
+ const raw = await state.getSubjectToken();
2157
+ actorSub = decodeJwtSub(raw);
2158
+ }
2159
+ const proof = proofOf(input);
2160
+ const idToken = isIdToken(input) ? input : null;
2161
+ const subject_token = proof ? proof.token : idToken?.token;
2162
+ const orderId = proof ? proof.orderId : idToken?.orderId;
2163
+ if (!subject_token || !orderId) throw new Error("Customer switch proof is invalid");
2164
+ const subject_token_type = proof ? PROOF : ID_TOKEN;
2165
+ const b1 = {
2166
+ actor_token: actorToken,
2167
+ actor_token_type: ACCESS_TOKEN
2168
+ };
2169
+ const b2 = {
2170
+ subject_token,
2171
+ subject_token_type,
2172
+ order_id: orderId
2173
+ };
2174
+ const result = await switchCustomerToken({
2175
+ ...b1,
2176
+ ...b2
2177
+ }, {
2178
+ baseUrl: state.baseUrl,
2179
+ fetchImpl: state.fetchImpl
2180
+ });
2181
+ const expiresAt = Date.now() + result.expires_in * 1e3;
2182
+ const d1 = {
2183
+ accessToken: result.access_token,
2184
+ expiresAt
2185
+ };
2186
+ const d2 = {
2187
+ actorSub,
2188
+ orderId
2189
+ };
2190
+ state.delegated = {
2191
+ ...d1,
2192
+ ...d2
2193
+ };
2194
+ state.accessToken = result.access_token;
2195
+ state.expiresAt = expiresAt;
2196
+ emit$1(result.customer_id);
2197
+ return result;
2198
+ };
1956
2199
  //#endregion
1957
2200
  //#region src/auth/index.ts
1958
2201
  const auth = {
@@ -1961,7 +2204,11 @@ const auth = {
1961
2204
  getBaseUrl,
1962
2205
  clear,
1963
2206
  exchangeSubjectToken,
1964
- resolveBaseUrl
2207
+ resolveBaseUrl,
2208
+ switchCustomer,
2209
+ isDelegatedSession,
2210
+ onCustomerChange,
2211
+ releaseDelegatedOrder
1965
2212
  };
1966
2213
  //#endregion
1967
2214
  //#region src/order/customer.ts
@@ -2239,6 +2486,7 @@ const resolveAccessToken = async (options) => {
2239
2486
  const withAuthRetry = async (options, run) => {
2240
2487
  const response = await run(await resolveAccessToken(options));
2241
2488
  if (response.status !== 401 || options.accessToken) return response;
2489
+ if (auth.isDelegatedSession()) throw new DelegatedSessionExpiredError();
2242
2490
  auth.clear();
2243
2491
  return run(await resolveAccessToken(options));
2244
2492
  };
@@ -2358,11 +2606,11 @@ const getCmsBlock = (name, options) => {
2358
2606
  };
2359
2607
  //#endregion
2360
2608
  //#region src/services/customers.ts
2361
- const customerPath = (id) => {
2609
+ const customerPath$1 = (id) => {
2362
2610
  return `/api/storefront/v1/customers/${encodeURIComponent(id)}`;
2363
2611
  };
2364
2612
  const getCustomer = (id, options) => {
2365
- return storefrontJson(customerPath(id), {}, options);
2613
+ return storefrontJson(customerPath$1(id), {}, options);
2366
2614
  };
2367
2615
  const createCustomer = (body, options) => {
2368
2616
  return storefrontJson("/api/storefront/v1/customers", {
@@ -2371,19 +2619,19 @@ const createCustomer = (body, options) => {
2371
2619
  }, options);
2372
2620
  };
2373
2621
  const identifyAnonymousCustomer = (id, body, options) => {
2374
- return storefrontJson(`${customerPath(id)}/identify/anonymous`, {
2622
+ return storefrontJson(`${customerPath$1(id)}/identify/anonymous`, {
2375
2623
  method: "PATCH",
2376
2624
  body: JSON.stringify(body)
2377
2625
  }, options);
2378
2626
  };
2379
2627
  const updateCustomer = (id, body, options) => {
2380
- return storefrontJson(customerPath(id), {
2628
+ return storefrontJson(customerPath$1(id), {
2381
2629
  method: "PATCH",
2382
2630
  body: JSON.stringify(body)
2383
2631
  }, options);
2384
2632
  };
2385
2633
  const deleteCustomer = (id, options) => {
2386
- return storefrontVoid(customerPath(id), { method: "DELETE" }, options);
2634
+ return storefrontVoid(customerPath$1(id), { method: "DELETE" }, options);
2387
2635
  };
2388
2636
  //#endregion
2389
2637
  //#region src/services/fulfillment.ts
@@ -2570,6 +2818,7 @@ const VERIFY_PATH = "/api/storefront/v1/masivo/totp/verify";
2570
2818
  const CREATE_PATH = "/api/storefront/v1/masivo/totp";
2571
2819
  const verifyMasivoTotp = (input, options) => {
2572
2820
  const params = new URLSearchParams({ code: input.code });
2821
+ if (input.order_id) params.set("order_id", input.order_id);
2573
2822
  return storefrontJson(`${VERIFY_PATH}?${params.toString()}`, {}, options);
2574
2823
  };
2575
2824
  const createMasivoTotp = (input, options) => {
@@ -2756,6 +3005,36 @@ const updateOrder = (id, body, options) => {
2756
3005
  options
2757
3006
  ]);
2758
3007
  };
3008
+ const customerPath = (id) => `${orderPath(id)}/customer`;
3009
+ /**
3010
+ * `PUT /storefront/v1/orders/:id/customer`. Copies that customer onto the
3011
+ * open order. `409` `ORDER_NOT_OPEN` when the order is not `OPEN`. `404`
3012
+ * `CUSTOMER_NOT_FOUND` when the customer does not exist. `409`
3013
+ * `ORDER_OPEN_SCOPE_TAKEN` when they already have an open cart in scope.
3014
+ */
3015
+ const setOrderCustomer = (id, body, options) => {
3016
+ const init = {
3017
+ method: "PUT",
3018
+ body: JSON.stringify(body)
3019
+ };
3020
+ return storefrontJson(...[
3021
+ customerPath(id),
3022
+ init,
3023
+ options
3024
+ ]);
3025
+ };
3026
+ /**
3027
+ * `DELETE /storefront/v1/orders/:id/customer`. Makes the open order
3028
+ * anonymous and clears the customer snapshot. `customer_id` stays.
3029
+ * `409` `ORDER_NOT_OPEN` when the order is not `OPEN`.
3030
+ */
3031
+ const clearOrderCustomer = (id, options) => {
3032
+ return storefrontJson(...[
3033
+ customerPath(id),
3034
+ { method: "DELETE" },
3035
+ options
3036
+ ]);
3037
+ };
2759
3038
  const postCancel = (path, options, body) => {
2760
3039
  const method = "POST";
2761
3040
  if (!body) return storefrontJson(path, { method }, options);
@@ -3255,6 +3534,8 @@ const services = {
3255
3534
  post: createOrder,
3256
3535
  get: getOrder,
3257
3536
  patch: updateOrder,
3537
+ setCustomer: setOrderCustomer,
3538
+ clearCustomer: clearOrderCustomer,
3258
3539
  cancel: cancelOrder,
3259
3540
  refund: refundOrder,
3260
3541
  reorder: reorderOrder,
@@ -3476,6 +3757,22 @@ const isGenId = (value) => {
3476
3757
  return HASH_RUN.test(value);
3477
3758
  };
3478
3759
  //#endregion
3760
+ //#region src/order/OrderNotOpenError.ts
3761
+ /**
3762
+ * The order is not `OPEN` (`CLOSED`, `COMPLETED`, `VOIDED`, `REVERSED`,
3763
+ * `REFUNDED`, or legacy `CANCELLED`). Same code as the storefront API's
3764
+ * `409`, so callers map both the same way.
3765
+ */
3766
+ var OrderNotOpenError = class extends StorefrontHttpError {
3767
+ constructor(status = 409, details = null) {
3768
+ super("Order is not open", status, "ORDER_NOT_OPEN", details ?? { code: "ORDER_NOT_OPEN" });
3769
+ this.name = "OrderNotOpenError";
3770
+ }
3771
+ };
3772
+ const isOrderNotOpenError = (error) => {
3773
+ return error instanceof OrderNotOpenError;
3774
+ };
3775
+ //#endregion
3479
3776
  //#region src/order/OrderPaymentRequiredError.ts
3480
3777
  /**
3481
3778
  * `placeOrder` has no real tender (no `payment`, no `setTenders`, no
@@ -3574,6 +3871,7 @@ const withExponentialRetry = async (run, options = {}) => {
3574
3871
  return await run();
3575
3872
  } catch (error) {
3576
3873
  lastError = error;
3874
+ if ((error.code ?? "").startsWith("SCHEDULED_AT_")) throw error;
3577
3875
  if (attempt === extraAttempts) break;
3578
3876
  await sleep(baseDelayMs * 2 ** attempt);
3579
3877
  }
@@ -3599,6 +3897,24 @@ var OrderSyncRejectedError = class extends StorefrontHttpError {
3599
3897
  this.name = "OrderSyncRejectedError";
3600
3898
  }
3601
3899
  };
3900
+ const sameSlot = (left, right) => {
3901
+ if (left === right) return true;
3902
+ if (!left || !right) return false;
3903
+ return Date.parse(left) === Date.parse(right);
3904
+ };
3905
+ const SCHEDULED_AT_ERROR_CODES = [
3906
+ "ORDER_NOT_OPEN",
3907
+ "SCHEDULED_AT_IN_PAST",
3908
+ "SCHEDULED_AT_BEFORE_LEAD",
3909
+ "SCHEDULED_AT_BEYOND_HORIZON",
3910
+ "SCHEDULED_AT_INVALID_SLOT",
3911
+ "SCHEDULED_AT_STORE_CLOSED"
3912
+ ];
3913
+ const isScheduledAtError = (error) => {
3914
+ if (!(error instanceof StorefrontHttpError)) return false;
3915
+ const code = error.code ?? "";
3916
+ return SCHEDULED_AT_ERROR_CODES.some((item) => item === code);
3917
+ };
3602
3918
  const createFlushController = (runtime) => {
3603
3919
  let timer = null;
3604
3920
  let inFlight = false;
@@ -3613,7 +3929,7 @@ const createFlushController = (runtime) => {
3613
3929
  pending = false;
3614
3930
  rejected = null;
3615
3931
  };
3616
- const patchBody = (order) => {
3932
+ const patchBody = (order, synced) => {
3617
3933
  const b1 = {
3618
3934
  store_id: order.store_id,
3619
3935
  channel_id: order.channel_id
@@ -3632,13 +3948,17 @@ const createFlushController = (runtime) => {
3632
3948
  redeem: order.redeem
3633
3949
  };
3634
3950
  const b6 = { redemption_steps: order.redemption_steps };
3951
+ const nextSlot = order.scheduled_at ?? null;
3952
+ const prevSlot = synced.scheduled_at ?? null;
3953
+ const slot = sameSlot(nextSlot, prevSlot) ? {} : { scheduled_at: nextSlot };
3635
3954
  return {
3636
3955
  ...b1,
3637
3956
  ...b2,
3638
3957
  ...b3,
3639
3958
  ...b4,
3640
3959
  ...b5,
3641
- ...b6
3960
+ ...b6,
3961
+ ...slot
3642
3962
  };
3643
3963
  };
3644
3964
  const insertBody = (order) => {
@@ -3662,6 +3982,7 @@ const createFlushController = (runtime) => {
3662
3982
  };
3663
3983
  const b7 = { redeem: order.redeem };
3664
3984
  const b8 = { redemption_steps: order.redemption_steps };
3985
+ const slot = order.scheduled_at ? { scheduled_at: order.scheduled_at } : {};
3665
3986
  return {
3666
3987
  ...b1,
3667
3988
  ...b2,
@@ -3670,7 +3991,8 @@ const createFlushController = (runtime) => {
3670
3991
  ...b5,
3671
3992
  ...b6,
3672
3993
  ...b7,
3673
- ...b8
3994
+ ...b8,
3995
+ ...slot
3674
3996
  };
3675
3997
  };
3676
3998
  const flushNow = () => {
@@ -3723,9 +4045,10 @@ const createFlushController = (runtime) => {
3723
4045
  try {
3724
4046
  const updated = await withExponentialRetry(() => {
3725
4047
  if (!wasPersisted) return services.orders.post(insertBody(payload), runtime.http);
4048
+ const body = patchBody(payload, batchBase);
3726
4049
  const p1 = [
3727
4050
  payload.id,
3728
- patchBody(payload),
4051
+ body,
3729
4052
  runtime.http
3730
4053
  ];
3731
4054
  return services.orders.patch(...p1);
@@ -5112,51 +5435,65 @@ const DAY_OF_WEEK = [
5112
5435
  "friday",
5113
5436
  "saturday"
5114
5437
  ];
5115
- const timeToMinutes = (time) => {
5438
+ const timeToMinutes$1 = (time) => {
5116
5439
  const [hours = "0", minutes = "0"] = time.split(":");
5117
5440
  return Number(hours) * 60 + Number(minutes);
5118
5441
  };
5119
- const hasSpecificDates = (period) => {
5442
+ const hasSpecificDates$1 = (period) => {
5120
5443
  return period.start_date != null && period.end_date != null;
5121
5444
  };
5122
- const periodCoversDate = (period, now) => {
5123
- if (!hasSpecificDates(period)) return false;
5445
+ const periodCoversDate$1 = (period, now) => {
5446
+ if (!hasSpecificDates$1(period)) return false;
5124
5447
  const nowMs = now.getTime();
5125
5448
  const startMs = Date.parse(period.start_date);
5126
5449
  const endMs = Date.parse(period.end_date);
5127
5450
  if (Number.isNaN(startMs) || Number.isNaN(endMs)) return false;
5128
5451
  return startMs <= nowMs && nowMs <= endMs;
5129
5452
  };
5130
- const timeMatches = (period, now) => {
5453
+ /**
5454
+ * `end <= start` is an overnight or full-day window on the day it starts:
5455
+ * 18:00–02:00 matches from 18:00 onward, 10:00–00:00 until midnight,
5456
+ * 00:00–00:00 the whole day. The next morning is `isWrapTail`.
5457
+ */
5458
+ const timeMatches$1 = (period, now) => {
5131
5459
  if (!period.start_time || !period.end_time) return false;
5132
5460
  const nowMinutes = now.getHours() * 60 + now.getMinutes();
5133
- const start = timeToMinutes(period.start_time);
5134
- const end = timeToMinutes(period.end_time);
5461
+ const start = timeToMinutes$1(period.start_time);
5462
+ const end = timeToMinutes$1(period.end_time);
5463
+ if (end <= start) return nowMinutes >= start;
5135
5464
  return nowMinutes >= start && nowMinutes < end;
5136
5465
  };
5137
- const isClosedAt = (period, now) => {
5466
+ const isClosedAt$1 = (period, now) => {
5138
5467
  if (!period.closed) return false;
5139
5468
  if (!period.start_time || !period.end_time) return true;
5140
- return timeMatches(period, now);
5469
+ return timeMatches$1(period, now);
5141
5470
  };
5142
- const isOpenAt = (period, now) => {
5471
+ const isOpenAt$1 = (period, now) => {
5143
5472
  if (period.closed) return false;
5144
- return timeMatches(period, now);
5473
+ return timeMatches$1(period, now);
5145
5474
  };
5146
- const isPeriodsActiveAt = (periods, now) => {
5147
- return getOpenPeriodAt(periods, now) != null;
5475
+ const isPeriodsActiveAt = (periods, now, span = now) => {
5476
+ return getOpenPeriodAt$1(periods, now, span) != null;
5148
5477
  };
5149
- const getOpenPeriodAt = (periods, now) => {
5478
+ const getOpenPeriodAt$1 = (periods, now, span = now) => {
5150
5479
  if (periods.length === 0) return null;
5151
5480
  const coveringSpecific = periods.filter((period) => {
5152
- return periodCoversDate(period, now);
5481
+ return periodCoversDate$1(period, span);
5153
5482
  });
5154
- const fallback = periods.filter((period) => !hasSpecificDates(period));
5483
+ const fallback = periods.filter((period) => !hasSpecificDates$1(period));
5155
5484
  const specificOpens = coveringSpecific.filter((period) => !period.closed);
5156
- if (coveringSpecific.filter((period) => period.closed).some((period) => isClosedAt(period, now))) return null;
5157
- if (specificOpens.length > 0) return specificOpens.find((period) => isOpenAt(period, now)) ?? null;
5158
- if (fallback.some((period) => isClosedAt(period, now))) return null;
5159
- return fallback.filter((period) => !period.closed).find((period) => isOpenAt(period, now)) ?? null;
5485
+ if (coveringSpecific.filter((period) => period.closed).some((period) => isClosedAt$1(period, now))) return null;
5486
+ if (specificOpens.length > 0) return specificOpens.find((period) => isOpenAt$1(period, now)) ?? null;
5487
+ if (fallback.some((period) => isClosedAt$1(period, now))) return null;
5488
+ return fallback.filter((period) => !period.closed).find((period) => isOpenAt$1(period, now)) ?? null;
5489
+ };
5490
+ /** Next-morning slice of a window whose end is before or at its start. */
5491
+ const isWrapTail = (period, nowMinutes) => {
5492
+ if (!period.start_time || !period.end_time) return false;
5493
+ const start = timeToMinutes$1(period.start_time);
5494
+ const end = timeToMinutes$1(period.end_time);
5495
+ if (end > start || end === 0) return false;
5496
+ return nowMinutes < end;
5160
5497
  };
5161
5498
  const atTimeOnDate = (day, time) => {
5162
5499
  const [hours = "0", minutes = "0"] = time.split(":");
@@ -5165,80 +5502,213 @@ const atTimeOnDate = (day, time) => {
5165
5502
  return next;
5166
5503
  };
5167
5504
  const isWithinPeriodDates = (period, instantMs) => {
5168
- if (!hasSpecificDates(period)) return true;
5505
+ if (!hasSpecificDates$1(period)) return true;
5169
5506
  const startMs = Date.parse(period.start_date);
5170
5507
  const endMs = Date.parse(period.end_date);
5171
5508
  if (Number.isNaN(startMs) || Number.isNaN(endMs)) return false;
5172
5509
  return startMs <= instantMs && instantMs <= endMs;
5173
5510
  };
5174
- const dayPeriods = (schedule, instant) => {
5175
- const day = DAY_OF_WEEK[instant.getDay()];
5511
+ const previousWeekday = (now) => {
5512
+ return DAY_OF_WEEK[(now.getDay() + 6) % 7];
5513
+ };
5514
+ const yesterdayPeriods = (schedule, now) => {
5515
+ const day = previousWeekday(now);
5176
5516
  if (!day) return [];
5177
5517
  return schedule.find((row) => row.day_of_week === day)?.periods ?? [];
5178
5518
  };
5179
- const getActiveUntil = (periods, now) => {
5180
- const open = getOpenPeriodAt(periods, now);
5519
+ const splitPeriods = (periods, span) => {
5520
+ const specific = periods.filter((period) => periodCoversDate$1(period, span)).filter((period) => hasSpecificDates$1(period));
5521
+ return {
5522
+ specific,
5523
+ fallback: periods.filter((period) => !hasSpecificDates$1(period)),
5524
+ specificOpens: specific.filter((period) => !period.closed)
5525
+ };
5526
+ };
5527
+ /** Yesterday's closed overnight window still covers this morning. */
5528
+ const yesterdayClosedWrap = (schedule, now, span = now) => {
5529
+ const periods = yesterdayPeriods(schedule, now);
5530
+ const minutes = now.getHours() * 60 + now.getMinutes();
5531
+ const { specific, fallback, specificOpens } = splitPeriods(periods, span);
5532
+ const closedTail = (band) => {
5533
+ return band.some((period) => period.closed && isWrapTail(period, minutes));
5534
+ };
5535
+ if (closedTail(specific)) return true;
5536
+ if (specificOpens.length > 0) return false;
5537
+ return closedTail(fallback);
5538
+ };
5539
+ /**
5540
+ * Open minutes after midnight that belong to yesterday's overnight window.
5541
+ * A dated open on that day replaces the everyday hours.
5542
+ */
5543
+ const yesterdayOpenUntil = (schedule, now, span = now) => {
5544
+ const periods = yesterdayPeriods(schedule, now);
5545
+ const minutes = now.getHours() * 60 + now.getMinutes();
5546
+ const { specific, fallback, specificOpens } = splitPeriods(periods, span);
5547
+ if (specific.some((period) => isClosedAt$1(period, now))) return null;
5548
+ const band = specificOpens.length > 0 ? specificOpens : null;
5549
+ const pool = band ?? fallback.filter((period) => !period.closed);
5550
+ if (!band && fallback.some((period) => isClosedAt$1(period, now))) return null;
5551
+ const open = pool.find((period) => isWrapTail(period, minutes));
5552
+ if (!open?.end_time) return null;
5553
+ return atTimeOnDate(now, open.end_time);
5554
+ };
5555
+ const rollsToNextDay = (period) => {
5556
+ if (!period.start_time || !period.end_time) return false;
5557
+ const start = timeToMinutes$1(period.start_time);
5558
+ return timeToMinutes$1(period.end_time) <= start;
5559
+ };
5560
+ const getActiveUntil = (periods, now, span = now) => {
5561
+ const open = getOpenPeriodAt$1(periods, now, span);
5181
5562
  if (!open?.end_time) return null;
5182
- let untilMs = atTimeOnDate(now, open.end_time).getTime();
5183
- const openIsSpecific = hasSpecificDates(open);
5563
+ const end = atTimeOnDate(now, open.end_time);
5564
+ const until = new Date(end);
5565
+ if (rollsToNextDay(open)) until.setDate(until.getDate() + 1);
5566
+ let untilMs = until.getTime();
5567
+ const openIsSpecific = hasSpecificDates$1(open);
5184
5568
  for (const period of periods) {
5185
5569
  if (!period.closed || !period.start_time) continue;
5186
5570
  const closeMs = atTimeOnDate(now, period.start_time).getTime();
5187
5571
  if (closeMs <= now.getTime() || closeMs >= untilMs) continue;
5188
5572
  if (!isWithinPeriodDates(period, closeMs)) continue;
5189
- if (!hasSpecificDates(period) && openIsSpecific) continue;
5573
+ if (!hasSpecificDates$1(period) && openIsSpecific) continue;
5190
5574
  untilMs = closeMs;
5191
5575
  }
5192
5576
  return new Date(untilMs);
5193
5577
  };
5578
+ const dayPeriods$2 = (schedule, instant) => {
5579
+ const day = DAY_OF_WEEK[instant.getDay()];
5580
+ if (!day) return [];
5581
+ return schedule.find((row) => row.day_of_week === day)?.periods ?? [];
5582
+ };
5583
+ const closedNow$1 = (periods, now, span = now) => {
5584
+ return periods.some((period) => {
5585
+ if (!period.closed) return false;
5586
+ if (hasSpecificDates$1(period) && !periodCoversDate$1(period, span)) return false;
5587
+ return isClosedAt$1(period, now);
5588
+ });
5589
+ };
5590
+ /** Active at `now`, including yesterday's overnight tail or closed wrap. */
5591
+ const scheduleActiveAt = (schedule, now, span = now) => {
5592
+ if (yesterdayClosedWrap(schedule, now, span)) return false;
5593
+ const periods = dayPeriods$2(schedule, now);
5594
+ if (isPeriodsActiveAt(periods, now, span)) return true;
5595
+ if (closedNow$1(periods, now, span)) return false;
5596
+ return yesterdayOpenUntil(schedule, now, span) != null;
5597
+ };
5194
5598
  const getNextStart = (schedule, now) => {
5195
5599
  const nowMs = now.getTime();
5196
5600
  let nextStartMs = null;
5197
- const consider = (candidate, periods) => {
5601
+ const consider = (candidate) => {
5198
5602
  const startMs = candidate.getTime();
5199
5603
  if (startMs <= nowMs) return;
5200
- if (!isPeriodsActiveAt(periods, candidate)) return;
5604
+ if (!scheduleActiveAt(schedule, candidate)) return;
5201
5605
  if (nextStartMs === null || startMs < nextStartMs) nextStartMs = startMs;
5202
5606
  };
5203
- for (let dayOffset = 0; dayOffset < 8; dayOffset += 1) {
5607
+ for (let dayOffset = -1; dayOffset < 8; dayOffset += 1) {
5204
5608
  const candidateDate = new Date(now);
5205
5609
  candidateDate.setDate(candidateDate.getDate() + dayOffset);
5206
5610
  candidateDate.setSeconds(0, 0);
5207
- const periods = dayPeriods(schedule, candidateDate);
5611
+ const periods = dayPeriods$2(schedule, candidateDate);
5208
5612
  if (periods.length === 0) continue;
5209
5613
  for (const period of periods) {
5210
5614
  if (period.closed || !period.start_time) continue;
5211
5615
  const start = atTimeOnDate(candidateDate, period.start_time);
5212
5616
  if (!isWithinPeriodDates(period, start.getTime())) continue;
5213
- consider(start, periods);
5617
+ consider(start);
5214
5618
  }
5215
5619
  for (const period of periods) {
5216
5620
  if (!period.closed || !period.end_time) continue;
5217
5621
  const reopen = atTimeOnDate(candidateDate, period.end_time);
5622
+ if (rollsToNextDay(period)) reopen.setDate(reopen.getDate() + 1);
5218
5623
  if (!isWithinPeriodDates(period, reopen.getTime())) continue;
5219
- consider(reopen, periods);
5624
+ consider(reopen);
5220
5625
  }
5221
5626
  }
5222
5627
  return nextStartMs === null ? null : new Date(nextStartMs);
5223
5628
  };
5629
+ const safeTimeZone$1 = (timeZone) => {
5630
+ try {
5631
+ Intl.DateTimeFormat("en-US", { timeZone });
5632
+ return timeZone;
5633
+ } catch {
5634
+ return "UTC";
5635
+ }
5636
+ };
5637
+ /** Local getters of the returned Date match the wall clock in `timeZone`. */
5638
+ const wallClockDate$1 = (instant, timeZone) => {
5639
+ const zone = safeTimeZone$1(timeZone);
5640
+ const date = {
5641
+ year: "numeric",
5642
+ month: "2-digit",
5643
+ day: "2-digit"
5644
+ };
5645
+ const time = {
5646
+ hour: "2-digit",
5647
+ minute: "2-digit"
5648
+ };
5649
+ const sec = {
5650
+ second: "2-digit",
5651
+ hourCycle: "h23"
5652
+ };
5653
+ const opts = {
5654
+ timeZone: zone,
5655
+ ...date,
5656
+ ...time,
5657
+ ...sec
5658
+ };
5659
+ const parts = new Intl.DateTimeFormat("en-US", opts).formatToParts(instant);
5660
+ const read = (type) => {
5661
+ const part = parts.find((item) => item.type === type);
5662
+ return Number(part?.value ?? 0);
5663
+ };
5664
+ let year = read("year");
5665
+ let month = read("month");
5666
+ let day = read("day");
5667
+ let hour = read("hour");
5668
+ if (hour === 24) {
5669
+ hour = 0;
5670
+ const next = new Date(Date.UTC(year, month - 1, day + 1));
5671
+ year = next.getUTCFullYear();
5672
+ month = next.getUTCMonth() + 1;
5673
+ day = next.getUTCDate();
5674
+ }
5675
+ const ymd = [
5676
+ year,
5677
+ month - 1,
5678
+ day
5679
+ ];
5680
+ const hms = [
5681
+ hour,
5682
+ read("minute"),
5683
+ read("second")
5684
+ ];
5685
+ return new Date(...ymd, ...hms);
5686
+ };
5224
5687
  /** Whether `schedule` is in hours now, and until / next open. */
5225
- const getScheduleWindow = (schedule, now = /* @__PURE__ */ new Date()) => {
5688
+ const getScheduleWindow = (schedule, now = /* @__PURE__ */ new Date(), span) => {
5226
5689
  if (schedule == null) return {
5227
5690
  active: true,
5228
5691
  until: null
5229
5692
  };
5230
- const periods = dayPeriods(schedule, now);
5231
- if (isPeriodsActiveAt(periods, now)) return {
5693
+ const covers = span ?? now;
5694
+ if (!scheduleActiveAt(schedule, now, covers)) return {
5695
+ active: false,
5696
+ next: getNextStart(schedule, now)
5697
+ };
5698
+ const periods = dayPeriods$2(schedule, now);
5699
+ if (isPeriodsActiveAt(periods, now, covers)) return {
5232
5700
  active: true,
5233
- until: getActiveUntil(periods, now)
5701
+ until: getActiveUntil(periods, now, covers)
5234
5702
  };
5235
5703
  return {
5236
- active: false,
5237
- next: getNextStart(schedule, now)
5704
+ active: true,
5705
+ until: yesterdayOpenUntil(schedule, now, covers)
5238
5706
  };
5239
5707
  };
5240
- const isOnSchedule = (schedule, now) => {
5241
- return getScheduleWindow(schedule, now).active;
5708
+ const isOnSchedule = (schedule, now, timeZone) => {
5709
+ const instant = now ?? /* @__PURE__ */ new Date();
5710
+ const clock = timeZone ? wallClockDate$1(instant, timeZone) : instant;
5711
+ return getScheduleWindow(schedule, clock, timeZone ? instant : void 0).active;
5242
5712
  };
5243
5713
  //#endregion
5244
5714
  //#region src/order/recommendations.ts
@@ -5785,6 +6255,25 @@ const buildLocalDraft = (insert) => {
5785
6255
  const sameBrand = (left, right) => {
5786
6256
  return (optionalBrandId(left) ?? null) === (optionalBrandId(right) ?? null);
5787
6257
  };
6258
+ const proofToken = (proof) => {
6259
+ if (typeof proof === "string") return proof;
6260
+ if ("type" in proof) return null;
6261
+ return proof.customer_switch_proof ?? null;
6262
+ };
6263
+ /** Order id carried by a switch proof. Null for an ID token or a bad proof. */
6264
+ const proofOrderId = (proof) => {
6265
+ const part = proofToken(proof)?.split(".")[1];
6266
+ if (!part) return null;
6267
+ try {
6268
+ const b64 = part.replace(/-/g, "+").replace(/_/g, "/");
6269
+ const pad = b64 + "=".repeat((4 - b64.length % 4) % 4);
6270
+ const payload = JSON.parse(atob(pad));
6271
+ if (typeof payload.order_id !== "string" || !payload.order_id) return null;
6272
+ return payload.order_id;
6273
+ } catch {
6274
+ return null;
6275
+ }
6276
+ };
5788
6277
  /** Newest OPEN order for customer + scope, if any. */
5789
6278
  const findOpenOrder = async (scope, http) => {
5790
6279
  const brandId = optionalBrandId(scope.brand_id);
@@ -6192,6 +6681,72 @@ var Order = class Order {
6192
6681
  };
6193
6682
  this.scheduleAfterMutate();
6194
6683
  }
6684
+ async commitCustomer(run) {
6685
+ await this.persist();
6686
+ await this.flush.settle();
6687
+ if (this.deleted) return this.getOrder();
6688
+ try {
6689
+ const next = await run();
6690
+ this.applyServerOrder(next);
6691
+ } catch (error) {
6692
+ if (error instanceof StorefrontHttpError && error.code === "ORDER_NOT_OPEN") throw new OrderNotOpenError(error.status, error.details);
6693
+ throw error;
6694
+ }
6695
+ return this.getOrder();
6696
+ }
6697
+ /**
6698
+ * Replaces the customer on this open order. The server copies that
6699
+ * customer's profile onto the order and sets `customer_id` and
6700
+ * `is_anonymous` from the customer. Rejects with `OrderNotOpenError`
6701
+ * (`409`, `details.code` `ORDER_NOT_OPEN`) when the order is not `OPEN`
6702
+ * (`CLOSED`, `COMPLETED`, `VOIDED`, `REVERSED`, `REFUNDED`, or legacy
6703
+ * `CANCELLED`). `404` `CUSTOMER_NOT_FOUND` when that customer does not
6704
+ * exist. `409` `ORDER_OPEN_SCOPE_TAKEN` when they already have an open
6705
+ * cart for this store, channel, fulfillment, and brand.
6706
+ *
6707
+ * After a different `customer_id`, the order belongs to that customer.
6708
+ * Later calls need their storefront access token.
6709
+ */
6710
+ async setCustomer(customerId) {
6711
+ if (this.deleted) return this.getOrder();
6712
+ const body = { customer_id: customerId };
6713
+ const run = () => {
6714
+ const id = this.draft.id;
6715
+ return services.orders.setCustomer(id, body, this.http);
6716
+ };
6717
+ return this.commitCustomer(run);
6718
+ }
6719
+ /**
6720
+ * Makes this open order anonymous: `is_anonymous` becomes true and the
6721
+ * customer snapshot is cleared to guest placeholders. `customer_id`
6722
+ * stays the authenticated owner. Rejects with `OrderNotOpenError`
6723
+ * (`409`, `ORDER_NOT_OPEN`) when the order is not `OPEN`.
6724
+ */
6725
+ async clearCustomer() {
6726
+ if (this.deleted) return this.getOrder();
6727
+ const run = () => {
6728
+ const id = this.draft.id;
6729
+ return services.orders.clearCustomer(id, this.http);
6730
+ };
6731
+ return this.commitCustomer(run);
6732
+ }
6733
+ /**
6734
+ * Moves this open order to the customer proved by `proof` and keeps
6735
+ * the returned order on this instance. Requires `auth.configure()`
6736
+ * without a fixed `accessToken` on this order.
6737
+ */
6738
+ async switchCustomer(proof) {
6739
+ if (this.http.accessToken) throw new OrderAuthPinnedError();
6740
+ const bound = proofOrderId(proof);
6741
+ if (bound && bound !== this.draft.id) throw new Error("Customer switch proof is for a different order");
6742
+ const arg = typeof proof === "object" && "type" in proof && proof.type === "id_token" ? {
6743
+ ...proof,
6744
+ orderId: this.draft.id
6745
+ } : proof;
6746
+ const result = await auth.switchCustomer(arg);
6747
+ this.applyServerOrder(result.order);
6748
+ return this.getOrder();
6749
+ }
6195
6750
  async cancelOrder() {
6196
6751
  if (this.deleted) return;
6197
6752
  this.flush.cancel();
@@ -6220,6 +6775,7 @@ var Order = class Order {
6220
6775
  }
6221
6776
  this.deleted = true;
6222
6777
  this.notify();
6778
+ auth.releaseDelegatedOrder(this.draft.id);
6223
6779
  }
6224
6780
  /**
6225
6781
  * Reopens this placed, unpaid order: the server voids every payment
@@ -6265,6 +6821,7 @@ var Order = class Order {
6265
6821
  this.synced = cloneOrder(next);
6266
6822
  this.deleted = true;
6267
6823
  this.notify();
6824
+ auth.releaseDelegatedOrder(this.draft.id);
6268
6825
  }
6269
6826
  async validateOrder() {
6270
6827
  const draft = this.getOrder();
@@ -6348,6 +6905,7 @@ var Order = class Order {
6348
6905
  this.replaceDraft(cloneOrder(next));
6349
6906
  this.synced = cloneOrder(next);
6350
6907
  this.notify();
6908
+ if (next.status !== "OPEN") auth.releaseDelegatedOrder(next.id);
6351
6909
  }
6352
6910
  /** Keeps the applied redeems while the server holds the same capped ones. */
6353
6911
  replaceDraft(next) {
@@ -6602,6 +7160,15 @@ var Order = class Order {
6602
7160
  this.notify();
6603
7161
  this.flush.schedule();
6604
7162
  }
7163
+ /** Sets or clears the pickup time on an open order. */
7164
+ setScheduledAt(scheduledAt) {
7165
+ this.draft = {
7166
+ ...this.draft,
7167
+ scheduled_at: scheduledAt
7168
+ };
7169
+ this.scheduleAfterMutate();
7170
+ return this.getOrder();
7171
+ }
6605
7172
  notify() {
6606
7173
  this.externalNotify?.();
6607
7174
  this.listeners.forEach((listener) => listener());
@@ -7795,7 +8362,7 @@ const checkRootProductQuantities = (roots, lookup, out) => {
7795
8362
  }
7796
8363
  }
7797
8364
  };
7798
- const validateProductRoot = (root, lines, lookup, locale, out) => {
8365
+ const validateProductRoot = (root, lines, lookup, locale, out, at, timeZone) => {
7799
8366
  const product = lookup.productsById[root.id];
7800
8367
  if (!product) {
7801
8368
  pushLineIssue(out, root.uid, { code: "PRODUCT_NOT_FOUND" });
@@ -7809,7 +8376,8 @@ const validateProductRoot = (root, lines, lookup, locale, out) => {
7809
8376
  const status = product.availability.status;
7810
8377
  if (status === "HIDDEN") pushLineIssue(out, root.uid, { code: "HIDDEN" });
7811
8378
  if (status === "OUT_OF_STOCK") pushLineIssue(out, root.uid, { code: "OUT_OF_STOCK" });
7812
- if (!isOnSchedule(product.availability.schedules)) pushLineIssue(out, root.uid, { code: "OUTSIDE_HOURS" });
8379
+ const hours = product.availability.schedules;
8380
+ if (!isOnSchedule(hours, at, timeZone)) pushLineIssue(out, root.uid, { code: "OUTSIDE_HOURS" });
7813
8381
  collectUnmatchedModifiers(lines, lookup, root.id, root.uid, out);
7814
8382
  let expected;
7815
8383
  if (needsSlotPlacement(lines, root.uid, lookup)) {
@@ -8043,6 +8611,8 @@ const compareRootGroups = (submitted, expected, out) => {
8043
8611
  /** Compare cart lines/totals against a fresh menu (exact Big.js equality). */
8044
8612
  const validateOrderAgainstMenu = (input) => {
8045
8613
  const locale = input.locale ?? "en_us";
8614
+ const at = input.scheduleAt ?? /* @__PURE__ */ new Date();
8615
+ const zone = input.timeZone;
8046
8616
  const issues = [];
8047
8617
  const lineIssues = {};
8048
8618
  if (!input.menu) return {
@@ -8067,7 +8637,18 @@ const validateOrderAgainstMenu = (input) => {
8067
8637
  checkFeeSelfConsistency(root, lineIssues);
8068
8638
  continue;
8069
8639
  }
8070
- validateProductRoot(root, menuLines, lookup, locale, lineIssues);
8640
+ const p1 = [
8641
+ root,
8642
+ menuLines,
8643
+ lookup,
8644
+ locale
8645
+ ];
8646
+ const p2 = [
8647
+ lineIssues,
8648
+ at,
8649
+ zone
8650
+ ];
8651
+ validateProductRoot(...p1, ...p2);
8071
8652
  }
8072
8653
  checkRootProductQuantities(roots, lookup, lineIssues);
8073
8654
  if (input.expectedFeeLines) compareExpectedFeeLines(input.lines, input.expectedFeeLines, lineIssues);
@@ -8175,229 +8756,8 @@ const collectOrderLineDiffs = (input) => {
8175
8756
  return diffs;
8176
8757
  };
8177
8758
  //#endregion
8178
- //#region src/logs/serializeError.ts
8179
- const UNKNOWN_MESSAGE = "Unknown error";
8180
- const MAX_CAUSE_DEPTH = 3;
8181
- const safeStringify = (value) => {
8182
- try {
8183
- const json = JSON.stringify(value);
8184
- return json === void 0 ? String(value) : json;
8185
- } catch {
8186
- return String(value);
8187
- }
8188
- };
8189
- const messageOf = (value) => {
8190
- if (value instanceof Error) return value.message || value.name;
8191
- if (typeof value === "string") return value;
8192
- if (value && typeof value === "object" && "message" in value) {
8193
- const message = value.message;
8194
- if (typeof message === "string") return message;
8195
- }
8196
- if (value === void 0) return "";
8197
- return safeStringify(value);
8198
- };
8199
- /** `TypeError: boom ← caused by: Error: socket closed` style chain. */
8200
- const describeCause = (cause, depth = 0) => {
8201
- if (cause instanceof Error) {
8202
- const head = `${cause.name}: ${cause.message}`;
8203
- const inner = cause.cause;
8204
- if (inner === void 0 || depth >= MAX_CAUSE_DEPTH) return head;
8205
- return `${head} ← caused by: ${describeCause(inner, depth + 1)}`;
8206
- }
8207
- return messageOf(cause);
8208
- };
8209
- const nameOf = (value) => {
8210
- if (value instanceof Error) return value.name;
8211
- if (value && typeof value === "object" && "name" in value) {
8212
- const name = value.name;
8213
- if (typeof name === "string") return name;
8214
- }
8215
- return null;
8216
- };
8217
- const stackOf = (value) => {
8218
- if (value && typeof value === "object" && "stack" in value) {
8219
- const stack = value.stack;
8220
- if (typeof stack === "string" && stack) return stack;
8221
- }
8222
- return null;
8223
- };
8224
- const causeOf = (value) => {
8225
- if (!value || typeof value !== "object" || !("cause" in value)) return null;
8226
- const cause = value.cause;
8227
- if (cause === void 0 || cause === null) return null;
8228
- return describeCause(cause) || null;
8229
- };
8230
- /** Accepts anything a `catch` can receive. */
8231
- const serializeError = (value) => {
8232
- const message = truncate(messageOf(value).trim() || UNKNOWN_MESSAGE, LOG_LIMITS.message);
8233
- const name = nameOf(value);
8234
- const stack = stackOf(value);
8235
- const cause = causeOf(value);
8236
- const e1 = { name: name ? truncate(name, LOG_LIMITS.errorName) : null };
8237
- const e2 = { stack: stack ? truncate(stack, LOG_LIMITS.stack) : null };
8238
- const e3 = { cause: cause ? truncate(cause, LOG_LIMITS.cause) : null };
8239
- return {
8240
- message,
8241
- error: {
8242
- ...e1,
8243
- ...e2,
8244
- ...e3
8245
- }
8246
- };
8247
- };
8248
- //#endregion
8249
- //#region src/logs/log.ts
8250
- const LOGS_PATH = "/api/storefront/v1/logs";
8251
- const POS_LOGS_PATH = "/api/storefront/v1/pos/logs";
8252
- const FLUSH_DELAY_MS = 500;
8253
- let queue = [];
8254
- let timer = null;
8255
- const optionalText = (value, max) => {
8256
- const trimmed = value?.trim();
8257
- if (!trimmed) return null;
8258
- return truncate(trimmed, max);
8259
- };
8260
- const buildEntry = (level, message, options, error) => {
8261
- const text = typeof message === "string" ? message.trim() : "";
8262
- if (!text) return null;
8263
- const b1 = {
8264
- level,
8265
- message: truncate(text, LOG_LIMITS.message)
8266
- };
8267
- const b2 = { alias: optionalText(options.alias, LOG_LIMITS.alias) };
8268
- const b3 = { detail: optionalText(options.detail, LOG_LIMITS.detail) };
8269
- const b4 = { status: optionalText(options.status, LOG_LIMITS.status) };
8270
- const b5 = {
8271
- error: error ?? null,
8272
- occurred_at: (/* @__PURE__ */ new Date()).toISOString()
8273
- };
8274
- return {
8275
- ...b1,
8276
- ...b2,
8277
- ...b3,
8278
- ...b4,
8279
- ...getLogContext(),
8280
- ...b5
8281
- };
8282
- };
8283
- const isAuthConfigured = () => {
8284
- try {
8285
- getBaseUrl();
8286
- return true;
8287
- } catch {
8288
- return false;
8289
- }
8290
- };
8291
- /** Browsers cap keepalive bodies at 64 KB; bigger batches send normally. */
8292
- const postInit = (body) => {
8293
- return {
8294
- method: "POST",
8295
- body,
8296
- keepalive: body.length < LOG_LIMITS.keepaliveBytes
8297
- };
8298
- };
8299
- const sendPos = async (device, logs) => {
8300
- const fetchImpl = device.http.fetchImpl ?? fetch;
8301
- const url = `${resolveBaseUrl(device.http.baseUrl)}${POS_LOGS_PATH}`;
8302
- const { device_uid, device_secret } = device;
8303
- const body = JSON.stringify({
8304
- device_uid,
8305
- device_secret,
8306
- logs
8307
- });
8308
- const headers = { "Content-Type": "application/json" };
8309
- return (await fetchImpl(url, {
8310
- ...postInit(body),
8311
- headers
8312
- })).ok;
8313
- };
8314
- const sendBatch = async (logs) => {
8315
- try {
8316
- const device = getPosLogDevice();
8317
- if (device) return await sendPos(device, logs);
8318
- if (!isAuthConfigured()) return false;
8319
- await storefrontJson(LOGS_PATH, postInit(JSON.stringify({ logs })));
8320
- return true;
8321
- } catch {
8322
- return false;
8323
- }
8324
- };
8325
- /** Sends queued logs now; resolves once every batch has been attempted. */
8326
- const flushLogs = async () => {
8327
- if (timer) clearTimeout(timer);
8328
- timer = null;
8329
- const pending = queue;
8330
- queue = [];
8331
- const jobs = [];
8332
- for (let i = 0; i < pending.length; i += LOG_LIMITS.batch) {
8333
- const chunk = pending.slice(i, i + LOG_LIMITS.batch);
8334
- const entries = chunk.map((item) => item.entry);
8335
- const job = sendBatch(entries).then((sent) => {
8336
- for (const item of chunk) item.resolve(sent);
8337
- });
8338
- jobs.push(job);
8339
- }
8340
- await Promise.all(jobs);
8341
- };
8342
- const enqueue = (entry) => {
8343
- return new Promise((resolve) => {
8344
- queue.push({
8345
- entry,
8346
- resolve
8347
- });
8348
- if (queue.length >= LOG_LIMITS.batch) {
8349
- flushLogs();
8350
- return;
8351
- }
8352
- if (!timer) timer = setTimeout(() => void flushLogs(), FLUSH_DELAY_MS);
8353
- });
8354
- };
8355
- /**
8356
- * Reports a log to the merchant dashboard with the SDK's current context
8357
- * (channel, fulfillment, store, brand, device). Never throws; resolves to
8358
- * `false` when the log could not be delivered.
8359
- */
8360
- const log = (level, message, options = {}) => {
8361
- const entry = buildEntry(level, message, options);
8362
- if (!entry) return Promise.resolve(false);
8363
- return enqueue(entry);
8364
- };
8365
- /** `log("error", …)` with message, name, stack and cause read from `error`. */
8366
- const logError = (error, options = {}) => {
8367
- const { message, error: info } = serializeError(error);
8368
- const entry = buildEntry("error", message, options, info);
8369
- if (!entry) return Promise.resolve(false);
8370
- return enqueue(entry);
8371
- };
8372
- //#endregion
8373
- //#region src/webhooks/verifyWebhookSignature.ts
8374
- const SIGNATURE_PREFIX = "sha256=";
8375
- const hmacHex = async (secret, body) => {
8376
- const raw = new TextEncoder().encode(secret);
8377
- const key = await crypto.subtle.importKey("raw", raw, {
8378
- name: "HMAC",
8379
- hash: "SHA-256"
8380
- }, false, ["sign"]);
8381
- const data = new TextEncoder().encode(body);
8382
- const sig = new Uint8Array(await crypto.subtle.sign("HMAC", key, data));
8383
- return Array.from(sig, (byte) => byte.toString(16).padStart(2, "0")).join("");
8384
- };
8385
- const safeEqual = (left, right) => {
8386
- if (left.length !== right.length) return false;
8387
- let diff = 0;
8388
- for (let i = 0; i < left.length; i += 1) diff |= left.charCodeAt(i) ^ right.charCodeAt(i);
8389
- return diff === 0;
8390
- };
8391
- /** Checks a Fire Spark webhook/callback signature in constant time. */
8392
- const verifyWebhookSignature = async (input) => {
8393
- const { secret, body, signature } = input;
8394
- if (!secret || !signature?.startsWith(SIGNATURE_PREFIX)) return false;
8395
- const received = signature.slice(7).toLowerCase();
8396
- return safeEqual(received, await hmacHex(secret, body));
8397
- };
8398
- //#endregion
8399
- //#region ../../node_modules/.pnpm/dayjs@1.11.9/node_modules/dayjs/dayjs.min.js
8400
- var require_dayjs_min = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8759
+ //#region src/rules/assert.ts
8760
+ var import_dayjs_min = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
8401
8761
  (function(t, e) {
8402
8762
  "object" == typeof exports && "undefined" != typeof module ? module.exports = e() : "function" == typeof define && define.amd ? define(e) : (t = "undefined" != typeof globalThis ? globalThis : t || self).dayjs = e();
8403
8763
  })(exports, (function() {
@@ -8692,9 +9052,7 @@ var require_dayjs_min = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8692
9052
  return w(1e3 * t);
8693
9053
  }, w.en = D[g], w.Ls = D, w.p = {}, w;
8694
9054
  }));
8695
- }));
8696
- //#endregion
8697
- //#region src/rules/assert.ts
9055
+ })))(), 1);
8698
9056
  const assert = (value, message) => {
8699
9057
  if (value) return;
8700
9058
  throw new Error(message);
@@ -8723,7 +9081,6 @@ const countCartProducts = (lines, includeModifiers) => {
8723
9081
  };
8724
9082
  //#endregion
8725
9083
  //#region src/rules/absoluteDateMatcher.ts
8726
- var import_dayjs_min = /* @__PURE__ */ __toESM(require_dayjs_min(), 1);
8727
9084
  const matchAbsoluteDateOperator = (condition, matcher) => {
8728
9085
  const { primitive, operator, value } = condition;
8729
9086
  assert(primitive === "absolute date", primitiveError("absolute date"));
@@ -8794,6 +9151,14 @@ const paymentCaptured = (condition, captured) => {
8794
9151
  assert(value != null, "Invalid value");
8795
9152
  return matchBooleanOperator(condition, captured ?? false);
8796
9153
  };
9154
+ const orderIsScheduled = (condition, scheduledAt) => {
9155
+ const { type, primitive, value } = condition;
9156
+ const matcherType = "Order is scheduled";
9157
+ assert(primitive === "boolean", primitiveError("boolean"));
9158
+ assert(type === matcherType, typeError(matcherType));
9159
+ assert(value != null, "Invalid value");
9160
+ return matchBooleanOperator(condition, Boolean(scheduledAt));
9161
+ };
8797
9162
  //#endregion
8798
9163
  //#region src/rules/currencyMatcher.ts
8799
9164
  const matchCurrencyOperator = (condition, matcher) => {
@@ -9520,6 +9885,10 @@ var RulesEngine = class {
9520
9885
  const payment = this.getDependency("payment", type);
9521
9886
  return paymentCaptured(condition, payment.captured);
9522
9887
  }
9888
+ if (type === "Order is scheduled") {
9889
+ const order = this.getDependency("order", type);
9890
+ return orderIsScheduled(condition, order.scheduled_at);
9891
+ }
9523
9892
  throw new Error(`Condition ${type} not supported in boolean matcher`);
9524
9893
  };
9525
9894
  assertCurrency = (condition) => {
@@ -9687,6 +10056,731 @@ var RulesEngine = class {
9687
10056
  };
9688
10057
  };
9689
10058
  //#endregion
10059
+ //#region src/prepTime/resolvePrepTime.ts
10060
+ const MIN_MINUTES = 1;
10061
+ const MAX_MINUTES = 1e3;
10062
+ const clampMinutes = (value) => {
10063
+ const rounded = Math.round(value);
10064
+ if (rounded < MIN_MINUTES) return MIN_MINUTES;
10065
+ if (rounded > MAX_MINUTES) return MAX_MINUTES;
10066
+ return rounded;
10067
+ };
10068
+ const matchesPreparing = (effect, count) => {
10069
+ const { operator, preparing } = effect;
10070
+ if (operator == null || preparing == null) return true;
10071
+ if (operator === "gt") return count > preparing;
10072
+ if (operator === "gte") return count >= preparing;
10073
+ if (operator === "lt") return count < preparing;
10074
+ if (operator === "lte") return count <= preparing;
10075
+ return count === preparing;
10076
+ };
10077
+ const applyEffect = (rule, order) => {
10078
+ const { effect } = rule;
10079
+ if (effect.strategy === "PER_ORDER") return clampMinutes(effect.value);
10080
+ const count = countCartProducts(order.lines, false);
10081
+ return clampMinutes(effect.value * count);
10082
+ };
10083
+ /** Cart-aware prep minutes. One minute when no rule matches. */
10084
+ const resolveSchedulePrepMinutes = (input) => {
10085
+ const { rules, order } = input;
10086
+ const preparingCount = input.preparingCount ?? 0;
10087
+ const engine = new RulesEngine({ order });
10088
+ const ranked = rules.filter((item) => !item.fallback);
10089
+ const hit = engine.matchRules(ranked, { exclusive: false }).find((rule) => {
10090
+ return matchesPreparing(rule.effect, preparingCount);
10091
+ });
10092
+ const fallback = rules.find((item) => item.fallback);
10093
+ const rule = hit ?? fallback;
10094
+ if (!rule) return MIN_MINUTES;
10095
+ return applyEffect(rule, order);
10096
+ };
10097
+ //#endregion
10098
+ //#region src/schedule/scheduleTime.ts
10099
+ /** Default earliest pickup slot: one hour from now. */
10100
+ const SCHEDULED_ORDER_MIN_LEAD_MINUTES = 60;
10101
+ /** Latest bookable instant. No merchant preference stores this yet. */
10102
+ const SCHEDULED_ORDER_MAX_HORIZON_HOURS = 48;
10103
+ const SCHEDULED_ORDER_MAX_HORIZON_MINUTES = 2880;
10104
+ /** Slot grid used when a customer picks a time. */
10105
+ const SCHEDULED_ORDER_SLOT_MINUTES = 15;
10106
+ const MINUTE_MS$1 = 6e4;
10107
+ const DAY_MINUTES = 1440;
10108
+ const safeTimeZone = (timeZone) => {
10109
+ try {
10110
+ Intl.DateTimeFormat("en-US", { timeZone });
10111
+ return timeZone;
10112
+ } catch {
10113
+ return "UTC";
10114
+ }
10115
+ };
10116
+ const partValue = (parts, type) => {
10117
+ return Number(parts.find((part) => part.type === type)?.value ?? 0);
10118
+ };
10119
+ const formatters = /* @__PURE__ */ new Map();
10120
+ const zonedFormat = (timeZone) => {
10121
+ const zone = safeTimeZone(timeZone);
10122
+ const cached = formatters.get(zone);
10123
+ if (cached) return cached;
10124
+ const b1 = {
10125
+ year: "numeric",
10126
+ month: "2-digit",
10127
+ day: "2-digit"
10128
+ };
10129
+ const b2 = {
10130
+ hour: "2-digit",
10131
+ minute: "2-digit",
10132
+ second: "2-digit"
10133
+ };
10134
+ const opts = {
10135
+ timeZone: zone,
10136
+ hourCycle: "h23",
10137
+ ...b1,
10138
+ ...b2
10139
+ };
10140
+ const format = new Intl.DateTimeFormat("en-US", opts);
10141
+ formatters.set(zone, format);
10142
+ return format;
10143
+ };
10144
+ /** Calendar fields of `instant` in `timeZone`. Hour 24 rolls forward. */
10145
+ const zonedParts = (instant, timeZone) => {
10146
+ const parts = zonedFormat(timeZone).formatToParts(instant);
10147
+ let year = partValue(parts, "year");
10148
+ let month = partValue(parts, "month");
10149
+ let day = partValue(parts, "day");
10150
+ let hour = partValue(parts, "hour");
10151
+ if (hour === 24) {
10152
+ hour = 0;
10153
+ const next = new Date(Date.UTC(year, month - 1, day + 1));
10154
+ year = next.getUTCFullYear();
10155
+ month = next.getUTCMonth() + 1;
10156
+ day = next.getUTCDate();
10157
+ }
10158
+ return {
10159
+ year,
10160
+ month,
10161
+ day,
10162
+ hour,
10163
+ minute: partValue(parts, "minute"),
10164
+ second: partValue(parts, "second")
10165
+ };
10166
+ };
10167
+ /**
10168
+ * A Date whose local getters match the wall clock in `timeZone`.
10169
+ * Same trick as the call-center shift helper.
10170
+ */
10171
+ const wallClockDate = (instant, timeZone) => {
10172
+ const parts = zonedParts(instant, timeZone);
10173
+ const ymd = [
10174
+ parts.year,
10175
+ parts.month - 1,
10176
+ parts.day
10177
+ ];
10178
+ const hms = [
10179
+ parts.hour,
10180
+ parts.minute,
10181
+ parts.second
10182
+ ];
10183
+ return new Date(...ymd, ...hms);
10184
+ };
10185
+ const offsetMs = (instant, timeZone) => {
10186
+ const parts = zonedParts(instant, timeZone);
10187
+ const month = parts.month - 1;
10188
+ const hms = [
10189
+ parts.hour,
10190
+ parts.minute,
10191
+ parts.second
10192
+ ];
10193
+ return Date.UTC(parts.year, month, parts.day, ...hms) - instant.getTime();
10194
+ };
10195
+ const sameWall = (instant, year, month, day, minutes, timeZone) => {
10196
+ const parts = zonedParts(instant, timeZone);
10197
+ const hour = Math.floor(minutes / 60);
10198
+ const minute = minutes % 60;
10199
+ if (parts.year !== year || parts.month !== month) return false;
10200
+ if (parts.day !== day || parts.hour !== hour) return false;
10201
+ return parts.minute === minute;
10202
+ };
10203
+ /**
10204
+ * UTC instant of a wall-clock minute in `timeZone`.
10205
+ * Missing civil times (spring-forward gaps) return null.
10206
+ * Repeated civil times (fall-back) return both instants, earlier first.
10207
+ */
10208
+ const zoneOffsetHours = /* @__PURE__ */ new Map();
10209
+ const instantAtOffset = (wall, hours, year, month, day, minutes, timeZone) => {
10210
+ const guess = new Date(wall + hours * MINUTE_MS$1 * 60);
10211
+ const utc = new Date(wall - offsetMs(guess, timeZone));
10212
+ if (!sameWall(utc, year, month, day, minutes, timeZone)) return null;
10213
+ return utc;
10214
+ };
10215
+ const wallMinuteToInstants = (year, month, day, minutes, timeZone) => {
10216
+ const hour = Math.floor(minutes / 60);
10217
+ const minute = minutes % 60;
10218
+ const monthIndex = month - 1;
10219
+ const wall = Date.UTC(year, monthIndex, day, hour, minute, 0);
10220
+ const zone = safeTimeZone(timeZone);
10221
+ const found = /* @__PURE__ */ new Map();
10222
+ const keep = (utc, hours) => {
10223
+ found.set(utc.getTime(), utc);
10224
+ zoneOffsetHours.set(zone, hours);
10225
+ const hour = MINUTE_MS$1 * 60;
10226
+ for (const delta of [hour, -36e5]) {
10227
+ const other = new Date(utc.getTime() + delta);
10228
+ const parts = zonedParts(other, timeZone);
10229
+ const sameDay = parts.year === year && parts.month === month;
10230
+ const sameMinute = parts.hour * 60 + parts.minute === minutes;
10231
+ if (sameDay && parts.day === day && sameMinute) found.set(other.getTime(), other);
10232
+ }
10233
+ };
10234
+ const cached = zoneOffsetHours.get(zone);
10235
+ const seeds = cached == null ? [] : [
10236
+ cached - 1,
10237
+ cached,
10238
+ cached + 1
10239
+ ];
10240
+ for (const hours of seeds) {
10241
+ const utc = instantAtOffset(wall, hours, year, month, day, minutes, timeZone);
10242
+ if (utc) keep(utc, hours);
10243
+ }
10244
+ if (found.size === 0) for (let hours = -14; hours <= 14; hours += 1) {
10245
+ const utc = instantAtOffset(wall, hours, year, month, day, minutes, timeZone);
10246
+ if (utc) keep(utc, hours);
10247
+ }
10248
+ return [...found.values()].sort((a, b) => a.getTime() - b.getTime());
10249
+ };
10250
+ const addCalendarDays = (year, month, day, days) => {
10251
+ const utc = new Date(Date.UTC(year, month - 1, day + days));
10252
+ return {
10253
+ year: utc.getUTCFullYear(),
10254
+ month: utc.getUTCMonth() + 1,
10255
+ day: utc.getUTCDate()
10256
+ };
10257
+ };
10258
+ const WEEKDAYS = [
10259
+ "sunday",
10260
+ "monday",
10261
+ "tuesday",
10262
+ "wednesday",
10263
+ "thursday",
10264
+ "friday",
10265
+ "saturday"
10266
+ ];
10267
+ const weekdayOf = (year, month, day) => {
10268
+ const utc = new Date(Date.UTC(year, month - 1, day));
10269
+ return WEEKDAYS[utc.getUTCDay()] ?? "sunday";
10270
+ };
10271
+ const labelFormatters = /* @__PURE__ */ new Map();
10272
+ const scheduleLabel = (instant, timeZone, locale = "en-US") => {
10273
+ const zone = safeTimeZone(timeZone);
10274
+ const key = `${locale}|${zone}`;
10275
+ const cached = labelFormatters.get(key);
10276
+ if (cached) return cached.format(instant);
10277
+ const date = {
10278
+ month: "short",
10279
+ day: "numeric"
10280
+ };
10281
+ const time = {
10282
+ hour: "numeric",
10283
+ minute: "2-digit"
10284
+ };
10285
+ const opts = {
10286
+ timeZone: zone,
10287
+ ...date,
10288
+ ...time
10289
+ };
10290
+ const format = new Intl.DateTimeFormat(locale, opts);
10291
+ labelFormatters.set(key, format);
10292
+ return format.format(instant);
10293
+ };
10294
+ /** Absolute UTC instant. Naive strings are read in `timeZone`. */
10295
+ const scheduledInstant = (value, timeZone) => {
10296
+ const trimmed = value.trim();
10297
+ if (!trimmed) return null;
10298
+ if (/(?:z|[+-]\d{2}:?\d{2})$/i.test(trimmed)) {
10299
+ const date = new Date(trimmed);
10300
+ if (Number.isNaN(date.getTime())) return null;
10301
+ return date.toISOString();
10302
+ }
10303
+ const match = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})/.exec(trimmed);
10304
+ if (!match) return null;
10305
+ const year = Number(match[1]);
10306
+ const month = Number(match[2]);
10307
+ const day = Number(match[3]);
10308
+ const minutes = Number(match[4]) * 60 + Number(match[5]);
10309
+ const first = wallMinuteToInstants(year, month, day, minutes, timeZone)[0];
10310
+ if (!first) return null;
10311
+ return first.toISOString();
10312
+ };
10313
+ const dayPeriods$1 = (schedule, weekday) => {
10314
+ return schedule.find((row) => row.day_of_week === weekday)?.periods ?? [];
10315
+ };
10316
+ const periodMinutes = (start, end) => {
10317
+ const [sh = "0", sm = "0"] = start.split(":");
10318
+ const [eh = "0", em = "0"] = end.split(":");
10319
+ const startMin = Number(sh) * 60 + Number(sm);
10320
+ let endMin = Number(eh) * 60 + Number(em);
10321
+ if (endMin <= startMin) endMin += DAY_MINUTES;
10322
+ return {
10323
+ startMin,
10324
+ endMin
10325
+ };
10326
+ };
10327
+ const DAY_MINUTES_COUNT = DAY_MINUTES;
10328
+ //#endregion
10329
+ //#region src/schedule/scheduleInZone.ts
10330
+ const timeToMinutes = (time) => {
10331
+ const [hours = "0", minutes = "0"] = time.split(":");
10332
+ return Number(hours) * 60 + Number(minutes);
10333
+ };
10334
+ const hasSpecificDates = (period) => {
10335
+ return period.start_date != null && period.end_date != null;
10336
+ };
10337
+ const periodCoversDate = (period, instantMs) => {
10338
+ if (!hasSpecificDates(period)) return false;
10339
+ const startMs = Date.parse(period.start_date);
10340
+ const endMs = Date.parse(period.end_date);
10341
+ if (Number.isNaN(startMs) || Number.isNaN(endMs)) return false;
10342
+ return startMs <= instantMs && instantMs <= endMs;
10343
+ };
10344
+ const timeMatches = (period, now) => {
10345
+ if (!period.start_time || !period.end_time) return false;
10346
+ const nowMinutes = now.getHours() * 60 + now.getMinutes();
10347
+ const start = timeToMinutes(period.start_time);
10348
+ const end = timeToMinutes(period.end_time);
10349
+ if (end <= start) return nowMinutes >= start;
10350
+ return nowMinutes >= start && nowMinutes < end;
10351
+ };
10352
+ const isClosedAt = (period, now) => {
10353
+ if (!period.closed) return false;
10354
+ if (!period.start_time || !period.end_time) return true;
10355
+ return timeMatches(period, now);
10356
+ };
10357
+ const isOpenAt = (period, now) => {
10358
+ if (period.closed) return false;
10359
+ return timeMatches(period, now);
10360
+ };
10361
+ const getOpenPeriodAt = (periods, now, instantMs) => {
10362
+ if (periods.length === 0) return null;
10363
+ const coveringSpecific = periods.filter((period) => {
10364
+ return periodCoversDate(period, instantMs);
10365
+ });
10366
+ const fallback = periods.filter((period) => !hasSpecificDates(period));
10367
+ const specificOpens = coveringSpecific.filter((period) => !period.closed);
10368
+ if (coveringSpecific.filter((period) => period.closed).some((period) => isClosedAt(period, now))) return null;
10369
+ if (specificOpens.length > 0) return specificOpens.find((period) => isOpenAt(period, now)) ?? null;
10370
+ if (fallback.some((period) => isClosedAt(period, now))) return null;
10371
+ return fallback.filter((period) => !period.closed).find((period) => isOpenAt(period, now)) ?? null;
10372
+ };
10373
+ const closedNow = (periods, now, instantMs) => {
10374
+ return periods.some((period) => {
10375
+ if (!period.closed) return false;
10376
+ if (hasSpecificDates(period) && !periodCoversDate(period, instantMs)) return false;
10377
+ return isClosedAt(period, now);
10378
+ });
10379
+ };
10380
+ const isOvernightTail = (period, nowMinutes) => {
10381
+ if (period.closed || !period.start_time || !period.end_time) return false;
10382
+ const start = timeToMinutes(period.start_time);
10383
+ const end = timeToMinutes(period.end_time);
10384
+ if (end > start) return false;
10385
+ return nowMinutes < end;
10386
+ };
10387
+ const inPreviousOvernight = (schedule, now, instantMs) => {
10388
+ const day = WEEKDAYS[(now.getDay() + 6) % 7] ?? "sunday";
10389
+ const periods = schedule.find((row) => row.day_of_week === day)?.periods ?? [];
10390
+ const nowMinutes = now.getHours() * 60 + now.getMinutes();
10391
+ const specific = periods.filter((period) => {
10392
+ return periodCoversDate(period, instantMs);
10393
+ }).filter((period) => hasSpecificDates(period));
10394
+ const fallback = periods.filter((period) => !hasSpecificDates(period));
10395
+ if (specific.some((period) => isClosedAt(period, now))) return false;
10396
+ const opens = specific.filter((period) => !period.closed);
10397
+ if (opens.length > 0) return opens.some((period) => isOvernightTail(period, nowMinutes));
10398
+ if (fallback.some((period) => isClosedAt(period, now))) return false;
10399
+ return fallback.some((period) => isOvernightTail(period, nowMinutes));
10400
+ };
10401
+ const dayPeriods = (schedule, instant) => {
10402
+ const day = WEEKDAYS[instant.getDay()] ?? "sunday";
10403
+ return schedule.find((row) => row.day_of_week === day)?.periods ?? [];
10404
+ };
10405
+ /** Slot checks only. Existing menu and product callers stay on isOnSchedule. */
10406
+ const isOnScheduleInZone = (schedule, instant, timeZone) => {
10407
+ if (schedule == null) return true;
10408
+ const clock = wallClockDate(instant, timeZone);
10409
+ const periods = dayPeriods(schedule, clock);
10410
+ const instantMs = instant.getTime();
10411
+ if (getOpenPeriodAt(periods, clock, instantMs) != null) return true;
10412
+ if (closedNow(periods, clock, instantMs)) return false;
10413
+ return inPreviousOvernight(schedule, clock, instantMs);
10414
+ };
10415
+ //#endregion
10416
+ //#region src/schedule/buildScheduleSlots.ts
10417
+ const alwaysOpen = () => {
10418
+ return WEEKDAYS.map((day_of_week) => {
10419
+ return {
10420
+ day_of_week,
10421
+ periods: [{
10422
+ closed: false,
10423
+ start_time: "00:00",
10424
+ end_time: "24:00"
10425
+ }]
10426
+ };
10427
+ });
10428
+ };
10429
+ const MINUTE_MS = 6e4;
10430
+ const prepLeadMs = (cart, minLead) => {
10431
+ const lead = minLead * MINUTE_MS;
10432
+ const rules = cart?.prepTime?.rules;
10433
+ if (!rules?.length) return lead;
10434
+ const lines = (cart?.lines ?? []).map((line) => {
10435
+ return {
10436
+ id: line.id ?? "",
10437
+ quantity: line.quantity ?? 1,
10438
+ type: line.type,
10439
+ parent_uid: line.parent_uid
10440
+ };
10441
+ });
10442
+ const prep = resolveSchedulePrepMinutes({
10443
+ rules,
10444
+ order: {
10445
+ total: cart?.total ?? 0,
10446
+ is_anonymous: cart?.is_anonymous ?? false,
10447
+ lines
10448
+ },
10449
+ preparingCount: cart?.preparingCount
10450
+ });
10451
+ return Math.max(lead, prep * MINUTE_MS);
10452
+ };
10453
+ const insideWindow = (line, instant) => {
10454
+ const windows = line.windows ?? [];
10455
+ if (windows.length === 0) return true;
10456
+ const ms = instant.getTime();
10457
+ return windows.some((window) => {
10458
+ const start = Date.parse(window.start);
10459
+ const end = Date.parse(window.end);
10460
+ if (Number.isNaN(start) || Number.isNaN(end)) return false;
10461
+ return ms >= start && ms < end;
10462
+ });
10463
+ };
10464
+ const lineAllows = (line, instant, timeZone) => {
10465
+ if (line.unavailable) return false;
10466
+ if (line.inactiveUntil) {
10467
+ const until = Date.parse(line.inactiveUntil);
10468
+ if (!Number.isNaN(until) && instant.getTime() < until) return false;
10469
+ }
10470
+ const productOpen = isOnScheduleInZone(line.schedules, instant, timeZone);
10471
+ if (line.schedules && !productOpen) return false;
10472
+ const category = line.categorySchedules;
10473
+ const categoryOpen = isOnScheduleInZone(category, instant, timeZone);
10474
+ if (category && !categoryOpen) return false;
10475
+ return insideWindow(line, instant);
10476
+ };
10477
+ const cartAllows = (cart, instant, timeZone) => {
10478
+ return (cart?.lines ?? []).every((line) => lineAllows(line, instant, timeZone));
10479
+ };
10480
+ const storeAllows = (config, instant, timeZone) => {
10481
+ const status = config.storeStatus;
10482
+ if (status && status !== "OPEN") return false;
10483
+ const hours = config.storeSchedule;
10484
+ if (!hours) return true;
10485
+ return isOnScheduleInZone(hours, instant, timeZone);
10486
+ };
10487
+ /** True when `instant` is one stepped slot the menu, store, and cart allow. */
10488
+ const isScheduleSlot = (instant, config) => {
10489
+ if (!Number.isFinite(instant.getTime())) return false;
10490
+ const { timezone, stepMinutes } = config;
10491
+ if (stepMinutes <= 0) return false;
10492
+ if (config.menuSchedule && config.menuSchedule.length === 0) return false;
10493
+ const parts = zonedParts(instant, timezone);
10494
+ if (parts.second !== 0) return false;
10495
+ if ((parts.hour * 60 + parts.minute) % stepMinutes !== 0) return false;
10496
+ if (!isOnScheduleInZone(config.menuSchedule ?? alwaysOpen(), instant, timezone)) return false;
10497
+ if (!storeAllows(config, instant, timezone)) return false;
10498
+ return cartAllows(config.cart, instant, timezone);
10499
+ };
10500
+ /**
10501
+ * Stepped instants inside menu hours, in the store timezone.
10502
+ * Drops the past, anything before now + lead (and prep, when a cart is
10503
+ * given), and slots a cart line cannot sell.
10504
+ */
10505
+ const buildScheduleSlots = (config) => {
10506
+ const { timezone, stepMinutes } = config;
10507
+ if (stepMinutes <= 0) return [];
10508
+ if (config.menuSchedule && config.menuSchedule.length === 0) return [];
10509
+ const menuSchedule = config.menuSchedule ?? alwaysOpen();
10510
+ const now = config.now ?? /* @__PURE__ */ new Date();
10511
+ const minLead = config.minLeadTime ?? 60;
10512
+ const horizonMin = config.maxHorizonMinutes ?? 2880;
10513
+ const readyAt = now.getTime() + prepLeadMs(config.cart, minLead);
10514
+ const latest = now.getTime() + horizonMin * MINUTE_MS;
10515
+ const start = zonedParts(now, timezone);
10516
+ const seen = /* @__PURE__ */ new Set();
10517
+ const slots = [];
10518
+ const horizonDays = Math.ceil(horizonMin / DAY_MINUTES_COUNT);
10519
+ const dayCount = Math.min(horizonDays, 7) + 1;
10520
+ for (let offset = -1; offset < dayCount; offset += 1) {
10521
+ const date = addCalendarDays(start.year, start.month, start.day, offset);
10522
+ const periods = dayPeriods$1(menuSchedule, weekdayOf(date.year, date.month, date.day));
10523
+ for (const period of periods) {
10524
+ if (period.closed || !period.start_time || !period.end_time) continue;
10525
+ const window = periodMinutes(period.start_time, period.end_time);
10526
+ let minute = Math.ceil(window.startMin / stepMinutes) * stepMinutes;
10527
+ for (; minute < window.endMin; minute += stepMinutes) {
10528
+ const spill = Math.floor(minute / DAY_MINUTES_COUNT);
10529
+ const minOfDay = minute % DAY_MINUTES_COUNT;
10530
+ const at = addCalendarDays(date.year, date.month, date.day, spill);
10531
+ const instants = wallMinuteToInstants(...[
10532
+ at.year,
10533
+ at.month,
10534
+ at.day,
10535
+ minOfDay,
10536
+ timezone
10537
+ ]);
10538
+ for (const instant of instants) {
10539
+ const ms = instant.getTime();
10540
+ if (seen.has(ms) || ms < readyAt || ms > latest) continue;
10541
+ if (!isOnScheduleInZone(menuSchedule, instant, timezone)) continue;
10542
+ if (!storeAllows(config, instant, timezone)) continue;
10543
+ if (!cartAllows(config.cart, instant, timezone)) continue;
10544
+ seen.add(ms);
10545
+ const startIso = instant.toISOString();
10546
+ const label = scheduleLabel(instant, timezone, config.locale);
10547
+ slots.push({
10548
+ start: startIso,
10549
+ label
10550
+ });
10551
+ }
10552
+ }
10553
+ }
10554
+ }
10555
+ slots.sort((left, right) => {
10556
+ if (left.start < right.start) return -1;
10557
+ if (left.start > right.start) return 1;
10558
+ return 0;
10559
+ });
10560
+ return slots;
10561
+ };
10562
+ //#endregion
10563
+ //#region src/logs/serializeError.ts
10564
+ const UNKNOWN_MESSAGE = "Unknown error";
10565
+ const MAX_CAUSE_DEPTH = 3;
10566
+ const safeStringify = (value) => {
10567
+ try {
10568
+ const json = JSON.stringify(value);
10569
+ return json === void 0 ? String(value) : json;
10570
+ } catch {
10571
+ return String(value);
10572
+ }
10573
+ };
10574
+ const messageOf = (value) => {
10575
+ if (value instanceof Error) return value.message || value.name;
10576
+ if (typeof value === "string") return value;
10577
+ if (value && typeof value === "object" && "message" in value) {
10578
+ const message = value.message;
10579
+ if (typeof message === "string") return message;
10580
+ }
10581
+ if (value === void 0) return "";
10582
+ return safeStringify(value);
10583
+ };
10584
+ /** `TypeError: boom ← caused by: Error: socket closed` style chain. */
10585
+ const describeCause = (cause, depth = 0) => {
10586
+ if (cause instanceof Error) {
10587
+ const head = `${cause.name}: ${cause.message}`;
10588
+ const inner = cause.cause;
10589
+ if (inner === void 0 || depth >= MAX_CAUSE_DEPTH) return head;
10590
+ return `${head} ← caused by: ${describeCause(inner, depth + 1)}`;
10591
+ }
10592
+ return messageOf(cause);
10593
+ };
10594
+ const nameOf = (value) => {
10595
+ if (value instanceof Error) return value.name;
10596
+ if (value && typeof value === "object" && "name" in value) {
10597
+ const name = value.name;
10598
+ if (typeof name === "string") return name;
10599
+ }
10600
+ return null;
10601
+ };
10602
+ const stackOf = (value) => {
10603
+ if (value && typeof value === "object" && "stack" in value) {
10604
+ const stack = value.stack;
10605
+ if (typeof stack === "string" && stack) return stack;
10606
+ }
10607
+ return null;
10608
+ };
10609
+ const causeOf = (value) => {
10610
+ if (!value || typeof value !== "object" || !("cause" in value)) return null;
10611
+ const cause = value.cause;
10612
+ if (cause === void 0 || cause === null) return null;
10613
+ return describeCause(cause) || null;
10614
+ };
10615
+ /** Accepts anything a `catch` can receive. */
10616
+ const serializeError = (value) => {
10617
+ const message = truncate(messageOf(value).trim() || UNKNOWN_MESSAGE, LOG_LIMITS.message);
10618
+ const name = nameOf(value);
10619
+ const stack = stackOf(value);
10620
+ const cause = causeOf(value);
10621
+ const e1 = { name: name ? truncate(name, LOG_LIMITS.errorName) : null };
10622
+ const e2 = { stack: stack ? truncate(stack, LOG_LIMITS.stack) : null };
10623
+ const e3 = { cause: cause ? truncate(cause, LOG_LIMITS.cause) : null };
10624
+ return {
10625
+ message,
10626
+ error: {
10627
+ ...e1,
10628
+ ...e2,
10629
+ ...e3
10630
+ }
10631
+ };
10632
+ };
10633
+ //#endregion
10634
+ //#region src/logs/log.ts
10635
+ const LOGS_PATH = "/api/storefront/v1/logs";
10636
+ const POS_LOGS_PATH = "/api/storefront/v1/pos/logs";
10637
+ const FLUSH_DELAY_MS = 500;
10638
+ let queue = [];
10639
+ let timer = null;
10640
+ const optionalText = (value, max) => {
10641
+ const trimmed = value?.trim();
10642
+ if (!trimmed) return null;
10643
+ return truncate(trimmed, max);
10644
+ };
10645
+ const buildEntry = (level, message, options, error) => {
10646
+ const text = typeof message === "string" ? message.trim() : "";
10647
+ if (!text) return null;
10648
+ const b1 = {
10649
+ level,
10650
+ message: truncate(text, LOG_LIMITS.message)
10651
+ };
10652
+ const b2 = { alias: optionalText(options.alias, LOG_LIMITS.alias) };
10653
+ const b3 = { detail: optionalText(options.detail, LOG_LIMITS.detail) };
10654
+ const b4 = { status: optionalText(options.status, LOG_LIMITS.status) };
10655
+ const b5 = {
10656
+ error: error ?? null,
10657
+ occurred_at: (/* @__PURE__ */ new Date()).toISOString()
10658
+ };
10659
+ return {
10660
+ ...b1,
10661
+ ...b2,
10662
+ ...b3,
10663
+ ...b4,
10664
+ ...getLogContext(),
10665
+ ...b5
10666
+ };
10667
+ };
10668
+ const isAuthConfigured = () => {
10669
+ try {
10670
+ getBaseUrl();
10671
+ return true;
10672
+ } catch {
10673
+ return false;
10674
+ }
10675
+ };
10676
+ /** Browsers cap keepalive bodies at 64 KB; bigger batches send normally. */
10677
+ const postInit = (body) => {
10678
+ return {
10679
+ method: "POST",
10680
+ body,
10681
+ keepalive: body.length < LOG_LIMITS.keepaliveBytes
10682
+ };
10683
+ };
10684
+ const sendPos = async (device, logs) => {
10685
+ const fetchImpl = device.http.fetchImpl ?? fetch;
10686
+ const url = `${resolveBaseUrl(device.http.baseUrl)}${POS_LOGS_PATH}`;
10687
+ const { device_uid, device_secret } = device;
10688
+ const body = JSON.stringify({
10689
+ device_uid,
10690
+ device_secret,
10691
+ logs
10692
+ });
10693
+ const headers = { "Content-Type": "application/json" };
10694
+ return (await fetchImpl(url, {
10695
+ ...postInit(body),
10696
+ headers
10697
+ })).ok;
10698
+ };
10699
+ const sendBatch = async (logs) => {
10700
+ try {
10701
+ const device = getPosLogDevice();
10702
+ if (device) return await sendPos(device, logs);
10703
+ if (!isAuthConfigured()) return false;
10704
+ await storefrontJson(LOGS_PATH, postInit(JSON.stringify({ logs })));
10705
+ return true;
10706
+ } catch {
10707
+ return false;
10708
+ }
10709
+ };
10710
+ /** Sends queued logs now; resolves once every batch has been attempted. */
10711
+ const flushLogs = async () => {
10712
+ if (timer) clearTimeout(timer);
10713
+ timer = null;
10714
+ const pending = queue;
10715
+ queue = [];
10716
+ const jobs = [];
10717
+ for (let i = 0; i < pending.length; i += LOG_LIMITS.batch) {
10718
+ const chunk = pending.slice(i, i + LOG_LIMITS.batch);
10719
+ const entries = chunk.map((item) => item.entry);
10720
+ const job = sendBatch(entries).then((sent) => {
10721
+ for (const item of chunk) item.resolve(sent);
10722
+ });
10723
+ jobs.push(job);
10724
+ }
10725
+ await Promise.all(jobs);
10726
+ };
10727
+ const enqueue = (entry) => {
10728
+ return new Promise((resolve) => {
10729
+ queue.push({
10730
+ entry,
10731
+ resolve
10732
+ });
10733
+ if (queue.length >= LOG_LIMITS.batch) {
10734
+ flushLogs();
10735
+ return;
10736
+ }
10737
+ if (!timer) timer = setTimeout(() => void flushLogs(), FLUSH_DELAY_MS);
10738
+ });
10739
+ };
10740
+ /**
10741
+ * Reports a log to the merchant dashboard with the SDK's current context
10742
+ * (channel, fulfillment, store, brand, device). Never throws; resolves to
10743
+ * `false` when the log could not be delivered.
10744
+ */
10745
+ const log = (level, message, options = {}) => {
10746
+ const entry = buildEntry(level, message, options);
10747
+ if (!entry) return Promise.resolve(false);
10748
+ return enqueue(entry);
10749
+ };
10750
+ /** `log("error", …)` with message, name, stack and cause read from `error`. */
10751
+ const logError = (error, options = {}) => {
10752
+ const { message, error: info } = serializeError(error);
10753
+ const entry = buildEntry("error", message, options, info);
10754
+ if (!entry) return Promise.resolve(false);
10755
+ return enqueue(entry);
10756
+ };
10757
+ //#endregion
10758
+ //#region src/webhooks/verifyWebhookSignature.ts
10759
+ const SIGNATURE_PREFIX = "sha256=";
10760
+ const hmacHex = async (secret, body) => {
10761
+ const raw = new TextEncoder().encode(secret);
10762
+ const key = await crypto.subtle.importKey("raw", raw, {
10763
+ name: "HMAC",
10764
+ hash: "SHA-256"
10765
+ }, false, ["sign"]);
10766
+ const data = new TextEncoder().encode(body);
10767
+ const sig = new Uint8Array(await crypto.subtle.sign("HMAC", key, data));
10768
+ return Array.from(sig, (byte) => byte.toString(16).padStart(2, "0")).join("");
10769
+ };
10770
+ const safeEqual = (left, right) => {
10771
+ if (left.length !== right.length) return false;
10772
+ let diff = 0;
10773
+ for (let i = 0; i < left.length; i += 1) diff |= left.charCodeAt(i) ^ right.charCodeAt(i);
10774
+ return diff === 0;
10775
+ };
10776
+ /** Checks a Fire Spark webhook/callback signature in constant time. */
10777
+ const verifyWebhookSignature = async (input) => {
10778
+ const { secret, body, signature } = input;
10779
+ if (!secret || !signature?.startsWith(SIGNATURE_PREFIX)) return false;
10780
+ const received = signature.slice(7).toLowerCase();
10781
+ return safeEqual(received, await hmacHex(secret, body));
10782
+ };
10783
+ //#endregion
9690
10784
  //#region src/rules/checkout.ts
9691
10785
  const DEFAULT_SHIPPING_FIELDS = {
9692
10786
  address_line1: "required",
@@ -10149,4 +11243,4 @@ const cardPreviewGroups = (value) => {
10149
11243
  return groups;
10150
11244
  };
10151
11245
  //#endregion
10152
- 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, redeemsForPayment, redemptionCap, redemptionSteps, remainingAmount, removeLine, replaceLine, resolveCheckout, resolveModifierImageUrl, reversePayment, runPaymentAttempt, selectablePayments, serializeError, services, setLineQuantity, storefrontJson, toOrderLines, toPaymentCardBrand, validateCardNumber, validateCardholderName, validateCvc, validateExpiry, validateOrderAgainstMenu, verifyWebhookSignature, visitedStates };
11246
+ export { CARD_BRANDS, CUSTOMER_INFO_PLACEHOLDERS, DEFAULT_RECOMMENDATION_TYPES, DelegatedSessionExpiredError, ID_HASH_FORMATS, Order, OrderAuthPinnedError, OrderNotOpenError, OrderPaymentRequiredError, OrderSyncPendingError, OrderSyncRejectedError, OrderValidationError, RecurringDateCron, RulesEngine, SCHEDULED_AT_ERROR_CODES, SCHEDULED_ORDER_MAX_HORIZON_HOURS, SCHEDULED_ORDER_MAX_HORIZON_MINUTES, SCHEDULED_ORDER_MIN_LEAD_MINUTES, SCHEDULED_ORDER_SLOT_MINUTES, StorefrontHttpError, addLines, auth, buildCustomerSnapshot, buildScheduleSlots, 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, isDelegatedSessionExpiredError, isDeviceCharged, isDeviceNotLinked, isExpiryComplete, isGenId, isKioskUnauthorized, isMilestoneStepDone, isOnSchedule, isOrderNotOpenError, isOrderPaymentRequiredError, isOrderValidationError, isPosDeviceNotLinked, isPosUnauthorized, isRealPaymentMethod, isScheduleSlot, isScheduledAtError, 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, redeemsForPayment, redemptionCap, redemptionSteps, remainingAmount, removeLine, replaceLine, resolveCheckout, resolveModifierImageUrl, resolveSchedulePrepMinutes, reversePayment, runPaymentAttempt, scheduledInstant, selectablePayments, serializeError, services, setLineQuantity, storefrontJson, toOrderLines, toPaymentCardBrand, validateCardNumber, validateCardholderName, validateCvc, validateExpiry, validateOrderAgainstMenu, verifyWebhookSignature, visitedStates };