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.