Add Anthropic API, test environment, remove locale settings
Feature 1: Anthropic API Integration - Add @anthropic-ai/sdk with adapter wrapping OpenAI-shaped interface - Support Claude models (opus, sonnet, haiku) with extended thinking - Auto-reset model on provider switch, JSON retry logic - Add Claude model pricing to ai-usage tracker - Update AI settings form with Anthropic provider option Feature 2: Remove Locale Settings UI - Strip Localization tab from admin settings - Remove i18n settings from router inferCategory and getFeatureFlags - Keep franc document language detection intact Feature 3: Test Environment with Role Impersonation - Add isTest field to User, Program, Project, Competition models - Test environment service: create/teardown with realistic dummy data - JWT-based impersonation for test users (@test.local emails) - Impersonation banner with quick-switch between test roles - Test environment panel in admin settings (SUPER_ADMIN only) - Email redirect: @test.local emails routed to admin with [TEST] prefix - Complete data isolation: 45+ isTest:false filters across platform - All global queries on User/Project/Program/Competition - AI services blocked from processing test data - Cron jobs skip test rounds/users - Analytics/exports exclude test data - Admin layout/pickers hide test programs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,7 @@ import { roundEngineRouter } from './roundEngine'
|
||||
import { roundAssignmentRouter } from './roundAssignment'
|
||||
import { deliberationRouter } from './deliberation'
|
||||
import { resultLockRouter } from './resultLock'
|
||||
import { testEnvironmentRouter } from './testEnvironment'
|
||||
|
||||
/**
|
||||
* Root tRPC router that combines all domain routers
|
||||
@@ -108,6 +109,8 @@ export const appRouter = router({
|
||||
roundAssignment: roundAssignmentRouter,
|
||||
deliberation: deliberationRouter,
|
||||
resultLock: resultLockRouter,
|
||||
// Test environment
|
||||
testEnvironment: testEnvironmentRouter,
|
||||
})
|
||||
|
||||
export type AppRouter = typeof appRouter
|
||||
|
||||
@@ -12,8 +12,8 @@ const editionOrRoundInput = z.object({
|
||||
})
|
||||
|
||||
function projectWhere(input: { roundId?: string; programId?: string }) {
|
||||
if (input.roundId) return { projectRoundStates: { some: { roundId: input.roundId } } }
|
||||
return { programId: input.programId! }
|
||||
if (input.roundId) return { isTest: false, projectRoundStates: { some: { roundId: input.roundId } } }
|
||||
return { isTest: false, programId: input.programId! }
|
||||
}
|
||||
|
||||
function assignmentWhere(input: { roundId?: string; programId?: string }) {
|
||||
@@ -263,7 +263,7 @@ export const analyticsRouter = router({
|
||||
if (round?.roundType === 'EVALUATION') {
|
||||
// For evaluation rounds, break down by evaluation status per project
|
||||
const projects = await ctx.prisma.projectRoundState.findMany({
|
||||
where: { roundId: input.roundId },
|
||||
where: { roundId: input.roundId, project: { isTest: false } },
|
||||
select: {
|
||||
projectId: true,
|
||||
project: {
|
||||
@@ -309,7 +309,7 @@ export const analyticsRouter = router({
|
||||
// Non-evaluation rounds: use ProjectRoundState
|
||||
const states = await ctx.prisma.projectRoundState.groupBy({
|
||||
by: ['state'],
|
||||
where: { roundId: input.roundId },
|
||||
where: { roundId: input.roundId, project: { isTest: false } },
|
||||
_count: true,
|
||||
})
|
||||
return states.map((s) => ({
|
||||
@@ -469,8 +469,8 @@ export const analyticsRouter = router({
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const where = input.roundId
|
||||
? { assignments: { some: { roundId: input.roundId } } }
|
||||
: { programId: input.programId }
|
||||
? { isTest: false, assignments: { some: { roundId: input.roundId } } }
|
||||
: { isTest: false, programId: input.programId }
|
||||
|
||||
const distribution = await ctx.prisma.project.groupBy({
|
||||
by: ['country'],
|
||||
@@ -537,7 +537,7 @@ export const analyticsRouter = router({
|
||||
|
||||
// Count distinct projects per round via assignments
|
||||
const projectAssignments = await ctx.prisma.assignment.findMany({
|
||||
where: { roundId: { in: roundIds } },
|
||||
where: { roundId: { in: roundIds }, project: { isTest: false } },
|
||||
select: { roundId: true, projectId: true },
|
||||
distinct: ['roundId', 'projectId'],
|
||||
})
|
||||
@@ -714,12 +714,14 @@ export const analyticsRouter = router({
|
||||
const roundId = input?.roundId
|
||||
|
||||
const projectFilter = roundId
|
||||
? { projectRoundStates: { some: { roundId } } }
|
||||
: {}
|
||||
const assignmentFilter = roundId ? { roundId } : {}
|
||||
? { isTest: false, projectRoundStates: { some: { roundId } } }
|
||||
: { isTest: false }
|
||||
const assignmentFilter = roundId
|
||||
? { roundId }
|
||||
: { round: { competition: { isTest: false } } }
|
||||
const evalFilter = roundId
|
||||
? { assignment: { roundId }, status: 'SUBMITTED' as const }
|
||||
: { status: 'SUBMITTED' as const }
|
||||
: { assignment: { round: { competition: { isTest: false } } }, status: 'SUBMITTED' as const }
|
||||
|
||||
const [
|
||||
programCount,
|
||||
@@ -730,9 +732,9 @@ export const analyticsRouter = router({
|
||||
totalAssignments,
|
||||
evaluationScores,
|
||||
] = await Promise.all([
|
||||
ctx.prisma.program.count(),
|
||||
ctx.prisma.program.count({ where: { isTest: false } }),
|
||||
ctx.prisma.round.findMany({
|
||||
where: { status: 'ROUND_ACTIVE' },
|
||||
where: { status: 'ROUND_ACTIVE', competition: { isTest: false } },
|
||||
select: { id: true, name: true },
|
||||
take: 5,
|
||||
}),
|
||||
@@ -743,7 +745,7 @@ export const analyticsRouter = router({
|
||||
select: { userId: true },
|
||||
distinct: ['userId'],
|
||||
}).then((rows) => rows.length)
|
||||
: ctx.prisma.user.count({ where: { role: 'JURY_MEMBER', status: 'ACTIVE' } }),
|
||||
: ctx.prisma.user.count({ where: { isTest: false, role: 'JURY_MEMBER', status: 'ACTIVE' } }),
|
||||
ctx.prisma.evaluation.count({ where: evalFilter }),
|
||||
ctx.prisma.assignment.count({ where: assignmentFilter }),
|
||||
ctx.prisma.evaluation.findMany({
|
||||
@@ -988,7 +990,7 @@ export const analyticsRouter = router({
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const where: Record<string, unknown> = {}
|
||||
const where: Record<string, unknown> = { isTest: false }
|
||||
|
||||
if (input.roundId) {
|
||||
where.projectRoundStates = { some: { roundId: input.roundId } }
|
||||
@@ -1151,15 +1153,15 @@ export const analyticsRouter = router({
|
||||
switch (roundType) {
|
||||
case 'INTAKE': {
|
||||
const [total, byState, byCategory] = await Promise.all([
|
||||
ctx.prisma.projectRoundState.count({ where: { roundId: input.roundId } }),
|
||||
ctx.prisma.projectRoundState.count({ where: { roundId: input.roundId, project: { isTest: false } } }),
|
||||
ctx.prisma.projectRoundState.groupBy({
|
||||
by: ['state'],
|
||||
where: { roundId: input.roundId },
|
||||
where: { roundId: input.roundId, project: { isTest: false } },
|
||||
_count: true,
|
||||
}),
|
||||
ctx.prisma.project.groupBy({
|
||||
by: ['competitionCategory'],
|
||||
where: { projectRoundStates: { some: { roundId: input.roundId } } },
|
||||
where: { isTest: false, projectRoundStates: { some: { roundId: input.roundId } } },
|
||||
_count: true,
|
||||
}),
|
||||
])
|
||||
@@ -1395,7 +1397,7 @@ export const analyticsRouter = router({
|
||||
// Get competition rounds for file grouping
|
||||
let competitionRounds: { id: string; name: string; roundType: string }[] = []
|
||||
const competition = await ctx.prisma.competition.findFirst({
|
||||
where: { programId: projectRaw.programId },
|
||||
where: { programId: projectRaw.programId, isTest: false },
|
||||
include: { rounds: { select: { id: true, name: true, roundType: true }, orderBy: { sortOrder: 'asc' } } },
|
||||
})
|
||||
if (competition) {
|
||||
@@ -1478,9 +1480,23 @@ export const analyticsRouter = router({
|
||||
.query(async ({ ctx, input }) => {
|
||||
const limit = input?.limit ?? 10
|
||||
|
||||
// Exclude actions performed by test users
|
||||
const testUserIds = await ctx.prisma.user.findMany({
|
||||
where: { isTest: true },
|
||||
select: { id: true },
|
||||
}).then((users) => users.map((u) => u.id))
|
||||
|
||||
const entries = await ctx.prisma.decisionAuditLog.findMany({
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: limit,
|
||||
...(testUserIds.length > 0 && {
|
||||
where: {
|
||||
OR: [
|
||||
{ actorId: null },
|
||||
{ actorId: { notIn: testUserIds } },
|
||||
],
|
||||
},
|
||||
}),
|
||||
select: {
|
||||
id: true,
|
||||
eventType: true,
|
||||
@@ -1496,7 +1512,7 @@ export const analyticsRouter = router({
|
||||
const actorIds = [...new Set(entries.map((e) => e.actorId).filter(Boolean))] as string[]
|
||||
const actors = actorIds.length > 0
|
||||
? await ctx.prisma.user.findMany({
|
||||
where: { id: { in: actorIds } },
|
||||
where: { id: { in: actorIds }, isTest: false },
|
||||
select: { id: true, name: true },
|
||||
})
|
||||
: []
|
||||
|
||||
@@ -105,7 +105,7 @@ export const applicationRouter = router({
|
||||
if (input.mode === 'edition') {
|
||||
// Edition-wide application mode
|
||||
const program = await ctx.prisma.program.findFirst({
|
||||
where: { slug: input.slug },
|
||||
where: { slug: input.slug, isTest: false },
|
||||
})
|
||||
|
||||
if (!program) {
|
||||
@@ -687,6 +687,7 @@ export const applicationRouter = router({
|
||||
const projects = await ctx.prisma.project.findMany({
|
||||
where: {
|
||||
isDraft: true,
|
||||
isTest: false,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -837,6 +838,7 @@ export const applicationRouter = router({
|
||||
const projects = await ctx.prisma.project.findMany({
|
||||
where: {
|
||||
isDraft: true,
|
||||
isTest: false,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -560,6 +560,7 @@ async function runAIAssignmentJob(jobId: string, roundId: string, userId: string
|
||||
where: {
|
||||
role: 'JURY_MEMBER',
|
||||
status: 'ACTIVE',
|
||||
isTest: false,
|
||||
...(scopedJurorIds ? { id: { in: scopedJurorIds } } : {}),
|
||||
},
|
||||
select: {
|
||||
@@ -1255,6 +1256,7 @@ export const assignmentRouter = router({
|
||||
where: {
|
||||
role: 'JURY_MEMBER',
|
||||
status: 'ACTIVE',
|
||||
isTest: false,
|
||||
...(scopedJurorIds ? { id: { in: scopedJurorIds } } : {}),
|
||||
},
|
||||
select: {
|
||||
|
||||
@@ -142,7 +142,7 @@ export const competitionRouter = router({
|
||||
.input(z.object({ programId: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
return ctx.prisma.competition.findMany({
|
||||
where: { programId: input.programId },
|
||||
where: { programId: input.programId, isTest: false },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
_count: {
|
||||
@@ -254,7 +254,7 @@ export const competitionRouter = router({
|
||||
const competitionIds = [...new Set(memberships.map((m) => m.juryGroup.competitionId))]
|
||||
if (competitionIds.length === 0) return []
|
||||
return ctx.prisma.competition.findMany({
|
||||
where: { id: { in: competitionIds }, status: { not: 'ARCHIVED' } },
|
||||
where: { id: { in: competitionIds }, status: { not: 'ARCHIVED' }, isTest: false },
|
||||
include: {
|
||||
rounds: {
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
|
||||
@@ -172,18 +172,19 @@ export const dashboardRouter = router({
|
||||
|
||||
// 7. Project count
|
||||
ctx.prisma.project.count({
|
||||
where: { programId: editionId },
|
||||
where: { programId: editionId, isTest: false },
|
||||
}),
|
||||
|
||||
// 8. New projects this week
|
||||
ctx.prisma.project.count({
|
||||
where: { programId: editionId, createdAt: { gte: sevenDaysAgo } },
|
||||
where: { programId: editionId, isTest: false, createdAt: { gte: sevenDaysAgo } },
|
||||
}),
|
||||
|
||||
// 9. Total jurors
|
||||
ctx.prisma.user.count({
|
||||
where: {
|
||||
role: 'JURY_MEMBER',
|
||||
isTest: false,
|
||||
status: { in: ['ACTIVE', 'INVITED', 'NONE'] },
|
||||
assignments: { some: { round: { competition: { programId: editionId } } } },
|
||||
},
|
||||
@@ -193,6 +194,7 @@ export const dashboardRouter = router({
|
||||
ctx.prisma.user.count({
|
||||
where: {
|
||||
role: 'JURY_MEMBER',
|
||||
isTest: false,
|
||||
status: 'ACTIVE',
|
||||
assignments: { some: { round: { competition: { programId: editionId } } } },
|
||||
},
|
||||
@@ -212,7 +214,7 @@ export const dashboardRouter = router({
|
||||
|
||||
// 13. Latest projects
|
||||
ctx.prisma.project.findMany({
|
||||
where: { programId: editionId },
|
||||
where: { programId: editionId, isTest: false },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 8,
|
||||
select: {
|
||||
@@ -232,20 +234,20 @@ export const dashboardRouter = router({
|
||||
// 14. Category breakdown
|
||||
ctx.prisma.project.groupBy({
|
||||
by: ['competitionCategory'],
|
||||
where: { programId: editionId },
|
||||
where: { programId: editionId, isTest: false },
|
||||
_count: true,
|
||||
}),
|
||||
|
||||
// 15. Ocean issue breakdown
|
||||
ctx.prisma.project.groupBy({
|
||||
by: ['oceanIssue'],
|
||||
where: { programId: editionId },
|
||||
where: { programId: editionId, isTest: false },
|
||||
_count: true,
|
||||
}),
|
||||
|
||||
// 16. Recent activity
|
||||
// 16. Recent activity (exclude test user actions)
|
||||
ctx.prisma.auditLog.findMany({
|
||||
where: { timestamp: { gte: sevenDaysAgo } },
|
||||
where: { timestamp: { gte: sevenDaysAgo }, user: { isTest: false } },
|
||||
orderBy: { timestamp: 'desc' },
|
||||
take: 8,
|
||||
select: {
|
||||
|
||||
@@ -105,6 +105,7 @@ export const exportRouter = router({
|
||||
.query(async ({ ctx, input }) => {
|
||||
const projects = await ctx.prisma.project.findMany({
|
||||
where: {
|
||||
isTest: false,
|
||||
assignments: { some: { roundId: input.roundId } },
|
||||
},
|
||||
include: {
|
||||
@@ -355,7 +356,7 @@ export const exportRouter = router({
|
||||
}
|
||||
|
||||
const logs = await ctx.prisma.auditLog.findMany({
|
||||
where,
|
||||
where: { ...where, user: { isTest: false } },
|
||||
orderBy: { timestamp: 'desc' },
|
||||
include: {
|
||||
user: { select: { name: true, email: true } },
|
||||
@@ -431,7 +432,7 @@ export const exportRouter = router({
|
||||
if (includeSection('summary')) {
|
||||
const [projectCount, assignmentCount, evaluationCount, jurorCount] = await Promise.all([
|
||||
ctx.prisma.project.count({
|
||||
where: { assignments: { some: { roundId: input.roundId } } },
|
||||
where: { isTest: false, assignments: { some: { roundId: input.roundId } } },
|
||||
}),
|
||||
ctx.prisma.assignment.count({ where: { roundId: input.roundId } }),
|
||||
ctx.prisma.evaluation.count({
|
||||
@@ -486,7 +487,7 @@ export const exportRouter = router({
|
||||
// Rankings
|
||||
if (includeSection('rankings')) {
|
||||
const projects = await ctx.prisma.project.findMany({
|
||||
where: { assignments: { some: { roundId: input.roundId } } },
|
||||
where: { isTest: false, assignments: { some: { roundId: input.roundId } } },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
|
||||
@@ -994,6 +994,7 @@ export const fileRouter = router({
|
||||
// Build project filter
|
||||
const projectWhere: Record<string, unknown> = {
|
||||
programId: window.competition.programId,
|
||||
isTest: false,
|
||||
}
|
||||
if (input.search) {
|
||||
projectWhere.OR = [
|
||||
@@ -1303,6 +1304,7 @@ export const fileRouter = router({
|
||||
// Build project filter
|
||||
const projectWhere: Record<string, unknown> = {
|
||||
programId: round.competition.programId,
|
||||
isTest: false,
|
||||
}
|
||||
if (input.search) {
|
||||
projectWhere.OR = [
|
||||
|
||||
@@ -115,6 +115,7 @@ export async function runFilteringJob(jobId: string, roundId: string, userId: st
|
||||
roundId,
|
||||
exitedAt: null,
|
||||
state: { in: ['PENDING', 'IN_PROGRESS'] },
|
||||
project: { isTest: false },
|
||||
},
|
||||
include: {
|
||||
project: {
|
||||
|
||||
@@ -420,6 +420,7 @@ export const mentorRouter = router({
|
||||
const projects = await ctx.prisma.project.findMany({
|
||||
where: {
|
||||
programId: input.programId,
|
||||
isTest: false,
|
||||
mentorAssignment: null,
|
||||
wantsMentorship: true,
|
||||
},
|
||||
|
||||
@@ -402,7 +402,7 @@ async function resolveRecipients(
|
||||
const role = filter?.role as string
|
||||
if (!role) return []
|
||||
const users = await prisma.user.findMany({
|
||||
where: { role: role as any, status: 'ACTIVE' },
|
||||
where: { role: role as any, status: 'ACTIVE', isTest: false },
|
||||
select: { id: true },
|
||||
})
|
||||
return users.map((u) => u.id)
|
||||
@@ -412,7 +412,7 @@ async function resolveRecipients(
|
||||
const targetRoundId = roundId || (filter?.roundId as string)
|
||||
if (!targetRoundId) return []
|
||||
const assignments = await prisma.assignment.findMany({
|
||||
where: { roundId: targetRoundId },
|
||||
where: { roundId: targetRoundId, user: { isTest: false } },
|
||||
select: { userId: true },
|
||||
distinct: ['userId'],
|
||||
})
|
||||
@@ -423,7 +423,7 @@ async function resolveRecipients(
|
||||
const programId = filter?.programId as string
|
||||
if (!programId) return []
|
||||
const projects = await prisma.project.findMany({
|
||||
where: { programId },
|
||||
where: { programId, isTest: false },
|
||||
select: { submittedByUserId: true },
|
||||
})
|
||||
const ids = new Set(projects.map((p) => p.submittedByUserId).filter(Boolean) as string[])
|
||||
@@ -432,7 +432,7 @@ async function resolveRecipients(
|
||||
|
||||
case 'ALL': {
|
||||
const users = await prisma.user.findMany({
|
||||
where: { status: 'ACTIVE' },
|
||||
where: { status: 'ACTIVE', isTest: false },
|
||||
select: { id: true },
|
||||
})
|
||||
return users.map((u) => u.id)
|
||||
|
||||
@@ -22,7 +22,7 @@ export const programRouter = router({
|
||||
const includeStages = input?.includeStages || false
|
||||
|
||||
const programs = await ctx.prisma.program.findMany({
|
||||
where: input?.status ? { status: input.status } : undefined,
|
||||
where: input?.status ? { isTest: false, status: input.status } : { isTest: false },
|
||||
orderBy: { year: 'desc' },
|
||||
include: includeStages
|
||||
? {
|
||||
|
||||
@@ -103,6 +103,7 @@ export const projectPoolRouter = router({
|
||||
|
||||
// Build where clause
|
||||
const where: Record<string, unknown> = {
|
||||
isTest: false,
|
||||
programId,
|
||||
}
|
||||
|
||||
@@ -317,6 +318,7 @@ export const projectPoolRouter = router({
|
||||
|
||||
// Find projects to assign
|
||||
const where: Record<string, unknown> = {
|
||||
isTest: false,
|
||||
programId,
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,9 @@ export const projectRouter = router({
|
||||
const skip = (page - 1) * perPage
|
||||
|
||||
// Build where clause
|
||||
const where: Record<string, unknown> = {}
|
||||
const where: Record<string, unknown> = {
|
||||
isTest: false,
|
||||
}
|
||||
|
||||
// Filter by program
|
||||
if (programId) where.programId = programId
|
||||
@@ -219,7 +221,9 @@ export const projectRouter = router({
|
||||
wantsMentorship, hasFiles, hasAssignments,
|
||||
} = input
|
||||
|
||||
const where: Record<string, unknown> = {}
|
||||
const where: Record<string, unknown> = {
|
||||
isTest: false,
|
||||
}
|
||||
|
||||
if (programId) where.programId = programId
|
||||
if (roundId) {
|
||||
@@ -357,19 +361,19 @@ export const projectRouter = router({
|
||||
.query(async ({ ctx }) => {
|
||||
const [countries, categories, issues] = await Promise.all([
|
||||
ctx.prisma.project.findMany({
|
||||
where: { country: { not: null } },
|
||||
where: { isTest: false, country: { not: null } },
|
||||
select: { country: true },
|
||||
distinct: ['country'],
|
||||
orderBy: { country: 'asc' },
|
||||
}),
|
||||
ctx.prisma.project.groupBy({
|
||||
by: ['competitionCategory'],
|
||||
where: { competitionCategory: { not: null } },
|
||||
where: { isTest: false, competitionCategory: { not: null } },
|
||||
_count: true,
|
||||
}),
|
||||
ctx.prisma.project.groupBy({
|
||||
by: ['oceanIssue'],
|
||||
where: { oceanIssue: { not: null } },
|
||||
where: { isTest: false, oceanIssue: { not: null } },
|
||||
_count: true,
|
||||
}),
|
||||
])
|
||||
@@ -838,7 +842,7 @@ export const projectRouter = router({
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const projects = await ctx.prisma.project.findMany({
|
||||
where: { id: { in: input.ids } },
|
||||
where: { id: { in: input.ids }, isTest: false },
|
||||
select: { id: true, title: true, status: true },
|
||||
})
|
||||
|
||||
@@ -948,11 +952,13 @@ export const projectRouter = router({
|
||||
programId: z.string().optional(),
|
||||
}))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const where: Record<string, unknown> = {}
|
||||
const where: Record<string, unknown> = {
|
||||
isTest: false,
|
||||
}
|
||||
if (input.programId) where.programId = input.programId
|
||||
|
||||
const projects = await ctx.prisma.project.findMany({
|
||||
where: Object.keys(where).length > 0 ? where : undefined,
|
||||
where,
|
||||
select: { tags: true },
|
||||
})
|
||||
|
||||
@@ -984,6 +990,7 @@ export const projectRouter = router({
|
||||
const projects = await ctx.prisma.project.findMany({
|
||||
where: {
|
||||
id: { in: input.ids },
|
||||
isTest: false,
|
||||
},
|
||||
select: { id: true, title: true },
|
||||
})
|
||||
@@ -1102,6 +1109,7 @@ export const projectRouter = router({
|
||||
|
||||
const where: Record<string, unknown> = {
|
||||
programId,
|
||||
isTest: false,
|
||||
projectRoundStates: { none: {} }, // Projects not assigned to any round
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ export const specialAwardRouter = router({
|
||||
let { competition } = award
|
||||
if (!competition && award.programId) {
|
||||
const comp = await ctx.prisma.competition.findFirst({
|
||||
where: { programId: award.programId },
|
||||
where: { programId: award.programId, isTest: false },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
select: { id: true, name: true, rounds: { select: { id: true, name: true, roundType: true, sortOrder: true }, orderBy: { sortOrder: 'asc' as const } } },
|
||||
})
|
||||
@@ -141,7 +141,7 @@ export const specialAwardRouter = router({
|
||||
let competitionId = input.competitionId
|
||||
if (!competitionId) {
|
||||
const comp = await ctx.prisma.competition.findFirst({
|
||||
where: { programId: input.programId },
|
||||
where: { programId: input.programId, isTest: false },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
select: { id: true },
|
||||
})
|
||||
@@ -217,7 +217,7 @@ export const specialAwardRouter = router({
|
||||
})
|
||||
if (existing && !existing.competitionId) {
|
||||
const comp = await ctx.prisma.competition.findFirst({
|
||||
where: { programId: existing.programId },
|
||||
where: { programId: existing.programId, isTest: false },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
select: { id: true },
|
||||
})
|
||||
@@ -404,7 +404,7 @@ export const specialAwardRouter = router({
|
||||
const { awardId, eligibleOnly, page, perPage } = input
|
||||
const skip = (page - 1) * perPage
|
||||
|
||||
const where: Record<string, unknown> = { awardId }
|
||||
const where: Record<string, unknown> = { awardId, project: { isTest: false } }
|
||||
if (eligibleOnly) where.eligible = true
|
||||
|
||||
const [eligibilities, total] = await Promise.all([
|
||||
|
||||
@@ -53,7 +53,7 @@ async function runTaggingJob(jobId: string, userId: string) {
|
||||
if (!job.programId) {
|
||||
throw new Error('Job must have a programId')
|
||||
}
|
||||
const whereClause = { programId: job.programId }
|
||||
const whereClause = { programId: job.programId, isTest: false }
|
||||
|
||||
const allProjects = await prisma.project.findMany({
|
||||
where: whereClause,
|
||||
@@ -196,11 +196,13 @@ export const tagRouter = router({
|
||||
const userCount = await ctx.prisma.user.count({
|
||||
where: {
|
||||
expertiseTags: { has: tag.name },
|
||||
isTest: false,
|
||||
},
|
||||
})
|
||||
const projectCount = await ctx.prisma.project.count({
|
||||
where: {
|
||||
tags: { has: tag.name },
|
||||
isTest: false,
|
||||
},
|
||||
})
|
||||
return {
|
||||
@@ -228,10 +230,10 @@ export const tagRouter = router({
|
||||
// Get usage counts
|
||||
const [userCount, projectCount] = await Promise.all([
|
||||
ctx.prisma.user.count({
|
||||
where: { expertiseTags: { has: tag.name } },
|
||||
where: { expertiseTags: { has: tag.name }, isTest: false },
|
||||
}),
|
||||
ctx.prisma.project.count({
|
||||
where: { tags: { has: tag.name } },
|
||||
where: { tags: { has: tag.name }, isTest: false },
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -354,7 +356,7 @@ export const tagRouter = router({
|
||||
|
||||
// Update users
|
||||
const usersWithTag = await ctx.prisma.user.findMany({
|
||||
where: { expertiseTags: { has: oldTag.name } },
|
||||
where: { expertiseTags: { has: oldTag.name }, isTest: false },
|
||||
select: { id: true, expertiseTags: true },
|
||||
})
|
||||
|
||||
@@ -371,7 +373,7 @@ export const tagRouter = router({
|
||||
|
||||
// Update projects
|
||||
const projectsWithTag = await ctx.prisma.project.findMany({
|
||||
where: { tags: { has: oldTag.name } },
|
||||
where: { tags: { has: oldTag.name }, isTest: false },
|
||||
select: { id: true, tags: true },
|
||||
})
|
||||
|
||||
@@ -412,9 +414,9 @@ export const tagRouter = router({
|
||||
where: { id: input.id },
|
||||
})
|
||||
|
||||
// Remove tag from all users
|
||||
// Remove tag from all users (excluding test users)
|
||||
const usersWithTag = await ctx.prisma.user.findMany({
|
||||
where: { expertiseTags: { has: tag.name } },
|
||||
where: { expertiseTags: { has: tag.name }, isTest: false },
|
||||
select: { id: true, expertiseTags: true },
|
||||
})
|
||||
|
||||
@@ -427,9 +429,9 @@ export const tagRouter = router({
|
||||
})
|
||||
}
|
||||
|
||||
// Remove tag from all projects
|
||||
// Remove tag from all projects (excluding test projects)
|
||||
const projectsWithTag = await ctx.prisma.project.findMany({
|
||||
where: { tags: { has: tag.name } },
|
||||
where: { tags: { has: tag.name }, isTest: false },
|
||||
select: { id: true, tags: true },
|
||||
})
|
||||
|
||||
|
||||
92
src/server/routers/testEnvironment.ts
Normal file
92
src/server/routers/testEnvironment.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { router, superAdminProcedure, protectedProcedure } from '../trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { createTestEnvironment, tearDownTestEnvironment } from '../services/test-environment'
|
||||
|
||||
export const testEnvironmentRouter = router({
|
||||
/**
|
||||
* Get the current test environment status.
|
||||
* Uses a custom auth check: allows access if realRole OR role is SUPER_ADMIN.
|
||||
* This enables the impersonation banner to fetch test users while impersonating.
|
||||
*/
|
||||
status: protectedProcedure.query(async ({ ctx }) => {
|
||||
// Allow access if the user's actual role (or impersonated-from role) is SUPER_ADMIN
|
||||
const effectiveRole = (ctx.session?.user as any)?.realRole || ctx.user.role
|
||||
if (effectiveRole !== 'SUPER_ADMIN') {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'Super admin access required' })
|
||||
}
|
||||
|
||||
const competition = await ctx.prisma.competition.findFirst({
|
||||
where: { isTest: true },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
program: {
|
||||
select: { id: true, name: true },
|
||||
},
|
||||
rounds: {
|
||||
select: { id: true, name: true, roundType: true, status: true, sortOrder: true },
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!competition) {
|
||||
return { active: false as const }
|
||||
}
|
||||
|
||||
// Get test users grouped by role
|
||||
const testUsers = await ctx.prisma.user.findMany({
|
||||
where: { isTest: true },
|
||||
select: { id: true, name: true, email: true, role: true },
|
||||
orderBy: [{ role: 'asc' }, { name: 'asc' }],
|
||||
})
|
||||
|
||||
// Get email redirect setting
|
||||
const emailRedirect = await ctx.prisma.systemSettings.findUnique({
|
||||
where: { key: 'test_email_redirect' },
|
||||
select: { value: true },
|
||||
})
|
||||
|
||||
return {
|
||||
active: true as const,
|
||||
competition: {
|
||||
id: competition.id,
|
||||
name: competition.name,
|
||||
status: competition.status,
|
||||
createdAt: competition.createdAt,
|
||||
programId: competition.program.id,
|
||||
programName: competition.program.name,
|
||||
},
|
||||
rounds: competition.rounds,
|
||||
users: testUsers,
|
||||
emailRedirect: emailRedirect?.value || null,
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Create a test environment. Idempotent — tears down existing first.
|
||||
*/
|
||||
create: superAdminProcedure.mutation(async ({ ctx }) => {
|
||||
const result = await createTestEnvironment(ctx.prisma, ctx.user.email || '')
|
||||
return result
|
||||
}),
|
||||
|
||||
/**
|
||||
* Tear down the test environment.
|
||||
*/
|
||||
tearDown: superAdminProcedure.mutation(async ({ ctx }) => {
|
||||
const program = await ctx.prisma.program.findFirst({
|
||||
where: { isTest: true },
|
||||
select: { id: true },
|
||||
})
|
||||
|
||||
if (!program) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'No test environment found' })
|
||||
}
|
||||
|
||||
await tearDownTestEnvironment(ctx.prisma, program.id)
|
||||
return { success: true }
|
||||
}),
|
||||
})
|
||||
@@ -249,7 +249,9 @@ export const userRouter = router({
|
||||
const { role, roles, status, search, page, perPage } = input
|
||||
const skip = (page - 1) * perPage
|
||||
|
||||
const where: Record<string, unknown> = {}
|
||||
const where: Record<string, unknown> = {
|
||||
isTest: false,
|
||||
}
|
||||
|
||||
if (roles && roles.length > 0) {
|
||||
where.role = { in: roles }
|
||||
@@ -316,6 +318,7 @@ export const userRouter = router({
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const where: Record<string, unknown> = {
|
||||
isTest: false,
|
||||
status: { in: ['NONE', 'INVITED'] },
|
||||
}
|
||||
|
||||
@@ -929,6 +932,7 @@ export const userRouter = router({
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const where: Record<string, unknown> = {
|
||||
isTest: false,
|
||||
role: 'JURY_MEMBER',
|
||||
status: 'ACTIVE',
|
||||
}
|
||||
|
||||
@@ -290,6 +290,7 @@ export async function generateSummary({
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
isTest: true,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -297,6 +298,10 @@ export async function generateSummary({
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Project not found' })
|
||||
}
|
||||
|
||||
if (project.isTest) {
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Cannot generate AI summaries for test projects' })
|
||||
}
|
||||
|
||||
// Fetch submitted evaluations for this project in this round
|
||||
const evaluations = await prisma.evaluation.findMany({
|
||||
where: {
|
||||
|
||||
@@ -103,6 +103,7 @@ async function generateCategoryShortlist(
|
||||
const projects = await prisma.project.findMany({
|
||||
where: {
|
||||
competitionCategory: category,
|
||||
isTest: false,
|
||||
assignments: { some: { roundId } },
|
||||
},
|
||||
include: {
|
||||
|
||||
@@ -473,6 +473,10 @@ export async function tagProject(
|
||||
throw new Error(`Project not found: ${projectId}`)
|
||||
}
|
||||
|
||||
if ((project as any).isTest) {
|
||||
throw new Error(`Cannot run AI tagging on test project: ${projectId}`)
|
||||
}
|
||||
|
||||
// Get available tags
|
||||
const availableTags = await getAvailableTags()
|
||||
if (availableTags.length === 0) {
|
||||
@@ -574,7 +578,7 @@ export async function tagProjectsBatch(
|
||||
|
||||
// Fetch full project data for all projects at once (single DB query)
|
||||
const fullProjects = await prisma.project.findMany({
|
||||
where: { id: { in: projects.map((p) => p.id) } },
|
||||
where: { id: { in: projects.map((p) => p.id) }, isTest: false },
|
||||
include: {
|
||||
projectTags: true,
|
||||
files: { select: { fileType: true } },
|
||||
@@ -712,6 +716,10 @@ export async function getTagSuggestions(
|
||||
throw new Error(`Project not found: ${projectId}`)
|
||||
}
|
||||
|
||||
if ((project as any).isTest) {
|
||||
throw new Error(`Cannot run AI tagging on test project: ${projectId}`)
|
||||
}
|
||||
|
||||
// Get available tags
|
||||
const availableTags = await getAvailableTags()
|
||||
if (availableTags.length === 0) {
|
||||
|
||||
@@ -88,6 +88,7 @@ export async function processEligibilityJob(
|
||||
where: {
|
||||
id: { in: passedIds },
|
||||
programId: award.programId,
|
||||
isTest: false,
|
||||
},
|
||||
select: projectSelect,
|
||||
})
|
||||
@@ -99,6 +100,7 @@ export async function processEligibilityJob(
|
||||
projects = await prisma.project.findMany({
|
||||
where: {
|
||||
programId: award.programId,
|
||||
isTest: false,
|
||||
status: { in: [...statusFilter] },
|
||||
},
|
||||
select: projectSelect,
|
||||
|
||||
@@ -320,7 +320,7 @@ export async function analyzeAllUnanalyzed(): Promise<{
|
||||
total: number
|
||||
}> {
|
||||
const files = await prisma.projectFile.findMany({
|
||||
where: { analyzedAt: null },
|
||||
where: { analyzedAt: null, project: { isTest: false } },
|
||||
select: {
|
||||
id: true,
|
||||
objectKey: true,
|
||||
|
||||
@@ -32,6 +32,7 @@ export async function processDigests(
|
||||
// Find users who opted in for this digest frequency
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
isTest: false,
|
||||
digestFrequency: type,
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
|
||||
@@ -56,7 +56,7 @@ export async function sendManualReminders(roundId: string): Promise<ReminderResu
|
||||
if (usersToNotify.length === 0) return { sent, errors }
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
where: { id: { in: usersToNotify } },
|
||||
where: { id: { in: usersToNotify }, isTest: false },
|
||||
select: { id: true, name: true, email: true },
|
||||
})
|
||||
|
||||
@@ -133,6 +133,7 @@ export async function processEvaluationReminders(roundId?: string): Promise<Remi
|
||||
status: 'ROUND_ACTIVE' as const,
|
||||
windowCloseAt: { gt: now },
|
||||
windowOpenAt: { lte: now },
|
||||
competition: { isTest: false },
|
||||
...(roundId && { id: roundId }),
|
||||
},
|
||||
select: {
|
||||
@@ -213,7 +214,7 @@ async function sendRemindersForRound(
|
||||
|
||||
// Get user details and their pending counts
|
||||
const users = await prisma.user.findMany({
|
||||
where: { id: { in: usersToNotify } },
|
||||
where: { id: { in: usersToNotify }, isTest: false },
|
||||
select: { id: true, name: true, email: true },
|
||||
})
|
||||
|
||||
|
||||
@@ -297,6 +297,7 @@ export async function notifyAdmins(params: {
|
||||
where: {
|
||||
role: { in: ['SUPER_ADMIN', 'PROGRAM_ADMIN'] },
|
||||
status: 'ACTIVE',
|
||||
isTest: false,
|
||||
},
|
||||
select: { id: true },
|
||||
})
|
||||
|
||||
@@ -232,6 +232,7 @@ export async function getAIMentorSuggestionsBatch(
|
||||
{ role: 'JURY_MEMBER' },
|
||||
],
|
||||
status: 'ACTIVE',
|
||||
isTest: false,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@@ -458,6 +459,7 @@ export async function getRoundRobinMentor(
|
||||
{ role: 'JURY_MEMBER' },
|
||||
],
|
||||
status: 'ACTIVE',
|
||||
isTest: false,
|
||||
id: { notIn: excludeMentorIds },
|
||||
},
|
||||
select: {
|
||||
|
||||
@@ -40,7 +40,7 @@ export async function sendNotification(
|
||||
): Promise<NotificationResult> {
|
||||
// Get user with notification preferences
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
where: { id: userId, isTest: false },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
|
||||
@@ -94,7 +94,7 @@ export async function previewRoundAssignment(
|
||||
|
||||
// Load jury group members
|
||||
const members = await db.juryGroupMember.findMany({
|
||||
where: { juryGroupId: ctx.juryGroup.id },
|
||||
where: { juryGroupId: ctx.juryGroup.id, user: { isTest: false } },
|
||||
include: {
|
||||
user: { select: { id: true, name: true, email: true, role: true, bio: true, expertiseTags: true, country: true, availabilityJson: true } },
|
||||
},
|
||||
|
||||
@@ -16,7 +16,7 @@ export async function processScheduledRounds(): Promise<{
|
||||
|
||||
// Find a SUPER_ADMIN to use as the actor for audit logging
|
||||
const systemActor = await prisma.user.findFirst({
|
||||
where: { role: 'SUPER_ADMIN' },
|
||||
where: { role: 'SUPER_ADMIN', isTest: false },
|
||||
select: { id: true },
|
||||
})
|
||||
|
||||
@@ -29,7 +29,7 @@ export async function processScheduledRounds(): Promise<{
|
||||
where: {
|
||||
status: 'ROUND_DRAFT',
|
||||
windowOpenAt: { lte: now },
|
||||
competition: { status: { not: 'ARCHIVED' } },
|
||||
competition: { status: { not: 'ARCHIVED' }, isTest: false },
|
||||
},
|
||||
select: { id: true, name: true },
|
||||
})
|
||||
@@ -49,6 +49,7 @@ export async function processScheduledRounds(): Promise<{
|
||||
where: {
|
||||
status: 'ROUND_ACTIVE',
|
||||
windowCloseAt: { lte: now },
|
||||
competition: { isTest: false },
|
||||
},
|
||||
select: { id: true, name: true },
|
||||
})
|
||||
|
||||
@@ -362,6 +362,7 @@ export async function getSmartSuggestions(options: {
|
||||
where: {
|
||||
role,
|
||||
status: 'ACTIVE',
|
||||
isTest: false,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@@ -683,6 +684,7 @@ export async function getMentorSuggestionsForProject(
|
||||
where: {
|
||||
role: 'MENTOR',
|
||||
status: 'ACTIVE',
|
||||
isTest: false,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
676
src/server/services/test-environment.ts
Normal file
676
src/server/services/test-environment.ts
Normal file
@@ -0,0 +1,676 @@
|
||||
import type { PrismaClient } from '@prisma/client'
|
||||
|
||||
// ─── Test Data Constants ─────────────────────────────────────────────────────
|
||||
|
||||
const TEST_JURY = [
|
||||
{ name: 'Sophie Laurent', email: 'sophie.laurent@test.local', country: 'France', expertiseTags: ['Marine Biology', 'Conservation'] },
|
||||
{ name: 'Marco Bianchi', email: 'marco.bianchi@test.local', country: 'Italy', expertiseTags: ['Sustainable Fishing', 'Policy'] },
|
||||
{ name: 'Elena Petrova', email: 'elena.petrova@test.local', country: 'Germany', expertiseTags: ['Ocean Technology', 'Engineering'] },
|
||||
{ name: 'James Chen', email: 'james.chen@test.local', country: 'Singapore', expertiseTags: ['Blue Economy', 'Innovation'] },
|
||||
{ name: 'Aisha Diallo', email: 'aisha.diallo@test.local', country: 'Senegal', expertiseTags: ['Community Development', 'Education'] },
|
||||
{ name: 'Carlos Rivera', email: 'carlos.rivera@test.local', country: 'Spain', expertiseTags: ['Climate Science', 'Renewable Energy'] },
|
||||
]
|
||||
|
||||
const TEST_APPLICANTS_OWNERS = [
|
||||
{ name: 'Léa Moreau', email: 'lea.moreau@test.local', country: 'France' },
|
||||
{ name: 'Henrik Johansson', email: 'henrik.johansson@test.local', country: 'Sweden' },
|
||||
{ name: 'Fatima Al-Rashid', email: 'fatima.alrashid@test.local', country: 'UAE' },
|
||||
{ name: 'Yuki Tanaka', email: 'yuki.tanaka@test.local', country: 'Japan' },
|
||||
{ name: 'Priya Sharma', email: 'priya.sharma@test.local', country: 'India' },
|
||||
{ name: 'Lucas Oliveira', email: 'lucas.oliveira@test.local', country: 'Brazil' },
|
||||
{ name: 'Nadia Kowalski', email: 'nadia.kowalski@test.local', country: 'Poland' },
|
||||
{ name: 'Samuel Okonkwo', email: 'samuel.okonkwo@test.local', country: 'Nigeria' },
|
||||
{ name: 'Ingrid Hansen', email: 'ingrid.hansen@test.local', country: 'Norway' },
|
||||
{ name: 'Diego Fernández', email: 'diego.fernandez@test.local', country: 'Mexico' },
|
||||
{ name: 'Amira Benali', email: 'amira.benali@test.local', country: 'Morocco' },
|
||||
{ name: 'Thomas Müller', email: 'thomas.muller@test.local', country: 'Germany' },
|
||||
]
|
||||
|
||||
const TEST_APPLICANTS_TEAMMATES = [
|
||||
{ name: 'Marie Dubois', email: 'marie.dubois@test.local', country: 'France' },
|
||||
{ name: 'Kenji Watanabe', email: 'kenji.watanabe@test.local', country: 'Japan' },
|
||||
{ name: 'Ana Costa', email: 'ana.costa@test.local', country: 'Brazil' },
|
||||
{ name: 'Erik Lindqvist', email: 'erik.lindqvist@test.local', country: 'Sweden' },
|
||||
]
|
||||
|
||||
const TEST_OTHER_ROLES = [
|
||||
{ name: 'Dr. Catherine Blanc', email: 'catherine.blanc@test.local', role: 'MENTOR' as const, country: 'Monaco' },
|
||||
{ name: 'Oliver Schmidt', email: 'oliver.schmidt@test.local', role: 'OBSERVER' as const, country: 'Switzerland' },
|
||||
{ name: 'Isabella Romano', email: 'isabella.romano@test.local', role: 'AWARD_MASTER' as const, country: 'Italy' },
|
||||
{ name: 'Philippe Durand', email: 'philippe.durand@test.local', role: 'PROGRAM_ADMIN' as const, country: 'Monaco' },
|
||||
]
|
||||
|
||||
const TEST_PROJECTS = [
|
||||
{
|
||||
title: 'OceanGuard: AI-Powered Marine Debris Detection',
|
||||
teamName: 'OceanGuard Technologies',
|
||||
description: 'Using satellite imagery and deep learning to identify and track marine debris across the Mediterranean. Our proprietary algorithm achieves 94% accuracy in detecting microplastic concentration zones, enabling targeted cleanup operations.',
|
||||
country: 'France',
|
||||
category: 'STARTUP' as const,
|
||||
oceanIssue: 'POLLUTION_REDUCTION' as const,
|
||||
},
|
||||
{
|
||||
title: 'Blue Carbon Ventures: Seagrass Restoration at Scale',
|
||||
teamName: 'Blue Carbon Ventures',
|
||||
description: 'Developing cost-effective seagrass meadow restoration techniques for Mediterranean coastal zones. Our approach combines drone-based seed dispersal with AI monitoring to restore 500+ hectares of seagrass by 2030.',
|
||||
country: 'Italy',
|
||||
category: 'STARTUP' as const,
|
||||
oceanIssue: 'BLUE_CARBON' as const,
|
||||
},
|
||||
{
|
||||
title: 'Reef Resilience: Coral Thermal Adaptation Program',
|
||||
teamName: 'Reef Resilience Lab',
|
||||
description: 'Selective breeding of thermally tolerant coral genotypes combined with biofilm-enhanced substrate technology to accelerate reef recovery in warming waters. Currently operating pilot programs in 3 Mediterranean sites.',
|
||||
country: 'Monaco',
|
||||
category: 'STARTUP' as const,
|
||||
oceanIssue: 'HABITAT_RESTORATION' as const,
|
||||
},
|
||||
{
|
||||
title: 'WaveHarvest: Ocean Energy for Coastal Communities',
|
||||
teamName: 'WaveHarvest Energy',
|
||||
description: 'Modular wave energy converters designed for small island nations and remote coastal communities. Our patented oscillating water column system delivers reliable 50kW power at 1/3 the cost of competing technologies.',
|
||||
country: 'Norway',
|
||||
category: 'STARTUP' as const,
|
||||
oceanIssue: 'CLIMATE_MITIGATION' as const,
|
||||
},
|
||||
{
|
||||
title: 'SailCargo: Zero-Emission Maritime Logistics',
|
||||
teamName: 'SailCargo Collective',
|
||||
description: 'Reviving wind-powered shipping for short-sea routes in the Mediterranean using modernized sailing vessel designs with solar-electric auxiliary propulsion. Connecting ports along the French and Italian Riviera.',
|
||||
country: 'Spain',
|
||||
category: 'STARTUP' as const,
|
||||
oceanIssue: 'SUSTAINABLE_SHIPPING' as const,
|
||||
},
|
||||
{
|
||||
title: 'Neptune Analytics: Smart Fisheries Management',
|
||||
teamName: 'Neptune Analytics',
|
||||
description: 'IoT sensor network and machine learning platform for real-time fisheries monitoring. Tracks fish stock health, migration patterns, and fishing vessel compliance to support science-based quota management.',
|
||||
country: 'Portugal',
|
||||
category: 'STARTUP' as const,
|
||||
oceanIssue: 'SUSTAINABLE_FISHING' as const,
|
||||
},
|
||||
{
|
||||
title: 'AquaLens: Underwater Environmental Monitoring',
|
||||
teamName: 'AquaLens Research',
|
||||
description: 'A network of autonomous underwater drones equipped with hyperspectral cameras for continuous marine ecosystem monitoring. Real-time data on water quality, biodiversity indices, and pollutant levels.',
|
||||
country: 'Germany',
|
||||
category: 'BUSINESS_CONCEPT' as const,
|
||||
oceanIssue: 'TECHNOLOGY_INNOVATION' as const,
|
||||
},
|
||||
{
|
||||
title: 'TidalConnect: Ocean Literacy Education Platform',
|
||||
teamName: 'TidalConnect Foundation',
|
||||
description: 'Interactive mobile platform bringing ocean science education to underserved coastal communities in West Africa. Features gamified learning modules, local language support, and citizen science data collection.',
|
||||
country: 'Senegal',
|
||||
category: 'BUSINESS_CONCEPT' as const,
|
||||
oceanIssue: 'COMMUNITY_CAPACITY' as const,
|
||||
},
|
||||
{
|
||||
title: 'BioFouling Solutions: Eco-Friendly Marine Coatings',
|
||||
teamName: 'BioFouling Solutions',
|
||||
description: 'Biomimetic antifouling coatings inspired by shark skin microstructure. Our non-toxic coating reduces fuel consumption by 12% while eliminating the release of harmful biocides into marine environments.',
|
||||
country: 'Sweden',
|
||||
category: 'BUSINESS_CONCEPT' as const,
|
||||
oceanIssue: 'POLLUTION_REDUCTION' as const,
|
||||
},
|
||||
{
|
||||
title: 'Kelp Climate: Industrial-Scale Kelp Farming',
|
||||
teamName: 'Kelp Climate Co.',
|
||||
description: 'Offshore macroalgae cultivation combining carbon sequestration with sustainable biomaterials production. Our proprietary deep-water cultivation system supports 10x faster growth rates than nearshore farms.',
|
||||
country: 'Japan',
|
||||
category: 'BUSINESS_CONCEPT' as const,
|
||||
oceanIssue: 'BLUE_CARBON' as const,
|
||||
},
|
||||
{
|
||||
title: 'MarineTrack: Vessel Emission Compliance System',
|
||||
teamName: 'MarineTrack Systems',
|
||||
description: 'Real-time satellite and AIS-based monitoring platform for enforcing IMO 2030 emission standards. Automatically detects scrubber discharge violations and sulfur emission exceedances across Mediterranean shipping lanes.',
|
||||
country: 'Greece',
|
||||
category: 'STARTUP' as const,
|
||||
oceanIssue: 'SUSTAINABLE_SHIPPING' as const,
|
||||
},
|
||||
{
|
||||
title: 'Mangrove Guardians: Community-Led Restoration',
|
||||
teamName: 'Mangrove Guardians Network',
|
||||
description: 'Empowering coastal fishing communities in Southeast Asia to restore and manage mangrove ecosystems through microfinance-linked conservation incentives and satellite-verified carbon credits.',
|
||||
country: 'India',
|
||||
category: 'BUSINESS_CONCEPT' as const,
|
||||
oceanIssue: 'HABITAT_RESTORATION' as const,
|
||||
},
|
||||
]
|
||||
|
||||
const ROUND_DEFINITIONS = [
|
||||
{ name: 'Intake', slug: 'intake', roundType: 'INTAKE' as const, sortOrder: 0 },
|
||||
{ name: 'Eligibility Screening', slug: 'filtering', roundType: 'FILTERING' as const, sortOrder: 1 },
|
||||
{ name: 'Expert Evaluation', slug: 'evaluation', roundType: 'EVALUATION' as const, sortOrder: 2 },
|
||||
{ name: 'Document Submission', slug: 'submission', roundType: 'SUBMISSION' as const, sortOrder: 3 },
|
||||
{ name: 'Mentoring Phase', slug: 'mentoring', roundType: 'MENTORING' as const, sortOrder: 4 },
|
||||
{ name: 'Live Finals', slug: 'live-final', roundType: 'LIVE_FINAL' as const, sortOrder: 5 },
|
||||
{ name: 'Final Deliberation', slug: 'deliberation', roundType: 'DELIBERATION' as const, sortOrder: 6 },
|
||||
]
|
||||
|
||||
// ─── Public API ──────────────────────────────────────────────────────────────
|
||||
|
||||
export interface TestEnvironmentResult {
|
||||
programId: string
|
||||
competitionId: string
|
||||
users: Array<{ id: string; name: string; email: string; role: string }>
|
||||
projectCount: number
|
||||
roundCount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a complete test environment with realistic data.
|
||||
* Idempotent — tears down existing test env first.
|
||||
*/
|
||||
export async function createTestEnvironment(
|
||||
prisma: PrismaClient,
|
||||
adminEmail: string
|
||||
): Promise<TestEnvironmentResult> {
|
||||
// Tear down existing if any
|
||||
const existing = await prisma.competition.findFirst({ where: { isTest: true } })
|
||||
if (existing) {
|
||||
const existingProgram = await prisma.program.findFirst({ where: { isTest: true } })
|
||||
if (existingProgram) {
|
||||
await tearDownTestEnvironment(prisma, existingProgram.id)
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Email redirect setting
|
||||
await prisma.systemSettings.upsert({
|
||||
where: { key: 'test_email_redirect' },
|
||||
update: { value: adminEmail },
|
||||
create: { key: 'test_email_redirect', value: adminEmail, category: 'DEFAULTS' },
|
||||
})
|
||||
|
||||
// 2. Program
|
||||
const program = await prisma.program.create({
|
||||
data: {
|
||||
name: '[TEST] Test Environment 2026',
|
||||
year: 2026,
|
||||
status: 'ACTIVE',
|
||||
description: 'Test environment for role impersonation and feature testing',
|
||||
isTest: true,
|
||||
},
|
||||
})
|
||||
|
||||
// 3. Create test users
|
||||
const createdUsers: Array<{ id: string; name: string; email: string; role: string }> = []
|
||||
|
||||
// Jury members
|
||||
const juryUsers = await Promise.all(
|
||||
TEST_JURY.map((j) =>
|
||||
prisma.user.create({
|
||||
data: {
|
||||
name: j.name,
|
||||
email: j.email,
|
||||
role: 'JURY_MEMBER',
|
||||
status: 'ACTIVE',
|
||||
country: j.country,
|
||||
expertiseTags: j.expertiseTags,
|
||||
isTest: true,
|
||||
mustSetPassword: false,
|
||||
},
|
||||
})
|
||||
)
|
||||
)
|
||||
juryUsers.forEach((u) => createdUsers.push({ id: u.id, name: u.name!, email: u.email, role: u.role }))
|
||||
|
||||
// Applicant owners
|
||||
const ownerUsers = await Promise.all(
|
||||
TEST_APPLICANTS_OWNERS.map((a) =>
|
||||
prisma.user.create({
|
||||
data: {
|
||||
name: a.name,
|
||||
email: a.email,
|
||||
role: 'APPLICANT',
|
||||
status: 'ACTIVE',
|
||||
country: a.country,
|
||||
isTest: true,
|
||||
mustSetPassword: false,
|
||||
},
|
||||
})
|
||||
)
|
||||
)
|
||||
ownerUsers.forEach((u) => createdUsers.push({ id: u.id, name: u.name!, email: u.email, role: u.role }))
|
||||
|
||||
// Applicant teammates
|
||||
const teammateUsers = await Promise.all(
|
||||
TEST_APPLICANTS_TEAMMATES.map((a) =>
|
||||
prisma.user.create({
|
||||
data: {
|
||||
name: a.name,
|
||||
email: a.email,
|
||||
role: 'APPLICANT',
|
||||
status: 'ACTIVE',
|
||||
country: a.country,
|
||||
isTest: true,
|
||||
mustSetPassword: false,
|
||||
},
|
||||
})
|
||||
)
|
||||
)
|
||||
teammateUsers.forEach((u) => createdUsers.push({ id: u.id, name: u.name!, email: u.email, role: u.role }))
|
||||
|
||||
// Other roles
|
||||
const otherUsers = await Promise.all(
|
||||
TEST_OTHER_ROLES.map((r) =>
|
||||
prisma.user.create({
|
||||
data: {
|
||||
name: r.name,
|
||||
email: r.email,
|
||||
role: r.role,
|
||||
status: 'ACTIVE',
|
||||
country: r.country,
|
||||
isTest: true,
|
||||
mustSetPassword: false,
|
||||
},
|
||||
})
|
||||
)
|
||||
)
|
||||
otherUsers.forEach((u) => createdUsers.push({ id: u.id, name: u.name!, email: u.email, role: u.role }))
|
||||
|
||||
// 4. Create projects (one per owner)
|
||||
const projects = await Promise.all(
|
||||
TEST_PROJECTS.map((p, i) =>
|
||||
prisma.project.create({
|
||||
data: {
|
||||
programId: program.id,
|
||||
title: p.title,
|
||||
teamName: p.teamName,
|
||||
description: p.description,
|
||||
country: p.country,
|
||||
competitionCategory: p.category,
|
||||
oceanIssue: p.oceanIssue,
|
||||
status: 'SUBMITTED',
|
||||
submissionSource: 'MANUAL',
|
||||
submittedAt: new Date(),
|
||||
submittedByUserId: ownerUsers[i].id,
|
||||
isTest: true,
|
||||
},
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
// 5. Add teammates to projects 0, 1, and 3
|
||||
const teammateAssignments = [
|
||||
{ projectIdx: 0, teammateIdx: 0 },
|
||||
{ projectIdx: 0, teammateIdx: 1 },
|
||||
{ projectIdx: 1, teammateIdx: 2 },
|
||||
{ projectIdx: 3, teammateIdx: 3 },
|
||||
]
|
||||
await Promise.all(
|
||||
teammateAssignments.map((ta) =>
|
||||
prisma.teamMember.create({
|
||||
data: {
|
||||
projectId: projects[ta.projectIdx].id,
|
||||
userId: teammateUsers[ta.teammateIdx].id,
|
||||
role: 'MEMBER',
|
||||
},
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
// Also add owners as team members (OWNER role)
|
||||
await Promise.all(
|
||||
projects.map((proj, i) =>
|
||||
prisma.teamMember.create({
|
||||
data: {
|
||||
projectId: proj.id,
|
||||
userId: ownerUsers[i].id,
|
||||
role: 'LEAD',
|
||||
},
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
// 6. Competition
|
||||
const competition = await prisma.competition.create({
|
||||
data: {
|
||||
programId: program.id,
|
||||
name: 'Test Competition 2026',
|
||||
slug: `test-env-${Date.now()}`,
|
||||
status: 'ACTIVE',
|
||||
isTest: true,
|
||||
},
|
||||
})
|
||||
|
||||
// 7. Jury Group
|
||||
const observerUser = otherUsers.find((u) => u.role === 'OBSERVER')
|
||||
const juryGroup = await prisma.juryGroup.create({
|
||||
data: {
|
||||
competitionId: competition.id,
|
||||
name: 'Test Jury Panel',
|
||||
slug: 'test-jury-panel',
|
||||
},
|
||||
})
|
||||
|
||||
// Add jury members to group
|
||||
await Promise.all(
|
||||
juryUsers.map((ju) =>
|
||||
prisma.juryGroupMember.create({
|
||||
data: {
|
||||
juryGroupId: juryGroup.id,
|
||||
userId: ju.id,
|
||||
role: 'MEMBER',
|
||||
},
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
// Add observer to jury group
|
||||
if (observerUser) {
|
||||
await prisma.juryGroupMember.create({
|
||||
data: {
|
||||
juryGroupId: juryGroup.id,
|
||||
userId: observerUser.id,
|
||||
role: 'OBSERVER',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 8. Rounds (INTAKE=ACTIVE, rest=DRAFT)
|
||||
const rounds = await Promise.all(
|
||||
ROUND_DEFINITIONS.map((rd) =>
|
||||
prisma.round.create({
|
||||
data: {
|
||||
competitionId: competition.id,
|
||||
name: rd.name,
|
||||
slug: rd.slug,
|
||||
roundType: rd.roundType,
|
||||
status: rd.sortOrder === 0 ? 'ROUND_ACTIVE' : 'ROUND_DRAFT',
|
||||
sortOrder: rd.sortOrder,
|
||||
juryGroupId: rd.roundType === 'EVALUATION' ? juryGroup.id : undefined,
|
||||
windowOpenAt: rd.sortOrder === 0 ? new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) : undefined,
|
||||
windowCloseAt: rd.sortOrder === 0 ? new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) : undefined,
|
||||
},
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
const intakeRound = rounds[0]
|
||||
const evaluationRound = rounds[2]
|
||||
|
||||
// 9. Evaluation form on the evaluation round
|
||||
const evalForm = await prisma.evaluationForm.create({
|
||||
data: {
|
||||
roundId: evaluationRound.id,
|
||||
version: 1,
|
||||
isActive: true,
|
||||
criteriaJson: [
|
||||
{ id: 'innovation', label: 'Innovation & Originality', description: 'Novelty of approach and potential impact', scale: '1-10', weight: 30, required: true },
|
||||
{ id: 'feasibility', label: 'Technical Feasibility', description: 'Viability of implementation and scalability', scale: '1-10', weight: 40, required: true },
|
||||
{ id: 'ocean_impact', label: 'Ocean Impact Potential', description: 'Direct benefit to marine ecosystems', scale: '1-10', weight: 30, required: true },
|
||||
],
|
||||
scalesJson: { '1-10': { min: 1, max: 10, labels: { 1: 'Poor', 5: 'Average', 10: 'Excellent' } } },
|
||||
},
|
||||
})
|
||||
|
||||
// 10. ProjectRoundState for all projects in INTAKE round (PENDING)
|
||||
await Promise.all(
|
||||
projects.map((proj) =>
|
||||
prisma.projectRoundState.create({
|
||||
data: {
|
||||
projectId: proj.id,
|
||||
roundId: intakeRound.id,
|
||||
state: 'PENDING',
|
||||
},
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
// 11. Assignments — 5 projects per jury member in evaluation round (round-robin)
|
||||
const assignmentsPerJury = 5
|
||||
const assignmentData: Array<{ userId: string; projectId: string; roundId: string; juryGroupId: string }> = []
|
||||
|
||||
for (let j = 0; j < juryUsers.length; j++) {
|
||||
for (let a = 0; a < assignmentsPerJury; a++) {
|
||||
const projectIdx = (j * 2 + a) % projects.length
|
||||
const key = `${juryUsers[j].id}-${projects[projectIdx].id}`
|
||||
// Avoid duplicate assignments
|
||||
if (!assignmentData.some((ad) => ad.userId === juryUsers[j].id && ad.projectId === projects[projectIdx].id)) {
|
||||
assignmentData.push({
|
||||
userId: juryUsers[j].id,
|
||||
projectId: projects[projectIdx].id,
|
||||
roundId: evaluationRound.id,
|
||||
juryGroupId: juryGroup.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const assignments = await Promise.all(
|
||||
assignmentData.map((ad) =>
|
||||
prisma.assignment.create({
|
||||
data: {
|
||||
userId: ad.userId,
|
||||
projectId: ad.projectId,
|
||||
roundId: ad.roundId,
|
||||
juryGroupId: ad.juryGroupId,
|
||||
method: 'MANUAL',
|
||||
isCompleted: false,
|
||||
},
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
// 12. Partial evaluations — first 2 jury members score their first 3-4 assignments
|
||||
const evalJurors = juryUsers.slice(0, 2)
|
||||
for (const juror of evalJurors) {
|
||||
const jurorAssignments = assignments.filter((a) => a.userId === juror.id).slice(0, 3 + Math.round(Math.random()))
|
||||
|
||||
for (const assignment of jurorAssignments) {
|
||||
const innovationScore = 5 + Math.floor(Math.random() * 5) // 5-9
|
||||
const feasibilityScore = 4 + Math.floor(Math.random() * 5) // 4-8
|
||||
const impactScore = 5 + Math.floor(Math.random() * 4) // 5-8
|
||||
const globalScore = Math.round((innovationScore * 0.3 + feasibilityScore * 0.4 + impactScore * 0.3))
|
||||
|
||||
await prisma.evaluation.create({
|
||||
data: {
|
||||
assignmentId: assignment.id,
|
||||
formId: evalForm.id,
|
||||
status: 'SUBMITTED',
|
||||
criterionScoresJson: {
|
||||
innovation: innovationScore,
|
||||
feasibility: feasibilityScore,
|
||||
ocean_impact: impactScore,
|
||||
},
|
||||
globalScore,
|
||||
binaryDecision: globalScore >= 6,
|
||||
feedbackText: `Strong project with notable ${innovationScore >= 7 ? 'innovation' : 'potential'}. ${feasibilityScore >= 7 ? 'Highly feasible approach.' : 'Implementation timeline needs refinement.'}`,
|
||||
submittedAt: new Date(Date.now() - Math.floor(Math.random() * 3 * 24 * 60 * 60 * 1000)),
|
||||
},
|
||||
})
|
||||
|
||||
// Mark assignment as completed
|
||||
await prisma.assignment.update({
|
||||
where: { id: assignment.id },
|
||||
data: { isCompleted: true },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 13. Advancement rules linking consecutive rounds
|
||||
for (let i = 0; i < rounds.length - 1; i++) {
|
||||
await prisma.advancementRule.create({
|
||||
data: {
|
||||
roundId: rounds[i].id,
|
||||
ruleType: 'ADMIN_SELECTION',
|
||||
configJson: {},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
programId: program.id,
|
||||
competitionId: competition.id,
|
||||
users: createdUsers,
|
||||
projectCount: projects.length,
|
||||
roundCount: rounds.length,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tear down the test environment — delete all test data.
|
||||
* Follows reverse-dependency order to avoid FK constraint errors.
|
||||
*/
|
||||
export async function tearDownTestEnvironment(
|
||||
prisma: PrismaClient,
|
||||
programId: string
|
||||
): Promise<void> {
|
||||
// Verify this is actually a test program
|
||||
const program = await prisma.program.findUnique({
|
||||
where: { id: programId },
|
||||
select: { isTest: true },
|
||||
})
|
||||
if (!program?.isTest) {
|
||||
throw new Error('Cannot tear down a non-test program')
|
||||
}
|
||||
|
||||
// Get all test competition IDs
|
||||
const competitions = await prisma.competition.findMany({
|
||||
where: { programId, isTest: true },
|
||||
select: { id: true },
|
||||
})
|
||||
const competitionIds = competitions.map((c) => c.id)
|
||||
|
||||
// Get all round IDs
|
||||
const rounds = await prisma.round.findMany({
|
||||
where: { competitionId: { in: competitionIds } },
|
||||
select: { id: true },
|
||||
})
|
||||
const roundIds = rounds.map((r) => r.id)
|
||||
|
||||
// Get all test project IDs
|
||||
const projects = await prisma.project.findMany({
|
||||
where: { programId, isTest: true },
|
||||
select: { id: true },
|
||||
})
|
||||
const projectIds = projects.map((p) => p.id)
|
||||
|
||||
// Get all test user IDs
|
||||
const users = await prisma.user.findMany({
|
||||
where: { isTest: true },
|
||||
select: { id: true },
|
||||
})
|
||||
const userIds = users.map((u) => u.id)
|
||||
|
||||
// Delete in reverse-dependency order
|
||||
|
||||
// Deliberation data
|
||||
if (roundIds.length > 0) {
|
||||
await prisma.deliberationVote.deleteMany({ where: { session: { roundId: { in: roundIds } } } })
|
||||
await prisma.deliberationResult.deleteMany({ where: { session: { roundId: { in: roundIds } } } })
|
||||
await prisma.deliberationParticipant.deleteMany({ where: { session: { roundId: { in: roundIds } } } })
|
||||
await prisma.deliberationSession.deleteMany({ where: { roundId: { in: roundIds } } })
|
||||
}
|
||||
|
||||
// Decision/Audit
|
||||
if (competitionIds.length > 0) {
|
||||
await prisma.resultLock.deleteMany({ where: { competitionId: { in: competitionIds } } })
|
||||
}
|
||||
if (roundIds.length > 0) {
|
||||
await prisma.decisionAuditLog.deleteMany({
|
||||
where: { entityType: 'Round', entityId: { in: roundIds } },
|
||||
})
|
||||
}
|
||||
|
||||
// Evaluations → Assignments
|
||||
if (roundIds.length > 0) {
|
||||
await prisma.evaluation.deleteMany({ where: { assignment: { roundId: { in: roundIds } } } })
|
||||
await prisma.conflictOfInterest.deleteMany({ where: { assignment: { roundId: { in: roundIds } } } })
|
||||
await prisma.assignmentException.deleteMany({ where: { assignment: { roundId: { in: roundIds } } } })
|
||||
await prisma.assignment.deleteMany({ where: { roundId: { in: roundIds } } })
|
||||
}
|
||||
|
||||
// Project round states
|
||||
if (roundIds.length > 0) {
|
||||
await prisma.projectRoundState.deleteMany({ where: { roundId: { in: roundIds } } })
|
||||
}
|
||||
|
||||
// Filtering
|
||||
if (roundIds.length > 0) {
|
||||
await prisma.filteringResult.deleteMany({ where: { roundId: { in: roundIds } } })
|
||||
await prisma.filteringRule.deleteMany({ where: { roundId: { in: roundIds } } })
|
||||
}
|
||||
|
||||
// Evaluation forms & advancement rules
|
||||
if (roundIds.length > 0) {
|
||||
await prisma.evaluationForm.deleteMany({ where: { roundId: { in: roundIds } } })
|
||||
await prisma.advancementRule.deleteMany({ where: { roundId: { in: roundIds } } })
|
||||
}
|
||||
|
||||
// Live voting
|
||||
if (roundIds.length > 0) {
|
||||
await prisma.liveVote.deleteMany({ where: { session: { roundId: { in: roundIds } } } })
|
||||
await prisma.liveVotingSession.deleteMany({ where: { roundId: { in: roundIds } } })
|
||||
}
|
||||
|
||||
// Assignment intents and policies
|
||||
if (roundIds.length > 0) {
|
||||
await prisma.assignmentIntent.deleteMany({ where: { roundId: { in: roundIds } } })
|
||||
}
|
||||
|
||||
// Reminder logs
|
||||
if (roundIds.length > 0) {
|
||||
await prisma.reminderLog.deleteMany({ where: { roundId: { in: roundIds } } })
|
||||
}
|
||||
|
||||
// Jury groups
|
||||
if (competitionIds.length > 0) {
|
||||
await prisma.juryGroupMember.deleteMany({ where: { juryGroup: { competitionId: { in: competitionIds } } } })
|
||||
await prisma.juryGroup.deleteMany({ where: { competitionId: { in: competitionIds } } })
|
||||
}
|
||||
|
||||
// Submission windows
|
||||
if (competitionIds.length > 0) {
|
||||
await prisma.roundSubmissionVisibility.deleteMany({ where: { submissionWindow: { competitionId: { in: competitionIds } } } })
|
||||
await prisma.submissionWindow.deleteMany({ where: { competitionId: { in: competitionIds } } })
|
||||
}
|
||||
|
||||
// Rounds
|
||||
if (competitionIds.length > 0) {
|
||||
await prisma.round.deleteMany({ where: { competitionId: { in: competitionIds } } })
|
||||
}
|
||||
|
||||
// Project-related data
|
||||
if (projectIds.length > 0) {
|
||||
await prisma.evaluationSummary.deleteMany({ where: { projectId: { in: projectIds } } })
|
||||
await prisma.evaluationDiscussion.deleteMany({ where: { projectId: { in: projectIds } } })
|
||||
await prisma.awardEligibility.deleteMany({ where: { projectId: { in: projectIds } } })
|
||||
await prisma.awardVote.deleteMany({ where: { projectId: { in: projectIds } } })
|
||||
await prisma.projectTag.deleteMany({ where: { projectId: { in: projectIds } } })
|
||||
await prisma.projectStatusHistory.deleteMany({ where: { projectId: { in: projectIds } } })
|
||||
await prisma.mentorMessage.deleteMany({ where: { projectId: { in: projectIds } } })
|
||||
await prisma.mentorAssignment.deleteMany({ where: { projectId: { in: projectIds } } })
|
||||
await prisma.cohortProject.deleteMany({ where: { projectId: { in: projectIds } } })
|
||||
await prisma.teamMember.deleteMany({ where: { projectId: { in: projectIds } } })
|
||||
await prisma.projectFile.deleteMany({ where: { projectId: { in: projectIds } } })
|
||||
}
|
||||
|
||||
// Special awards
|
||||
if (competitionIds.length > 0) {
|
||||
await prisma.specialAward.deleteMany({ where: { competitionId: { in: competitionIds } } })
|
||||
}
|
||||
|
||||
// Competitions
|
||||
await prisma.competition.deleteMany({ where: { programId, isTest: true } })
|
||||
|
||||
// Projects
|
||||
await prisma.project.deleteMany({ where: { programId, isTest: true } })
|
||||
|
||||
// Audit logs from test users
|
||||
if (userIds.length > 0) {
|
||||
await prisma.auditLog.deleteMany({ where: { userId: { in: userIds } } })
|
||||
await prisma.notificationLog.deleteMany({ where: { userId: { in: userIds } } })
|
||||
}
|
||||
|
||||
// Users
|
||||
await prisma.user.deleteMany({ where: { isTest: true } })
|
||||
|
||||
// Program
|
||||
await prisma.program.deleteMany({ where: { id: programId, isTest: true } })
|
||||
|
||||
// Clean up email redirect setting
|
||||
await prisma.systemSettings.deleteMany({ where: { key: 'test_email_redirect' } })
|
||||
}
|
||||
Reference in New Issue
Block a user