1037 lines
29 KiB
JavaScript
1037 lines
29 KiB
JavaScript
#!/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<boolean>} 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<boolean>} 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|null>} 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<Object|null>} 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|null>} 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<Object|null>} 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<void>}
|
|
*/
|
|
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<number>} 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<boolean>} 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<boolean>} 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<boolean>} 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<Array|null>} 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<Object|null>} 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<Array|null>} 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<Object|null>} 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<number>} 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<void>}
|
|
*/
|
|
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;
|