Competition/Round architecture: full platform rewrite (Phases 1-9)
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s
Replace Pipeline/Stage system with Competition/Round architecture. New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy, ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow. New services: round-engine, round-assignment, deliberation, result-lock, submission-manager, competition-context, ai-prompt-guard. Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with structured prompts, retry logic, and injection detection. All legacy pipeline/stage code removed. 4 new migrations + seed aligned. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,42 +22,36 @@ export const applicantRouter = router({
|
||||
getSubmissionBySlug: publicProcedure
|
||||
.input(z.object({ slug: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const stage = await ctx.prisma.stage.findFirst({
|
||||
const round = await ctx.prisma.round.findFirst({
|
||||
where: { slug: input.slug },
|
||||
include: {
|
||||
track: {
|
||||
competition: {
|
||||
include: {
|
||||
pipeline: {
|
||||
include: {
|
||||
program: { select: { id: true, name: true, year: true, description: true } },
|
||||
},
|
||||
},
|
||||
program: { select: { id: true, name: true, year: true, description: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!stage) {
|
||||
if (!round) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'Stage not found',
|
||||
message: 'Round not found',
|
||||
})
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const isOpen = stage.windowCloseAt
|
||||
? now < stage.windowCloseAt
|
||||
: stage.status === 'STAGE_ACTIVE'
|
||||
const isOpen = round.status === 'ROUND_ACTIVE'
|
||||
|
||||
return {
|
||||
stage: {
|
||||
id: stage.id,
|
||||
name: stage.name,
|
||||
slug: stage.slug,
|
||||
windowCloseAt: stage.windowCloseAt,
|
||||
id: round.id,
|
||||
name: round.name,
|
||||
slug: round.slug,
|
||||
windowCloseAt: null,
|
||||
isOpen,
|
||||
},
|
||||
program: stage.track.pipeline.program,
|
||||
program: round.competition.program,
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -65,7 +59,7 @@ export const applicantRouter = router({
|
||||
* Get the current user's submission for a round (as submitter or team member)
|
||||
*/
|
||||
getMySubmission: protectedProcedure
|
||||
.input(z.object({ stageId: z.string().optional(), programId: z.string().optional() }))
|
||||
.input(z.object({ roundId: z.string().optional(), programId: z.string().optional() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
// Only applicants can use this
|
||||
if (ctx.user.role !== 'APPLICANT') {
|
||||
@@ -86,8 +80,8 @@ export const applicantRouter = router({
|
||||
],
|
||||
}
|
||||
|
||||
if (input.stageId) {
|
||||
where.stageStates = { some: { stageId: input.stageId } }
|
||||
if (input.roundId) {
|
||||
where.roundAssignments = { some: { roundId: input.roundId } }
|
||||
}
|
||||
if (input.programId) {
|
||||
where.programId = input.programId
|
||||
@@ -239,7 +233,7 @@ export const applicantRouter = router({
|
||||
fileName: z.string(),
|
||||
mimeType: z.string(),
|
||||
fileType: z.enum(['EXEC_SUMMARY', 'BUSINESS_PLAN', 'VIDEO_PITCH', 'PRESENTATION', 'SUPPORTING_DOC', 'OTHER']),
|
||||
stageId: z.string().optional(),
|
||||
roundId: z.string().optional(),
|
||||
requirementId: z.string().optional(),
|
||||
})
|
||||
)
|
||||
@@ -323,7 +317,7 @@ export const applicantRouter = router({
|
||||
bucket: SUBMISSIONS_BUCKET,
|
||||
objectKey,
|
||||
isLate,
|
||||
stageId: input.stageId || null,
|
||||
roundId: input.roundId || null,
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -340,7 +334,7 @@ export const applicantRouter = router({
|
||||
fileType: z.enum(['EXEC_SUMMARY', 'BUSINESS_PLAN', 'VIDEO_PITCH', 'PRESENTATION', 'SUPPORTING_DOC', 'OTHER']),
|
||||
bucket: z.string(),
|
||||
objectKey: z.string(),
|
||||
stageId: z.string().optional(),
|
||||
roundId: z.string().optional(),
|
||||
isLate: z.boolean().optional(),
|
||||
requirementId: z.string().optional(),
|
||||
})
|
||||
@@ -378,7 +372,7 @@ export const applicantRouter = router({
|
||||
})
|
||||
}
|
||||
|
||||
const { projectId, stageId, isLate, requirementId, ...fileData } = input
|
||||
const { projectId, roundId, isLate, requirementId, ...fileData } = input
|
||||
|
||||
// Delete existing file: by requirementId if provided, otherwise by fileType
|
||||
if (requirementId) {
|
||||
@@ -397,12 +391,12 @@ export const applicantRouter = router({
|
||||
})
|
||||
}
|
||||
|
||||
// Create new file record (roundId column kept null for new data)
|
||||
// Create new file record
|
||||
const file = await ctx.prisma.projectFile.create({
|
||||
data: {
|
||||
projectId,
|
||||
...fileData,
|
||||
roundId: null,
|
||||
roundId: roundId || null,
|
||||
isLate: isLate || false,
|
||||
requirementId: requirementId || null,
|
||||
},
|
||||
@@ -1153,7 +1147,7 @@ export const applicantRouter = router({
|
||||
})
|
||||
|
||||
if (!project) {
|
||||
return { project: null, openStages: [], timeline: [], currentStatus: null }
|
||||
return { project: null, openRounds: [], timeline: [], currentStatus: null }
|
||||
}
|
||||
|
||||
const currentStatus = project.status ?? 'SUBMITTED'
|
||||
@@ -1239,19 +1233,17 @@ export const applicantRouter = router({
|
||||
}
|
||||
|
||||
const programId = project.programId
|
||||
const openStages = programId
|
||||
? await ctx.prisma.stage.findMany({
|
||||
const openRounds = programId
|
||||
? await ctx.prisma.round.findMany({
|
||||
where: {
|
||||
track: { pipeline: { programId } },
|
||||
status: 'STAGE_ACTIVE',
|
||||
competition: { programId },
|
||||
status: 'ROUND_ACTIVE',
|
||||
},
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
stageType: true,
|
||||
windowOpenAt: true,
|
||||
windowCloseAt: true,
|
||||
},
|
||||
})
|
||||
@@ -1267,7 +1259,7 @@ export const applicantRouter = router({
|
||||
isTeamLead,
|
||||
userRole: userMembership?.role || (project.submittedByUserId === ctx.user.id ? 'LEAD' : null),
|
||||
},
|
||||
openStages,
|
||||
openRounds,
|
||||
timeline,
|
||||
currentStatus,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user