feat: selectFinalists creates PENDING confirmations and sends emails
- New service module createPendingConfirmation: writes a PENDING FinalistConfirmation row with a signed token whose exp matches the computed deadline. - selectFinalists admin mutation: reads windowHours from the round's configJson.confirmationWindowHours (default 24), validates category match + quota, then creates one confirmation per selected project and sends a notification email to the team lead. Email failures are logged but never roll back the row creation. - New email helpers: getFinalistConfirmationTemplate + sendFinalistConfirmationEmail.
This commit is contained in:
@@ -2567,3 +2567,79 @@ export async function sendMentorOnboardingEmail(email: string, name: string | nu
|
||||
const template = getMentorOnboardingTemplate(name || '', baseUrl)
|
||||
await sendEmail({ to: email, subject: template.subject, text: template.text, html: template.html })
|
||||
}
|
||||
|
||||
function getFinalistConfirmationTemplate(
|
||||
name: string,
|
||||
projectTitle: string,
|
||||
deadlineIso: string,
|
||||
confirmUrl: string,
|
||||
): EmailTemplate {
|
||||
const subject = `Grand Finale: confirm your attendance for "${projectTitle}"`
|
||||
const greeting = name ? `Hi ${name},` : 'Hi,'
|
||||
const text = [
|
||||
greeting,
|
||||
'',
|
||||
`Congratulations — your project "${projectTitle}" has been selected as a finalist`,
|
||||
'for the Monaco Ocean Protection Challenge grand finale.',
|
||||
'',
|
||||
`Please confirm your team's attendance by ${deadlineIso}.`,
|
||||
'On the confirmation page you will:',
|
||||
' • Choose which team members will attend',
|
||||
' • Indicate who needs visa support',
|
||||
'',
|
||||
`Confirm here: ${confirmUrl}`,
|
||||
'',
|
||||
'If your team cannot attend, please use the same link to decline so',
|
||||
'we can offer the slot to a waitlisted team in time.',
|
||||
'',
|
||||
'The MOPC team',
|
||||
].join('\n')
|
||||
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body style="margin:0;padding:0;background:#f6f8fa;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;color:#0f172a;">
|
||||
<div style="max-width:560px;margin:32px auto;background:#fff;border-radius:8px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,0.06);">
|
||||
<div style="background:#053d57;padding:24px 28px;color:#fefefe;">
|
||||
<h1 style="margin:0;font-size:20px;font-weight:600;">You're a Grand Finale finalist</h1>
|
||||
</div>
|
||||
<div style="padding:24px 28px;line-height:1.5;font-size:14px;">
|
||||
<p style="margin-top:0;">${greeting}</p>
|
||||
<p>Congratulations — your project <strong>${escapeHtml(projectTitle)}</strong> has been selected as a finalist for the Monaco Ocean Protection Challenge grand finale.</p>
|
||||
<p style="margin-top:20px;padding:12px 16px;background:#fef3c7;border-left:3px solid #d97706;border-radius:4px;">
|
||||
<strong>Confirm by ${escapeHtml(deadlineIso)}.</strong>
|
||||
</p>
|
||||
<p>On the confirmation page you'll choose which team members will attend and indicate who needs visa support.</p>
|
||||
<p style="margin-top:24px;">
|
||||
<a href="${confirmUrl}" style="display:inline-block;padding:10px 20px;background:#de0f1e;color:#fff;text-decoration:none;border-radius:6px;font-weight:600;">Confirm Attendance</a>
|
||||
</p>
|
||||
<p style="margin-top:24px;color:#64748b;font-size:12px;">
|
||||
If your team cannot attend, please use the same link to decline so we can offer the slot to a waitlisted team in time.
|
||||
</p>
|
||||
</div>
|
||||
<div style="padding:16px 28px;background:#f1f5f9;color:#64748b;font-size:12px;text-align:center;">
|
||||
Monaco Ocean Protection Challenge
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`.trim()
|
||||
|
||||
return { subject, text, html }
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a finalist confirmation email. Failures are intentionally not awaited
|
||||
* inside any DB transaction — the calling tRPC mutation logs failures but
|
||||
* does not roll back the confirmation row creation.
|
||||
*/
|
||||
export async function sendFinalistConfirmationEmail(
|
||||
email: string,
|
||||
name: string | null,
|
||||
projectTitle: string,
|
||||
deadline: Date,
|
||||
confirmUrl: string,
|
||||
): Promise<void> {
|
||||
const template = getFinalistConfirmationTemplate(name || '', projectTitle, deadline.toISOString(), confirmUrl)
|
||||
await sendEmail({ to: email, subject: template.subject, text: template.text, html: template.html })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user