diff --git a/api/src/certification/evaluation/domain/services/scoring/scoring-v2.js b/api/src/certification/evaluation/domain/services/scoring/scoring-v2.js index 28a17cf98ba..dfd70691353 100644 --- a/api/src/certification/evaluation/domain/services/scoring/scoring-v2.js +++ b/api/src/certification/evaluation/domain/services/scoring/scoring-v2.js @@ -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, @@ -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, @@ -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, diff --git a/api/src/certification/evaluation/domain/services/scoring/scoring-v3.js b/api/src/certification/evaluation/domain/services/scoring/scoring-v3.js index 6a802234b6d..011ea277deb 100644 --- a/api/src/certification/evaluation/domain/services/scoring/scoring-v3.js +++ b/api/src/certification/evaluation/domain/services/scoring/scoring-v3.js @@ -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'; @@ -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, }); @@ -93,6 +96,7 @@ export const handleV3CertificationScoring = async ({ }); const assessmentResult = await _createV3AssessmentResult({ + toBeCancelled, allAnswers: candidateAnswers, emitter, certificationAssessment, @@ -125,6 +129,7 @@ export const handleV3CertificationScoring = async ({ }; function _createV3AssessmentResult({ + toBeCancelled, allAnswers, emitter, certificationAssessment, @@ -132,6 +137,16 @@ function _createV3AssessmentResult({ 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, diff --git a/api/src/certification/scoring/domain/models/factories/AssessmentResultFactory.js b/api/src/certification/scoring/domain/models/factories/AssessmentResultFactory.js index 40883ff8e6f..9194b3abe37 100644 --- a/api/src/certification/scoring/domain/models/factories/AssessmentResultFactory.js +++ b/api/src/certification/scoring/domain/models/factories/AssessmentResultFactory.js @@ -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, diff --git a/api/src/certification/session-management/application/cancellation-controller.js b/api/src/certification/session-management/application/cancellation-controller.js index e7c625c139a..a83ab2fdec6 100644 --- a/api/src/certification/session-management/application/cancellation-controller.js +++ b/api/src/certification/session-management/application/cancellation-controller.js @@ -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); }; diff --git a/api/src/certification/session-management/domain/models/SessionManagement.js b/api/src/certification/session-management/domain/models/SessionManagement.js index a6f2d191513..4b72e37fe39 100644 --- a/api/src/certification/session-management/domain/models/SessionManagement.js +++ b/api/src/certification/session-management/domain/models/SessionManagement.js @@ -71,6 +71,10 @@ class SessionManagement { return this.publishedAt !== null; } + get isFinalized() { + return this.finalizedAt !== null; + } + isSupervisable(invigilatorPassword) { return this.invigilatorPassword === invigilatorPassword; } diff --git a/api/src/certification/session-management/domain/usecases/cancel-certification-course.js b/api/src/certification/session-management/domain/usecases/cancel-certification-course.js deleted file mode 100644 index 95e46885406..00000000000 --- a/api/src/certification/session-management/domain/usecases/cancel-certification-course.js +++ /dev/null @@ -1,7 +0,0 @@ -const cancelCertificationCourse = async function ({ certificationCourseId, certificationCourseRepository }) { - const certificationCourse = await certificationCourseRepository.get({ id: certificationCourseId }); - certificationCourse.cancel(); - await certificationCourseRepository.update({ certificationCourse }); -}; - -export { cancelCertificationCourse }; diff --git a/api/src/certification/session-management/domain/usecases/cancel.js b/api/src/certification/session-management/domain/usecases/cancel.js new file mode 100644 index 00000000000..a4984356281 --- /dev/null +++ b/api/src/certification/session-management/domain/usecases/cancel.js @@ -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 }); +}; diff --git a/api/src/certification/session-management/domain/usecases/uncancel-certification-course.js b/api/src/certification/session-management/domain/usecases/uncancel-certification-course.js deleted file mode 100644 index edd21173ebb..00000000000 --- a/api/src/certification/session-management/domain/usecases/uncancel-certification-course.js +++ /dev/null @@ -1,7 +0,0 @@ -const uncancelCertificationCourse = async function ({ certificationCourseId, certificationCourseRepository }) { - const certificationCourse = await certificationCourseRepository.get({ id: certificationCourseId }); - certificationCourse.uncancel(); - await certificationCourseRepository.update({ certificationCourse }); -}; - -export { uncancelCertificationCourse }; diff --git a/api/src/certification/session-management/domain/usecases/uncancel.js b/api/src/certification/session-management/domain/usecases/uncancel.js new file mode 100644 index 00000000000..ca2bc6ac6fa --- /dev/null +++ b/api/src/certification/session-management/domain/usecases/uncancel.js @@ -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 }); +}; diff --git a/api/src/certification/session-management/infrastructure/repositories/certification-rescoring-repository.js b/api/src/certification/session-management/infrastructure/repositories/certification-rescoring-repository.js new file mode 100644 index 00000000000..f657f72da63 --- /dev/null +++ b/api/src/certification/session-management/infrastructure/repositories/certification-rescoring-repository.js @@ -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, + }); +}; diff --git a/api/src/certification/session-management/infrastructure/repositories/index.js b/api/src/certification/session-management/infrastructure/repositories/index.js index f3ec77be661..d4e9159095c 100644 --- a/api/src/certification/session-management/infrastructure/repositories/index.js +++ b/api/src/certification/session-management/infrastructure/repositories/index.js @@ -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'; @@ -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'; @@ -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, @@ -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, diff --git a/api/src/shared/domain/errors.js b/api/src/shared/domain/errors.js index 02b9e083a4e..4feb87eb6be 100644 --- a/api/src/shared/domain/errors.js +++ b/api/src/shared/domain/errors.js @@ -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, @@ -1122,6 +1128,7 @@ export { NoSkillsInCampaignError, NoStagesForCampaign, NotEnoughDaysPassedBeforeResetCampaignParticipationError, + NotFinalizedSessionError, NotFoundError, NotImplementedError, ObjectValidationError, diff --git a/api/src/shared/domain/events/CertificationCancelled.js b/api/src/shared/domain/events/CertificationCancelled.js new file mode 100644 index 00000000000..14c00cc1433 --- /dev/null +++ b/api/src/shared/domain/events/CertificationCancelled.js @@ -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; + } +} diff --git a/api/src/shared/domain/events/CertificationUncancelled.js b/api/src/shared/domain/events/CertificationUncancelled.js new file mode 100644 index 00000000000..d0c9ad23f12 --- /dev/null +++ b/api/src/shared/domain/events/CertificationUncancelled.js @@ -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; + } +} diff --git a/api/src/shared/domain/events/handle-certification-rescoring.js b/api/src/shared/domain/events/handle-certification-rescoring.js index f4e38872ef9..e8c7cb60a32 100644 --- a/api/src/shared/domain/events/handle-certification-rescoring.js +++ b/api/src/shared/domain/events/handle-certification-rescoring.js @@ -1,3 +1,6 @@ +/** + * @typedef {import('./index.js').CertificationAssessmentRepository} CertificationAssessmentRepository + */ import { ChallengeDeneutralized } from '../../../certification/evaluation/domain/events/ChallengeDeneutralized.js'; import { ChallengeNeutralized } from '../../../certification/evaluation/domain/events/ChallengeNeutralized.js'; import { services } from '../../../certification/evaluation/domain/services/index.js'; @@ -8,9 +11,11 @@ import CertificationRescoredByScript from '../../../certification/session-manage import { AlgorithmEngineVersion } from '../../../certification/shared/domain/models/AlgorithmEngineVersion.js'; import { V3_REPRODUCIBILITY_RATE } from '../constants.js'; import { CertificationComputeError } from '../errors.js'; -import { CertificationResult } from '../models/CertificationResult.js'; +import { CertificationResult } from '../models/index.js'; +import CertificationCancelled from './CertificationCancelled.js'; import { CertificationCourseUnrejected } from './CertificationCourseUnrejected.js'; import { CertificationRescoringCompleted } from './CertificationRescoringCompleted.js'; +import CertificationUncancelled from './CertificationUncancelled.js'; import { checkEventTypes } from './check-event-types.js'; const eventTypes = [ @@ -19,9 +24,15 @@ const eventTypes = [ CertificationJuryDone, CertificationCourseRejected, CertificationCourseUnrejected, + CertificationCancelled, CertificationRescoredByScript, + CertificationUncancelled, ]; +/** + * @param {Object} params + * @param {CertificationAssessmentRepository} params.certificationAssessmentRepository + */ async function handleCertificationRescoring({ event, assessmentResultRepository, @@ -181,7 +192,11 @@ function _getEmitterFromEvent(event) { emitter = CertificationResult.emitters.PIX_ALGO_NEUTRALIZATION; } - if (event instanceof CertificationJuryDone || event instanceof CertificationRescoredByScript) { + if ( + event instanceof CertificationJuryDone || + event instanceof CertificationRescoredByScript || + event instanceof CertificationUncancelled + ) { emitter = CertificationResult.emitters.PIX_ALGO_AUTO_JURY; } @@ -189,6 +204,10 @@ function _getEmitterFromEvent(event) { emitter = CertificationResult.emitters.PIX_ALGO_FRAUD_REJECTION; } + if (event instanceof CertificationCancelled) { + emitter = CertificationResult.emitters.PIX_ALGO_CANCELLATION; + } + return emitter; } diff --git a/api/src/shared/domain/events/index.js b/api/src/shared/domain/events/index.js index 8cd39b2b185..dc6e54bb578 100644 --- a/api/src/shared/domain/events/index.js +++ b/api/src/shared/domain/events/index.js @@ -40,11 +40,14 @@ import * as competenceRepository from '../../infrastructure/repositories/compete import * as knowledgeElementRepository from '../../infrastructure/repositories/knowledge-element-repository.js'; import * as organizationRepository from '../../infrastructure/repositories/organization-repository.js'; import * as skillRepository from '../../infrastructure/repositories/skill-repository.js'; -import { injectDefaults } from '../../infrastructure/utils/dependency-injection.js'; +import { injectDefaults, injectDependencies } from '../../infrastructure/utils/dependency-injection.js'; import { logger } from '../../infrastructure/utils/logger.js'; const { performance } = perf_hooks; +/** + * @typedef {certificationAssessmentRepository} CertificationAssessmentRepository + */ const dependencies = { answerRepository, assessmentRepository, @@ -119,4 +122,10 @@ const _forTestOnly = { }, }; -export { _forTestOnly, eventBus, eventDispatcher }; +/** + * Using {@link https://jsdoc.app/tags-type "Closure Compiler's syntax"} to document injected dependencies + * @typedef {handleCertificationRescoring} HandleCertificationRescoring + */ +const handlersAsServices = injectDependencies(handlersToBeInjected, dependencies); + +export { _forTestOnly, eventBus, eventDispatcher, handlersAsServices }; diff --git a/api/src/shared/domain/models/AssessmentResult.js b/api/src/shared/domain/models/AssessmentResult.js index 56cb049374a..a22318eee55 100644 --- a/api/src/shared/domain/models/AssessmentResult.js +++ b/api/src/shared/domain/models/AssessmentResult.js @@ -2,6 +2,7 @@ * @typedef {import('../../../certification/shared/domain/models/CompetenceMark.js').CompetenceMark} CompetenceMark * @typedef {import('../../../certification/shared/domain/models/JuryComment.js').JuryComment} JuryComment */ +import { NotFinalizedSessionError } from '../errors.js'; import { Assessment } from './Assessment.js'; /** @@ -9,6 +10,7 @@ import { Assessment } from './Assessment.js'; * @enum {string} */ const status = Object.freeze({ + CANCELLED: 'cancelled', REJECTED: 'rejected', VALIDATED: 'validated', ERROR: 'error', @@ -100,6 +102,13 @@ class AssessmentResult { reject() { this.status = AssessmentResult.status.REJECTED; } + + cancel() { + if (!Object.values(AssessmentResult.status).includes(this.status)) { + throw new NotFinalizedSessionError(); + } + this.status = AssessmentResult.status.CANCELLED; + } } AssessmentResult.status = status; diff --git a/api/src/shared/domain/models/CertificationResult.js b/api/src/shared/domain/models/CertificationResult.js index bfd03ebabb7..e063e853087 100644 --- a/api/src/shared/domain/models/CertificationResult.js +++ b/api/src/shared/domain/models/CertificationResult.js @@ -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 { diff --git a/api/src/shared/infrastructure/repositories/assessment-result-repository.js b/api/src/shared/infrastructure/repositories/assessment-result-repository.js index 38a403626b7..36a37fc58d2 100644 --- a/api/src/shared/infrastructure/repositories/assessment-result-repository.js +++ b/api/src/shared/infrastructure/repositories/assessment-result-repository.js @@ -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 }) { @@ -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 }) { diff --git a/api/tests/certification/evaluation/unit/domain/services/scoring/scoring-v2_test.js b/api/tests/certification/evaluation/unit/domain/services/scoring/scoring-v2_test.js index 7efde72f9a2..9d06f58a2f5 100644 --- a/api/tests/certification/evaluation/unit/domain/services/scoring/scoring-v2_test.js +++ b/api/tests/certification/evaluation/unit/domain/services/scoring/scoring-v2_test.js @@ -16,6 +16,7 @@ import { ABORT_REASONS } from '../../../../../../../src/certification/shared/dom import { AutoJuryCommentKeys } from '../../../../../../../src/certification/shared/domain/models/JuryComment.js'; import * as scoringService from '../../../../../../../src/evaluation/domain/services/scoring/scoring-service.js'; import { CertificationComputeError } from '../../../../../../../src/shared/domain/errors.js'; +import CertificationCancelled from '../../../../../../../src/shared/domain/events/CertificationCancelled.js'; import { AssessmentResult, status } from '../../../../../../../src/shared/domain/models/AssessmentResult.js'; import { CertificationResult } from '../../../../../../../src/shared/domain/models/index.js'; import { catchErr, domainBuilder, expect, sinon } from '../../../../../../test-helper.js'; @@ -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 () { diff --git a/api/tests/certification/scoring/unit/domain/factories/AssessmentResultFactory_test.js b/api/tests/certification/scoring/unit/domain/factories/AssessmentResultFactory_test.js index 2cc6b3ed6bb..b6a2e129dc7 100644 --- a/api/tests/certification/scoring/unit/domain/factories/AssessmentResultFactory_test.js +++ b/api/tests/certification/scoring/unit/domain/factories/AssessmentResultFactory_test.js @@ -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 diff --git a/api/tests/certification/session-management/acceptance/application/cancellation-route_test.js b/api/tests/certification/session-management/acceptance/application/cancellation-route_test.js index fcb87e36ecb..ee2cad75b88 100644 --- a/api/tests/certification/session-management/acceptance/application/cancellation-route_test.js +++ b/api/tests/certification/session-management/acceptance/application/cancellation-route_test.js @@ -1,46 +1,388 @@ +import { PIX_ADMIN } from '../../../../../src/authorization/domain/constants.js'; +import { AlgorithmEngineVersion } from '../../../../../src/certification/shared/domain/models/AlgorithmEngineVersion.js'; +import { SESSIONS_VERSIONS } from '../../../../../src/certification/shared/domain/models/SessionVersion.js'; +import { Assessment, CertificationResult } from '../../../../../src/shared/domain/models/index.js'; +import { AssessmentResult } from '../../../../../src/shared/domain/models/index.js'; +import { AnswerStatus } from '../../../../../src/shared/domain/models/index.js'; import { createServer, databaseBuilder, expect, generateAuthenticatedUserRequestHeaders, insertUserWithRoleSuperAdmin, + knex, + learningContentBuilder, + mockLearningContent, } from '../../../../test-helper.js'; describe('Certification | Session-management | Acceptance | Application | Routes | cancellation', function () { let server; + const challengeId = 'k_challenge_id'; beforeEach(async function () { server = await createServer(); + + const learningContent = [ + { + id: '1. Information et données', + competences: [ + { + id: 'index Compétence A', + tubes: [ + { + id: 'recTube1', + skills: [ + { + id: 'recSkill0_0', + nom: '@recSkill0_0', + challenges: [{ id: challengeId }], + }, + ], + }, + ], + }, + ], + courses: [ + { + id: 'rec_active_course_id', + name: "A la recherche de l'information #01", + description: "Mener une recherche et une veille d'information", + isActive: true, + competenceId: 'index Compétence A', + challengeIds: [challengeId], + }, + ], + }, + ]; + + const learningContentObjects = learningContentBuilder.fromAreas(learningContent); + await mockLearningContent(learningContentObjects); }); describe('PATCH /api/admin/certification-courses/{certificationCourseId}/cancel', function () { - it('should respond with a 204', async function () { - // given - databaseBuilder.factory.buildCertificationCourse({ id: 123 }); - const options = { - method: 'PATCH', - url: '/api/admin/certification-courses/123/cancel', - headers: generateAuthenticatedUserRequestHeaders(), - }; - await insertUserWithRoleSuperAdmin(); - await databaseBuilder.commit(); + context('when certification is v2', function () { + it('should create a new cancelled assessment-result', async function () { + // given + const juryMember = databaseBuilder.factory.buildUser.withRole({ roles: PIX_ADMIN.ROLES.SUPER_ADMIN }); + const session = databaseBuilder.factory.buildSession({ + version: SESSIONS_VERSIONS.V2, + finalizedAt: new Date('2024-01-15'), + }); + const certificationCourse = databaseBuilder.factory.buildCertificationCourse({ + id: 123, + version: AlgorithmEngineVersion.V2, + sessionId: session.id, + }); + databaseBuilder.factory.buildCertificationCandidate({ + userId: certificationCourse.userId, + reconciledAt: new Date('2024-01-15'), + sessionId: session.id, + }); + const assessment = databaseBuilder.factory.buildAssessment({ + id: 456, + type: Assessment.types.CERTIFICATION, + userId: certificationCourse.userId, + certificationCourseId: certificationCourse.id, + }); + databaseBuilder.factory.buildCertificationChallenge({ + courseId: certificationCourse.id, + challengeId, + }); + const assessmentResult = databaseBuilder.factory.buildAssessmentResult({ + assessmentId: assessment.id, + }); + databaseBuilder.factory.buildCertificationCourseLastAssessmentResult({ + certificationCourseId: certificationCourse.id, + lastAssessmentResultId: assessmentResult.id, + }); + databaseBuilder.factory.buildCompetenceMark({ assessmentResultId: assessmentResult.id }); + const certificationChallengeOk = databaseBuilder.factory.buildCertificationChallenge({ + courseId: certificationCourse.id, + isNeutralized: false, + challengeId, + competenceId: 'index Compétence A', + associatedSkillName: '@recSkill0_0', + associatedSkillId: 'recSkill0_0', + }); + const answerId = databaseBuilder.factory.buildAnswer({ + assessmentId: assessment.id, + challengeId: certificationChallengeOk.challengeId, + result: AnswerStatus.OK.status, + }).id; - // when - const response = await server.inject(options); + databaseBuilder.factory.buildKnowledgeElement({ + assessmentId: assessment.id, + answerId, + skillId: 'recSkill0_0', + competenceId: 'index Compétence A', + userId: certificationCourse.userId, + earnedPix: 16, + }); - // then - expect(response.statusCode).to.equal(204); + const options = { + method: 'PATCH', + url: '/api/admin/certification-courses/123/cancel', + headers: generateAuthenticatedUserRequestHeaders({ userId: juryMember.id }), + }; + await insertUserWithRoleSuperAdmin(); + await databaseBuilder.commit(); + + // when + const response = await server.inject(options); + + // then + expect(response.statusCode).to.equal(204); + const cancelledAssessmentResult = await knex('assessment-results') + .where({ + assessmentId: assessment.id, + status: AssessmentResult.status.CANCELLED, + juryId: juryMember.id, + }) + .first(); + expect(cancelledAssessmentResult).not.to.be.undefined; + expect( + await knex('certification-courses-last-assessment-results').where({ + lastAssessmentResultId: cancelledAssessmentResult.id, + certificationCourseId: certificationCourse.id, + }), + ).not.to.be.null; + const competenceMarks = await knex('competence-marks').where({ + assessmentResultId: cancelledAssessmentResult.id, + }); + expect(competenceMarks).to.have.lengthOf(1); + expect(response.statusCode).to.equal(204); + }); + }); + + context('when certification is v3', function () { + it('should create a new cancelled assessment-result', async function () { + // given + const juryMember = databaseBuilder.factory.buildUser.withRole({ roles: PIX_ADMIN.ROLES.SUPER_ADMIN }); + const session = databaseBuilder.factory.buildSession({ + version: SESSIONS_VERSIONS.V3, + finalizedAt: new Date('2024-01-15'), + }); + const certificationCourse = databaseBuilder.factory.buildCertificationCourse({ + id: 123, + version: AlgorithmEngineVersion.V3, + sessionId: session.id, + createdAt: new Date('2024-01-15'), + abortReason: 'technical', + }); + databaseBuilder.factory.buildCertificationCandidate({ + userId: certificationCourse.userId, + reconciledAt: new Date('2024-01-15'), + sessionId: session.id, + finalizedAt: new Date('2024-01-15'), + }); + const assessment = databaseBuilder.factory.buildAssessment({ + id: 456, + type: Assessment.types.CERTIFICATION, + userId: certificationCourse.userId, + certificationCourseId: certificationCourse.id, + }); + databaseBuilder.factory.buildCertificationChallenge({ + courseId: certificationCourse.id, + challengeId, + }); + const assessmentResult = databaseBuilder.factory.buildAssessmentResult({ + assessmentId: assessment.id, + }); + databaseBuilder.factory.buildCertificationCourseLastAssessmentResult({ + certificationCourseId: certificationCourse.id, + lastAssessmentResultId: assessmentResult.id, + }); + databaseBuilder.factory.buildCompetenceMark({ assessmentResultId: assessmentResult.id }); + const certificationChallengeOk = databaseBuilder.factory.buildCertificationChallenge({ + courseId: certificationCourse.id, + isNeutralized: false, + challengeId, + competenceId: 'index Compétence A', + associatedSkillName: '@recSkill0_0', + associatedSkillId: 'recSkill0_0', + }); + const answerId = databaseBuilder.factory.buildAnswer({ + assessmentId: assessment.id, + challengeId: certificationChallengeOk.challengeId, + result: AnswerStatus.OK.status, + }).id; + + databaseBuilder.factory.buildKnowledgeElement({ + assessmentId: assessment.id, + answerId, + skillId: 'recSkill0_0', + competenceId: 'index Compétence A', + userId: certificationCourse.userId, + earnedPix: 16, + }); + + databaseBuilder.factory.buildFlashAlgorithmConfiguration({}); + databaseBuilder.factory.buildScoringConfiguration({ + createdAt: new Date('2024-01-14'), + createdByUserId: juryMember.id, + }); + databaseBuilder.factory.buildCompetenceScoringConfiguration({ + configuration: [ + { + competence: 'index Compétence A', + values: [ + { + bounds: { + max: 0, + min: -5, + }, + competenceLevel: 0, + }, + { + bounds: { + max: 5, + min: 0, + }, + competenceLevel: 1, + }, + ], + }, + ], + createdAt: new Date('2024-01-14'), + createdByUserId: juryMember.id, + }); + + const options = { + method: 'PATCH', + url: '/api/admin/certification-courses/123/cancel', + headers: generateAuthenticatedUserRequestHeaders({ userId: juryMember.id }), + }; + await insertUserWithRoleSuperAdmin(); + await databaseBuilder.commit(); + + // when + const response = await server.inject(options); + + // then + const cancelledAssessmentResult = await knex('assessment-results') + .where({ + assessmentId: assessment.id, + status: AssessmentResult.status.CANCELLED, + juryId: juryMember.id, + }) + .first(); + + expect(response.statusCode).to.equal(204); + expect(cancelledAssessmentResult).not.to.be.undefined; + expect( + await knex('certification-courses-last-assessment-results').where({ + lastAssessmentResultId: cancelledAssessmentResult.id, + certificationCourseId: certificationCourse.id, + }), + ).not.to.be.null; + const competenceMarks = await knex('competence-marks').where({ + assessmentResultId: cancelledAssessmentResult.id, + }); + expect(competenceMarks).to.have.lengthOf(1); + }); }); }); describe('PATCH /api/admin/certification-courses/{certificationCourseId}/uncancel', function () { - it('should respond with a 204', async function () { + it('should uncancel the certification with a new assessment-result', async function () { // given - databaseBuilder.factory.buildCertificationCourse({ id: 123 }); + const juryMember = databaseBuilder.factory.buildUser.withRole({ roles: PIX_ADMIN.ROLES.SUPER_ADMIN }); + const session = databaseBuilder.factory.buildSession({ + version: SESSIONS_VERSIONS.V3, + finalizedAt: new Date('2024-01-15'), + }); + const certificationCourse = databaseBuilder.factory.buildCertificationCourse({ + id: 123, + version: AlgorithmEngineVersion.V3, + sessionId: session.id, + createdAt: new Date('2024-01-15'), + abortReason: 'candidate', + isCancelled: true, + }); + databaseBuilder.factory.buildCertificationCandidate({ + userId: certificationCourse.userId, + reconciledAt: new Date('2024-01-15'), + sessionId: session.id, + finalizedAt: new Date('2024-01-15'), + }); + const assessment = databaseBuilder.factory.buildAssessment({ + id: 456, + type: Assessment.types.CERTIFICATION, + userId: certificationCourse.userId, + certificationCourseId: certificationCourse.id, + }); + databaseBuilder.factory.buildCertificationChallenge({ + courseId: certificationCourse.id, + challengeId, + }); + const assessmentResult = databaseBuilder.factory.buildAssessmentResult({ + assessmentId: assessment.id, + status: AssessmentResult.status.CANCELLED, + emitter: CertificationResult.emitters.PIX_ALGO_CANCELLATION, + }); + databaseBuilder.factory.buildCertificationCourseLastAssessmentResult({ + certificationCourseId: certificationCourse.id, + lastAssessmentResultId: assessmentResult.id, + }); + databaseBuilder.factory.buildCompetenceMark({ assessmentResultId: assessmentResult.id }); + const certificationChallengeOk = databaseBuilder.factory.buildCertificationChallenge({ + courseId: certificationCourse.id, + isNeutralized: false, + challengeId, + competenceId: 'index Compétence A', + associatedSkillName: '@recSkill0_0', + associatedSkillId: 'recSkill0_0', + }); + const answerId = databaseBuilder.factory.buildAnswer({ + assessmentId: assessment.id, + challengeId: certificationChallengeOk.challengeId, + result: AnswerStatus.OK.status, + }).id; + + databaseBuilder.factory.buildKnowledgeElement({ + assessmentId: assessment.id, + answerId, + skillId: 'recSkill0_0', + competenceId: 'index Compétence A', + userId: certificationCourse.userId, + earnedPix: 16, + }); + + databaseBuilder.factory.buildFlashAlgorithmConfiguration({}); + databaseBuilder.factory.buildScoringConfiguration({ + createdAt: new Date('2024-01-14'), + createdByUserId: juryMember.id, + }); + databaseBuilder.factory.buildCompetenceScoringConfiguration({ + configuration: [ + { + competence: 'index Compétence A', + values: [ + { + bounds: { + max: 0, + min: -5, + }, + competenceLevel: 0, + }, + { + bounds: { + max: 5, + min: 0, + }, + competenceLevel: 1, + }, + ], + }, + ], + createdAt: new Date('2024-01-14'), + createdByUserId: juryMember.id, + }); + const options = { method: 'PATCH', url: '/api/admin/certification-courses/123/uncancel', - headers: generateAuthenticatedUserRequestHeaders(), + headers: generateAuthenticatedUserRequestHeaders({ userId: juryMember.id }), }; await insertUserWithRoleSuperAdmin(); await databaseBuilder.commit(); @@ -49,7 +391,34 @@ describe('Certification | Session-management | Acceptance | Application | Routes const response = await server.inject(options); // then + const rejectedAssessmentResult = await knex('assessment-results') + .where({ + assessmentId: assessment.id, + status: AssessmentResult.status.REJECTED, + emitter: CertificationResult.emitters.PIX_ALGO_AUTO_JURY, + juryId: juryMember.id, + }) + .first(); + const certificationCourseUncancelled = await knex('certification-courses') + .where({ + id: certificationCourse.id, + }) + .first(); + expect(response.statusCode).to.equal(204); + + expect(certificationCourseUncancelled.isCancelled).to.equal(false); + expect(rejectedAssessmentResult).not.to.be.undefined; + expect( + await knex('certification-courses-last-assessment-results').where({ + lastAssessmentResultId: rejectedAssessmentResult.id, + certificationCourseId: certificationCourse.id, + }), + ).not.to.be.null; + const competenceMarks = await knex('competence-marks').where({ + assessmentResultId: rejectedAssessmentResult.id, + }); + expect(competenceMarks).to.have.lengthOf(1); }); }); }); diff --git a/api/tests/certification/session-management/integration/infrastructure/repositories/certification-rescoring-repository_test.js b/api/tests/certification/session-management/integration/infrastructure/repositories/certification-rescoring-repository_test.js new file mode 100644 index 00000000000..e1bf85f653a --- /dev/null +++ b/api/tests/certification/session-management/integration/infrastructure/repositories/certification-rescoring-repository_test.js @@ -0,0 +1,25 @@ +import { sessionRepositories } from '../../../../../../src/certification/session-management/infrastructure/repositories/index.js'; +import { NotFoundError } from '../../../../../../src/shared/domain/errors.js'; +import CertificationCancelled from '../../../../../../src/shared/domain/events/CertificationCancelled.js'; +import { catchErr, expect } from '../../../../../test-helper.js'; + +describe('Integration | Repository | certification-rescoring-repository', function () { + describe('#execute', function () { + it('should trigger a rescoring', async function () { + // given + const certificationCancelledEvent = new CertificationCancelled({ certificationCourseId: 444, juryId: 555 }); + + // when + const error = await catchErr(sessionRepositories.certificationRescoringRepository.execute)({ + event: certificationCancelledEvent, + }); + + // then + expect(error).to.deepEqualInstance( + new NotFoundError( + `L'assessment de certification avec un certificationCourseId de ${certificationCancelledEvent.certificationCourseId} n'existe pas ou son accès est restreint`, + ), + ); + }); + }); +}); diff --git a/api/tests/certification/session-management/unit/application/cancellation-controller_test.js b/api/tests/certification/session-management/unit/application/cancellation-controller_test.js index 08d0f477f38..c2bd12312fe 100644 --- a/api/tests/certification/session-management/unit/application/cancellation-controller_test.js +++ b/api/tests/certification/session-management/unit/application/cancellation-controller_test.js @@ -4,40 +4,53 @@ import { expect, hFake, sinon } from '../../../../test-helper.js'; describe('Certification | Session-management | Unit | Application | Controller | cancellation', function () { describe('#cancel', function () { - it('should call cancel-certification-course usecase', async function () { + it('should call cancel usecase', async function () { // given - sinon.stub(usecases, 'cancelCertificationCourse'); + sinon.stub(usecases, 'cancel'); const request = { + auth: { + credentials: { + userId: 345, + }, + }, params: { certificationCourseId: 123, }, }; - usecases.cancelCertificationCourse.resolves(); + usecases.cancel.resolves(); // when await cancellationController.cancel(request, hFake); // then - expect(usecases.cancelCertificationCourse).to.have.been.calledWithExactly({ certificationCourseId: 123 }); + expect(usecases.cancel).to.have.been.calledWithExactly({ + certificationCourseId: 123, + juryId: 345, + }); }); }); - describe('#uncancelCertificationCourse', function () { - it('should call uncancel-certification-course usecase', async function () { + describe('#uncancel', function () { + it('should call uncancel usecase', async function () { // given - sinon.stub(usecases, 'uncancelCertificationCourse'); + sinon.stub(usecases, 'uncancel'); const request = { + auth: { + credentials: { + userId: 345, + }, + }, params: { certificationCourseId: 123, }, }; - usecases.uncancelCertificationCourse.resolves(); + usecases.uncancel.resolves(); // when await cancellationController.uncancel(request, hFake); // then - expect(usecases.uncancelCertificationCourse).to.have.been.calledWithExactly({ certificationCourseId: 123 }); + expect(usecases.uncancel).to.have.been.calledWithExactly({ certificationCourseId: 123, juryId: 345 }); }); }); }); diff --git a/api/tests/certification/session-management/unit/domain/usecases/cancel-certification-course_test.js b/api/tests/certification/session-management/unit/domain/usecases/cancel-certification-course_test.js deleted file mode 100644 index c221372422c..00000000000 --- a/api/tests/certification/session-management/unit/domain/usecases/cancel-certification-course_test.js +++ /dev/null @@ -1,26 +0,0 @@ -import { cancelCertificationCourse } from '../../../../../../src/certification/session-management/domain/usecases/cancel-certification-course.js'; -import { domainBuilder, expect, sinon } from '../../../../../test-helper.js'; - -describe('Certification | Session-management | Unit | Domain | UseCases | cancel-certification-course', function () { - it('should cancel the certification course', async function () { - // given - const certificationCourse = domainBuilder.buildCertificationCourse({ id: 123 }); - sinon.spy(certificationCourse, 'cancel'); - const certificationCourseRepository = { - update: sinon.stub(), - get: sinon.stub(), - }; - certificationCourseRepository.get.withArgs({ id: 123 }).resolves(certificationCourse); - certificationCourseRepository.update.resolves(); - - // when - await cancelCertificationCourse({ - certificationCourseId: 123, - certificationCourseRepository, - }); - - // then - expect(certificationCourse.cancel).to.have.been.calledOnce; - expect(certificationCourseRepository.update).to.have.been.calledWithExactly({ certificationCourse }); - }); -}); diff --git a/api/tests/certification/session-management/unit/domain/usecases/cancel_test.js b/api/tests/certification/session-management/unit/domain/usecases/cancel_test.js new file mode 100644 index 00000000000..b7e22953e15 --- /dev/null +++ b/api/tests/certification/session-management/unit/domain/usecases/cancel_test.js @@ -0,0 +1,84 @@ +import { cancel } from '../../../../../../src/certification/session-management/domain/usecases/cancel.js'; +import { NotFinalizedSessionError } from '../../../../../../src/shared/domain/errors.js'; +import CertificationCancelled from '../../../../../../src/shared/domain/events/CertificationCancelled.js'; +import { catchErr, domainBuilder, expect, sinon } from '../../../../../test-helper.js'; + +describe('Certification | Session-management | Unit | Domain | UseCases | cancel', function () { + describe('when session is finalized', function () { + it('should cancel the certification course', async function () { + // given + const juryId = 123; + const session = domainBuilder.certification.sessionManagement.buildSession({ + finalizedAt: new Date('2020-01-01'), + }); + const certificationCourse = domainBuilder.buildCertificationCourse({ id: 123, sessionId: session.id }); + sinon.spy(certificationCourse, 'cancel'); + const certificationCourseRepository = { + update: sinon.stub(), + get: sinon.stub(), + }; + const sessionRepository = { + get: sinon.stub(), + }; + const certificationRescoringRepository = { + execute: sinon.stub(), + }; + certificationCourseRepository.get.withArgs({ id: 123 }).resolves(certificationCourse); + certificationCourseRepository.update.resolves(); + certificationRescoringRepository.execute.resolves(); + sessionRepository.get.withArgs({ id: certificationCourse.getSessionId() }).resolves(session); + + // when + await cancel({ + certificationCourseId: 123, + juryId, + certificationCourseRepository, + sessionRepository, + certificationRescoringRepository, + }); + + // then + expect(certificationCourse.cancel).to.have.been.calledOnce; + expect(certificationCourseRepository.update).to.have.been.calledWithExactly({ certificationCourse }); + expect(certificationRescoringRepository.execute).to.have.been.calledWithExactly({ + event: new CertificationCancelled({ + certificationCourseId: certificationCourse.getId(), + juryId, + }), + }); + }); + }); + + describe('when session is not finalized', function () { + it('should not cancel the certification course', async function () { + // given + const juryId = 123; + const session = domainBuilder.certification.sessionManagement.buildSession({ finalizedAt: null }); + const certificationCourse = domainBuilder.buildCertificationCourse({ id: 123, sessionId: session.id }); + sinon.spy(certificationCourse, 'cancel'); + const certificationCourseRepository = { + update: sinon.stub(), + get: sinon.stub(), + }; + const sessionRepository = { + get: sinon.stub(), + }; + certificationCourseRepository.get.withArgs({ id: 123 }).resolves(certificationCourse); + certificationCourseRepository.update.resolves(); + sessionRepository.get.withArgs({ id: certificationCourse.getSessionId() }).resolves(session); + + // when + const error = await catchErr(cancel)({ + certificationCourseId: 123, + certificationCourseRepository, + sessionRepository, + juryId, + }); + + // then + expect(certificationCourse.cancel).to.not.have.been.called; + expect(certificationCourseRepository.update).to.not.have.been.called; + expect(error).to.be.instanceOf(NotFinalizedSessionError); + }); + }); +}); diff --git a/api/tests/certification/session-management/unit/domain/usecases/uncancel-certification-course_test.js b/api/tests/certification/session-management/unit/domain/usecases/uncancel-certification-course_test.js deleted file mode 100644 index b0565cf74e7..00000000000 --- a/api/tests/certification/session-management/unit/domain/usecases/uncancel-certification-course_test.js +++ /dev/null @@ -1,26 +0,0 @@ -import { uncancelCertificationCourse } from '../../../../../../src/certification/session-management/domain/usecases/uncancel-certification-course.js'; -import { domainBuilder, expect, sinon } from '../../../../../test-helper.js'; - -describe('Certification | Session-management | Unit | Domain | UseCases | uncancel-certification-course', function () { - it('should uncancel the certification course', async function () { - // given - const certificationCourse = domainBuilder.buildCertificationCourse({ id: 123 }); - sinon.spy(certificationCourse, 'uncancel'); - const certificationCourseRepository = { - update: sinon.stub(), - get: sinon.stub(), - }; - certificationCourseRepository.get.withArgs({ id: 123 }).resolves(certificationCourse); - certificationCourseRepository.update.resolves(); - - // when - await uncancelCertificationCourse({ - certificationCourseId: 123, - certificationCourseRepository, - }); - - // then - expect(certificationCourse.uncancel).to.have.been.calledOnce; - expect(certificationCourseRepository.update).to.have.been.calledWithExactly({ certificationCourse }); - }); -}); diff --git a/api/tests/certification/session-management/unit/domain/usecases/uncancel_test.js b/api/tests/certification/session-management/unit/domain/usecases/uncancel_test.js new file mode 100644 index 00000000000..7e5d4377324 --- /dev/null +++ b/api/tests/certification/session-management/unit/domain/usecases/uncancel_test.js @@ -0,0 +1,80 @@ +import { uncancel } from '../../../../../../src/certification/session-management/domain/usecases/uncancel.js'; +import { NotFinalizedSessionError } from '../../../../../../src/shared/domain/errors.js'; +import CertificationUncancelled from '../../../../../../src/shared/domain/events/CertificationUncancelled.js'; +import { catchErr, domainBuilder, expect, sinon } from '../../../../../test-helper.js'; + +describe('Certification | Session-management | Unit | Domain | UseCases | uncancel', function () { + it('should uncancel the certification course', async function () { + // given + const juryId = 123; + const session = domainBuilder.certification.sessionManagement.buildSession({ finalizedAt: new Date('2020-01-01') }); + const certificationCourse = domainBuilder.buildCertificationCourse({ id: 123, sessionId: session.id }); + sinon.spy(certificationCourse, 'uncancel'); + const certificationCourseRepository = { + update: sinon.stub(), + get: sinon.stub(), + }; + const certificationRescoringRepository = { + execute: sinon.stub(), + }; + const sessionRepository = { + get: sinon.stub(), + }; + certificationCourseRepository.get.withArgs({ id: 123 }).resolves(certificationCourse); + certificationCourseRepository.update.resolves(); + certificationRescoringRepository.execute.resolves(); + sessionRepository.get.withArgs({ id: certificationCourse.getSessionId() }).resolves(session); + + // when + await uncancel({ + certificationCourseId: 123, + juryId, + certificationCourseRepository, + certificationRescoringRepository, + sessionRepository, + }); + + // then + expect(certificationCourse.uncancel).to.have.been.calledOnce; + expect(certificationCourseRepository.update).to.have.been.calledWithExactly({ certificationCourse }); + expect(certificationRescoringRepository.execute).to.have.been.calledWithExactly({ + event: new CertificationUncancelled({ + certificationCourseId: certificationCourse.getId(), + juryId, + }), + }); + }); + + describe('when session is not finalized', function () { + it('should not uncancel the certification course', async function () { + // given + const juryId = 123; + const session = domainBuilder.certification.sessionManagement.buildSession({ finalizedAt: null }); + const certificationCourse = domainBuilder.buildCertificationCourse({ id: 123, sessionId: session.id }); + sinon.spy(certificationCourse, 'uncancel'); + const certificationCourseRepository = { + update: sinon.stub(), + get: sinon.stub(), + }; + const sessionRepository = { + get: sinon.stub(), + }; + certificationCourseRepository.get.withArgs({ id: 123 }).resolves(certificationCourse); + certificationCourseRepository.update.resolves(); + sessionRepository.get.withArgs({ id: certificationCourse.getSessionId() }).resolves(session); + + // when + const error = await catchErr(uncancel)({ + juryId, + certificationCourseId: 123, + certificationCourseRepository, + sessionRepository, + }); + + // then + expect(certificationCourse.uncancel).to.not.have.been.called; + expect(certificationCourseRepository.update).to.not.have.been.called; + expect(error).to.be.instanceOf(NotFinalizedSessionError); + }); + }); +}); diff --git a/api/tests/shared/unit/domain/models/AssessmentResult_test.js b/api/tests/shared/unit/domain/models/AssessmentResult_test.js index 861327086df..4bdef27dcfa 100644 --- a/api/tests/shared/unit/domain/models/AssessmentResult_test.js +++ b/api/tests/shared/unit/domain/models/AssessmentResult_test.js @@ -39,37 +39,21 @@ describe('Unit | Domain | Models | AssessmentResult', function () { expect(isValidated).to.be.true; }); - it('should return false if the assessment result is rejected', function () { - // given - const assessmentResult = domainBuilder.buildAssessmentResult.rejected(); - - // when - const isValidated = assessmentResult.isValidated(); - - // then - expect(isValidated).to.be.false; - }); - - it('should return false if the assessment result is in error', function () { - // given - const assessmentResult = domainBuilder.buildAssessmentResult.error(); - - // when - const isValidated = assessmentResult.isValidated(); - - // then - expect(isValidated).to.be.false; - }); - - it('should return false if the assessment result is started', function () { - // given - const assessmentResult = domainBuilder.buildAssessmentResult.started(); - - // when - const isValidated = assessmentResult.isValidated(); - - // then - expect(isValidated).to.be.false; - }); + // eslint-disable-next-line mocha/no-setup-in-describe + [AssessmentResult.status.CANCELLED, AssessmentResult.status.REJECTED, AssessmentResult.status.ERROR].forEach( + (assessmentResultStatus) => { + it(`should return false if the assessment result is ${assessmentResultStatus}`, function () { + // given + const assessmentResult = domainBuilder.buildAssessmentResult(); + assessmentResult.status = assessmentResultStatus; + + // when + const isValidated = assessmentResult.isValidated(); + + // then + expect(isValidated).to.be.false; + }); + }, + ); }); });