feat(intake): add the submission notification template (#224)

Editable from the settings screen like every other template. reviewUrl is the only required placeholder: a notification with no link in it still sends, still looks fine in the log, and is useless to whoever receives it, which is what the required-placeholder validation exists to catch.

The two signed links are deliberately optional. They are absent whenever INTAKE_ACTION_SECRET is unset, and a template demanding them would leave an unconfigured environment unable to send this at all.

A test asserts the template offers no way to publish. That the email cannot publish is what bounds the risk taken by pricing items on arrival, and it is a property of the copy as much as of the routes — a publish link in the body would be one nobody reviewed.

Where the notification goes is an admin setting rather than an environment variable, for the same reason drafting_model is one: it is changed by whoever runs the shop, not by whoever deploys it. Empty is the default and means do not notify, which is a working configuration.

INTAKE_ACTION_SECRET warns rather than fails at boot, like the drafting key. Being told an item arrived matters far more than being able to discard it in one click.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 15:23:32 -05:00
co-authored by Claude Opus 5
parent acc1b406d8
commit 0517faca60
5 changed files with 110 additions and 3 deletions
+34
View File
@@ -274,3 +274,37 @@ describe('every template can address the customer', () => {
);
});
});
describe('the intake notification template', () => {
// Without the review link the email is a notification you cannot act on.
it('requires the review url', () => {
expect(missingPlaceholders('intakeDraft', 'An item arrived.')).toContain('reviewUrl');
});
it('accepts a body carrying the review url', () => {
expect(missingPlaceholders('intakeDraft', 'Review it: {{reviewUrl}}')).toEqual([]);
});
// The signed links are deliberately optional. They are absent whenever
// INTAKE_ACTION_SECRET is unset, and a template demanding them would leave an
// unconfigured environment unable to send this at all.
it('does not require the signed action links', () => {
const missing = missingPlaceholders('intakeDraft', '{{reviewUrl}}');
expect(missing).not.toContain('discardUrl');
expect(missing).not.toContain('regenerateUrl');
});
it('offers the drafted copy to the template author', () => {
for (const name of ['itemName', 'draftName', 'draftDescription', 'price', 'submitterNote']) {
expect(TEMPLATES.intakeDraft.available).toContain(name);
}
});
// The email must never be able to publish. That is what bounds the risk taken
// by pricing items on arrival, and it is a property of the copy as much as of
// the routes — a publish link here would be one nobody reviewed.
it('offers no way to publish', () => {
expect(TEMPLATES.intakeDraft.available).not.toContain('publishUrl');
expect(TEMPLATES.intakeDraft.defaultBody).not.toMatch(/publishUrl/);
});
});