feat(intake): record where an upload link was sent, and how to say it (#260)

Adds upload_links.contact_email and the uploadLink mail template.

The column is nullable on purpose. Links already exist in QA and a migration cannot invent addresses for them, so they are grandfathered rather than backfilled with something untrue; the requirement belongs in the create route, which is where new links are actually made.

The template requires submitUrl, the same guard verification has on verifyUrl. An email inviting somebody to send in photos, with no way for them to do it, sends perfectly happily and looks fine in the log — it is the one failure here worth making impossible, and a test asserts the default body satisfies the guard it declares.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 17:50:11 -05:00
co-authored by Claude Opus 5
parent 823796b92a
commit eaecc43379
4 changed files with 83 additions and 1 deletions
+1
View File
@@ -280,6 +280,7 @@ export const uploadLinks = pgTable("upload_links", {
maxSubmissions: integer("max_submissions"),
lastUsedAt: timestamp("last_used_at", { withTimezone: true, mode: 'string' }),
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
contactEmail: text("contact_email"),
}, (table) => [
unique("upload_links_token_hash_key").on(table.tokenHash),
]);
+17 -1
View File
@@ -18,7 +18,8 @@ export type TemplateKey =
| 'favoriteWithdrawn'
| 'cartReminder'
| 'emailChanged'
| 'intakeDraft';
| 'intakeDraft'
| 'uploadLink';
export interface TemplateDefinition {
/** Shown in the admin so a card is identifiable without reading its body. */
@@ -153,6 +154,21 @@ export const TEMPLATES: Record<TemplateKey, TemplateDefinition> = {
'Nothing is listed until you publish it from that screen, and the price ' +
'above is a suggestion rather than a decision.\n\n' +
'[Ask for another draft]({{regenerateUrl}}) - [Discard it]({{discardUrl}})'
},
uploadLink: {
label: 'Upload link for a contributor',
// The link itself, for the same reason verification requires verifyUrl: an
// email inviting somebody to send in photos, with no way to do it, sends
// perfectly happily and wastes everyone's time.
required: ['submitUrl'],
available: ['submitUrl', 'label', 'submissionsAllowed'],
defaultSubject: 'Send us your items',
defaultBody:
'You can send us photos of items you would like us to sell.\n\n' +
'[Send in an item]({{submitUrl}})\n\n' +
'You can send {{submissionsAllowed}}. Photograph one item at a time, and tell us anything you know about it — where it came from, what it is made of, any damage. A photo cannot show any of that.\n\n' +
'Keep this link to yourself: anyone who has it can send us items in your name.'
}
};