feat(final-docs): auto pre-deadline reminder cron
This commit is contained in:
@@ -153,6 +153,57 @@ export async function sendManualFinalDocReminders(
|
||||
return { sent }
|
||||
}
|
||||
|
||||
/**
|
||||
* Cron: remind finalist teams (enrolled in an active LIVE_FINAL round) with
|
||||
* missing required documents, once, when the deadline is within the configured
|
||||
* window. Stamps FinalistConfirmation.finalDocsReminderSentAt.
|
||||
*/
|
||||
export async function sendDueFinalDocReminders(prisma: PrismaClient): Promise<{ remindersSent: number }> {
|
||||
const now = new Date()
|
||||
const rounds = await prisma.round.findMany({
|
||||
where: { roundType: 'LIVE_FINAL', status: 'ROUND_ACTIVE' },
|
||||
select: { id: true, windowCloseAt: true, configJson: true, competition: { select: { programId: true } } },
|
||||
})
|
||||
|
||||
let remindersSent = 0
|
||||
for (const round of rounds) {
|
||||
if (!round.windowCloseAt) continue
|
||||
const cfg = (round.configJson ?? {}) as { finalDocsReminderHoursBeforeDeadline?: number }
|
||||
const windowMs = (cfg.finalDocsReminderHoursBeforeDeadline ?? 48) * 3_600_000
|
||||
const isDue = round.windowCloseAt.getTime() <= now.getTime() + windowMs && round.windowCloseAt.getTime() > now.getTime()
|
||||
if (!isDue) continue
|
||||
|
||||
const states = await prisma.projectRoundState.findMany({ where: { roundId: round.id }, select: { projectId: true } })
|
||||
for (const { projectId } of states) {
|
||||
const confirmation = await prisma.finalistConfirmation.findFirst({
|
||||
where: { projectId, finalDocsReminderSentAt: null },
|
||||
select: { id: true },
|
||||
})
|
||||
if (!confirmation) continue
|
||||
const status = await getFinalDocumentStatusForProject(prisma, projectId)
|
||||
if (!status) continue
|
||||
const missing = status.requirements.filter((r) => r.isRequired && !r.uploaded).map((r) => r.name)
|
||||
if (missing.length === 0) continue
|
||||
|
||||
const project = await prisma.project.findUnique({
|
||||
where: { id: projectId },
|
||||
select: { title: true, teamMembers: { where: { role: 'LEAD' }, take: 1, select: { userId: true } } },
|
||||
})
|
||||
const leadUserId = project?.teamMembers[0]?.userId
|
||||
if (!project || !leadUserId) continue
|
||||
|
||||
try {
|
||||
await remindTeam(prisma, { projectId, projectTitle: project.title, deadline: status.deadline, missing, leadUserId })
|
||||
await prisma.finalistConfirmation.update({ where: { id: confirmation.id }, data: { finalDocsReminderSentAt: new Date() } })
|
||||
remindersSent++
|
||||
} catch (e) {
|
||||
console.error('[final-docs] reminder failed for', projectId, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
return { remindersSent }
|
||||
}
|
||||
|
||||
export type ReviewDocument = { requirementId: string; requirementName: string; file: { id: string; fileName: string; mimeType: string; url: string } | null }
|
||||
export type ReviewTeam = { projectId: string; teamName: string; category: string | null; documents: ReviewDocument[]; submitted: boolean }
|
||||
export type ReviewPayload = { round: { id: string; name: string; deadline: Date | null }; totalCount: number; submittedCount: number; teams: ReviewTeam[] }
|
||||
|
||||
Reference in New Issue
Block a user