Skip to content

Commit

Permalink
✨ new cancelled event
Browse files Browse the repository at this point in the history
Co-authored-by: Alexandre COIN <[email protected]>
Co-authored-by: Andreia Pena <[email protected]>
  • Loading branch information
3 people committed Jan 16, 2025
1 parent 691a104 commit 1ee1841
Show file tree
Hide file tree
Showing 14 changed files with 359 additions and 39 deletions.
14 changes: 14 additions & 0 deletions api/lib/domain/events/CertificationCancelled.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { assertNotNullOrUndefined } from '../../../src/shared/domain/models/asserts.js';

export default class CertificationCancelled {
/**
* @param {Object} params
* @param {number} params.certificationCourseId - certification course that will be rescored
* @param {number} params.juryId - Id of the jury member who cancelled the certification
*/
constructor({ certificationCourseId, juryId }) {
assertNotNullOrUndefined(certificationCourseId);
this.certificationCourseId = certificationCourseId;
this.juryId = juryId;
}
}
13 changes: 13 additions & 0 deletions api/lib/domain/events/handle-certification-rescoring.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/**
* @typedef {import('./index.js').CertificationAssessmentRepository} CertificationAssessmentRepository
*/
import { ChallengeDeneutralized } from '../../../src/certification/evaluation/domain/events/ChallengeDeneutralized.js';
import { ChallengeNeutralized } from '../../../src/certification/evaluation/domain/events/ChallengeNeutralized.js';
import { services } from '../../../src/certification/evaluation/domain/services/index.js';
Expand All @@ -9,6 +12,7 @@ import { AlgorithmEngineVersion } from '../../../src/certification/shared/domain
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';
import CertificationCancelled from './CertificationCancelled.js';
import { CertificationCourseUnrejected } from './CertificationCourseUnrejected.js';
import { CertificationRescoringCompleted } from './CertificationRescoringCompleted.js';
import { checkEventTypes } from './check-event-types.js';
Expand All @@ -19,9 +23,14 @@ const eventTypes = [
CertificationJuryDone,
CertificationCourseRejected,
CertificationCourseUnrejected,
CertificationCancelled,
CertificationRescoredByScript,
];

/**
* @param {Object} params
* @param {CertificationAssessmentRepository} params.certificationAssessmentRepository
*/
async function handleCertificationRescoring({
event,
assessmentResultRepository,
Expand Down Expand Up @@ -189,6 +198,10 @@ function _getEmitterFromEvent(event) {
emitter = CertificationResult.emitters.PIX_ALGO_FRAUD_REJECTION;
}

if (event instanceof CertificationCancelled) {
emitter = CertificationResult.emitters.PIX_ALGO_CANCELLATION;
}

return emitter;
}

Expand Down
4 changes: 4 additions & 0 deletions api/lib/domain/events/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ import { handleComplementaryCertificationsScoring } from './handle-complementary

const { performance } = perf_hooks;

/**
* @typedef {certificationAssessmentRepository} CertificationAssessmentRepository
*/

const dependencies = {
answerRepository,
assessmentRepository,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@

import _ from 'lodash';

import { AssessmentResult } from '../../../../../shared/domain/models/AssessmentResult.js';
import CertificationCancelled from '../../../../../../lib/domain/events/CertificationCancelled.js';
import { AssessmentResult } from '../../../../../shared/domain/models/index.js';
import {
AnswerCollectionForScoring,
CertificationAssessmentScore,
Expand Down Expand Up @@ -66,9 +67,12 @@ export const handleV2CertificationScoring = async ({
id: certificationAssessment.certificationCourseId,
});

const isCancelled = event instanceof CertificationCancelled;

const assessmentResult = _createV2AssessmentResult({
juryId: event?.juryId,
emitter,
isCancelled,
certificationCourse,
certificationAssessment,
certificationAssessmentScore,
Expand Down Expand Up @@ -258,11 +262,22 @@ function _getResult(answers, certificationChallenges, testedCompetences, allArea
function _createV2AssessmentResult({
juryId,
emitter,
isCancelled,
certificationCourse,
certificationAssessmentScore,
certificationAssessment,
scoringCertificationService,
}) {
if (isCancelled) {
return AssessmentResultFactory.buildCancelledAssessmentResult({
juryId,
pixScore: certificationAssessmentScore.nbPix,
reproducibilityRate: certificationAssessmentScore.getPercentageCorrectAnswers(),
assessmentId: certificationAssessment.id,
emitter,
});
}

if (certificationCourse.isRejectedForFraud()) {
return AssessmentResultFactory.buildFraud({
pixScore: certificationAssessmentScore.nbPix,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ export class AssessmentResultFactory {
});
}

static buildCancelledAssessmentResult({ pixScore, reproducibilityRate, assessmentId, juryId, emitter }) {
return new AssessmentResult({
emitter,
pixScore,
reproducibilityRate,
status: AssessmentResult.status.CANCELLED,
assessmentId,
juryId,
});
}

static buildStandardAssessmentResult({ pixScore, reproducibilityRate, status, assessmentId, juryId, emitter }) {
return new AssessmentResult({
emitter,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import * as events from '../../../../lib/domain/events/index.js';
import { usecases } from '../domain/usecases/index.js';

const cancel = async function (request, h) {
const cancel = async function (request, h, dependencies = { events }) {
const juryId = request.auth.credentials.userId;
const certificationCourseId = request.params.certificationCourseId;
await usecases.cancelCertificationCourse({ certificationCourseId });
const certificationCancelledEvent = await usecases.cancelCertificationCourse({ certificationCourseId, juryId });
await dependencies.events.eventDispatcher.dispatch(certificationCancelledEvent);

return h.response().code(204);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
const cancelCertificationCourse = async function ({ certificationCourseId, certificationCourseRepository }) {
/**
* @typedef {import('./index.js'.CertificationCourseRepository} CertificationCourseRepository
*/

import CertificationCancelled from '../../../../../lib/domain/events/CertificationCancelled.js';

/**
* @param {Object} params
* @param {number} params.certificationCourseId
* @param {CertificationCourseRepository} params.certificationCourseRepository
* @returns {Promise<CertificationCancelled>}
*/
export const cancelCertificationCourse = async function ({
certificationCourseId,
juryId,
certificationCourseRepository,
}) {
const certificationCourse = await certificationCourseRepository.get({ id: certificationCourseId });
certificationCourse.cancel();
await certificationCourseRepository.update({ certificationCourse });
};

export { cancelCertificationCourse };
return new CertificationCancelled({ certificationCourseId: certificationCourse.getId(), juryId });
};
1 change: 1 addition & 0 deletions api/src/shared/domain/models/CertificationResult.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const emitters = {
PIX_ALGO_AUTO_JURY: 'PIX-ALGO-AUTO-JURY',
PIX_ALGO_NEUTRALIZATION: 'PIX-ALGO-NEUTRALIZATION',
PIX_ALGO_FRAUD_REJECTION: 'PIX-ALGO-FRAUD-REJECTION',
PIX_ALGO_CANCELLATION: 'PIX-ALGO-CANCELLATION',
};

class CertificationResult {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
JuryCommentContexts,
} from '../../../certification/shared/domain/models/JuryComment.js';
import { DomainTransaction } from '../../../shared/domain/DomainTransaction.js';
import { AssessmentResultNotCreatedError, MissingAssessmentId, NotFoundError } from '../../domain/errors.js';
import { MissingAssessmentId, NotFoundError } from '../../domain/errors.js';
import { AssessmentResult } from '../../domain/models/AssessmentResult.js';

function _toDomain({ assessmentResultDTO, competencesMarksDTO }) {
Expand Down Expand Up @@ -47,33 +47,30 @@ const save = async function ({ certificationCourseId, assessmentResult }) {
if (_.isNil(assessmentId)) {
throw new MissingAssessmentId();
}
try {
const knexConn = DomainTransaction.getConnection();
const [savedAssessmentResultData] = await knexConn('assessment-results')
.insert({
pixScore,
reproducibilityRate,
status,
emitter,
commentByJury,
id,
juryId,
assessmentId,
commentForCandidate: assessmentResult.commentForCandidate?.fallbackComment,
commentForOrganization: assessmentResult.commentForOrganization?.fallbackComment,
commentByAutoJury,
})
.returning('*');

await knexConn('certification-courses-last-assessment-results')
.insert({ certificationCourseId, lastAssessmentResultId: savedAssessmentResultData.id })
.onConflict('certificationCourseId')
.merge(['lastAssessmentResultId']);

return _toDomain({ assessmentResultDTO: savedAssessmentResultData, competencesMarksDTO: [] });
} catch {
throw new AssessmentResultNotCreatedError();
}

const knexConn = DomainTransaction.getConnection();
const [savedAssessmentResultData] = await knexConn('assessment-results')
.insert({
pixScore,
reproducibilityRate,
status,
emitter,
commentByJury,
id,
juryId,
assessmentId,
commentForCandidate: assessmentResult.commentForCandidate?.fallbackComment,
commentForOrganization: assessmentResult.commentForOrganization?.fallbackComment,
commentByAutoJury,
})
.returning('*');

await knexConn('certification-courses-last-assessment-results')
.insert({ certificationCourseId, lastAssessmentResultId: savedAssessmentResultData.id })
.onConflict('certificationCourseId')
.merge(['lastAssessmentResultId']);

return _toDomain({ assessmentResultDTO: savedAssessmentResultData, competencesMarksDTO: [] });
};

const findLatestLevelAndPixScoreByAssessmentId = async function ({ assessmentId, limitDate }) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import _ from 'lodash';

import CertificationCancelled from '../../../../../../../lib/domain/events/CertificationCancelled.js';
import { ChallengeDeneutralized } from '../../../../../../../src/certification/evaluation/domain/events/ChallengeDeneutralized.js';
import { ChallengeNeutralized } from '../../../../../../../src/certification/evaluation/domain/events/ChallengeNeutralized.js';
import {
Expand Down Expand Up @@ -393,6 +394,66 @@ describe('Certification | Shared | Unit | Domain | Services | Scoring V2', funct
});
});
});

context('when certification is cancelled', function () {
it('builds and save a cancelled assessment result', async function () {
// given
const certificationCourseId = 123;
const juryId = 456;
const event = new CertificationCancelled({ certificationCourseId, juryId });
const certificationCourse = domainBuilder.buildCertificationCourse({
id: certificationCourseId,
abortReason: null,
isCancelled: true,
});
const certificationAssessmentScore = domainBuilder.buildCertificationAssessmentScore({
competenceMarks: [],
percentageCorrectAnswers: 49,
hasEnoughNonNeutralizedChallengesToBeTrusted: true,
});
const certificationAssessment = domainBuilder.buildCertificationAssessment({
id: 45674567,
certificationCourseId,
userId: 4567,
});
const savedAssessmentResult = { id: 123123 };

dependencies.calculateCertificationAssessmentScore.resolves(certificationAssessmentScore);
scoringCertificationService.isLackOfAnswersForTechnicalReason.returns(false);
certificationCourseRepository.get
.withArgs({ id: certificationAssessment.certificationCourseId })
.resolves(certificationCourse);
assessmentResultRepository.save.resolves(savedAssessmentResult);
competenceMarkRepository.save.resolves();

// when
await handleV2CertificationScoring({
event,
emitter: CertificationResult.emitters.PIX_ALGO_CANCELLATION,
certificationAssessment,
assessmentResultRepository,
certificationCourseRepository,
competenceMarkRepository,
scoringCertificationService,
dependencies,
});

// then
const expectedAssessmentResult = new AssessmentResult({
pixScore: 0,
reproducibilityRate: certificationAssessmentScore.getPercentageCorrectAnswers(),
status: AssessmentResult.status.CANCELLED,
assessmentId: certificationAssessment.id,
emitter: CertificationResult.emitters.PIX_ALGO_CANCELLATION,
juryId,
});

expect(assessmentResultRepository.save).to.have.been.calledWithExactly({
certificationCourseId: 123,
assessmentResult: expectedAssessmentResult,
});
});
});
});

context('for rescoring certification', function () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,32 @@ describe('Certification | Scoring | Unit | Domain | Factories | AssessmentResult
});
});

describe('#buildCancelled', function () {
it('should return a cancelled AssessmentResult', function () {
// when
const actualAssessmentResult = AssessmentResultFactory.buildCancelledAssessmentResult({
pixScore: 55,
reproducibilityRate: 50.25,
assessmentId: 123,
juryId: 456,
emitter: CertificationResult.emitters.PIX_ALGO_CANCELLATION,
});

// then
const expectedAssessmentResult = domainBuilder.buildAssessmentResult({
assessmentId: 123,
juryId: 456,
emitter: CertificationResult.emitters.PIX_ALGO_CANCELLATION,
status: AssessmentResult.status.CANCELLED,
pixScore: 55,
reproducibilityRate: 50.25,
});
expectedAssessmentResult.id = undefined;
expectedAssessmentResult.createdAt = undefined;
expect(actualAssessmentResult).to.deepEqualInstance(expectedAssessmentResult);
});
});

describe('#buildNotTrustableAssessmentResult', function () {
it('should return a not trustable AssessmentResult', function () {
// when
Expand Down
Loading

0 comments on commit 1ee1841

Please sign in to comment.