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