From 36503c722539290f93c43ac1da996039d7aeaa8e Mon Sep 17 00:00:00 2001 From: P-Jeremy Date: Thu, 14 Nov 2024 13:14:51 +0100 Subject: [PATCH 1/3] :sparkles: api: add method on certification assessment to determine if assessment should be scored --- .../domain/models/CertificationAssessment.js | 10 ++ .../models/CertificationAssessment_test.js | 154 ++++++++++++++++++ 2 files changed, 164 insertions(+) diff --git a/api/src/certification/session-management/domain/models/CertificationAssessment.js b/api/src/certification/session-management/domain/models/CertificationAssessment.js index 23311ce8ca5..a9910e53fb7 100644 --- a/api/src/certification/session-management/domain/models/CertificationAssessment.js +++ b/api/src/certification/session-management/domain/models/CertificationAssessment.js @@ -179,6 +179,16 @@ class CertificationAssessment { return [states.STARTED, states.ENDED_BY_SUPERVISOR, states.ENDED_DUE_TO_FINALIZATION]; } + get isComplementaryOnly() { + return this.certificationChallenges.every( + (certificationChallenge) => certificationChallenge.certifiableBadgeKey !== null, + ); + } + + get isScoringBlockedDueToComplementaryOnlyChallenges() { + return this.isComplementaryOnly; + } + _getLastChallenge() { return _.orderBy(this.certificationChallenges, 'createdAt', 'desc')[0]; } diff --git a/api/tests/certification/session-management/unit/domain/models/CertificationAssessment_test.js b/api/tests/certification/session-management/unit/domain/models/CertificationAssessment_test.js index fb6a3d13df7..2d48ba7aab2 100644 --- a/api/tests/certification/session-management/unit/domain/models/CertificationAssessment_test.js +++ b/api/tests/certification/session-management/unit/domain/models/CertificationAssessment_test.js @@ -911,4 +911,158 @@ describe('Unit | Domain | Models | CertificationAssessment', function () { ]); }); }); + + describe('#get isComplementaryOnly', function () { + it('should return true if challenges are only complementary', function () { + //given + const certificationAssessment = domainBuilder.buildCertificationAssessment({ + certificationChallenges: [ + domainBuilder.buildCertificationChallengeWithType({ + challengeId: 'rec123', + certifiableBadgeKey: 'TOTO', + }), + domainBuilder.buildCertificationChallengeWithType({ + challengeId: 'rec456', + certifiableBadgeKey: 'TOTO', + }), + domainBuilder.buildCertificationChallengeWithType({ + challengeId: 'rec789', + certifiableBadgeKey: 'TOTO', + }), + ], + certificationAnswersByDate: [ + domainBuilder.buildAnswer({ + challengeId: 'rec123', + }), + domainBuilder.buildAnswer({ + challengeId: 'rec456', + }), + domainBuilder.buildAnswer({ + challengeId: 'rec789', + }), + ], + }); + + // when + const isComplementaryOnly = certificationAssessment.isComplementaryOnly; + + // then + expect(isComplementaryOnly).to.be.true; + }); + + it('should return false if challenges are not complementary', function () { + //given + const certificationAssessment = domainBuilder.buildCertificationAssessment({ + certificationChallenges: [ + domainBuilder.buildCertificationChallengeWithType({ + challengeId: 'rec123', + certifiableBadgeKey: null, + }), + domainBuilder.buildCertificationChallengeWithType({ + challengeId: 'rec456', + certifiableBadgeKey: null, + }), + domainBuilder.buildCertificationChallengeWithType({ + challengeId: 'rec789', + certifiableBadgeKey: null, + }), + ], + certificationAnswersByDate: [ + domainBuilder.buildAnswer({ + challengeId: 'rec123', + }), + domainBuilder.buildAnswer({ + challengeId: 'rec456', + }), + domainBuilder.buildAnswer({ + challengeId: 'rec789', + }), + ], + }); + + // when + const isComplementaryOnly = certificationAssessment.isComplementaryOnly; + + // then + expect(isComplementaryOnly).to.be.false; + }); + }); + + describe('#get isScoringBlockedDueToComplementaryOnlyChallenges', function () { + it('should return true if challenges are only complementary', function () { + //given + const certificationAssessment = domainBuilder.buildCertificationAssessment({ + certificationChallenges: [ + domainBuilder.buildCertificationChallengeWithType({ + challengeId: 'rec123', + certifiableBadgeKey: 'TOTO', + }), + domainBuilder.buildCertificationChallengeWithType({ + challengeId: 'rec456', + certifiableBadgeKey: 'TOTO', + }), + domainBuilder.buildCertificationChallengeWithType({ + challengeId: 'rec789', + certifiableBadgeKey: 'TOTO', + }), + ], + certificationAnswersByDate: [ + domainBuilder.buildAnswer({ + challengeId: 'rec123', + }), + domainBuilder.buildAnswer({ + challengeId: 'rec456', + }), + domainBuilder.buildAnswer({ + challengeId: 'rec789', + }), + ], + }); + + // when + const isScoringBlockedDueToComplementaryOnlyChallenges = + certificationAssessment.isScoringBlockedDueToComplementaryOnlyChallenges; + + // then + expect(isScoringBlockedDueToComplementaryOnlyChallenges).to.be.true; + }); + + it('should return false if challenges are not complementary', function () { + //given + const certificationAssessment = domainBuilder.buildCertificationAssessment({ + certificationChallenges: [ + domainBuilder.buildCertificationChallengeWithType({ + challengeId: 'rec123', + certifiableBadgeKey: null, + }), + domainBuilder.buildCertificationChallengeWithType({ + challengeId: 'rec456', + certifiableBadgeKey: null, + }), + domainBuilder.buildCertificationChallengeWithType({ + challengeId: 'rec789', + certifiableBadgeKey: null, + }), + ], + certificationAnswersByDate: [ + domainBuilder.buildAnswer({ + challengeId: 'rec123', + }), + domainBuilder.buildAnswer({ + challengeId: 'rec456', + }), + domainBuilder.buildAnswer({ + challengeId: 'rec789', + }), + ], + }); + + // when + const isScoringBlockedDueToComplementaryOnlyChallenges = + certificationAssessment.isScoringBlockedDueToComplementaryOnlyChallenges; + + // then + expect(isScoringBlockedDueToComplementaryOnlyChallenges).to.be.false; + }); + }); }); From 8a36fbbf467bd793d161bef601ce31f99c8c1bfa Mon Sep 17 00:00:00 2001 From: P-Jeremy Date: Thu, 14 Nov 2024 13:32:14 +0100 Subject: [PATCH 2/3] :sparkles: api: block scoring on complementary certification only on assessment completed --- .../certification-completed-job-controller.js | 4 ++ ...ification-completed-job-controller_test.js | 51 ++++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/api/src/certification/evaluation/application/jobs/certification-completed-job-controller.js b/api/src/certification/evaluation/application/jobs/certification-completed-job-controller.js index c16567a5ea3..c92317bdd33 100644 --- a/api/src/certification/evaluation/application/jobs/certification-completed-job-controller.js +++ b/api/src/certification/evaluation/application/jobs/certification-completed-job-controller.js @@ -43,6 +43,10 @@ export class CertificationCompletedJobController extends JobController { const certificationAssessment = await certificationAssessmentRepository.get(assessmentId); let certificationScoringCompletedEvent; + if (certificationAssessment.isScoringBlockedDueToComplementaryOnlyChallenges) { + return; + } + if (AlgorithmEngineVersion.isV3(certificationAssessment.version)) { certificationScoringCompletedEvent = await _handleV3CertificationScoring({ certificationAssessment, diff --git a/api/tests/certification/evaluation/unit/application/jobs/certification-completed-job-controller_test.js b/api/tests/certification/evaluation/unit/application/jobs/certification-completed-job-controller_test.js index e79d317c588..ae0eda8d265 100644 --- a/api/tests/certification/evaluation/unit/application/jobs/certification-completed-job-controller_test.js +++ b/api/tests/certification/evaluation/unit/application/jobs/certification-completed-job-controller_test.js @@ -2,6 +2,7 @@ import { CertificationCompletedJob } from '../../../../../../lib/domain/events/C import { CertificationScoringCompleted } from '../../../../../../lib/domain/events/CertificationScoringCompleted.js'; import { CertificationCompletedJobController } from '../../../../../../src/certification/evaluation/application/jobs/certification-completed-job-controller.js'; import { AssessmentResultFactory } from '../../../../../../src/certification/scoring/domain/models/factories/AssessmentResultFactory.js'; +import { AlgorithmEngineVersion } from '../../../../../../src/certification/shared/domain/models/AlgorithmEngineVersion.js'; import { ABORT_REASONS, CertificationCourse, @@ -81,6 +82,7 @@ describe('Unit | Certification | Application | jobs | CertificationCompletedJobC id: assessmentId, certificationCourseId, userId, + version: AlgorithmEngineVersion.V2, }); certificationAssessmentRepository.get.withArgs(assessmentId).resolves(certificationAssessment); }); @@ -265,6 +267,53 @@ describe('Unit | Certification | Application | jobs | CertificationCompletedJobC }); }); }); + + context('when assessment only has only complementary certification challenges', function () { + it('should return', async function () { + // given + const certificationAssessmentWithOnlyComplementaryCertificationChallenges = + domainBuilder.buildCertificationAssessment({ + id: assessmentId, + certificationCourseId, + userId, + version: AlgorithmEngineVersion.V2, + certificationChallenges: [ + domainBuilder.buildCertificationChallengeWithType({ + id: 1234, + certifiableBadgeKey: 'TOTO', + }), + domainBuilder.buildCertificationChallengeWithType({ + id: 567, + certifiableBadgeKey: 'TOTO', + }), + domainBuilder.buildCertificationChallengeWithType({ + id: 8910, + certifiableBadgeKey: 'TOTO', + }), + ], + }); + certificationAssessmentRepository.get + .withArgs(assessmentId) + .resolves(certificationAssessmentWithOnlyComplementaryCertificationChallenges); + + const dependencies = { + assessmentResultRepository, + certificationAssessmentRepository, + certificationCourseRepository, + competenceMarkRepository, + scoringCertificationService, + services, + events, + }; + + // when + await certificationCompletedJobController.handle({ data, dependencies }); + + // then + expect(certificationCourseRepository.update).to.not.have.been.called; + expect(events.eventDispatcher.dispatch).to.not.have.been.called; + }); + }); }); context('when certification is V3', function () { @@ -285,7 +334,7 @@ describe('Unit | Certification | Application | jobs | CertificationCompletedJobC certificationCourseId, userId, createdAt: Symbol('someCreationDate'), - version: 3, + version: AlgorithmEngineVersion.V3, }; certificationAssessmentRepository.get.withArgs(assessmentId).resolves(certificationAssessment); certificationCourse = domainBuilder.buildCertificationCourse({ From 68d6f1af565ccfdf39c5131fcdfb4c2b96676d4e Mon Sep 17 00:00:00 2001 From: P-Jeremy Date: Thu, 14 Nov 2024 14:26:39 +0100 Subject: [PATCH 3/3] :sparkles: api: block rescoring on complementary certification only --- .../events/handle-certification-rescoring.js | 9 ++-- .../handle-certification-rescoring_test.js | 51 ++++++++++++++++--- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/api/lib/domain/events/handle-certification-rescoring.js b/api/lib/domain/events/handle-certification-rescoring.js index 3a68ecc08f9..e3f121f6ce8 100644 --- a/api/lib/domain/events/handle-certification-rescoring.js +++ b/api/lib/domain/events/handle-certification-rescoring.js @@ -1,5 +1,5 @@ import { AssessmentResultFactory } from '../../../src/certification/scoring/domain/models/factories/AssessmentResultFactory.js'; -import { SessionVersion } from '../../../src/certification/shared/domain/models/SessionVersion.js'; +import { AlgorithmEngineVersion } from '../../../src/certification/shared/domain/models/AlgorithmEngineVersion.js'; import { V3_REPRODUCIBILITY_RATE } from '../../../src/shared/domain/constants.js'; import { CertificationComputeError } from '../../../src/shared/domain/errors.js'; import { CertificationResult } from '../../../src/shared/domain/models/CertificationResult.js'; @@ -35,8 +35,11 @@ async function handleCertificationRescoring({ certificationCourseId: event.certificationCourseId, }); - // TODO: switch to certif-course version, not session - if (SessionVersion.isV3(certificationAssessment.version)) { + if (certificationAssessment.isScoringBlockedDueToComplementaryOnlyChallenges) { + return; + } + + if (AlgorithmEngineVersion.isV3(certificationAssessment.version)) { return _handleV3CertificationScoring({ certificationAssessment, event, diff --git a/api/tests/unit/domain/events/handle-certification-rescoring_test.js b/api/tests/unit/domain/events/handle-certification-rescoring_test.js index e14a3f84751..819604efaa5 100644 --- a/api/tests/unit/domain/events/handle-certification-rescoring_test.js +++ b/api/tests/unit/domain/events/handle-certification-rescoring_test.js @@ -4,8 +4,8 @@ import { ChallengeDeneutralized } from '../../../../lib/domain/events/ChallengeD import { ChallengeNeutralized } from '../../../../lib/domain/events/ChallengeNeutralized.js'; import { _forTestOnly } from '../../../../lib/domain/events/index.js'; import { CertificationAssessment } from '../../../../src/certification/session-management/domain/models/CertificationAssessment.js'; +import { AlgorithmEngineVersion } from '../../../../src/certification/shared/domain/models/AlgorithmEngineVersion.js'; import { ABORT_REASONS } from '../../../../src/certification/shared/domain/models/CertificationCourse.js'; -import { SESSIONS_VERSIONS } from '../../../../src/certification/shared/domain/models/SessionVersion.js'; import { CertificationComputeError } from '../../../../src/shared/domain/errors.js'; import { AssessmentResult, CertificationResult } from '../../../../src/shared/domain/models/index.js'; import { domainBuilder, expect, sinon } from '../../../test-helper.js'; @@ -55,7 +55,7 @@ describe('Unit | Domain | Events | handle-certification-rescoring', function () it('should save the score with a rejected status', async function () { // given const certificationAssessment = domainBuilder.buildCertificationAssessment({ - version: SESSIONS_VERSIONS.V3, + version: AlgorithmEngineVersion.V3, }); const abortedCertificationCourse = domainBuilder.buildCertificationCourse({ abortReason: ABORT_REASONS.CANDIDATE, @@ -91,7 +91,7 @@ describe('Unit | Domain | Events | handle-certification-rescoring', function () it('should save the score with a rejected status and cancel the certification course', async function () { // given const certificationAssessment = domainBuilder.buildCertificationAssessment({ - version: SESSIONS_VERSIONS.V3, + version: AlgorithmEngineVersion.V3, }); const abortedCertificationCourse = domainBuilder.buildCertificationCourse({ @@ -142,7 +142,7 @@ describe('Unit | Domain | Events | handle-certification-rescoring', function () // given const certificationCourseStartDate = new Date('2022-01-01'); const certificationAssessment = domainBuilder.buildCertificationAssessment({ - version: SESSIONS_VERSIONS.V3, + version: AlgorithmEngineVersion.V3, }); const abortedCertificationCourse = domainBuilder.buildCertificationCourse({ @@ -183,7 +183,7 @@ describe('Unit | Domain | Events | handle-certification-rescoring', function () // given const certificationCourseStartDate = new Date('2022-01-01'); const certificationAssessment = domainBuilder.buildCertificationAssessment({ - version: SESSIONS_VERSIONS.V3, + version: AlgorithmEngineVersion.V3, }); const abortedCertificationCourse = domainBuilder.buildCertificationCourse({ @@ -221,7 +221,7 @@ describe('Unit | Domain | Events | handle-certification-rescoring', function () const certificationCourseStartDate = new Date('2022-01-01'); // given const certificationAssessment = domainBuilder.buildCertificationAssessment({ - version: SESSIONS_VERSIONS.V3, + version: AlgorithmEngineVersion.V3, }); const abortedCertificationCourse = domainBuilder.buildCertificationCourse({ @@ -748,5 +748,44 @@ describe('Unit | Domain | Events | handle-certification-rescoring', function () expect(assessmentResultRepository.save).to.have.been.calledOnce; }); }); + + context('when assessment in only about complementary certification', function () { + it('should return', async function () { + // given + const event = new ChallengeNeutralized({ certificationCourseId: 1, juryId: 7 }); + const certificationAssessment = new CertificationAssessment({ + id: 123, + userId: 123, + certificationCourseId: 789, + createdAt: new Date('2020-01-01'), + completedAt: new Date('2020-01-01'), + state: CertificationAssessment.states.STARTED, + version: AlgorithmEngineVersion.V2, + certificationChallenges: [ + domainBuilder.buildCertificationChallengeWithType({ certifiableBadgeKey: 'TOTO' }), + domainBuilder.buildCertificationChallengeWithType({ certifiableBadgeKey: 'TOTO' }), + ], + certificationAnswersByDate: ['answer'], + }); + certificationAssessmentRepository.getByCertificationCourseId + .withArgs({ certificationCourseId: 1 }) + .resolves(certificationAssessment); + + // when + await handleCertificationRescoring({ + event, + assessmentResultRepository, + certificationAssessmentRepository, + competenceMarkRepository, + scoringCertificationService, + certificationEvaluationServices, + certificationCourseRepository, + }); + + // then + expect(certificationEvaluationServices.handleV2CertificationScoring).to.not.have.been.called; + expect(assessmentResultRepository.save).to.not.have.been.called; + }); + }); }); });