diff --git a/src/services/ChallengePhaseService.js b/src/services/ChallengePhaseService.js index 380fb29..6710e54 100644 --- a/src/services/ChallengePhaseService.js +++ b/src/services/ChallengePhaseService.js @@ -31,6 +31,10 @@ const PHASE_RESOURCE_ROLE_REQUIREMENTS = Object.freeze({ review: "Reviewer", "checkpoint review": "Checkpoint Reviewer", }); +const SUBMISSION_PHASE_NAME_SET = new Set(["submission", "topgear submission"]); +const REGISTRATION_PHASE_NAME = "registration"; + +const normalizePhaseName = (name) => String(name || "").trim().toLowerCase(); async function hasPendingScorecardsForPhase(challengePhaseId) { if (!config.REVIEW_DB_URL) { @@ -561,7 +565,14 @@ async function partiallyUpdateChallengePhase(currentUser, challengeId, id, data) return reopenedPhaseIdentifiers.has(String(phase.predecessor)); }); - if (dependentOpenPhases.length === 0) { + const normalizedPhaseName = normalizePhaseName(phaseName); + const hasSubmissionVariantOpen = openPhases.some((phase) => + SUBMISSION_PHASE_NAME_SET.has(normalizePhaseName(phase?.name)) + ); + const allowRegistrationReopenWithoutExplicitDependency = + normalizedPhaseName === REGISTRATION_PHASE_NAME && hasSubmissionVariantOpen; + + if (dependentOpenPhases.length === 0 && !allowRegistrationReopenWithoutExplicitDependency) { throw new errors.ForbiddenError( `Cannot reopen ${phaseName} because no currently open phase depends on it` ); diff --git a/test/unit/ChallengePhaseService.test.js b/test/unit/ChallengePhaseService.test.js index df2af9d..46b21f8 100644 --- a/test/unit/ChallengePhaseService.test.js +++ b/test/unit/ChallengePhaseService.test.js @@ -330,7 +330,7 @@ describe('challenge phase service unit tests', () => { } }) - it('partially update challenge phase - cannot reopen when open phase is not a successor', async () => { + it('partially update challenge phase - can reopen registration when submission is open', async () => { const startDate = new Date('2025-06-01T00:00:00.000Z') const endDate = new Date('2025-06-02T00:00:00.000Z') @@ -350,6 +350,60 @@ describe('challenge phase service unit tests', () => { } }) + try { + const challengePhase = await service.partiallyUpdateChallengePhase( + authUser, + data.challenge.id, + data.challengePhase1Id, + { + isOpen: true + } + ) + should.equal(challengePhase.id, data.challengePhase1Id) + should.equal(challengePhase.isOpen, true) + should.equal(challengePhase.actualEndDate, null) + } finally { + await prisma.challengePhase.update({ + where: { id: data.challengePhase1Id }, + data: { + isOpen: false, + actualStartDate: startDate, + actualEndDate: endDate + } + }) + await prisma.challengePhase.update({ + where: { id: data.challengePhase2Id }, + data: { + isOpen: false, + predecessor: data.challengePhase1Id, + name: 'Submission' + } + }) + } + + }) + + it('partially update challenge phase - cannot reopen when open phase is not a successor or submission variant', async () => { + const startDate = new Date('2025-06-01T00:00:00.000Z') + const endDate = new Date('2025-06-02T00:00:00.000Z') + + await prisma.challengePhase.update({ + where: { id: data.challengePhase1Id }, + data: { + isOpen: false, + actualStartDate: startDate, + actualEndDate: endDate + } + }) + await prisma.challengePhase.update({ + where: { id: data.challengePhase2Id }, + data: { + isOpen: true, + predecessor: null, + name: 'Review' + } + }) + try { await service.partiallyUpdateChallengePhase(authUser, data.challenge.id, data.challengePhase1Id, { isOpen: true @@ -366,7 +420,16 @@ describe('challenge phase service unit tests', () => { where: { id: data.challengePhase2Id }, data: { isOpen: false, - predecessor: data.challengePhase1Id + predecessor: data.challengePhase1Id, + name: 'Submission' + } + }) + await prisma.challengePhase.update({ + where: { id: data.challengePhase1Id }, + data: { + isOpen: false, + actualStartDate: startDate, + actualEndDate: endDate } }) }