import { pool } from '../db'; import { sendMail } from '../mailer'; import { renderTemplate } from '../emailTemplates'; import { loadStoredTemplate } from '../routes/adminEmailTemplates'; import { getSettings } from '../adminSettings'; import { trimTrailingSlashes } from '../utils'; import { actionUrl } from './actionLinks'; interface NotifyRow { item_name: string; price_cents: number; ai_name: string | null; ai_description: string | null; submitter_note: string | null; link_label: string | null; } /** * Tells the admin an item arrived and has been drafted. * * Everything here is best-effort by design. The review queue is the source of * truth: a ready draft is visible and actionable whether or not this ever sent, * so a missing recipient, an SMTP outage, or a template that will not render * must all end in a log line rather than an exception reaching the worker and * marking a perfectly good draft as failed. */ export async function notifyDraftReady(itemId: number): Promise { const { intakeNotifyEmail } = await getSettings(); const to = intakeNotifyEmail?.trim(); if (!to) { // Not an error, and deliberately not a warning either. Nobody has said // where to send it, and the draft is waiting in the queue regardless. return; } const { rows } = await pool.query( `SELECT i.name AS item_name, i.price_cents, d.ai_name, d.ai_description, d.submitter_note, l.label AS link_label FROM item_drafts d JOIN items i ON i.id = d.item_id LEFT JOIN upload_links l ON l.id = d.upload_link_id WHERE d.item_id = $1`, [itemId] ); const row = rows[0]; if (!row) return; const base = trimTrailingSlashes(process.env.PUBLIC_URL ?? ''); const template = renderTemplate('intakeDraft', await loadStoredTemplate('intakeDraft'), { itemName: row.item_name, draftName: row.ai_name ?? row.item_name, // Said plainly rather than left blank. An empty description in a // notification reads as a bug; "no description was drafted" reads as the // fact that it is, and tells the admin what to expect on the screen. draftDescription: row.ai_description ?? 'No description was drafted for this item.', price: `$${(row.price_cents / 100).toFixed(2)}`, submitterNote: row.submitter_note ?? 'The sender left no note.', linkLabel: row.link_label ?? 'an upload link', reviewUrl: `${base}/admin`, // Empty rather than a broken link when there is no secret to sign with. // The body renders without them; a link that could not be verified would // be worse than none. regenerateUrl: actionUrl(itemId, 'regenerate') ?? '', discardUrl: actionUrl(itemId, 'discard') ?? '' }); await sendMail(to, template.subject, template.html); }