Steps 1-11, still need to do 12

This commit is contained in:
2025-08-07 15:29:23 -05:00
parent b66a516d20
commit 4019e921d3
15 changed files with 3731 additions and 55 deletions

View File

@@ -9,6 +9,7 @@
const { getConfig } = require("./config/environment");
const ProductService = require("./services/product");
const ScheduleService = require("./services/schedule");
const Logger = require("./utils/logger");
/**
@@ -18,6 +19,7 @@ class ShopifyPriceUpdater {
constructor() {
this.logger = new Logger();
this.productService = new ProductService();
this.scheduleService = new ScheduleService(this.logger);
this.config = null;
this.startTime = null;
}
@@ -136,14 +138,30 @@ class ShopifyPriceUpdater {
`Starting price updates with ${this.config.priceAdjustmentPercentage}% adjustment`
);
// Update product prices
const results = await this.productService.updateProductPrices(
products,
this.config.priceAdjustmentPercentage
);
// Mark operation as in progress to prevent cancellation during updates
if (this.setOperationInProgress) {
this.setOperationInProgress(true);
}
return results;
try {
// Update product prices
const results = await this.productService.updateProductPrices(
products,
this.config.priceAdjustmentPercentage
);
return results;
} finally {
// Mark operation as complete
if (this.setOperationInProgress) {
this.setOperationInProgress(false);
}
}
} catch (error) {
// Ensure operation state is cleared on error
if (this.setOperationInProgress) {
this.setOperationInProgress(false);
}
await this.logger.error(`Price update failed: ${error.message}`);
return null;
}
@@ -280,11 +298,29 @@ class ShopifyPriceUpdater {
await this.logger.info(`Starting price rollback operations`);
// Execute rollback operations
const results = await this.productService.rollbackProductPrices(products);
// Mark operation as in progress to prevent cancellation during rollback
if (this.setOperationInProgress) {
this.setOperationInProgress(true);
}
return results;
try {
// Execute rollback operations
const results = await this.productService.rollbackProductPrices(
products
);
return results;
} finally {
// Mark operation as complete
if (this.setOperationInProgress) {
this.setOperationInProgress(false);
}
}
} catch (error) {
// Ensure operation state is cleared on error
if (this.setOperationInProgress) {
this.setOperationInProgress(false);
}
await this.logger.error(`Price rollback failed: ${error.message}`);
return null;
}
@@ -418,6 +454,14 @@ class ShopifyPriceUpdater {
return await this.handleCriticalFailure("API connection failed", 1);
}
// Check for scheduled execution and handle scheduling if configured
if (this.config.isScheduled) {
const shouldProceed = await this.handleScheduledExecution();
if (!shouldProceed) {
return 0; // Operation was cancelled during scheduling
}
}
// Display operation mode indication in console output (Requirements 9.3, 8.4)
await this.displayOperationModeHeader();
@@ -467,6 +511,57 @@ class ShopifyPriceUpdater {
}
}
/**
* Handle scheduled execution workflow
* @returns {Promise<boolean>} True if execution should proceed, false if cancelled
*/
async handleScheduledExecution() {
try {
// Use the already validated scheduled time from config
const scheduledTime = this.config.scheduledExecutionTime;
// Display scheduling confirmation and countdown
await this.logger.info("🕐 Scheduled execution mode activated");
await this.scheduleService.displayScheduleInfo(scheduledTime);
// Wait until scheduled time with cancellation support
const shouldProceed = await this.scheduleService.waitUntilScheduledTime(
scheduledTime,
() => {
// Cancellation callback - log the cancellation
this.logger.info("Scheduled operation cancelled by user");
}
);
if (!shouldProceed) {
// Update scheduling state - no longer waiting
if (this.setSchedulingActive) {
this.setSchedulingActive(false);
}
await this.logger.info("Operation cancelled. Exiting gracefully.");
return false;
}
// Scheduling wait period is complete, operations will begin
if (this.setSchedulingActive) {
this.setSchedulingActive(false);
}
// Log that scheduled execution is starting
await this.logger.info(
"⏰ Scheduled time reached. Beginning operation..."
);
return true;
} catch (error) {
// Update scheduling state on error
if (this.setSchedulingActive) {
this.setSchedulingActive(false);
}
await this.logger.error(`Scheduling error: ${error.message}`);
return false;
}
}
/**
* Safe wrapper for initialization with enhanced error handling
* @returns {Promise<boolean>} True if successful
@@ -760,30 +855,111 @@ class ShopifyPriceUpdater {
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
});
// Enhanced signal handling state management
let schedulingActive = false;
let operationInProgress = false;
let signalHandlersSetup = false;
process.on("SIGTERM", async () => {
console.log("\n🛑 Received SIGTERM. Shutting down gracefully...");
/**
* Enhanced signal handler that coordinates with scheduling and operation states
* @param {string} signal - The signal received (SIGINT, SIGTERM)
* @param {number} exitCode - Exit code to use
*/
const handleShutdown = async (signal, exitCode) => {
// During scheduled waiting period - provide clear cancellation message
if (schedulingActive && !operationInProgress) {
console.log(`\n🛑 Received ${signal} during scheduled wait period.`);
console.log("📋 Cancelling scheduled operation...");
try {
// Clean up scheduling resources
if (app.scheduleService) {
app.scheduleService.cleanup();
}
// Log cancellation to progress file
const logger = new Logger();
await logger.warning(
`Scheduled operation cancelled by ${signal} signal`
);
console.log(
"✅ Scheduled operation cancelled successfully. No price updates were performed."
);
} catch (error) {
console.error("Failed to log cancellation:", error.message);
}
process.exit(0); // Clean cancellation, exit with success
return;
}
// During active price update operations - prevent interruption
if (operationInProgress) {
console.log(
`\n⚠️ Received ${signal} during active price update operations.`
);
console.log(
"🔒 Cannot cancel while price updates are in progress to prevent data corruption."
);
console.log("⏳ Please wait for current operations to complete...");
console.log(
"💡 Tip: You can cancel during the countdown period before operations begin."
);
return; // Do not exit, let operations complete
}
// Normal shutdown for non-scheduled operations or after operations complete
console.log(`\n🛑 Received ${signal}. Shutting down gracefully...`);
try {
// Clean up scheduling resources
if (app.scheduleService) {
app.scheduleService.cleanup();
}
// Attempt to log shutdown to progress file
const logger = new Logger();
await logger.warning("Operation terminated by system (SIGTERM)");
await logger.warning(`Operation interrupted by ${signal}`);
} catch (error) {
console.error("Failed to log shutdown:", error.message);
}
process.exit(143); // Standard exit code for SIGTERM
});
process.exit(exitCode);
};
/**
* Set up enhanced signal handlers with proper coordination
*/
const setupSignalHandlers = () => {
if (signalHandlersSetup) {
return; // Avoid duplicate handlers
}
process.on("SIGINT", () => handleShutdown("SIGINT", 130));
process.on("SIGTERM", () => handleShutdown("SIGTERM", 143));
signalHandlersSetup = true;
};
/**
* Update scheduling state for signal handler coordination
* @param {boolean} active - Whether scheduling is currently active
*/
const setSchedulingActive = (active) => {
schedulingActive = active;
};
/**
* Update operation state for signal handler coordination
* @param {boolean} inProgress - Whether price update operations are in progress
*/
const setOperationInProgress = (inProgress) => {
operationInProgress = inProgress;
};
// Make state management functions available to the app
app.setSchedulingActive = setSchedulingActive;
app.setOperationInProgress = setOperationInProgress;
// Set up enhanced signal handlers
setupSignalHandlers();
// Handle unhandled promise rejections with enhanced logging
process.on("unhandledRejection", async (reason, promise) => {
@@ -820,10 +996,34 @@ async function main() {
});
try {
// Check if scheduling is active to coordinate signal handling
const { getConfig } = require("./config/environment");
const config = getConfig();
// Set initial scheduling state
if (config.isScheduled) {
setSchedulingActive(true);
}
const exitCode = await app.run();
// Clear all states after run completes
setSchedulingActive(false);
setOperationInProgress(false);
process.exit(exitCode);
} catch (error) {
console.error("Fatal error:", error.message);
// Clean up scheduling resources on error
if (app.scheduleService) {
app.scheduleService.cleanup();
}
// Clear states on error
setSchedulingActive(false);
setOperationInProgress(false);
process.exit(2);
}
}