Round system redesign: Phases 1-7 complete
Full pipeline/track/stage architecture replacing the legacy round system. Schema: 11 new models (Pipeline, Track, Stage, StageTransition, ProjectStageState, RoutingRule, Cohort, CohortProject, LiveProgressCursor, OverrideAction, AudienceVoter) + 8 new enums. Backend: 9 new routers (pipeline, stage, routing, stageFiltering, stageAssignment, cohort, live, decision, award) + 6 new services (stage-engine, routing-engine, stage-filtering, stage-assignment, stage-notifications, live-control). Frontend: Pipeline wizard (17 components), jury stage pages (7), applicant pipeline pages (3), public stage pages (2), admin pipeline pages (5), shared stage components (3), SSE route, live hook. Phase 6 refit: 23 routers/services migrated from roundId to stageId, all frontend components refitted. Deleted round.ts (985 lines), roundTemplate.ts, round-helpers.ts, round-settings.ts, round-type-settings.tsx, 10 legacy admin pages, 7 legacy jury pages, 3 legacy dialogs. Phase 7 validation: 36 tests (10 unit + 8 integration files) all passing, TypeScript 0 errors, Next.js build succeeds, 13 integrity checks, legacy symbol sweep clean, auto-seed on first Docker startup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,36 +15,36 @@ interface ReminderResult {
|
||||
}
|
||||
|
||||
/**
|
||||
* Find active rounds with approaching voting deadlines and send reminders
|
||||
* Find active stages with approaching deadlines and send reminders
|
||||
* to jurors who have incomplete assignments.
|
||||
*/
|
||||
export async function processEvaluationReminders(roundId?: string): Promise<ReminderResult> {
|
||||
export async function processEvaluationReminders(stageId?: string): Promise<ReminderResult> {
|
||||
const now = new Date()
|
||||
let totalSent = 0
|
||||
let totalErrors = 0
|
||||
|
||||
// Find active rounds with voting end dates in the future
|
||||
const rounds = await prisma.round.findMany({
|
||||
// Find active stages with window close dates in the future
|
||||
const stages = await prisma.stage.findMany({
|
||||
where: {
|
||||
status: 'ACTIVE',
|
||||
votingEndAt: { gt: now },
|
||||
votingStartAt: { lte: now },
|
||||
...(roundId && { id: roundId }),
|
||||
status: 'STAGE_ACTIVE',
|
||||
windowCloseAt: { gt: now },
|
||||
windowOpenAt: { lte: now },
|
||||
...(stageId && { id: stageId }),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
votingEndAt: true,
|
||||
program: { select: { name: true } },
|
||||
windowCloseAt: true,
|
||||
track: { select: { name: true } },
|
||||
},
|
||||
})
|
||||
|
||||
for (const round of rounds) {
|
||||
if (!round.votingEndAt) continue
|
||||
for (const stage of stages) {
|
||||
if (!stage.windowCloseAt) continue
|
||||
|
||||
const msUntilDeadline = round.votingEndAt.getTime() - now.getTime()
|
||||
const msUntilDeadline = stage.windowCloseAt.getTime() - now.getTime()
|
||||
|
||||
// Determine which reminder types should fire for this round
|
||||
// Determine which reminder types should fire for this stage
|
||||
const applicableTypes = REMINDER_TYPES.filter(
|
||||
({ thresholdMs }) => msUntilDeadline <= thresholdMs
|
||||
)
|
||||
@@ -52,7 +52,7 @@ export async function processEvaluationReminders(roundId?: string): Promise<Remi
|
||||
if (applicableTypes.length === 0) continue
|
||||
|
||||
for (const { type } of applicableTypes) {
|
||||
const result = await sendRemindersForRound(round, type, now)
|
||||
const result = await sendRemindersForStage(stage, type, now)
|
||||
totalSent += result.sent
|
||||
totalErrors += result.errors
|
||||
}
|
||||
@@ -61,12 +61,12 @@ export async function processEvaluationReminders(roundId?: string): Promise<Remi
|
||||
return { sent: totalSent, errors: totalErrors }
|
||||
}
|
||||
|
||||
async function sendRemindersForRound(
|
||||
round: {
|
||||
async function sendRemindersForStage(
|
||||
stage: {
|
||||
id: string
|
||||
name: string
|
||||
votingEndAt: Date | null
|
||||
program: { name: string }
|
||||
windowCloseAt: Date | null
|
||||
track: { name: string }
|
||||
},
|
||||
type: ReminderType,
|
||||
now: Date
|
||||
@@ -74,12 +74,12 @@ async function sendRemindersForRound(
|
||||
let sent = 0
|
||||
let errors = 0
|
||||
|
||||
if (!round.votingEndAt) return { sent, errors }
|
||||
if (!stage.windowCloseAt) return { sent, errors }
|
||||
|
||||
// Find jurors with incomplete assignments for this round
|
||||
// Find jurors with incomplete assignments for this stage
|
||||
const incompleteAssignments = await prisma.assignment.findMany({
|
||||
where: {
|
||||
roundId: round.id,
|
||||
stageId: stage.id,
|
||||
isCompleted: false,
|
||||
},
|
||||
select: {
|
||||
@@ -92,10 +92,10 @@ async function sendRemindersForRound(
|
||||
|
||||
if (userIds.length === 0) return { sent, errors }
|
||||
|
||||
// Check which users already received this reminder type for this round
|
||||
// Check which users already received this reminder type for this stage
|
||||
const existingReminders = await prisma.reminderLog.findMany({
|
||||
where: {
|
||||
roundId: round.id,
|
||||
stageId: stage.id,
|
||||
type,
|
||||
userId: { in: userIds },
|
||||
},
|
||||
@@ -114,7 +114,7 @@ async function sendRemindersForRound(
|
||||
})
|
||||
|
||||
const baseUrl = process.env.NEXTAUTH_URL || 'https://monaco-opc.com'
|
||||
const deadlineStr = round.votingEndAt.toLocaleDateString('en-US', {
|
||||
const deadlineStr = stage.windowCloseAt.toLocaleDateString('en-US', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
@@ -144,12 +144,12 @@ async function sendRemindersForRound(
|
||||
emailTemplateType,
|
||||
{
|
||||
name: user.name || undefined,
|
||||
title: `Evaluation Reminder - ${round.name}`,
|
||||
message: `You have ${pendingCount} pending evaluation${pendingCount !== 1 ? 's' : ''} for ${round.name}.`,
|
||||
linkUrl: `${baseUrl}/jury/assignments?round=${round.id}`,
|
||||
title: `Evaluation Reminder - ${stage.name}`,
|
||||
message: `You have ${pendingCount} pending evaluation${pendingCount !== 1 ? 's' : ''} for ${stage.name}.`,
|
||||
linkUrl: `${baseUrl}/jury/stages/${stage.id}/assignments`,
|
||||
metadata: {
|
||||
pendingCount,
|
||||
roundName: round.name,
|
||||
stageName: stage.name,
|
||||
deadline: deadlineStr,
|
||||
},
|
||||
}
|
||||
@@ -158,7 +158,7 @@ async function sendRemindersForRound(
|
||||
// Log the sent reminder
|
||||
await prisma.reminderLog.create({
|
||||
data: {
|
||||
roundId: round.id,
|
||||
stageId: stage.id,
|
||||
userId: user.id,
|
||||
type,
|
||||
},
|
||||
@@ -167,7 +167,7 @@ async function sendRemindersForRound(
|
||||
sent++
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to send ${type} reminder to ${user.email} for round ${round.name}:`,
|
||||
`Failed to send ${type} reminder to ${user.email} for stage ${stage.name}:`,
|
||||
error
|
||||
)
|
||||
errors++
|
||||
|
||||
Reference in New Issue
Block a user