Files
MOPC-Portal/scripts/backfill-intake-round.ts
Matt f24bea3df2 feat: extend notification system with batch sender, bulk dialog, and logging
Add NotificationLog schema extensions (nullable userId, email, roundId,
projectId, batchId fields), batch notification sender service, and bulk
notification dialog UI. Include utility scripts for debugging and seeding.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 13:29:06 +01:00

113 lines
3.3 KiB
TypeScript

/**
* Backfill all projects into the intake round (and any intermediate rounds
* between intake and their earliest assigned round) with COMPLETED state.
*
* Usage: npx tsx scripts/backfill-intake-round.ts
* Add --dry-run to preview without making changes.
*/
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
const dryRun = process.argv.includes('--dry-run')
async function main() {
console.log(dryRun ? '🔍 DRY RUN — no changes will be made\n' : '🚀 Backfilling intake round states...\n')
// Find the intake round
const intakeRound = await prisma.round.findFirst({
where: { roundType: 'INTAKE' },
select: { id: true, name: true, sortOrder: true, competitionId: true },
})
if (!intakeRound) {
console.log('❌ No INTAKE round found')
return
}
console.log(`Intake round: "${intakeRound.name}" (sortOrder: ${intakeRound.sortOrder})`)
// Get all rounds in the competition ordered by sortOrder
const allRounds = await prisma.round.findMany({
where: { competitionId: intakeRound.competitionId },
select: { id: true, name: true, sortOrder: true },
orderBy: { sortOrder: 'asc' },
})
// Find all projects NOT in the intake round
const projects = await prisma.project.findMany({
where: {
projectRoundStates: {
none: { roundId: intakeRound.id },
},
},
select: {
id: true,
title: true,
projectRoundStates: {
select: { roundId: true, round: { select: { sortOrder: true } } },
orderBy: { round: { sortOrder: 'asc' } },
},
},
})
console.log(`${projects.length} projects not in intake round\n`)
if (projects.length === 0) {
console.log('✅ All projects already in intake round')
return
}
// For each project, create COMPLETED states for intake + any intermediate rounds
const toCreate: Array<{ projectId: string; roundId: string; state: 'COMPLETED' }> = []
for (const project of projects) {
// Find the earliest round this project is already in
const earliestSortOrder = project.projectRoundStates.length > 0
? Math.min(...project.projectRoundStates.map(ps => ps.round.sortOrder))
: Infinity
const existingRoundIds = new Set(project.projectRoundStates.map(ps => ps.roundId))
// Add COMPLETED for intake + all intermediate rounds before the earliest assigned round
for (const round of allRounds) {
if (round.sortOrder >= earliestSortOrder) break
if (existingRoundIds.has(round.id)) continue
toCreate.push({
projectId: project.id,
roundId: round.id,
state: 'COMPLETED',
})
}
}
console.log(`Creating ${toCreate.length} ProjectRoundState records...`)
if (!dryRun) {
await prisma.projectRoundState.createMany({
data: toCreate,
skipDuplicates: true,
})
}
// Summary by round
const byRound = new Map<string, number>()
for (const r of toCreate) {
const name = allRounds.find(ar => ar.id === r.roundId)?.name ?? r.roundId
byRound.set(name, (byRound.get(name) ?? 0) + 1)
}
for (const [name, count] of byRound) {
console.log(` ${name}: ${count} projects`)
}
console.log(`\n✅ Done! ${toCreate.length} records ${dryRun ? 'would be' : ''} created`)
}
main()
.catch((e) => {
console.error('❌ Error:', e)
process.exit(1)
})
.finally(() => prisma.$disconnect())