feat: public confirm/decline procedures with waitlist auto-promotion
- finalist.getByToken: public lookup of a confirmation by signed token, with all the data the public page needs (project, team members, current state). Throws on expired/tampered tokens. - finalist.confirm: validates team membership of every selected user, checks against program.defaultAttendeeCap, atomically writes status=CONFIRMED + AttendingMember rows in a transaction. - finalist.decline: captures optional reason, then promotes the next WAITING waitlist entry in the same category (no-op if waitlist empty). Resolves the new windowHours from the LIVE_FINAL round configJson. - promoteNextWaitlistEntry service: encapsulates the cascade (mark PROMOTED, create fresh PENDING confirmation, send email).
This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
import type { CompetitionCategory, PrismaClient } from '@prisma/client'
|
||||
import { signFinalistToken } from '@/lib/finalist-token'
|
||||
import { sendFinalistConfirmationEmail } from '@/lib/email'
|
||||
|
||||
type AnyPrisma = Pick<PrismaClient, 'finalistConfirmation'>
|
||||
type AnyPrisma = Pick<PrismaClient, 'finalistConfirmation' | 'waitlistEntry' | 'project'>
|
||||
|
||||
/**
|
||||
* Create a PENDING FinalistConfirmation row with a signed token. Caller is
|
||||
* responsible for sending the notification email separately.
|
||||
*/
|
||||
export async function createPendingConfirmation(
|
||||
prisma: AnyPrisma,
|
||||
prisma: Pick<PrismaClient, 'finalistConfirmation'>,
|
||||
args: {
|
||||
projectId: string
|
||||
category: CompetitionCategory
|
||||
@@ -38,3 +39,69 @@ export async function createPendingConfirmation(
|
||||
})
|
||||
return { id, token, deadline }
|
||||
}
|
||||
|
||||
/**
|
||||
* Promote the lowest-ranked WAITING waitlist entry in the given category to
|
||||
* PROMOTED, create a fresh PENDING confirmation for the project, and send
|
||||
* the notification email. No-op if no WAITING entry exists.
|
||||
*/
|
||||
export async function promoteNextWaitlistEntry(
|
||||
prisma: AnyPrisma,
|
||||
args: { programId: string; category: CompetitionCategory; windowHours: number },
|
||||
): Promise<{ promoted: boolean; entryId?: string; confirmationId?: string }> {
|
||||
const entry = await prisma.waitlistEntry.findFirst({
|
||||
where: {
|
||||
programId: args.programId,
|
||||
category: args.category,
|
||||
status: 'WAITING',
|
||||
},
|
||||
orderBy: { rank: 'asc' },
|
||||
})
|
||||
if (!entry) return { promoted: false }
|
||||
|
||||
await prisma.waitlistEntry.update({
|
||||
where: { id: entry.id },
|
||||
data: { status: 'PROMOTED' },
|
||||
})
|
||||
|
||||
const { id: confirmationId, token, deadline } = await createPendingConfirmation(prisma, {
|
||||
projectId: entry.projectId,
|
||||
category: args.category,
|
||||
windowHours: args.windowHours,
|
||||
promotedFromWaitlistEntryId: entry.id,
|
||||
})
|
||||
|
||||
// Send email — log and continue on failure.
|
||||
const project = await prisma.project.findUnique({
|
||||
where: { id: entry.projectId },
|
||||
select: {
|
||||
title: true,
|
||||
teamMembers: {
|
||||
where: { role: 'LEAD' },
|
||||
take: 1,
|
||||
select: { user: { select: { email: true, name: true } } },
|
||||
},
|
||||
},
|
||||
})
|
||||
const lead = project?.teamMembers[0]?.user
|
||||
if (lead?.email && project) {
|
||||
const baseUrl = (process.env.NEXTAUTH_URL ?? 'http://localhost:3000').replace(/\/$/, '')
|
||||
const confirmUrl = `${baseUrl}/finalist/confirm/${token}`
|
||||
try {
|
||||
await sendFinalistConfirmationEmail(
|
||||
lead.email,
|
||||
lead.name ?? null,
|
||||
project.title,
|
||||
deadline,
|
||||
confirmUrl,
|
||||
)
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`[promoteNextWaitlistEntry] failed to send email for project ${entry.projectId}:`,
|
||||
err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return { promoted: true, entryId: entry.id, confirmationId }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user