PageSourceSearch

https://www.serta.com/scripts/commerce/api.js

js serta.com collected 2026-10-03 20:01:32 UTC 25,139 bytes, 716 lines download raw bytes

1import { handleFreeGift, removeFreeGifts, getFreeGiftLines } from './free-gift-handler.js';
2import {
3  PRODUCTS__LIST_QUERY,
4  PRODUCT_BY_ID_QUERY,
5  CREATE_CART_QUERY,
6  ADD_TO_CART_QUERY,
7  MAIN_MENU_QUERY,
8  PRODUCT_PRICES_QUERY,
9  PRODUCT_VARIANTS_QUERY,
10  GET_SORTED_PRODUCTS_BY_COLLECTION_QUERY,
11  UPDATE_CART_ITEM_QUANTITY_QUERY,
12  REMOVE_CART_ITEM_QUERY,
13  GET_CART_QUERY,
14  CART_ATTRIBUTES_UPDATE_QUERY,
15  buildDynamicProductPricesQuery,
16  buildDynamicProductSwatchesQuery,
17  GET_PRODUCT_DETAILS_BY_ID_QUERY,
18  GET_PRODUCT_BY_HANDLE_QUERY,
19  GET_PRODUCT_RECOMMENDATIONS_QUERY,
20  CUSTOMER_DETAILS_QUERY,
21  GET_MULTIPLE_PRODUCTS_DETAILS_QUERY,
22  PRODUCT_COLOR_SWATCHES_QUERY,
23  GET_BUNDLE_PRODUCTS_QUERY,
24  GET_COLLECTION_PRODUCTS_QUERY,
25} from './graphql.js';
26
27import executeEdgeGraphQL from './api-edge-client.js';
28import getInContextVariables from '../utils/graphql-context.js';
29import { normalizeQuickShipEligible, normalizeEligibleZipCodes } from '../utils/quickship.js';
30
31// Initialize logger for commerce API module
32// Use global SertaLogger
33const logger = window.SertaLogger();
34
35export async function getCustomerData(locale = 'en', country = 'US') {
36  const variables = {
37    ...getInContextVariables(locale, country),
38  };
39  const { data, error } = await executeEdgeGraphQL(CUSTOMER_DETAILS_QUERY, variables, 'POST', locale, 'customer');
40
41  return { customer: data?.customer || null, error };
42}
43
44/**
45 * Gets list of products from Edge Compute
46 */
47export async function getProductsList(locale = 'en', country = 'US') {
48  const variables = getInContextVariables(locale, country);
49  const { data, error } = await executeEdgeGraphQL(PRODUCTS__LIST_QUERY, variables, 'GET', locale, 'product');
50  return { products: data?.products?.edges?.map((edge) => edge.node) || [], error };
51}
52
53/**
54 * Gets product by ID from Edge Compute
55 */
56export async function getProductById(productId, locale = 'en', country = 'US') {
57  const variables = {
58    ...getInContextVariables(locale, country),
59    id: productId,
60  };
61  const { data, error } = await executeEdgeGraphQL(PRODUCT_BY_ID_QUERY, variables, 'POST', locale, 'product');
62  return { product: data?.product || null, error };
63}
64
65/**
66 * Gets full product details by handle from Storefront API via Edge API
67 * @param {string} handle - Shopify product handle (URL slug)
68 * @param {string} locale - Locale code
69 * @param {string} country - Country code
70 * @returns {Promise<{product: Object|null, shop: Object|null, error: any}>}
71 */
72export async function getProductByHandle(handle, locale = 'en', country = 'US') {
73  const variables = {
74    ...getInContextVariables(locale, country),
75    handle,
76  };
77  const { data, error } = await executeEdgeGraphQL(
78    GET_PRODUCT_BY_HANDLE_QUERY,
79    variables,
80    'POST',
81    locale,
82    'product',
83  );
84  if (error) {
85    logger.error('CommerceAPI', `Error fetching product by handle: ${handle}`, error);
86  }
87  return { product: data?.product || null, shop: data?.shop || null, error };
88}
89
90export async function getProductRecommendations(productId, locale = 'en', country = 'US') {
91  if (!productId) return { products: [], error: null };
92  const variables = {
93    ...getInContextVariables(locale, country),
94    productId,
95  };
96  const { data, error } = await executeEdgeGraphQL(
97    GET_PRODUCT_RECOMMENDATIONS_QUERY,
98    variables,
99    'POST',
100    locale,
101    'product',
102  );
103  return { products: data?.productRecommendations || [], error };
104}
105
106/**
107 * Fetches a single product's variants by its handle
108 * @param {string} productId - Shopify Product ID (GraphQL format)
109 * @returns {Promise<Object|null>} Product object or null
110 */
111export async function getProductVariantsById(productId, locale = 'en', country = 'US') {
112  const variables = {
113    ...getInContextVariables(locale, country),
114    id: productId,
115  };
116  const { data, error } = await executeEdgeGraphQL(PRODUCT_VARIANTS_QUERY, variables, 'POST', locale, 'product');
117
118  return { product: data?.product || null, error };
119}
120
121/**
122 * Fetches a single product's variant prices by its ID
123 * @param {string} productId - Shopify Product ID (GraphQL format)
124 * @returns {Promise<Object|null>} Product object with price data or null
125 */
126export async function getProductPricesById(productId, locale = 'en', country = 'US') {
127  const variables = {
128    ...getInContextVariables(locale, country),
129    id: productId,
130  };
131  const { data, error } = await executeEdgeGraphQL(PRODUCT_PRICES_QUERY, variables, 'POST', locale, 'product');
132  return { product: data?.product || null, error };
133}
134
135/**
136 * Creates a new cart using Edge Compute
137 */
138export async function createCart(locale = 'en', country = 'US') {
139  const variables = getInContextVariables(locale, country);
140  const { data, error } = await executeEdgeGraphQL(CREATE_CART_QUERY, variables, 'POST', locale, 'cart');
141  return { cart: data?.cartCreate?.cart?.id || null, error };
142}
143
144/**
145 * Adds item(s) to cart using Edge Compute
146 * Internal function - use addToCartWithFreeGifts for user-facing operations
147 */
148export async function addToCart(cartId, lines, locale = 'en', country = 'US') {
149  const variables = { cartId, lines, ...getInContextVariables(locale, country) };
150  const { data, error } = await executeEdgeGraphQL(ADD_TO_CART_QUERY, variables, 'POST', locale, 'cart');
151  return { cart: data?.cartLinesAdd?.cart || null, error };
152}
153
154/**
155 * Adds item(s) to cart and automatically handles free gift addition
156 * This is the main function to use for adding products to cart
157 */
158export async function addToCartWithFreeGifts(cartId, lines, locale = 'en', country = 'US') {
159  // First add the requested items to cart
160  const result = await addToCart(cartId, lines, locale, country);
161
162  if (result.error || !result.cart) {
163    return result;
164  }
165
166  // Build a map of merchandiseId → delta quantity from the original input lines.
167  //
168  // FIX (Issue #1 — "Add to Cart twice only adds one free gift"):
169  // Previously this used `new Set()` to track only IDs, then read `edge.node.quantity`
170  // (the *total* cart quantity after the add, e.g. 2) as the quantity to pass to
171  // handleFreeGift. That was wrong: Shopify's cartLinesAdd accumulates quantities on
172  // existing lines, so after a second "Add to Cart" click the cart line already shows
173  // qty 2, but the correct value to propagate is the *delta* (1 — what the user just
174  // requested). Using the total caused handleFreeGift to receive qty 2 on the second
175  // call and then skip the free gift entirely because it already existed in cart.
176  // Using the delta here means: first click adds free gift at qty 1; second click
177  // calls cartLinesAdd again with qty 1, which Shopify accumulates to qty 2. ✅
178  const addedMerchandiseDeltaQuantities = new Map(
179    (lines || [])
180      .filter((line) => line.merchandiseId)
181      .map((line) => [line.merchandiseId, line.quantity || 1]),
182  );
183  const addedLines = (result.cart.lines?.edges || []).filter((edge) => {
184    const merchandiseId = edge.node?.merchandise?.id;
185    if (!merchandiseId || !addedMerchandiseDeltaQuantities.has(merchandiseId)) {
186      return false;
187    }
188    const attributes = edge.node?.attributes || [];
189    const isFreeGift = attributes.some((attr) => (
190      attr.key === '_free_gift' && attr.value === 'true'
191    ));
192    return !isFreeGift;
193  });
194
195  // Process each added line for free gifts sequentially using promise reduction
196  const updatedCart = await addedLines.reduce(
197    async (cartPromise, edge) => {
198      const cartAcc = await cartPromise;
199      const merchandiseId = edge.node?.merchandise?.id;
200      // Use the delta quantity from the input lines (not edge.node.quantity which is
201      // the post-add total). See comment above for the full explanation.
202      const quantity = addedMerchandiseDeltaQuantities.get(merchandiseId) || 1;
203      const maybeUpdatedCart = await handleFreeGift(
204        cartId,
205        edge.node,
206        quantity,
207        addToCart,
208        cartAcc,
209        locale,
210        country,
211      );
212      return maybeUpdatedCart || cartAcc;
213    },
214    Promise.resolve(result.cart),
215  );
216
217  return { cart: updatedCart, error: null };
218}
219
220/**
221 * Fetches cart data by cart ID
222 * @param {string} cartId - The Shopify Cart ID
223 * @returns {Promise<Object|null>} Cart object or null
224 */
225export async function getCartById(cartId, locale = 'en', country = 'US') {
226  const variables = { cartId, ...getInContextVariables(locale, country) };
227  const { data, error } = await executeEdgeGraphQL(
228    GET_CART_QUERY,
229    variables,
230    'POST',
231    locale,
232    'cart',
233  );
234  return { cart: data?.cart || null, error };
235}
236
237/**
238 * Retrieves main navigation menu using Edge Compute
239 */
240export async function getMainMenu(locale = 'en', country = 'US') {
241  const variables = getInContextVariables(locale, country);
242  const { data, error } = await executeEdgeGraphQL(MAIN_MENU_QUERY, variables, 'POST', locale, 'menu');
243  return { menu: data?.menu || null, error };
244}
245
246/**
247 * Retrieves main navigation menu using Edge Compute (GET method)
248 * EC handles the query internally; only variables are sent as top-level params.
249 */
250export async function getMainMenuCached(locale = 'en', country = 'US') {
251  const variables = getInContextVariables(locale, country);
252  const { data, error } = await executeEdgeGraphQL(
253    '', // no query string sent for GET
254    variables, // becomes ?country=US&language=EN
255    'GET',
256    locale,
257    'getmenu', // EC GET alias route
258  );
259  return { menu: data?.menu || null, error };
260}
261
262/**
263 * Fetches prices for multiple products in a single query
264 * @param {Array} products - Array of product objects with handle and gid properties
265 * @returns {Promise<Object|null>} Object with product prices keyed by product GID
266 */
267export async function getProductPrices(products, locale = 'en', country = 'US') {
268  if (!products || products.length === 0) {
269    return {};
270  }
271  const variables = getInContextVariables(locale, country);
272  const query = buildDynamicProductPricesQuery(products);
273  const json = await executeEdgeGraphQL(query, variables, 'POST', locale, 'product');
274  if (!json?.data) {
275    return {};
276  }
277  return json.data;
278}
279
280/**
281 * Fetches color swatches for multiple products in a single query
282 * @param {Array} products - Array of product objects with handle and gid properties
283 * @returns {Promise<Object|null>} Object with product color swatches keyed by product GID
284 */
285export async function getProductSwatches(products, locale = 'en', country = 'US') {
286  if (!products || products.length === 0) {
287    return {};
288  }
289  const variables = getInContextVariables(locale, country);
290  const query = buildDynamicProductSwatchesQuery(products);
291  const json = await executeEdgeGraphQL(query, variables, 'POST', locale, 'product');
292
293  if (!json?.data) {
294    return {};
295  }
296  return json.data;
297}
298
299/**
300 * Fetches color swatches for a single product by handle
301 * @param {string} handle - Product handle
302 * @param {string} locale - Locale for the request
303 * @param {string} country - Country for the request
304 * @returns {Promise<Object|null>} Product object with color swatch data or null
305 */
306export async function getProductColorSwatchesByHandle(handle, locale = 'en', country = 'US') {
307  if (!handle) {
308    logger.warn('CommerceAPI', 'getProductColorSwatchesByHandle called without handle');
309    return { product: null, error: 'No handle provided' };
310  }
311
312  const variables = {
313    ...getInContextVariables(locale, country),
314    handle,
315  };
316
317  const result = await executeEdgeGraphQL(PRODUCT_COLOR_SWATCHES_QUERY, variables, 'POST', locale, 'product');
318
319  return { product: result?.data?.product || null, error: result?.error || null };
320}
321
322/**
323 * Fetches sorted products from a specific collection
324 * @param {string} handle - Collection handle
325 * @param {string} cursor - Pagination cursor
326 * @param {number} pageSize - Number of products per page
327 * @param {string} sortKey - Sort key (e.g., TITLE, PRICE, BEST_SELLING)
328 * @param {boolean} reverse - Whether to reverse the sort order
329 * @returns {Promise<Object|null>} Collection with sorted products
330 */
331export async function getSortedProductsByCollection(
332  handle,
333  sortKey,
334  reverse = false,
335  cursor = null,
336  pageSize = 12,
337  locale = 'en',
338  country = 'US',
339) {
340  const variables = {
341    handle,
342    cursor,
343    pageSize,
344    sortKey,
345    reverse,
346    ...getInContextVariables(locale, country),
347  };
348  const json = await executeEdgeGraphQL(GET_SORTED_PRODUCTS_BY_COLLECTION_QUERY, variables, 'POST', locale, 'menu');
349  return json?.data?.collection || null;
350}
351
352/**
353 * Fetches products from a collection with full card data (title, handle, images, variants)
354 * Used for bundles PLP where product data is not pre-rendered in the DOM.
355 * @param {string} handle - Collection handle
356 * @param {number} pageSize - Number of products to fetch
357 * @returns {Promise<Object|null>} Collection with products
358 */
359export async function getCollectionProducts(
360  handle,
361  pageSize = 100,
362  locale = 'en',
363  country = 'US',
364) {
365  const variables = {
366    handle,
367    pageSize,
368    ...getInContextVariables(locale, country),
369  };
370  const json = await executeEdgeGraphQL(GET_COLLECTION_PRODUCTS_QUERY, variables, 'POST', locale, 'menu');
371  return json?.data?.collection || null;
372}
373
374/**
375 * Updates the quantity of an item in the cart
376 * Internal function - use updateCartItemQuantityWithFreeGifts for user-facing operations
377 * @param {string} cartId - The Shopify Cart ID
378 * @param {string} lineId - The ID of the line item to update
379 * @param {number} quantity - The new quantity
380 * @returns {Promise<Object|null>} Updated cart object or null
381 */
382export async function updateCartItemQuantity(cartId, lineId, quantity, locale = 'en', country = 'US') {
383  const lines = [{
384    id: lineId,
385    quantity,
386  }];
387  const variables = { cartId, lines, ...getInContextVariables(locale, country) };
388  const { data, error } = await executeEdgeGraphQL(UPDATE_CART_ITEM_QUANTITY_QUERY, variables, 'POST', locale, 'cart');
389  return { cart: data?.cartLinesUpdate?.cart || null, error };
390}
391
392/**
393 * Writes cart-level attributes to a Shopify cart.
394 * Used for GA4 identity + campaign attribution handoff to Shopify checkout.
395 * @param {string} cartId - The Shopify Cart ID
396 * @param {Array<{key: string, value: string}>} attributes - Attribute inputs
397 * @returns {Promise<{cart: Object|null, error: any}>}
398 */
399export async function updateCartAttributes(cartId, attributes, locale = 'en', country = 'US') {
400  const variables = { cartId, attributes, ...getInContextVariables(locale, country) };
401  const { data, error } = await executeEdgeGraphQL(CART_ATTRIBUTES_UPDATE_QUERY, variables, 'POST', locale, 'cart');
402  return { cart: data?.cartAttributesUpdate?.cart || null, error };
403}
404
405/**
406 * Updates quantity and syncs associated free gift quantities
407 * This is the main function to use for updating product quantities in cart
408 */
409export async function updateCartItemQuantityWithFreeGifts(
410  cartId,
411  lineId,
412  quantity,
413  locale = 'en',
414  country = 'US',
415) {
416  // Get current cart to find product ID and free gift lines
417  const { cart: currentCart } = await getCartById(cartId, locale, country);
418
419  if (!currentCart) {
420    return { cart: null, error: 'Cart not found' };
421  }
422
423  // Find the line being updated to get its product ID
424  const lineToUpdate = currentCart.lines?.edges.find((edge) => edge.node.id === lineId);
425  const sourceProductId = lineToUpdate?.node?.merchandise?.product?.id;
426  const sourceVariantId = lineToUpdate?.node?.merchandise?.id;
427
428  // Update the requested item quantity
429  const result = await updateCartItemQuantity(cartId, lineId, quantity, locale, country);
430
431  if (result.error || !result.cart || !sourceProductId) {
432    return result;
433  }
434
435  // Find associated free gift lines
436  const freeGiftLines = getFreeGiftLines(result.cart, sourceProductId, sourceVariantId);
437
438  if (freeGiftLines.length > 0) {
439    logger.info('UpdateQuantity', `Updating ${freeGiftLines.length} free gift(s) to quantity ${quantity}`);
440    // Update free gift quantities to match the source product quantity
441    const linesToUpdate = freeGiftLines.map((freeGift) => ({
442      id: freeGift.id,
443      quantity,
444    }));
445
446    const variables = {
447      cartId,
448      lines: linesToUpdate,
449      ...getInContextVariables(locale, country),
450    };
451    const { data, error } = await executeEdgeGraphQL(
452      UPDATE_CART_ITEM_QUANTITY_QUERY,
453      variables,
454      'POST',
455      locale,
456      'cart',
457    );
458    return { cart: data?.cartLinesUpdate?.cart || null, error };
459  }
460
461  return result;
462}
463/**
464 * Removes an item from the cart
465 * Internal function - use removeCartItemWithFreeGifts for user-facing operations
466 * @param {string} cartId - The Shopify Cart ID
467 * @param {string} lineId - The ID of the line item to remove
468 * @returns {Promise<Object|null>} Updated cart object or null
469 */
470export async function removeCartItem(cartId, lineId, locale = 'en', country = 'US') {
471  const lineIds = [lineId];
472  const variables = { cartId, lineIds, ...getInContextVariables(locale, country) };
473  const { data, error } = await executeEdgeGraphQL(REMOVE_CART_ITEM_QUERY, variables, 'POST', locale, 'cart');
474  return { cart: data?.cartLinesRemove?.cart || null, error };
475}
476
477/**
478 * Removes an item from cart and automatically removes associated free gifts
479 * This is the main function to use for removing products from cart
480 */
481export async function removeCartItemWithFreeGifts(cartId, lineId, locale = 'en', country = 'US') {
482  // First get the cart to find the product ID being removed
483  const { cart: currentCart } = await getCartById(cartId, locale, country);
484
485  if (!currentCart) {
486    return { cart: null, error: 'Cart not found' };
487  }
488
489  // Find the line being removed to get its product ID
490  const lineToRemove = currentCart.lines?.edges.find((edge) => edge.node.id === lineId);
491  const sourceProductId = lineToRemove?.node?.merchandise?.product?.id;
492  const sourceVariantId = lineToRemove?.node?.merchandise?.id;
493
494  logger.info(
495    'RemoveCartItem',
496    `Removing item with source product ID: ${sourceProductId}`,
497  );
498
499  // Remove the requested item
500  const result = await removeCartItem(cartId, lineId, locale, country);
501
502  if (result.error || !result.cart || !sourceProductId) {
503    return result;
504  }
505
506  // Find and remove associated free gifts
507  const freeGiftLineIds = await removeFreeGifts(
508    cartId,
509    sourceProductId,
510    sourceVariantId,
511    getCartById,
512    locale,
513    country,
514  );
515
516  if (freeGiftLineIds.length > 0) {
517    logger.info(
518      'RemoveCartItem',
519      `Found ${freeGiftLineIds.length} free gift(s) to remove`,
520    );
521    // Remove all free gifts in one call
522    const variables = {
523      cartId,
524      lineIds: freeGiftLineIds,
525      ...getInContextVariables(locale, country),
526    };
527    const { data, error } = await executeEdgeGraphQL(
528      REMOVE_CART_ITEM_QUERY,
529      variables,
530      'POST',
531      locale,
532      'cart',
533    );
534    return { cart: data?.cartLinesRemove?.cart || null, error };
535  }
536
537  return result;
538}
539
540export async function getProductDetails(productId, locale = 'en', country = 'US') {
541  const variables = {
542    ...getInContextVariables(locale, country),
543    id: productId,
544  };
545  const { data, error } = await executeEdgeGraphQL(GET_PRODUCT_DETAILS_BY_ID_QUERY, variables, 'POST', locale, 'product');
546  return { product: data?.product || null, error };
547}
548
549/**
550 * Fetches multiple products by their IDs in a bulk operation
551 * @param {Array<string>} productIds - Array of Shopify Product IDs (GraphQL format)
552 * @param {string} locale - Locale for the request
553 * @param {string} country - Country for the request
554 * @returns {Promise<Object>} Object containing products array and error if any
555 */
556export async function getMultipleProductDetails(productIds, locale = 'en', country = 'US') {
557  if (!productIds || productIds.length === 0) {
558    return { products: [], error: null };
559  }
560
561  try {
562    const variables = {
563      ...getInContextVariables(locale, country),
564      ids: productIds, // Use productIds directly (they're already in GID format)
565    };
566
567    const { data, error } = await executeEdgeGraphQL(
568      GET_MULTIPLE_PRODUCTS_DETAILS_QUERY,
569      variables,
570      'POST',
571      locale,
572      'product',
573    );
574
575    if (error) {
576      logger.error('CommerceAPI', 'Error fetching multiple product details', error);
577      return { products: [], error };
578    }
579
580    // Map the nodes data to include original productId for reference
581    const products = (data?.nodes || [])
582      .filter((node) => node && node.id) // Filter out null nodes
583      .map((product) => ({
584        productId: product.id, // Keep the GID format for consistency
585        product,
586      }));
587
588    return {
589      products,
590      error: null,
591    };
592  } catch (error) {
593    logger.error('CommerceAPI', 'Error fetching multiple product details', error);
594    return { products: [], error };
595  }
596}
597
598/**
599 * Gets bundle products from Storefront API via Edge API
600 * @param {number} first - Number of products to fetch
601 * @param {string} locale - Locale code
602 * @param {string} country - Country code
603 * @returns {Promise<{products: Array, error: any}>}
604 */
605export async function getBundleProducts(first = 250, locale = 'en', country = 'US') {
606  const variables = {
607    ...getInContextVariables(locale, country),
608    first,
609  };
610  const { data, error } = await executeEdgeGraphQL(
611    GET_BUNDLE_PRODUCTS_QUERY,
612    variables,
613    'POST',
614    locale,
615    'product',
616  );
617  if (error) {
618    logger.error('CommerceAPI', 'Error fetching bundle products', error);
619    return { products: [], error };
620  }
621  // Filter to only products that actually have requiresComponents on their variants
622  const allProducts = data?.products?.edges?.map((edge) => edge.node) || [];
623  const bundleProducts = allProducts.filter((product) => {
624    const variants = product.variants?.edges || [];
625    return variants.some((v) => v.node.requiresComponents === true);
626  });
627  return { products: bundleProducts, error: null };
628}
629
630export async function getQuickShipEligibilityForProductIds(productIds = [], locale = 'en', country = 'US') {
631  const uniqueProductIds = [...new Set(productIds.filter(Boolean))];
632
633  if (!uniqueProductIds.length) {
634    return {};
635  }
636
637  const entries = await Promise.all(uniqueProductIds.map(async (productId) => {
638    try {
639      const { product } = await getProductVariantsById(productId, locale, country);
640
641      if (!product) {
642        logger.warn('CommerceAPI', `No product returned for: ${productId}`);
643        return [productId, {
644          productQuickShipEligible: false,
645          variantEligibilityById: {},
646          quickShipEligibleSizes: [],
647          variantEligibleZipCodesById: {},
648          quickShipEligibleZipCodes: [],
649        }];
650      }
651
652      // Only use regular variants - adjacentVariants don't have quickship metafield data
653      const variants = product?.variants?.edges?.map(({ node }) => node) || [];
654
655      if (variants.length === 0) {
656        logger.debug('CommerceAPI', `No variants found for product: ${productId}`);
657      }
658
659      const variantEligibilityById = variants.reduce((acc, variant) => {
660        // Handle metafield object structure { value: "true" } or direct boolean
661        const quickShipValue = variant?.quickShipEligible?.value
662          ?? variant?.quickShipEligible
663          ?? variant?.quickshipeligible;
664        acc[variant.id] = normalizeQuickShipEligible(quickShipValue);
665        return acc;
666      }, {});
667
668      const quickShipEligibleSizes = [...new Set(
669        variants
670          .filter((variant) => variantEligibilityById[variant.id] === true)
671          .map((variant) => (
672            variant?.selectedOptions?.find((option) => option?.name?.toLowerCase() === 'size')?.value
673          ))
674          .filter(Boolean),
675      )];
676
677      const variantEligibleZipCodesById = variants.reduce((acc, variant) => {
678        acc[variant.id] = variant?.eligibleZipCodes ?? null;
679        return acc;
680      }, {});
681
682      const quickShipEligibleZipCodes = [...new Set(
683        variants
684          .filter((variant) => variantEligibilityById[variant.id] === true)
685          .flatMap((variant) => normalizeEligibleZipCodes(variant?.eligibleZipCodes)),
686      )];
687
688      const productQuickShipEligible = variants.some(
689        (variant) => variantEligibilityById[variant.id] === true,
690      );
691
692      logger.debug('CommerceAPI', `Product ${productId} - eligible: ${productQuickShipEligible}, variants checked: ${variants.length}`);
693
694      return [productId, {
695        productQuickShipEligible,
696        variantEligibilityById,
697        quickShipEligibleSizes,
698        variantEligibleZipCodesById,
699        quickShipEligibleZipCodes,
700      }];
701    } catch (error) {
702      logger.error('CommerceAPI', `Error fetching quick ship eligibility for product: ${productId}`, error);
703      return [productId, {
704        productQuickShipEligible: false,
705        variantEligibilityById: {},
706        quickShipEligibleSizes: [],
707        variantEligibleZipCodesById: {},
708        quickShipEligibleZipCodes: [],
709      }];
710    }
711  }));
712
713  const result = Object.fromEntries(entries);
714  logger.debug('CommerceAPI', `Quick ship eligibility checked for ${uniqueProductIds.length} products`);
715  return result;
716}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.