#!/usr/bin/env node /** * Shopify Price Updater - Main Application Entry Point * * This script connects to Shopify's GraphQL API to update product prices * based on specific tag criteria and configurable percentage adjustments. */ const { getConfig } = require("./config/environment"); const ProductService = require("./services/product"); const ScheduleService = require("./services/schedule"); const Logger = require("./utils/logger"); /** * Main application class that orchestrates the price update workflow */ class ShopifyPriceUpdater { constructor() { this.logger = new Logger(); this.productService = new ProductService(); this.scheduleService = new ScheduleService(this.logger); this.config = null; this.startTime = null; } /** * Initialize the application and load configuration * @returns {Promise} True if initialization successful */ async initialize() { try { // Load and validate configuration this.config = getConfig(); // Log operation start with configuration based on operation mode (Requirements 3.1, 8.4, 9.3) if (this.config.operationMode === "rollback") { await this.logger.logRollbackStart(this.config); } else { await this.logger.logOperationStart(this.config); } return true; } catch (error) { await this.logger.error(`Initialization failed: ${error.message}`); return false; } } /** * Test connection to Shopify API * @returns {Promise} True if connection successful */ async testConnection() { try { await this.logger.info("Testing connection to Shopify API..."); const isConnected = await this.productService.shopifyService.testConnection(); if (!isConnected) { await this.logger.error( "Failed to connect to Shopify API. Please check your credentials." ); return false; } await this.logger.info("Successfully connected to Shopify API"); return true; } catch (error) { await this.logger.error(`Connection test failed: ${error.message}`); return false; } } /** * Fetch products by tag and validate them * @returns {Promise} Array of valid products or null if failed */ async fetchAndValidateProducts() { try { // Fetch products by tag await this.logger.info( `Fetching products with tag: ${this.config.targetTag}` ); const products = await this.productService.fetchProductsByTag( this.config.targetTag ); // Log product count (Requirement 3.2) await this.logger.logProductCount(products.length); if (products.length === 0) { await this.logger.info( "No products found with the specified tag. Operation completed." ); return []; } // Validate products for price updates const validProducts = await this.productService.validateProducts( products ); // Display summary statistics const summary = this.productService.getProductSummary(validProducts); await this.logger.info(`Product Summary:`); await this.logger.info(` - Total Products: ${summary.totalProducts}`); await this.logger.info(` - Total Variants: ${summary.totalVariants}`); await this.logger.info( ` - Price Range: $${summary.priceRange.min} - $${summary.priceRange.max}` ); return validProducts; } catch (error) { await this.logger.error(`Failed to fetch products: ${error.message}`); return null; } } /** * Update prices for all products * @param {Array} products - Array of products to update * @returns {Promise} Update results or null if failed */ async updatePrices(products) { try { if (products.length === 0) { return { totalProducts: 0, totalVariants: 0, successfulUpdates: 0, failedUpdates: 0, errors: [], }; } await this.logger.info( `Starting price updates with ${this.config.priceAdjustmentPercentage}% adjustment` ); // Mark operation as in progress to prevent cancellation during updates if (this.setOperationInProgress) { this.setOperationInProgress(true); } try { // Update product prices const results = await this.productService.updateProductPrices( products, this.config.priceAdjustmentPercentage ); return results; } finally { // Mark operation as complete if (this.setOperationInProgress) { this.setOperationInProgress(false); } } } catch (error) { // Ensure operation state is cleared on error if (this.setOperationInProgress) { this.setOperationInProgress(false); } await this.logger.error(`Price update failed: ${error.message}`); return null; } } /** * Display final summary and determine exit status * @param {Object} results - Update results * @returns {number} Exit status code */ async displaySummaryAndGetExitCode(results) { // Prepare comprehensive summary for logging (Requirement 3.4) const summary = { totalProducts: results.totalProducts, totalVariants: results.totalVariants, successfulUpdates: results.successfulUpdates, failedUpdates: results.failedUpdates, startTime: this.startTime, errors: results.errors || [], }; // Log completion summary await this.logger.logCompletionSummary(summary); // Perform error analysis if there were failures (Requirement 3.5) if (results.errors && results.errors.length > 0) { await this.logger.logErrorAnalysis(results.errors, summary); } // Determine exit status with enhanced logic (Requirement 4.5) const successRate = summary.totalVariants > 0 ? (summary.successfulUpdates / summary.totalVariants) * 100 : 0; if (results.failedUpdates === 0) { await this.logger.info("šŸŽ‰ All operations completed successfully!"); return 0; // Success } else if (results.successfulUpdates > 0) { if (successRate >= 90) { await this.logger.info( `āœ… Operation completed with high success rate (${successRate.toFixed( 1 )}%). Minor issues encountered.` ); return 0; // High success rate, treat as success } else if (successRate >= 50) { await this.logger.warning( `āš ļø Operation completed with moderate success rate (${successRate.toFixed( 1 )}%). Review errors above.` ); return 1; // Partial failure } else { await this.logger.error( `āŒ Operation completed with low success rate (${successRate.toFixed( 1 )}%). Significant issues detected.` ); return 2; // Poor success rate } } else { await this.logger.error( "āŒ All update operations failed. Please check your configuration and try again." ); return 2; // Complete failure } } /** * Fetch products by tag and validate them for rollback operations * @returns {Promise} Array of rollback-eligible products or null if failed */ async fetchAndValidateProductsForRollback() { try { // Fetch products by tag await this.logger.info( `Fetching products with tag: ${this.config.targetTag}` ); const products = await this.productService.fetchProductsByTag( this.config.targetTag ); // Log product count (Requirement 3.2) await this.logger.logProductCount(products.length); if (products.length === 0) { await this.logger.info( "No products found with the specified tag. Operation completed." ); return []; } // Validate products for rollback operations const eligibleProducts = await this.productService.validateProductsForRollback(products); // Display summary statistics for rollback const summary = this.productService.getProductSummary(eligibleProducts); await this.logger.info(`Rollback Product Summary:`); await this.logger.info(` - Total Products: ${summary.totalProducts}`); await this.logger.info(` - Total Variants: ${summary.totalVariants}`); await this.logger.info( ` - Price Range: ${summary.priceRange.min} - ${summary.priceRange.max}` ); return eligibleProducts; } catch (error) { await this.logger.error( `Failed to fetch products for rollback: ${error.message}` ); return null; } } /** * Execute rollback operations for all products * @param {Array} products - Array of products to rollback * @returns {Promise} Rollback results or null if failed */ async rollbackPrices(products) { try { if (products.length === 0) { return { totalProducts: 0, totalVariants: 0, eligibleVariants: 0, successfulRollbacks: 0, failedRollbacks: 0, skippedVariants: 0, errors: [], }; } await this.logger.info(`Starting price rollback operations`); // Mark operation as in progress to prevent cancellation during rollback if (this.setOperationInProgress) { this.setOperationInProgress(true); } try { // Execute rollback operations const results = await this.productService.rollbackProductPrices( products ); return results; } finally { // Mark operation as complete if (this.setOperationInProgress) { this.setOperationInProgress(false); } } } catch (error) { // Ensure operation state is cleared on error if (this.setOperationInProgress) { this.setOperationInProgress(false); } await this.logger.error(`Price rollback failed: ${error.message}`); return null; } } /** * Display operation mode header with clear indication of active mode (Requirements 9.3, 8.4) * @returns {Promise} */ async displayOperationModeHeader() { const colors = { reset: "\x1b[0m", bright: "\x1b[1m", blue: "\x1b[34m", green: "\x1b[32m", yellow: "\x1b[33m", }; console.log("\n" + "=".repeat(60)); if (this.config.operationMode === "rollback") { console.log( `${colors.bright}${colors.yellow}šŸ”„ SHOPIFY PRICE ROLLBACK MODE${colors.reset}` ); console.log( `${colors.yellow}Reverting prices from compare-at to main price${colors.reset}` ); } else { console.log( `${colors.bright}${colors.green}šŸ“ˆ SHOPIFY PRICE UPDATE MODE${colors.reset}` ); console.log( `${colors.green}Adjusting prices by ${this.config.priceAdjustmentPercentage}%${colors.reset}` ); } console.log("=".repeat(60) + "\n"); // Log operation mode to progress file as well await this.logger.info( `Operation Mode: ${this.config.operationMode.toUpperCase()}` ); } /** * Display rollback-specific summary and determine exit status * @param {Object} results - Rollback results * @returns {number} Exit status code */ async displayRollbackSummary(results) { // Prepare comprehensive summary for rollback logging (Requirement 3.4, 8.4) const summary = { totalProducts: results.totalProducts, totalVariants: results.totalVariants, eligibleVariants: results.eligibleVariants, successfulRollbacks: results.successfulRollbacks, failedRollbacks: results.failedRollbacks, skippedVariants: results.skippedVariants, startTime: this.startTime, errors: results.errors || [], }; // Log rollback completion summary await this.logger.logRollbackSummary(summary); // Perform error analysis if there were failures (Requirement 3.5) if (results.errors && results.errors.length > 0) { await this.logger.logErrorAnalysis(results.errors, summary); } // Determine exit status with enhanced logic for rollback (Requirement 4.5) const successRate = summary.eligibleVariants > 0 ? (summary.successfulRollbacks / summary.eligibleVariants) * 100 : 0; if (results.failedRollbacks === 0) { await this.logger.info( "šŸŽ‰ All rollback operations completed successfully!" ); return 0; // Success } else if (results.successfulRollbacks > 0) { if (successRate >= 90) { await this.logger.info( `āœ… Rollback completed with high success rate (${successRate.toFixed( 1 )}%). Minor issues encountered.` ); return 0; // High success rate, treat as success } else if (successRate >= 50) { await this.logger.warning( `āš ļø Rollback completed with moderate success rate (${successRate.toFixed( 1 )}%). Review errors above.` ); return 1; // Partial failure } else { await this.logger.error( `āŒ Rollback completed with low success rate (${successRate.toFixed( 1 )}%). Significant issues detected.` ); return 2; // Poor success rate } } else { await this.logger.error( "āŒ All rollback operations failed. Please check your configuration and try again." ); return 2; // Complete failure } } /** * Run the complete application workflow with dual operation mode support * @returns {Promise} Exit status code */ async run() { this.startTime = new Date(); let operationResults = null; try { // Initialize application with enhanced error handling const initialized = await this.safeInitialize(); if (!initialized) { return await this.handleCriticalFailure("Initialization failed", 1); } // Test API connection with enhanced error handling const connected = await this.safeTestConnection(); if (!connected) { return await this.handleCriticalFailure("API connection failed", 1); } // Check for scheduled execution and handle scheduling if configured if (this.config.isScheduled) { const shouldProceed = await this.handleScheduledExecution(); if (!shouldProceed) { return 0; // Operation was cancelled during scheduling } } // Display operation mode indication in console output (Requirements 9.3, 8.4) await this.displayOperationModeHeader(); // Operation mode detection logic - route to appropriate workflow (Requirements 8.1, 9.1, 9.2) if (this.config.operationMode === "rollback") { // Rollback workflow const products = await this.safeFetchAndValidateProductsForRollback(); if (products === null) { return await this.handleCriticalFailure( "Product fetching for rollback failed", 1 ); } operationResults = await this.safeRollbackPrices(products); if (operationResults === null) { return await this.handleCriticalFailure( "Price rollback process failed", 1 ); } // Display rollback-specific summary and determine exit code return await this.displayRollbackSummary(operationResults); } else { // Default update workflow (Requirements 9.4, 9.5 - backward compatibility) const products = await this.safeFetchAndValidateProducts(); if (products === null) { return await this.handleCriticalFailure("Product fetching failed", 1); } operationResults = await this.safeUpdatePrices(products); if (operationResults === null) { return await this.handleCriticalFailure( "Price update process failed", 1 ); } // Display summary and determine exit code return await this.displaySummaryAndGetExitCode(operationResults); } } catch (error) { // Handle any unexpected errors with comprehensive logging (Requirement 4.5) await this.handleUnexpectedError(error, operationResults); return 2; // Unexpected error } } /** * Handle scheduled execution workflow * @returns {Promise} True if execution should proceed, false if cancelled */ async handleScheduledExecution() { try { // Use the already validated scheduled time from config const scheduledTime = this.config.scheduledExecutionTime; // Display scheduling confirmation and countdown await this.logger.info("šŸ• Scheduled execution mode activated"); await this.scheduleService.displayScheduleInfo(scheduledTime); // Wait until scheduled time with cancellation support const shouldProceed = await this.scheduleService.waitUntilScheduledTime( scheduledTime, () => { // Cancellation callback - log the cancellation this.logger.info("Scheduled operation cancelled by user"); } ); if (!shouldProceed) { // Update scheduling state - no longer waiting if (this.setSchedulingActive) { this.setSchedulingActive(false); } await this.logger.info("Operation cancelled. Exiting gracefully."); return false; } // Scheduling wait period is complete, operations will begin if (this.setSchedulingActive) { this.setSchedulingActive(false); } // Log that scheduled execution is starting await this.logger.info( "ā° Scheduled time reached. Beginning operation..." ); return true; } catch (error) { // Update scheduling state on error if (this.setSchedulingActive) { this.setSchedulingActive(false); } await this.logger.error(`Scheduling error: ${error.message}`); return false; } } /** * Safe wrapper for initialization with enhanced error handling * @returns {Promise} True if successful */ async safeInitialize() { try { return await this.initialize(); } catch (error) { await this.logger.error(`Initialization error: ${error.message}`); if (error.stack) { console.error("Stack trace:", error.stack); } return false; } } /** * Safe wrapper for connection testing with enhanced error handling * @returns {Promise} True if successful */ async safeTestConnection() { try { return await this.testConnection(); } catch (error) { await this.logger.error(`Connection test error: ${error.message}`); if (error.errorHistory) { await this.logger.error( `Connection attempts made: ${error.totalAttempts || "Unknown"}` ); } return false; } } /** * Safe wrapper for product fetching with enhanced error handling * @returns {Promise} Products array or null if failed */ async safeFetchAndValidateProducts() { try { return await this.fetchAndValidateProducts(); } catch (error) { await this.logger.error(`Product fetching error: ${error.message}`); if (error.errorHistory) { await this.logger.error( `Fetch attempts made: ${error.totalAttempts || "Unknown"}` ); } return null; } } /** * Safe wrapper for price updates with enhanced error handling * @param {Array} products - Products to update * @returns {Promise} Update results or null if failed */ async safeUpdatePrices(products) { try { return await this.updatePrices(products); } catch (error) { await this.logger.error(`Price update error: ${error.message}`); if (error.errorHistory) { await this.logger.error( `Update attempts made: ${error.totalAttempts || "Unknown"}` ); } // Return partial results if available return { totalProducts: products.length, totalVariants: products.reduce( (sum, p) => sum + (p.variants?.length || 0), 0 ), successfulUpdates: 0, failedUpdates: products.reduce( (sum, p) => sum + (p.variants?.length || 0), 0 ), errors: [ { productTitle: "System Error", productId: "N/A", errorMessage: error.message, }, ], }; } } /** * Safe wrapper for product fetching for rollback with enhanced error handling * @returns {Promise} Products array or null if failed */ async safeFetchAndValidateProductsForRollback() { try { return await this.fetchAndValidateProductsForRollback(); } catch (error) { await this.logger.error( `Product fetching for rollback error: ${error.message}` ); if (error.errorHistory) { await this.logger.error( `Fetch attempts made: ${error.totalAttempts || "Unknown"}` ); } return null; } } /** * Safe wrapper for rollback operations with enhanced error handling * @param {Array} products - Products to rollback * @returns {Promise} Rollback results or null if failed */ async safeRollbackPrices(products) { try { return await this.rollbackPrices(products); } catch (error) { await this.logger.error(`Price rollback error: ${error.message}`); if (error.errorHistory) { await this.logger.error( `Rollback attempts made: ${error.totalAttempts || "Unknown"}` ); } // Return partial results if available return { totalProducts: products.length, totalVariants: products.reduce( (sum, p) => sum + (p.variants?.length || 0), 0 ), eligibleVariants: products.reduce( (sum, p) => sum + (p.variants?.length || 0), 0 ), successfulRollbacks: 0, failedRollbacks: products.reduce( (sum, p) => sum + (p.variants?.length || 0), 0 ), skippedVariants: 0, errors: [ { productTitle: "System Error", productId: "N/A", errorMessage: error.message, }, ], }; } } /** * Handle critical failures with proper logging for both operation modes (Requirements 9.2, 9.3) * @param {string} message - Failure message * @param {number} exitCode - Exit code to return * @returns {Promise} Exit code */ async handleCriticalFailure(message, exitCode) { await this.logger.error( `Critical failure in ${ this.config?.operationMode || "unknown" } mode: ${message}` ); // Ensure progress logging continues even for critical failures // Use appropriate summary format based on operation mode try { if (this.config?.operationMode === "rollback") { const summary = { totalProducts: 0, totalVariants: 0, eligibleVariants: 0, successfulRollbacks: 0, failedRollbacks: 0, skippedVariants: 0, startTime: this.startTime, errors: [ { productTitle: "Critical System Error", productId: "N/A", errorMessage: message, }, ], }; await this.logger.logRollbackSummary(summary); } else { const summary = { totalProducts: 0, totalVariants: 0, successfulUpdates: 0, failedUpdates: 0, startTime: this.startTime, errors: [ { productTitle: "Critical System Error", productId: "N/A", errorMessage: message, }, ], }; await this.logger.logCompletionSummary(summary); } } catch (loggingError) { console.error( "Failed to log critical failure summary:", loggingError.message ); } return exitCode; } /** * Handle unexpected errors with comprehensive logging for both operation modes (Requirements 9.2, 9.3) * @param {Error} error - The unexpected error * @param {Object} operationResults - Partial results if available * @returns {Promise} */ async handleUnexpectedError(error, operationResults) { await this.logger.error( `Unexpected error occurred in ${ this.config?.operationMode || "unknown" } mode: ${error.message}` ); // Log error details if (error.stack) { await this.logger.error("Stack trace:"); console.error(error.stack); } if (error.errorHistory) { await this.logger.error( "Error history available - check logs for retry attempts" ); } // Ensure progress logging continues even for unexpected errors // Use appropriate summary format based on operation mode try { if (this.config?.operationMode === "rollback") { const summary = { totalProducts: operationResults?.totalProducts || 0, totalVariants: operationResults?.totalVariants || 0, eligibleVariants: operationResults?.eligibleVariants || 0, successfulRollbacks: operationResults?.successfulRollbacks || 0, failedRollbacks: operationResults?.failedRollbacks || 0, skippedVariants: operationResults?.skippedVariants || 0, startTime: this.startTime, errors: operationResults?.errors || [ { productTitle: "Unexpected System Error", productId: "N/A", errorMessage: error.message, }, ], }; await this.logger.logRollbackSummary(summary); } else { const summary = { totalProducts: operationResults?.totalProducts || 0, totalVariants: operationResults?.totalVariants || 0, successfulUpdates: operationResults?.successfulUpdates || 0, failedUpdates: operationResults?.failedUpdates || 0, startTime: this.startTime, errors: operationResults?.errors || [ { productTitle: "Unexpected System Error", productId: "N/A", errorMessage: error.message, }, ], }; await this.logger.logCompletionSummary(summary); } } catch (loggingError) { console.error( "Failed to log unexpected error summary:", loggingError.message ); } } } /** * Main execution function * Handles graceful exit with appropriate status codes */ async function main() { const app = new ShopifyPriceUpdater(); // Enhanced signal handling state management let schedulingActive = false; let operationInProgress = false; let signalHandlersSetup = false; /** * Enhanced signal handler that coordinates with scheduling and operation states * @param {string} signal - The signal received (SIGINT, SIGTERM) * @param {number} exitCode - Exit code to use */ const handleShutdown = async (signal, exitCode) => { // During scheduled waiting period - provide clear cancellation message if (schedulingActive && !operationInProgress) { console.log(`\nšŸ›‘ Received ${signal} during scheduled wait period.`); console.log("šŸ“‹ Cancelling scheduled operation..."); try { // Clean up scheduling resources if (app.scheduleService) { app.scheduleService.cleanup(); } // Log cancellation to progress file const logger = new Logger(); await logger.warning( `Scheduled operation cancelled by ${signal} signal` ); console.log( "āœ… Scheduled operation cancelled successfully. No price updates were performed." ); } catch (error) { console.error("Failed to log cancellation:", error.message); } process.exit(0); // Clean cancellation, exit with success return; } // During active price update operations - prevent interruption if (operationInProgress) { console.log( `\nāš ļø Received ${signal} during active price update operations.` ); console.log( "šŸ”’ Cannot cancel while price updates are in progress to prevent data corruption." ); console.log("ā³ Please wait for current operations to complete..."); console.log( "šŸ’” Tip: You can cancel during the countdown period before operations begin." ); return; // Do not exit, let operations complete } // Normal shutdown for non-scheduled operations or after operations complete console.log(`\nšŸ›‘ Received ${signal}. Shutting down gracefully...`); try { // Clean up scheduling resources if (app.scheduleService) { app.scheduleService.cleanup(); } // Attempt to log shutdown to progress file const logger = new Logger(); await logger.warning(`Operation interrupted by ${signal}`); } catch (error) { console.error("Failed to log shutdown:", error.message); } process.exit(exitCode); }; /** * Set up enhanced signal handlers with proper coordination */ const setupSignalHandlers = () => { if (signalHandlersSetup) { return; // Avoid duplicate handlers } process.on("SIGINT", () => handleShutdown("SIGINT", 130)); process.on("SIGTERM", () => handleShutdown("SIGTERM", 143)); signalHandlersSetup = true; }; /** * Update scheduling state for signal handler coordination * @param {boolean} active - Whether scheduling is currently active */ const setSchedulingActive = (active) => { schedulingActive = active; }; /** * Update operation state for signal handler coordination * @param {boolean} inProgress - Whether price update operations are in progress */ const setOperationInProgress = (inProgress) => { operationInProgress = inProgress; }; // Make state management functions available to the app app.setSchedulingActive = setSchedulingActive; app.setOperationInProgress = setOperationInProgress; // Set up enhanced signal handlers setupSignalHandlers(); // Handle unhandled promise rejections with enhanced logging process.on("unhandledRejection", async (reason, promise) => { console.error("🚨 Unhandled Promise Rejection detected:"); console.error("Promise:", promise); console.error("Reason:", reason); try { // Attempt to log to progress file const logger = new Logger(); await logger.error(`Unhandled Promise Rejection: ${reason}`); } catch (error) { console.error("Failed to log unhandled rejection:", error.message); } process.exit(1); }); // Handle uncaught exceptions with enhanced logging process.on("uncaughtException", async (error) => { console.error("🚨 Uncaught Exception detected:"); console.error("Error:", error.message); console.error("Stack:", error.stack); try { // Attempt to log to progress file const logger = new Logger(); await logger.error(`Uncaught Exception: ${error.message}`); } catch (loggingError) { console.error("Failed to log uncaught exception:", loggingError.message); } process.exit(1); }); try { // Check if scheduling is active to coordinate signal handling const { getConfig } = require("./config/environment"); const config = getConfig(); // Set initial scheduling state if (config.isScheduled) { setSchedulingActive(true); } const exitCode = await app.run(); // Clear all states after run completes setSchedulingActive(false); setOperationInProgress(false); process.exit(exitCode); } catch (error) { console.error("Fatal error:", error.message); // Clean up scheduling resources on error if (app.scheduleService) { app.scheduleService.cleanup(); } // Clear states on error setSchedulingActive(false); setOperationInProgress(false); process.exit(2); } } // Only run main if this file is executed directly if (require.main === module) { main(); } module.exports = ShopifyPriceUpdater;