Feature/224 notification impl #265
@@ -6,6 +6,7 @@ import { typeForExtension } from '../uploadTypes';
|
||||
import { getAnthropicClient } from './anthropicClient';
|
||||
import { draftListing } from './draftListing';
|
||||
import { applyDraft } from './applyDraft';
|
||||
import { notifyDraftReady } from './notifyDraft';
|
||||
|
||||
/**
|
||||
* Turns queued submissions into drafts.
|
||||
@@ -144,6 +145,14 @@ export async function draftQueued(limit = DEFAULT_BATCH): Promise<SweepResult> {
|
||||
}
|
||||
await draftOne(client, row.item_id, row.submitter_note, photos);
|
||||
drafted++;
|
||||
|
||||
// Fire and forget, and deliberately after the draft is committed. A mail
|
||||
// failure must never mark a draft that was written correctly as failed —
|
||||
// the queue is what the admin actually works from, and the email is a
|
||||
// convenience on top of it.
|
||||
void notifyDraftReady(row.item_id).catch((err) =>
|
||||
console.error(`[drafting] notifying for item ${row.item_id}:`, err)
|
||||
);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
console.error(`[drafting] item ${row.item_id}: ${message}`);
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
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<void> {
|
||||
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<NotifyRow>(
|
||||
`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);
|
||||
}
|
||||
Reference in New Issue
Block a user