Implemented Rollback Functionality
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
const ShopifyService = require("./shopify");
|
||||
const { calculateNewPrice, preparePriceUpdate } = require("../utils/price");
|
||||
const {
|
||||
calculateNewPrice,
|
||||
preparePriceUpdate,
|
||||
validateRollbackEligibility,
|
||||
prepareRollbackUpdate,
|
||||
} = require("../utils/price");
|
||||
const Logger = require("../utils/logger");
|
||||
|
||||
/**
|
||||
@@ -251,6 +256,452 @@ class ProductService {
|
||||
return validProducts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that products have the required data for rollback operations
|
||||
* Filters products to only include those with variants that have valid compare-at prices
|
||||
* @param {Array} products - Array of products to validate for rollback
|
||||
* @returns {Promise<Array>} Array of products eligible for rollback
|
||||
*/
|
||||
async validateProductsForRollback(products) {
|
||||
const eligibleProducts = [];
|
||||
let skippedProductCount = 0;
|
||||
let totalVariantsProcessed = 0;
|
||||
let eligibleVariantsCount = 0;
|
||||
|
||||
await this.logger.info(
|
||||
`Starting rollback validation for ${products.length} products`
|
||||
);
|
||||
|
||||
for (const product of products) {
|
||||
// Check if product has variants
|
||||
if (!product.variants || product.variants.length === 0) {
|
||||
await this.logger.warning(
|
||||
`Skipping product "${product.title}" for rollback - no variants found`
|
||||
);
|
||||
skippedProductCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check each variant for rollback eligibility
|
||||
const eligibleVariants = [];
|
||||
for (const variant of product.variants) {
|
||||
totalVariantsProcessed++;
|
||||
|
||||
const eligibilityResult = validateRollbackEligibility(variant);
|
||||
|
||||
if (eligibilityResult.isEligible) {
|
||||
eligibleVariants.push(variant);
|
||||
eligibleVariantsCount++;
|
||||
} else {
|
||||
await this.logger.warning(
|
||||
`Skipping variant "${variant.title}" in product "${product.title}" for rollback - ${eligibilityResult.reason}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Only include products that have at least one eligible variant
|
||||
if (eligibleVariants.length === 0) {
|
||||
await this.logger.warning(
|
||||
`Skipping product "${product.title}" for rollback - no variants with valid compare-at prices`
|
||||
);
|
||||
skippedProductCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add product with only eligible variants
|
||||
eligibleProducts.push({
|
||||
...product,
|
||||
variants: eligibleVariants,
|
||||
});
|
||||
}
|
||||
|
||||
// Log validation summary
|
||||
if (skippedProductCount > 0) {
|
||||
await this.logger.warning(
|
||||
`Skipped ${skippedProductCount} products during rollback validation`
|
||||
);
|
||||
}
|
||||
|
||||
await this.logger.info(
|
||||
`Rollback validation completed: ${eligibleProducts.length} products eligible (${eligibleVariantsCount}/${totalVariantsProcessed} variants eligible)`
|
||||
);
|
||||
|
||||
return eligibleProducts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a single product variant price for rollback operation
|
||||
* Sets the main price to the compare-at price and removes the compare-at price
|
||||
* @param {Object} variant - Variant to rollback
|
||||
* @param {string} variant.id - Variant ID
|
||||
* @param {number} variant.price - Current price
|
||||
* @param {number} variant.compareAtPrice - Compare-at price to use as new price
|
||||
* @param {string} productId - Product ID that contains this variant
|
||||
* @returns {Promise<Object>} Rollback result
|
||||
*/
|
||||
async rollbackVariantPrice(variant, productId) {
|
||||
try {
|
||||
// Validate rollback eligibility before attempting operation (Requirement 4.1)
|
||||
const eligibilityResult = validateRollbackEligibility(variant);
|
||||
if (!eligibilityResult.isEligible) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Rollback not eligible: ${eligibilityResult.reason}`,
|
||||
rollbackDetails: {
|
||||
oldPrice: variant.price,
|
||||
compareAtPrice: variant.compareAtPrice,
|
||||
newPrice: null,
|
||||
},
|
||||
errorType: "validation",
|
||||
retryable: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Prepare rollback update using utility function
|
||||
const rollbackUpdate = prepareRollbackUpdate(variant);
|
||||
|
||||
const variables = {
|
||||
productId: productId,
|
||||
variants: [
|
||||
{
|
||||
id: variant.id,
|
||||
price: rollbackUpdate.newPrice.toString(), // Shopify expects price as string
|
||||
compareAtPrice: rollbackUpdate.compareAtPrice, // null to remove compare-at price
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Use existing retry logic for rollback API operations (Requirement 4.2)
|
||||
const response = await this.shopifyService.executeWithRetry(
|
||||
() =>
|
||||
this.shopifyService.executeMutation(
|
||||
this.getProductVariantUpdateMutation(),
|
||||
variables
|
||||
),
|
||||
this.logger
|
||||
);
|
||||
|
||||
// Check for user errors in the response
|
||||
if (
|
||||
response.productVariantsBulkUpdate.userErrors &&
|
||||
response.productVariantsBulkUpdate.userErrors.length > 0
|
||||
) {
|
||||
const errors = response.productVariantsBulkUpdate.userErrors
|
||||
.map((error) => `${error.field}: ${error.message}`)
|
||||
.join(", ");
|
||||
|
||||
// Categorize Shopify API errors for better error analysis (Requirement 4.5)
|
||||
const errorType = this.categorizeShopifyError(errors);
|
||||
|
||||
throw new Error(`Shopify API errors: ${errors}`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
updatedVariant: response.productVariantsBulkUpdate.productVariants[0],
|
||||
rollbackDetails: {
|
||||
oldPrice: variant.price,
|
||||
compareAtPrice: variant.compareAtPrice,
|
||||
newPrice: rollbackUpdate.newPrice,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
// Enhanced error handling with categorization (Requirements 4.3, 4.4, 4.5)
|
||||
const errorInfo = this.analyzeRollbackError(error, variant);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
rollbackDetails: {
|
||||
oldPrice: variant.price,
|
||||
compareAtPrice: variant.compareAtPrice,
|
||||
newPrice: null,
|
||||
},
|
||||
errorType: errorInfo.type,
|
||||
retryable: errorInfo.retryable,
|
||||
errorHistory: error.errorHistory || null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single product for rollback operations
|
||||
* @param {Object} product - Product to process for rollback
|
||||
* @param {Object} results - Results object to update
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async processProductForRollback(product, results) {
|
||||
for (const variant of product.variants) {
|
||||
results.totalVariants++;
|
||||
|
||||
try {
|
||||
// Perform rollback operation on the variant with enhanced error handling
|
||||
const rollbackResult = await this.rollbackVariantPrice(
|
||||
variant,
|
||||
product.id
|
||||
);
|
||||
|
||||
if (rollbackResult.success) {
|
||||
results.successfulRollbacks++;
|
||||
|
||||
// Log successful rollback using rollback-specific logging method
|
||||
await this.logger.logRollbackUpdate({
|
||||
productId: product.id,
|
||||
productTitle: product.title,
|
||||
variantId: variant.id,
|
||||
oldPrice: rollbackResult.rollbackDetails.oldPrice,
|
||||
newPrice: rollbackResult.rollbackDetails.newPrice,
|
||||
compareAtPrice: rollbackResult.rollbackDetails.compareAtPrice,
|
||||
});
|
||||
} else {
|
||||
// Handle different types of rollback failures (Requirements 4.1, 4.3, 4.4)
|
||||
if (rollbackResult.errorType === "validation") {
|
||||
// Skip variants without compare-at prices gracefully (Requirement 4.1)
|
||||
results.skippedVariants++;
|
||||
|
||||
await this.logger.warning(
|
||||
`Skipped variant "${variant.title || variant.id}" in product "${
|
||||
product.title
|
||||
}": ${rollbackResult.error}`
|
||||
);
|
||||
} else {
|
||||
// Handle API and other errors (Requirements 4.2, 4.3, 4.4)
|
||||
results.failedRollbacks++;
|
||||
|
||||
// Enhanced error entry with additional context (Requirement 4.5)
|
||||
const errorEntry = {
|
||||
productId: product.id,
|
||||
productTitle: product.title,
|
||||
variantId: variant.id,
|
||||
errorMessage: rollbackResult.error,
|
||||
errorType: rollbackResult.errorType,
|
||||
retryable: rollbackResult.retryable,
|
||||
errorHistory: rollbackResult.errorHistory,
|
||||
};
|
||||
|
||||
results.errors.push(errorEntry);
|
||||
|
||||
// Log rollback-specific error with enhanced details
|
||||
await this.logger.error(
|
||||
`Rollback failed for variant "${
|
||||
variant.title || variant.id
|
||||
}" in product "${product.title}": ${rollbackResult.error}`
|
||||
);
|
||||
|
||||
// Log to progress file as well
|
||||
await this.logger.logProductError(errorEntry);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle unexpected errors that bypass the rollbackVariantPrice error handling (Requirement 4.4)
|
||||
results.failedRollbacks++;
|
||||
|
||||
// Analyze unexpected error for better categorization
|
||||
const errorInfo = this.analyzeRollbackError(error, variant);
|
||||
|
||||
const errorEntry = {
|
||||
productId: product.id,
|
||||
productTitle: product.title,
|
||||
variantId: variant.id,
|
||||
errorMessage: `Unexpected rollback error: ${error.message}`,
|
||||
errorType: errorInfo.type,
|
||||
retryable: errorInfo.retryable,
|
||||
errorHistory: error.errorHistory || null,
|
||||
};
|
||||
|
||||
results.errors.push(errorEntry);
|
||||
|
||||
await this.logger.error(
|
||||
`Unexpected rollback error for variant "${
|
||||
variant.title || variant.id
|
||||
}" in product "${product.title}": ${error.message}`
|
||||
);
|
||||
|
||||
await this.logger.logProductError(errorEntry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback prices for all variants in a batch of products
|
||||
* Sets main prices to compare-at prices and removes compare-at prices
|
||||
* @param {Array} products - Array of products to rollback
|
||||
* @returns {Promise<Object>} Batch rollback results
|
||||
*/
|
||||
async rollbackProductPrices(products) {
|
||||
await this.logger.info(
|
||||
`Starting price rollback for ${products.length} products`
|
||||
);
|
||||
|
||||
const results = {
|
||||
totalProducts: products.length,
|
||||
totalVariants: 0,
|
||||
eligibleVariants: products.reduce(
|
||||
(sum, product) => sum + product.variants.length,
|
||||
0
|
||||
),
|
||||
successfulRollbacks: 0,
|
||||
failedRollbacks: 0,
|
||||
skippedVariants: 0,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
let processedProducts = 0;
|
||||
let consecutiveErrors = 0;
|
||||
const maxConsecutiveErrors = 5; // Stop processing if too many consecutive errors
|
||||
|
||||
// Process products in batches to manage rate limits with enhanced error handling
|
||||
for (let i = 0; i < products.length; i += this.batchSize) {
|
||||
const batch = products.slice(i, i + this.batchSize);
|
||||
const batchNumber = Math.floor(i / this.batchSize) + 1;
|
||||
const totalBatches = Math.ceil(products.length / this.batchSize);
|
||||
|
||||
await this.logger.info(
|
||||
`Processing rollback batch ${batchNumber} of ${totalBatches} (${batch.length} products)`
|
||||
);
|
||||
|
||||
let batchErrors = 0;
|
||||
const batchStartTime = Date.now();
|
||||
|
||||
// Process each product in the batch with error recovery (Requirements 4.3, 4.4)
|
||||
for (const product of batch) {
|
||||
try {
|
||||
const variantsBefore = results.totalVariants;
|
||||
await this.processProductForRollback(product, results);
|
||||
|
||||
// Check if this product had any successful operations
|
||||
const variantsProcessed = results.totalVariants - variantsBefore;
|
||||
const productErrors = results.errors.filter(
|
||||
(e) => e.productId === product.id
|
||||
).length;
|
||||
|
||||
if (productErrors === 0) {
|
||||
consecutiveErrors = 0; // Reset consecutive error counter on success
|
||||
} else if (productErrors === variantsProcessed) {
|
||||
// All variants in this product failed
|
||||
consecutiveErrors++;
|
||||
batchErrors++;
|
||||
}
|
||||
|
||||
processedProducts++;
|
||||
|
||||
// Log progress for large batches
|
||||
if (processedProducts % 10 === 0) {
|
||||
await this.logger.info(
|
||||
`Progress: ${processedProducts}/${products.length} products processed`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle product-level errors that bypass processProductForRollback (Requirement 4.4)
|
||||
consecutiveErrors++;
|
||||
batchErrors++;
|
||||
|
||||
await this.logger.error(
|
||||
`Failed to process product "${product.title}" (${product.id}): ${error.message}`
|
||||
);
|
||||
|
||||
// Add product-level error to results
|
||||
const errorEntry = {
|
||||
productId: product.id,
|
||||
productTitle: product.title,
|
||||
variantId: "N/A",
|
||||
errorMessage: `Product processing failed: ${error.message}`,
|
||||
errorType: "product_processing_error",
|
||||
retryable: false,
|
||||
};
|
||||
|
||||
results.errors.push(errorEntry);
|
||||
await this.logger.logProductError(errorEntry);
|
||||
}
|
||||
|
||||
// Check for too many consecutive errors (Requirement 4.4)
|
||||
if (consecutiveErrors >= maxConsecutiveErrors) {
|
||||
await this.logger.error(
|
||||
`Stopping rollback operation due to ${maxConsecutiveErrors} consecutive errors. This may indicate a systemic issue.`
|
||||
);
|
||||
|
||||
// Add summary error for remaining products
|
||||
const remainingProducts = products.length - processedProducts;
|
||||
if (remainingProducts > 0) {
|
||||
const systemErrorEntry = {
|
||||
productId: "SYSTEM",
|
||||
productTitle: `${remainingProducts} remaining products`,
|
||||
variantId: "N/A",
|
||||
errorMessage: `Processing stopped due to consecutive errors (${maxConsecutiveErrors} in a row)`,
|
||||
errorType: "system_error",
|
||||
retryable: true,
|
||||
};
|
||||
results.errors.push(systemErrorEntry);
|
||||
}
|
||||
|
||||
break; // Exit product loop
|
||||
}
|
||||
}
|
||||
|
||||
// Exit batch loop if we hit consecutive error limit
|
||||
if (consecutiveErrors >= maxConsecutiveErrors) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Log batch completion with error summary
|
||||
const batchDuration = Math.round((Date.now() - batchStartTime) / 1000);
|
||||
if (batchErrors > 0) {
|
||||
await this.logger.warning(
|
||||
`Batch ${batchNumber} completed with ${batchErrors} product errors in ${batchDuration}s`
|
||||
);
|
||||
} else {
|
||||
await this.logger.info(
|
||||
`Batch ${batchNumber} completed successfully in ${batchDuration}s`
|
||||
);
|
||||
}
|
||||
|
||||
// Add adaptive delay between batches based on error rate (Requirement 4.2)
|
||||
if (i + this.batchSize < products.length) {
|
||||
let delay = 500; // Base delay
|
||||
|
||||
// Increase delay if we're seeing errors (rate limiting or server issues)
|
||||
if (batchErrors > 0) {
|
||||
delay = Math.min(delay * (1 + batchErrors), 5000); // Cap at 5 seconds
|
||||
await this.logger.info(
|
||||
`Increasing delay to ${delay}ms due to batch errors`
|
||||
);
|
||||
}
|
||||
|
||||
await this.delay(delay);
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced completion logging with error analysis (Requirement 4.5)
|
||||
const successRate =
|
||||
results.eligibleVariants > 0
|
||||
? (
|
||||
(results.successfulRollbacks / results.eligibleVariants) *
|
||||
100
|
||||
).toFixed(1)
|
||||
: 0;
|
||||
|
||||
await this.logger.info(
|
||||
`Price rollback completed. Success: ${results.successfulRollbacks}, Failed: ${results.failedRollbacks}, Skipped: ${results.skippedVariants}, Success Rate: ${successRate}%`
|
||||
);
|
||||
|
||||
// Log error summary if there were failures
|
||||
if (results.errors.length > 0) {
|
||||
const errorsByType = {};
|
||||
results.errors.forEach((error) => {
|
||||
const type = error.errorType || "unknown";
|
||||
errorsByType[type] = (errorsByType[type] || 0) + 1;
|
||||
});
|
||||
|
||||
await this.logger.warning(
|
||||
`Error breakdown: ${Object.entries(errorsByType)
|
||||
.map(([type, count]) => `${type}: ${count}`)
|
||||
.join(", ")}`
|
||||
);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get summary statistics for fetched products
|
||||
* @param {Array} products - Array of products
|
||||
@@ -558,6 +1009,152 @@ class ProductService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Categorize Shopify API errors for better error analysis (Requirement 4.5)
|
||||
* @param {string} errorMessage - Shopify API error message
|
||||
* @returns {string} Error category
|
||||
*/
|
||||
categorizeShopifyError(errorMessage) {
|
||||
const message = errorMessage.toLowerCase();
|
||||
|
||||
if (
|
||||
message.includes("price") &&
|
||||
(message.includes("invalid") || message.includes("must be"))
|
||||
) {
|
||||
return "price_validation";
|
||||
}
|
||||
if (message.includes("variant") && message.includes("not found")) {
|
||||
return "variant_not_found";
|
||||
}
|
||||
if (message.includes("product") && message.includes("not found")) {
|
||||
return "product_not_found";
|
||||
}
|
||||
if (message.includes("permission") || message.includes("access")) {
|
||||
return "permission_denied";
|
||||
}
|
||||
if (message.includes("rate limit") || message.includes("throttled")) {
|
||||
return "rate_limit";
|
||||
}
|
||||
|
||||
return "shopify_api_error";
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze rollback errors for enhanced error handling (Requirements 4.3, 4.4, 4.5)
|
||||
* @param {Error} error - Error to analyze
|
||||
* @param {Object} variant - Variant that caused the error
|
||||
* @returns {Object} Error analysis result
|
||||
*/
|
||||
analyzeRollbackError(error, variant) {
|
||||
const message = error.message.toLowerCase();
|
||||
|
||||
// Network and connection errors (retryable)
|
||||
if (
|
||||
message.includes("network") ||
|
||||
message.includes("connection") ||
|
||||
message.includes("timeout") ||
|
||||
message.includes("econnreset")
|
||||
) {
|
||||
return {
|
||||
type: "network_error",
|
||||
retryable: true,
|
||||
category: "Network Issues",
|
||||
};
|
||||
}
|
||||
|
||||
// Rate limiting errors (retryable)
|
||||
if (
|
||||
message.includes("rate limit") ||
|
||||
message.includes("429") ||
|
||||
message.includes("throttled")
|
||||
) {
|
||||
return {
|
||||
type: "rate_limit",
|
||||
retryable: true,
|
||||
category: "Rate Limiting",
|
||||
};
|
||||
}
|
||||
|
||||
// Server errors (retryable)
|
||||
if (
|
||||
message.includes("500") ||
|
||||
message.includes("502") ||
|
||||
message.includes("503") ||
|
||||
message.includes("server error")
|
||||
) {
|
||||
return {
|
||||
type: "server_error",
|
||||
retryable: true,
|
||||
category: "Server Errors",
|
||||
};
|
||||
}
|
||||
|
||||
// Authentication errors (not retryable)
|
||||
if (
|
||||
message.includes("unauthorized") ||
|
||||
message.includes("401") ||
|
||||
message.includes("authentication")
|
||||
) {
|
||||
return {
|
||||
type: "authentication_error",
|
||||
retryable: false,
|
||||
category: "Authentication",
|
||||
};
|
||||
}
|
||||
|
||||
// Permission errors (not retryable)
|
||||
if (
|
||||
message.includes("forbidden") ||
|
||||
message.includes("403") ||
|
||||
message.includes("permission")
|
||||
) {
|
||||
return {
|
||||
type: "permission_error",
|
||||
retryable: false,
|
||||
category: "Permissions",
|
||||
};
|
||||
}
|
||||
|
||||
// Data validation errors (not retryable)
|
||||
if (
|
||||
message.includes("invalid") ||
|
||||
message.includes("validation") ||
|
||||
message.includes("price") ||
|
||||
message.includes("compare-at")
|
||||
) {
|
||||
return {
|
||||
type: "validation_error",
|
||||
retryable: false,
|
||||
category: "Data Validation",
|
||||
};
|
||||
}
|
||||
|
||||
// Resource not found errors (not retryable)
|
||||
if (message.includes("not found") || message.includes("404")) {
|
||||
return {
|
||||
type: "not_found_error",
|
||||
retryable: false,
|
||||
category: "Resource Not Found",
|
||||
};
|
||||
}
|
||||
|
||||
// Shopify API specific errors
|
||||
if (message.includes("shopify") && message.includes("api")) {
|
||||
return {
|
||||
type: "shopify_api_error",
|
||||
retryable: false,
|
||||
category: "Shopify API",
|
||||
};
|
||||
}
|
||||
|
||||
// Unknown errors (potentially retryable)
|
||||
return {
|
||||
type: "unknown_error",
|
||||
retryable: true,
|
||||
category: "Other",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to add delay between operations
|
||||
* @param {number} ms - Milliseconds to delay
|
||||
|
||||
@@ -35,6 +35,28 @@ class ProgressService {
|
||||
- Price Adjustment: ${config.priceAdjustmentPercentage}%
|
||||
- Started: ${timestamp}
|
||||
|
||||
**Progress:**
|
||||
`;
|
||||
|
||||
await this.appendToProgressFile(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the start of a price rollback operation (Requirements 7.1, 8.3)
|
||||
* @param {Object} config - Configuration object with operation details
|
||||
* @param {string} config.targetTag - The tag being targeted
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async logRollbackStart(config) {
|
||||
const timestamp = this.formatTimestamp();
|
||||
const content = `
|
||||
## Price Rollback Operation - ${timestamp}
|
||||
|
||||
**Configuration:**
|
||||
- Target Tag: ${config.targetTag}
|
||||
- Operation Mode: rollback
|
||||
- Started: ${timestamp}
|
||||
|
||||
**Progress:**
|
||||
`;
|
||||
|
||||
@@ -66,6 +88,28 @@ class ProgressService {
|
||||
await this.appendToProgressFile(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a successful product rollback (Requirements 7.2, 8.3)
|
||||
* @param {Object} entry - Rollback progress entry object
|
||||
* @param {string} entry.productId - Shopify product ID
|
||||
* @param {string} entry.productTitle - Product title
|
||||
* @param {string} entry.variantId - Variant ID
|
||||
* @param {number} entry.oldPrice - Original price before rollback
|
||||
* @param {number} entry.compareAtPrice - Compare-at price being used as new price
|
||||
* @param {number} entry.newPrice - New price (same as compare-at price)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async logRollbackUpdate(entry) {
|
||||
const timestamp = this.formatTimestamp();
|
||||
const content = `- 🔄 **${entry.productTitle}** (${entry.productId})
|
||||
- Variant: ${entry.variantId}
|
||||
- Price: $${entry.oldPrice} → $${entry.newPrice} (from Compare At: $${entry.compareAtPrice})
|
||||
- Rolled back: ${timestamp}
|
||||
`;
|
||||
|
||||
await this.appendToProgressFile(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs an error that occurred during product processing
|
||||
* @param {Object} entry - Progress entry object with error details
|
||||
@@ -111,6 +155,42 @@ class ProgressService {
|
||||
|
||||
---
|
||||
|
||||
`;
|
||||
|
||||
await this.appendToProgressFile(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the completion summary of a rollback operation (Requirements 7.3, 8.3)
|
||||
* @param {Object} summary - Rollback summary statistics
|
||||
* @param {number} summary.totalProducts - Total products processed
|
||||
* @param {number} summary.totalVariants - Total variants processed
|
||||
* @param {number} summary.eligibleVariants - Variants eligible for rollback
|
||||
* @param {number} summary.successfulRollbacks - Number of successful rollbacks
|
||||
* @param {number} summary.failedRollbacks - Number of failed rollbacks
|
||||
* @param {number} summary.skippedVariants - Variants skipped (no compare-at price)
|
||||
* @param {Date} summary.startTime - Operation start time
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async logRollbackSummary(summary) {
|
||||
const timestamp = this.formatTimestamp();
|
||||
const duration = summary.startTime
|
||||
? Math.round((new Date() - summary.startTime) / 1000)
|
||||
: "Unknown";
|
||||
|
||||
const content = `
|
||||
**Rollback Summary:**
|
||||
- Total Products Processed: ${summary.totalProducts}
|
||||
- Total Variants Processed: ${summary.totalVariants}
|
||||
- Eligible Variants: ${summary.eligibleVariants}
|
||||
- Successful Rollbacks: ${summary.successfulRollbacks}
|
||||
- Failed Rollbacks: ${summary.failedRollbacks}
|
||||
- Skipped Variants: ${summary.skippedVariants} (no compare-at price)
|
||||
- Duration: ${duration} seconds
|
||||
- Completed: ${timestamp}
|
||||
|
||||
---
|
||||
|
||||
`;
|
||||
|
||||
await this.appendToProgressFile(content);
|
||||
@@ -191,6 +271,7 @@ ${content}`;
|
||||
// Categorize errors by type
|
||||
const errorCategories = {};
|
||||
const errorDetails = [];
|
||||
const retryableCount = { retryable: 0, nonRetryable: 0, unknown: 0 };
|
||||
|
||||
errors.forEach((error, index) => {
|
||||
const category = this.categorizeError(
|
||||
@@ -201,6 +282,15 @@ ${content}`;
|
||||
}
|
||||
errorCategories[category]++;
|
||||
|
||||
// Track retryable status for rollback analysis
|
||||
if (error.retryable === true) {
|
||||
retryableCount.retryable++;
|
||||
} else if (error.retryable === false) {
|
||||
retryableCount.nonRetryable++;
|
||||
} else {
|
||||
retryableCount.unknown++;
|
||||
}
|
||||
|
||||
errorDetails.push({
|
||||
index: index + 1,
|
||||
product: error.productTitle || "Unknown",
|
||||
@@ -208,6 +298,14 @@ ${content}`;
|
||||
variantId: error.variantId || "N/A",
|
||||
error: error.errorMessage || error.error || "Unknown error",
|
||||
category,
|
||||
errorType: error.errorType || "unknown",
|
||||
retryable:
|
||||
error.retryable !== undefined
|
||||
? error.retryable
|
||||
? "Yes"
|
||||
: "No"
|
||||
: "Unknown",
|
||||
hasHistory: error.errorHistory ? "Yes" : "No",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -222,15 +320,28 @@ ${content}`;
|
||||
content += `- ${category}: ${count} error${count !== 1 ? "s" : ""}\n`;
|
||||
});
|
||||
|
||||
// Add retryable analysis for rollback operations
|
||||
if (retryableCount.retryable > 0 || retryableCount.nonRetryable > 0) {
|
||||
content += `
|
||||
**Retryability Analysis:**
|
||||
- Retryable Errors: ${retryableCount.retryable}
|
||||
- Non-Retryable Errors: ${retryableCount.nonRetryable}
|
||||
- Unknown Retryability: ${retryableCount.unknown}
|
||||
`;
|
||||
}
|
||||
|
||||
content += `
|
||||
**Detailed Error Log:**
|
||||
`;
|
||||
|
||||
// Add detailed error information
|
||||
// Add detailed error information with enhanced fields
|
||||
errorDetails.forEach((detail) => {
|
||||
content += `${detail.index}. **${detail.product}** (${detail.productId})
|
||||
- Variant: ${detail.variantId}
|
||||
- Category: ${detail.category}
|
||||
- Error Type: ${detail.errorType}
|
||||
- Retryable: ${detail.retryable}
|
||||
- Has Retry History: ${detail.hasHistory}
|
||||
- Error: ${detail.error}
|
||||
`;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user