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

[TECH] Créer un nouvel assessment result lorsqu'on annule et désannule une certification sur Pix Admin (PIX-16045). #11131

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 '../../../../../../src/shared/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 toBeCancelled = event instanceof CertificationCancelled;

const assessmentResult = _createV2AssessmentResult({
juryId: event?.juryId,
emitter,
toBeCancelled,
certificationCourse,
certificationAssessment,
certificationAssessmentScore,
Expand Down Expand Up @@ -258,11 +262,22 @@ function _getResult(answers, certificationChallenges, testedCompetences, allArea
function _createV2AssessmentResult({
juryId,
emitter,
toBeCancelled,
certificationCourse,
certificationAssessmentScore,
certificationAssessment,
scoringCertificationService,
}) {
if (toBeCancelled) {
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,7 @@
*/
import Debug from 'debug';

import CertificationCancelled from '../../../../../../src/shared/domain/events/CertificationCancelled.js';
import { config } from '../../../../../shared/config.js';
import { CompetenceMark } from '../../../../../shared/domain/models/index.js';
import { FlashAssessmentAlgorithm } from '../../../../flash-certification/domain/models/FlashAssessmentAlgorithm.js';
Expand Down Expand Up @@ -58,6 +59,8 @@ export const handleV3CertificationScoring = async ({
const candidateAnswers = await answerRepository.findByAssessment(assessmentId);
debugScoringForV3Certification(`CandidateAnswers count: ${candidateAnswers.length}`);

const toBeCancelled = event instanceof CertificationCancelled;

const { allChallenges, askedChallenges, challengeCalibrations } = await dependencies.findByCertificationCourseId({
certificationCourseId,
});
Expand Down Expand Up @@ -93,6 +96,7 @@ export const handleV3CertificationScoring = async ({
});

const assessmentResult = await _createV3AssessmentResult({
toBeCancelled,
allAnswers: candidateAnswers,
emitter,
certificationAssessment,
Expand Down Expand Up @@ -125,13 +129,24 @@ export const handleV3CertificationScoring = async ({
};

function _createV3AssessmentResult({
toBeCancelled,
allAnswers,
emitter,
certificationAssessment,
certificationAssessmentScore,
certificationCourse,
juryId,
}) {
if (toBeCancelled) {
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,14 +1,18 @@
import { usecases } from '../domain/usecases/index.js';

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

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

const uncancel = async function (request, h) {
const juryId = request.auth.credentials.userId;
const certificationCourseId = request.params.certificationCourseId;
await usecases.uncancelCertificationCourse({ certificationCourseId });
await usecases.uncancel({ certificationCourseId, juryId });

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ class SessionManagement {
return this.publishedAt !== null;
}

get isFinalized() {
return this.finalizedAt !== null;
}

isSupervisable(invigilatorPassword) {
return this.invigilatorPassword === invigilatorPassword;
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* @typedef {import('./index.js'.CertificationCourseRepository} CertificationCourseRepository
* @typedef {import('./index.js'.SessionRepository} SessionRepository
* @typedef {import('./index.js'.CertificationRescoringRepository} CertificationRescoringRepository
*/

import CertificationCancelled from '../../../../../src/shared/domain/events/CertificationCancelled.js';
import { NotFinalizedSessionError } from '../../../../shared/domain/errors.js';

/**
* @param {Object} params
* @param {number} params.certificationCourseId
* @param {CertificationCourseRepository} params.certificationCourseRepository
* @param {SessionRepository} params.sessionRepository
* @param {CertificationRescoringRepository} params.certificationRescoringRepository
*/
export const cancel = async function ({
certificationCourseId,
juryId,
certificationCourseRepository,
sessionRepository,
certificationRescoringRepository,
}) {
const certificationCourse = await certificationCourseRepository.get({ id: certificationCourseId });
const session = await sessionRepository.get({ id: certificationCourse.getSessionId() });
if (!session.isFinalized) {
throw new NotFinalizedSessionError();
}

const event = new CertificationCancelled({
certificationCourseId,
juryId,
});

await certificationRescoringRepository.execute({ event });

// Note: update after event to ensure we doing it well, even when rescoring. Needeed this only for v2 certification
certificationCourse.cancel();
await certificationCourseRepository.update({ certificationCourse });
Copy link
Contributor

@Steph0 Steph0 Feb 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comme c'est le mecanisme principal pour le moment, je propose de mettre comme dans uncancel, a savoir d'abord l'update avant l'event. Comme ca si l'event echoue, le update aura bien ete fait dans tous les cas.

De maniere generale d'ailleurs je trouve mieux une meilleure pratique que les operations locales soit toutes validees avant de propager un evenement a d'autres contextes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vu ensemble, c'est le fameux twist pour le scoring v2 => todo: faire un commentaire pour prévenir du choix d'appels dans cet ordre là

};

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* @typedef {import('./index.js'.CertificationCourseRepository} CertificationCourseRepository
* @typedef {import('./index.js'.CertificationRescoringRepository} CertificationRescoringRepository
* @typedef {import('./index.js'.SessionRepository} SessionRepository
*/

import { NotFinalizedSessionError } from '../../../../shared/domain/errors.js';
import CertificationUncancelled from '../../../../shared/domain/events/CertificationUncancelled.js';

/**
* @param {Object} params
* @param {number} params.certificationCourseId
* @param {number} params.juryId
* @param {CertificationCourseRepository} params.certificationCourseRepository
* @param {CertificationRescoringRepository} params.certificationRescoringRepository
* @param {SessionRepository} params.SessionRepository
*/
export const uncancel = async function ({
certificationCourseId,
juryId,
certificationCourseRepository,
certificationRescoringRepository,
sessionRepository,
}) {
const certificationCourse = await certificationCourseRepository.get({ id: certificationCourseId });
const session = await sessionRepository.get({ id: certificationCourse.getSessionId() });
if (!session.isFinalized) {
throw new NotFinalizedSessionError();
}

certificationCourse.uncancel();
await certificationCourseRepository.update({ certificationCourse });

const event = new CertificationUncancelled({
certificationCourseId: certificationCourse.getId(),
juryId,
});

return certificationRescoringRepository.execute({ event });
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* @typedef {import('../../../../../src/shared/domain/events/CertificationCancelled.js'} CertificationCancelled
* @typedef {import('../../../../../src/shared/domain/events/CertificationUncancelled.js'} CertificationUncancelled
* @typedef {import('./index.js'.LibServices} LibServices
*/

/**
* @param {Object} params
* @param {CertificationCancelled|CertificationUncancelled} params.event
* @param {LibServices} params.libServices
*/
export const execute = async ({ event, libServices }) => {
return libServices.handleCertificationRescoring({
event,
});
};
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { handlersAsServices as libServices } from '../../../../../src/shared/domain/events/index.js';
import * as certificationIssueReportRepository from '../../../../certification/shared/infrastructure/repositories/certification-issue-report-repository.js';
import * as issueReportCategoryRepository from '../../../../certification/shared/infrastructure/repositories/issue-report-category-repository.js';
import * as answerRepository from '../../../../shared/infrastructure/repositories/answer-repository.js';
Expand All @@ -21,6 +22,7 @@ import * as certificationCandidateRepository from './certification-candidate-rep
import * as certificationCompanionAlertRepository from './certification-companion-alert-repository.js';
import * as certificationOfficerRepository from './certification-officer-repository.js';
import * as certificationRepository from './certification-repository.js';
import * as certificationRescoringRepository from './certification-rescoring-repository.js';
import * as competenceMarkRepository from './competence-mark-repository.js';
import * as courseAssessmentResultRepository from './course-assessment-result-repository.js';
import * as cpfExportRepository from './cpf-export-repository.js';
Expand Down Expand Up @@ -75,6 +77,7 @@ import * as v3CertificationCourseDetailsForAdministrationRepository from './v3-c
* @typedef {juryCertificationSummaryRepository} JuryCertificationSummaryRepository
* @typedef {certificationCandidateRepository} CertificationCandidateRepository
* @typedef {typeof certificationCompanionAlertRepository} CertificationCompanionAlertRepository
* @typedef {certificationRescoringRepository} CertificationRescoringRepository
*/
const repositoriesWithoutInjectedDependencies = {
assessmentRepository,
Expand Down Expand Up @@ -109,13 +112,17 @@ const repositoriesWithoutInjectedDependencies = {
certificationCpfCountryRepository,
certificationCandidateRepository,
certificationCompanionAlertRepository,
certificationRescoringRepository,
};

/**
* Using {@link https://jsdoc.app/tags-type "Closure Compiler's syntax"} to document injected dependencies
*
* @typedef {libServices} LibServices
*/
const dependencies = {};
const dependencies = {
libServices,
};

const sessionRepositories = injectDependencies(repositoriesWithoutInjectedDependencies, dependencies);
export {
answerRepository,
Expand Down
7 changes: 7 additions & 0 deletions api/src/shared/domain/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,12 @@ class AuditLoggerApiError extends DomainError {
}
}

class NotFinalizedSessionError extends DomainError {
constructor(message = 'A certification course cannot be cancelled while session has not been finalized.') {
super(message);
}
}

export {
AccountRecoveryDemandExpired,
AccountRecoveryUserAlreadyConfirmEmail,
Expand Down Expand Up @@ -1122,6 +1128,7 @@ export {
NoSkillsInCampaignError,
NoStagesForCampaign,
NotEnoughDaysPassedBeforeResetCampaignParticipationError,
NotFinalizedSessionError,
NotFoundError,
NotImplementedError,
ObjectValidationError,
Expand Down
15 changes: 15 additions & 0 deletions api/src/shared/domain/events/CertificationCancelled.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { assertNotNullOrUndefined } from '../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;
assertNotNullOrUndefined(juryId);
this.juryId = juryId;
}
}
15 changes: 15 additions & 0 deletions api/src/shared/domain/events/CertificationUncancelled.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { assertNotNullOrUndefined } from '../models/asserts.js';

export default class CertificationUncancelled {
/**
* @param {Object} params
* @param {number} params.certificationCourseId - certification course that will be rescored
* @param {number} params.juryId - Id of the jury member who uncancelled the certification
*/
constructor({ certificationCourseId, juryId }) {
assertNotNullOrUndefined(certificationCourseId);
this.certificationCourseId = certificationCourseId;
assertNotNullOrUndefined(juryId);
this.juryId = juryId;
}
}
Loading