Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
import { z } from 'zod'
import { TRPCError } from '@trpc/server'
2026-02-19 12:59:35 +01:00
import { Prisma , type PrismaClient } from '@prisma/client'
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
import { router , adminProcedure , protectedProcedure } from '../trpc'
import { logAudit } from '@/server/utils/audit'
import { validateRoundConfig , defaultRoundConfig } from '@/types/competition-configs'
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
import { generateShortlist } from '../services/ai-shortlist'
2026-02-19 12:59:35 +01:00
import { createBulkNotifications } from '../services/in-app-notification'
import { sendAnnouncementEmail } from '@/lib/email'
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
import {
openWindow ,
closeWindow ,
lockWindow ,
checkDeadlinePolicy ,
validateSubmission ,
getVisibleWindows ,
} from '../services/submission-manager'
const roundTypeEnum = z . enum ( [
'INTAKE' ,
'FILTERING' ,
'EVALUATION' ,
'SUBMISSION' ,
'MENTORING' ,
'LIVE_FINAL' ,
'DELIBERATION' ,
] )
export const roundRouter = router ( {
/ * *
* Create a new round within a competition
* /
create : adminProcedure
. input (
z . object ( {
competitionId : z.string ( ) ,
name : z.string ( ) . min ( 1 ) . max ( 255 ) ,
slug : z.string ( ) . min ( 1 ) . max ( 100 ) . regex ( /^[a-z0-9-]+$/ ) ,
roundType : roundTypeEnum ,
sortOrder : z.number ( ) . int ( ) . nonnegative ( ) ,
configJson : z.record ( z . unknown ( ) ) . optional ( ) ,
windowOpenAt : z.date ( ) . nullable ( ) . optional ( ) ,
windowCloseAt : z.date ( ) . nullable ( ) . optional ( ) ,
juryGroupId : z.string ( ) . nullable ( ) . optional ( ) ,
submissionWindowId : z.string ( ) . nullable ( ) . optional ( ) ,
purposeKey : z.string ( ) . nullable ( ) . optional ( ) ,
} )
)
. mutation ( async ( { ctx , input } ) = > {
// Verify competition exists
await ctx . prisma . competition . findUniqueOrThrow ( {
where : { id : input.competitionId } ,
} )
// Validate configJson against the Zod schema for this roundType
const config = input . configJson
? validateRoundConfig ( input . roundType , input . configJson )
: defaultRoundConfig ( input . roundType )
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
const round = await ctx . prisma . round . create ( {
data : {
competitionId : input.competitionId ,
name : input.name ,
slug : input.slug ,
roundType : input.roundType ,
sortOrder : input.sortOrder ,
configJson : config as unknown as Prisma . InputJsonValue ,
windowOpenAt : input.windowOpenAt ? ? undefined ,
windowCloseAt : input.windowCloseAt ? ? undefined ,
juryGroupId : input.juryGroupId ? ? undefined ,
submissionWindowId : input.submissionWindowId ? ? undefined ,
purposeKey : input.purposeKey ? ? undefined ,
} ,
} )
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
await logAudit ( {
prisma : ctx.prisma ,
userId : ctx.user.id ,
action : 'CREATE' ,
entityType : 'Round' ,
entityId : round.id ,
detailsJson : {
name : input.name ,
roundType : input.roundType ,
competitionId : input.competitionId ,
} ,
ipAddress : ctx.ip ,
userAgent : ctx.userAgent ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
} )
return round
} ) ,
/ * *
* Get round by ID with all relations
* /
getById : protectedProcedure
. input ( z . object ( { id : z.string ( ) } ) )
. query ( async ( { ctx , input } ) = > {
const round = await ctx . prisma . round . findUnique ( {
where : { id : input.id } ,
include : {
juryGroup : {
2026-02-17 14:27:01 +01:00
include : {
members : {
include : {
user : { select : { id : true , name : true , email : true } } ,
} ,
} ,
} ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
} ,
submissionWindow : {
include : { fileRequirements : true } ,
} ,
advancementRules : { orderBy : { sortOrder : 'asc' } } ,
visibleSubmissionWindows : {
include : { submissionWindow : true } ,
} ,
_count : {
select : { projectRoundStates : true } ,
} ,
} ,
} )
if ( ! round ) {
throw new TRPCError ( { code : 'NOT_FOUND' , message : 'Round not found' } )
}
return round
} ) ,
/ * *
* Update round settings / config
* /
update : adminProcedure
. input (
z . object ( {
id : z.string ( ) ,
name : z.string ( ) . min ( 1 ) . max ( 255 ) . optional ( ) ,
slug : z.string ( ) . min ( 1 ) . max ( 100 ) . regex ( /^[a-z0-9-]+$/ ) . optional ( ) ,
status : z.enum ( [ 'ROUND_DRAFT' , 'ROUND_ACTIVE' , 'ROUND_CLOSED' , 'ROUND_ARCHIVED' ] ) . optional ( ) ,
configJson : z.record ( z . unknown ( ) ) . optional ( ) ,
windowOpenAt : z.date ( ) . nullable ( ) . optional ( ) ,
windowCloseAt : z.date ( ) . nullable ( ) . optional ( ) ,
juryGroupId : z.string ( ) . nullable ( ) . optional ( ) ,
submissionWindowId : z.string ( ) . nullable ( ) . optional ( ) ,
purposeKey : z.string ( ) . nullable ( ) . optional ( ) ,
} )
)
. mutation ( async ( { ctx , input } ) = > {
const { id , configJson , . . . data } = input
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
const existing = await ctx . prisma . round . findUniqueOrThrow ( { where : { id } } )
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
// If configJson provided, validate it against the round type
let validatedConfig : Prisma.InputJsonValue | undefined
if ( configJson ) {
const parsed = validateRoundConfig ( existing . roundType , configJson )
validatedConfig = parsed as unknown as Prisma . InputJsonValue
}
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
const round = await ctx . prisma . round . update ( {
where : { id } ,
data : {
. . . data ,
. . . ( validatedConfig !== undefined ? { configJson : validatedConfig } : { } ) ,
} ,
} )
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
await logAudit ( {
prisma : ctx.prisma ,
userId : ctx.user.id ,
action : 'UPDATE' ,
entityType : 'Round' ,
entityId : id ,
detailsJson : {
changes : input ,
previous : {
name : existing.name ,
status : existing.status ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
} ,
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
} ,
ipAddress : ctx.ip ,
userAgent : ctx.userAgent ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
} )
return round
} ) ,
/ * *
* Reorder rounds within a competition
* /
updateOrder : adminProcedure
. input (
z . object ( {
competitionId : z.string ( ) ,
roundIds : z.array ( z . string ( ) ) ,
} )
)
. mutation ( async ( { ctx , input } ) = > {
return ctx . prisma . $transaction (
input . roundIds . map ( ( roundId , index ) = >
ctx . prisma . round . update ( {
where : { id : roundId } ,
data : { sortOrder : index } ,
} )
)
)
} ) ,
/ * *
* Delete a round
* /
delete : adminProcedure
. input ( z . object ( { id : z.string ( ) } ) )
. mutation ( async ( { ctx , input } ) = > {
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
const existing = await ctx . prisma . round . findUniqueOrThrow ( { where : { id : input.id } } )
await ctx . prisma . round . delete ( { where : { id : input.id } } )
await logAudit ( {
prisma : ctx.prisma ,
userId : ctx.user.id ,
action : 'DELETE' ,
entityType : 'Round' ,
entityId : input.id ,
detailsJson : {
name : existing.name ,
roundType : existing.roundType ,
competitionId : existing.competitionId ,
} ,
ipAddress : ctx.ip ,
userAgent : ctx.userAgent ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
} )
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
return existing
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
} ) ,
2026-02-16 09:20:02 +01:00
// =========================================================================
// Project Advancement (Manual Only)
// =========================================================================
/ * *
* Advance PASSED projects from one round to the next .
* This is ALWAYS manual — no auto - advancement after AI filtering .
* Admin must explicitly trigger this after reviewing results .
* /
advanceProjects : adminProcedure
. input (
z . object ( {
roundId : z.string ( ) ,
targetRoundId : z.string ( ) . optional ( ) ,
projectIds : z.array ( z . string ( ) ) . optional ( ) ,
2026-02-16 19:09:23 +01:00
autoPassPending : z.boolean ( ) . optional ( ) ,
2026-02-16 09:20:02 +01:00
} )
)
. mutation ( async ( { ctx , input } ) = > {
2026-02-16 19:09:23 +01:00
const { roundId , targetRoundId , projectIds , autoPassPending } = input
2026-02-16 09:20:02 +01:00
2026-02-19 12:59:35 +01:00
// Get current round with competition context + status
2026-02-16 09:20:02 +01:00
const currentRound = await ctx . prisma . round . findUniqueOrThrow ( {
where : { id : roundId } ,
2026-02-19 12:59:35 +01:00
select : { id : true , name : true , competitionId : true , sortOrder : true , status : true , configJson : true } ,
2026-02-16 09:20:02 +01:00
} )
2026-02-19 12:59:35 +01:00
// Validate: current round must be ROUND_ACTIVE or ROUND_CLOSED
if ( currentRound . status !== 'ROUND_ACTIVE' && currentRound . status !== 'ROUND_CLOSED' ) {
throw new TRPCError ( {
code : 'BAD_REQUEST' ,
message : ` Cannot advance from round with status ${ currentRound . status } . Round must be ROUND_ACTIVE or ROUND_CLOSED. ` ,
} )
}
2026-02-16 09:20:02 +01:00
// Determine target round
2026-02-19 12:59:35 +01:00
let targetRound : { id : string ; name : string ; competitionId : string ; sortOrder : number ; configJson : unknown }
2026-02-16 09:20:02 +01:00
if ( targetRoundId ) {
targetRound = await ctx . prisma . round . findUniqueOrThrow ( {
where : { id : targetRoundId } ,
2026-02-19 12:59:35 +01:00
select : { id : true , name : true , competitionId : true , sortOrder : true , configJson : true } ,
2026-02-16 09:20:02 +01:00
} )
2026-02-19 12:59:35 +01:00
// Validate: target must be in same competition
if ( targetRound . competitionId !== currentRound . competitionId ) {
throw new TRPCError ( {
code : 'BAD_REQUEST' ,
message : 'Target round must belong to the same competition as the source round.' ,
} )
}
// Validate: target must be after current round
if ( targetRound . sortOrder <= currentRound . sortOrder ) {
throw new TRPCError ( {
code : 'BAD_REQUEST' ,
message : 'Target round must come after the current round (higher sortOrder).' ,
} )
}
2026-02-16 09:20:02 +01:00
} else {
// Find next round in same competition by sortOrder
const nextRound = await ctx . prisma . round . findFirst ( {
where : {
competitionId : currentRound.competitionId ,
sortOrder : { gt : currentRound.sortOrder } ,
} ,
orderBy : { sortOrder : 'asc' } ,
2026-02-19 12:59:35 +01:00
select : { id : true , name : true , competitionId : true , sortOrder : true , configJson : true } ,
2026-02-16 09:20:02 +01:00
} )
if ( ! nextRound ) {
throw new TRPCError ( {
code : 'BAD_REQUEST' ,
message : 'No subsequent round exists in this competition. Create the next round first.' ,
} )
}
targetRound = nextRound
}
2026-02-19 12:59:35 +01:00
// Validate projectIds exist in current round if provided
2026-02-16 09:20:02 +01:00
if ( projectIds && projectIds . length > 0 ) {
2026-02-19 12:59:35 +01:00
const existingStates = await ctx . prisma . projectRoundState . findMany ( {
where : { roundId , projectId : { in : projectIds } } ,
2026-02-16 09:20:02 +01:00
select : { projectId : true } ,
} )
2026-02-19 12:59:35 +01:00
const existingIds = new Set ( existingStates . map ( ( s ) = > s . projectId ) )
const missing = projectIds . filter ( ( id ) = > ! existingIds . has ( id ) )
if ( missing . length > 0 ) {
throw new TRPCError ( {
code : 'BAD_REQUEST' ,
message : ` Projects not found in current round: ${ missing . join ( ', ' ) } ` ,
} )
}
2026-02-16 09:20:02 +01:00
}
2026-02-19 12:59:35 +01:00
// Transaction: auto-pass + create entries in target round + mark current as COMPLETED
let autoPassedCount = 0
let idsToAdvance : string [ ]
2026-02-16 09:20:02 +01:00
await ctx . prisma . $transaction ( async ( tx ) = > {
2026-02-19 12:59:35 +01:00
// Auto-pass all PENDING projects first (for intake/bulk workflows) — inside tx
if ( autoPassPending ) {
const result = await tx . projectRoundState . updateMany ( {
where : { roundId , state : 'PENDING' } ,
data : { state : 'PASSED' } ,
} )
autoPassedCount = result . count
}
// Determine which projects to advance
if ( projectIds && projectIds . length > 0 ) {
idsToAdvance = projectIds
} else {
// Default: all PASSED projects in current round
const passedStates = await tx . projectRoundState . findMany ( {
where : { roundId , state : 'PASSED' } ,
select : { projectId : true } ,
} )
idsToAdvance = passedStates . map ( ( s ) = > s . projectId )
}
if ( idsToAdvance . length === 0 ) return
2026-02-16 09:20:02 +01:00
// Create ProjectRoundState in target round
await tx . projectRoundState . createMany ( {
data : idsToAdvance.map ( ( projectId ) = > ( {
projectId ,
roundId : targetRound.id ,
} ) ) ,
skipDuplicates : true ,
} )
// Mark current round states as COMPLETED
await tx . projectRoundState . updateMany ( {
where : {
roundId ,
projectId : { in : idsToAdvance } ,
state : 'PASSED' ,
} ,
data : { state : 'COMPLETED' } ,
} )
// Update project status to ASSIGNED
await tx . project . updateMany ( {
where : { id : { in : idsToAdvance } } ,
data : { status : 'ASSIGNED' } ,
} )
// Status history
await tx . projectStatusHistory . createMany ( {
data : idsToAdvance.map ( ( projectId ) = > ( {
projectId ,
status : 'ASSIGNED' ,
changedBy : ctx.user?.id ,
} ) ) ,
} )
} )
2026-02-19 12:59:35 +01:00
// If nothing to advance (set inside tx), return early
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if ( ! idsToAdvance ! || idsToAdvance ! . length === 0 ) {
return { advancedCount : 0 , autoPassedCount , targetRoundId : targetRound.id , targetRoundName : targetRound.name }
}
2026-02-16 09:20:02 +01:00
// Audit
await logAudit ( {
prisma : ctx.prisma ,
userId : ctx.user.id ,
action : 'ADVANCE_PROJECTS' ,
entityType : 'Round' ,
entityId : roundId ,
detailsJson : {
fromRound : currentRound.name ,
toRound : targetRound.name ,
targetRoundId : targetRound.id ,
2026-02-19 12:59:35 +01:00
projectCount : idsToAdvance ! . length ,
2026-02-16 19:09:23 +01:00
autoPassedCount ,
2026-02-19 12:59:35 +01:00
projectIds : idsToAdvance ! ,
2026-02-16 09:20:02 +01:00
} ,
ipAddress : ctx.ip ,
userAgent : ctx.userAgent ,
} )
2026-02-19 12:59:35 +01:00
// Fix 5: notifyOnEntry — notify team members when projects enter target round
try {
const targetConfig = ( targetRound . configJson as Record < string , unknown > ) || { }
if ( targetConfig . notifyOnEntry ) {
const teamMembers = await ctx . prisma . teamMember . findMany ( {
where : { projectId : { in : idsToAdvance ! } } ,
select : { userId : true } ,
} )
const userIds = [ . . . new Set ( teamMembers . map ( ( tm ) = > tm . userId ) ) ]
if ( userIds . length > 0 ) {
void createBulkNotifications ( {
userIds ,
type : 'round_entry' ,
title : ` Projects entered: ${ targetRound . name } ` ,
message : ` Your project has been advanced to the round " ${ targetRound . name } ". ` ,
linkUrl : '/dashboard' ,
linkLabel : 'View Dashboard' ,
icon : 'ArrowRight' ,
} )
}
}
} catch ( notifyErr ) {
console . error ( '[advanceProjects] notifyOnEntry notification failed (non-fatal):' , notifyErr )
}
// Fix 6: notifyOnAdvance — notify applicants from source round that projects advanced
try {
const sourceConfig = ( currentRound . configJson as Record < string , unknown > ) || { }
if ( sourceConfig . notifyOnAdvance ) {
const projects = await ctx . prisma . project . findMany ( {
where : { id : { in : idsToAdvance ! } } ,
select : {
id : true ,
title : true ,
submittedByEmail : true ,
teamMembers : {
select : { user : { select : { id : true , email : true , name : true } } } ,
} ,
} ,
} )
// Collect unique user IDs for in-app notifications
const applicantUserIds = new Set < string > ( )
for ( const project of projects ) {
for ( const tm of project . teamMembers ) {
applicantUserIds . add ( tm . user . id )
}
}
if ( applicantUserIds . size > 0 ) {
void createBulkNotifications ( {
userIds : [ . . . applicantUserIds ] ,
type : 'project_advanced' ,
title : 'Your project has advanced!' ,
message : ` Congratulations! Your project has advanced from " ${ currentRound . name } " to " ${ targetRound . name } ". ` ,
linkUrl : '/dashboard' ,
linkLabel : 'View Dashboard' ,
icon : 'Trophy' ,
priority : 'high' ,
} )
}
// Send emails to team members (fire-and-forget)
for ( const project of projects ) {
const recipients = new Map < string , string | null > ( )
for ( const tm of project . teamMembers ) {
if ( tm . user . email ) recipients . set ( tm . user . email , tm . user . name )
}
if ( recipients . size === 0 && project . submittedByEmail ) {
recipients . set ( project . submittedByEmail , null )
}
for ( const [ email , name ] of recipients ) {
void sendAnnouncementEmail (
email ,
name ,
` Your project has advanced to: ${ targetRound . name } ` ,
` Congratulations! Your project " ${ project . title } " has advanced from " ${ currentRound . name } " to " ${ targetRound . name } " in the Monaco Ocean Protection Challenge. ` ,
'View Your Dashboard' ,
2026-02-23 14:27:58 +01:00
` ${ process . env . NEXTAUTH_URL || 'https://portal.monaco-opc.com' } /dashboard ` ,
2026-02-19 12:59:35 +01:00
) . catch ( ( err ) = > {
console . error ( ` [advanceProjects] notifyOnAdvance email failed for ${ email } : ` , err )
} )
}
}
}
} catch ( notifyErr ) {
console . error ( '[advanceProjects] notifyOnAdvance notification failed (non-fatal):' , notifyErr )
}
2026-02-16 09:20:02 +01:00
return {
2026-02-19 12:59:35 +01:00
advancedCount : idsToAdvance ! . length ,
2026-02-16 19:09:23 +01:00
autoPassedCount ,
2026-02-16 09:20:02 +01:00
targetRoundId : targetRound.id ,
targetRoundName : targetRound.name ,
}
} ) ,
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
// =========================================================================
// AI Shortlist Recommendations
// =========================================================================
/ * *
* Generate AI - powered shortlist recommendations for a round .
* Runs independently for STARTUP and BUSINESS_CONCEPT categories .
* Uses per - round config for advancement targets and file parsing .
* /
generateAIRecommendations : adminProcedure
. input (
z . object ( {
roundId : z.string ( ) ,
rubric : z.string ( ) . optional ( ) ,
} )
)
. mutation ( async ( { ctx , input } ) = > {
const round = await ctx . prisma . round . findUniqueOrThrow ( {
where : { id : input.roundId } ,
select : {
id : true ,
name : true ,
competitionId : true ,
configJson : true ,
} ,
} )
const config = ( round . configJson as Record < string , unknown > ) ? ? { }
const startupTopN = ( config . startupAdvanceCount as number ) || 10
const conceptTopN = ( config . conceptAdvanceCount as number ) || 10
const aiParseFiles = ! ! config . aiParseFiles
const result = await generateShortlist (
{
roundId : input.roundId ,
competitionId : round.competitionId ,
startupTopN ,
conceptTopN ,
rubric : input.rubric ,
aiParseFiles ,
} ,
ctx . prisma ,
)
await logAudit ( {
prisma : ctx.prisma ,
userId : ctx.user.id ,
action : 'AI_SHORTLIST' ,
entityType : 'Round' ,
entityId : input.roundId ,
detailsJson : {
roundName : round.name ,
startupTopN ,
conceptTopN ,
aiParseFiles ,
success : result.success ,
startupCount : result.recommendations.STARTUP.length ,
conceptCount : result.recommendations.BUSINESS_CONCEPT.length ,
tokensUsed : result.tokensUsed ,
errors : result.errors ,
} ,
ipAddress : ctx.ip ,
userAgent : ctx.userAgent ,
} )
return result
} ) ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
// =========================================================================
// Submission Window Management
// =========================================================================
/ * *
* Create a submission window for a round
* /
createSubmissionWindow : adminProcedure
. input (
z . object ( {
competitionId : z.string ( ) ,
name : z.string ( ) . min ( 1 ) . max ( 255 ) ,
slug : z.string ( ) . min ( 1 ) . max ( 100 ) . regex ( /^[a-z0-9-]+$/ ) ,
roundNumber : z.number ( ) . int ( ) . min ( 1 ) ,
windowOpenAt : z.date ( ) . optional ( ) ,
windowCloseAt : z.date ( ) . optional ( ) ,
deadlinePolicy : z.enum ( [ 'HARD_DEADLINE' , 'FLAG' , 'GRACE' ] ) . default ( 'HARD_DEADLINE' ) ,
graceHours : z.number ( ) . int ( ) . min ( 0 ) . optional ( ) ,
lockOnClose : z.boolean ( ) . default ( true ) ,
} )
)
. mutation ( async ( { ctx , input } ) = > {
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
const window = await ctx . prisma . submissionWindow . create ( {
data : {
competitionId : input.competitionId ,
name : input.name ,
slug : input.slug ,
roundNumber : input.roundNumber ,
windowOpenAt : input.windowOpenAt ,
windowCloseAt : input.windowCloseAt ,
deadlinePolicy : input.deadlinePolicy ,
graceHours : input.graceHours ,
lockOnClose : input.lockOnClose ,
} ,
} )
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
await logAudit ( {
prisma : ctx.prisma ,
userId : ctx.user.id ,
action : 'CREATE' ,
entityType : 'SubmissionWindow' ,
entityId : window.id ,
detailsJson : { name : input.name , competitionId : input.competitionId } ,
ipAddress : ctx.ip ,
userAgent : ctx.userAgent ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
} )
return window
} ) ,
Rounds overhaul: full CRUD submission windows, scheduling UI, analytics, design refresh
- Fix special award FK crash: replace 4x raw auditLog.create with logAudit() helper
- Add updateSubmissionWindow + deleteSubmissionWindow mutations to round router
- Add per-round analytics (_count, juryGroup) to competition.getById
- Remove redundant acceptedCategories from intake config
- Rewrite submission window manager with full CRUD, all fields, date pickers
- Add round scheduling card (open/close dates) to round detail page
- Add project count, assignment count, jury group to round list cards
- Visual redesign: pipeline view, brand colors, progress bars, enhanced cards
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:07:09 +01:00
/ * *
* Update an existing submission window
* /
updateSubmissionWindow : adminProcedure
. input (
z . object ( {
id : z.string ( ) ,
name : z.string ( ) . min ( 1 ) . max ( 255 ) . optional ( ) ,
slug : z.string ( ) . min ( 1 ) . max ( 100 ) . regex ( /^[a-z0-9-]+$/ ) . optional ( ) ,
roundNumber : z.number ( ) . int ( ) . min ( 1 ) . optional ( ) ,
windowOpenAt : z.date ( ) . nullable ( ) . optional ( ) ,
windowCloseAt : z.date ( ) . nullable ( ) . optional ( ) ,
deadlinePolicy : z.enum ( [ 'HARD_DEADLINE' , 'FLAG' , 'GRACE' ] ) . optional ( ) ,
graceHours : z.number ( ) . int ( ) . min ( 0 ) . nullable ( ) . optional ( ) ,
lockOnClose : z.boolean ( ) . optional ( ) ,
sortOrder : z.number ( ) . int ( ) . optional ( ) ,
} )
)
. mutation ( async ( { ctx , input } ) = > {
const { id , . . . data } = input
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
const window = await ctx . prisma . submissionWindow . update ( {
where : { id } ,
data ,
} )
await logAudit ( {
prisma : ctx.prisma ,
userId : ctx.user.id ,
action : 'UPDATE' ,
entityType : 'SubmissionWindow' ,
entityId : id ,
detailsJson : data ,
ipAddress : ctx.ip ,
userAgent : ctx.userAgent ,
Rounds overhaul: full CRUD submission windows, scheduling UI, analytics, design refresh
- Fix special award FK crash: replace 4x raw auditLog.create with logAudit() helper
- Add updateSubmissionWindow + deleteSubmissionWindow mutations to round router
- Add per-round analytics (_count, juryGroup) to competition.getById
- Remove redundant acceptedCategories from intake config
- Rewrite submission window manager with full CRUD, all fields, date pickers
- Add round scheduling card (open/close dates) to round detail page
- Add project count, assignment count, jury group to round list cards
- Visual redesign: pipeline view, brand colors, progress bars, enhanced cards
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:07:09 +01:00
} )
return window
} ) ,
/ * *
* Delete a submission window ( only if no files uploaded )
* /
deleteSubmissionWindow : adminProcedure
. input ( z . object ( { id : z.string ( ) } ) )
. mutation ( async ( { ctx , input } ) = > {
// Check if window has uploaded files
const window = await ctx . prisma . submissionWindow . findUniqueOrThrow ( {
where : { id : input.id } ,
select : { id : true , name : true , _count : { select : { projectFiles : true } } } ,
} )
if ( window . _count . projectFiles > 0 ) {
throw new TRPCError ( {
code : 'BAD_REQUEST' ,
message : ` Cannot delete window " ${ window . name } " — it has ${ window . _count . projectFiles } uploaded files. Remove files first. ` ,
} )
}
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
await ctx . prisma . submissionWindow . delete ( { where : { id : input.id } } )
await logAudit ( {
prisma : ctx.prisma ,
userId : ctx.user.id ,
action : 'DELETE' ,
entityType : 'SubmissionWindow' ,
entityId : input.id ,
detailsJson : { name : window.name } ,
ipAddress : ctx.ip ,
userAgent : ctx.userAgent ,
Rounds overhaul: full CRUD submission windows, scheduling UI, analytics, design refresh
- Fix special award FK crash: replace 4x raw auditLog.create with logAudit() helper
- Add updateSubmissionWindow + deleteSubmissionWindow mutations to round router
- Add per-round analytics (_count, juryGroup) to competition.getById
- Remove redundant acceptedCategories from intake config
- Rewrite submission window manager with full CRUD, all fields, date pickers
- Add round scheduling card (open/close dates) to round detail page
- Add project count, assignment count, jury group to round list cards
- Visual redesign: pipeline view, brand colors, progress bars, enhanced cards
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:07:09 +01:00
} )
return { success : true }
} ) ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
/ * *
* Open a submission window
* /
openSubmissionWindow : adminProcedure
. input ( z . object ( { windowId : z.string ( ) } ) )
. mutation ( async ( { ctx , input } ) = > {
const result = await openWindow ( input . windowId , ctx . user . id , ctx . prisma )
if ( ! result . success ) {
throw new TRPCError ( {
code : 'BAD_REQUEST' ,
message : result.errors?.join ( '; ' ) ? ? 'Failed to open window' ,
} )
}
return result
} ) ,
/ * *
* Close a submission window
* /
closeSubmissionWindow : adminProcedure
. input ( z . object ( { windowId : z.string ( ) } ) )
. mutation ( async ( { ctx , input } ) = > {
const result = await closeWindow ( input . windowId , ctx . user . id , ctx . prisma )
if ( ! result . success ) {
throw new TRPCError ( {
code : 'BAD_REQUEST' ,
message : result.errors?.join ( '; ' ) ? ? 'Failed to close window' ,
} )
}
return result
} ) ,
/ * *
* Lock a submission window
* /
lockSubmissionWindow : adminProcedure
. input ( z . object ( { windowId : z.string ( ) } ) )
. mutation ( async ( { ctx , input } ) = > {
const result = await lockWindow ( input . windowId , ctx . user . id , ctx . prisma )
if ( ! result . success ) {
throw new TRPCError ( {
code : 'BAD_REQUEST' ,
message : result.errors?.join ( '; ' ) ? ? 'Failed to lock window' ,
} )
}
return result
} ) ,
/ * *
* Check deadline status of a window
* /
checkDeadline : protectedProcedure
. input ( z . object ( { windowId : z.string ( ) } ) )
. query ( async ( { ctx , input } ) = > {
return checkDeadlinePolicy ( input . windowId , ctx . prisma )
} ) ,
/ * *
* Validate files against window requirements
* /
validateSubmission : protectedProcedure
. input (
z . object ( {
projectId : z.string ( ) ,
windowId : z.string ( ) ,
files : z.array (
z . object ( {
mimeType : z.string ( ) ,
size : z.number ( ) ,
requirementId : z.string ( ) . optional ( ) ,
} )
) ,
} )
)
. mutation ( async ( { ctx , input } ) = > {
return validateSubmission ( input . projectId , input . windowId , input . files , ctx . prisma )
} ) ,
/ * *
* Get visible submission windows for a round
* /
getVisibleWindows : protectedProcedure
. input ( z . object ( { roundId : z.string ( ) } ) )
. query ( async ( { ctx , input } ) = > {
return getVisibleWindows ( input . roundId , ctx . prisma )
} ) ,
// =========================================================================
// File Requirements Management
// =========================================================================
/ * *
* Create a file requirement for a submission window
* /
createFileRequirement : adminProcedure
. input (
z . object ( {
submissionWindowId : z.string ( ) ,
slug : z.string ( ) . min ( 1 ) . max ( 100 ) . regex ( /^[a-z0-9-]+$/ ) ,
label : z.string ( ) . min ( 1 ) . max ( 255 ) ,
description : z.string ( ) . max ( 2000 ) . optional ( ) ,
mimeTypes : z.array ( z . string ( ) ) . default ( [ ] ) ,
maxSizeMb : z.number ( ) . int ( ) . min ( 0 ) . optional ( ) ,
required : z.boolean ( ) . default ( false ) ,
sortOrder : z.number ( ) . int ( ) . default ( 0 ) ,
} )
)
. mutation ( async ( { ctx , input } ) = > {
return ctx . prisma . submissionFileRequirement . create ( {
data : input ,
} )
} ) ,
/ * *
* Update a file requirement
* /
updateFileRequirement : adminProcedure
. input (
z . object ( {
id : z.string ( ) ,
label : z.string ( ) . min ( 1 ) . max ( 255 ) . optional ( ) ,
description : z.string ( ) . max ( 2000 ) . optional ( ) . nullable ( ) ,
mimeTypes : z.array ( z . string ( ) ) . optional ( ) ,
maxSizeMb : z.number ( ) . min ( 0 ) . optional ( ) . nullable ( ) ,
required : z.boolean ( ) . optional ( ) ,
sortOrder : z.number ( ) . int ( ) . optional ( ) ,
} )
)
. mutation ( async ( { ctx , input } ) = > {
const { id , . . . data } = input
return ctx . prisma . submissionFileRequirement . update ( {
where : { id } ,
data ,
} )
} ) ,
/ * *
* Delete a file requirement
* /
deleteFileRequirement : adminProcedure
. input ( z . object ( { id : z.string ( ) } ) )
. mutation ( async ( { ctx , input } ) = > {
return ctx . prisma . submissionFileRequirement . delete ( {
where : { id : input.id } ,
} )
} ) ,
/ * *
* Get submission windows for applicants in a competition
* /
getApplicantWindows : protectedProcedure
. input ( z . object ( { competitionId : z.string ( ) } ) )
. query ( async ( { ctx , input } ) = > {
return ctx . prisma . submissionWindow . findMany ( {
where : { competitionId : input.competitionId } ,
include : {
fileRequirements : { orderBy : { sortOrder : 'asc' } } ,
} ,
orderBy : { sortOrder : 'asc' } ,
} )
} ) ,
2026-03-01 14:47:42 +01:00
/ * *
* Get the most recent SUBMISSION round config for a program .
* Used on the project edit page to show required document slots .
* /
getSubmissionRoundForProgram : adminProcedure
. input ( z . object ( { programId : z.string ( ) } ) )
. query ( async ( { ctx , input } ) = > {
const round = await ctx . prisma . round . findFirst ( {
where : {
roundType : 'SUBMISSION' ,
competition : { programId : input.programId } ,
} ,
select : {
id : true ,
name : true ,
configJson : true ,
} ,
orderBy : { sortOrder : 'desc' } ,
} )
return round ? ? null
} ) ,
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
} )