Skip to content

Commit 6a5fb9b

Browse files
committed
feat(api): use external jury result in v3 pix+edu certificates
1 parent 81e8c55 commit 6a5fb9b

8 files changed

Lines changed: 187 additions & 83 deletions

File tree

api/src/certification/results/domain/models/CertificateSummary.js

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { AssessmentResult } from '../../../../shared/domain/models/AssessmentResult.js';
2-
import { PIX_PLUS_EDU_EXTERNAL_LEVELS } from '../../../shared/domain/constants/mesh-configuration.js';
32
import { AlgorithmEngineVersion } from '../../../shared/domain/models/AlgorithmEngineVersion.js';
4-
import { isEduFramework } from '../../../shared/domain/models/Frameworks.js';
53
import { hasCoreScope } from '../../../shared/domain/models/Frameworks.js';
64
import { JuryComment, JuryCommentContexts } from '../../../shared/domain/models/JuryComment.js';
75
import { CertificateMeshLevel } from './v3/CertificateMeshLevel.js';
@@ -134,19 +132,11 @@ export class CertificateSummary {
134132
certificateType,
135133
reachedMeshLevel: isRejectedV3
136134
? null
137-
: _getReachedMeshLevel({ reachedMeshIndex, certificationFramework, algorithmVersion, eduV3ExternalJuryResult }),
135+
: _getReachedMeshLevel({ reachedMeshIndex, certificationFramework, eduV3ExternalJuryResult }),
138136
});
139137
}
140138
}
141139

142-
function _getReachedMeshLevel({ reachedMeshIndex, certificationFramework, algorithmVersion, eduV3ExternalJuryResult }) {
143-
if (
144-
AlgorithmEngineVersion.isV3(algorithmVersion) &&
145-
isEduFramework(certificationFramework) &&
146-
Object.values(PIX_PLUS_EDU_EXTERNAL_LEVELS).includes(eduV3ExternalJuryResult)
147-
) {
148-
return `LEVEL_${eduV3ExternalJuryResult}`;
149-
}
150-
151-
return new CertificateMeshLevel({ reachedMeshIndex, certificationFramework }).meshLevel;
140+
function _getReachedMeshLevel({ reachedMeshIndex, certificationFramework, eduV3ExternalJuryResult }) {
141+
return new CertificateMeshLevel({ reachedMeshIndex, certificationFramework, eduV3ExternalJuryResult }).meshLevel;
152142
}

api/src/certification/results/domain/models/v3/Certificate.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export class Certificate {
3232
deliveredAt,
3333
pixScore,
3434
reachedMeshIndex,
35+
eduV3ExternalJuryResult,
3536
verificationCode,
3637
resultCompetenceTree,
3738
algorithmEngineVersion,
@@ -47,7 +48,7 @@ export class Certificate {
4748
this.deliveredAt = deliveredAt;
4849
this.certificationCenter = certificationCenter;
4950
this.pixScore = pixScore;
50-
this.globalLevel = this.#findLevel(reachedMeshIndex, certificationFramework);
51+
this.globalLevel = this.#findLevel({ reachedMeshIndex, certificationFramework, eduV3ExternalJuryResult });
5152
this.verificationCode = verificationCode;
5253
this.maxReachableScore = MAX_REACHABLE_SCORE;
5354
this.resultCompetenceTree = this.globalLevel?.meshLevel && this.pixScore ? resultCompetenceTree : null;
@@ -57,8 +58,12 @@ export class Certificate {
5758
this.certificationFramework = certificationFramework;
5859
}
5960

60-
#findLevel(reachedMeshIndex, certificationFramework) {
61-
const globalCertificationLevel = new CertificateMeshLevel({ reachedMeshIndex, certificationFramework });
61+
#findLevel({ reachedMeshIndex, certificationFramework, eduV3ExternalJuryResult }) {
62+
const globalCertificationLevel = new CertificateMeshLevel({
63+
reachedMeshIndex,
64+
certificationFramework,
65+
eduV3ExternalJuryResult,
66+
});
6267

6368
return globalCertificationLevel.meshLevel === CORE_CERTIFICATE_LEVELS.preBeginner ? null : globalCertificationLevel;
6469
}

api/src/certification/results/domain/models/v3/CertificateMeshLevel.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
import Joi from 'joi';
55

66
import { EntityValidationError } from '../../../../../shared/domain/errors.js';
7-
import { Frameworks } from '../../../../shared/domain/models/Frameworks.js';
7+
import { PIX_PLUS_EDU_EXTERNAL_LEVELS } from '../../../../shared/domain/constants/mesh-configuration.js';
8+
import { Frameworks, isEduFramework } from '../../../../shared/domain/models/Frameworks.js';
89

910
export const CORE_LEVELS = {
1011
0: 'LEVEL_PRE_BEGINNER',
@@ -39,10 +40,11 @@ export class CertificateMeshLevel {
3940
* @param {object} props
4041
* @param {number} props.reachedMeshIndex
4142
* @param {string} props.certificationFramework
43+
* @param {string} [props.eduV3ExternalJuryResult]
4244
*/
43-
constructor({ reachedMeshIndex, certificationFramework }) {
45+
constructor({ reachedMeshIndex, certificationFramework, eduV3ExternalJuryResult }) {
4446
this.certificationFramework = certificationFramework;
45-
this.meshLevel = this.#getLevelKey({ reachedMeshIndex, certificationFramework });
47+
this.meshLevel = this.#getLevelKey({ reachedMeshIndex, certificationFramework, eduV3ExternalJuryResult });
4648
this.#validate();
4749
}
4850

@@ -58,15 +60,19 @@ export class CertificateMeshLevel {
5860
return this.#translate({ translate, key: `${this.meshLevel}.description` });
5961
}
6062

61-
#getLevelKey({ reachedMeshIndex, certificationFramework }) {
62-
if (reachedMeshIndex === null) return null;
63+
#getLevelKey({ reachedMeshIndex, certificationFramework, eduV3ExternalJuryResult }) {
64+
if (reachedMeshIndex === null || !certificationFramework) return null;
65+
66+
if (
67+
isEduFramework(certificationFramework) &&
68+
Object.values(PIX_PLUS_EDU_EXTERNAL_LEVELS).includes(eduV3ExternalJuryResult)
69+
) {
70+
return `LEVEL_${eduV3ExternalJuryResult}`;
71+
}
6372

6473
switch (certificationFramework) {
6574
case Frameworks.CORE:
6675
case Frameworks.CLEA:
67-
if (reachedMeshIndex === null) {
68-
return null;
69-
}
7076
return CORE_LEVELS[reachedMeshIndex];
7177
case Frameworks.EDU_1ER_DEGRE:
7278
case Frameworks.EDU_2ND_DEGRE:

api/src/certification/results/infrastructure/repositories/certificate-repository.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ function _selectCertificationCourseDTOs(knexConn) {
134134
certificationFramework: 'certification-courses.framework',
135135
pixScore: 'assessment-results.pixScore',
136136
reachedMeshIndex: 'assessment-results.reachedMeshIndex',
137+
eduV3ExternalJuryResult: 'assessment-results.eduV3ExternalJuryResult',
137138
assessmentResultId: 'assessment-results.id',
138139
competenceMarks: knexConn.raw(`
139140
json_agg(

api/tests/certification/results/unit/domain/models/CertificateSummary_test.js

Lines changed: 22 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -242,47 +242,22 @@ describe('Unit | Domain | Models | CertificationSummary', function () {
242242
});
243243
});
244244

245-
context('when certification is Pix+ V3 EDU', function () {
246-
['EDU_1ER_DEGRE', 'EDU_2ND_DEGRE', 'EDU_CPE'].forEach((eduFramework) => {
247-
context(`for framework ${eduFramework}`, function () {
248-
['ADVANCED', 'EXPERT'].forEach((eduV3ExternalJuryResult) => {
249-
it(`should override reachedMeshLevel with the external jury result LEVEL_${eduV3ExternalJuryResult}`, function () {
250-
// when
251-
const actualCertificateSummary = CertificateSummary.buildFrom({
252-
...baseData,
253-
certificationFramework: Frameworks[eduFramework],
254-
algorithmVersion: AlgorithmEngineVersion.V3,
255-
assessmentResultStatus: AssessmentResult.status.VALIDATED,
256-
isPublished: true,
257-
isExtraCertificationAcquired: true,
258-
reachedMeshIndex: 0,
259-
eduV3ExternalJuryResult,
260-
});
261-
262-
// then
263-
expect(actualCertificateSummary.reachedMeshLevel).to.equal(`LEVEL_${eduV3ExternalJuryResult}`);
264-
});
265-
});
266-
267-
[null, 'UNSET'].forEach((eduV3ExternalJuryResult) => {
268-
it(`should keep the admissible level when external jury result is ${eduV3ExternalJuryResult}`, function () {
269-
// when
270-
const actualCertificateSummary = CertificateSummary.buildFrom({
271-
...baseData,
272-
certificationFramework: Frameworks[eduFramework],
273-
algorithmVersion: AlgorithmEngineVersion.V3,
274-
assessmentResultStatus: AssessmentResult.status.VALIDATED,
275-
isPublished: true,
276-
isExtraCertificationAcquired: true,
277-
reachedMeshIndex: 0,
278-
eduV3ExternalJuryResult,
279-
});
280-
281-
// then
282-
expect(actualCertificateSummary.reachedMeshLevel).to.equal('LEVEL_ADMISSIBLE');
283-
});
284-
});
245+
context('when certification is Pix+ V3 EDU with an external jury result', function () {
246+
it('should pass eduV3ExternalJuryResult through to CertificateMeshLevel', function () {
247+
// when
248+
const actualCertificateSummary = CertificateSummary.buildFrom({
249+
...baseData,
250+
certificationFramework: Frameworks.EDU_2ND_DEGRE,
251+
algorithmVersion: AlgorithmEngineVersion.V3,
252+
assessmentResultStatus: AssessmentResult.status.VALIDATED,
253+
isPublished: true,
254+
isExtraCertificationAcquired: true,
255+
reachedMeshIndex: 0,
256+
eduV3ExternalJuryResult: PIX_PLUS_EDU_EXTERNAL_LEVELS.ADVANCED,
285257
});
258+
259+
// then
260+
expect(actualCertificateSummary.reachedMeshLevel).to.equal('LEVEL_ADVANCED');
286261
});
287262
});
288263
});
@@ -330,9 +305,14 @@ describe('Unit | Domain | Models | CertificationSummary', function () {
330305
});
331306
});
332307
context('badgeUrl', function () {
333-
it('should return null when certification is not V3', function () {
308+
it('should return null when certification is CORE', function () {
334309
const certificateSummary = CertificateSummary.buildFrom({
335-
algorithmVersion: AlgorithmEngineVersion.V2,
310+
id: 123,
311+
algorithmVersion: AlgorithmEngineVersion.V3,
312+
certificationFramework: Frameworks.CORE,
313+
assessmentResultStatus: AssessmentResult.status.VALIDATED,
314+
isPublished: true,
315+
reachedMeshIndex: 1,
336316
});
337317

338318
expect(certificateSummary.badgeUrl).to.equal(null);

api/tests/certification/results/unit/domain/models/v3/CertificateMeshLevel_test.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
CORE_LEVELS,
44
EDU_LEVELS,
55
} from '../../../../../../../src/certification/results/domain/models/v3/CertificateMeshLevel.js';
6+
import { PIX_PLUS_EDU_EXTERNAL_LEVELS } from '../../../../../../../src/certification/shared/domain/constants/mesh-configuration.js';
67
import { Frameworks } from '../../../../../../../src/certification/shared/domain/models/Frameworks.js';
78
import { getI18n } from '../../../../../../../src/shared/infrastructure/i18n/i18n.js';
89
import { expect } from '../../../../../../test-helper.js';
@@ -54,6 +55,40 @@ describe('Unit | Domain | Models | CertificateMeshLevel', function () {
5455
expect(globalCertificationLevel.meshLevel).to.equal(EDU_LEVELS['0']);
5556
});
5657
});
58+
59+
describe('when the external jury result is set', function () {
60+
['EDU_1ER_DEGRE', 'EDU_2ND_DEGRE', 'EDU_CPE'].forEach((eduFramework) => {
61+
context(`for framework ${eduFramework}`, function () {
62+
Object.values(PIX_PLUS_EDU_EXTERNAL_LEVELS).forEach((externalLevel) => {
63+
it(`should return LEVEL_${externalLevel} when eduV3ExternalJuryResult is ${externalLevel}`, function () {
64+
// when
65+
const globalCertificationLevel = new CertificateMeshLevel({
66+
reachedMeshIndex: 0,
67+
certificationFramework: Frameworks[eduFramework],
68+
eduV3ExternalJuryResult: externalLevel,
69+
});
70+
71+
// then
72+
expect(globalCertificationLevel.meshLevel).to.equal(`LEVEL_${externalLevel}`);
73+
});
74+
});
75+
76+
[null, undefined, 'UNSET'].forEach((eduV3ExternalJuryResult) => {
77+
it(`should fall back to LEVEL_ADMISSIBLE when eduV3ExternalJuryResult is ${eduV3ExternalJuryResult}`, function () {
78+
// when
79+
const globalCertificationLevel = new CertificateMeshLevel({
80+
reachedMeshIndex: 0,
81+
certificationFramework: Frameworks[eduFramework],
82+
eduV3ExternalJuryResult,
83+
});
84+
85+
// then
86+
expect(globalCertificationLevel.meshLevel).to.equal('LEVEL_ADMISSIBLE');
87+
});
88+
});
89+
});
90+
});
91+
});
5792
});
5893

5994
describe('#getLevelLabel', function () {

api/tests/certification/results/unit/domain/models/v3/Certificate_test.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Certificate } from '../../../../../../../src/certification/results/domain/models/v3/Certificate.js';
22
import { CertificateMeshLevel } from '../../../../../../../src/certification/results/domain/models/v3/CertificateMeshLevel.js';
3+
import { PIX_PLUS_EDU_EXTERNAL_LEVELS } from '../../../../../../../src/certification/shared/domain/constants/mesh-configuration.js';
34
import { Frameworks } from '../../../../../../../src/certification/shared/domain/models/Frameworks.js';
45
import { expect } from '../../../../../../test-helper.js';
56

@@ -77,5 +78,57 @@ describe('Unit | Domain | Models | Certification | Results | Certificate v3', fu
7778
);
7879
});
7980
});
81+
82+
describe('when the framework is EDU with an external jury result', function () {
83+
it('should return the level based on the external jury result', function () {
84+
// given & when
85+
const certificate = new Certificate({
86+
id: 1,
87+
firstName: 'Jean',
88+
lastName: 'Bon',
89+
birthdate: '1992-06-12',
90+
birthplace: 'Paris',
91+
certificationCenter: 'L\u2019universit\u00e9 du Pix',
92+
deliveredAt: new Date('2025-10-30T01:02:03Z'),
93+
pixScore: 63,
94+
verificationCode: 'P-SOMECODE',
95+
resultCompetenceTree: [Symbol('competence')],
96+
reachedMeshIndex: 0,
97+
certificationFramework: Frameworks.EDU_2ND_DEGRE,
98+
eduV3ExternalJuryResult: PIX_PLUS_EDU_EXTERNAL_LEVELS.ADVANCED,
99+
});
100+
101+
// then
102+
expect(certificate.globalLevel).to.deepEqualInstance(
103+
new CertificateMeshLevel({
104+
reachedMeshIndex: 0,
105+
certificationFramework: Frameworks.EDU_2ND_DEGRE,
106+
eduV3ExternalJuryResult: PIX_PLUS_EDU_EXTERNAL_LEVELS.ADVANCED,
107+
}),
108+
);
109+
expect(certificate.globalLevel.meshLevel).to.equal('LEVEL_ADVANCED');
110+
});
111+
112+
it('should fall back to admissible level when external jury result is not set', function () {
113+
// given & when
114+
const certificate = new Certificate({
115+
id: 1,
116+
firstName: 'Jean',
117+
lastName: 'Bon',
118+
birthdate: '1992-06-12',
119+
birthplace: 'Paris',
120+
certificationCenter: 'L\u2019universit\u00e9 du Pix',
121+
deliveredAt: new Date('2025-10-30T01:02:03Z'),
122+
pixScore: 63,
123+
verificationCode: 'P-SOMECODE',
124+
resultCompetenceTree: [Symbol('competence')],
125+
reachedMeshIndex: 0,
126+
certificationFramework: Frameworks.EDU_1ER_DEGRE,
127+
});
128+
129+
// then
130+
expect(certificate.globalLevel.meshLevel).to.equal('LEVEL_ADMISSIBLE');
131+
});
132+
});
80133
});
81134
});

0 commit comments

Comments
 (0)