feat(pitch-deck): English email templates, investor language preference, link-only invite mode
Build pitch-deck / build-push-deploy (push) Successful in 1m55s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 36s
CI / test-python-voice (push) Successful in 35s
CI / test-bqas (push) Successful in 35s

- Add English email template variants (greeting, message, closing, subject, CTA copy)
- Add `preferred_lang` column to `pitch_investors` — stored per investor, deck opens in that language by default
- Invite form: DE/EN language toggle that switches email defaults and pitch language setting
- Invite form: "Send email" toggle — when off, creates investor + returns magic link without sending email (for cold outreach attachment)
- `app/page.tsx`: initializes pitch language from investor's `preferred_lang` before first render (no flash)
- Migration 007 added to `/api/admin/migrate` route for production rollout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-05-06 23:18:33 +02:00
parent e013702a02
commit 17b9006b88
12 changed files with 559 additions and 130 deletions
+19 -6
View File
@@ -11,7 +11,7 @@ export async function POST(request: NextRequest) {
const adminId = guard.kind === 'admin' ? guard.admin.id : null
const body = await request.json().catch(() => ({}))
const { email, name, company, greeting, message, closing } = body
const { email, name, company, greeting, message, closing, lang = 'de', send_email = true } = body
if (!email || typeof email !== 'string') {
return NextResponse.json({ error: 'Email required' }, { status: 400 })
@@ -25,17 +25,20 @@ export async function POST(request: NextRequest) {
const normalizedEmail = email.toLowerCase().trim()
const normalizedLang = lang === 'en' ? 'en' : 'de'
// Upsert investor
const { rows } = await pool.query(
`INSERT INTO pitch_investors (email, name, company)
VALUES ($1, $2, $3)
`INSERT INTO pitch_investors (email, name, company, preferred_lang)
VALUES ($1, $2, $3, $4)
ON CONFLICT (email) DO UPDATE SET
name = COALESCE(EXCLUDED.name, pitch_investors.name),
company = COALESCE(EXCLUDED.company, pitch_investors.company),
preferred_lang = EXCLUDED.preferred_lang,
status = CASE WHEN pitch_investors.status = 'revoked' THEN 'invited' ELSE pitch_investors.status END,
updated_at = NOW()
RETURNING id, status`,
[normalizedEmail, name || null, company || null],
[normalizedEmail, name || null, company || null, normalizedLang],
)
const investor = rows[0]
@@ -54,12 +57,21 @@ export async function POST(request: NextRequest) {
const baseUrl = process.env.PITCH_BASE_URL || 'https://pitch.breakpilot.ai'
const magicLinkUrl = `${baseUrl}/auth/verify?token=${token}`
await sendMagicLinkEmail(normalizedEmail, name || null, magicLinkUrl, greeting, message, closing)
if (send_email) {
await sendMagicLinkEmail(normalizedEmail, name || null, magicLinkUrl, greeting, message, closing, normalizedLang)
}
await logAdminAudit(
adminId,
'investor_invited',
{ email: normalizedEmail, name: name || null, company: company || null, expires_at: expiresAt.toISOString() },
{
email: normalizedEmail,
name: name || null,
company: company || null,
expires_at: expiresAt.toISOString(),
lang: normalizedLang,
send_email: !!send_email,
},
request,
investor.id,
)
@@ -69,5 +81,6 @@ export async function POST(request: NextRequest) {
investor_id: investor.id,
email: normalizedEmail,
expires_at: expiresAt.toISOString(),
magic_link_url: magicLinkUrl,
})
}