const fs = require('fs'); const path = require('path'); const { v4: uuidv4 } = require('uuid'); const DATA_DIR = path.join(__dirname, '..', '..', 'data'); const QUEUE_PATH = path.join(DATA_DIR, 'scheduled-emails.json'); function ensureStore() { try { if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true }); if (!fs.existsSync(QUEUE_PATH)) fs.writeFileSync(QUEUE_PATH, JSON.stringify({ jobs: [] }, null, 2), 'utf-8'); } catch (e) { // Best-effort; throws will surface to caller } } function loadAll() { ensureStore(); try { const raw = fs.readFileSync(QUEUE_PATH, 'utf-8'); const data = JSON.parse(raw); const jobs = Array.isArray(data?.jobs) ? data.jobs : []; return jobs; } catch (e) { return []; } } function saveAll(jobs) { ensureStore(); const payload = { jobs: Array.isArray(jobs) ? jobs : [] }; // Simple atomic-ish write const tmp = QUEUE_PATH + '.tmp'; fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf-8'); fs.renameSync(tmp, QUEUE_PATH); } /** * Add a scheduled job * @param {object} job { id?, eventId, createdById, scheduledAt: ISO string, payload: body for emailEventAttendees } */ function addJob(job) { const now = new Date(); const id = job.id || uuidv4(); const rec = { id, eventId: job.eventId || null, broadcast: !!job.broadcast, channel: job.channel || 'email', recipientSummary: job.recipientSummary || null, createdById: job.createdById || null, scheduledAt: job.scheduledAt, createdAt: now.toISOString(), status: 'queued', // queued | sending | sent | error attempts: 0, lastError: null, payload: job.payload || {}, }; const jobs = loadAll(); jobs.push(rec); saveAll(jobs); return rec; } function listJobs(filter = {}) { const jobs = loadAll(); // Basic filter support return jobs.filter(j => { if (filter.status && j.status !== filter.status) return false; if (filter.eventId && j.eventId !== filter.eventId) return false; if (filter.channel && (j.channel || 'email') !== filter.channel) return false; return true; }); } /** * Permanently remove jobs that finished sending more than maxAgeMs ago. * Only touches 'sent' jobs - queued/sending/error jobs are left for admins to review. */ function purgeSentJobs(maxAgeMs = 24 * 60 * 60 * 1000) { const jobs = loadAll(); const now = Date.now(); const kept = jobs.filter(j => { if (j.status !== 'sent' || !j.sentAt) return true; return (now - new Date(j.sentAt).getTime()) <= maxAgeMs; }); if (kept.length !== jobs.length) saveAll(kept); return jobs.length - kept.length; } function getDueJobs(now = new Date()) { const jobs = loadAll(); const t = now instanceof Date ? now : new Date(now); return jobs.filter(j => j.status === 'queued' && new Date(j.scheduledAt).getTime() <= t.getTime()); } function updateJob(id, patch) { const jobs = loadAll(); const idx = jobs.findIndex(j => j.id === id); if (idx === -1) return null; jobs[idx] = { ...jobs[idx], ...patch }; saveAll(jobs); return jobs[idx]; } function getJob(id) { const jobs = loadAll(); return jobs.find(j => j.id === id) || null; } function deleteJob(id) { const jobs = loadAll(); const filtered = jobs.filter(j => j.id !== id); if (filtered.length === jobs.length) return false; saveAll(filtered); return true; } module.exports = { addJob, listJobs, getDueJobs, updateJob, getJob, deleteJob, purgeSentJobs, };