import { z } from 'zod' import { router, adminProcedure } from '../trpc' import { logAudit } from '../utils/audit' export const gracePeriodRouter = router({ /** * Grant a grace period to a juror */ grant: adminProcedure .input( z.object({ roundId: z.string(), userId: z.string(), projectId: z.string().optional(), // Optional: specific project or all projects extendedUntil: z.date(), reason: z.string().optional(), }) ) .mutation(async ({ ctx, input }) => { const gracePeriod = await ctx.prisma.gracePeriod.create({ data: { ...input, grantedById: ctx.user.id, }, }) // Audit log await logAudit({ prisma: ctx.prisma, userId: ctx.user.id, action: 'GRANT_GRACE_PERIOD', entityType: 'GracePeriod', entityId: gracePeriod.id, detailsJson: { roundId: input.roundId, userId: input.userId, projectId: input.projectId, extendedUntil: input.extendedUntil.toISOString(), }, ipAddress: ctx.ip, userAgent: ctx.userAgent, }) return gracePeriod }), /** * List grace periods for a round */ listByRound: adminProcedure .input(z.object({ roundId: z.string() })) .query(async ({ ctx, input }) => { return ctx.prisma.gracePeriod.findMany({ where: { roundId: input.roundId }, include: { user: { select: { id: true, name: true, email: true } }, grantedBy: { select: { id: true, name: true } }, }, orderBy: { createdAt: 'desc' }, }) }), /** * List active grace periods for a round */ listActiveByRound: adminProcedure .input(z.object({ roundId: z.string() })) .query(async ({ ctx, input }) => { return ctx.prisma.gracePeriod.findMany({ where: { roundId: input.roundId, extendedUntil: { gte: new Date() }, }, include: { user: { select: { id: true, name: true, email: true } }, grantedBy: { select: { id: true, name: true } }, }, orderBy: { extendedUntil: 'asc' }, }) }), /** * Get grace periods for a specific user in a round */ getByUser: adminProcedure .input( z.object({ roundId: z.string(), userId: z.string(), }) ) .query(async ({ ctx, input }) => { return ctx.prisma.gracePeriod.findMany({ where: { roundId: input.roundId, userId: input.userId, }, orderBy: { createdAt: 'desc' }, }) }), /** * Update a grace period */ update: adminProcedure .input( z.object({ id: z.string(), extendedUntil: z.date().optional(), reason: z.string().optional(), }) ) .mutation(async ({ ctx, input }) => { const { id, ...data } = input const gracePeriod = await ctx.prisma.gracePeriod.update({ where: { id }, data, }) // Audit log await logAudit({ prisma: ctx.prisma, userId: ctx.user.id, action: 'UPDATE_GRACE_PERIOD', entityType: 'GracePeriod', entityId: id, detailsJson: data, ipAddress: ctx.ip, userAgent: ctx.userAgent, }) return gracePeriod }), /** * Revoke a grace period */ revoke: adminProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) => { const gracePeriod = await ctx.prisma.gracePeriod.delete({ where: { id: input.id }, }) // Audit log await logAudit({ prisma: ctx.prisma, userId: ctx.user.id, action: 'REVOKE_GRACE_PERIOD', entityType: 'GracePeriod', entityId: input.id, detailsJson: { userId: gracePeriod.userId, roundId: gracePeriod.roundId, }, ipAddress: ctx.ip, userAgent: ctx.userAgent, }) return gracePeriod }), /** * Bulk grant grace periods */ bulkGrant: adminProcedure .input( z.object({ roundId: z.string(), userIds: z.array(z.string()), extendedUntil: z.date(), reason: z.string().optional(), }) ) .mutation(async ({ ctx, input }) => { const created = await ctx.prisma.gracePeriod.createMany({ data: input.userIds.map((userId) => ({ roundId: input.roundId, userId, extendedUntil: input.extendedUntil, reason: input.reason, grantedById: ctx.user.id, })), skipDuplicates: true, }) // Audit log await logAudit({ prisma: ctx.prisma, userId: ctx.user.id, action: 'BULK_GRANT_GRACE_PERIOD', entityType: 'GracePeriod', detailsJson: { roundId: input.roundId, userCount: input.userIds.length, created: created.count, }, ipAddress: ctx.ip, userAgent: ctx.userAgent, }) return { created: created.count } }), })