Files
hope-events/backend/src/controllers/scheduledEmailsController.js
T
joshuaandClaude Sonnet 5 b3ff2b9c5e Fix scheduled WhatsApp messages sending as email; add 24h cleanup and recipient display
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>
2026-08-06 23:37:35 +02:00

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 };