@godaddy/gd-commerce-server 0.1.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/LICENSE.md +3 -0
- package/README.md +59 -0
- package/dist/index.d.ts +146 -0
- package/dist/index.js +1567 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1567 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
|
|
4
|
+
//#region src/lib/commerce/checkout-config.ts
|
|
5
|
+
const DEFAULT_CHECKOUT_CONFIGURATION = {
|
|
6
|
+
enablePromotionCodes: false,
|
|
7
|
+
enableTaxCollection: false,
|
|
8
|
+
enableShipping: false
|
|
9
|
+
};
|
|
10
|
+
function parseCommerceCheckoutConfiguration(raw) {
|
|
11
|
+
if (!raw?.trim()) return DEFAULT_CHECKOUT_CONFIGURATION;
|
|
12
|
+
let value;
|
|
13
|
+
try {
|
|
14
|
+
value = JSON.parse(raw);
|
|
15
|
+
} catch {
|
|
16
|
+
return DEFAULT_CHECKOUT_CONFIGURATION;
|
|
17
|
+
}
|
|
18
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return DEFAULT_CHECKOUT_CONFIGURATION;
|
|
19
|
+
const candidate = value;
|
|
20
|
+
for (const key of [
|
|
21
|
+
"enablePromotionCodes",
|
|
22
|
+
"enableTaxCollection",
|
|
23
|
+
"enableShipping"
|
|
24
|
+
]) if (typeof candidate[key] !== "boolean") return DEFAULT_CHECKOUT_CONFIGURATION;
|
|
25
|
+
const shipping = candidate.shipping;
|
|
26
|
+
if (shipping !== void 0) {
|
|
27
|
+
if (shipping === null || typeof shipping !== "object" || Array.isArray(shipping)) return DEFAULT_CHECKOUT_CONFIGURATION;
|
|
28
|
+
if ((shipping.originAddress !== null && typeof shipping.originAddress === "object" && !Array.isArray(shipping.originAddress)) === (typeof shipping.fulfillmentLocationId === "string" && shipping.fulfillmentLocationId.trim() !== "")) return DEFAULT_CHECKOUT_CONFIGURATION;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
enablePromotionCodes: candidate.enablePromotionCodes,
|
|
32
|
+
enableTaxCollection: candidate.enableTaxCollection,
|
|
33
|
+
enableShipping: candidate.enableShipping,
|
|
34
|
+
...shipping ? { shipping } : {}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/lib/commerce/config.ts
|
|
40
|
+
const DEFAULT_API_BASE_URL = "https://api.godaddy.com";
|
|
41
|
+
function normalizeApiBaseUrl(raw) {
|
|
42
|
+
let url;
|
|
43
|
+
try {
|
|
44
|
+
url = new URL(raw);
|
|
45
|
+
} catch {
|
|
46
|
+
throw new Error("Commerce config: apiBaseUrl must be a valid HTTPS origin.");
|
|
47
|
+
}
|
|
48
|
+
if (url.protocol !== "https:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash) throw new Error("Commerce config: apiBaseUrl must be an HTTPS origin without credentials, path, query, or fragment.");
|
|
49
|
+
return url.origin;
|
|
50
|
+
}
|
|
51
|
+
function requireValue(environment, key) {
|
|
52
|
+
const value = environment[key]?.trim();
|
|
53
|
+
if (!value) throw new Error(`Commerce config: ${key} is missing. Configure it in the server environment.`);
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
/** Read server environment values on each call. Loading files or secret stores belongs to the host. */
|
|
57
|
+
function readCommerceConfig(options = {}) {
|
|
58
|
+
const environment = options.environment ?? process.env;
|
|
59
|
+
return {
|
|
60
|
+
clientId: requireValue(environment, "GODADDY_OAUTH_CLIENT_ID"),
|
|
61
|
+
clientSecret: requireValue(environment, "GODADDY_OAUTH_CLIENT_SECRET"),
|
|
62
|
+
storeId: requireValue(environment, "GODADDY_STORE_ID"),
|
|
63
|
+
channelId: requireValue(environment, "GODADDY_CHANNEL_ID"),
|
|
64
|
+
currencyCode: requireValue(environment, "GODADDY_CURRENCY_CODE"),
|
|
65
|
+
apiBaseUrl: normalizeApiBaseUrl(options.apiBaseUrl ?? DEFAULT_API_BASE_URL),
|
|
66
|
+
...options.sourceApp ? { sourceApp: options.sourceApp } : {},
|
|
67
|
+
...options.owner ? { owner: options.owner } : {}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function createRuntimeCommerceConfiguration(options = {}) {
|
|
71
|
+
return {
|
|
72
|
+
read: () => readCommerceConfig(options),
|
|
73
|
+
readCheckout: () => parseCommerceCheckoutConfiguration((options.environment ?? process.env).GODADDY_CHECKOUT_CONFIGURATION)
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function commerceConfigurationForResponse(res) {
|
|
77
|
+
const configuration = res.locals.commerceConfiguration;
|
|
78
|
+
if (configuration && typeof configuration === "object" && "read" in configuration && typeof configuration.read === "function" && "readCheckout" in configuration && typeof configuration.readCheckout === "function") return configuration;
|
|
79
|
+
return createRuntimeCommerceConfiguration();
|
|
80
|
+
}
|
|
81
|
+
function readCommerceConfigForResponse(res) {
|
|
82
|
+
return commerceConfigurationForResponse(res).read();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/lib/commerce/checkout-subgraph.ts
|
|
87
|
+
/**
|
|
88
|
+
* The hosted checkout page will not render a card form without this. GDC's
|
|
89
|
+
* checkout GraphQL accepts a session with no payment methods and returns 200,
|
|
90
|
+
* so an omitted or deleted default fails silently — the shopper sees "No
|
|
91
|
+
* payment methods available" with no error anywhere in the request/response
|
|
92
|
+
* cycle. Do not remove this while debugging an unrelated checkout issue.
|
|
93
|
+
*/
|
|
94
|
+
const DEFAULT_CHECKOUT_PAYMENT_METHODS = { card: {
|
|
95
|
+
processor: "godaddy",
|
|
96
|
+
checkoutTypes: ["standard"]
|
|
97
|
+
} };
|
|
98
|
+
function stripUndefined(value) {
|
|
99
|
+
return Object.fromEntries(Object.entries(value).filter(([, entryValue]) => entryValue !== void 0));
|
|
100
|
+
}
|
|
101
|
+
function applyCheckoutDefaults(base, overrides = {}) {
|
|
102
|
+
const definedOverrides = stripUndefined(overrides);
|
|
103
|
+
const result = {
|
|
104
|
+
...base,
|
|
105
|
+
...definedOverrides
|
|
106
|
+
};
|
|
107
|
+
if (result.paymentMethods === void 0) result.paymentMethods = DEFAULT_CHECKOUT_PAYMENT_METHODS;
|
|
108
|
+
if (result.enablePaymentMethodCollection === void 0) result.enablePaymentMethodCollection = true;
|
|
109
|
+
if (result.enableBillingAddressCollection === void 0) result.enableBillingAddressCollection = true;
|
|
110
|
+
if (result.enableShipping === void 0) result.enableShipping = false;
|
|
111
|
+
if (result.enableShippingAddressCollection === void 0) result.enableShippingAddressCollection = false;
|
|
112
|
+
if (result.enableLocalPickup === void 0) result.enableLocalPickup = false;
|
|
113
|
+
if (result.enablePhoneCollection === void 0) result.enablePhoneCollection = false;
|
|
114
|
+
if (result.enableTaxCollection === void 0) result.enableTaxCollection = false;
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
function checkoutGraphqlEndpoint({ apiBaseUrl }) {
|
|
118
|
+
const { host, protocol } = new URL(apiBaseUrl);
|
|
119
|
+
return `${protocol}//checkout.commerce.${host}`;
|
|
120
|
+
}
|
|
121
|
+
function authorizationHeaders({ accessToken }) {
|
|
122
|
+
return { Authorization: `Bearer ${accessToken}` };
|
|
123
|
+
}
|
|
124
|
+
async function getOAuthAccessToken({ clientId, clientSecret, apiBaseUrl, scope = "commerce.product:read", fetch: fetchImplementation }) {
|
|
125
|
+
if (!clientId || !clientSecret) throw new Error("clientId and clientSecret are required");
|
|
126
|
+
const requestFetch = fetchImplementation ?? fetch;
|
|
127
|
+
const body = new URLSearchParams();
|
|
128
|
+
body.append("grant_type", "client_credentials");
|
|
129
|
+
body.append("client_id", clientId);
|
|
130
|
+
body.append("client_secret", clientSecret);
|
|
131
|
+
body.append("scope", scope);
|
|
132
|
+
const response = await requestFetch(new URL("/v2/oauth2/token", apiBaseUrl).toString(), {
|
|
133
|
+
method: "POST",
|
|
134
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
135
|
+
body: body.toString(),
|
|
136
|
+
cache: "no-store"
|
|
137
|
+
});
|
|
138
|
+
if (!response.ok) throw new Error(`Failed to get access token: ${response.status} ${response.statusText}`);
|
|
139
|
+
return await response.json();
|
|
140
|
+
}
|
|
141
|
+
function buildBuyNowCheckoutInput({ storeId, skuId, quantity = 1, returnUrl, successUrl, channelId }, overrides = {}) {
|
|
142
|
+
return applyCheckoutDefaults({
|
|
143
|
+
storeId,
|
|
144
|
+
returnUrl,
|
|
145
|
+
successUrl,
|
|
146
|
+
channelId,
|
|
147
|
+
lineItems: [{
|
|
148
|
+
skuId,
|
|
149
|
+
quantity
|
|
150
|
+
}]
|
|
151
|
+
}, overrides);
|
|
152
|
+
}
|
|
153
|
+
function buildCartCheckoutInput({ storeId, draftOrderId, returnUrl, successUrl, channelId }, overrides = {}) {
|
|
154
|
+
return applyCheckoutDefaults({
|
|
155
|
+
storeId,
|
|
156
|
+
returnUrl,
|
|
157
|
+
successUrl,
|
|
158
|
+
channelId,
|
|
159
|
+
draftOrderId
|
|
160
|
+
}, overrides);
|
|
161
|
+
}
|
|
162
|
+
function buildNonCatalogCheckoutInput({ storeId, lineItemData, quantity = 1, returnUrl, successUrl, channelId }, overrides = {}) {
|
|
163
|
+
return applyCheckoutDefaults({
|
|
164
|
+
storeId,
|
|
165
|
+
returnUrl,
|
|
166
|
+
successUrl,
|
|
167
|
+
channelId,
|
|
168
|
+
lineItems: [{
|
|
169
|
+
lineItemData,
|
|
170
|
+
quantity
|
|
171
|
+
}]
|
|
172
|
+
}, overrides);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
//#endregion
|
|
176
|
+
//#region src/lib/commerce/gql.ts
|
|
177
|
+
var GraphQLErrorWithCodes = class extends Error {
|
|
178
|
+
constructor(errors, status) {
|
|
179
|
+
const errorMessage = errors.length === 1 ? `${errors[0]?.message || "Unknown error"}` : errors.map((error) => error.message).filter(Boolean).join("; ");
|
|
180
|
+
super(errorMessage);
|
|
181
|
+
this.errors = errors;
|
|
182
|
+
this.status = status;
|
|
183
|
+
this.name = "GraphQLErrorWithCodes";
|
|
184
|
+
}
|
|
185
|
+
get codes() {
|
|
186
|
+
return this.errors.map((error) => error.code).filter(Boolean);
|
|
187
|
+
}
|
|
188
|
+
get messages() {
|
|
189
|
+
return this.errors.map((error) => error.message).filter(Boolean);
|
|
190
|
+
}
|
|
191
|
+
get statuses() {
|
|
192
|
+
return this.errors.map((error) => error.status).filter((status) => typeof status === "number");
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
function storefrontHeaders({ storeId, clientId }) {
|
|
196
|
+
return {
|
|
197
|
+
"X-Store-ID": storeId,
|
|
198
|
+
"X-Client-ID": clientId
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
async function gqlRequest({ endpoint, query, variables, headers: headersInit, fetch: fetchImplementation }) {
|
|
202
|
+
const requestHeaders = new Headers(headersInit);
|
|
203
|
+
requestHeaders.set("Accept", "application/json");
|
|
204
|
+
requestHeaders.set("Content-Type", "application/json");
|
|
205
|
+
const response = await (fetchImplementation ?? fetch)(endpoint, {
|
|
206
|
+
method: "POST",
|
|
207
|
+
headers: requestHeaders,
|
|
208
|
+
body: JSON.stringify({
|
|
209
|
+
query,
|
|
210
|
+
variables: variables ?? {}
|
|
211
|
+
}),
|
|
212
|
+
cache: "no-store"
|
|
213
|
+
});
|
|
214
|
+
let result;
|
|
215
|
+
try {
|
|
216
|
+
result = await response.json();
|
|
217
|
+
} catch {
|
|
218
|
+
throw new GraphQLErrorWithCodes([{
|
|
219
|
+
message: `GraphQL request failed: ${response.status} ${response.statusText}`,
|
|
220
|
+
status: response.status
|
|
221
|
+
}], response.ok ? void 0 : response.status);
|
|
222
|
+
}
|
|
223
|
+
if (result.errors?.length) throw new GraphQLErrorWithCodes(result.errors.map((error) => ({
|
|
224
|
+
message: error.message,
|
|
225
|
+
code: error.extensions?.code,
|
|
226
|
+
status: error.extensions?.status ?? error.extensions?.http?.status ?? (response.ok ? void 0 : response.status)
|
|
227
|
+
})), response.ok ? void 0 : response.status);
|
|
228
|
+
if (!response.ok) throw new GraphQLErrorWithCodes([{
|
|
229
|
+
message: `GraphQL request failed: ${response.status} ${response.statusText}`,
|
|
230
|
+
status: response.status
|
|
231
|
+
}], response.status);
|
|
232
|
+
if (result.data === void 0) throw new Error("GraphQL response did not include data");
|
|
233
|
+
return result.data;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region src/lib/commerce/create-checkout-session.ts
|
|
238
|
+
const createCheckoutSessionMutation = `
|
|
239
|
+
mutation CreateCheckoutSession($input: MutationCreateCheckoutSessionInput!) {
|
|
240
|
+
createCheckoutSession(input: $input) {
|
|
241
|
+
id
|
|
242
|
+
token
|
|
243
|
+
url
|
|
244
|
+
sourceApp
|
|
245
|
+
returnUrl
|
|
246
|
+
successUrl
|
|
247
|
+
storeId
|
|
248
|
+
businessId
|
|
249
|
+
channelId
|
|
250
|
+
customerId
|
|
251
|
+
storeName
|
|
252
|
+
environment
|
|
253
|
+
enableTips
|
|
254
|
+
enabledLocales
|
|
255
|
+
enableSurcharge
|
|
256
|
+
enableLocalPickup
|
|
257
|
+
enableShipping
|
|
258
|
+
enablePhoneCollection
|
|
259
|
+
enableNotesCollection
|
|
260
|
+
enablePromotionCodes
|
|
261
|
+
enableTaxCollection
|
|
262
|
+
enableShippingAddressCollection
|
|
263
|
+
enableBillingAddressCollection
|
|
264
|
+
enableAddressAutocomplete
|
|
265
|
+
paymentMethods {
|
|
266
|
+
card {
|
|
267
|
+
processor
|
|
268
|
+
checkoutTypes
|
|
269
|
+
}
|
|
270
|
+
ccavenue {
|
|
271
|
+
processor
|
|
272
|
+
checkoutTypes
|
|
273
|
+
}
|
|
274
|
+
express {
|
|
275
|
+
processor
|
|
276
|
+
checkoutTypes
|
|
277
|
+
}
|
|
278
|
+
applePay {
|
|
279
|
+
processor
|
|
280
|
+
checkoutTypes
|
|
281
|
+
}
|
|
282
|
+
googlePay {
|
|
283
|
+
processor
|
|
284
|
+
checkoutTypes
|
|
285
|
+
}
|
|
286
|
+
paypal {
|
|
287
|
+
processor
|
|
288
|
+
checkoutTypes
|
|
289
|
+
}
|
|
290
|
+
paze {
|
|
291
|
+
processor
|
|
292
|
+
checkoutTypes
|
|
293
|
+
}
|
|
294
|
+
offline {
|
|
295
|
+
processor
|
|
296
|
+
checkoutTypes
|
|
297
|
+
}
|
|
298
|
+
mercadopago {
|
|
299
|
+
processor
|
|
300
|
+
checkoutTypes
|
|
301
|
+
}
|
|
302
|
+
ach {
|
|
303
|
+
processor
|
|
304
|
+
checkoutTypes
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
draftOrder {
|
|
308
|
+
id
|
|
309
|
+
statuses {
|
|
310
|
+
status
|
|
311
|
+
}
|
|
312
|
+
totals {
|
|
313
|
+
total {
|
|
314
|
+
currencyCode
|
|
315
|
+
value
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
`;
|
|
322
|
+
function promotionCodesEnabled(configuration) {
|
|
323
|
+
return "enablePromotionCodes" in configuration && configuration.enablePromotionCodes === true;
|
|
324
|
+
}
|
|
325
|
+
async function createCheckoutSession(params, configuration = createRuntimeCommerceConfiguration()) {
|
|
326
|
+
const { draftOrderId, skuId, quantity, lineItemData, returnUrl, successUrl } = params;
|
|
327
|
+
if (!returnUrl || !successUrl) throw new Error("createCheckoutSession: returnUrl and successUrl are required");
|
|
328
|
+
if ([
|
|
329
|
+
draftOrderId,
|
|
330
|
+
skuId,
|
|
331
|
+
lineItemData
|
|
332
|
+
].filter(Boolean).length !== 1) throw new Error("createCheckoutSession: exactly one of draftOrderId, skuId, or lineItemData is required");
|
|
333
|
+
const { storeId, channelId, clientId, clientSecret, apiBaseUrl, sourceApp, owner, currencyCode: configCurrencyCode } = configuration.read();
|
|
334
|
+
const checkoutConfiguration = configuration.readCheckout();
|
|
335
|
+
const enablePromotionCodes = promotionCodesEnabled(checkoutConfiguration);
|
|
336
|
+
const catalogShippingEnabled = lineItemData === void 0 && checkoutConfiguration.enableShipping;
|
|
337
|
+
const token = await getOAuthAccessToken({
|
|
338
|
+
clientId,
|
|
339
|
+
clientSecret,
|
|
340
|
+
apiBaseUrl,
|
|
341
|
+
scope: "commerce.product:read"
|
|
342
|
+
});
|
|
343
|
+
const attribution = {
|
|
344
|
+
sourceApp,
|
|
345
|
+
owner
|
|
346
|
+
};
|
|
347
|
+
const catalogCheckoutOverrides = {
|
|
348
|
+
...attribution,
|
|
349
|
+
enablePromotionCodes,
|
|
350
|
+
enableTaxCollection: checkoutConfiguration.enableTaxCollection,
|
|
351
|
+
enableShipping: checkoutConfiguration.enableShipping,
|
|
352
|
+
enableShippingAddressCollection: checkoutConfiguration.enableShipping,
|
|
353
|
+
paymentMethods: DEFAULT_CHECKOUT_PAYMENT_METHODS,
|
|
354
|
+
shipping: catalogShippingEnabled ? checkoutConfiguration.shipping : void 0
|
|
355
|
+
};
|
|
356
|
+
const resolvedLineItemData = lineItemData !== void 0 ? {
|
|
357
|
+
...lineItemData,
|
|
358
|
+
priceData: {
|
|
359
|
+
...lineItemData.priceData,
|
|
360
|
+
currencyCode: configCurrencyCode
|
|
361
|
+
}
|
|
362
|
+
} : void 0;
|
|
363
|
+
const input = draftOrderId ? buildCartCheckoutInput({
|
|
364
|
+
storeId,
|
|
365
|
+
channelId,
|
|
366
|
+
draftOrderId,
|
|
367
|
+
returnUrl,
|
|
368
|
+
successUrl
|
|
369
|
+
}, catalogCheckoutOverrides) : resolvedLineItemData ? buildNonCatalogCheckoutInput({
|
|
370
|
+
storeId,
|
|
371
|
+
channelId,
|
|
372
|
+
lineItemData: resolvedLineItemData,
|
|
373
|
+
quantity: quantity ?? 1,
|
|
374
|
+
returnUrl,
|
|
375
|
+
successUrl
|
|
376
|
+
}, {
|
|
377
|
+
...attribution,
|
|
378
|
+
enableTaxCollection: checkoutConfiguration.enableTaxCollection,
|
|
379
|
+
paymentMethods: DEFAULT_CHECKOUT_PAYMENT_METHODS
|
|
380
|
+
}) : buildBuyNowCheckoutInput({
|
|
381
|
+
storeId,
|
|
382
|
+
channelId,
|
|
383
|
+
skuId,
|
|
384
|
+
quantity: quantity ?? 1,
|
|
385
|
+
returnUrl,
|
|
386
|
+
successUrl
|
|
387
|
+
}, catalogCheckoutOverrides);
|
|
388
|
+
let result;
|
|
389
|
+
try {
|
|
390
|
+
result = await gqlRequest({
|
|
391
|
+
endpoint: checkoutGraphqlEndpoint({ apiBaseUrl }),
|
|
392
|
+
query: createCheckoutSessionMutation,
|
|
393
|
+
variables: { input },
|
|
394
|
+
headers: authorizationHeaders({ accessToken: token.access_token })
|
|
395
|
+
});
|
|
396
|
+
} catch (error) {
|
|
397
|
+
throw new Error("Commerce checkout session could not be created", { cause: error });
|
|
398
|
+
}
|
|
399
|
+
const session = result.createCheckoutSession;
|
|
400
|
+
if (!session?.url || !session.id) throw new Error("Checkout session was not created");
|
|
401
|
+
if (session.storeId !== storeId || session.channelId !== channelId) throw new Error(`Checkout session binding mismatch: expected store ${storeId} and channel ${channelId}, received store ${session.storeId ?? "missing"} and channel ${session.channelId ?? "missing"}`);
|
|
402
|
+
const expectedShipping = lineItemData === void 0 && checkoutConfiguration.enableShipping;
|
|
403
|
+
if (lineItemData === void 0 && enablePromotionCodes && session.enablePromotionCodes !== true) throw new Error("Checkout session did not enable configured promotion codes");
|
|
404
|
+
if (checkoutConfiguration.enableTaxCollection && session.enableTaxCollection !== true) throw new Error("Checkout session did not enable configured tax collection");
|
|
405
|
+
if (expectedShipping && (session.enableShipping !== true || session.enableShippingAddressCollection !== true)) throw new Error("Checkout session did not enable configured shipping and address collection");
|
|
406
|
+
if (!session.paymentMethods?.card?.processor) throw new Error("Checkout session did not configure payment methods.");
|
|
407
|
+
return {
|
|
408
|
+
url: session.url,
|
|
409
|
+
id: session.id,
|
|
410
|
+
draftOrderId: session.draftOrder?.id ?? null,
|
|
411
|
+
storeId,
|
|
412
|
+
channelId,
|
|
413
|
+
businessId: session.businessId ?? null,
|
|
414
|
+
storeName: session.storeName ?? null,
|
|
415
|
+
sourceApp: session.sourceApp ?? null
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
//#endregion
|
|
420
|
+
//#region src/lib/commerce/get-order-status.ts
|
|
421
|
+
async function getOrderStatus(orderId, configuration = createRuntimeCommerceConfiguration()) {
|
|
422
|
+
if (typeof orderId !== "string" || !orderId.trim() || orderId === "." || orderId === "..") throw new Error("getOrderStatus: a valid orderId is required");
|
|
423
|
+
const { storeId, channelId, clientId, clientSecret, apiBaseUrl, currencyCode } = configuration.read();
|
|
424
|
+
const token = await getOAuthAccessToken({
|
|
425
|
+
clientId,
|
|
426
|
+
clientSecret,
|
|
427
|
+
apiBaseUrl,
|
|
428
|
+
scope: "commerce.order:read"
|
|
429
|
+
});
|
|
430
|
+
const response = await fetch(new URL(`/v1/commerce/stores/${encodeURIComponent(storeId)}/orders/${encodeURIComponent(orderId)}`, apiBaseUrl), {
|
|
431
|
+
method: "GET",
|
|
432
|
+
headers: {
|
|
433
|
+
...authorizationHeaders({ accessToken: token.access_token }),
|
|
434
|
+
Accept: "application/json"
|
|
435
|
+
},
|
|
436
|
+
cache: "no-store"
|
|
437
|
+
});
|
|
438
|
+
if (!response.ok) throw new Error(`Failed to load order: upstream returned ${response.status}`);
|
|
439
|
+
const order = (await response.json())?.order;
|
|
440
|
+
if (!order?.id || order.id !== orderId) throw new Error("Order lookup did not return the requested order");
|
|
441
|
+
if (order.context?.storeId !== storeId || order.context?.channelId !== channelId) throw new Error("Order lookup returned a different store or channel");
|
|
442
|
+
const total = order.totals?.total;
|
|
443
|
+
return {
|
|
444
|
+
id: order.id,
|
|
445
|
+
status: order.statuses?.paymentStatus ?? "unknown",
|
|
446
|
+
amount: total?.value ?? 0,
|
|
447
|
+
currency: total?.currencyCode ?? currencyCode,
|
|
448
|
+
createdAt: order.createdAt,
|
|
449
|
+
updatedAt: order.updatedAt,
|
|
450
|
+
lineItems: (order.lineItems ?? []).map(({ id, title, quantity }) => ({
|
|
451
|
+
id,
|
|
452
|
+
name: title,
|
|
453
|
+
quantity
|
|
454
|
+
}))
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
//#endregion
|
|
459
|
+
//#region src/lib/commerce/checkout-return-urls.ts
|
|
460
|
+
function parseDestination(value) {
|
|
461
|
+
if (typeof value !== "string" || /[\s\\]/.test(value)) return null;
|
|
462
|
+
try {
|
|
463
|
+
const url = new URL(value);
|
|
464
|
+
if (url.protocol !== "https:" || url.username || url.password || url.hash) return null;
|
|
465
|
+
return url;
|
|
466
|
+
} catch {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function destinationKey(url) {
|
|
471
|
+
const canonical = new URL(url);
|
|
472
|
+
canonical.searchParams.sort();
|
|
473
|
+
return canonical.href;
|
|
474
|
+
}
|
|
475
|
+
function createCheckoutReturnUrlValidator(policy) {
|
|
476
|
+
function allowedDestinations(values) {
|
|
477
|
+
return new Set(values.map((value) => {
|
|
478
|
+
const url = parseDestination(value);
|
|
479
|
+
if (!url || url.searchParams.has("orderId")) throw new Error("Checkout return destinations must be absolute HTTPS URLs without credentials, fragments, or orderId.");
|
|
480
|
+
return destinationKey(url);
|
|
481
|
+
}));
|
|
482
|
+
}
|
|
483
|
+
const returnUrls = allowedDestinations(policy.returnUrls);
|
|
484
|
+
const successUrls = allowedDestinations(policy.successUrls);
|
|
485
|
+
return (returnUrl, successUrl) => {
|
|
486
|
+
const cancel = parseDestination(returnUrl);
|
|
487
|
+
const success = parseDestination(successUrl);
|
|
488
|
+
if (!cancel || !success || success.searchParams.getAll("orderId").length > 1) return null;
|
|
489
|
+
const successDestination = new URL(success);
|
|
490
|
+
successDestination.searchParams.delete("orderId");
|
|
491
|
+
if (!returnUrls.has(destinationKey(cancel)) || !successUrls.has(destinationKey(successDestination))) return null;
|
|
492
|
+
return {
|
|
493
|
+
returnUrl: cancel.href,
|
|
494
|
+
successUrl: success.href
|
|
495
|
+
};
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
//#endregion
|
|
500
|
+
//#region src/lib/commerce/cart-scope.ts
|
|
501
|
+
/** Public cache/storage scope, not an authorization credential. */
|
|
502
|
+
function getCommerceCartScope(config) {
|
|
503
|
+
return createHash("sha256").update(JSON.stringify([
|
|
504
|
+
config.apiBaseUrl,
|
|
505
|
+
config.storeId,
|
|
506
|
+
config.channelId,
|
|
507
|
+
config.currencyCode
|
|
508
|
+
])).digest("hex").slice(0, 32);
|
|
509
|
+
}
|
|
510
|
+
/** Existing custom clients may omit the header; managed components always send it. */
|
|
511
|
+
function validateCommerceCartScope(req, res, config) {
|
|
512
|
+
const suppliedScope = req.headers?.["x-commerce-scope"];
|
|
513
|
+
if (suppliedScope === void 0 || suppliedScope === getCommerceCartScope(config)) return true;
|
|
514
|
+
res.status(409).json({ error: "The connected store changed. Reload the page before continuing." });
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
//#endregion
|
|
519
|
+
//#region src/lib/commerce/order-subgraph.ts
|
|
520
|
+
function createMoney(value, currencyCode) {
|
|
521
|
+
return {
|
|
522
|
+
value,
|
|
523
|
+
currencyCode
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
function orderStorefrontEndpoint({ apiBaseUrl }) {
|
|
527
|
+
return new URL("/v1/commerce/order-storefront-subgraph", apiBaseUrl).toString();
|
|
528
|
+
}
|
|
529
|
+
function buildEmptyCartOrderInput({ storeId, channelId, currencyCode = "USD", owner }) {
|
|
530
|
+
return {
|
|
531
|
+
context: {
|
|
532
|
+
storeId,
|
|
533
|
+
channelId: channelId || "",
|
|
534
|
+
...owner ? { owner } : {}
|
|
535
|
+
},
|
|
536
|
+
totals: {
|
|
537
|
+
subTotal: createMoney(0, currencyCode),
|
|
538
|
+
shippingTotal: createMoney(0, currencyCode),
|
|
539
|
+
discountTotal: createMoney(0, currencyCode),
|
|
540
|
+
feeTotal: createMoney(0, currencyCode),
|
|
541
|
+
taxTotal: createMoney(0, currencyCode),
|
|
542
|
+
total: createMoney(0, currencyCode)
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
function buildAddLineItemBySkuIdInput(orderId, item) {
|
|
547
|
+
return {
|
|
548
|
+
orderId,
|
|
549
|
+
skuId: item.skuId,
|
|
550
|
+
name: item.name,
|
|
551
|
+
quantity: item.quantity,
|
|
552
|
+
fulfillmentMode: "NONE",
|
|
553
|
+
status: "DRAFT"
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
const getCartOrderQuery = `
|
|
557
|
+
query GetCartOrder($id: ID!) {
|
|
558
|
+
orderById(id: $id) {
|
|
559
|
+
id
|
|
560
|
+
customerId
|
|
561
|
+
createdAt
|
|
562
|
+
updatedAt
|
|
563
|
+
context {
|
|
564
|
+
storeId
|
|
565
|
+
channelId
|
|
566
|
+
}
|
|
567
|
+
lineItems {
|
|
568
|
+
id
|
|
569
|
+
name
|
|
570
|
+
quantity
|
|
571
|
+
skuId
|
|
572
|
+
type
|
|
573
|
+
fulfillmentMode
|
|
574
|
+
details {
|
|
575
|
+
productAssetUrl
|
|
576
|
+
sku
|
|
577
|
+
unitOfMeasure
|
|
578
|
+
selectedOptions {
|
|
579
|
+
attribute
|
|
580
|
+
values
|
|
581
|
+
}
|
|
582
|
+
selectedAddons {
|
|
583
|
+
attribute
|
|
584
|
+
sku
|
|
585
|
+
values {
|
|
586
|
+
name
|
|
587
|
+
costAdjustment {
|
|
588
|
+
value
|
|
589
|
+
currencyCode
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
totals {
|
|
595
|
+
subTotal {
|
|
596
|
+
value
|
|
597
|
+
currencyCode
|
|
598
|
+
}
|
|
599
|
+
taxTotal {
|
|
600
|
+
value
|
|
601
|
+
currencyCode
|
|
602
|
+
}
|
|
603
|
+
discountTotal {
|
|
604
|
+
value
|
|
605
|
+
currencyCode
|
|
606
|
+
}
|
|
607
|
+
feeTotal {
|
|
608
|
+
value
|
|
609
|
+
currencyCode
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
discounts {
|
|
613
|
+
id
|
|
614
|
+
name
|
|
615
|
+
code
|
|
616
|
+
amount {
|
|
617
|
+
value
|
|
618
|
+
currencyCode
|
|
619
|
+
}
|
|
620
|
+
ratePercentage
|
|
621
|
+
}
|
|
622
|
+
taxes {
|
|
623
|
+
id
|
|
624
|
+
name
|
|
625
|
+
amount {
|
|
626
|
+
value
|
|
627
|
+
currencyCode
|
|
628
|
+
}
|
|
629
|
+
ratePercentage
|
|
630
|
+
}
|
|
631
|
+
notes {
|
|
632
|
+
id
|
|
633
|
+
content
|
|
634
|
+
author
|
|
635
|
+
authorType
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
totals {
|
|
639
|
+
subTotal {
|
|
640
|
+
value
|
|
641
|
+
currencyCode
|
|
642
|
+
}
|
|
643
|
+
shippingTotal {
|
|
644
|
+
value
|
|
645
|
+
currencyCode
|
|
646
|
+
}
|
|
647
|
+
taxTotal {
|
|
648
|
+
value
|
|
649
|
+
currencyCode
|
|
650
|
+
}
|
|
651
|
+
discountTotal {
|
|
652
|
+
value
|
|
653
|
+
currencyCode
|
|
654
|
+
}
|
|
655
|
+
productDiscountTotal {
|
|
656
|
+
value
|
|
657
|
+
currencyCode
|
|
658
|
+
}
|
|
659
|
+
shippingDiscountTotal {
|
|
660
|
+
value
|
|
661
|
+
currencyCode
|
|
662
|
+
}
|
|
663
|
+
feeTotal {
|
|
664
|
+
value
|
|
665
|
+
currencyCode
|
|
666
|
+
}
|
|
667
|
+
total {
|
|
668
|
+
value
|
|
669
|
+
currencyCode
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
discounts {
|
|
673
|
+
id
|
|
674
|
+
name
|
|
675
|
+
code
|
|
676
|
+
amount {
|
|
677
|
+
value
|
|
678
|
+
currencyCode
|
|
679
|
+
}
|
|
680
|
+
ratePercentage
|
|
681
|
+
appliedBeforeTax
|
|
682
|
+
}
|
|
683
|
+
taxes {
|
|
684
|
+
id
|
|
685
|
+
name
|
|
686
|
+
amount {
|
|
687
|
+
value
|
|
688
|
+
currencyCode
|
|
689
|
+
}
|
|
690
|
+
ratePercentage
|
|
691
|
+
included
|
|
692
|
+
exempted
|
|
693
|
+
}
|
|
694
|
+
shipping {
|
|
695
|
+
firstName
|
|
696
|
+
lastName
|
|
697
|
+
email
|
|
698
|
+
phone
|
|
699
|
+
companyName
|
|
700
|
+
address {
|
|
701
|
+
addressLine1
|
|
702
|
+
addressLine2
|
|
703
|
+
addressLine3
|
|
704
|
+
adminArea1
|
|
705
|
+
adminArea2
|
|
706
|
+
adminArea3
|
|
707
|
+
adminArea4
|
|
708
|
+
postalCode
|
|
709
|
+
countryCode
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
notes {
|
|
713
|
+
id
|
|
714
|
+
content
|
|
715
|
+
author
|
|
716
|
+
authorType
|
|
717
|
+
createdAt
|
|
718
|
+
}
|
|
719
|
+
tags
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
`;
|
|
723
|
+
|
|
724
|
+
//#endregion
|
|
725
|
+
//#region src/server/api/commerce/cart/[id]/discounts/POST.ts
|
|
726
|
+
const applyDiscountCodesMutation = `
|
|
727
|
+
mutation ApplyDiscountCodes($input: ApplyDiscountCodesInput!) {
|
|
728
|
+
applyDiscountCodes(input: $input) {
|
|
729
|
+
id
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
`;
|
|
733
|
+
async function handler(req, res) {
|
|
734
|
+
try {
|
|
735
|
+
const cartId = req.params.id;
|
|
736
|
+
if (typeof cartId !== "string" || !cartId) {
|
|
737
|
+
res.status(400).json({ error: "Missing cart id" });
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const body = req.body ?? {};
|
|
741
|
+
const codes = Array.isArray(body.discountCodes) ? body.discountCodes.filter((code) => typeof code === "string" && code.length > 0) : [];
|
|
742
|
+
if (codes.length === 0) {
|
|
743
|
+
res.status(400).json({ error: "discountCodes must be a non-empty string array" });
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
const config = readCommerceConfigForResponse(res);
|
|
747
|
+
if (!validateCommerceCartScope(req, res, config)) return;
|
|
748
|
+
const { storeId, clientId, apiBaseUrl } = config;
|
|
749
|
+
const endpoint = orderStorefrontEndpoint({ apiBaseUrl });
|
|
750
|
+
const headers = storefrontHeaders({
|
|
751
|
+
storeId,
|
|
752
|
+
clientId
|
|
753
|
+
});
|
|
754
|
+
await gqlRequest({
|
|
755
|
+
endpoint,
|
|
756
|
+
query: applyDiscountCodesMutation,
|
|
757
|
+
variables: { input: {
|
|
758
|
+
orderId: cartId,
|
|
759
|
+
discountCodes: codes
|
|
760
|
+
} },
|
|
761
|
+
headers
|
|
762
|
+
});
|
|
763
|
+
const hydrated = await gqlRequest({
|
|
764
|
+
endpoint,
|
|
765
|
+
query: getCartOrderQuery,
|
|
766
|
+
variables: { id: cartId },
|
|
767
|
+
headers
|
|
768
|
+
});
|
|
769
|
+
res.json({ cart: hydrated.orderById ?? null });
|
|
770
|
+
} catch (error) {
|
|
771
|
+
res.status(500).json({
|
|
772
|
+
error: "Failed to apply discount codes",
|
|
773
|
+
message: error instanceof Error ? error.message : String(error)
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
//#endregion
|
|
779
|
+
//#region src/server/api/commerce/cart/[id]/GET.ts
|
|
780
|
+
function isCartNotFoundError(error) {
|
|
781
|
+
if (!(error instanceof GraphQLErrorWithCodes)) return false;
|
|
782
|
+
const isFailure = (status) => status !== void 0 && status >= 400 && status !== 404 && status !== 410;
|
|
783
|
+
if (isFailure(error.status) || error.errors.length === 0) return false;
|
|
784
|
+
return error.errors.every(({ code, message, status }) => {
|
|
785
|
+
if (isFailure(status)) return false;
|
|
786
|
+
if (code === "INTERNAL_SERVER_ERROR" && message === "Order not found") return true;
|
|
787
|
+
if (code && /^(?:DRAFT[_-]?)?(?:ORDER|CART)[_-]?(?:NOT[_-]?FOUND|EXPIRED)$/i.test(code)) return true;
|
|
788
|
+
if (code && !/^(?:NOT[_-]?FOUND|EXPIRED)$/i.test(code)) return false;
|
|
789
|
+
return /\b(?:cart|(?:draft[ -])?order)\s+(?:(?:is|was|has)\s+)?(?:not found|expired)\b/i.test(message ?? "");
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
async function handler$1(req, res) {
|
|
793
|
+
try {
|
|
794
|
+
const cartId = req.params.id;
|
|
795
|
+
if (typeof cartId !== "string" || !cartId) {
|
|
796
|
+
res.status(400).json({ error: "Missing cart id" });
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
const config = readCommerceConfigForResponse(res);
|
|
800
|
+
if (!validateCommerceCartScope(req, res, config)) return;
|
|
801
|
+
const { storeId, clientId, apiBaseUrl } = config;
|
|
802
|
+
const data = await gqlRequest({
|
|
803
|
+
endpoint: orderStorefrontEndpoint({ apiBaseUrl }),
|
|
804
|
+
query: getCartOrderQuery,
|
|
805
|
+
variables: { id: cartId },
|
|
806
|
+
headers: storefrontHeaders({
|
|
807
|
+
storeId,
|
|
808
|
+
clientId
|
|
809
|
+
})
|
|
810
|
+
});
|
|
811
|
+
res.json({ cart: data.orderById ?? null });
|
|
812
|
+
} catch (error) {
|
|
813
|
+
if (isCartNotFoundError(error)) {
|
|
814
|
+
res.status(200).json({ cart: null });
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
res.status(500).json({
|
|
818
|
+
error: "Failed to load cart",
|
|
819
|
+
message: error instanceof Error ? error.message : String(error)
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
//#endregion
|
|
825
|
+
//#region src/server/api/commerce/cart/[id]/items/[itemId]/DELETE.ts
|
|
826
|
+
const deleteLineItemByIdMutation = `
|
|
827
|
+
mutation DeleteLineItemById($id: ID!, $orderId: ID!) {
|
|
828
|
+
deleteLineItemById(id: $id, orderId: $orderId)
|
|
829
|
+
}
|
|
830
|
+
`;
|
|
831
|
+
async function handler$2(req, res) {
|
|
832
|
+
try {
|
|
833
|
+
const cartId = req.params.id;
|
|
834
|
+
const itemId = req.params.itemId;
|
|
835
|
+
if (typeof cartId !== "string" || !cartId || typeof itemId !== "string" || !itemId) {
|
|
836
|
+
res.status(400).json({ error: "Missing cart id or item id" });
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
const config = readCommerceConfigForResponse(res);
|
|
840
|
+
if (!validateCommerceCartScope(req, res, config)) return;
|
|
841
|
+
const { storeId, clientId, apiBaseUrl } = config;
|
|
842
|
+
const endpoint = orderStorefrontEndpoint({ apiBaseUrl });
|
|
843
|
+
const headers = storefrontHeaders({
|
|
844
|
+
storeId,
|
|
845
|
+
clientId
|
|
846
|
+
});
|
|
847
|
+
await gqlRequest({
|
|
848
|
+
endpoint,
|
|
849
|
+
query: deleteLineItemByIdMutation,
|
|
850
|
+
variables: {
|
|
851
|
+
id: itemId,
|
|
852
|
+
orderId: cartId
|
|
853
|
+
},
|
|
854
|
+
headers
|
|
855
|
+
});
|
|
856
|
+
const hydrated = await gqlRequest({
|
|
857
|
+
endpoint,
|
|
858
|
+
query: getCartOrderQuery,
|
|
859
|
+
variables: { id: cartId },
|
|
860
|
+
headers
|
|
861
|
+
});
|
|
862
|
+
res.json({ cart: hydrated.orderById ?? null });
|
|
863
|
+
} catch (error) {
|
|
864
|
+
res.status(500).json({
|
|
865
|
+
error: "Failed to delete line item",
|
|
866
|
+
message: error instanceof Error ? error.message : String(error)
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
//#endregion
|
|
872
|
+
//#region src/server/api/commerce/cart/[id]/items/[itemId]/PATCH.ts
|
|
873
|
+
const updateLineItemByIdMutation = `
|
|
874
|
+
mutation UpdateLineItemById($input: UpdateLineItemByIdInput!) {
|
|
875
|
+
updateLineItemById(input: $input) {
|
|
876
|
+
id
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
`;
|
|
880
|
+
async function handler$3(req, res) {
|
|
881
|
+
try {
|
|
882
|
+
const cartId = req.params.id;
|
|
883
|
+
const itemId = req.params.itemId;
|
|
884
|
+
if (typeof cartId !== "string" || !cartId || typeof itemId !== "string" || !itemId) {
|
|
885
|
+
res.status(400).json({ error: "Missing cart id or item id" });
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
const body = req.body ?? {};
|
|
889
|
+
const config = readCommerceConfigForResponse(res);
|
|
890
|
+
if (!validateCommerceCartScope(req, res, config)) return;
|
|
891
|
+
const { storeId, clientId, apiBaseUrl } = config;
|
|
892
|
+
const endpoint = orderStorefrontEndpoint({ apiBaseUrl });
|
|
893
|
+
const headers = storefrontHeaders({
|
|
894
|
+
storeId,
|
|
895
|
+
clientId
|
|
896
|
+
});
|
|
897
|
+
await gqlRequest({
|
|
898
|
+
endpoint,
|
|
899
|
+
query: updateLineItemByIdMutation,
|
|
900
|
+
variables: { input: {
|
|
901
|
+
id: itemId,
|
|
902
|
+
orderId: cartId,
|
|
903
|
+
name: body.name,
|
|
904
|
+
quantity: body.quantity,
|
|
905
|
+
fulfillmentMode: body.fulfillmentMode,
|
|
906
|
+
status: body.status,
|
|
907
|
+
type: body.type,
|
|
908
|
+
details: body.details
|
|
909
|
+
} },
|
|
910
|
+
headers
|
|
911
|
+
});
|
|
912
|
+
const hydrated = await gqlRequest({
|
|
913
|
+
endpoint,
|
|
914
|
+
query: getCartOrderQuery,
|
|
915
|
+
variables: { id: cartId },
|
|
916
|
+
headers
|
|
917
|
+
});
|
|
918
|
+
res.json({ cart: hydrated.orderById ?? null });
|
|
919
|
+
} catch (error) {
|
|
920
|
+
res.status(500).json({
|
|
921
|
+
error: "Failed to update line item",
|
|
922
|
+
message: error instanceof Error ? error.message : String(error)
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
//#endregion
|
|
928
|
+
//#region src/server/api/commerce/cart/[id]/items/POST.ts
|
|
929
|
+
const addLineItemBySkuIdMutation$1 = `
|
|
930
|
+
mutation AddLineItemBySkuId($input: AddLineItemInput!) {
|
|
931
|
+
addLineItemBySkuId(input: $input) {
|
|
932
|
+
id
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
`;
|
|
936
|
+
async function handler$4(req, res) {
|
|
937
|
+
try {
|
|
938
|
+
const cartId = req.params.id;
|
|
939
|
+
if (typeof cartId !== "string" || !cartId) {
|
|
940
|
+
res.status(400).json({ error: "Missing cart id" });
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
const body = req.body ?? {};
|
|
944
|
+
if (!body.skuId || !body.name || typeof body.quantity !== "number") {
|
|
945
|
+
res.status(400).json({ error: "Missing required fields: skuId, name, quantity" });
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
const config = readCommerceConfigForResponse(res);
|
|
949
|
+
if (!validateCommerceCartScope(req, res, config)) return;
|
|
950
|
+
const { storeId, clientId, apiBaseUrl } = config;
|
|
951
|
+
const endpoint = orderStorefrontEndpoint({ apiBaseUrl });
|
|
952
|
+
const headers = storefrontHeaders({
|
|
953
|
+
storeId,
|
|
954
|
+
clientId
|
|
955
|
+
});
|
|
956
|
+
await gqlRequest({
|
|
957
|
+
endpoint,
|
|
958
|
+
query: addLineItemBySkuIdMutation$1,
|
|
959
|
+
variables: { input: buildAddLineItemBySkuIdInput(cartId, {
|
|
960
|
+
skuId: body.skuId,
|
|
961
|
+
name: body.name,
|
|
962
|
+
quantity: body.quantity
|
|
963
|
+
}) },
|
|
964
|
+
headers
|
|
965
|
+
});
|
|
966
|
+
const hydrated = await gqlRequest({
|
|
967
|
+
endpoint,
|
|
968
|
+
query: getCartOrderQuery,
|
|
969
|
+
variables: { id: cartId },
|
|
970
|
+
headers
|
|
971
|
+
});
|
|
972
|
+
res.status(201).json({ cart: hydrated.orderById ?? null });
|
|
973
|
+
} catch (error) {
|
|
974
|
+
res.status(500).json({
|
|
975
|
+
error: "Failed to add line item",
|
|
976
|
+
message: error instanceof Error ? error.message : String(error)
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
//#endregion
|
|
982
|
+
//#region src/server/api/commerce/cart/POST.ts
|
|
983
|
+
const addCartOrderMutation = `
|
|
984
|
+
mutation AddCartOrder($input: AddDraftOrderInput!) {
|
|
985
|
+
addDraftOrder(input: $input) {
|
|
986
|
+
id
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
`;
|
|
990
|
+
const addLineItemBySkuIdMutation = `
|
|
991
|
+
mutation AddLineItemBySkuId($input: AddLineItemInput!) {
|
|
992
|
+
addLineItemBySkuId(input: $input) {
|
|
993
|
+
id
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
`;
|
|
997
|
+
async function handler$5(req, res) {
|
|
998
|
+
try {
|
|
999
|
+
const body = req.body ?? {};
|
|
1000
|
+
const initialItems = Array.isArray(body.lineItems) ? body.lineItems : [];
|
|
1001
|
+
for (const item of initialItems) if (!item.skuId || !item.name || typeof item.quantity !== "number") {
|
|
1002
|
+
res.status(400).json({ error: "Each lineItem must have skuId, a non-empty name, and a numeric quantity" });
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
const config = readCommerceConfigForResponse(res);
|
|
1006
|
+
if (!validateCommerceCartScope(req, res, config)) return;
|
|
1007
|
+
const { storeId, channelId, clientId, apiBaseUrl, currencyCode } = config;
|
|
1008
|
+
const endpoint = orderStorefrontEndpoint({ apiBaseUrl });
|
|
1009
|
+
const headers = storefrontHeaders({
|
|
1010
|
+
storeId,
|
|
1011
|
+
clientId
|
|
1012
|
+
});
|
|
1013
|
+
const newCartId = (await gqlRequest({
|
|
1014
|
+
endpoint,
|
|
1015
|
+
query: addCartOrderMutation,
|
|
1016
|
+
variables: { input: buildEmptyCartOrderInput({
|
|
1017
|
+
storeId,
|
|
1018
|
+
channelId,
|
|
1019
|
+
owner: config.owner,
|
|
1020
|
+
currencyCode: body.currencyCode ?? currencyCode
|
|
1021
|
+
}) },
|
|
1022
|
+
headers
|
|
1023
|
+
})).addDraftOrder?.id;
|
|
1024
|
+
if (!newCartId) {
|
|
1025
|
+
res.status(500).json({ error: "Failed to create cart: missing id in mutation response" });
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
for (const item of initialItems) await gqlRequest({
|
|
1029
|
+
endpoint,
|
|
1030
|
+
query: addLineItemBySkuIdMutation,
|
|
1031
|
+
variables: { input: buildAddLineItemBySkuIdInput(newCartId, item) },
|
|
1032
|
+
headers
|
|
1033
|
+
});
|
|
1034
|
+
const hydrated = await gqlRequest({
|
|
1035
|
+
endpoint,
|
|
1036
|
+
query: getCartOrderQuery,
|
|
1037
|
+
variables: { id: newCartId },
|
|
1038
|
+
headers
|
|
1039
|
+
});
|
|
1040
|
+
res.status(201).json({ cart: hydrated.orderById ?? null });
|
|
1041
|
+
} catch (error) {
|
|
1042
|
+
res.status(500).json({
|
|
1043
|
+
error: "Failed to create cart",
|
|
1044
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
//#endregion
|
|
1050
|
+
//#region src/server/api/commerce/checkout/POST.ts
|
|
1051
|
+
async function handler$6(req, res) {
|
|
1052
|
+
try {
|
|
1053
|
+
const configuration = commerceConfigurationForResponse(res);
|
|
1054
|
+
const { draftOrderId, skuId, quantity, lineItemData, returnUrl, successUrl } = req.body ?? {};
|
|
1055
|
+
if (lineItemData !== void 0) {
|
|
1056
|
+
res.status(400).json({ error: "Non-catalog checkout must be created by the server." });
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
if ([draftOrderId, skuId].filter(Boolean).length !== 1 || [draftOrderId, skuId].some((value) => value !== void 0 && (typeof value !== "string" || !value.trim()))) {
|
|
1060
|
+
res.status(400).json({ error: "exactly one non-empty draftOrderId or skuId is required" });
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
if (quantity !== void 0 && (!Number.isSafeInteger(quantity) || quantity < 1)) {
|
|
1064
|
+
res.status(400).json({ error: "quantity must be a positive whole number" });
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
if (req.headers?.["x-commerce-scope"] !== void 0) {
|
|
1068
|
+
if (!validateCommerceCartScope(req, res, configuration.read())) return;
|
|
1069
|
+
}
|
|
1070
|
+
const validateReturnUrls = res.locals.commerceCheckoutReturnUrlValidator;
|
|
1071
|
+
if (!validateReturnUrls) {
|
|
1072
|
+
res.status(503).json({ error: "Checkout return destinations are not configured." });
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
const destinations = validateReturnUrls(returnUrl, successUrl);
|
|
1076
|
+
if (!destinations) {
|
|
1077
|
+
res.status(400).json({ error: "Checkout return destinations are not allowed." });
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
const session = await createCheckoutSession({
|
|
1081
|
+
draftOrderId,
|
|
1082
|
+
skuId,
|
|
1083
|
+
quantity,
|
|
1084
|
+
...destinations
|
|
1085
|
+
}, configuration);
|
|
1086
|
+
res.status(200).json(session);
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
res.status(500).json({
|
|
1089
|
+
error: "Failed to create checkout session",
|
|
1090
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
//#endregion
|
|
1096
|
+
//#region src/server/api/commerce/config/GET.ts
|
|
1097
|
+
async function handler$7(_req, res) {
|
|
1098
|
+
res.setHeader("Cache-Control", "no-store");
|
|
1099
|
+
try {
|
|
1100
|
+
const config = commerceConfigurationForResponse(res).read();
|
|
1101
|
+
res.json({
|
|
1102
|
+
cartScope: getCommerceCartScope(config),
|
|
1103
|
+
currencyCode: config.currencyCode
|
|
1104
|
+
});
|
|
1105
|
+
} catch (cause) {
|
|
1106
|
+
res.status(503).json({
|
|
1107
|
+
error: "Commerce configuration is unavailable. Complete the store connection before continuing.",
|
|
1108
|
+
message: cause instanceof Error ? cause.message : "Invalid Commerce configuration."
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
//#endregion
|
|
1114
|
+
//#region src/server/api/commerce/order-status/GET.ts
|
|
1115
|
+
async function handler$8(req, res) {
|
|
1116
|
+
try {
|
|
1117
|
+
const { orderId } = req.query;
|
|
1118
|
+
if (!orderId || typeof orderId !== "string") {
|
|
1119
|
+
res.status(400).json({
|
|
1120
|
+
success: false,
|
|
1121
|
+
error: "missing or invalid orderId query parameter"
|
|
1122
|
+
});
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
const order = await getOrderStatus(orderId, commerceConfigurationForResponse(res));
|
|
1126
|
+
res.status(200).json({
|
|
1127
|
+
success: true,
|
|
1128
|
+
order
|
|
1129
|
+
});
|
|
1130
|
+
} catch (error) {
|
|
1131
|
+
res.status(500).json({
|
|
1132
|
+
success: false,
|
|
1133
|
+
error: "Failed to get order status",
|
|
1134
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
//#endregion
|
|
1140
|
+
//#region src/lib/commerce/catalog-subgraph.ts
|
|
1141
|
+
function catalogStorefrontEndpoint({ storeId, apiBaseUrl }) {
|
|
1142
|
+
return new URL(`/v2/commerce/stores/${storeId}/catalog-subgraph/storefront`, apiBaseUrl).toString();
|
|
1143
|
+
}
|
|
1144
|
+
function buildSkuGroupsVariables(input) {
|
|
1145
|
+
const productIds = input.productIds?.filter(Boolean) ?? [];
|
|
1146
|
+
const categoryIds = input.categoryIds?.filter(Boolean) ?? [];
|
|
1147
|
+
const hasExplicitFilters = productIds.length > 0 || categoryIds.length > 0;
|
|
1148
|
+
return {
|
|
1149
|
+
...input.first !== void 0 && { first: input.first },
|
|
1150
|
+
...input.after && { after: input.after },
|
|
1151
|
+
...productIds.length > 0 && { id: { in: [...productIds] } },
|
|
1152
|
+
...categoryIds.length > 0 && { listId: { in: [...categoryIds] } },
|
|
1153
|
+
...!hasExplicitFilters && input.searchQuery && { label: { contains: input.searchQuery } }
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
function buildSkuGroupVariables({ productId, selectedAttributeValues, skuGroupFirst }) {
|
|
1157
|
+
const attributeValues = selectedAttributeValues ? [...selectedAttributeValues] : [];
|
|
1158
|
+
return {
|
|
1159
|
+
id: productId,
|
|
1160
|
+
attributeValues,
|
|
1161
|
+
...!attributeValues.length && { first: skuGroupFirst ?? 50 }
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
//#endregion
|
|
1166
|
+
//#region src/server/api/commerce/products/[id]/GET.ts
|
|
1167
|
+
const skuGroupQuery = `
|
|
1168
|
+
query SkuGroup($id: String!, $first: Int, $attributeValues: [String!] = []) {
|
|
1169
|
+
skuGroup(id: $id) {
|
|
1170
|
+
id
|
|
1171
|
+
status
|
|
1172
|
+
name
|
|
1173
|
+
label
|
|
1174
|
+
description
|
|
1175
|
+
htmlDescription
|
|
1176
|
+
type
|
|
1177
|
+
priceRange(status: { eq: "ACTIVE" }) {
|
|
1178
|
+
min
|
|
1179
|
+
max
|
|
1180
|
+
}
|
|
1181
|
+
compareAtPriceRange(status: { eq: "ACTIVE" }) {
|
|
1182
|
+
min
|
|
1183
|
+
max
|
|
1184
|
+
}
|
|
1185
|
+
mediaObjects(first: 25) {
|
|
1186
|
+
edges {
|
|
1187
|
+
node {
|
|
1188
|
+
url
|
|
1189
|
+
type
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
attributes(first: 50, orderBy: { position: ASC }) {
|
|
1194
|
+
edges {
|
|
1195
|
+
node {
|
|
1196
|
+
id
|
|
1197
|
+
name
|
|
1198
|
+
label
|
|
1199
|
+
description
|
|
1200
|
+
htmlDescription
|
|
1201
|
+
values(first: 50, orderBy: { position: ASC }) {
|
|
1202
|
+
edges {
|
|
1203
|
+
node {
|
|
1204
|
+
id
|
|
1205
|
+
name
|
|
1206
|
+
label
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
skus(attributeValues: { has: $attributeValues }, first: $first, status: { eq: "ACTIVE" }) {
|
|
1214
|
+
pageInfo { hasNextPage }
|
|
1215
|
+
totalCount
|
|
1216
|
+
edges {
|
|
1217
|
+
node {
|
|
1218
|
+
id
|
|
1219
|
+
label
|
|
1220
|
+
name
|
|
1221
|
+
description
|
|
1222
|
+
prices(first: 10) {
|
|
1223
|
+
edges {
|
|
1224
|
+
node {
|
|
1225
|
+
value { value currencyCode }
|
|
1226
|
+
compareAtValue { value currencyCode }
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
mediaObjects(first: 25) {
|
|
1231
|
+
edges { node { url type } }
|
|
1232
|
+
}
|
|
1233
|
+
inventoryCounts {
|
|
1234
|
+
edges {
|
|
1235
|
+
node {
|
|
1236
|
+
id
|
|
1237
|
+
quantity
|
|
1238
|
+
type
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
`;
|
|
1248
|
+
function asStringArray$1(value) {
|
|
1249
|
+
if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
|
|
1250
|
+
if (typeof value === "string" && value.length > 0) return [value];
|
|
1251
|
+
return [];
|
|
1252
|
+
}
|
|
1253
|
+
function asNumber$1(value) {
|
|
1254
|
+
if (typeof value !== "string" || value.length === 0) return void 0;
|
|
1255
|
+
const n = Number(value);
|
|
1256
|
+
return Number.isFinite(n) ? n : void 0;
|
|
1257
|
+
}
|
|
1258
|
+
async function handler$9(req, res) {
|
|
1259
|
+
try {
|
|
1260
|
+
const productId = req.params.id;
|
|
1261
|
+
if (typeof productId !== "string" || !productId) {
|
|
1262
|
+
res.status(400).json({ error: "Missing product id" });
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
const config = readCommerceConfigForResponse(res);
|
|
1266
|
+
if (!validateCommerceCartScope(req, res, config)) return;
|
|
1267
|
+
const { storeId, clientId, apiBaseUrl } = config;
|
|
1268
|
+
const variables = buildSkuGroupVariables({
|
|
1269
|
+
productId,
|
|
1270
|
+
selectedAttributeValues: asStringArray$1(req.query.attributeValues),
|
|
1271
|
+
skuGroupFirst: asNumber$1(req.query.skuGroupFirst)
|
|
1272
|
+
});
|
|
1273
|
+
const data = await gqlRequest({
|
|
1274
|
+
endpoint: catalogStorefrontEndpoint({
|
|
1275
|
+
storeId,
|
|
1276
|
+
apiBaseUrl
|
|
1277
|
+
}),
|
|
1278
|
+
query: skuGroupQuery,
|
|
1279
|
+
variables,
|
|
1280
|
+
headers: storefrontHeaders({
|
|
1281
|
+
storeId,
|
|
1282
|
+
clientId
|
|
1283
|
+
})
|
|
1284
|
+
});
|
|
1285
|
+
res.json(data);
|
|
1286
|
+
} catch (error) {
|
|
1287
|
+
res.status(500).json({
|
|
1288
|
+
error: "Failed to load product",
|
|
1289
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
//#endregion
|
|
1295
|
+
//#region src/server/api/commerce/products/GET.ts
|
|
1296
|
+
const skuGroupsQuery = `
|
|
1297
|
+
query SkuGroups($first: Int, $after: String, $id: SKUGroupIdsFilter, $listId: ListIdFilter, $label: LabelFilter) {
|
|
1298
|
+
skuGroups(
|
|
1299
|
+
first: $first
|
|
1300
|
+
after: $after
|
|
1301
|
+
id: $id
|
|
1302
|
+
listId: $listId
|
|
1303
|
+
label: $label
|
|
1304
|
+
status: { eq: "ACTIVE" }
|
|
1305
|
+
) {
|
|
1306
|
+
edges {
|
|
1307
|
+
cursor
|
|
1308
|
+
node {
|
|
1309
|
+
id
|
|
1310
|
+
name
|
|
1311
|
+
label
|
|
1312
|
+
description
|
|
1313
|
+
htmlDescription
|
|
1314
|
+
type
|
|
1315
|
+
priceRange(status: { eq: "ACTIVE" }) {
|
|
1316
|
+
min
|
|
1317
|
+
max
|
|
1318
|
+
}
|
|
1319
|
+
compareAtPriceRange(status: { eq: "ACTIVE" }) {
|
|
1320
|
+
min
|
|
1321
|
+
max
|
|
1322
|
+
}
|
|
1323
|
+
mediaObjects(first: 25) {
|
|
1324
|
+
edges {
|
|
1325
|
+
node {
|
|
1326
|
+
url
|
|
1327
|
+
type
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
attributes(first: 50, orderBy: { position: ASC }) {
|
|
1332
|
+
edges {
|
|
1333
|
+
node {
|
|
1334
|
+
id
|
|
1335
|
+
name
|
|
1336
|
+
label
|
|
1337
|
+
description
|
|
1338
|
+
htmlDescription
|
|
1339
|
+
values(first: 50, orderBy: { position: ASC }) {
|
|
1340
|
+
edges {
|
|
1341
|
+
node {
|
|
1342
|
+
id
|
|
1343
|
+
name
|
|
1344
|
+
label
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
skus(first: 2, status: { eq: "ACTIVE" }) {
|
|
1352
|
+
pageInfo { hasNextPage }
|
|
1353
|
+
totalCount
|
|
1354
|
+
edges {
|
|
1355
|
+
node {
|
|
1356
|
+
id
|
|
1357
|
+
label
|
|
1358
|
+
name
|
|
1359
|
+
inventoryCounts {
|
|
1360
|
+
edges {
|
|
1361
|
+
node {
|
|
1362
|
+
id
|
|
1363
|
+
quantity
|
|
1364
|
+
type
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
pageInfo {
|
|
1374
|
+
hasNextPage
|
|
1375
|
+
hasPreviousPage
|
|
1376
|
+
startCursor
|
|
1377
|
+
endCursor
|
|
1378
|
+
}
|
|
1379
|
+
totalCount
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
`;
|
|
1383
|
+
function asStringArray(value) {
|
|
1384
|
+
if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
|
|
1385
|
+
if (typeof value === "string" && value.length > 0) return [value];
|
|
1386
|
+
return [];
|
|
1387
|
+
}
|
|
1388
|
+
function asNumber(value) {
|
|
1389
|
+
if (typeof value !== "string" || value.length === 0) return void 0;
|
|
1390
|
+
const n = Number(value);
|
|
1391
|
+
return Number.isFinite(n) ? n : void 0;
|
|
1392
|
+
}
|
|
1393
|
+
async function handler$10(req, res) {
|
|
1394
|
+
try {
|
|
1395
|
+
const config = readCommerceConfigForResponse(res);
|
|
1396
|
+
if (!validateCommerceCartScope(req, res, config)) return;
|
|
1397
|
+
const { storeId, clientId, apiBaseUrl } = config;
|
|
1398
|
+
const variables = buildSkuGroupsVariables({
|
|
1399
|
+
first: asNumber(req.query.first) ?? 24,
|
|
1400
|
+
after: typeof req.query.after === "string" ? req.query.after : void 0,
|
|
1401
|
+
searchQuery: typeof req.query.searchQuery === "string" ? req.query.searchQuery : void 0,
|
|
1402
|
+
productIds: asStringArray(req.query.productIds),
|
|
1403
|
+
categoryIds: asStringArray(req.query.categoryIds)
|
|
1404
|
+
});
|
|
1405
|
+
const data = await gqlRequest({
|
|
1406
|
+
endpoint: catalogStorefrontEndpoint({
|
|
1407
|
+
storeId,
|
|
1408
|
+
apiBaseUrl
|
|
1409
|
+
}),
|
|
1410
|
+
query: skuGroupsQuery,
|
|
1411
|
+
variables,
|
|
1412
|
+
headers: storefrontHeaders({
|
|
1413
|
+
storeId,
|
|
1414
|
+
clientId
|
|
1415
|
+
})
|
|
1416
|
+
});
|
|
1417
|
+
res.json(data);
|
|
1418
|
+
} catch (error) {
|
|
1419
|
+
res.status(500).json({
|
|
1420
|
+
error: "Failed to load products",
|
|
1421
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
//#endregion
|
|
1427
|
+
//#region src/server/api/commerce/skus/[id]/GET.ts
|
|
1428
|
+
const skuQuery = `
|
|
1429
|
+
query Sku($id: String!) {
|
|
1430
|
+
sku(id: $id) {
|
|
1431
|
+
id
|
|
1432
|
+
label
|
|
1433
|
+
name
|
|
1434
|
+
description
|
|
1435
|
+
htmlDescription
|
|
1436
|
+
code
|
|
1437
|
+
prices {
|
|
1438
|
+
edges {
|
|
1439
|
+
node {
|
|
1440
|
+
id
|
|
1441
|
+
value {
|
|
1442
|
+
value
|
|
1443
|
+
currencyCode
|
|
1444
|
+
}
|
|
1445
|
+
compareAtValue {
|
|
1446
|
+
value
|
|
1447
|
+
currencyCode
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
inventoryCounts {
|
|
1453
|
+
edges {
|
|
1454
|
+
node {
|
|
1455
|
+
id
|
|
1456
|
+
quantity
|
|
1457
|
+
type
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
mediaObjects {
|
|
1462
|
+
edges {
|
|
1463
|
+
node {
|
|
1464
|
+
id
|
|
1465
|
+
url
|
|
1466
|
+
type
|
|
1467
|
+
label
|
|
1468
|
+
position
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
attributeValues {
|
|
1473
|
+
edges {
|
|
1474
|
+
node {
|
|
1475
|
+
id
|
|
1476
|
+
name
|
|
1477
|
+
label
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
`;
|
|
1484
|
+
async function handler$11(req, res) {
|
|
1485
|
+
try {
|
|
1486
|
+
const skuId = req.params.id;
|
|
1487
|
+
if (typeof skuId !== "string" || !skuId) {
|
|
1488
|
+
res.status(400).json({ error: "Missing sku id" });
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
const config = readCommerceConfigForResponse(res);
|
|
1492
|
+
if (!validateCommerceCartScope(req, res, config)) return;
|
|
1493
|
+
const { storeId, clientId, apiBaseUrl } = config;
|
|
1494
|
+
const data = await gqlRequest({
|
|
1495
|
+
endpoint: catalogStorefrontEndpoint({
|
|
1496
|
+
storeId,
|
|
1497
|
+
apiBaseUrl
|
|
1498
|
+
}),
|
|
1499
|
+
query: skuQuery,
|
|
1500
|
+
variables: { id: skuId },
|
|
1501
|
+
headers: storefrontHeaders({
|
|
1502
|
+
storeId,
|
|
1503
|
+
clientId
|
|
1504
|
+
})
|
|
1505
|
+
});
|
|
1506
|
+
res.json(data);
|
|
1507
|
+
} catch (error) {
|
|
1508
|
+
res.status(500).json({
|
|
1509
|
+
error: "Failed to load sku",
|
|
1510
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
//#endregion
|
|
1516
|
+
//#region src/router.ts
|
|
1517
|
+
function createCommerceRouter(options = {}) {
|
|
1518
|
+
const router = Router();
|
|
1519
|
+
const configuration = options.configuration ?? createRuntimeCommerceConfiguration();
|
|
1520
|
+
const catalogEnabled = options.features?.catalog ?? true;
|
|
1521
|
+
const paymentsEnabled = options.features?.payments ?? true;
|
|
1522
|
+
const validateCheckoutReturnUrls = options.checkoutReturnUrls ? createCheckoutReturnUrlValidator(options.checkoutReturnUrls) : void 0;
|
|
1523
|
+
router.use((_req, res, next) => {
|
|
1524
|
+
res.locals.commerceConfiguration = configuration;
|
|
1525
|
+
res.locals.commerceCheckoutReturnUrlValidator = validateCheckoutReturnUrls;
|
|
1526
|
+
next();
|
|
1527
|
+
});
|
|
1528
|
+
if (catalogEnabled) {
|
|
1529
|
+
router.get("/config", handler$7);
|
|
1530
|
+
router.get("/products", handler$10);
|
|
1531
|
+
router.get("/products/:id", handler$9);
|
|
1532
|
+
router.get("/skus/:id", handler$11);
|
|
1533
|
+
router.post("/cart", handler$5);
|
|
1534
|
+
router.get("/cart/:id", handler$1);
|
|
1535
|
+
router.post("/cart/:id/items", handler$4);
|
|
1536
|
+
router.patch("/cart/:id/items/:itemId", handler$3);
|
|
1537
|
+
router.delete("/cart/:id/items/:itemId", handler$2);
|
|
1538
|
+
router.post("/cart/:id/discounts", handler);
|
|
1539
|
+
}
|
|
1540
|
+
if (paymentsEnabled) {
|
|
1541
|
+
router.post("/checkout", handler$6);
|
|
1542
|
+
router.get("/order-status", handler$8);
|
|
1543
|
+
}
|
|
1544
|
+
return router;
|
|
1545
|
+
}
|
|
1546
|
+
function createCommerceCatalogRouter(configuration) {
|
|
1547
|
+
return createCommerceRouter({
|
|
1548
|
+
configuration,
|
|
1549
|
+
features: {
|
|
1550
|
+
catalog: true,
|
|
1551
|
+
payments: false
|
|
1552
|
+
}
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
function createGoDaddyPaymentsRouter(configuration, checkoutReturnUrls) {
|
|
1556
|
+
return createCommerceRouter({
|
|
1557
|
+
configuration,
|
|
1558
|
+
checkoutReturnUrls,
|
|
1559
|
+
features: {
|
|
1560
|
+
catalog: false,
|
|
1561
|
+
payments: true
|
|
1562
|
+
}
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
//#endregion
|
|
1567
|
+
export { createCheckoutSession, createCommerceCatalogRouter, createCommerceRouter, createGoDaddyPaymentsRouter, createRuntimeCommerceConfiguration, getOrderStatus, parseCommerceCheckoutConfiguration, readCommerceConfig };
|