@fire-spark/core 0.15.2 → 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.
- package/dist/index.d.ts +197 -4
- package/dist/index.js +1404 -291
- 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
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
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
|
|
@@ -2565,6 +2813,22 @@ const fetchKioskRealtimeToken = async (input, options) => {
|
|
|
2565
2813
|
return (await response.json()).data;
|
|
2566
2814
|
};
|
|
2567
2815
|
//#endregion
|
|
2816
|
+
//#region src/services/masivo.ts
|
|
2817
|
+
const VERIFY_PATH = "/api/storefront/v1/masivo/totp/verify";
|
|
2818
|
+
const CREATE_PATH = "/api/storefront/v1/masivo/totp";
|
|
2819
|
+
const verifyMasivoTotp = (input, options) => {
|
|
2820
|
+
const params = new URLSearchParams({ code: input.code });
|
|
2821
|
+
if (input.order_id) params.set("order_id", input.order_id);
|
|
2822
|
+
return storefrontJson(`${VERIFY_PATH}?${params.toString()}`, {}, options);
|
|
2823
|
+
};
|
|
2824
|
+
const createMasivoTotp = (input, options) => {
|
|
2825
|
+
const init = {
|
|
2826
|
+
method: "POST",
|
|
2827
|
+
body: JSON.stringify({ customer_id: input.customer_id })
|
|
2828
|
+
};
|
|
2829
|
+
return storefrontJson(CREATE_PATH, init, options);
|
|
2830
|
+
};
|
|
2831
|
+
//#endregion
|
|
2568
2832
|
//#region src/services/menus.ts
|
|
2569
2833
|
const menuQuery = (scope, stream) => {
|
|
2570
2834
|
const params = new URLSearchParams();
|
|
@@ -2741,6 +3005,36 @@ const updateOrder = (id, body, options) => {
|
|
|
2741
3005
|
options
|
|
2742
3006
|
]);
|
|
2743
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
|
+
};
|
|
2744
3038
|
const postCancel = (path, options, body) => {
|
|
2745
3039
|
const method = "POST";
|
|
2746
3040
|
if (!body) return storefrontJson(path, { method }, options);
|
|
@@ -3231,11 +3525,17 @@ const services = {
|
|
|
3231
3525
|
get: listMenus,
|
|
3232
3526
|
stream: listMenusStream
|
|
3233
3527
|
},
|
|
3528
|
+
masivo: {
|
|
3529
|
+
verifyTotp: verifyMasivoTotp,
|
|
3530
|
+
createTotp: createMasivoTotp
|
|
3531
|
+
},
|
|
3234
3532
|
orders: {
|
|
3235
3533
|
list: listOrders,
|
|
3236
3534
|
post: createOrder,
|
|
3237
3535
|
get: getOrder,
|
|
3238
3536
|
patch: updateOrder,
|
|
3537
|
+
setCustomer: setOrderCustomer,
|
|
3538
|
+
clearCustomer: clearOrderCustomer,
|
|
3239
3539
|
cancel: cancelOrder,
|
|
3240
3540
|
refund: refundOrder,
|
|
3241
3541
|
reorder: reorderOrder,
|
|
@@ -3457,6 +3757,22 @@ const isGenId = (value) => {
|
|
|
3457
3757
|
return HASH_RUN.test(value);
|
|
3458
3758
|
};
|
|
3459
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
|
|
3460
3776
|
//#region src/order/OrderPaymentRequiredError.ts
|
|
3461
3777
|
/**
|
|
3462
3778
|
* `placeOrder` has no real tender (no `payment`, no `setTenders`, no
|
|
@@ -3555,6 +3871,7 @@ const withExponentialRetry = async (run, options = {}) => {
|
|
|
3555
3871
|
return await run();
|
|
3556
3872
|
} catch (error) {
|
|
3557
3873
|
lastError = error;
|
|
3874
|
+
if ((error.code ?? "").startsWith("SCHEDULED_AT_")) throw error;
|
|
3558
3875
|
if (attempt === extraAttempts) break;
|
|
3559
3876
|
await sleep(baseDelayMs * 2 ** attempt);
|
|
3560
3877
|
}
|
|
@@ -3580,6 +3897,24 @@ var OrderSyncRejectedError = class extends StorefrontHttpError {
|
|
|
3580
3897
|
this.name = "OrderSyncRejectedError";
|
|
3581
3898
|
}
|
|
3582
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
|
+
};
|
|
3583
3918
|
const createFlushController = (runtime) => {
|
|
3584
3919
|
let timer = null;
|
|
3585
3920
|
let inFlight = false;
|
|
@@ -3594,7 +3929,7 @@ const createFlushController = (runtime) => {
|
|
|
3594
3929
|
pending = false;
|
|
3595
3930
|
rejected = null;
|
|
3596
3931
|
};
|
|
3597
|
-
const patchBody = (order) => {
|
|
3932
|
+
const patchBody = (order, synced) => {
|
|
3598
3933
|
const b1 = {
|
|
3599
3934
|
store_id: order.store_id,
|
|
3600
3935
|
channel_id: order.channel_id
|
|
@@ -3613,13 +3948,17 @@ const createFlushController = (runtime) => {
|
|
|
3613
3948
|
redeem: order.redeem
|
|
3614
3949
|
};
|
|
3615
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 };
|
|
3616
3954
|
return {
|
|
3617
3955
|
...b1,
|
|
3618
3956
|
...b2,
|
|
3619
3957
|
...b3,
|
|
3620
3958
|
...b4,
|
|
3621
3959
|
...b5,
|
|
3622
|
-
...b6
|
|
3960
|
+
...b6,
|
|
3961
|
+
...slot
|
|
3623
3962
|
};
|
|
3624
3963
|
};
|
|
3625
3964
|
const insertBody = (order) => {
|
|
@@ -3643,6 +3982,7 @@ const createFlushController = (runtime) => {
|
|
|
3643
3982
|
};
|
|
3644
3983
|
const b7 = { redeem: order.redeem };
|
|
3645
3984
|
const b8 = { redemption_steps: order.redemption_steps };
|
|
3985
|
+
const slot = order.scheduled_at ? { scheduled_at: order.scheduled_at } : {};
|
|
3646
3986
|
return {
|
|
3647
3987
|
...b1,
|
|
3648
3988
|
...b2,
|
|
@@ -3651,7 +3991,8 @@ const createFlushController = (runtime) => {
|
|
|
3651
3991
|
...b5,
|
|
3652
3992
|
...b6,
|
|
3653
3993
|
...b7,
|
|
3654
|
-
...b8
|
|
3994
|
+
...b8,
|
|
3995
|
+
...slot
|
|
3655
3996
|
};
|
|
3656
3997
|
};
|
|
3657
3998
|
const flushNow = () => {
|
|
@@ -3704,9 +4045,10 @@ const createFlushController = (runtime) => {
|
|
|
3704
4045
|
try {
|
|
3705
4046
|
const updated = await withExponentialRetry(() => {
|
|
3706
4047
|
if (!wasPersisted) return services.orders.post(insertBody(payload), runtime.http);
|
|
4048
|
+
const body = patchBody(payload, batchBase);
|
|
3707
4049
|
const p1 = [
|
|
3708
4050
|
payload.id,
|
|
3709
|
-
|
|
4051
|
+
body,
|
|
3710
4052
|
runtime.http
|
|
3711
4053
|
];
|
|
3712
4054
|
return services.orders.patch(...p1);
|
|
@@ -5093,51 +5435,65 @@ const DAY_OF_WEEK = [
|
|
|
5093
5435
|
"friday",
|
|
5094
5436
|
"saturday"
|
|
5095
5437
|
];
|
|
5096
|
-
const timeToMinutes = (time) => {
|
|
5438
|
+
const timeToMinutes$1 = (time) => {
|
|
5097
5439
|
const [hours = "0", minutes = "0"] = time.split(":");
|
|
5098
5440
|
return Number(hours) * 60 + Number(minutes);
|
|
5099
5441
|
};
|
|
5100
|
-
const hasSpecificDates = (period) => {
|
|
5442
|
+
const hasSpecificDates$1 = (period) => {
|
|
5101
5443
|
return period.start_date != null && period.end_date != null;
|
|
5102
5444
|
};
|
|
5103
|
-
const periodCoversDate = (period, now) => {
|
|
5104
|
-
if (!hasSpecificDates(period)) return false;
|
|
5445
|
+
const periodCoversDate$1 = (period, now) => {
|
|
5446
|
+
if (!hasSpecificDates$1(period)) return false;
|
|
5105
5447
|
const nowMs = now.getTime();
|
|
5106
5448
|
const startMs = Date.parse(period.start_date);
|
|
5107
5449
|
const endMs = Date.parse(period.end_date);
|
|
5108
5450
|
if (Number.isNaN(startMs) || Number.isNaN(endMs)) return false;
|
|
5109
5451
|
return startMs <= nowMs && nowMs <= endMs;
|
|
5110
5452
|
};
|
|
5111
|
-
|
|
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) => {
|
|
5112
5459
|
if (!period.start_time || !period.end_time) return false;
|
|
5113
5460
|
const nowMinutes = now.getHours() * 60 + now.getMinutes();
|
|
5114
|
-
const start = timeToMinutes(period.start_time);
|
|
5115
|
-
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;
|
|
5116
5464
|
return nowMinutes >= start && nowMinutes < end;
|
|
5117
5465
|
};
|
|
5118
|
-
const isClosedAt = (period, now) => {
|
|
5466
|
+
const isClosedAt$1 = (period, now) => {
|
|
5119
5467
|
if (!period.closed) return false;
|
|
5120
5468
|
if (!period.start_time || !period.end_time) return true;
|
|
5121
|
-
return timeMatches(period, now);
|
|
5469
|
+
return timeMatches$1(period, now);
|
|
5122
5470
|
};
|
|
5123
|
-
const isOpenAt = (period, now) => {
|
|
5471
|
+
const isOpenAt$1 = (period, now) => {
|
|
5124
5472
|
if (period.closed) return false;
|
|
5125
|
-
return timeMatches(period, now);
|
|
5473
|
+
return timeMatches$1(period, now);
|
|
5126
5474
|
};
|
|
5127
|
-
const isPeriodsActiveAt = (periods, now) => {
|
|
5128
|
-
return getOpenPeriodAt(periods, now) != null;
|
|
5475
|
+
const isPeriodsActiveAt = (periods, now, span = now) => {
|
|
5476
|
+
return getOpenPeriodAt$1(periods, now, span) != null;
|
|
5129
5477
|
};
|
|
5130
|
-
const getOpenPeriodAt = (periods, now) => {
|
|
5478
|
+
const getOpenPeriodAt$1 = (periods, now, span = now) => {
|
|
5131
5479
|
if (periods.length === 0) return null;
|
|
5132
5480
|
const coveringSpecific = periods.filter((period) => {
|
|
5133
|
-
return periodCoversDate(period,
|
|
5481
|
+
return periodCoversDate$1(period, span);
|
|
5134
5482
|
});
|
|
5135
|
-
const fallback = periods.filter((period) => !hasSpecificDates(period));
|
|
5483
|
+
const fallback = periods.filter((period) => !hasSpecificDates$1(period));
|
|
5136
5484
|
const specificOpens = coveringSpecific.filter((period) => !period.closed);
|
|
5137
|
-
if (coveringSpecific.filter((period) => period.closed).some((period) => isClosedAt(period, now))) return null;
|
|
5138
|
-
if (specificOpens.length > 0) return specificOpens.find((period) => isOpenAt(period, now)) ?? null;
|
|
5139
|
-
if (fallback.some((period) => isClosedAt(period, now))) return null;
|
|
5140
|
-
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;
|
|
5141
5497
|
};
|
|
5142
5498
|
const atTimeOnDate = (day, time) => {
|
|
5143
5499
|
const [hours = "0", minutes = "0"] = time.split(":");
|
|
@@ -5146,80 +5502,213 @@ const atTimeOnDate = (day, time) => {
|
|
|
5146
5502
|
return next;
|
|
5147
5503
|
};
|
|
5148
5504
|
const isWithinPeriodDates = (period, instantMs) => {
|
|
5149
|
-
if (!hasSpecificDates(period)) return true;
|
|
5505
|
+
if (!hasSpecificDates$1(period)) return true;
|
|
5150
5506
|
const startMs = Date.parse(period.start_date);
|
|
5151
5507
|
const endMs = Date.parse(period.end_date);
|
|
5152
5508
|
if (Number.isNaN(startMs) || Number.isNaN(endMs)) return false;
|
|
5153
5509
|
return startMs <= instantMs && instantMs <= endMs;
|
|
5154
5510
|
};
|
|
5155
|
-
const
|
|
5156
|
-
|
|
5511
|
+
const previousWeekday = (now) => {
|
|
5512
|
+
return DAY_OF_WEEK[(now.getDay() + 6) % 7];
|
|
5513
|
+
};
|
|
5514
|
+
const yesterdayPeriods = (schedule, now) => {
|
|
5515
|
+
const day = previousWeekday(now);
|
|
5157
5516
|
if (!day) return [];
|
|
5158
5517
|
return schedule.find((row) => row.day_of_week === day)?.periods ?? [];
|
|
5159
5518
|
};
|
|
5160
|
-
const
|
|
5161
|
-
const
|
|
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);
|
|
5162
5562
|
if (!open?.end_time) return null;
|
|
5163
|
-
|
|
5164
|
-
const
|
|
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);
|
|
5165
5568
|
for (const period of periods) {
|
|
5166
5569
|
if (!period.closed || !period.start_time) continue;
|
|
5167
5570
|
const closeMs = atTimeOnDate(now, period.start_time).getTime();
|
|
5168
5571
|
if (closeMs <= now.getTime() || closeMs >= untilMs) continue;
|
|
5169
5572
|
if (!isWithinPeriodDates(period, closeMs)) continue;
|
|
5170
|
-
if (!hasSpecificDates(period) && openIsSpecific) continue;
|
|
5573
|
+
if (!hasSpecificDates$1(period) && openIsSpecific) continue;
|
|
5171
5574
|
untilMs = closeMs;
|
|
5172
5575
|
}
|
|
5173
5576
|
return new Date(untilMs);
|
|
5174
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
|
+
};
|
|
5175
5598
|
const getNextStart = (schedule, now) => {
|
|
5176
5599
|
const nowMs = now.getTime();
|
|
5177
5600
|
let nextStartMs = null;
|
|
5178
|
-
const consider = (candidate
|
|
5601
|
+
const consider = (candidate) => {
|
|
5179
5602
|
const startMs = candidate.getTime();
|
|
5180
5603
|
if (startMs <= nowMs) return;
|
|
5181
|
-
if (!
|
|
5604
|
+
if (!scheduleActiveAt(schedule, candidate)) return;
|
|
5182
5605
|
if (nextStartMs === null || startMs < nextStartMs) nextStartMs = startMs;
|
|
5183
5606
|
};
|
|
5184
|
-
for (let dayOffset =
|
|
5607
|
+
for (let dayOffset = -1; dayOffset < 8; dayOffset += 1) {
|
|
5185
5608
|
const candidateDate = new Date(now);
|
|
5186
5609
|
candidateDate.setDate(candidateDate.getDate() + dayOffset);
|
|
5187
5610
|
candidateDate.setSeconds(0, 0);
|
|
5188
|
-
const periods = dayPeriods(schedule, candidateDate);
|
|
5611
|
+
const periods = dayPeriods$2(schedule, candidateDate);
|
|
5189
5612
|
if (periods.length === 0) continue;
|
|
5190
5613
|
for (const period of periods) {
|
|
5191
5614
|
if (period.closed || !period.start_time) continue;
|
|
5192
5615
|
const start = atTimeOnDate(candidateDate, period.start_time);
|
|
5193
5616
|
if (!isWithinPeriodDates(period, start.getTime())) continue;
|
|
5194
|
-
consider(start
|
|
5617
|
+
consider(start);
|
|
5195
5618
|
}
|
|
5196
5619
|
for (const period of periods) {
|
|
5197
5620
|
if (!period.closed || !period.end_time) continue;
|
|
5198
5621
|
const reopen = atTimeOnDate(candidateDate, period.end_time);
|
|
5622
|
+
if (rollsToNextDay(period)) reopen.setDate(reopen.getDate() + 1);
|
|
5199
5623
|
if (!isWithinPeriodDates(period, reopen.getTime())) continue;
|
|
5200
|
-
consider(reopen
|
|
5624
|
+
consider(reopen);
|
|
5201
5625
|
}
|
|
5202
5626
|
}
|
|
5203
5627
|
return nextStartMs === null ? null : new Date(nextStartMs);
|
|
5204
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
|
+
};
|
|
5205
5687
|
/** Whether `schedule` is in hours now, and until / next open. */
|
|
5206
|
-
const getScheduleWindow = (schedule, now = /* @__PURE__ */ new Date()) => {
|
|
5688
|
+
const getScheduleWindow = (schedule, now = /* @__PURE__ */ new Date(), span) => {
|
|
5207
5689
|
if (schedule == null) return {
|
|
5208
5690
|
active: true,
|
|
5209
5691
|
until: null
|
|
5210
5692
|
};
|
|
5211
|
-
const
|
|
5212
|
-
if (
|
|
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 {
|
|
5213
5700
|
active: true,
|
|
5214
|
-
until: getActiveUntil(periods, now)
|
|
5701
|
+
until: getActiveUntil(periods, now, covers)
|
|
5215
5702
|
};
|
|
5216
5703
|
return {
|
|
5217
|
-
active:
|
|
5218
|
-
|
|
5704
|
+
active: true,
|
|
5705
|
+
until: yesterdayOpenUntil(schedule, now, covers)
|
|
5219
5706
|
};
|
|
5220
5707
|
};
|
|
5221
|
-
const isOnSchedule = (schedule, now) => {
|
|
5222
|
-
|
|
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;
|
|
5223
5712
|
};
|
|
5224
5713
|
//#endregion
|
|
5225
5714
|
//#region src/order/recommendations.ts
|
|
@@ -5766,6 +6255,25 @@ const buildLocalDraft = (insert) => {
|
|
|
5766
6255
|
const sameBrand = (left, right) => {
|
|
5767
6256
|
return (optionalBrandId(left) ?? null) === (optionalBrandId(right) ?? null);
|
|
5768
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
|
+
};
|
|
5769
6277
|
/** Newest OPEN order for customer + scope, if any. */
|
|
5770
6278
|
const findOpenOrder = async (scope, http) => {
|
|
5771
6279
|
const brandId = optionalBrandId(scope.brand_id);
|
|
@@ -6173,6 +6681,72 @@ var Order = class Order {
|
|
|
6173
6681
|
};
|
|
6174
6682
|
this.scheduleAfterMutate();
|
|
6175
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
|
+
}
|
|
6176
6750
|
async cancelOrder() {
|
|
6177
6751
|
if (this.deleted) return;
|
|
6178
6752
|
this.flush.cancel();
|
|
@@ -6201,6 +6775,7 @@ var Order = class Order {
|
|
|
6201
6775
|
}
|
|
6202
6776
|
this.deleted = true;
|
|
6203
6777
|
this.notify();
|
|
6778
|
+
auth.releaseDelegatedOrder(this.draft.id);
|
|
6204
6779
|
}
|
|
6205
6780
|
/**
|
|
6206
6781
|
* Reopens this placed, unpaid order: the server voids every payment
|
|
@@ -6246,6 +6821,7 @@ var Order = class Order {
|
|
|
6246
6821
|
this.synced = cloneOrder(next);
|
|
6247
6822
|
this.deleted = true;
|
|
6248
6823
|
this.notify();
|
|
6824
|
+
auth.releaseDelegatedOrder(this.draft.id);
|
|
6249
6825
|
}
|
|
6250
6826
|
async validateOrder() {
|
|
6251
6827
|
const draft = this.getOrder();
|
|
@@ -6329,6 +6905,7 @@ var Order = class Order {
|
|
|
6329
6905
|
this.replaceDraft(cloneOrder(next));
|
|
6330
6906
|
this.synced = cloneOrder(next);
|
|
6331
6907
|
this.notify();
|
|
6908
|
+
if (next.status !== "OPEN") auth.releaseDelegatedOrder(next.id);
|
|
6332
6909
|
}
|
|
6333
6910
|
/** Keeps the applied redeems while the server holds the same capped ones. */
|
|
6334
6911
|
replaceDraft(next) {
|
|
@@ -6583,6 +7160,15 @@ var Order = class Order {
|
|
|
6583
7160
|
this.notify();
|
|
6584
7161
|
this.flush.schedule();
|
|
6585
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
|
+
}
|
|
6586
7172
|
notify() {
|
|
6587
7173
|
this.externalNotify?.();
|
|
6588
7174
|
this.listeners.forEach((listener) => listener());
|
|
@@ -7776,7 +8362,7 @@ const checkRootProductQuantities = (roots, lookup, out) => {
|
|
|
7776
8362
|
}
|
|
7777
8363
|
}
|
|
7778
8364
|
};
|
|
7779
|
-
const validateProductRoot = (root, lines, lookup, locale, out) => {
|
|
8365
|
+
const validateProductRoot = (root, lines, lookup, locale, out, at, timeZone) => {
|
|
7780
8366
|
const product = lookup.productsById[root.id];
|
|
7781
8367
|
if (!product) {
|
|
7782
8368
|
pushLineIssue(out, root.uid, { code: "PRODUCT_NOT_FOUND" });
|
|
@@ -7790,7 +8376,8 @@ const validateProductRoot = (root, lines, lookup, locale, out) => {
|
|
|
7790
8376
|
const status = product.availability.status;
|
|
7791
8377
|
if (status === "HIDDEN") pushLineIssue(out, root.uid, { code: "HIDDEN" });
|
|
7792
8378
|
if (status === "OUT_OF_STOCK") pushLineIssue(out, root.uid, { code: "OUT_OF_STOCK" });
|
|
7793
|
-
|
|
8379
|
+
const hours = product.availability.schedules;
|
|
8380
|
+
if (!isOnSchedule(hours, at, timeZone)) pushLineIssue(out, root.uid, { code: "OUTSIDE_HOURS" });
|
|
7794
8381
|
collectUnmatchedModifiers(lines, lookup, root.id, root.uid, out);
|
|
7795
8382
|
let expected;
|
|
7796
8383
|
if (needsSlotPlacement(lines, root.uid, lookup)) {
|
|
@@ -8024,6 +8611,8 @@ const compareRootGroups = (submitted, expected, out) => {
|
|
|
8024
8611
|
/** Compare cart lines/totals against a fresh menu (exact Big.js equality). */
|
|
8025
8612
|
const validateOrderAgainstMenu = (input) => {
|
|
8026
8613
|
const locale = input.locale ?? "en_us";
|
|
8614
|
+
const at = input.scheduleAt ?? /* @__PURE__ */ new Date();
|
|
8615
|
+
const zone = input.timeZone;
|
|
8027
8616
|
const issues = [];
|
|
8028
8617
|
const lineIssues = {};
|
|
8029
8618
|
if (!input.menu) return {
|
|
@@ -8048,7 +8637,18 @@ const validateOrderAgainstMenu = (input) => {
|
|
|
8048
8637
|
checkFeeSelfConsistency(root, lineIssues);
|
|
8049
8638
|
continue;
|
|
8050
8639
|
}
|
|
8051
|
-
|
|
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);
|
|
8052
8652
|
}
|
|
8053
8653
|
checkRootProductQuantities(roots, lookup, lineIssues);
|
|
8054
8654
|
if (input.expectedFeeLines) compareExpectedFeeLines(input.lines, input.expectedFeeLines, lineIssues);
|
|
@@ -8156,229 +8756,8 @@ const collectOrderLineDiffs = (input) => {
|
|
|
8156
8756
|
return diffs;
|
|
8157
8757
|
};
|
|
8158
8758
|
//#endregion
|
|
8159
|
-
//#region src/
|
|
8160
|
-
|
|
8161
|
-
const MAX_CAUSE_DEPTH = 3;
|
|
8162
|
-
const safeStringify = (value) => {
|
|
8163
|
-
try {
|
|
8164
|
-
const json = JSON.stringify(value);
|
|
8165
|
-
return json === void 0 ? String(value) : json;
|
|
8166
|
-
} catch {
|
|
8167
|
-
return String(value);
|
|
8168
|
-
}
|
|
8169
|
-
};
|
|
8170
|
-
const messageOf = (value) => {
|
|
8171
|
-
if (value instanceof Error) return value.message || value.name;
|
|
8172
|
-
if (typeof value === "string") return value;
|
|
8173
|
-
if (value && typeof value === "object" && "message" in value) {
|
|
8174
|
-
const message = value.message;
|
|
8175
|
-
if (typeof message === "string") return message;
|
|
8176
|
-
}
|
|
8177
|
-
if (value === void 0) return "";
|
|
8178
|
-
return safeStringify(value);
|
|
8179
|
-
};
|
|
8180
|
-
/** `TypeError: boom ← caused by: Error: socket closed` style chain. */
|
|
8181
|
-
const describeCause = (cause, depth = 0) => {
|
|
8182
|
-
if (cause instanceof Error) {
|
|
8183
|
-
const head = `${cause.name}: ${cause.message}`;
|
|
8184
|
-
const inner = cause.cause;
|
|
8185
|
-
if (inner === void 0 || depth >= MAX_CAUSE_DEPTH) return head;
|
|
8186
|
-
return `${head} ← caused by: ${describeCause(inner, depth + 1)}`;
|
|
8187
|
-
}
|
|
8188
|
-
return messageOf(cause);
|
|
8189
|
-
};
|
|
8190
|
-
const nameOf = (value) => {
|
|
8191
|
-
if (value instanceof Error) return value.name;
|
|
8192
|
-
if (value && typeof value === "object" && "name" in value) {
|
|
8193
|
-
const name = value.name;
|
|
8194
|
-
if (typeof name === "string") return name;
|
|
8195
|
-
}
|
|
8196
|
-
return null;
|
|
8197
|
-
};
|
|
8198
|
-
const stackOf = (value) => {
|
|
8199
|
-
if (value && typeof value === "object" && "stack" in value) {
|
|
8200
|
-
const stack = value.stack;
|
|
8201
|
-
if (typeof stack === "string" && stack) return stack;
|
|
8202
|
-
}
|
|
8203
|
-
return null;
|
|
8204
|
-
};
|
|
8205
|
-
const causeOf = (value) => {
|
|
8206
|
-
if (!value || typeof value !== "object" || !("cause" in value)) return null;
|
|
8207
|
-
const cause = value.cause;
|
|
8208
|
-
if (cause === void 0 || cause === null) return null;
|
|
8209
|
-
return describeCause(cause) || null;
|
|
8210
|
-
};
|
|
8211
|
-
/** Accepts anything a `catch` can receive. */
|
|
8212
|
-
const serializeError = (value) => {
|
|
8213
|
-
const message = truncate(messageOf(value).trim() || UNKNOWN_MESSAGE, LOG_LIMITS.message);
|
|
8214
|
-
const name = nameOf(value);
|
|
8215
|
-
const stack = stackOf(value);
|
|
8216
|
-
const cause = causeOf(value);
|
|
8217
|
-
const e1 = { name: name ? truncate(name, LOG_LIMITS.errorName) : null };
|
|
8218
|
-
const e2 = { stack: stack ? truncate(stack, LOG_LIMITS.stack) : null };
|
|
8219
|
-
const e3 = { cause: cause ? truncate(cause, LOG_LIMITS.cause) : null };
|
|
8220
|
-
return {
|
|
8221
|
-
message,
|
|
8222
|
-
error: {
|
|
8223
|
-
...e1,
|
|
8224
|
-
...e2,
|
|
8225
|
-
...e3
|
|
8226
|
-
}
|
|
8227
|
-
};
|
|
8228
|
-
};
|
|
8229
|
-
//#endregion
|
|
8230
|
-
//#region src/logs/log.ts
|
|
8231
|
-
const LOGS_PATH = "/api/storefront/v1/logs";
|
|
8232
|
-
const POS_LOGS_PATH = "/api/storefront/v1/pos/logs";
|
|
8233
|
-
const FLUSH_DELAY_MS = 500;
|
|
8234
|
-
let queue = [];
|
|
8235
|
-
let timer = null;
|
|
8236
|
-
const optionalText = (value, max) => {
|
|
8237
|
-
const trimmed = value?.trim();
|
|
8238
|
-
if (!trimmed) return null;
|
|
8239
|
-
return truncate(trimmed, max);
|
|
8240
|
-
};
|
|
8241
|
-
const buildEntry = (level, message, options, error) => {
|
|
8242
|
-
const text = typeof message === "string" ? message.trim() : "";
|
|
8243
|
-
if (!text) return null;
|
|
8244
|
-
const b1 = {
|
|
8245
|
-
level,
|
|
8246
|
-
message: truncate(text, LOG_LIMITS.message)
|
|
8247
|
-
};
|
|
8248
|
-
const b2 = { alias: optionalText(options.alias, LOG_LIMITS.alias) };
|
|
8249
|
-
const b3 = { detail: optionalText(options.detail, LOG_LIMITS.detail) };
|
|
8250
|
-
const b4 = { status: optionalText(options.status, LOG_LIMITS.status) };
|
|
8251
|
-
const b5 = {
|
|
8252
|
-
error: error ?? null,
|
|
8253
|
-
occurred_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
8254
|
-
};
|
|
8255
|
-
return {
|
|
8256
|
-
...b1,
|
|
8257
|
-
...b2,
|
|
8258
|
-
...b3,
|
|
8259
|
-
...b4,
|
|
8260
|
-
...getLogContext(),
|
|
8261
|
-
...b5
|
|
8262
|
-
};
|
|
8263
|
-
};
|
|
8264
|
-
const isAuthConfigured = () => {
|
|
8265
|
-
try {
|
|
8266
|
-
getBaseUrl();
|
|
8267
|
-
return true;
|
|
8268
|
-
} catch {
|
|
8269
|
-
return false;
|
|
8270
|
-
}
|
|
8271
|
-
};
|
|
8272
|
-
/** Browsers cap keepalive bodies at 64 KB; bigger batches send normally. */
|
|
8273
|
-
const postInit = (body) => {
|
|
8274
|
-
return {
|
|
8275
|
-
method: "POST",
|
|
8276
|
-
body,
|
|
8277
|
-
keepalive: body.length < LOG_LIMITS.keepaliveBytes
|
|
8278
|
-
};
|
|
8279
|
-
};
|
|
8280
|
-
const sendPos = async (device, logs) => {
|
|
8281
|
-
const fetchImpl = device.http.fetchImpl ?? fetch;
|
|
8282
|
-
const url = `${resolveBaseUrl(device.http.baseUrl)}${POS_LOGS_PATH}`;
|
|
8283
|
-
const { device_uid, device_secret } = device;
|
|
8284
|
-
const body = JSON.stringify({
|
|
8285
|
-
device_uid,
|
|
8286
|
-
device_secret,
|
|
8287
|
-
logs
|
|
8288
|
-
});
|
|
8289
|
-
const headers = { "Content-Type": "application/json" };
|
|
8290
|
-
return (await fetchImpl(url, {
|
|
8291
|
-
...postInit(body),
|
|
8292
|
-
headers
|
|
8293
|
-
})).ok;
|
|
8294
|
-
};
|
|
8295
|
-
const sendBatch = async (logs) => {
|
|
8296
|
-
try {
|
|
8297
|
-
const device = getPosLogDevice();
|
|
8298
|
-
if (device) return await sendPos(device, logs);
|
|
8299
|
-
if (!isAuthConfigured()) return false;
|
|
8300
|
-
await storefrontJson(LOGS_PATH, postInit(JSON.stringify({ logs })));
|
|
8301
|
-
return true;
|
|
8302
|
-
} catch {
|
|
8303
|
-
return false;
|
|
8304
|
-
}
|
|
8305
|
-
};
|
|
8306
|
-
/** Sends queued logs now; resolves once every batch has been attempted. */
|
|
8307
|
-
const flushLogs = async () => {
|
|
8308
|
-
if (timer) clearTimeout(timer);
|
|
8309
|
-
timer = null;
|
|
8310
|
-
const pending = queue;
|
|
8311
|
-
queue = [];
|
|
8312
|
-
const jobs = [];
|
|
8313
|
-
for (let i = 0; i < pending.length; i += LOG_LIMITS.batch) {
|
|
8314
|
-
const chunk = pending.slice(i, i + LOG_LIMITS.batch);
|
|
8315
|
-
const entries = chunk.map((item) => item.entry);
|
|
8316
|
-
const job = sendBatch(entries).then((sent) => {
|
|
8317
|
-
for (const item of chunk) item.resolve(sent);
|
|
8318
|
-
});
|
|
8319
|
-
jobs.push(job);
|
|
8320
|
-
}
|
|
8321
|
-
await Promise.all(jobs);
|
|
8322
|
-
};
|
|
8323
|
-
const enqueue = (entry) => {
|
|
8324
|
-
return new Promise((resolve) => {
|
|
8325
|
-
queue.push({
|
|
8326
|
-
entry,
|
|
8327
|
-
resolve
|
|
8328
|
-
});
|
|
8329
|
-
if (queue.length >= LOG_LIMITS.batch) {
|
|
8330
|
-
flushLogs();
|
|
8331
|
-
return;
|
|
8332
|
-
}
|
|
8333
|
-
if (!timer) timer = setTimeout(() => void flushLogs(), FLUSH_DELAY_MS);
|
|
8334
|
-
});
|
|
8335
|
-
};
|
|
8336
|
-
/**
|
|
8337
|
-
* Reports a log to the merchant dashboard with the SDK's current context
|
|
8338
|
-
* (channel, fulfillment, store, brand, device). Never throws; resolves to
|
|
8339
|
-
* `false` when the log could not be delivered.
|
|
8340
|
-
*/
|
|
8341
|
-
const log = (level, message, options = {}) => {
|
|
8342
|
-
const entry = buildEntry(level, message, options);
|
|
8343
|
-
if (!entry) return Promise.resolve(false);
|
|
8344
|
-
return enqueue(entry);
|
|
8345
|
-
};
|
|
8346
|
-
/** `log("error", …)` with message, name, stack and cause read from `error`. */
|
|
8347
|
-
const logError = (error, options = {}) => {
|
|
8348
|
-
const { message, error: info } = serializeError(error);
|
|
8349
|
-
const entry = buildEntry("error", message, options, info);
|
|
8350
|
-
if (!entry) return Promise.resolve(false);
|
|
8351
|
-
return enqueue(entry);
|
|
8352
|
-
};
|
|
8353
|
-
//#endregion
|
|
8354
|
-
//#region src/webhooks/verifyWebhookSignature.ts
|
|
8355
|
-
const SIGNATURE_PREFIX = "sha256=";
|
|
8356
|
-
const hmacHex = async (secret, body) => {
|
|
8357
|
-
const raw = new TextEncoder().encode(secret);
|
|
8358
|
-
const key = await crypto.subtle.importKey("raw", raw, {
|
|
8359
|
-
name: "HMAC",
|
|
8360
|
-
hash: "SHA-256"
|
|
8361
|
-
}, false, ["sign"]);
|
|
8362
|
-
const data = new TextEncoder().encode(body);
|
|
8363
|
-
const sig = new Uint8Array(await crypto.subtle.sign("HMAC", key, data));
|
|
8364
|
-
return Array.from(sig, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
8365
|
-
};
|
|
8366
|
-
const safeEqual = (left, right) => {
|
|
8367
|
-
if (left.length !== right.length) return false;
|
|
8368
|
-
let diff = 0;
|
|
8369
|
-
for (let i = 0; i < left.length; i += 1) diff |= left.charCodeAt(i) ^ right.charCodeAt(i);
|
|
8370
|
-
return diff === 0;
|
|
8371
|
-
};
|
|
8372
|
-
/** Checks a Fire Spark webhook/callback signature in constant time. */
|
|
8373
|
-
const verifyWebhookSignature = async (input) => {
|
|
8374
|
-
const { secret, body, signature } = input;
|
|
8375
|
-
if (!secret || !signature?.startsWith(SIGNATURE_PREFIX)) return false;
|
|
8376
|
-
const received = signature.slice(7).toLowerCase();
|
|
8377
|
-
return safeEqual(received, await hmacHex(secret, body));
|
|
8378
|
-
};
|
|
8379
|
-
//#endregion
|
|
8380
|
-
//#region ../../node_modules/.pnpm/dayjs@1.11.9/node_modules/dayjs/dayjs.min.js
|
|
8381
|
-
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) => {
|
|
8382
8761
|
(function(t, e) {
|
|
8383
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();
|
|
8384
8763
|
})(exports, (function() {
|
|
@@ -8673,9 +9052,7 @@ var require_dayjs_min = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
8673
9052
|
return w(1e3 * t);
|
|
8674
9053
|
}, w.en = D[g], w.Ls = D, w.p = {}, w;
|
|
8675
9054
|
}));
|
|
8676
|
-
}));
|
|
8677
|
-
//#endregion
|
|
8678
|
-
//#region src/rules/assert.ts
|
|
9055
|
+
})))(), 1);
|
|
8679
9056
|
const assert = (value, message) => {
|
|
8680
9057
|
if (value) return;
|
|
8681
9058
|
throw new Error(message);
|
|
@@ -8704,7 +9081,6 @@ const countCartProducts = (lines, includeModifiers) => {
|
|
|
8704
9081
|
};
|
|
8705
9082
|
//#endregion
|
|
8706
9083
|
//#region src/rules/absoluteDateMatcher.ts
|
|
8707
|
-
var import_dayjs_min = /* @__PURE__ */ __toESM(require_dayjs_min(), 1);
|
|
8708
9084
|
const matchAbsoluteDateOperator = (condition, matcher) => {
|
|
8709
9085
|
const { primitive, operator, value } = condition;
|
|
8710
9086
|
assert(primitive === "absolute date", primitiveError("absolute date"));
|
|
@@ -8775,6 +9151,14 @@ const paymentCaptured = (condition, captured) => {
|
|
|
8775
9151
|
assert(value != null, "Invalid value");
|
|
8776
9152
|
return matchBooleanOperator(condition, captured ?? false);
|
|
8777
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
|
+
};
|
|
8778
9162
|
//#endregion
|
|
8779
9163
|
//#region src/rules/currencyMatcher.ts
|
|
8780
9164
|
const matchCurrencyOperator = (condition, matcher) => {
|
|
@@ -9501,6 +9885,10 @@ var RulesEngine = class {
|
|
|
9501
9885
|
const payment = this.getDependency("payment", type);
|
|
9502
9886
|
return paymentCaptured(condition, payment.captured);
|
|
9503
9887
|
}
|
|
9888
|
+
if (type === "Order is scheduled") {
|
|
9889
|
+
const order = this.getDependency("order", type);
|
|
9890
|
+
return orderIsScheduled(condition, order.scheduled_at);
|
|
9891
|
+
}
|
|
9504
9892
|
throw new Error(`Condition ${type} not supported in boolean matcher`);
|
|
9505
9893
|
};
|
|
9506
9894
|
assertCurrency = (condition) => {
|
|
@@ -9668,6 +10056,731 @@ var RulesEngine = class {
|
|
|
9668
10056
|
};
|
|
9669
10057
|
};
|
|
9670
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
|
|
9671
10784
|
//#region src/rules/checkout.ts
|
|
9672
10785
|
const DEFAULT_SHIPPING_FIELDS = {
|
|
9673
10786
|
address_line1: "required",
|
|
@@ -10130,4 +11243,4 @@ const cardPreviewGroups = (value) => {
|
|
|
10130
11243
|
return groups;
|
|
10131
11244
|
};
|
|
10132
11245
|
//#endregion
|
|
10133
|
-
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 };
|