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:
@@ -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/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -236,4 +236,19 @@ describe('UPLOADS_BASE_URL', () => {
|
||||
expect(warnings.join(' ')).not.toMatch(/ANTHROPIC_API_KEY/);
|
||||
});
|
||||
});
|
||||
// #224. MINIMAL has no INTAKE_ACTION_SECRET, so it is already the absent case.
|
||||
describe('the intake action secret', () => {
|
||||
it('is not required', () => {
|
||||
expect(validateEnv(MINIMAL).errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('warns when it is absent', () => {
|
||||
expect(validateEnv(MINIMAL).warnings.join(' ')).toMatch(/INTAKE_ACTION_SECRET/);
|
||||
});
|
||||
|
||||
it('says nothing when it is set', () => {
|
||||
const { warnings } = validateEnv(withEnv({ INTAKE_ACTION_SECRET: 'a-secret' }));
|
||||
expect(warnings.join(' ')).not.toMatch(/INTAKE_ACTION_SECRET/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user