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>
104 lines
3.7 KiB
JavaScript
104 lines
3.7 KiB
JavaScript
const { listJobs, getJob, updateJob, deleteJob } = require('../utils/scheduledEmails');
|
|
|
|
// Normalize job for client UI
|
|
function toClient(job) {
|
|
const kind = job.broadcast ? 'broadcast' : (job.eventId ? 'attendees' : 'unknown');
|
|
const subject = job?.payload?.subject || '';
|
|
const html = job?.payload?.html || '';
|
|
const text = job?.payload?.text || '';
|
|
return {
|
|
id: job.id,
|
|
kind,
|
|
eventId: job.eventId || null,
|
|
broadcast: !!job.broadcast,
|
|
channel: job.channel || 'email',
|
|
recipient: job.recipientSummary || null,
|
|
scheduledAt: job.scheduledAt,
|
|
createdAt: job.createdAt,
|
|
status: job.status,
|
|
attempts: job.attempts || 0,
|
|
sentAt: job.sentAt || null,
|
|
lastError: job.lastError || null,
|
|
subject,
|
|
hasHtml: !!html,
|
|
hasText: !!text,
|
|
};
|
|
}
|
|
|
|
// GET /api/scheduled-emails?channel=email|whatsapp
|
|
// Sent jobs are purged from storage 24h after sending, so nothing older than that is ever returned here
|
|
const listScheduledEmails = async (req, res) => {
|
|
try {
|
|
const { channel } = req.query || {};
|
|
const raw = listJobs(channel ? { channel: String(channel) } : {});
|
|
const filtered = raw
|
|
// Provide most-relevant first: queued -> sending -> error -> recent sent
|
|
.sort((a, b) => {
|
|
const order = { queued: 0, sending: 1, error: 2, sent: 3 };
|
|
const oa = order[a.status] ?? 99;
|
|
const ob = order[b.status] ?? 99;
|
|
if (oa !== ob) return oa - ob;
|
|
// Then by scheduledAt asc
|
|
return new Date(a.scheduledAt).getTime() - new Date(b.scheduledAt).getTime();
|
|
})
|
|
.map(toClient);
|
|
|
|
return res.json({ jobs: filtered });
|
|
} catch (e) {
|
|
return res.status(400).json({ message: e?.message || 'Failed to list scheduled emails' });
|
|
}
|
|
};
|
|
|
|
// PATCH /api/scheduled-emails/:id
|
|
// Allows editing scheduledAt, subject, html/text on queued jobs only
|
|
const updateScheduledEmail = async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const job = getJob(id);
|
|
if (!job) return res.status(404).json({ message: 'Job not found' });
|
|
if (job.status !== 'queued') return res.status(400).json({ message: 'Only queued jobs can be edited' });
|
|
|
|
const { scheduledAt, subject, html, text } = req.body || {};
|
|
|
|
const patch = {};
|
|
if (scheduledAt) {
|
|
const when = new Date(scheduledAt);
|
|
if (isNaN(when.getTime())) return res.status(400).json({ message: 'scheduledAt must be a valid ISO date-time' });
|
|
patch.scheduledAt = when.toISOString();
|
|
}
|
|
if (subject != null || html != null || text != null) {
|
|
const payload = { ...(job.payload || {}) };
|
|
if (subject != null) payload.subject = subject;
|
|
if (html != null || text != null) {
|
|
// If html provided explicitly, set html; if text provided, set text
|
|
if (html != null) payload.html = html;
|
|
if (text != null) payload.text = text;
|
|
}
|
|
patch.payload = payload;
|
|
}
|
|
|
|
const updated = updateJob(id, patch);
|
|
return res.json({ message: 'Updated', job: toClient(updated) });
|
|
} catch (e) {
|
|
return res.status(400).json({ message: e?.message || 'Failed to update job' });
|
|
}
|
|
};
|
|
|
|
// DELETE /api/scheduled-emails/:id
|
|
// Only queued jobs can be removed
|
|
const deleteScheduledEmail = async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const job = getJob(id);
|
|
if (!job) return res.status(404).json({ message: 'Job not found' });
|
|
if (job.status !== 'queued') return res.status(400).json({ message: 'Only queued jobs can be deleted' });
|
|
const ok = deleteJob(id);
|
|
if (!ok) return res.status(404).json({ message: 'Job not found' });
|
|
return res.json({ message: 'Deleted' });
|
|
} catch (e) {
|
|
return res.status(400).json({ message: e?.message || 'Failed to delete job' });
|
|
}
|
|
};
|
|
|
|
module.exports = { listScheduledEmails, updateScheduledEmail, deleteScheduledEmail };
|