feat: auto-cascade cron + admin waitlist management procedures
- expirePendingPastDeadline service: scans PENDING confirmations past deadline, marks each EXPIRED + audit-logs, then promotes the next waitlist entry per affected category (using each program's grand-final round configJson for windowHours). - /api/cron/finalist-confirmations: hourly cron entrypoint (CRON_SECRET header gate), wraps the service. - finalist.addToWaitlist: insert at a specific rank, shifting later entries down (transactional). - finalist.reorderWaitlist: rewrite a category's rank order in one go, using a temp-rank trick to avoid unique-constraint conflicts mid-update. - finalist.manualPromote: out-of-rank-order admin promote with audit log (FINALIST_MANUAL_PROMOTE) + fresh confirmation email. 2 new tests. Suite at 14/14 for finalist-confirmation.
This commit is contained in:
@@ -521,6 +521,138 @@ describe('finalist.confirm and decline (public)', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('expirePendingPastDeadline marks expired confirmations and promotes next waitlist entry', async () => {
|
||||
const { expirePendingPastDeadline } = await import(
|
||||
'../../src/server/services/finalist-confirmation'
|
||||
)
|
||||
const { program, project } = await setupPendingConfirmation(`expire-${uid()}`)
|
||||
// Create the original PENDING confirmation with a past deadline
|
||||
const originalId = `cmfc_exp_${uid()}`
|
||||
const expiredExp = Math.floor(Date.now() / 1000) - 60
|
||||
const { signFinalistToken } = await import('../../src/lib/finalist-token')
|
||||
const originalToken = signFinalistToken({ confirmationId: originalId, exp: expiredExp })
|
||||
await prisma.finalistConfirmation.create({
|
||||
data: {
|
||||
id: originalId,
|
||||
projectId: project.id,
|
||||
category: 'STARTUP',
|
||||
status: 'PENDING',
|
||||
deadline: new Date(Date.now() - 60_000),
|
||||
token: originalToken,
|
||||
},
|
||||
})
|
||||
// And a waitlist entry to promote
|
||||
const backupProject = await createTestProject(program.id, {
|
||||
title: 'Cron Backup',
|
||||
competitionCategory: 'STARTUP',
|
||||
})
|
||||
const backupLead = await prisma.user.create({
|
||||
data: {
|
||||
id: uid('user'),
|
||||
email: `cronlead_${uid()}@test.local`,
|
||||
name: 'Cron Lead',
|
||||
role: 'APPLICANT',
|
||||
roles: ['APPLICANT'],
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
})
|
||||
userIds.push(backupLead.id)
|
||||
await prisma.teamMember.create({
|
||||
data: { projectId: backupProject.id, userId: backupLead.id, role: 'LEAD' },
|
||||
})
|
||||
const waitlistEntry = await prisma.waitlistEntry.create({
|
||||
data: {
|
||||
programId: program.id,
|
||||
projectId: backupProject.id,
|
||||
category: 'STARTUP',
|
||||
rank: 1,
|
||||
status: 'WAITING',
|
||||
},
|
||||
})
|
||||
|
||||
const result = await expirePendingPastDeadline(prisma)
|
||||
expect(result.expired).toBeGreaterThanOrEqual(1)
|
||||
expect(result.promoted).toBeGreaterThanOrEqual(1)
|
||||
|
||||
const updated = await prisma.finalistConfirmation.findUniqueOrThrow({
|
||||
where: { id: originalId },
|
||||
})
|
||||
expect(updated.status).toBe('EXPIRED')
|
||||
expect(updated.expiredAt).not.toBeNull()
|
||||
|
||||
const promoted = await prisma.finalistConfirmation.findUnique({
|
||||
where: { projectId: backupProject.id },
|
||||
})
|
||||
expect(promoted).not.toBeNull()
|
||||
expect(promoted?.status).toBe('PENDING')
|
||||
|
||||
const updatedEntry = await prisma.waitlistEntry.findUniqueOrThrow({
|
||||
where: { id: waitlistEntry.id },
|
||||
})
|
||||
expect(updatedEntry.status).toBe('PROMOTED')
|
||||
})
|
||||
|
||||
it('manualPromote bypasses rank order and audit-logs the override', async () => {
|
||||
const { program } = await setupPendingConfirmation(`manual-${uid()}`)
|
||||
const admin = await createTestUser('SUPER_ADMIN')
|
||||
userIds.push(admin.id)
|
||||
|
||||
// Three ranked waitlist entries
|
||||
const entries = []
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const p = await createTestProject(program.id, {
|
||||
title: `Rank ${i + 1}`,
|
||||
competitionCategory: 'STARTUP',
|
||||
})
|
||||
const lead = await prisma.user.create({
|
||||
data: {
|
||||
id: uid('user'),
|
||||
email: `rank${i}_${uid()}@test.local`,
|
||||
name: `Rank ${i + 1} Lead`,
|
||||
role: 'APPLICANT',
|
||||
roles: ['APPLICANT'],
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
})
|
||||
userIds.push(lead.id)
|
||||
await prisma.teamMember.create({
|
||||
data: { projectId: p.id, userId: lead.id, role: 'LEAD' },
|
||||
})
|
||||
const entry = await prisma.waitlistEntry.create({
|
||||
data: {
|
||||
programId: program.id,
|
||||
projectId: p.id,
|
||||
category: 'STARTUP',
|
||||
rank: i + 1,
|
||||
status: 'WAITING',
|
||||
},
|
||||
})
|
||||
entries.push(entry)
|
||||
}
|
||||
|
||||
// Manually promote rank #3 (out of order)
|
||||
const adminCaller = createCaller(finalistRouter, {
|
||||
id: admin.id,
|
||||
email: admin.email,
|
||||
role: 'SUPER_ADMIN',
|
||||
})
|
||||
await adminCaller.manualPromote({ waitlistEntryId: entries[2].id, windowHours: 24 })
|
||||
|
||||
const promoted = await prisma.waitlistEntry.findUniqueOrThrow({ where: { id: entries[2].id } })
|
||||
expect(promoted.status).toBe('PROMOTED')
|
||||
const stillWaiting = await prisma.waitlistEntry.findUniqueOrThrow({
|
||||
where: { id: entries[0].id },
|
||||
})
|
||||
expect(stillWaiting.status).toBe('WAITING')
|
||||
|
||||
// Confirmation row exists
|
||||
const confirmation = await prisma.finalistConfirmation.findUnique({
|
||||
where: { projectId: entries[2].projectId },
|
||||
})
|
||||
expect(confirmation).not.toBeNull()
|
||||
expect(confirmation?.promotedFromWaitlistEntryId).toBe(entries[2].id)
|
||||
})
|
||||
|
||||
it('getByToken rejects expired tokens', async () => {
|
||||
const { program, project } = await setupPendingConfirmation(`confirm-expired-${uid()}`)
|
||||
// Manually create a confirmation with a past deadline + signed-expired token
|
||||
|
||||
Reference in New Issue
Block a user