feat: finalist.unconfirm with mentor cascade
Admin can un-confirm a CONFIRMED finalist (e.g. to allow a category quota decrease). Sets status to SUPERSEDED, cascades to drop the active mentor assignment (if any) with droppedBy='finalist_unconfirmed' and the reason embedded. Mentor receives a MENTEE_DROPPED notification. Already-completed assignments are preserved untouched.
This commit is contained in:
@@ -7,6 +7,10 @@ import {
|
||||
createPendingConfirmation,
|
||||
promoteNextWaitlistEntry,
|
||||
} from '../services/finalist-confirmation'
|
||||
import {
|
||||
createNotification,
|
||||
NotificationTypes,
|
||||
} from '../services/in-app-notification'
|
||||
import { sendFinalistConfirmationEmail } from '@/lib/email'
|
||||
import { verifyFinalistToken } from '@/lib/finalist-token'
|
||||
|
||||
@@ -543,6 +547,93 @@ export const finalistRouter = router({
|
||||
return { ok: true }
|
||||
}),
|
||||
|
||||
/**
|
||||
* Admin un-confirm: flips a CONFIRMED finalist back to SUPERSEDED. Cascades
|
||||
* to drop the active mentor assignment (if any), notifies the mentor, and
|
||||
* audit-logs the override. Used to allow a category quota decrease when
|
||||
* the new quota would otherwise be below the confirmed count.
|
||||
*/
|
||||
unconfirm: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
confirmationId: z.string(),
|
||||
reason: z.string().min(5).max(500),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const confirmation = await ctx.prisma.finalistConfirmation.findUniqueOrThrow({
|
||||
where: { id: input.confirmationId },
|
||||
include: {
|
||||
project: {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
mentorAssignment: {
|
||||
select: {
|
||||
id: true,
|
||||
completionStatus: true,
|
||||
droppedAt: true,
|
||||
mentorId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if (confirmation.status !== 'CONFIRMED') {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: `Confirmation is not in CONFIRMED status (current: ${confirmation.status})`,
|
||||
})
|
||||
}
|
||||
|
||||
await ctx.prisma.finalistConfirmation.update({
|
||||
where: { id: confirmation.id },
|
||||
data: { status: 'SUPERSEDED' },
|
||||
})
|
||||
|
||||
// Cascade: drop active mentor assignment (skip if completed or already dropped)
|
||||
const ma = confirmation.project.mentorAssignment
|
||||
let cascadedMentorAssignment = false
|
||||
if (ma && !ma.droppedAt && ma.completionStatus !== 'completed') {
|
||||
await ctx.prisma.mentorAssignment.update({
|
||||
where: { id: ma.id },
|
||||
data: {
|
||||
droppedAt: new Date(),
|
||||
droppedReason: `Finalist un-confirmed: ${input.reason}`,
|
||||
droppedBy: 'finalist_unconfirmed',
|
||||
},
|
||||
})
|
||||
cascadedMentorAssignment = true
|
||||
// Notify mentor — best-effort
|
||||
try {
|
||||
await createNotification({
|
||||
userId: ma.mentorId,
|
||||
type: NotificationTypes.MENTEE_DROPPED,
|
||||
title: 'Mentee finalist slot withdrawn',
|
||||
message: `Your mentee "${confirmation.project.title}" is no longer a confirmed finalist. Your assignment has ended.`,
|
||||
priority: 'high',
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('[finalist.unconfirm] notify mentor failed:', err)
|
||||
}
|
||||
}
|
||||
|
||||
await logAudit({
|
||||
prisma: ctx.prisma,
|
||||
userId: ctx.user.id,
|
||||
action: 'FINALIST_UNCONFIRM',
|
||||
entityType: 'FinalistConfirmation',
|
||||
entityId: confirmation.id,
|
||||
detailsJson: {
|
||||
reason: input.reason,
|
||||
projectId: confirmation.projectId,
|
||||
cascadedMentorAssignment,
|
||||
},
|
||||
})
|
||||
return { ok: true, cascadedMentorAssignment }
|
||||
}),
|
||||
|
||||
/**
|
||||
* Manually promote a specific waitlist entry out of rank order. Sends a
|
||||
* fresh confirmation email + audit-logs the override (separate from
|
||||
|
||||
Reference in New Issue
Block a user