diff --git a/.env.example b/.env.example index 241e12f..cb3110e 100644 --- a/.env.example +++ b/.env.example @@ -24,4 +24,5 @@ PRICE_ADJUSTMENT_PERCENTAGE=10 # SCHEDULED_EXECUTION_TIME=2024-12-25T10:30:00Z # UTC timezone # SCHEDULED_EXECUTION_TIME=2024-12-25T10:30:00-05:00 # EST timezone # Leave commented out or remove to execute immediately (default behavior) -# SCHEDULED_EXECUTION_TIME=2024-12-25T10:30:00 \ No newline at end of file +# Note: It uses military time (24-hour format), and wants months and days to be two digits (e.g., 08 for August, 09 for September) +# SCHEDULED_EXECUTION_TIME=2024-12-25T10:30:00 diff --git a/.gitignore b/.gitignore index a70ab7c..eb8b139 100644 --- a/.gitignore +++ b/.gitignore @@ -151,4 +151,5 @@ coverage/ # Build artifacts build/ -dist/ \ No newline at end of file +dist/ +.env.test diff --git a/debug-tags.js b/debug-tags.js index 17a188c..0da8900 100644 --- a/debug-tags.js +++ b/debug-tags.js @@ -22,7 +22,7 @@ async function debugTags() { const productService = new ProductService(); // Fetch products and analyze tags - const products = await productService.debugFetchAllProductTags(100); + const products = await productService.debugFetchAllProductTags(); // Check if the target tag exists (case-insensitive search) const targetTag = config.targetTag.toLowerCase(); @@ -86,4 +86,4 @@ async function debugTags() { } // Run the debug function -debugTags(); +debugTags(); \ No newline at end of file diff --git a/src/services/product.js b/src/services/product.js index 1fb6aeb..eaaae33 100644 --- a/src/services/product.js +++ b/src/services/product.js @@ -31,16 +31,6 @@ class ProductService { id title tags - variants(first: 100) { - edges { - node { - id - price - compareAtPrice - title - } - } - } } cursor } @@ -77,6 +67,34 @@ class ProductService { `; } + /** + * GraphQL query to fetch variants for a product with pagination + */ + getVariantsByProductIdQuery() { + return ` + query getVariantsByProductId($productId: ID!, $first: Int!, $after: String) { + node(id: $productId) { + ... on Product { + variants(first: $first, after: $after) { + edges { + node { + id + price + compareAtPrice + title + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + `; + } + /** * GraphQL mutation to update product variant price and Compare At price */ @@ -98,6 +116,47 @@ class ProductService { `; } + /** + * Fetch all variants for a given product using pagination. + * @param {string} productId - The ID of the product to fetch variants for. + * @returns {Promise} A list of all variants for the product. + */ + async fetchAllVariants(productId) { + const allVariants = []; + let hasNextPage = true; + let cursor = null; + + while (hasNextPage) { + const variables = { + productId: productId, + first: 250, + after: cursor, + }; + + const response = await this.shopifyService.executeWithRetry( + () => + this.shopifyService.executeQuery( + this.getVariantsByProductIdQuery(), + variables + ), + this.logger + ); + + const productNode = response.node; + if (productNode && productNode.variants) { + const variants = productNode.variants.edges.map((edge) => edge.node); + allVariants.push(...variants); + + hasNextPage = productNode.variants.pageInfo.hasNextPage; + cursor = productNode.variants.pageInfo.endCursor; + } else { + hasNextPage = false; + } + } + + return allVariants; + } + /** * Fetch all products with the specified tag using cursor-based pagination * @param {string} tag - Tag to filter products by @@ -141,23 +200,14 @@ class ProductService { const { edges, pageInfo } = response.products; // Process products from this page - const pageProducts = edges.map((edge) => ({ - id: edge.node.id, - title: edge.node.title, - tags: edge.node.tags, - variants: edge.node.variants.edges.map((variantEdge) => ({ - id: variantEdge.node.id, - price: parseFloat(variantEdge.node.price), - compareAtPrice: variantEdge.node.compareAtPrice - ? parseFloat(variantEdge.node.compareAtPrice) - : null, - title: variantEdge.node.title, - })), - })); + for (const edge of edges) { + const product = edge.node; + product.variants = await this.fetchAllVariants(product.id); + allProducts.push(product); + } - allProducts.push(...pageProducts); await this.logger.info( - `Found ${pageProducts.length} products on page ${pageCount}` + `Found ${edges.length} products on page ${pageCount}` ); // Update pagination info @@ -214,15 +264,16 @@ class ProductService { // Check if variants have valid price data const validVariants = []; for (const variant of product.variants) { - if (typeof variant.price !== "number" || isNaN(variant.price)) { + const price = parseFloat(variant.price); + if (typeof price !== "number" || isNaN(price)) { await this.logger.warning( `Skipping variant "${variant.title}" in product "${product.title}" - invalid price: ${variant.price}` ); continue; } - if (variant.price < 0) { + if (price < 0) { await this.logger.warning( - `Skipping variant "${variant.title}" in product "${product.title}" - negative price: ${variant.price}` + `Skipping variant "${variant.title}" in product "${product.title}" - negative price: ${price}` ); continue; } @@ -250,7 +301,7 @@ class ProductService { ); } - await this.logger.info( + await this.logger.info( `Validated ${validProducts.length} products for price updates` ); return validProducts; @@ -865,7 +916,7 @@ class ProductService { try { // Prepare price update with Compare At price const priceUpdate = preparePriceUpdate( - variant.price, + parseFloat(variant.price), priceAdjustmentPercentage ); @@ -925,7 +976,7 @@ class ProductService { * @param {number} limit - Maximum number of products to fetch for debugging * @returns {Promise} Array of products with their tags */ - async debugFetchAllProductTags(limit = 50) { + async debugFetchAllProductTags(limit = Infinity) { await this.logger.info( `Fetching up to ${limit} products to analyze tags...` ); @@ -1165,4 +1216,4 @@ class ProductService { } } -module.exports = ProductService; +module.exports = ProductService; \ No newline at end of file