diff --git a/api/db/database-builder/factory/build-certification-candidate.js b/api/db/database-builder/factory/build-certification-candidate.js index d28feffe846..7c1c18ad9ae 100644 --- a/api/db/database-builder/factory/build-certification-candidate.js +++ b/api/db/database-builder/factory/build-certification-candidate.js @@ -32,7 +32,7 @@ const buildCertificationCandidate = function ({ } = {}) { sessionId = _.isUndefined(sessionId) ? buildSession().id : sessionId; userId = _.isUndefined(userId) ? buildUser().id : userId; - reconciledAt = userId ? new Date('2020-01-02') : undefined; + reconciledAt = userId && !reconciledAt ? new Date('2020-01-02') : reconciledAt; const values = { id, diff --git a/api/lib/domain/usecases/retrieve-last-or-create-certification-course.js b/api/lib/domain/usecases/retrieve-last-or-create-certification-course.js index 54e1870816e..789aa96848f 100644 --- a/api/lib/domain/usecases/retrieve-last-or-create-certification-course.js +++ b/api/lib/domain/usecases/retrieve-last-or-create-certification-course.js @@ -189,12 +189,6 @@ async function _startNewCertification({ }) { const challengesForCertification = []; - const placementProfile = await placementProfileService.getPlacementProfile({ - userId, - limitDate: new Date(), - version, - }); - const certificationCenter = await certificationCenterRepository.getBySessionId({ sessionId }); const complementaryCertificationCourseData = []; @@ -228,6 +222,12 @@ async function _startNewCertification({ let challengesForPixCertification = []; if (!CertificationVersion.isV3(version)) { + const placementProfile = await placementProfileService.getPlacementProfile({ + userId, + limitDate: certificationCandidate.reconciledAt, + version, + }); + challengesForPixCertification = await certificationChallengesService.pickCertificationChallenges( placementProfile, locale, diff --git a/api/src/certification/session-management/domain/models/CertificationCandidate.js b/api/src/certification/session-management/domain/models/CertificationCandidate.js new file mode 100644 index 00000000000..c7e26823835 --- /dev/null +++ b/api/src/certification/session-management/domain/models/CertificationCandidate.js @@ -0,0 +1,13 @@ +class CertificationCandidate { + /** + * @param {Object} param + * @param {number} param.userId + * @param {Date} param.reconciledAt + */ + constructor({ userId, reconciledAt } = {}) { + this.userId = userId; + this.reconciledAt = reconciledAt; + } +} + +export { CertificationCandidate }; diff --git a/api/src/certification/session-management/domain/usecases/get-certification-details.js b/api/src/certification/session-management/domain/usecases/get-certification-details.js index 8df8d6348a9..fd6c5104d5c 100644 --- a/api/src/certification/session-management/domain/usecases/get-certification-details.js +++ b/api/src/certification/session-management/domain/usecases/get-certification-details.js @@ -1,10 +1,27 @@ +/** + * @typedef {import('./index.js').CompetenceMarkRepository} CompetenceMarkRepository + * @typedef {import('./index.js').CertificationAssessmentRepository} CertificationAssessmentRepository + * @typedef {import('./index.js').CertificationCandidateRepository} CertificationCandidateRepository + * @typedef {import('./index.js').PlacementProfileService} PlacementProfileService + * @typedef {import('./index.js').ScoringCertificationService} ScoringCertificationService + */ import { CERTIFICATION_VERSIONS } from '../../../shared/domain/models/CertificationVersion.js'; import { CertificationDetails } from '../read-models/CertificationDetails.js'; +/** + * @param {Object} params + * @param {number} params.certificationCourseId + * @param {CompetenceMarkRepository} params.competenceMarkRepository + * @param {CertificationAssessmentRepository} params.certificationAssessmentRepository + * @param {CertificationCandidateRepository} params.certificationCandidateRepository + * @param {PlacementProfileService} params.placementProfileService + * @param {ScoringCertificationService} params.scoringCertificationService + */ const getCertificationDetails = async function ({ certificationCourseId, competenceMarkRepository, certificationAssessmentRepository, + certificationCandidateRepository, placementProfileService, scoringCertificationService, }) { @@ -13,36 +30,43 @@ const getCertificationDetails = async function ({ }); const competenceMarks = await competenceMarkRepository.findByCertificationCourseId({ certificationCourseId }); + const candidate = await certificationCandidateRepository.getByCertificationCourseId({ + certificationCourseId: certificationAssessment.certificationCourseId, + }); + + const placementProfile = await placementProfileService.getPlacementProfile({ + userId: candidate.userId, + limitDate: candidate.reconciledAt, + version: CERTIFICATION_VERSIONS.V2, + allowExcessPixAndLevels: false, + }); if (competenceMarks.length) { - return _retrievePersistedCertificationDetails(competenceMarks, certificationAssessment, placementProfileService); + return _retrievePersistedCertificationDetails({ + competenceMarks, + certificationAssessment, + placementProfile, + }); } else { - return _computeCertificationDetailsOnTheFly( + return _computeCertificationDetailsOnTheFly({ certificationAssessment, - placementProfileService, + placementProfile, scoringCertificationService, - ); + }); } }; export { getCertificationDetails }; -async function _computeCertificationDetailsOnTheFly( +async function _computeCertificationDetailsOnTheFly({ certificationAssessment, - placementProfileService, + placementProfile, scoringCertificationService, -) { +}) { const certificationAssessmentScore = await scoringCertificationService.calculateCertificationAssessmentScore({ certificationAssessment, continueOnError: true, }); - const placementProfile = await placementProfileService.getPlacementProfile({ - userId: certificationAssessment.userId, - limitDate: certificationAssessment.createdAt, - version: CERTIFICATION_VERSIONS.V2, - allowExcessPixAndLevels: false, - }); - return CertificationDetails.fromCertificationAssessmentScore({ certificationAssessmentScore, certificationAssessment, @@ -50,18 +74,11 @@ async function _computeCertificationDetailsOnTheFly( }); } -async function _retrievePersistedCertificationDetails( - competenceMarks, - certificationAssessment, - placementProfileService, -) { - const placementProfile = await placementProfileService.getPlacementProfile({ - userId: certificationAssessment.userId, - limitDate: certificationAssessment.createdAt, - version: CERTIFICATION_VERSIONS.V2, - allowExcessPixAndLevels: false, - }); - +/** + * @param {PlacementProfileService} placementProfileService + * @param {CertificationCandidateRepository} certificationCandidateRepository + */ +async function _retrievePersistedCertificationDetails({ competenceMarks, certificationAssessment, placementProfile }) { return CertificationDetails.from({ competenceMarks, certificationAssessment, diff --git a/api/src/certification/session-management/domain/usecases/index.js b/api/src/certification/session-management/domain/usecases/index.js index 8a7a9bc366f..ea43380ac3a 100644 --- a/api/src/certification/session-management/domain/usecases/index.js +++ b/api/src/certification/session-management/domain/usecases/index.js @@ -52,12 +52,14 @@ import { cpfReceiptsStorage } from '../../infrastructure/storage/cpf-receipts-st * @typedef {import('../../infrastructure/repositories/index.js').CertificationAssessmentRepository} CertificationAssessmentRepository * @typedef {import('../../infrastructure/repositories/index.js').CertificationCpfCityRepository} CertificationCpfCityRepository * @typedef {import('../../infrastructure/repositories/index.js').CertificationCpfCountryRepository} CertificationCpfCountryRepository + * @typedef {import('../../infrastructure/repositories/index.js').CertificationCandidateRepository} CertificationCandidateRepository * @typedef {import('../../infrastructure/storage/cpf-receipts-storage.js').cpfReceiptsStorage} CpfReceiptsStorage * @typedef {import('../../infrastructure/storage/cpf-exports-storage.js').cpfExportsStorage} CpfExportsStorage * @typedef {import('../../../shared/domain/services/certification-badges-service.js')} CertificationBadgesService * @typedef {import('../../../shared/domain/services/scoring-certification-service.js')} ScoringCertificationService * @typedef {import('../../../../shared/domain/services/placement-profile-service.js')} PlacementProfileService * @typedef {import('../../../shared/domain/services/certification-cpf-service.js')} CertificationCpfService + * @typedef {import('../../infrastructure/repositories/index.js').CertificationCandidateRepository} CertificationCandidateRepository **/ /** @@ -96,6 +98,7 @@ import { cpfReceiptsStorage } from '../../infrastructure/storage/cpf-receipts-st * @typedef {flashAlgorithmService} FlashAlgorithmService * @typedef {flashAlgorithmConfigurationRepository} FlashAlgorithmConfigurationRepository * @typedef {cpfExportRepository} CpfExportRepository + * @typedef {certificationCandidateRepository} CertificationCandidateRepository **/ const dependencies = { ...sessionRepositories, diff --git a/api/src/certification/session-management/infrastructure/repositories/certification-candidate-repository.js b/api/src/certification/session-management/infrastructure/repositories/certification-candidate-repository.js new file mode 100644 index 00000000000..969f6ac1cd9 --- /dev/null +++ b/api/src/certification/session-management/infrastructure/repositories/certification-candidate-repository.js @@ -0,0 +1,23 @@ +import { DomainTransaction } from '../../../../shared/domain/DomainTransaction.js'; +import { NotFoundError } from '../../../../shared/domain/errors.js'; +import { CertificationCandidate } from '../../domain/models/CertificationCandidate.js'; + +export const getByCertificationCourseId = async ({ certificationCourseId }) => { + const knexConn = DomainTransaction.getConnection(); + const certificationCandidate = await knexConn('certification-courses') + .select('certification-candidates.userId', 'certification-candidates.reconciledAt') + .innerJoin('sessions', 'sessions.id', 'certification-courses.sessionId') + .innerJoin('certification-candidates', 'sessions.id', 'certification-candidates.sessionId') + .where('certification-courses.id', '=', certificationCourseId) + .first(); + + if (!certificationCandidate) { + throw new NotFoundError(); + } + + return _toDomain(certificationCandidate); +}; + +const _toDomain = (candidateData) => { + return new CertificationCandidate({ userId: candidateData.userId, reconciledAt: candidateData.reconciledAt }); +}; diff --git a/api/src/certification/session-management/infrastructure/repositories/index.js b/api/src/certification/session-management/infrastructure/repositories/index.js index 9ed99e0d807..57c45cb12c4 100644 --- a/api/src/certification/session-management/infrastructure/repositories/index.js +++ b/api/src/certification/session-management/infrastructure/repositories/index.js @@ -17,6 +17,7 @@ import * as sharedCompetenceMarkRepository from '../../../shared/infrastructure/ import * as complementaryCertificationCourseResultRepository from '../../../shared/infrastructure/repositories/complementary-certification-course-result-repository.js'; import * as flashAlgorithmConfigurationRepository from '../../../shared/infrastructure/repositories/flash-algorithm-configuration-repository.js'; import * as certificationCandidateForSupervisingRepository from './certification-candidate-for-supervising-repository.js'; +import * as certificationCandidateRepository from './certification-candidate-repository.js'; import * as certificationOfficerRepository from './certification-officer-repository.js'; import * as competenceMarkRepository from './competence-mark-repository.js'; import * as courseAssessmentResultRepository from './course-assessment-result-repository.js'; @@ -67,6 +68,7 @@ import * as v3CertificationCourseDetailsForAdministrationRepository from './v3-c * @typedef {flashAlgorithmConfigurationRepository} FlashAlgorithmConfigurationRepository * @typedef {cpfExportRepository} CpfExportRepository * @typedef {juryCertificationSummaryRepository} JuryCertificationSummaryRepository + * @typedef {certificationCandidateRepository} CertificationCandidateRepository */ const repositoriesWithoutInjectedDependencies = { assessmentRepository, @@ -97,6 +99,7 @@ const repositoriesWithoutInjectedDependencies = { complementaryCertificationCourseResultRepository, certificationCpfCityRepository, certificationCpfCountryRepository, + certificationCandidateRepository, }; /** diff --git a/api/src/shared/domain/models/CertificationCandidate.js b/api/src/shared/domain/models/CertificationCandidate.js index 2d712d0e5bb..97df9d76751 100644 --- a/api/src/shared/domain/models/CertificationCandidate.js +++ b/api/src/shared/domain/models/CertificationCandidate.js @@ -38,6 +38,7 @@ class CertificationCandidate { subscriptions = [], hasSeenCertificationInstructions = false, accessibilityAdjustmentNeeded = false, + reconciledAt, } = {}) { this.id = id; this.firstName = firstName; @@ -63,6 +64,7 @@ class CertificationCandidate { this.prepaymentCode = prepaymentCode; this.hasSeenCertificationInstructions = hasSeenCertificationInstructions; this.accessibilityAdjustmentNeeded = accessibilityAdjustmentNeeded; + this.reconciledAt = reconciledAt; Object.defineProperty(this, 'complementaryCertification', { enumerable: true, diff --git a/api/tests/acceptance/application/certification-courses/certification-course-controller_test.js b/api/tests/acceptance/application/certification-courses/certification-course-controller_test.js index 6ce635f2361..36f8ce94780 100644 --- a/api/tests/acceptance/application/certification-courses/certification-course-controller_test.js +++ b/api/tests/acceptance/application/certification-courses/certification-course-controller_test.js @@ -4,6 +4,7 @@ import { CertificationVersion, } from '../../../../src/certification/shared/domain/models/CertificationVersion.js'; import { config } from '../../../../src/shared/config.js'; +import { KnowledgeElement } from '../../../../src/shared/domain/models/KnowledgeElement.js'; import { createServer, databaseBuilder, @@ -333,6 +334,7 @@ describe('Acceptance | API | Certification Course', function () { // given const { options, userId, sessionId } = _createRequestOptions(); _createNonExistingCertifCourseSetup({ learningContent, userId, sessionId }); + await databaseBuilder.commit(); // when @@ -342,6 +344,9 @@ describe('Acceptance | API | Certification Course', function () { const certificationCourses = await knex('certification-courses').where({ userId, sessionId }); expect(certificationCourses).to.have.length(1); expect(certificationCourses[0].version).to.equal(CERTIFICATION_VERSIONS.V2); + expect(response.result.data.attributes).to.include({ + 'nb-challenges': 2, + }); }); context('when the session is v3', function () { @@ -512,12 +517,23 @@ function _createNonExistingCertifCourseSetup({ learningContent, sessionId, userI sessionId, userId, authorizedToStart: true, + reconciledAt: new Date('2019-02-01'), }); databaseBuilder.factory.buildCoreSubscription({ certificationCandidateId: certificationCandidate.id }); databaseBuilder.factory.buildCorrectAnswersAndKnowledgeElementsForLearningContent.fromAreas({ learningContent, userId, earnedPix: 4, + placementDate: new Date('2019-01-01'), + }); + + // KnowledgeElement.StatusType.RESET after the reconciledAt date + databaseBuilder.factory.buildKnowledgeElement({ + status: KnowledgeElement.StatusType.RESET, + skillId: 'recSkill5_1', + competenceId: 'recCompetence5', + userId: userId, + createdAt: new Date('2023-03-03'), }); return { diff --git a/api/tests/certification/enrolment/integration/infrastructure/repositories/certification-candidate-repository_test.js b/api/tests/certification/enrolment/integration/infrastructure/repositories/certification-candidate-repository_test.js index a3849872243..57a598c70fa 100644 --- a/api/tests/certification/enrolment/integration/infrastructure/repositories/certification-candidate-repository_test.js +++ b/api/tests/certification/enrolment/integration/infrastructure/repositories/certification-candidate-repository_test.js @@ -138,12 +138,21 @@ describe('Integration | Repository | CertificationCandidate', function () { describe('#getBySessionIdAndUserId', function () { let userId; let complementaryCertificationId; + let certificationCandidateId; + let createdAt, reconciledAt; beforeEach(function () { // given + createdAt = new Date('2000-01-01'); + reconciledAt = new Date('2020-01-02'); userId = databaseBuilder.factory.buildUser().id; complementaryCertificationId = databaseBuilder.factory.buildComplementaryCertification().id; - const certificationCandidateId = databaseBuilder.factory.buildCertificationCandidate({ sessionId, userId }).id; + certificationCandidateId = databaseBuilder.factory.buildCertificationCandidate({ + sessionId, + userId, + createdAt, + reconciledAt, + }).id; databaseBuilder.factory.buildCoreSubscription({ certificationCandidateId }); databaseBuilder.factory.buildComplementaryCertificationSubscription({ complementaryCertificationId, @@ -156,13 +165,52 @@ describe('Integration | Repository | CertificationCandidate', function () { context('when there is one certification candidate with the given session id and user id', function () { it('should fetch the candidate', async function () { // when - const actualCandidates = await certificationCandidateRepository.getBySessionIdAndUserId({ sessionId, userId }); + const actualCandidate = await certificationCandidateRepository.getBySessionIdAndUserId({ sessionId, userId }); // then - expect(actualCandidates.sessionId).to.equal(sessionId); - expect(actualCandidates.userId).to.equal(userId); - expect(actualCandidates.complementaryCertification).not.to.be.null; - expect(actualCandidates.complementaryCertification.id).to.equal(complementaryCertificationId); + expect(actualCandidate).to.deep.equal({ + accessibilityAdjustmentNeeded: false, + authorizedToStart: false, + billingMode: null, + birthCity: 'PARIS 1', + birthCountry: 'France', + birthINSEECode: '75101', + birthPostalCode: null, + birthProvinceCode: null, + birthdate: '2000-01-04', + complementaryCertification: { + id: complementaryCertificationId, + key: 'DROIT', + label: 'UneSuperCertifComplémentaire', + }, + createdAt, + email: 'somemail@example.net', + externalId: 'externalId', + extraTimePercentage: 0.3, + firstName: 'first-name', + hasSeenCertificationInstructions: false, + id: certificationCandidateId, + lastName: 'last-name', + organizationLearnerId: null, + prepaymentCode: null, + reconciledAt, + resultRecipientEmail: 'somerecipientmail@example.net', + sessionId, + sex: 'M', + subscriptions: [ + { + certificationCandidateId: undefined, + complementaryCertificationId: null, + type: 'CORE', + }, + { + certificationCandidateId, + complementaryCertificationId, + type: 'COMPLEMENTARY', + }, + ], + userId: userId, + }); }); }); diff --git a/api/tests/certification/session-management/acceptance/application/certification-details-route_test.js b/api/tests/certification/session-management/acceptance/application/certification-details-route_test.js index a0c419d3177..2e604a99da4 100644 --- a/api/tests/certification/session-management/acceptance/application/certification-details-route_test.js +++ b/api/tests/certification/session-management/acceptance/application/certification-details-route_test.js @@ -56,7 +56,14 @@ describe('Certification | Session Management | Acceptance | Application | Routes const learningContentObjects = learningContentBuilder.fromAreas(learningContent); mockLearningContent(learningContentObjects); - databaseBuilder.factory.buildCertificationCourse({ id: 1234 }); + const sessionId = databaseBuilder.factory.buildSession().id; + const userId = databaseBuilder.factory.buildUser().id; + databaseBuilder.factory.buildCertificationCandidate({ + userId, + sessionId, + reconciledAt: new Date(), + }); + databaseBuilder.factory.buildCertificationCourse({ id: 1234, sessionId }); const assessmentId = databaseBuilder.factory.buildAssessment({ certificationCourseId: 1234, competenceId: 'competence_id', @@ -131,7 +138,13 @@ describe('Certification | Session Management | Acceptance | Application | Routes const learningContentObjects = learningContentBuilder.fromAreas(learningContent); mockLearningContent(learningContentObjects); - databaseBuilder.factory.buildCertificationCourse({ id: 1234, userId: user.id }); + const sessionId = databaseBuilder.factory.buildSession().id; + databaseBuilder.factory.buildCertificationCandidate({ + userId: user.id, + sessionId, + reconciledAt: new Date(), + }); + databaseBuilder.factory.buildCertificationCourse({ id: 1234, userId: user.id, sessionId }); const assessmentId = databaseBuilder.factory.buildAssessment({ certificationCourseId: 1234, competenceId: 'competence_id', diff --git a/api/tests/certification/session-management/integration/infrastructure/repositories/certification-candidate-repository_test.js b/api/tests/certification/session-management/integration/infrastructure/repositories/certification-candidate-repository_test.js new file mode 100644 index 00000000000..cfa98da5244 --- /dev/null +++ b/api/tests/certification/session-management/integration/infrastructure/repositories/certification-candidate-repository_test.js @@ -0,0 +1,41 @@ +import * as certificationCandidateRepository from '../../../../../../src/certification/session-management/infrastructure/repositories/certification-candidate-repository.js'; +import { NotFoundError } from '../../../../../../src/shared/domain/errors.js'; +import { catchErr, databaseBuilder, expect } from '../../../../../test-helper.js'; + +describe('Certification | Session Management | Integration | Infrastructure | Repositories | Certification Candidate', function () { + describe('#getByCertificationCourseId', function () { + it('should return a candidate', async function () { + // given + const reconciledAt = new Date('2024-01-02'); + const userId = databaseBuilder.factory.buildUser().id; + const sessionId = databaseBuilder.factory.buildSession().id; + const certificationCourseId = databaseBuilder.factory.buildCertificationCourse({ sessionId }).id; + databaseBuilder.factory.buildCertificationCandidate({ + sessionId, + userId, + reconciledAt, + }).id; + + await databaseBuilder.commit(); + + // when + const candidate = await certificationCandidateRepository.getByCertificationCourseId({ certificationCourseId }); + + // then + expect(candidate).to.deep.equal({ + reconciledAt, + userId, + }); + }); + + context('When the candidate does not exist', function () { + it('throws a not found error', async function () { + // when + const error = await catchErr(certificationCandidateRepository.getByCertificationCourseId)({ + certificationCourseId: 404, + }); + expect(error).to.be.an.instanceOf(NotFoundError); + }); + }); + }); +}); diff --git a/api/tests/certification/session-management/unit/domain/models/CertificationCandidate_test.js b/api/tests/certification/session-management/unit/domain/models/CertificationCandidate_test.js new file mode 100644 index 00000000000..3c31a1248cb --- /dev/null +++ b/api/tests/certification/session-management/unit/domain/models/CertificationCandidate_test.js @@ -0,0 +1,27 @@ +import { expect } from 'chai'; + +import { CertificationCandidate } from '../../../../../../src/certification/session-management/domain/models/CertificationCandidate.js'; + +describe('Certification | Session Management | Unit | Domain | Models | Certification Candidate', function () { + describe('constructor', function () { + it('should build a Certification Candidate', function () { + // given + const date = new Date(); + const rawData = { + userId: 2, + reconciledAt: date, + }; + + const expectedData = { + userId: 2, + reconciledAt: date, + }; + + // when + const certificationCandidate = new CertificationCandidate(rawData); + + // then + expect(certificationCandidate).to.deep.equal(expectedData); + }); + }); +}); diff --git a/api/tests/certification/session-management/unit/domain/usecases/get-certification-details_test.js b/api/tests/certification/session-management/unit/domain/usecases/get-certification-details_test.js index b44cffd243c..5072d883950 100644 --- a/api/tests/certification/session-management/unit/domain/usecases/get-certification-details_test.js +++ b/api/tests/certification/session-management/unit/domain/usecases/get-certification-details_test.js @@ -10,18 +10,18 @@ describe('Certification | Session-management | Unit | Domain | UseCases | get-ce const certificationAssessmentRepository = { getByCertificationCourseId: sinon.stub(), }; - const placementProfileService = { getPlacementProfile: sinon.stub(), }; - const competenceMarkRepository = { findByCertificationCourseId: sinon.stub(), }; - const scoringCertificationService = { calculateCertificationAssessmentScore: sinon.stub(), }; + const certificationCandidateRepository = { + getByCertificationCourseId: sinon.stub(), + }; const certificationCourseId = 1234; const certificationChallenge = domainBuilder.buildCertificationChallengeWithType({ @@ -39,10 +39,6 @@ describe('Certification | Session-management | Unit | Domain | UseCases | get-ce state: CertificationAssessmentStates.STARTED, }); - certificationAssessmentRepository.getByCertificationCourseId - .withArgs({ certificationCourseId }) - .resolves(certificationAssessment); - const competenceMark = domainBuilder.buildCompetenceMark({ competenceId: 'recComp1', areaCode: '1', @@ -59,15 +55,28 @@ describe('Certification | Session-management | Unit | Domain | UseCases | get-ce competencesData: [{ id: 'recComp1', index: '1.1', name: 'Manger des fruits', level: 3, score: 45 }], }); + const candidate = domainBuilder.certification.sessionManagement.buildCertificationCandidate({ + userId: certificationAssessment.userId, + reconciledAt: new Date('2024-09-26'), + }); + + certificationAssessmentRepository.getByCertificationCourseId + .withArgs({ certificationCourseId }) + .resolves(certificationAssessment); + competenceMarkRepository.findByCertificationCourseId.resolves([]); scoringCertificationService.calculateCertificationAssessmentScore .withArgs({ certificationAssessment, continueOnError: true }) .resolves(certificationAssessmentScore); + certificationCandidateRepository.getByCertificationCourseId + .withArgs({ certificationCourseId }) + .resolves(candidate); + placementProfileService.getPlacementProfile .withArgs({ - userId: certificationAssessment.userId, - limitDate: certificationAssessment.createdAt, + userId: candidate.userId, + limitDate: candidate.reconciledAt, version: certificationAssessment.version, allowExcessPixAndLevels: false, }) @@ -80,6 +89,7 @@ describe('Certification | Session-management | Unit | Domain | UseCases | get-ce competenceMarkRepository, certificationAssessmentRepository, scoringCertificationService, + certificationCandidateRepository, }); //then @@ -125,18 +135,18 @@ describe('Certification | Session-management | Unit | Domain | UseCases | get-ce const certificationAssessmentRepository = { getByCertificationCourseId: sinon.stub(), }; - const placementProfileService = { getPlacementProfile: sinon.stub(), }; - const competenceMarkRepository = { findByCertificationCourseId: sinon.stub(), }; - const scoringCertificationService = { calculateCertificationAssessmentScore: sinon.stub(), }; + const certificationCandidateRepository = { + getByCertificationCourseId: sinon.stub(), + }; const certificationCourseId = 1234; const certificationChallenge = domainBuilder.buildCertificationChallengeWithType({ @@ -166,14 +176,23 @@ describe('Certification | Session-management | Unit | Domain | UseCases | get-ce competencesData: [{ id: 'recComp1', index: '1.1', name: 'Manger des fruits', level: 3, score: 45 }], }); + const candidate = domainBuilder.certification.sessionManagement.buildCertificationCandidate({ + userId: certificationAssessment.userId, + reconciledAt: new Date('2024-09-26'), + }); + certificationAssessmentRepository.getByCertificationCourseId .withArgs({ certificationCourseId }) .resolves(certificationAssessment); + certificationCandidateRepository.getByCertificationCourseId + .withArgs({ certificationCourseId }) + .resolves(candidate); + placementProfileService.getPlacementProfile .withArgs({ - userId: certificationAssessment.userId, - limitDate: certificationAssessment.createdAt, + userId: candidate.userId, + limitDate: candidate.reconciledAt, version: certificationAssessment.version, allowExcessPixAndLevels: false, }) @@ -188,6 +207,7 @@ describe('Certification | Session-management | Unit | Domain | UseCases | get-ce certificationCourseId, placementProfileService, competenceMarkRepository, + certificationCandidateRepository, certificationAssessmentRepository, scoringCertificationService, }); diff --git a/api/tests/tooling/domain-builder/factory/build-certification-candidate.js b/api/tests/tooling/domain-builder/factory/build-certification-candidate.js index 1f9f604fd49..2e7a46cc1b2 100644 --- a/api/tests/tooling/domain-builder/factory/build-certification-candidate.js +++ b/api/tests/tooling/domain-builder/factory/build-certification-candidate.js @@ -26,6 +26,7 @@ const buildCertificationCandidate = function ({ prepaymentCode = null, subscriptions = [domainBuilder.buildCoreSubscription({ certificationCandidateId: 123 })], accessibilityAdjustmentNeeded = false, + reconciledAt = false, } = {}) { return new CertificationCandidate({ id, @@ -52,6 +53,7 @@ const buildCertificationCandidate = function ({ prepaymentCode, subscriptions, accessibilityAdjustmentNeeded, + reconciledAt, }); }; diff --git a/api/tests/tooling/domain-builder/factory/certification/session-management/build-certification-candidate.js b/api/tests/tooling/domain-builder/factory/certification/session-management/build-certification-candidate.js new file mode 100644 index 00000000000..c003c2b452c --- /dev/null +++ b/api/tests/tooling/domain-builder/factory/certification/session-management/build-certification-candidate.js @@ -0,0 +1,8 @@ +import { CertificationCandidate } from '../../../../../../src/certification/session-management/domain/models/CertificationCandidate.js'; + +export const buildCertificationCandidate = function ({ userId = 456, reconciledAt = new Date('2024-09-26') } = {}) { + return new CertificationCandidate({ + userId, + reconciledAt, + }); +}; diff --git a/api/tests/tooling/domain-builder/factory/index.js b/api/tests/tooling/domain-builder/factory/index.js index 6adeb0fe29b..df17ea3e366 100644 --- a/api/tests/tooling/domain-builder/factory/index.js +++ b/api/tests/tooling/domain-builder/factory/index.js @@ -195,6 +195,7 @@ import { buildCertificationChallengeCapacity } from './certification/scoring/bui import { buildCertificationChallengeForScoring } from './certification/scoring/build-certification-challenge-for-scoring.js'; import { buildCompetenceForScoring } from './certification/scoring/build-competence-for-scoring.js'; import { buildV3CertificationScoring } from './certification/scoring/build-v3-certification-scoring.js'; +import { buildCertificationCandidate as buildSessionManagementCandidate } from './certification/session-management/build-certification-candidate.js'; import { buildCertificationDetails } from './certification/session-management/build-certification-details.js'; import { buildCertificationSessionComplementaryCertification } from './certification/session-management/build-certification-session-complementary-certification.js'; import { buildSessionManagement } from './certification/session-management/build-session.js'; @@ -241,6 +242,7 @@ const certification = { sessionManagement: { buildCertificationSessionComplementaryCertification, buildSession: buildSessionManagement, + buildCertificationCandidate: buildSessionManagementCandidate, }, shared: { buildJuryComment: buildJuryComment, diff --git a/api/tests/unit/domain/models/CertificationCandidate_test.js b/api/tests/unit/domain/models/CertificationCandidate_test.js index 33d47057c7d..762a8c7617d 100644 --- a/api/tests/unit/domain/models/CertificationCandidate_test.js +++ b/api/tests/unit/domain/models/CertificationCandidate_test.js @@ -12,6 +12,7 @@ describe('Unit | Domain | Models | Certification Candidate', function () { beforeEach(function () { coreSubscription = domainBuilder.buildCoreSubscription(); + const date = new Date(); rawData = { firstName: 'Jean-Pierre', @@ -28,6 +29,7 @@ describe('Unit | Domain | Models | Certification Candidate', function () { sex: 'M', subscriptions: [coreSubscription], billingMode: 'FREE', + reconciledAt: date, }; expectedData = { @@ -45,6 +47,7 @@ describe('Unit | Domain | Models | Certification Candidate', function () { subscriptions: [coreSubscription], hasSeenCertificationInstructions: false, accessibilityAdjustmentNeeded: false, + reconciledAt: date, }; }); diff --git a/api/tests/unit/domain/usecases/retrieve-last-or-create-certification-course_test.js b/api/tests/unit/domain/usecases/retrieve-last-or-create-certification-course_test.js index 9c3d18db4ec..1575ef7f660 100644 --- a/api/tests/unit/domain/usecases/retrieve-last-or-create-certification-course_test.js +++ b/api/tests/unit/domain/usecases/retrieve-last-or-create-certification-course_test.js @@ -8,16 +8,16 @@ import { MAX_REACHABLE_LEVEL } from '../../../../src/shared/domain/constants.js' import { CandidateNotAuthorizedToJoinSessionError, CandidateNotAuthorizedToResumeCertificationTestError, + LanguageNotSupportedError, NotFoundError, UnexpectedUserAccountError, } from '../../../../src/shared/domain/errors.js'; -import { LanguageNotSupportedError } from '../../../../src/shared/domain/errors.js'; import { Assessment } from '../../../../src/shared/domain/models/Assessment.js'; import { catchErr, domainBuilder, expect, sinon } from '../../../test-helper.js'; describe('Unit | UseCase | retrieve-last-or-create-certification-course', function () { let clock; - let now; + let reconciledAt; let verificationCode; const sessionRepository = {}; @@ -51,8 +51,8 @@ describe('Unit | UseCase | retrieve-last-or-create-certification-course', functi }; beforeEach(function () { - now = new Date('2019-01-01T05:06:07Z'); - clock = sinon.useFakeTimers({ now, toFake: ['Date'] }); + reconciledAt = new Date('2019-01-01T05:06:07Z'); + clock = sinon.useFakeTimers({ now: reconciledAt, toFake: ['Date'] }); verificationCode = Symbol('verificationCode'); assessmentRepository.save = sinon.stub(); @@ -295,15 +295,17 @@ describe('Unit | UseCase | retrieve-last-or-create-certification-course', functi accessCode: 'accessCode', }); sessionRepository.get.withArgs({ id: 1 }).resolves(foundSession); + const certificationCandidate = domainBuilder.buildCertificationCandidate({ + userId: 2, + sessionId: 1, + authorizedToStart: true, + subscriptions: [domainBuilder.buildCoreSubscription()], + reconciledAt, + }); - certificationCandidateRepository.getBySessionIdAndUserId.withArgs({ sessionId: 1, userId: 2 }).resolves( - domainBuilder.buildCertificationCandidate({ - userId: 2, - sessionId: 1, - authorizedToStart: true, - subscriptions: [domainBuilder.buildCoreSubscription()], - }), - ); + certificationCandidateRepository.getBySessionIdAndUserId + .withArgs({ sessionId: 1, userId: 2 }) + .resolves(certificationCandidate); certificationCourseRepository.findOneCertificationCourseByUserIdAndSessionId .withArgs({ userId: 2, sessionId: 1 }) @@ -313,7 +315,7 @@ describe('Unit | UseCase | retrieve-last-or-create-certification-course', functi const { placementProfile, userCompetencesWithChallenges } = _buildPlacementProfileWithTwoChallenges({ placementProfileService, userId: 2, - now, + reconciledAt: certificationCandidate.reconciledAt, version: foundSession.version, }); @@ -365,6 +367,7 @@ describe('Unit | UseCase | retrieve-last-or-create-certification-course', functi sessionId: 1, authorizedToStart: true, subscriptions: [domainBuilder.buildCoreSubscription()], + reconciledAt, }); certificationCandidateRepository.getBySessionIdAndUserId .withArgs({ sessionId: 1, userId: 2 }) @@ -378,7 +381,7 @@ describe('Unit | UseCase | retrieve-last-or-create-certification-course', functi _buildPlacementProfileWithTwoChallenges({ placementProfileService, userId: 2, - now, + reconciledAt: foundCertificationCandidate.reconciledAt, version: foundSession.version, }); certificationChallengesService.pickCertificationChallenges @@ -505,6 +508,7 @@ describe('Unit | UseCase | retrieve-last-or-create-certification-course', functi sessionId: 1, authorizedToStart: true, subscriptions: [domainBuilder.buildCoreSubscription()], + reconciledAt, }); certificationCandidateRepository.getBySessionIdAndUserId .withArgs({ sessionId: 1, userId }) @@ -517,7 +521,7 @@ describe('Unit | UseCase | retrieve-last-or-create-certification-course', functi _buildPlacementProfileWithTwoChallenges({ placementProfileService, userId, - now, + reconciledAt: foundCertificationCandidate.reconciledAt, version: foundSession.version, }); @@ -618,13 +622,14 @@ describe('Unit | UseCase | retrieve-last-or-create-certification-course', functi authorizedToStart: true, subscriptions: [domainBuilder.buildCoreSubscription()], complementaryCertification, + reconciledAt, }); const { challenge1, challenge2, placementProfile, userCompetencesWithChallenges } = _buildPlacementProfileWithTwoChallenges({ placementProfileService, userId: 2, - now, + reconciledAt: foundCertificationCandidate.reconciledAt, version: foundSession.version, }); certificationChallengesService.pickCertificationChallenges @@ -739,13 +744,14 @@ describe('Unit | UseCase | retrieve-last-or-create-certification-course', functi authorizedToStart: true, subscriptions: [domainBuilder.buildCoreSubscription()], complementaryCertification, + reconciledAt, }); const { challenge1, challenge2, placementProfile, userCompetencesWithChallenges } = _buildPlacementProfileWithTwoChallenges({ placementProfileService, userId: 2, - now, + reconciledAt: foundCertificationCandidate.reconciledAt, version: foundSession.version, }); certificationChallengesService.pickCertificationChallenges @@ -853,6 +859,7 @@ describe('Unit | UseCase | retrieve-last-or-create-certification-course', functi authorizedToStart: true, subscriptions: [domainBuilder.buildCoreSubscription()], complementaryCertification, + reconciledAt, }); certificationCandidateRepository.getBySessionIdAndUserId @@ -863,7 +870,7 @@ describe('Unit | UseCase | retrieve-last-or-create-certification-course', functi _buildPlacementProfileWithTwoChallenges({ placementProfileService, userId: 2, - now, + reconciledAt: foundCertificationCandidate.reconciledAt, version: foundSession.version, }); certificationChallengesService.pickCertificationChallenges @@ -958,7 +965,7 @@ describe('Unit | UseCase | retrieve-last-or-create-certification-course', functi _buildPlacementProfileWithTwoChallenges({ placementProfileService, userId: 2, - now, + reconciledAt: foundCertificationCandidate.reconciledAt, version: foundSession.version, }); certificationChallengesService.pickCertificationChallenges @@ -1069,6 +1076,7 @@ describe('Unit | UseCase | retrieve-last-or-create-certification-course', functi sessionId: 1, subscriptions: [domainBuilder.buildCoreSubscription()], complementaryCertification: null, + reconciledAt, }); certificationCandidateRepository.getBySessionIdAndUserId @@ -1079,7 +1087,7 @@ describe('Unit | UseCase | retrieve-last-or-create-certification-course', functi _buildPlacementProfileWithTwoChallenges({ placementProfileService, userId: 2, - now, + reconciledAt: foundCertificationCandidate.reconciledAt, version: foundSession.version, }); certificationChallengesService.pickCertificationChallenges @@ -1166,6 +1174,7 @@ describe('Unit | UseCase | retrieve-last-or-create-certification-course', functi authorizedToStart: true, subscriptions: [domainBuilder.buildCoreSubscription()], complementaryCertification, + reconciledAt, }); certificationCandidateRepository.getBySessionIdAndUserId @@ -1176,7 +1185,7 @@ describe('Unit | UseCase | retrieve-last-or-create-certification-course', functi _buildPlacementProfileWithTwoChallenges({ placementProfileService, userId: 2, - now, + reconciledAt: foundCertificationCandidate.reconciledAt, version: foundSession.version, }); certificationChallengesService.pickCertificationChallenges @@ -1239,7 +1248,7 @@ describe('Unit | UseCase | retrieve-last-or-create-certification-course', functi }); }); -function _buildPlacementProfileWithTwoChallenges({ placementProfileService, userId, now, version }) { +function _buildPlacementProfileWithTwoChallenges({ placementProfileService, userId, reconciledAt, version }) { const challenge1 = domainBuilder.buildChallenge({ id: 'challenge1' }); const challenge2 = domainBuilder.buildChallenge({ id: 'challenge2' }); // TODO : use the domainBuilder to instanciate userCompetences @@ -1247,7 +1256,9 @@ function _buildPlacementProfileWithTwoChallenges({ placementProfileService, user isCertifiable: sinon.stub().returns(true), userCompetences: [{ challenges: [challenge1] }, { challenges: [challenge2] }], }; - placementProfileService.getPlacementProfile.withArgs({ userId, limitDate: now, version }).resolves(placementProfile); + placementProfileService.getPlacementProfile + .withArgs({ userId, limitDate: reconciledAt, version }) + .resolves(placementProfile); const userCompetencesWithChallenges = _.clone(placementProfile.userCompetences); userCompetencesWithChallenges[0].challenges[0].testedSkill = domainBuilder.buildSkill();