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:
2025-08-05 10:05:05 -05:00
commit 1e6881ba86
29 changed files with 10663 additions and 0 deletions

386
src/utils/logger.js Normal file
View File

@@ -0,0 +1,386 @@
const ProgressService = require("../services/progress");
class Logger {
constructor(progressService = null) {
this.progressService = progressService || new ProgressService();
this.colors = {
reset: "\x1b[0m",
bright: "\x1b[1m",
red: "\x1b[31m",
green: "\x1b[32m",
yellow: "\x1b[33m",
blue: "\x1b[34m",
cyan: "\x1b[36m",
};
}
/**
* Formats a timestamp for console display
* @param {Date} date - The date to format
* @returns {string} Formatted timestamp string
*/
formatTimestamp(date = new Date()) {
return date
.toISOString()
.replace("T", " ")
.replace(/\.\d{3}Z$/, "");
}
/**
* Formats a console message with color and timestamp
* @param {string} level - Log level (INFO, WARN, ERROR)
* @param {string} message - Message to log
* @param {string} color - ANSI color code
* @returns {string} Formatted message
*/
formatConsoleMessage(level, message, color) {
const timestamp = this.formatTimestamp();
return `${color}[${timestamp}] ${level}:${this.colors.reset} ${message}`;
}
/**
* Logs an info message to console
* @param {string} message - Message to log
* @returns {Promise<void>}
*/
async info(message) {
const formattedMessage = this.formatConsoleMessage(
"INFO",
message,
this.colors.cyan
);
console.log(formattedMessage);
}
/**
* Logs a warning message to console
* @param {string} message - Message to log
* @returns {Promise<void>}
*/
async warning(message) {
const formattedMessage = this.formatConsoleMessage(
"WARN",
message,
this.colors.yellow
);
console.warn(formattedMessage);
}
/**
* Logs an error message to console
* @param {string} message - Message to log
* @returns {Promise<void>}
*/
async error(message) {
const formattedMessage = this.formatConsoleMessage(
"ERROR",
message,
this.colors.red
);
console.error(formattedMessage);
}
/**
* Logs operation start with configuration details (Requirement 3.1)
* @param {Object} config - Configuration object
* @returns {Promise<void>}
*/
async logOperationStart(config) {
await this.info(`Starting price update operation with configuration:`);
await this.info(` Target Tag: ${config.targetTag}`);
await this.info(` Price Adjustment: ${config.priceAdjustmentPercentage}%`);
await this.info(` Shop Domain: ${config.shopDomain}`);
// Also log to progress file
await this.progressService.logOperationStart(config);
}
/**
* Logs product count information (Requirement 3.2)
* @param {number} count - Number of matching products found
* @returns {Promise<void>}
*/
async logProductCount(count) {
const message = `Found ${count} product${
count !== 1 ? "s" : ""
} matching the specified tag`;
await this.info(message);
}
/**
* Logs individual product update details (Requirement 3.3)
* @param {Object} entry - Product update entry
* @param {string} entry.productTitle - Product title
* @param {string} entry.productId - Product ID
* @param {string} entry.variantId - Variant ID
* @param {number} entry.oldPrice - Original price
* @param {number} entry.newPrice - Updated price
* @returns {Promise<void>}
*/
async logProductUpdate(entry) {
const compareAtInfo = entry.compareAtPrice
? ` (Compare At: $${entry.compareAtPrice})`
: "";
const message = `${this.colors.green}${this.colors.reset} Updated "${entry.productTitle}" - Price: $${entry.oldPrice}$${entry.newPrice}${compareAtInfo}`;
console.log(message);
// Also log to progress file
await this.progressService.logProductUpdate(entry);
}
/**
* Logs completion summary (Requirement 3.4)
* @param {Object} summary - Summary statistics
* @param {number} summary.totalProducts - Total products processed
* @param {number} summary.successfulUpdates - Successful updates
* @param {number} summary.failedUpdates - Failed updates
* @param {Date} summary.startTime - Operation start time
* @returns {Promise<void>}
*/
async logCompletionSummary(summary) {
await this.info("=".repeat(50));
await this.info("OPERATION COMPLETE");
await this.info("=".repeat(50));
await this.info(`Total Products Processed: ${summary.totalProducts}`);
await this.info(
`Successful Updates: ${this.colors.green}${summary.successfulUpdates}${this.colors.reset}`
);
if (summary.failedUpdates > 0) {
await this.info(
`Failed Updates: ${this.colors.red}${summary.failedUpdates}${this.colors.reset}`
);
} else {
await this.info(`Failed Updates: ${summary.failedUpdates}`);
}
if (summary.startTime) {
const duration = Math.round((new Date() - summary.startTime) / 1000);
await this.info(`Duration: ${duration} seconds`);
}
// Also log to progress file
await this.progressService.logCompletionSummary(summary);
}
/**
* Logs error details and continues processing (Requirement 3.5)
* @param {Object} entry - Error entry
* @param {string} entry.productTitle - Product title
* @param {string} entry.productId - Product ID
* @param {string} entry.variantId - Variant ID (optional)
* @param {string} entry.errorMessage - Error message
* @returns {Promise<void>}
*/
async logProductError(entry) {
const variantInfo = entry.variantId ? ` (Variant: ${entry.variantId})` : "";
const message = `${this.colors.red}${this.colors.reset} Failed to update "${entry.productTitle}"${variantInfo}: ${entry.errorMessage}`;
console.error(message);
// Also log to progress file
await this.progressService.logError(entry);
}
/**
* Logs API rate limiting information
* @param {number} retryAfter - Seconds to wait before retry
* @returns {Promise<void>}
*/
async logRateLimit(retryAfter) {
await this.warning(
`Rate limit encountered. Waiting ${retryAfter} seconds before retry...`
);
}
/**
* Logs network retry attempts
* @param {number} attempt - Current attempt number
* @param {number} maxAttempts - Maximum attempts
* @param {string} error - Error message
* @returns {Promise<void>}
*/
async logRetryAttempt(attempt, maxAttempts, error) {
await this.warning(
`Network error (attempt ${attempt}/${maxAttempts}): ${error}. Retrying...`
);
}
/**
* Logs when a product is skipped due to invalid data
* @param {string} productTitle - Product title
* @param {string} reason - Reason for skipping
* @returns {Promise<void>}
*/
async logSkippedProduct(productTitle, reason) {
await this.warning(`Skipped "${productTitle}": ${reason}`);
}
/**
* Logs comprehensive error analysis and recommendations
* @param {Array} errors - Array of error objects
* @param {Object} summary - Operation summary statistics
* @returns {Promise<void>}
*/
async logErrorAnalysis(errors, summary) {
if (!errors || errors.length === 0) {
return;
}
await this.info("=".repeat(50));
await this.info("ERROR ANALYSIS");
await this.info("=".repeat(50));
// Categorize errors
const categories = {};
errors.forEach((error) => {
const category = this.categorizeError(
error.errorMessage || error.error || "Unknown"
);
if (!categories[category]) {
categories[category] = [];
}
categories[category].push(error);
});
// Display category breakdown
await this.info("Error Categories:");
Object.entries(categories).forEach(([category, categoryErrors]) => {
const percentage = (
(categoryErrors.length / errors.length) *
100
).toFixed(1);
this.info(
` ${category}: ${categoryErrors.length} errors (${percentage}%)`
);
});
// Provide recommendations based on error patterns
await this.info("\nRecommendations:");
await this.provideErrorRecommendations(categories, summary);
// Log to progress file as well
await this.progressService.logErrorAnalysis(errors);
}
/**
* Categorize error for analysis (same logic as progress service)
* @param {string} errorMessage - Error message to categorize
* @returns {string} Error category
*/
categorizeError(errorMessage) {
const message = errorMessage.toLowerCase();
if (
message.includes("rate limit") ||
message.includes("429") ||
message.includes("throttled")
) {
return "Rate Limiting";
}
if (
message.includes("network") ||
message.includes("connection") ||
message.includes("timeout")
) {
return "Network Issues";
}
if (
message.includes("authentication") ||
message.includes("unauthorized") ||
message.includes("401")
) {
return "Authentication";
}
if (
message.includes("permission") ||
message.includes("forbidden") ||
message.includes("403")
) {
return "Permissions";
}
if (message.includes("not found") || message.includes("404")) {
return "Resource Not Found";
}
if (
message.includes("validation") ||
message.includes("invalid") ||
message.includes("price")
) {
return "Data Validation";
}
if (
message.includes("server error") ||
message.includes("500") ||
message.includes("502") ||
message.includes("503")
) {
return "Server Errors";
}
if (message.includes("shopify") && message.includes("api")) {
return "Shopify API";
}
return "Other";
}
/**
* Provide recommendations based on error patterns
* @param {Object} categories - Categorized errors
* @param {Object} summary - Operation summary
* @returns {Promise<void>}
*/
async provideErrorRecommendations(categories, summary) {
if (categories["Rate Limiting"]) {
await this.info(
" • Consider reducing batch size or adding delays between requests"
);
await this.info(
" • Check if your API plan supports the current request volume"
);
}
if (categories["Network Issues"]) {
await this.info(" • Check your internet connection stability");
await this.info(" • Consider running the script during off-peak hours");
}
if (categories["Authentication"]) {
await this.info(
" • Verify your Shopify access token is valid and not expired"
);
await this.info(" • Check that your app has the required permissions");
}
if (categories["Data Validation"]) {
await this.info(
" • Review product data for invalid prices or missing information"
);
await this.info(
" • Consider adding more robust data validation before updates"
);
}
if (categories["Server Errors"]) {
await this.info(" • Shopify may be experiencing temporary issues");
await this.info(" • Try running the script again later");
}
// Success rate analysis
const successRate =
summary.totalVariants > 0
? ((summary.successfulUpdates / summary.totalVariants) * 100).toFixed(1)
: 0;
if (successRate < 50) {
await this.warning(
" • Low success rate detected - consider reviewing configuration"
);
} else if (successRate < 90) {
await this.info(
" • Moderate success rate - some optimization may be beneficial"
);
}
}
}
module.exports = Logger;