Fix award eligibility FK constraint + add country column to round projects
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m13s

- specialAward.setEligibility: add ensureUserExists() guard and use Prisma
  connect syntax to prevent FK violation on stale session user IDs
- specialAward.confirmShortlist: same ensureUserExists() guard for confirmedBy
- Round projects table: add Country column showing project origin

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-18 22:47:20 +01:00
parent 6838b01724
commit aa1bf564ee
3 changed files with 35 additions and 7 deletions

View File

@@ -330,7 +330,7 @@ export function ProjectStatesTable({ competitionId, roundId }: ProjectStatesTabl
{/* Table */} {/* Table */}
<div className="border rounded-lg overflow-hidden"> <div className="border rounded-lg overflow-hidden">
{/* Header */} {/* Header */}
<div className="grid grid-cols-[40px_1fr_140px_120px_100px_48px] gap-2 px-4 py-2.5 bg-muted/40 text-xs font-medium text-muted-foreground border-b"> <div className="grid grid-cols-[40px_1fr_140px_160px_120px_100px_48px] gap-2 px-4 py-2.5 bg-muted/40 text-xs font-medium text-muted-foreground border-b">
<div> <div>
<Checkbox <Checkbox
checked={filtered.length > 0 && filtered.every((ps: any) => selectedIds.has(ps.projectId))} checked={filtered.length > 0 && filtered.every((ps: any) => selectedIds.has(ps.projectId))}
@@ -339,6 +339,7 @@ export function ProjectStatesTable({ competitionId, roundId }: ProjectStatesTabl
</div> </div>
<div>Project</div> <div>Project</div>
<div>Category</div> <div>Category</div>
<div>Country</div>
<div>State</div> <div>State</div>
<div>Entered</div> <div>Entered</div>
<div /> <div />
@@ -351,7 +352,7 @@ export function ProjectStatesTable({ competitionId, roundId }: ProjectStatesTabl
return ( return (
<div <div
key={ps.id} key={ps.id}
className="grid grid-cols-[40px_1fr_140px_120px_100px_48px] gap-2 px-4 py-3 items-center border-b last:border-b-0 hover:bg-muted/30 text-sm" className="grid grid-cols-[40px_1fr_140px_160px_120px_100px_48px] gap-2 px-4 py-3 items-center border-b last:border-b-0 hover:bg-muted/30 text-sm"
> >
<div> <div>
<Checkbox <Checkbox
@@ -373,6 +374,9 @@ export function ProjectStatesTable({ competitionId, roundId }: ProjectStatesTabl
{ps.project?.competitionCategory || '—'} {ps.project?.competitionCategory || '—'}
</Badge> </Badge>
</div> </div>
<div className="text-xs text-muted-foreground truncate">
{ps.project?.country || '—'}
</div>
<div> <div>
<Badge variant="outline" className={`text-xs ${cfg.color}`}> <Badge variant="outline" className={`text-xs ${cfg.color}`}>
<StateIcon className="h-3 w-3 mr-1" /> <StateIcon className="h-3 w-3 mr-1" />

View File

@@ -4,6 +4,25 @@ import { Prisma } from '@prisma/client'
import { router, protectedProcedure, adminProcedure } from '../trpc' import { router, protectedProcedure, adminProcedure } from '../trpc'
import { logAudit } from '../utils/audit' import { logAudit } from '../utils/audit'
import { processEligibilityJob } from '../services/award-eligibility-job' import { processEligibilityJob } from '../services/award-eligibility-job'
import type { PrismaClient } from '@prisma/client'
/**
* Verify the current session user exists in the database.
* Guards against stale JWT sessions (e.g., after database reseed).
*/
async function ensureUserExists(db: PrismaClient, userId: string): Promise<string> {
const user = await db.user.findUnique({
where: { id: userId },
select: { id: true },
})
if (!user) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Your session refers to a user that no longer exists. Please log out and log back in.',
})
}
return user.id
}
export const specialAwardRouter = router({ export const specialAwardRouter = router({
// ─── Admin Queries ────────────────────────────────────────────────────── // ─── Admin Queries ──────────────────────────────────────────────────────
@@ -425,6 +444,8 @@ export const specialAwardRouter = router({
}) })
) )
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const verifiedUserId = await ensureUserExists(ctx.prisma, ctx.user.id)
await ctx.prisma.awardEligibility.upsert({ await ctx.prisma.awardEligibility.upsert({
where: { where: {
awardId_projectId: { awardId_projectId: {
@@ -433,16 +454,16 @@ export const specialAwardRouter = router({
}, },
}, },
create: { create: {
awardId: input.awardId, award: { connect: { id: input.awardId } },
projectId: input.projectId, project: { connect: { id: input.projectId } },
eligible: input.eligible, eligible: input.eligible,
method: 'MANUAL', method: 'MANUAL',
overriddenBy: ctx.user.id, overriddenByUser: { connect: { id: verifiedUserId } },
overriddenAt: new Date(), overriddenAt: new Date(),
}, },
update: { update: {
eligible: input.eligible, eligible: input.eligible,
overriddenBy: ctx.user.id, overriddenByUser: { connect: { id: verifiedUserId } },
overriddenAt: new Date(), overriddenAt: new Date(),
}, },
}) })
@@ -1018,12 +1039,14 @@ export const specialAwardRouter = router({
}) })
} }
const verifiedUserId = await ensureUserExists(ctx.prisma, ctx.user.id)
// Mark all as confirmed // Mark all as confirmed
await ctx.prisma.awardEligibility.updateMany({ await ctx.prisma.awardEligibility.updateMany({
where: { awardId: input.awardId, shortlisted: true, eligible: true }, where: { awardId: input.awardId, shortlisted: true, eligible: true },
data: { data: {
confirmedAt: new Date(), confirmedAt: new Date(),
confirmedBy: ctx.user.id, confirmedBy: verifiedUserId,
}, },
}) })

View File

@@ -690,6 +690,7 @@ export async function getProjectRoundStates(
title: true, title: true,
teamName: true, teamName: true,
competitionCategory: true, competitionCategory: true,
country: true,
status: true, status: true,
}, },
}, },