// node_api/jobs/scheduler.js const cron = require('node-cron'); const { logger } = require('../utils/logger'); const { depreciationJob } = require('./depreciationJob'); const { JobStatus } = require('../models/JobStatus'); class JobScheduler { constructor() { this.jobs = new Map(); this.isInitialized = false; } async initialize() { if (this.isInitialized) { logger.warn('Job scheduler already initialized'); return; } try { await this.setupJobs(); this.isInitialized = true; logger.info('Job scheduler initialized successfully'); } catch (error) { logger.error('Failed to initialize job scheduler:', error); throw error; } } async setupJobs() { // Monthly depreciation job - runs on 1st of each month at 2 AM const depreciationJobSchedule = '0 2 1 * *'; const depreciationCronJob = cron.schedule( depreciationJobSchedule, async () => { await this.runJob('depreciation', depreciationJob); }, { scheduled: false, timezone: 'America/New_York' } ); this.jobs.set('depreciation', { cronJob: depreciationCronJob, schedule: depreciationJobSchedule, name: 'Monthly Depreciation Calculation', lastRun: null, nextRun: null, status: 'stopped' }); logger.info('Jobs configured:', Array.from(this.jobs.keys())); } async runJob(jobName, jobFunction) { const job = this.jobs.get(jobName); if (!job) { logger.error(`Job ${jobName} not found`); return; } const startTime = new Date(); const jobExecutionId = `${jobName}_${startTime.getTime()}`; try { logger.info(`Starting job: ${jobName} (ID: ${jobExecutionId})`); // Update job status job.status = 'running'; job.lastRun = startTime; // Log job start in database await JobStatus.create({ job_name: jobName, execution_id: jobExecutionId, status: 'running', started_at: startTime, details: { message: 'Job started' } }); // Execute the job function const result = await jobFunction(); // Job completed successfully const endTime = new Date(); const duration = endTime - startTime; job.status = 'completed'; job.lastRun = endTime; await JobStatus.update( { status: 'completed', completed_at: endTime, duration_ms: duration, details: result }, { where: { execution_id: jobExecutionId } } ); logger.info(`Job ${jobName} completed successfully in ${duration}ms`, result); } catch (error) { // Job failed const endTime = new Date(); const duration = endTime - startTime; job.status = 'failed'; job.lastRun = endTime; await JobStatus.update( { status: 'failed', completed_at: endTime, duration_ms: duration, error_message: error.message, details: { error: error.stack } }, { where: { execution_id: jobExecutionId } } ); logger.error(`Job ${jobName} failed:`, error); // Send notification about job failure await this.notifyJobFailure(jobName, error); } } async notifyJobFailure(jobName, error) { // TODO: Implement email notifications or webhook calls logger.error(`Job failure notification for ${jobName}:`, error.message); } startAll() { for (const [name, job] of this.jobs) { job.cronJob.start(); job.status = 'scheduled'; job.nextRun = this.getNextRunTime(job.schedule); logger.info(`Started job: ${name}, next run: ${job.nextRun}`); } } stopAll() { for (const [name, job] of this.jobs) { job.cronJob.stop(); job.status = 'stopped'; job.nextRun = null; logger.info(`Stopped job: ${name}`); } } async runManual(jobName) { const job = this.jobs.get(jobName); if (!job) { throw new Error(`Job ${jobName} not found`); } if (job.status === 'running') { throw new Error(`Job ${jobName} is already running`); } // Get the job function const jobFunction = this.getJobFunction(jobName); if (!jobFunction) { throw new Error(`Job function for ${jobName} not found`); } return await this.runJob(jobName, jobFunction); } getJobFunction(jobName) { const jobFunctions = { 'depreciation': depreciationJob }; return jobFunctions[jobName]; } getNextRunTime(schedule) { try { const task = cron.schedule(schedule, () => {}, { scheduled: false }); return task.nextDate(); } catch (error) { logger.error('Error calculating next run time:', error); return null; } } getJobStatus(jobName) { const job = this.jobs.get(jobName); if (!job) { return null; } return { name: job.name, status: job.status, schedule: job.schedule, lastRun: job.lastRun, nextRun: job.nextRun }; } getAllJobStatuses() { const statuses = {}; for (const [name, job] of this.jobs) { statuses[name] = this.getJobStatus(name); } return statuses; } async getJobHistory(jobName, limit = 50) { try { const history = await JobStatus.findAll({ where: { job_name: jobName }, order: [['started_at', 'DESC']], limit: limit }); return history; } catch (error) { logger.error('Error fetching job history:', error); return []; } } destroy() { this.stopAll(); this.jobs.clear(); this.isInitialized = false; logger.info('Job scheduler destroyed'); } } // Export singleton instance const jobScheduler = new JobScheduler(); module.exports = { jobScheduler };