diff --git a/prisma/seed-notification-settings.ts b/prisma/seed-notification-settings.ts
index 0b738b6..ec52456 100644
--- a/prisma/seed-notification-settings.ts
+++ b/prisma/seed-notification-settings.ts
@@ -271,6 +271,20 @@ const NOTIFICATION_EMAIL_SETTINGS = [
description: 'Email to the attendee when their visa application status changes',
sendEmail: true,
},
+ {
+ notificationType: 'GRAND_FINAL_DOCS_REMINDER',
+ category: 'logistics',
+ label: 'Final Documents Reminder',
+ description: 'Reminder to finalist teams to upload their Grand Final documents before the deadline',
+ sendEmail: true,
+ },
+ {
+ notificationType: 'GRAND_FINAL_DOCS_SUBMITTED',
+ category: 'logistics',
+ label: 'Final Documents Submitted',
+ description: 'Notifies the team mentor when a finalist uploads a Grand Final document',
+ sendEmail: false,
+ },
// Admin notifications (in-app only by default)
{
diff --git a/src/lib/email.ts b/src/lib/email.ts
index 2c9b5a1..fb47a8f 100644
--- a/src/lib/email.ts
+++ b/src/lib/email.ts
@@ -2238,6 +2238,42 @@ function getFinalistReminderTemplate(
}
}
+/**
+ * Reminder email sent to a finalist team to upload their Grand Final documents.
+ */
+function getGrandFinalDocsReminderTemplate(
+ name: string,
+ projectTitle: string,
+ deadline: Date | null,
+ uploadUrl: string,
+ missing: string[],
+): EmailTemplate {
+ const greeting = name ? `Hi ${name},` : 'Hi there,'
+ const formattedDeadline = deadline
+ ? deadline.toLocaleString('en-GB', { timeZone: 'Europe/Paris', dateStyle: 'full', timeStyle: 'short' })
+ : null
+ const missingLine = missing.length
+ ? `Still needed: ${escapeHtml(missing.join(', '))}.`
+ : 'Please make sure all required documents are uploaded.'
+ const content = `
+ ${sectionTitle('Grand Final — Final Documents')}
+ ${paragraph(greeting)}
+ ${paragraph(`Please upload the final documents for ${escapeHtml(projectTitle)} ahead of the Monaco Ocean Protection Challenge Grand Finale.`)}
+ ${paragraph(missingLine)}
+ ${formattedDeadline ? infoBox(`Deadline: ${escapeHtml(formattedDeadline)} (Paris time).`, 'warning') : ''}
+ ${ctaButton(uploadUrl, 'Upload documents')}
+ ${paragraph('If you have any questions, please reach out to the MOPC team.')}
+ `
+ const text = [
+ greeting, '',
+ `Please upload the final documents for "${projectTitle}" ahead of the Grand Finale.`,
+ missing.length ? `Still needed: ${missing.join(', ')}.` : 'Please make sure all required documents are uploaded.',
+ formattedDeadline ? `Deadline: ${formattedDeadline} (Paris time).` : '',
+ `Upload documents: ${uploadUrl}`, '', 'The MOPC team',
+ ].join('\n')
+ return { subject: 'Action needed: upload your Grand Final documents', html: getEmailWrapper(content), text }
+}
+
/**
* Notification email sent to a team when their confirmed finalist slot is withdrawn by an admin.
*/
@@ -2685,6 +2721,14 @@ export const NOTIFICATION_EMAIL_TEMPLATES: Record = {
new Date((ctx.metadata?.deadline as string) || Date.now()),
ctx.linkUrl || '',
),
+ GRAND_FINAL_DOCS_REMINDER: (ctx) =>
+ getGrandFinalDocsReminderTemplate(
+ ctx.name || '',
+ (ctx.metadata?.projectTitle as string) || 'Your project',
+ ctx.metadata?.deadline ? new Date(ctx.metadata.deadline as string) : null,
+ ctx.linkUrl || '',
+ (ctx.metadata?.missing as string[]) || [],
+ ),
FINALIST_WITHDRAWN: (ctx) =>
getFinalistWithdrawnTemplate(
ctx.name || '',
diff --git a/src/server/services/in-app-notification.ts b/src/server/services/in-app-notification.ts
index fe59d55..58a8121 100644
--- a/src/server/services/in-app-notification.ts
+++ b/src/server/services/in-app-notification.ts
@@ -105,6 +105,8 @@ export const NotificationTypes = {
FINALIST_WITHDRAWN: 'FINALIST_WITHDRAWN',
TRAVEL_CONFIRMED: 'TRAVEL_CONFIRMED',
VISA_STATUS_UPDATE: 'VISA_STATUS_UPDATE',
+ GRAND_FINAL_DOCS_REMINDER: 'GRAND_FINAL_DOCS_REMINDER',
+ GRAND_FINAL_DOCS_SUBMITTED: 'GRAND_FINAL_DOCS_SUBMITTED',
} as const
export type NotificationType = (typeof NotificationTypes)[keyof typeof NotificationTypes]