Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Rejeter une certification si le candidat n'a pas répondu à assez de questions (PIX-9980). #7511

10 changes: 10 additions & 0 deletions api/lib/domain/events/handle-certification-rescoring.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CertificationJuryDone } from './CertificationJuryDone.js';
import { checkEventTypes } from './check-event-types.js';
import { CertificationVersion } from '../../../src/shared/domain/models/CertificationVersion.js';
import { CertificationAssessmentScoreV3 } from '../models/CertificationAssessmentScoreV3.js';
import { ABORT_REASONS } from '../models/CertificationCourse.js';

const eventTypes = [ChallengeNeutralized, ChallengeDeneutralized, CertificationJuryDone];
const EMITTER = 'PIX-ALGO';
Expand Down Expand Up @@ -38,6 +39,7 @@ async function handleCertificationRescoring({
answerRepository,
certificationAssessment,
assessmentResultRepository,
certificationCourseRepository,
flashAlgorithmService,
});
}
Expand Down Expand Up @@ -70,16 +72,24 @@ async function _handleV3Certification({
answerRepository,
certificationAssessment,
assessmentResultRepository,
certificationCourseRepository,
flashAlgorithmService,
}) {
const allAnswers = await answerRepository.findByAssessment(certificationAssessment.id);
const challengeIds = allAnswers.map(({ challengeId }) => challengeId);
const challenges = await challengeRepository.getManyFlashParameters(challengeIds);

const certificationCourse = await certificationCourseRepository.get(certificationAssessment.certificationCourseId);

const abortReason = certificationCourse.isAbortReasonCandidateRelated()
? ABORT_REASONS.CANDIDATE
: ABORT_REASONS.TECHNICAL;

const certificationAssessmentScore = CertificationAssessmentScoreV3.fromChallengesAndAnswers({
challenges,
allAnswers,
flashAlgorithmService,
abortReason,
});

const assessmentResult = AssessmentResult.buildStandardAssessmentResult({
Expand Down
15 changes: 13 additions & 2 deletions api/lib/domain/events/handle-certification-scoring.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AssessmentCompleted } from './AssessmentCompleted.js';
import { checkEventTypes } from './check-event-types.js';
import { CertificationVersion } from '../../../src/shared/domain/models/CertificationVersion.js';
import { CertificationAssessmentScoreV3 } from '../models/CertificationAssessmentScoreV3.js';
import { ABORT_REASONS } from '../models/CertificationCourse.js';

const eventTypes = [AssessmentCompleted];
const EMITTER = 'PIX-ALGO';
Expand Down Expand Up @@ -67,10 +68,12 @@ async function _calculateCertificationScore({
certificationAssessment,
continueOnError: false,
});
const certificationCourse = await certificationCourseRepository.get(certificationAssessment.certificationCourseId);
await _saveResult({
certificationAssessmentScore,
certificationAssessment,
assessmentResultRepository,
certificationCourse,
certificationCourseRepository,
competenceMarkRepository,
});
Expand Down Expand Up @@ -107,16 +110,24 @@ async function _handleV3CertificationScoring({
const challengeIds = allAnswers.map(({ challengeId }) => challengeId);
const challenges = await challengeRepository.getMany(challengeIds, locale);

const certificationCourse = await certificationCourseRepository.get(certificationAssessment.certificationCourseId);

const abortReason = certificationCourse.isAbortReasonCandidateRelated()
? ABORT_REASONS.CANDIDATE
: ABORT_REASONS.TECHNICAL;

const certificationAssessmentScore = CertificationAssessmentScoreV3.fromChallengesAndAnswers({
challenges,
allAnswers,
flashAlgorithmService,
abortReason,
});

await _saveResult({
certificationAssessment,
certificationAssessmentScore,
assessmentResultRepository,
certificationCourse,
certificationCourseRepository,
competenceMarkRepository,
});
Expand All @@ -132,6 +143,7 @@ async function _saveResult({
certificationAssessment,
certificationAssessmentScore,
assessmentResultRepository,
certificationCourse,
certificationCourseRepository,
competenceMarkRepository,
}) {
Expand All @@ -148,7 +160,6 @@ async function _saveResult({
});
return competenceMarkRepository.save(competenceMarkDomain);
});
const certificationCourse = await certificationCourseRepository.get(certificationAssessment.certificationCourseId);
certificationCourse.complete({ now: new Date() });
return certificationCourseRepository.update(certificationCourse);
}
Expand Down Expand Up @@ -177,6 +188,7 @@ async function _saveResultAfterCertificationComputeError({
certificationCourseRepository,
certificationComputeError,
}) {
const certificationCourse = await certificationCourseRepository.get(certificationAssessment.certificationCourseId);
const assessmentResult = AssessmentResult.buildAlgoErrorResult({
error: certificationComputeError,
assessmentId: certificationAssessment.id,
Expand All @@ -186,7 +198,6 @@ async function _saveResultAfterCertificationComputeError({
certificationCourseId: certificationAssessment.certificationCourseId,
assessmentResult,
});
const certificationCourse = await certificationCourseRepository.get(certificationAssessment.certificationCourseId);
certificationCourse.complete({ now: new Date() });
return certificationCourseRepository.update(certificationCourse);
}
Expand Down
23 changes: 19 additions & 4 deletions api/lib/domain/models/CertificationAssessmentScoreV3.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { status } from './AssessmentResult.js';
import { status as CertificationStatus } from './AssessmentResult.js';
import { FlashAssessmentAlgorithm } from '../../../src/certification/flash-certification/domain/model/FlashAssessmentAlgorithm.js';
import { config } from '../../../src/shared/config.js';
import { ABORT_REASONS } from './CertificationCourse.js';

const MINIMUM_ESTIMATED_LEVEL = -8;
const MAXIMUM_ESTIMATED_LEVEL = 8;
Expand Down Expand Up @@ -47,12 +49,13 @@ const MAX_PIX_SCORE = 1024;
const INTERVAL_HEIGHT = MAX_PIX_SCORE / scoreIntervals.length;

class CertificationAssessmentScoreV3 {
constructor({ nbPix, percentageCorrectAnswers = 100 }) {
constructor({ nbPix, percentageCorrectAnswers = 100, status = CertificationStatus.VALIDATED }) {
this.nbPix = nbPix;
this.percentageCorrectAnswers = percentageCorrectAnswers;
this._status = status;
}

static fromChallengesAndAnswers({ challenges, allAnswers, flashAlgorithmService }) {
static fromChallengesAndAnswers({ challenges, allAnswers, flashAlgorithmService, abortReason }) {
const algorithm = new FlashAssessmentAlgorithm({
flashAlgorithmImplementation: flashAlgorithmService,
});
Expand All @@ -63,13 +66,18 @@ class CertificationAssessmentScoreV3 {

const nbPix = _computeScore(estimatedLevel);

const status = _isCertificationRejected({ answers: allAnswers, abortReason })
? CertificationStatus.REJECTED
: CertificationStatus.VALIDATED;

return new CertificationAssessmentScoreV3({
nbPix,
status,
});
}

get status() {
return status.VALIDATED;
return this._status;
}

get competenceMarks() {
Expand Down Expand Up @@ -105,4 +113,11 @@ const _computeScore = (estimatedLevel) => {
return Math.round(score);
};

const _isCertificationRejected = ({ answers, abortReason }) => {
return (
answers.length < config.v3Certification.scoring.minimumAnswersRequiredToValidateACertification &&
abortReason === ABORT_REASONS.CANDIDATE
);
};

export { CertificationAssessmentScoreV3 };
11 changes: 7 additions & 4 deletions api/lib/domain/models/CertificationCourse.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ const Joi = BaseJoi.extend(JoiDate);
import { EntityValidationError } from '../../../src/shared/domain/errors.js';
import { CertificationVersion } from '../../../src/shared/domain/models/CertificationVersion.js';

const ABORT_REASONS = ['candidate', 'technical'];
export const ABORT_REASONS = {
CANDIDATE: 'candidate',
TECHNICAL: 'technical',
};

class CertificationCourse {
constructor({
Expand Down Expand Up @@ -115,7 +118,7 @@ class CertificationCourse {

abort(reason) {
const { error } = Joi.string()
.valid(...ABORT_REASONS)
.valid(...Object.values(ABORT_REASONS))
.validate(reason);
if (error)
throw new EntityValidationError({
Expand Down Expand Up @@ -192,11 +195,11 @@ class CertificationCourse {
}

isAbortReasonCandidateRelated() {
return this._abortReason === 'candidate';
return this._abortReason === ABORT_REASONS.CANDIDATE;
}

isAbortReasonCandidateUnrelated() {
return this._abortReason === 'technical';
return this._abortReason === ABORT_REASONS.TECHNICAL;
}

isPublished() {
Expand Down
3 changes: 3 additions & 0 deletions api/src/shared/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,9 @@ const configuration = (function () {
defaultProbabilityToPickChallenge: parseInt(process.env.DEFAULT_PROBABILITY_TO_PICK_CHALLENGE, 10) || 51,
defaultCandidateCapacity: -3,
challengesBetweenSameCompetence: 2,
scoring: {
minimumAnswersRequiredToValidateACertification: 10,
},
},
version: process.env.CONTAINER_VERSION || 'development',
autonomousCourse: {
Expand Down
16 changes: 16 additions & 0 deletions api/tests/certification/shared/fixtures/challenges.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import _ from 'lodash';
import { domainBuilder } from '../../../test-helper.js';

export const generateChallengeList = ({ length }) =>
_.range(0, length).map((index) =>
domainBuilder.buildChallenge({
id: `chall${index}`,
}),
);

export const generateAnswersForChallenges = ({ challenges }) =>
challenges.map(({ id: challengeId }) =>
domainBuilder.buildAnswer({
challengeId,
}),
);
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { CertificationAssessmentScoreV3 } from '../../../../lib/domain/models/CertificationAssessmentScoreV3.js';
import { status as CertificationStatus } from '../../../../lib/domain/models/AssessmentResult.js';

const buildCertificationAssessmentScoreV3 = function ({ nbPix = 100 } = {}) {
const buildCertificationAssessmentScoreV3 = function ({ nbPix = 100, status = CertificationStatus.VALIDATED } = {}) {
return new CertificationAssessmentScoreV3({
status,
nbPix,
});
};
Expand Down
9 changes: 5 additions & 4 deletions api/tests/unit/domain/events/handle-auto-jury_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
CertificationIssueReportSubcategories,
CertificationIssueReportCategory,
} from '../../../../src/certification/shared/domain/models/CertificationIssueReportCategory.js';
import { ABORT_REASONS } from '../../../../lib/domain/models/CertificationCourse.js';

describe('Unit | Domain | Events | handle-auto-jury', function () {
it('fails when event is not of correct type', async function () {
Expand Down Expand Up @@ -221,7 +222,7 @@ describe('Unit | Domain | Events | handle-auto-jury', function () {
sessionId: 1234,
id: 4567,
completedAt: null,
abortReason: 'candidate',
abortReason: ABORT_REASONS.CANDIDATE,
});
certificationCourseRepository.findCertificationCoursesBySessionId
.withArgs({ sessionId: 1234 })
Expand Down Expand Up @@ -281,7 +282,7 @@ describe('Unit | Domain | Events | handle-auto-jury', function () {
});
const certificationCourse = domainBuilder.buildCertificationCourse({
completedAt: null,
abortReason: 'candidate',
abortReason: ABORT_REASONS.CANDIDATE,
});
certificationCourseRepository.findCertificationCoursesBySessionId
.withArgs({ sessionId: 1234 })
Expand Down Expand Up @@ -349,7 +350,7 @@ describe('Unit | Domain | Events | handle-auto-jury', function () {
});
const certificationCourse = domainBuilder.buildCertificationCourse({
completedAt: null,
abortReason: 'technical',
abortReason: ABORT_REASONS.TECHNICAL,
});
certificationCourseRepository.findCertificationCoursesBySessionId
.withArgs({ sessionId: 1234 })
Expand Down Expand Up @@ -419,7 +420,7 @@ describe('Unit | Domain | Events | handle-auto-jury', function () {
});
const certificationCourse = domainBuilder.buildCertificationCourse({
completedAt: null,
abortReason: 'candidate',
abortReason: ABORT_REASONS.CANDIDATE,
});
certificationCourseRepository.findCertificationCoursesBySessionId
.withArgs({ sessionId: 1234 })
Expand Down
Loading