Implemented Rollback Functionality

This commit is contained in:
2025-08-06 15:18:44 -05:00
parent d741dd5466
commit 78818793f2
20 changed files with 6365 additions and 74 deletions

View File

@@ -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:",