Implemented Rollback Functionality
This commit is contained in:
430
src/index.js
430
src/index.js
@@ -31,8 +31,12 @@ class ShopifyPriceUpdater {
|
||||
// Load and validate configuration
|
||||
this.config = getConfig();
|
||||
|
||||
// Log operation start with configuration (Requirement 3.1)
|
||||
await this.logger.logOperationStart(this.config);
|
||||
// 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) {
|
||||
@@ -210,7 +214,191 @@ class ShopifyPriceUpdater {
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the complete price update workflow
|
||||
* 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`);
|
||||
|
||||
// Execute rollback operations
|
||||
const results = await this.productService.rollbackProductPrices(products);
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
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() {
|
||||
@@ -230,23 +418,48 @@ class ShopifyPriceUpdater {
|
||||
return await this.handleCriticalFailure("API connection failed", 1);
|
||||
}
|
||||
|
||||
// Fetch and validate products with enhanced error handling
|
||||
const products = await this.safeFetchAndValidateProducts();
|
||||
if (products === null) {
|
||||
return await this.handleCriticalFailure("Product fetching failed", 1);
|
||||
}
|
||||
// Display operation mode indication in console output (Requirements 9.3, 8.4)
|
||||
await this.displayOperationModeHeader();
|
||||
|
||||
// Update prices with enhanced error handling
|
||||
operationResults = await this.safeUpdatePrices(products);
|
||||
if (operationResults === null) {
|
||||
return await this.handleCriticalFailure(
|
||||
"Price update process failed",
|
||||
1
|
||||
);
|
||||
}
|
||||
// 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
|
||||
);
|
||||
}
|
||||
|
||||
// Display summary and determine exit code
|
||||
return await this.displaySummaryAndGetExitCode(operationResults);
|
||||
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);
|
||||
@@ -345,31 +558,119 @@ class ShopifyPriceUpdater {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle critical failures with proper logging
|
||||
* 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: ${message}`);
|
||||
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 {
|
||||
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);
|
||||
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:",
|
||||
@@ -381,13 +682,17 @@ class ShopifyPriceUpdater {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle unexpected errors with comprehensive logging
|
||||
* 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: ${error.message}`);
|
||||
await this.logger.error(
|
||||
`Unexpected error occurred in ${
|
||||
this.config?.operationMode || "unknown"
|
||||
} mode: ${error.message}`
|
||||
);
|
||||
|
||||
// Log error details
|
||||
if (error.stack) {
|
||||
@@ -402,22 +707,43 @@ class ShopifyPriceUpdater {
|
||||
}
|
||||
|
||||
// Ensure progress logging continues even for unexpected errors
|
||||
// Use appropriate summary format based on operation mode
|
||||
try {
|
||||
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);
|
||||
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:",
|
||||
|
||||
Reference in New Issue
Block a user