The scheduled-job store never persisted the channel field, so the send worker always fell through to its email branch regardless of what was requested. Also purges sent jobs 24h after sending instead of keeping them forever, and surfaces who each scheduled job will go to in the admin "manage scheduled" lists (now correctly filtered per channel too). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
128 lines
3.4 KiB
JavaScript
128 lines
3.4 KiB
JavaScript
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,
|
|
};
|