Round system redesign: Phases 1-7 complete
Full pipeline/track/stage architecture replacing the legacy round system. Schema: 11 new models (Pipeline, Track, Stage, StageTransition, ProjectStageState, RoutingRule, Cohort, CohortProject, LiveProgressCursor, OverrideAction, AudienceVoter) + 8 new enums. Backend: 9 new routers (pipeline, stage, routing, stageFiltering, stageAssignment, cohort, live, decision, award) + 6 new services (stage-engine, routing-engine, stage-filtering, stage-assignment, stage-notifications, live-control). Frontend: Pipeline wizard (17 components), jury stage pages (7), applicant pipeline pages (3), public stage pages (2), admin pipeline pages (5), shared stage components (3), SSE route, live hook. Phase 6 refit: 23 routers/services migrated from roundId to stageId, all frontend components refitted. Deleted round.ts (985 lines), roundTemplate.ts, round-helpers.ts, round-settings.ts, round-type-settings.tsx, 10 legacy admin pages, 7 legacy jury pages, 3 legacy dialogs. Phase 7 validation: 36 tests (10 unit + 8 integration files) all passing, TypeScript 0 errors, Next.js build succeeds, 13 integrity checks, legacy symbol sweep clean, auto-seed on first Docker startup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
417
tests/helpers.ts
Normal file
417
tests/helpers.ts
Normal file
@@ -0,0 +1,417 @@
|
||||
/**
|
||||
* Test Data Factories
|
||||
*
|
||||
* Provides helper functions to create test data in the database.
|
||||
* Each factory returns the created record and uses unique identifiers
|
||||
* to avoid collisions between test files.
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'crypto'
|
||||
import { prisma } from './setup'
|
||||
import type {
|
||||
UserRole,
|
||||
StageType,
|
||||
StageStatus,
|
||||
TrackKind,
|
||||
ProjectStageStateValue,
|
||||
AssignmentMethod,
|
||||
} from '@prisma/client'
|
||||
|
||||
export function uid(prefix = 'test'): string {
|
||||
return `${prefix}-${randomUUID().slice(0, 12)}`
|
||||
}
|
||||
|
||||
// ─── User Factory ──────────────────────────────────────────────────────────
|
||||
|
||||
export async function createTestUser(
|
||||
role: UserRole = 'JURY_MEMBER',
|
||||
overrides: Partial<{
|
||||
email: string
|
||||
name: string
|
||||
status: string
|
||||
expertiseTags: string[]
|
||||
maxAssignments: number
|
||||
country: string
|
||||
}> = {},
|
||||
) {
|
||||
const id = uid('user')
|
||||
return prisma.user.create({
|
||||
data: {
|
||||
id,
|
||||
email: overrides.email ?? `${id}@test.local`,
|
||||
name: overrides.name ?? `Test ${role}`,
|
||||
role,
|
||||
status: (overrides.status as any) ?? 'ACTIVE',
|
||||
expertiseTags: overrides.expertiseTags ?? [],
|
||||
maxAssignments: overrides.maxAssignments ?? null,
|
||||
country: overrides.country ?? null,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Program Factory ───────────────────────────────────────────────────────
|
||||
|
||||
export async function createTestProgram(
|
||||
overrides: Partial<{ name: string; year: number }> = {},
|
||||
) {
|
||||
const id = uid('prog')
|
||||
return prisma.program.create({
|
||||
data: {
|
||||
id,
|
||||
name: overrides.name ?? `Test Program ${id}`,
|
||||
year: overrides.year ?? 2026,
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Pipeline Factory ──────────────────────────────────────────────────────
|
||||
|
||||
export async function createTestPipeline(
|
||||
programId: string,
|
||||
overrides: Partial<{ name: string; slug: string; status: string }> = {},
|
||||
) {
|
||||
const id = uid('pipe')
|
||||
return prisma.pipeline.create({
|
||||
data: {
|
||||
id,
|
||||
programId,
|
||||
name: overrides.name ?? `Pipeline ${id}`,
|
||||
slug: overrides.slug ?? id,
|
||||
status: overrides.status ?? 'DRAFT',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Track Factory ─────────────────────────────────────────────────────────
|
||||
|
||||
export async function createTestTrack(
|
||||
pipelineId: string,
|
||||
overrides: Partial<{
|
||||
name: string
|
||||
slug: string
|
||||
kind: TrackKind
|
||||
sortOrder: number
|
||||
routingMode: string
|
||||
decisionMode: string
|
||||
}> = {},
|
||||
) {
|
||||
const id = uid('track')
|
||||
return prisma.track.create({
|
||||
data: {
|
||||
id,
|
||||
pipelineId,
|
||||
name: overrides.name ?? `Track ${id}`,
|
||||
slug: overrides.slug ?? id,
|
||||
kind: overrides.kind ?? 'MAIN',
|
||||
sortOrder: overrides.sortOrder ?? 0,
|
||||
routingMode: (overrides.routingMode as any) ?? null,
|
||||
decisionMode: (overrides.decisionMode as any) ?? null,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Stage Factory ─────────────────────────────────────────────────────────
|
||||
|
||||
export async function createTestStage(
|
||||
trackId: string,
|
||||
overrides: Partial<{
|
||||
name: string
|
||||
slug: string
|
||||
stageType: StageType
|
||||
status: StageStatus
|
||||
sortOrder: number
|
||||
configJson: Record<string, unknown>
|
||||
windowOpenAt: Date
|
||||
windowCloseAt: Date
|
||||
}> = {},
|
||||
) {
|
||||
const id = uid('stage')
|
||||
return prisma.stage.create({
|
||||
data: {
|
||||
id,
|
||||
trackId,
|
||||
name: overrides.name ?? `Stage ${id}`,
|
||||
slug: overrides.slug ?? id,
|
||||
stageType: overrides.stageType ?? 'EVALUATION',
|
||||
status: overrides.status ?? 'STAGE_ACTIVE',
|
||||
sortOrder: overrides.sortOrder ?? 0,
|
||||
configJson: (overrides.configJson as any) ?? undefined,
|
||||
windowOpenAt: overrides.windowOpenAt ?? null,
|
||||
windowCloseAt: overrides.windowCloseAt ?? null,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Stage Transition Factory ──────────────────────────────────────────────
|
||||
|
||||
export async function createTestTransition(
|
||||
fromStageId: string,
|
||||
toStageId: string,
|
||||
overrides: Partial<{ isDefault: boolean; guardJson: Record<string, unknown> }> = {},
|
||||
) {
|
||||
return prisma.stageTransition.create({
|
||||
data: {
|
||||
fromStageId,
|
||||
toStageId,
|
||||
isDefault: overrides.isDefault ?? true,
|
||||
guardJson: (overrides.guardJson as any) ?? undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Project Factory ───────────────────────────────────────────────────────
|
||||
|
||||
export async function createTestProject(
|
||||
programId: string,
|
||||
overrides: Partial<{
|
||||
title: string
|
||||
description: string
|
||||
country: string
|
||||
tags: string[]
|
||||
competitionCategory: string
|
||||
oceanIssue: string
|
||||
}> = {},
|
||||
) {
|
||||
const id = uid('proj')
|
||||
return prisma.project.create({
|
||||
data: {
|
||||
id,
|
||||
programId,
|
||||
title: overrides.title ?? `Test Project ${id}`,
|
||||
description: overrides.description ?? 'Test project description',
|
||||
country: overrides.country ?? 'France',
|
||||
tags: overrides.tags ?? [],
|
||||
competitionCategory: (overrides.competitionCategory as any) ?? null,
|
||||
oceanIssue: (overrides.oceanIssue as any) ?? null,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ─── ProjectStageState Factory ─────────────────────────────────────────────
|
||||
|
||||
export async function createTestPSS(
|
||||
projectId: string,
|
||||
trackId: string,
|
||||
stageId: string,
|
||||
overrides: Partial<{
|
||||
state: ProjectStageStateValue
|
||||
exitedAt: Date | null
|
||||
}> = {},
|
||||
) {
|
||||
return prisma.projectStageState.create({
|
||||
data: {
|
||||
projectId,
|
||||
trackId,
|
||||
stageId,
|
||||
state: overrides.state ?? 'PENDING',
|
||||
exitedAt: overrides.exitedAt ?? null,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Assignment Factory ────────────────────────────────────────────────────
|
||||
|
||||
export async function createTestAssignment(
|
||||
userId: string,
|
||||
projectId: string,
|
||||
stageId: string,
|
||||
overrides: Partial<{
|
||||
method: AssignmentMethod
|
||||
isCompleted: boolean
|
||||
}> = {},
|
||||
) {
|
||||
return prisma.assignment.create({
|
||||
data: {
|
||||
userId,
|
||||
projectId,
|
||||
stageId,
|
||||
method: overrides.method ?? 'MANUAL',
|
||||
isCompleted: overrides.isCompleted ?? false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Evaluation Form Factory ───────────────────────────────────────────────
|
||||
|
||||
export async function createTestEvaluationForm(
|
||||
stageId: string,
|
||||
criteria: Array<{
|
||||
id: string
|
||||
label: string
|
||||
scale: string
|
||||
weight: number
|
||||
}> = [],
|
||||
) {
|
||||
return prisma.evaluationForm.create({
|
||||
data: {
|
||||
stageId,
|
||||
criteriaJson: criteria.length > 0
|
||||
? criteria
|
||||
: [
|
||||
{ id: 'c1', label: 'Innovation', scale: '1-10', weight: 1 },
|
||||
{ id: 'c2', label: 'Impact', scale: '1-10', weight: 1 },
|
||||
],
|
||||
isActive: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Routing Rule Factory ──────────────────────────────────────────────────
|
||||
|
||||
export async function createTestRoutingRule(
|
||||
pipelineId: string,
|
||||
destinationTrackId: string,
|
||||
overrides: Partial<{
|
||||
name: string
|
||||
priority: number
|
||||
predicateJson: Record<string, unknown>
|
||||
sourceTrackId: string
|
||||
destinationStageId: string
|
||||
isActive: boolean
|
||||
}> = {},
|
||||
) {
|
||||
const id = uid('rule')
|
||||
return prisma.routingRule.create({
|
||||
data: {
|
||||
id,
|
||||
pipelineId,
|
||||
name: overrides.name ?? `Rule ${id}`,
|
||||
destinationTrackId,
|
||||
sourceTrackId: overrides.sourceTrackId ?? null,
|
||||
destinationStageId: overrides.destinationStageId ?? null,
|
||||
predicateJson: (overrides.predicateJson ?? { field: 'project.status', operator: 'eq', value: 'SUBMITTED' }) as any,
|
||||
priority: overrides.priority ?? 0,
|
||||
isActive: overrides.isActive ?? true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Filtering Rule Factory ────────────────────────────────────────────────
|
||||
|
||||
export async function createTestFilteringRule(
|
||||
stageId: string,
|
||||
overrides: Partial<{
|
||||
name: string
|
||||
ruleType: string
|
||||
configJson: Record<string, unknown>
|
||||
priority: number
|
||||
}> = {},
|
||||
) {
|
||||
return prisma.filteringRule.create({
|
||||
data: {
|
||||
stageId,
|
||||
name: overrides.name ?? 'Test Filter Rule',
|
||||
ruleType: (overrides.ruleType as any) ?? 'DOCUMENT_CHECK',
|
||||
configJson: (overrides.configJson ?? { requiredFileTypes: ['EXEC_SUMMARY'], action: 'REJECT' }) as any,
|
||||
priority: overrides.priority ?? 0,
|
||||
isActive: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ─── COI Factory ───────────────────────────────────────────────────────────
|
||||
|
||||
export async function createTestCOI(
|
||||
assignmentId: string,
|
||||
userId: string,
|
||||
projectId: string,
|
||||
hasConflict = true,
|
||||
) {
|
||||
return prisma.conflictOfInterest.create({
|
||||
data: {
|
||||
assignmentId,
|
||||
userId,
|
||||
projectId,
|
||||
hasConflict,
|
||||
conflictType: hasConflict ? 'personal' : null,
|
||||
description: hasConflict ? 'Test conflict' : null,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Cohort + CohortProject Factory ────────────────────────────────────────
|
||||
|
||||
export async function createTestCohort(
|
||||
stageId: string,
|
||||
overrides: Partial<{
|
||||
name: string
|
||||
isOpen: boolean
|
||||
votingMode: string
|
||||
}> = {},
|
||||
) {
|
||||
const id = uid('cohort')
|
||||
return prisma.cohort.create({
|
||||
data: {
|
||||
id,
|
||||
stageId,
|
||||
name: overrides.name ?? `Cohort ${id}`,
|
||||
isOpen: overrides.isOpen ?? false,
|
||||
votingMode: overrides.votingMode ?? 'simple',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function createTestCohortProject(
|
||||
cohortId: string,
|
||||
projectId: string,
|
||||
sortOrder = 0,
|
||||
) {
|
||||
return prisma.cohortProject.create({
|
||||
data: {
|
||||
cohortId,
|
||||
projectId,
|
||||
sortOrder,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Cleanup Utility ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Cleanup all test data created by a specific test run.
|
||||
* Pass the programId to cascade-delete most related data.
|
||||
*/
|
||||
export async function cleanupTestData(programId: string, userIds: string[] = []) {
|
||||
// Delete in reverse dependency order — scoped by programId or userIds
|
||||
if (userIds.length > 0) {
|
||||
await prisma.decisionAuditLog.deleteMany({ where: { actorId: { in: userIds } } })
|
||||
await prisma.overrideAction.deleteMany({ where: { actorId: { in: userIds } } })
|
||||
await prisma.auditLog.deleteMany({ where: { userId: { in: userIds } } })
|
||||
}
|
||||
await prisma.cohortProject.deleteMany({ where: { cohort: { stage: { track: { pipeline: { programId } } } } } })
|
||||
await prisma.cohort.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
|
||||
await prisma.liveProgressCursor.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
|
||||
await prisma.filteringResult.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
|
||||
await prisma.filteringRule.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
|
||||
await prisma.filteringJob.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
|
||||
await prisma.assignmentJob.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
|
||||
await prisma.conflictOfInterest.deleteMany({ where: { assignment: { stage: { track: { pipeline: { programId } } } } } })
|
||||
await prisma.evaluation.deleteMany({ where: { assignment: { stage: { track: { pipeline: { programId } } } } } })
|
||||
await prisma.assignment.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
|
||||
await prisma.evaluationForm.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
|
||||
await prisma.fileRequirement.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
|
||||
await prisma.gracePeriod.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
|
||||
await prisma.reminderLog.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
|
||||
await prisma.evaluationSummary.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
|
||||
await prisma.evaluationDiscussion.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
|
||||
await prisma.projectStageState.deleteMany({ where: { track: { pipeline: { programId } } } })
|
||||
await prisma.stageTransition.deleteMany({ where: { fromStage: { track: { pipeline: { programId } } } } })
|
||||
await prisma.routingRule.deleteMany({ where: { pipeline: { programId } } })
|
||||
await prisma.awardEligibility.deleteMany({ where: { award: { program: { id: programId } } } })
|
||||
await prisma.awardVote.deleteMany({ where: { award: { program: { id: programId } } } })
|
||||
await prisma.awardJuror.deleteMany({ where: { award: { program: { id: programId } } } })
|
||||
await prisma.specialAward.deleteMany({ where: { programId } })
|
||||
await prisma.stage.deleteMany({ where: { track: { pipeline: { programId } } } })
|
||||
await prisma.track.deleteMany({ where: { pipeline: { programId } } })
|
||||
await prisma.pipeline.deleteMany({ where: { programId } })
|
||||
await prisma.projectStatusHistory.deleteMany({ where: { project: { programId } } })
|
||||
await prisma.projectFile.deleteMany({ where: { project: { programId } } })
|
||||
await prisma.projectTag.deleteMany({ where: { project: { programId } } })
|
||||
await prisma.project.deleteMany({ where: { programId } })
|
||||
await prisma.program.deleteMany({ where: { id: programId } })
|
||||
|
||||
if (userIds.length > 0) {
|
||||
await prisma.user.deleteMany({ where: { id: { in: userIds } } })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user