Initial commit: Complete Shopify Price Updater implementation
- Full Node.js application with Shopify GraphQL API integration - Compare At price support for promotional pricing - Comprehensive error handling and retry logic - Progress tracking with markdown logging - Complete test suite with unit and integration tests - Production-ready with proper exit codes and signal handling
This commit is contained in:
510
src/index.js
Normal file
510
src/index.js
Normal file
@@ -0,0 +1,510 @@
|
||||
#!/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 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.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 (Requirement 3.1)
|
||||
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`
|
||||
);
|
||||
|
||||
// Update product prices
|
||||
const results = await this.productService.updateProductPrices(
|
||||
products,
|
||||
this.config.priceAdjustmentPercentage
|
||||
);
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the complete price update workflow
|
||||
* @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);
|
||||
}
|
||||
|
||||
// Fetch and validate products with enhanced error handling
|
||||
const products = await this.safeFetchAndValidateProducts();
|
||||
if (products === null) {
|
||||
return await this.handleCriticalFailure("Product fetching failed", 1);
|
||||
}
|
||||
|
||||
// Update prices with enhanced error handling
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle critical failures with proper logging
|
||||
* @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}`);
|
||||
|
||||
// Ensure progress logging continues even for critical failures
|
||||
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);
|
||||
} catch (loggingError) {
|
||||
console.error(
|
||||
"Failed to log critical failure summary:",
|
||||
loggingError.message
|
||||
);
|
||||
}
|
||||
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle unexpected errors with comprehensive logging
|
||||
* @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}`);
|
||||
|
||||
// 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
|
||||
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);
|
||||
} 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();
|
||||
|
||||
// Handle process signals for graceful shutdown with enhanced logging
|
||||
process.on("SIGINT", async () => {
|
||||
console.log("\n🛑 Received SIGINT (Ctrl+C). Shutting down gracefully...");
|
||||
try {
|
||||
// Attempt to log shutdown to progress file
|
||||
const logger = new Logger();
|
||||
await logger.warning("Operation interrupted by user (SIGINT)");
|
||||
} catch (error) {
|
||||
console.error("Failed to log shutdown:", error.message);
|
||||
}
|
||||
process.exit(130); // Standard exit code for SIGINT
|
||||
});
|
||||
|
||||
process.on("SIGTERM", async () => {
|
||||
console.log("\n🛑 Received SIGTERM. Shutting down gracefully...");
|
||||
try {
|
||||
// Attempt to log shutdown to progress file
|
||||
const logger = new Logger();
|
||||
await logger.warning("Operation terminated by system (SIGTERM)");
|
||||
} catch (error) {
|
||||
console.error("Failed to log shutdown:", error.message);
|
||||
}
|
||||
process.exit(143); // Standard exit code for SIGTERM
|
||||
});
|
||||
|
||||
// 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 {
|
||||
const exitCode = await app.run();
|
||||
process.exit(exitCode);
|
||||
} catch (error) {
|
||||
console.error("Fatal error:", error.message);
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
// Only run main if this file is executed directly
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = ShopifyPriceUpdater;
|
||||
Reference in New Issue
Block a user