From cb88b969407eccfaf9b215b8429b74f194f26346 Mon Sep 17 00:00:00 2001 From: Lou Goubin <103123143+lgoubin@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:06:04 +0200 Subject: [PATCH 01/11] feat(admin): add activate version button on page 3 for pix plus versions --- .../versions/version/calibration.js | 10 +++++- .../versions/version/calibration-test.js | 35 +++++++++++++++++-- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/admin/app/controllers/authenticated/certification-frameworks/certification-framework/versions/version/calibration.js b/admin/app/controllers/authenticated/certification-frameworks/certification-framework/versions/version/calibration.js index 702f4c53f15..d20c4a075ef 100644 --- a/admin/app/controllers/authenticated/certification-frameworks/certification-framework/versions/version/calibration.js +++ b/admin/app/controllers/authenticated/certification-frameworks/certification-framework/versions/version/calibration.js @@ -13,8 +13,16 @@ export default class CalibrationController extends Controller { this.isConfirmationModalOpen = !this.isConfirmationModalOpen; } + get hasNoExternalCalibrationId() { + return !this.model.draftVersion.externalCalibrationId; + } + + get isPixPlusScope() { + return this.model.draftVersion.scope !== 'CORE'; + } + @action activateVersion() { - return this.versionController.activateVersion(this.model.draftVersion); + return this.versionController.activateVersion(this.model.draftVersion, this.model.calibrationScoringConfiguration); } } diff --git a/admin/tests/unit/controllers/authenticated/certification-frameworks/certification-framework/versions/version/calibration-test.js b/admin/tests/unit/controllers/authenticated/certification-frameworks/certification-framework/versions/version/calibration-test.js index df7de75bfab..42f17a02cff 100644 --- a/admin/tests/unit/controllers/authenticated/certification-frameworks/certification-framework/versions/version/calibration-test.js +++ b/admin/tests/unit/controllers/authenticated/certification-frameworks/certification-framework/versions/version/calibration-test.js @@ -32,17 +32,46 @@ module( }); }); + module('#hasNoExternalCalibrationId', function () { + test('returns true when externalCalibrationId is null', function (assert) { + controller.model = { draftVersion: { externalCalibrationId: null } }; + + assert.true(controller.hasNoExternalCalibrationId); + }); + + test('returns false when externalCalibrationId is set', function (assert) { + controller.model = { draftVersion: { externalCalibrationId: 42 } }; + + assert.false(controller.hasNoExternalCalibrationId); + }); + }); + + module('#isPixPlusScope', function () { + test('returns true when scope is not CORE', function (assert) { + controller.model = { draftVersion: { scope: 'PIX_PLUS_EDU_2ND_DEGRE' } }; + + assert.true(controller.isPixPlusScope); + }); + + test('returns false when scope is CORE', function (assert) { + controller.model = { draftVersion: { scope: 'CORE' } }; + + assert.false(controller.isPixPlusScope); + }); + }); + module('#activateVersion', function () { - test('delegates to versionController.activateVersion with draftVersion only', function (assert) { + test('delegates to versionController.activateVersion with draftVersion and calibrationScoringConfiguration', function (assert) { const draftVersion = { id: 1 }; - controller.model = { draftVersion }; + const calibrationScoringConfiguration = { globalScoringConfiguration: [] }; + controller.model = { draftVersion, calibrationScoringConfiguration }; const activateVersionStub = sinon.stub(); controller.versionController = { activateVersion: activateVersionStub }; controller.activateVersion(); - sinon.assert.calledWithExactly(activateVersionStub, draftVersion); + sinon.assert.calledWithExactly(activateVersionStub, draftVersion, calibrationScoringConfiguration); assert.ok(true); }); }); From 078c86422b52b74191a88b95a94c3dc720aae0f2 Mon Sep 17 00:00:00 2001 From: Lou Goubin <103123143+lgoubin@users.noreply.github.com> Date: Mon, 31 Aug 2026 11:03:12 +0200 Subject: [PATCH 02/11] feat(api): create route to save scoring on active Pix+ version --- api/src/certification/configuration/domain/usecases/index.js | 1 + .../acceptance/application/certification-version-route_test.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/api/src/certification/configuration/domain/usecases/index.js b/api/src/certification/configuration/domain/usecases/index.js index aca59ff2a4f..3a3d0cfab2f 100644 --- a/api/src/certification/configuration/domain/usecases/index.js +++ b/api/src/certification/configuration/domain/usecases/index.js @@ -32,6 +32,7 @@ import { saveScoringConfiguration } from './save-scoring-configuration.js'; import { searchAttachableTargetProfiles } from './search-attachable-target-profiles.js'; import { sendTargetProfileNotifications } from './send-target-profile-notifications.js'; import { updateScoBlockedAccessDate } from './update-sco-blocked-access-date.js'; +import { saveScoringConfiguration } from './save-scoring-configuration.js'; import { updateVersion } from './update-version.js'; import { updateVersionComment } from './update-version-comment.js'; diff --git a/api/tests/certification/configuration/acceptance/application/certification-version-route_test.js b/api/tests/certification/configuration/acceptance/application/certification-version-route_test.js index 210a674bb2f..0feb60ab467 100644 --- a/api/tests/certification/configuration/acceptance/application/certification-version-route_test.js +++ b/api/tests/certification/configuration/acceptance/application/certification-version-route_test.js @@ -969,7 +969,7 @@ describe('Acceptance | Certification | Configuration | API | certification-versi // then expect(response.statusCode).to.equal(204); const updatedVersion = await knex('certification_versions').where({ id: 42 }).first(); - expect(updatedVersion.globalScoringConfiguration).to.deep.equal(globalScoringConfiguration); + expect(JSON.parse(updatedVersion.globalScoringConfiguration)).to.deep.equal(globalScoringConfiguration); }); it('returns 403 when the user is not a super admin', async function () { From a6168b46f0158c4fff2047848d53b0503208357d Mon Sep 17 00:00:00 2001 From: Lou Goubin <103123143+lgoubin@users.noreply.github.com> Date: Mon, 31 Aug 2026 15:03:18 +0200 Subject: [PATCH 03/11] feat(api): expose scope and hasGlobalScoringInfo to front end --- .../application/certification-version-route_test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/tests/certification/configuration/acceptance/application/certification-version-route_test.js b/api/tests/certification/configuration/acceptance/application/certification-version-route_test.js index 0feb60ab467..db5df749d11 100644 --- a/api/tests/certification/configuration/acceptance/application/certification-version-route_test.js +++ b/api/tests/certification/configuration/acceptance/application/certification-version-route_test.js @@ -21,7 +21,7 @@ import { domainBuilder } from '../../../../tooling/domain-builder/domain-builder import { getServer } from '../../../../tooling/server/shared-server.js'; import { generateAuthenticatedUserRequestHeaders } from '../../../../tooling/test-utils/http-server.js'; -describe('Acceptance | Certification | Configuration | API | certification-version-route', function () { +describe.only('Acceptance | Certification | Configuration | API | certification-version-route', function () { let server; let superAdmin; @@ -969,7 +969,7 @@ describe('Acceptance | Certification | Configuration | API | certification-versi // then expect(response.statusCode).to.equal(204); const updatedVersion = await knex('certification_versions').where({ id: 42 }).first(); - expect(JSON.parse(updatedVersion.globalScoringConfiguration)).to.deep.equal(globalScoringConfiguration); + expect(updatedVersion.globalScoringConfiguration).to.deep.equal(globalScoringConfiguration); }); it('returns 403 when the user is not a super admin', async function () { From 9438f28f2b5e6522c8de802e430e5c423808b970 Mon Sep 17 00:00:00 2001 From: Lou Goubin <103123143+lgoubin@users.noreply.github.com> Date: Tue, 1 Sep 2026 10:32:04 +0200 Subject: [PATCH 04/11] feat(admin): allow Pix+ version activation without scoring --- .../versions/version/calibration.js | 10 +----- .../versions/version/calibration-test.js | 35 ++----------------- .../versions/version/scoring-test.js | 1 - 3 files changed, 4 insertions(+), 42 deletions(-) diff --git a/admin/app/controllers/authenticated/certification-frameworks/certification-framework/versions/version/calibration.js b/admin/app/controllers/authenticated/certification-frameworks/certification-framework/versions/version/calibration.js index d20c4a075ef..702f4c53f15 100644 --- a/admin/app/controllers/authenticated/certification-frameworks/certification-framework/versions/version/calibration.js +++ b/admin/app/controllers/authenticated/certification-frameworks/certification-framework/versions/version/calibration.js @@ -13,16 +13,8 @@ export default class CalibrationController extends Controller { this.isConfirmationModalOpen = !this.isConfirmationModalOpen; } - get hasNoExternalCalibrationId() { - return !this.model.draftVersion.externalCalibrationId; - } - - get isPixPlusScope() { - return this.model.draftVersion.scope !== 'CORE'; - } - @action activateVersion() { - return this.versionController.activateVersion(this.model.draftVersion, this.model.calibrationScoringConfiguration); + return this.versionController.activateVersion(this.model.draftVersion); } } diff --git a/admin/tests/unit/controllers/authenticated/certification-frameworks/certification-framework/versions/version/calibration-test.js b/admin/tests/unit/controllers/authenticated/certification-frameworks/certification-framework/versions/version/calibration-test.js index 42f17a02cff..df7de75bfab 100644 --- a/admin/tests/unit/controllers/authenticated/certification-frameworks/certification-framework/versions/version/calibration-test.js +++ b/admin/tests/unit/controllers/authenticated/certification-frameworks/certification-framework/versions/version/calibration-test.js @@ -32,46 +32,17 @@ module( }); }); - module('#hasNoExternalCalibrationId', function () { - test('returns true when externalCalibrationId is null', function (assert) { - controller.model = { draftVersion: { externalCalibrationId: null } }; - - assert.true(controller.hasNoExternalCalibrationId); - }); - - test('returns false when externalCalibrationId is set', function (assert) { - controller.model = { draftVersion: { externalCalibrationId: 42 } }; - - assert.false(controller.hasNoExternalCalibrationId); - }); - }); - - module('#isPixPlusScope', function () { - test('returns true when scope is not CORE', function (assert) { - controller.model = { draftVersion: { scope: 'PIX_PLUS_EDU_2ND_DEGRE' } }; - - assert.true(controller.isPixPlusScope); - }); - - test('returns false when scope is CORE', function (assert) { - controller.model = { draftVersion: { scope: 'CORE' } }; - - assert.false(controller.isPixPlusScope); - }); - }); - module('#activateVersion', function () { - test('delegates to versionController.activateVersion with draftVersion and calibrationScoringConfiguration', function (assert) { + test('delegates to versionController.activateVersion with draftVersion only', function (assert) { const draftVersion = { id: 1 }; - const calibrationScoringConfiguration = { globalScoringConfiguration: [] }; - controller.model = { draftVersion, calibrationScoringConfiguration }; + controller.model = { draftVersion }; const activateVersionStub = sinon.stub(); controller.versionController = { activateVersion: activateVersionStub }; controller.activateVersion(); - sinon.assert.calledWithExactly(activateVersionStub, draftVersion, calibrationScoringConfiguration); + sinon.assert.calledWithExactly(activateVersionStub, draftVersion); assert.ok(true); }); }); diff --git a/admin/tests/unit/controllers/authenticated/certification-frameworks/certification-framework/versions/version/scoring-test.js b/admin/tests/unit/controllers/authenticated/certification-frameworks/certification-framework/versions/version/scoring-test.js index 049f594f970..18fb7daabb6 100644 --- a/admin/tests/unit/controllers/authenticated/certification-frameworks/certification-framework/versions/version/scoring-test.js +++ b/admin/tests/unit/controllers/authenticated/certification-frameworks/certification-framework/versions/version/scoring-test.js @@ -1,6 +1,5 @@ import { setupTest } from 'ember-qunit'; import { module, test } from 'qunit'; -import sinon from 'sinon'; module( 'Unit | Controller | authenticated/certification-frameworks/certification-framework/versions/version/scoring', From c1528654db09f62f51dfa01fdd10648e910498ae Mon Sep 17 00:00:00 2001 From: Lou Goubin <103123143+lgoubin@users.noreply.github.com> Date: Tue, 1 Sep 2026 10:33:55 +0200 Subject: [PATCH 05/11] feat(admin): save scoring configuration on active Pix+ version --- .../certification-framework/versions/version.js | 3 ++- .../certification-framework/versions/version/scoring.js | 8 ++++++-- .../versions/version/scoring-test.js | 1 + 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/admin/app/controllers/authenticated/certification-frameworks/certification-framework/versions/version.js b/admin/app/controllers/authenticated/certification-frameworks/certification-framework/versions/version.js index 28a1d08793c..b729ef5f346 100644 --- a/admin/app/controllers/authenticated/certification-frameworks/certification-framework/versions/version.js +++ b/admin/app/controllers/authenticated/certification-frameworks/certification-framework/versions/version.js @@ -13,7 +13,8 @@ export default class VersionController extends Controller { version.globalScoringConfiguration = version.globalScoringConfiguration?.length ? [...version.globalScoringConfiguration] : (calibrationScoringConfiguration?.globalScoringConfiguration ?? []); - version.competencesScoringConfiguration = calibrationScoringConfiguration?.competencesScoringConfiguration ?? []; + version.competencesScoringConfiguration = + calibrationScoringConfiguration?.competencesScoringConfiguration ?? []; await version.save({ adapterOptions: { saveScoring: true } }); } diff --git a/admin/app/controllers/authenticated/certification-frameworks/certification-framework/versions/version/scoring.js b/admin/app/controllers/authenticated/certification-frameworks/certification-framework/versions/version/scoring.js index 29cb57d6e5b..e3b7626c7c6 100644 --- a/admin/app/controllers/authenticated/certification-frameworks/certification-framework/versions/version/scoring.js +++ b/admin/app/controllers/authenticated/certification-frameworks/certification-framework/versions/version/scoring.js @@ -46,7 +46,9 @@ export default class ScoringController extends Controller { await this.router.transitionTo('authenticated.certification-frameworks.certification-framework'); } catch { this.pixToast.sendErrorNotification({ - message: this.intl.t('components.certification-frameworks.certification-framework.versions.scoring.save-error'), + message: this.intl.t( + 'components.certification-frameworks.certification-framework.versions.scoring.save-error', + ), }); } } @@ -58,7 +60,9 @@ export default class ScoringController extends Controller { await this.versionController.activateVersion(this.model.editVersion); } catch { this.pixToast.sendErrorNotification({ - message: this.intl.t('components.certification-frameworks.certification-framework.versions.scoring.save-error'), + message: this.intl.t( + 'components.certification-frameworks.certification-framework.versions.scoring.save-error', + ), }); } } diff --git a/admin/tests/unit/controllers/authenticated/certification-frameworks/certification-framework/versions/version/scoring-test.js b/admin/tests/unit/controllers/authenticated/certification-frameworks/certification-framework/versions/version/scoring-test.js index 18fb7daabb6..049f594f970 100644 --- a/admin/tests/unit/controllers/authenticated/certification-frameworks/certification-framework/versions/version/scoring-test.js +++ b/admin/tests/unit/controllers/authenticated/certification-frameworks/certification-framework/versions/version/scoring-test.js @@ -1,5 +1,6 @@ import { setupTest } from 'ember-qunit'; import { module, test } from 'qunit'; +import sinon from 'sinon'; module( 'Unit | Controller | authenticated/certification-frameworks/certification-framework/versions/version/scoring', From 161c318e068ba439ac2d9da318616fc4dc753961 Mon Sep 17 00:00:00 2001 From: Lou Goubin <103123143+lgoubin@users.noreply.github.com> Date: Tue, 1 Sep 2026 18:05:31 +0200 Subject: [PATCH 06/11] lint(api): fix --- api/src/certification/configuration/domain/usecases/index.js | 1 - .../acceptance/application/certification-version-route_test.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/api/src/certification/configuration/domain/usecases/index.js b/api/src/certification/configuration/domain/usecases/index.js index 3a3d0cfab2f..aca59ff2a4f 100644 --- a/api/src/certification/configuration/domain/usecases/index.js +++ b/api/src/certification/configuration/domain/usecases/index.js @@ -32,7 +32,6 @@ import { saveScoringConfiguration } from './save-scoring-configuration.js'; import { searchAttachableTargetProfiles } from './search-attachable-target-profiles.js'; import { sendTargetProfileNotifications } from './send-target-profile-notifications.js'; import { updateScoBlockedAccessDate } from './update-sco-blocked-access-date.js'; -import { saveScoringConfiguration } from './save-scoring-configuration.js'; import { updateVersion } from './update-version.js'; import { updateVersionComment } from './update-version-comment.js'; diff --git a/api/tests/certification/configuration/acceptance/application/certification-version-route_test.js b/api/tests/certification/configuration/acceptance/application/certification-version-route_test.js index db5df749d11..210a674bb2f 100644 --- a/api/tests/certification/configuration/acceptance/application/certification-version-route_test.js +++ b/api/tests/certification/configuration/acceptance/application/certification-version-route_test.js @@ -21,7 +21,7 @@ import { domainBuilder } from '../../../../tooling/domain-builder/domain-builder import { getServer } from '../../../../tooling/server/shared-server.js'; import { generateAuthenticatedUserRequestHeaders } from '../../../../tooling/test-utils/http-server.js'; -describe.only('Acceptance | Certification | Configuration | API | certification-version-route', function () { +describe('Acceptance | Certification | Configuration | API | certification-version-route', function () { let server; let superAdmin; From c7415baaa01424703a768c8274cc712ad4e65ced Mon Sep 17 00:00:00 2001 From: Lou Goubin <103123143+lgoubin@users.noreply.github.com> Date: Tue, 1 Sep 2026 18:05:58 +0200 Subject: [PATCH 07/11] lint(admin): fix --- .../certification-framework/versions/version.js | 3 +-- .../certification-framework/versions/version/scoring.js | 8 ++------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/admin/app/controllers/authenticated/certification-frameworks/certification-framework/versions/version.js b/admin/app/controllers/authenticated/certification-frameworks/certification-framework/versions/version.js index b729ef5f346..28a1d08793c 100644 --- a/admin/app/controllers/authenticated/certification-frameworks/certification-framework/versions/version.js +++ b/admin/app/controllers/authenticated/certification-frameworks/certification-framework/versions/version.js @@ -13,8 +13,7 @@ export default class VersionController extends Controller { version.globalScoringConfiguration = version.globalScoringConfiguration?.length ? [...version.globalScoringConfiguration] : (calibrationScoringConfiguration?.globalScoringConfiguration ?? []); - version.competencesScoringConfiguration = - calibrationScoringConfiguration?.competencesScoringConfiguration ?? []; + version.competencesScoringConfiguration = calibrationScoringConfiguration?.competencesScoringConfiguration ?? []; await version.save({ adapterOptions: { saveScoring: true } }); } diff --git a/admin/app/controllers/authenticated/certification-frameworks/certification-framework/versions/version/scoring.js b/admin/app/controllers/authenticated/certification-frameworks/certification-framework/versions/version/scoring.js index e3b7626c7c6..29cb57d6e5b 100644 --- a/admin/app/controllers/authenticated/certification-frameworks/certification-framework/versions/version/scoring.js +++ b/admin/app/controllers/authenticated/certification-frameworks/certification-framework/versions/version/scoring.js @@ -46,9 +46,7 @@ export default class ScoringController extends Controller { await this.router.transitionTo('authenticated.certification-frameworks.certification-framework'); } catch { this.pixToast.sendErrorNotification({ - message: this.intl.t( - 'components.certification-frameworks.certification-framework.versions.scoring.save-error', - ), + message: this.intl.t('components.certification-frameworks.certification-framework.versions.scoring.save-error'), }); } } @@ -60,9 +58,7 @@ export default class ScoringController extends Controller { await this.versionController.activateVersion(this.model.editVersion); } catch { this.pixToast.sendErrorNotification({ - message: this.intl.t( - 'components.certification-frameworks.certification-framework.versions.scoring.save-error', - ), + message: this.intl.t('components.certification-frameworks.certification-framework.versions.scoring.save-error'), }); } } From 18de5e0a89559722b4ce2a80d59039562ce482fd Mon Sep 17 00:00:00 2001 From: Lou Goubin <103123143+lgoubin@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:57:40 +0200 Subject: [PATCH 08/11] feature(api): trigger scoring job when scoring is activated on already activated Pix+ version --- .../domain/models/ScoreCertificationJob.js | 5 ++ .../configuration/domain/usecases/index.js | 4 + .../usecases/save-scoring-configuration.js | 10 +++ ...rtification-courses-to-score-repository.js | 11 +++ .../score-certification-job-repository.js | 10 +++ .../score-certification-job-controller.js | 14 +++ .../certification-version-route_test.js | 12 ++- .../save-scoring-configuration_test.js | 36 ++++++++ ...cation-courses-to-score-repository_test.js | 85 +++++++++++++++++++ .../save-scoring-configuration_test.js | 67 ++++++++++++++- ...score-certification-job-controller_test.js | 21 +++++ 11 files changed, 271 insertions(+), 4 deletions(-) create mode 100644 api/src/certification/configuration/domain/models/ScoreCertificationJob.js create mode 100644 api/src/certification/configuration/infrastructure/repositories/certification-courses-to-score-repository.js create mode 100644 api/src/certification/configuration/infrastructure/repositories/jobs/score-certification-job-repository.js create mode 100644 api/src/certification/evaluation/application/jobs/score-certification-job-controller.js create mode 100644 api/tests/certification/configuration/integration/infrastructure/repositories/certification-courses-to-score-repository_test.js create mode 100644 api/tests/certification/evaluation/unit/application/jobs/score-certification-job-controller_test.js diff --git a/api/src/certification/configuration/domain/models/ScoreCertificationJob.js b/api/src/certification/configuration/domain/models/ScoreCertificationJob.js new file mode 100644 index 00000000000..eeeebf5630e --- /dev/null +++ b/api/src/certification/configuration/domain/models/ScoreCertificationJob.js @@ -0,0 +1,5 @@ +export class ScoreCertificationJob { + constructor({ certificationCourseId }) { + this.certificationCourseId = certificationCourseId; + } +} diff --git a/api/src/certification/configuration/domain/usecases/index.js b/api/src/certification/configuration/domain/usecases/index.js index aca59ff2a4f..cc7fa6194a2 100644 --- a/api/src/certification/configuration/domain/usecases/index.js +++ b/api/src/certification/configuration/domain/usecases/index.js @@ -6,10 +6,12 @@ import * as attachableTargetProfileRepository from '../../infrastructure/reposit import * as calibratedChallengesRepository from '../../infrastructure/repositories/calibrated-challenges-repository.js'; import * as calibrationRepository from '../../infrastructure/repositories/calibration-repository.js'; import * as centerRepository from '../../infrastructure/repositories/center-repository.js'; +import * as certificationCoursesToScoreRepository from '../../infrastructure/repositories/certification-courses-to-score-repository.js'; import * as certificationInfoRepository from '../../infrastructure/repositories/certification-info-repository.js'; import * as complementaryCertificationBadgesRepository from '../../infrastructure/repositories/complementary-certification-badge-repository.js'; import * as complementaryCertificationForTargetProfileAttachmentRepository from '../../infrastructure/repositories/complementary-certification-for-target-profile-attachment-repository.js'; import * as complementaryCertificationRepository from '../../infrastructure/repositories/complementary-certification-repository.js'; +import { scoreCertificationJobRepository } from '../../infrastructure/repositories/jobs/score-certification-job-repository.js'; import * as organizationRepository from '../../infrastructure/repositories/organization-repository.js'; import * as ScoBlockedAccessDatesRepository from '../../infrastructure/repositories/sco-blocked-access-dates-repository.js'; import * as versionDetailsRepository from '../../infrastructure/repositories/version-details-repository.js'; @@ -56,6 +58,8 @@ import { updateVersionComment } from './update-version-comment.js'; const dependencies = { calibratedChallengesRepository, attachableTargetProfileRepository, + certificationCoursesToScoreRepository, + scoreCertificationJobRepository, centerRepository, ScoBlockedAccessDatesRepository, certificationInfoRepository, diff --git a/api/src/certification/configuration/domain/usecases/save-scoring-configuration.js b/api/src/certification/configuration/domain/usecases/save-scoring-configuration.js index 9fa4f56a742..3df5babed32 100644 --- a/api/src/certification/configuration/domain/usecases/save-scoring-configuration.js +++ b/api/src/certification/configuration/domain/usecases/save-scoring-configuration.js @@ -1,4 +1,5 @@ import { NotFoundError } from '../../../../shared/domain/errors.js'; +import { ScoreCertificationJob } from '../models/ScoreCertificationJob.js'; /** * @param {object} params @@ -6,12 +7,16 @@ import { NotFoundError } from '../../../../shared/domain/errors.js'; * @param {Array} params.globalScoringConfiguration * @param {Array|null} params.competencesScoringConfiguration * @param {object} params.versionRepository + * @param {object} params.certificationCoursesToScoreRepository + * @param {object} params.scoreCertificationJobRepository */ export async function saveScoringConfiguration({ id, globalScoringConfiguration, competencesScoringConfiguration, versionRepository, + certificationCoursesToScoreRepository, + scoreCertificationJobRepository, }) { const version = await versionRepository.getById({ id }); @@ -20,4 +25,9 @@ export async function saveScoringConfiguration({ } await versionRepository.updateScoring({ id, globalScoringConfiguration, competencesScoringConfiguration }); + + const certificationCourseIds = await certificationCoursesToScoreRepository.findIdsByVersionId({ versionId: id }); + await scoreCertificationJobRepository.performAsync( + ...certificationCourseIds.map((certificationCourseId) => new ScoreCertificationJob({ certificationCourseId })), + ); } diff --git a/api/src/certification/configuration/infrastructure/repositories/certification-courses-to-score-repository.js b/api/src/certification/configuration/infrastructure/repositories/certification-courses-to-score-repository.js new file mode 100644 index 00000000000..dac106b2cc6 --- /dev/null +++ b/api/src/certification/configuration/infrastructure/repositories/certification-courses-to-score-repository.js @@ -0,0 +1,11 @@ +import { DomainTransaction } from '../../../../shared/domain/DomainTransaction.js'; + +export async function findIdsByVersionId({ versionId }) { + const knexConn = DomainTransaction.getConnection(); + const rows = await knexConn('certification-courses') + .select('certification-courses.id') + .join('sessions', 'sessions.id', 'certification-courses.sessionId') + .whereNotNull('sessions.finalizedAt') + .where('certification-courses.versionId', versionId); + return rows.map((row) => row.id); +} diff --git a/api/src/certification/configuration/infrastructure/repositories/jobs/score-certification-job-repository.js b/api/src/certification/configuration/infrastructure/repositories/jobs/score-certification-job-repository.js new file mode 100644 index 00000000000..ef1199c6891 --- /dev/null +++ b/api/src/certification/configuration/infrastructure/repositories/jobs/score-certification-job-repository.js @@ -0,0 +1,10 @@ +import { JobRepository } from '../../../../../shared/infrastructure/repositories/jobs/job-repository.js'; +import { ScoreCertificationJob } from '../../../domain/models/ScoreCertificationJob.js'; + +class ScoreCertificationJobRepository extends JobRepository { + constructor() { + super({ name: ScoreCertificationJob.name }); + } +} + +export const scoreCertificationJobRepository = new ScoreCertificationJobRepository(); diff --git a/api/src/certification/evaluation/application/jobs/score-certification-job-controller.js b/api/src/certification/evaluation/application/jobs/score-certification-job-controller.js new file mode 100644 index 00000000000..e9c763fc970 --- /dev/null +++ b/api/src/certification/evaluation/application/jobs/score-certification-job-controller.js @@ -0,0 +1,14 @@ +import { JobController } from '../../../../shared/application/jobs/job-controller.js'; +import { ScoreCertificationJob } from '../../../configuration/domain/models/ScoreCertificationJob.js'; +import { usecases } from '../../domain/usecases/index.js'; + +export class ScoreCertificationJobController extends JobController { + constructor() { + super(ScoreCertificationJob.name); + } + + async handle({ data }) { + const { certificationCourseId } = data; + await usecases.scoreV3Certification({ certificationCourseId }); + } +} diff --git a/api/tests/certification/configuration/acceptance/application/certification-version-route_test.js b/api/tests/certification/configuration/acceptance/application/certification-version-route_test.js index 210a674bb2f..7e22525bdc9 100644 --- a/api/tests/certification/configuration/acceptance/application/certification-version-route_test.js +++ b/api/tests/certification/configuration/acceptance/application/certification-version-route_test.js @@ -932,7 +932,7 @@ describe('Acceptance | Certification | Configuration | API | certification-versi }); describe('PATCH /api/admin/certification-versions/{certificationVersionId}/scoring', function () { - it('updates the scoring configuration of a version', async function () { + it('updates the scoring configuration of a version and enqueues scoring jobs for existing certifications', async function () { // given domainBuilder.certification.configuration .versionBuilder() @@ -944,6 +944,13 @@ describe('Acceptance | Certification | Configuration | API | certification-versi competencesScoringConfiguration: null, }) .insertToDB({ databaseBuilder }); + + const finalizedSession = databaseBuilder.factory.buildSession({ finalizedAt: new Date('2025-01-01') }); + const course = databaseBuilder.factory.buildCertificationCourse({ + sessionId: finalizedSession.id, + versionId: 42, + }); + await databaseBuilder.commit(); const globalScoringConfiguration = [{ meshLevel: 1, bounds: { min: -2, max: 3 } }]; @@ -970,6 +977,9 @@ describe('Acceptance | Certification | Configuration | API | certification-versi expect(response.statusCode).to.equal(204); const updatedVersion = await knex('certification_versions').where({ id: 42 }).first(); expect(updatedVersion.globalScoringConfiguration).to.deep.equal(globalScoringConfiguration); + await expect('ScoreCertificationJob').to.have.been.performed.withJobPayload( + sinon.match({ certificationCourseId: course.id }), + ); }); it('returns 403 when the user is not a super admin', async function () { diff --git a/api/tests/certification/configuration/integration/domain/usecases/save-scoring-configuration_test.js b/api/tests/certification/configuration/integration/domain/usecases/save-scoring-configuration_test.js index 0dea37ab199..dc6c20834d8 100644 --- a/api/tests/certification/configuration/integration/domain/usecases/save-scoring-configuration_test.js +++ b/api/tests/certification/configuration/integration/domain/usecases/save-scoring-configuration_test.js @@ -1,8 +1,11 @@ import { expect } from 'chai'; +import { ScoreCertificationJob } from '../../../../../../src/certification/configuration/domain/models/ScoreCertificationJob.js'; import { usecases } from '../../../../../../src/certification/configuration/domain/usecases/index.js'; import * as versionRepository from '../../../../../../src/certification/configuration/infrastructure/repositories/version-repository.js'; import { SCOPES } from '../../../../../../src/certification/shared/domain/models/Scopes.js'; +import { EMPTY_CORRELATION_INFO } from '../../../../../../src/shared/infrastructure/execution-context-manager.js'; +import { JobPriority } from '../../../../../../src/shared/infrastructure/repositories/jobs/job-repository.js'; import { databaseBuilder } from '../../../../../tooling/databases.js'; import { domainBuilder } from '../../../../../tooling/domain-builder/domain-builder.js'; @@ -35,4 +38,37 @@ describe('Certification | Configuration | Integration | Domain | UseCase | save- expect(updatedVersion.globalScoringConfiguration).to.deep.equal(globalScoringConfiguration); expect(updatedVersion.competencesScoringConfiguration).to.be.null; }); + + it('enqueues a ScoreCertificationJob for each certification course on a finalized session', async function () { + // given + const version = domainBuilder.certification.configuration + .versionBuilder() + .asActive() + .withParameters({ id: 456, scope: SCOPES.PIX_PLUS_DROIT, globalScoringConfiguration: [] }) + .insertToDB({ databaseBuilder }); + + const finalizedSession = databaseBuilder.factory.buildSession({ finalizedAt: new Date('2025-01-01') }); + const course = databaseBuilder.factory.buildCertificationCourse({ + sessionId: finalizedSession.id, + versionId: version.id, + }); + + await databaseBuilder.commit(); + + // when + await usecases.saveScoringConfiguration({ + id: 456, + globalScoringConfiguration: [{ meshLevel: 1, bounds: { min: 0, max: 100 } }], + competencesScoringConfiguration: null, + }); + + // then + await expect(ScoreCertificationJob.name).to.have.been.performed.withJob({ + data: { + certificationCourseId: course.id, + correlationContext: EMPTY_CORRELATION_INFO, + }, + priority: JobPriority.DEFAULT, + }); + }); }); diff --git a/api/tests/certification/configuration/integration/infrastructure/repositories/certification-courses-to-score-repository_test.js b/api/tests/certification/configuration/integration/infrastructure/repositories/certification-courses-to-score-repository_test.js new file mode 100644 index 00000000000..06051a685ed --- /dev/null +++ b/api/tests/certification/configuration/integration/infrastructure/repositories/certification-courses-to-score-repository_test.js @@ -0,0 +1,85 @@ +import { expect } from 'chai'; + +import { findIdsByVersionId } from '../../../../../../src/certification/configuration/infrastructure/repositories/certification-courses-to-score-repository.js'; +import { SCOPES } from '../../../../../../src/certification/shared/domain/models/Scopes.js'; +import { databaseBuilder } from '../../../../../tooling/databases.js'; +import { domainBuilder } from '../../../../../tooling/domain-builder/domain-builder.js'; + +describe('Certification | Configuration | Integration | Infrastructure | Repositories | certification-courses-to-score-repository', function () { + describe('#findIdsByVersionId', function () { + it('returns ids of certification courses on finalized sessions for the given version', async function () { + // given + const version = domainBuilder.certification.configuration + .versionBuilder() + .asActive() + .withParameters({ id: 1, scope: SCOPES.PIX_PLUS_DROIT }) + .insertToDB({ databaseBuilder }); + + const finalizedSession = databaseBuilder.factory.buildSession({ finalizedAt: new Date('2025-01-01') }); + const courseOnFinalizedSession = databaseBuilder.factory.buildCertificationCourse({ + sessionId: finalizedSession.id, + versionId: version.id, + }); + + await databaseBuilder.commit(); + + // when + const ids = await findIdsByVersionId({ versionId: version.id }); + + // then + expect(ids).to.deep.equal([courseOnFinalizedSession.id]); + }); + + it('excludes certification courses on non-finalized sessions', async function () { + // given + const version = domainBuilder.certification.configuration + .versionBuilder() + .asActive() + .withParameters({ id: 2, scope: SCOPES.PIX_PLUS_DROIT }) + .insertToDB({ databaseBuilder }); + + const nonFinalizedSession = databaseBuilder.factory.buildSession({ finalizedAt: null }); + databaseBuilder.factory.buildCertificationCourse({ + sessionId: nonFinalizedSession.id, + versionId: version.id, + }); + + await databaseBuilder.commit(); + + // when + const ids = await findIdsByVersionId({ versionId: version.id }); + + // then + expect(ids).to.be.empty; + }); + + it('excludes certification courses belonging to a different version', async function () { + // given + const version = domainBuilder.certification.configuration + .versionBuilder() + .asActive() + .withParameters({ id: 3, scope: SCOPES.PIX_PLUS_DROIT }) + .insertToDB({ databaseBuilder }); + + const otherVersion = domainBuilder.certification.configuration + .versionBuilder() + .asActive() + .withParameters({ id: 4, scope: SCOPES.PIX_PLUS_EDU_1ER_DEGRE }) + .insertToDB({ databaseBuilder }); + + const finalizedSession = databaseBuilder.factory.buildSession({ finalizedAt: new Date('2025-01-01') }); + databaseBuilder.factory.buildCertificationCourse({ + sessionId: finalizedSession.id, + versionId: otherVersion.id, + }); + + await databaseBuilder.commit(); + + // when + const ids = await findIdsByVersionId({ versionId: version.id }); + + // then + expect(ids).to.be.empty; + }); + }); +}); diff --git a/api/tests/certification/configuration/unit/domain/usecases/save-scoring-configuration_test.js b/api/tests/certification/configuration/unit/domain/usecases/save-scoring-configuration_test.js index b3429c7b4ae..750c3e27e40 100644 --- a/api/tests/certification/configuration/unit/domain/usecases/save-scoring-configuration_test.js +++ b/api/tests/certification/configuration/unit/domain/usecases/save-scoring-configuration_test.js @@ -1,6 +1,7 @@ import { expect } from 'chai'; import sinon from 'sinon'; +import { ScoreCertificationJob } from '../../../../../../src/certification/configuration/domain/models/ScoreCertificationJob.js'; import { saveScoringConfiguration } from '../../../../../../src/certification/configuration/domain/usecases/save-scoring-configuration.js'; import { NotFoundError } from '../../../../../../src/shared/domain/errors.js'; import { domainBuilder } from '../../../../../tooling/domain-builder/domain-builder.js'; @@ -8,12 +9,20 @@ import { catchErr } from '../../../../../tooling/test-utils/error.js'; describe('Certification | Configuration | Unit | UseCase | save-scoring-configuration', function () { let versionRepository; + let certificationCoursesToScoreRepository; + let scoreCertificationJobRepository; beforeEach(function () { versionRepository = { getById: sinon.stub(), updateScoring: sinon.stub(), }; + certificationCoursesToScoreRepository = { + findIdsByVersionId: sinon.stub(), + }; + scoreCertificationJobRepository = { + performAsync: sinon.stub(), + }; }); context('when the version does not exist', function () { @@ -27,6 +36,8 @@ describe('Certification | Configuration | Unit | UseCase | save-scoring-configur globalScoringConfiguration: [], competencesScoringConfiguration: null, versionRepository, + certificationCoursesToScoreRepository, + scoreCertificationJobRepository, }); // then @@ -35,16 +46,22 @@ describe('Certification | Configuration | Unit | UseCase | save-scoring-configur }); context('when the version exists', function () { - it('calls updateScoring with the right parameters', async function () { - // given - const version = domainBuilder.certification.configuration + let version; + + beforeEach(function () { + version = domainBuilder.certification.configuration .versionBuilder() .asActive() .withParameters({ id: 42 }) .build(); versionRepository.getById.resolves(version); versionRepository.updateScoring.resolves(); + certificationCoursesToScoreRepository.findIdsByVersionId.resolves([]); + scoreCertificationJobRepository.performAsync.resolves(); + }); + it('calls updateScoring with the right parameters', async function () { + // given const globalScoringConfiguration = [{ meshLevel: 1, bounds: { min: 0, max: 100 } }]; const competencesScoringConfiguration = null; @@ -54,6 +71,8 @@ describe('Certification | Configuration | Unit | UseCase | save-scoring-configur globalScoringConfiguration, competencesScoringConfiguration, versionRepository, + certificationCoursesToScoreRepository, + scoreCertificationJobRepository, }); // then @@ -63,5 +82,47 @@ describe('Certification | Configuration | Unit | UseCase | save-scoring-configur competencesScoringConfiguration, }); }); + + it('enqueues a ScoreCertificationJob for each certification course on finalized sessions', async function () { + // given + certificationCoursesToScoreRepository.findIdsByVersionId.resolves([10, 20, 30]); + + // when + await saveScoringConfiguration({ + id: 42, + globalScoringConfiguration: [], + competencesScoringConfiguration: null, + versionRepository, + certificationCoursesToScoreRepository, + scoreCertificationJobRepository, + }); + + // then + sinon.assert.calledWithExactly(certificationCoursesToScoreRepository.findIdsByVersionId, { versionId: 42 }); + sinon.assert.calledWithExactly( + scoreCertificationJobRepository.performAsync, + new ScoreCertificationJob({ certificationCourseId: 10 }), + new ScoreCertificationJob({ certificationCourseId: 20 }), + new ScoreCertificationJob({ certificationCourseId: 30 }), + ); + }); + + it('enqueues no job when no certification courses are found', async function () { + // given + certificationCoursesToScoreRepository.findIdsByVersionId.resolves([]); + + // when + await saveScoringConfiguration({ + id: 42, + globalScoringConfiguration: [], + competencesScoringConfiguration: null, + versionRepository, + certificationCoursesToScoreRepository, + scoreCertificationJobRepository, + }); + + // then + sinon.assert.calledWithExactly(scoreCertificationJobRepository.performAsync); + }); }); }); diff --git a/api/tests/certification/evaluation/unit/application/jobs/score-certification-job-controller_test.js b/api/tests/certification/evaluation/unit/application/jobs/score-certification-job-controller_test.js new file mode 100644 index 00000000000..d57a5fb93b3 --- /dev/null +++ b/api/tests/certification/evaluation/unit/application/jobs/score-certification-job-controller_test.js @@ -0,0 +1,21 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; + +import { ScoreCertificationJob } from '../../../../../../src/certification/configuration/domain/models/ScoreCertificationJob.js'; +import { ScoreCertificationJobController } from '../../../../../../src/certification/evaluation/application/jobs/score-certification-job-controller.js'; +import { usecases } from '../../../../../../src/certification/evaluation/domain/usecases/index.js'; + +describe('Unit | Certification | Evaluation | Application | jobs | ScoreCertificationJobController', function () { + it('calls scoreV3Certification with the certificationCourseId', async function () { + // given + sinon.stub(usecases, 'scoreV3Certification'); + const controller = new ScoreCertificationJobController(); + const data = new ScoreCertificationJob({ certificationCourseId: 123 }); + + // when + await controller.handle({ data }); + + // then + expect(usecases.scoreV3Certification).to.have.been.calledWithExactly({ certificationCourseId: 123 }); + }); +}); From 7ba2479ebdaab465386c6267f1516f6fa6b66485 Mon Sep 17 00:00:00 2001 From: Lou Goubin <103123143+lgoubin@users.noreply.github.com> Date: Thu, 3 Sep 2026 09:41:59 +0200 Subject: [PATCH 09/11] refactor(api): handle non-scored certification display better --- .../domain/models/JuryCertification.js | 8 +++- .../read-models/JuryCertificationSummary.js | 4 ++ .../jury-certification-repository.js | 6 ++- .../domain/models/JuryCertification_test.js | 46 +++++++++++++++++++ .../JuryCertificationSummary_test.js | 12 +++++ 5 files changed, 74 insertions(+), 2 deletions(-) diff --git a/api/src/certification/session-management/domain/models/JuryCertification.js b/api/src/certification/session-management/domain/models/JuryCertification.js index bff42988dbc..7bca1b94532 100644 --- a/api/src/certification/session-management/domain/models/JuryCertification.js +++ b/api/src/certification/session-management/domain/models/JuryCertification.js @@ -105,6 +105,10 @@ export class JuryCertification { return `${this.certificationFramework}.NONE`; } + if (this.status === 'pending-scoring') { + return null; + } + const resultKey = this.eduV3ExternalJuryResult || (this.reachedMeshIndex ?? 'BELOW_MINIMUM'); return `${this.certificationFramework}.${resultKey}`; @@ -154,7 +158,9 @@ export class JuryCertification { birthCountry: juryCertificationDTO.birthCountry, birthPostalCode: juryCertificationDTO.birthPostalCode, createdAt: juryCertificationDTO.createdAt, - status: juryCertificationDTO.assessmentResultStatus, + status: + juryCertificationDTO.assessmentResultStatus ?? + (juryCertificationDTO.hasScoringConfiguration ? null : 'pending-scoring'), isPublished: juryCertificationDTO.isPublished, isRejectedForFraud: juryCertificationDTO.isRejectedForFraud, juryId: juryCertificationDTO.juryId, diff --git a/api/src/certification/session-management/domain/read-models/JuryCertificationSummary.js b/api/src/certification/session-management/domain/read-models/JuryCertificationSummary.js index bce968f174c..83249b6dfba 100644 --- a/api/src/certification/session-management/domain/read-models/JuryCertificationSummary.js +++ b/api/src/certification/session-management/domain/read-models/JuryCertificationSummary.js @@ -44,6 +44,10 @@ export class JuryCertificationSummary { return `${this.certificationFramework}.NONE`; } + if (this.status === STARTED) { + return null; + } + const resultKey = this.eduV3ExternalJuryResult || (this.reachedMeshIndex ?? 'BELOW_MINIMUM'); return `${this.certificationFramework}.${resultKey}`; diff --git a/api/src/certification/session-management/infrastructure/repositories/jury-certification-repository.js b/api/src/certification/session-management/infrastructure/repositories/jury-certification-repository.js index 54a0bd01e5d..8ee5d4a8ebf 100644 --- a/api/src/certification/session-management/infrastructure/repositories/jury-certification-repository.js +++ b/api/src/certification/session-management/infrastructure/repositories/jury-certification-repository.js @@ -120,6 +120,9 @@ function _selectJuryCertifications(knexConn) { commentByAutoJury: 'assessment-results.commentByAutoJury', certificationFramework: 'certification-courses.framework', lastAnswerAt: 'certification-courses.lastAnswerAt', + hasScoringConfiguration: knexConn.raw( + `(certification_versions."globalScoringConfiguration" IS NOT NULL AND jsonb_array_length(certification_versions."globalScoringConfiguration") > 0)`, + ), }) .from('certification-courses') .join('assessments', 'assessments.certificationCourseId', 'certification-courses.id') @@ -133,7 +136,8 @@ function _selectJuryCertifications(knexConn) { 'assessment-results.id', 'certification-courses-last-assessment-results.lastAssessmentResultId', ) - .groupBy('certification-courses.id', 'assessments.id', 'assessment-results.id'); + .leftJoin('certification_versions', 'certification_versions.id', 'certification-courses.versionId') + .groupBy('certification-courses.id', 'assessments.id', 'assessment-results.id', 'certification_versions.id'); } async function _toDomainWithComplementaryCertifications({ diff --git a/api/tests/certification/session-management/unit/domain/models/JuryCertification_test.js b/api/tests/certification/session-management/unit/domain/models/JuryCertification_test.js index 6db84da6445..476d14c35d1 100644 --- a/api/tests/certification/session-management/unit/domain/models/JuryCertification_test.js +++ b/api/tests/certification/session-management/unit/domain/models/JuryCertification_test.js @@ -183,6 +183,40 @@ describe('Unit | Domain | Models | JuryCertification', function () { }); expect(juryCertification).to.deepEqualInstance(expectedJuryCertification); }); + + context('when there is no assessment result', function () { + it('should set status to "pending-scoring" when there is no scoring configuration', function () { + // given + juryCertificationDTO.assessmentResultStatus = null; + juryCertificationDTO.hasScoringConfiguration = false; + + // when + const juryCertification = JuryCertification.from({ + juryCertificationDTO, + certificationIssueReports: [], + competenceMarkDTOs: [], + }); + + // then + expect(juryCertification.status).to.equal('pending-scoring'); + }); + + it('should set status to null when there is a scoring configuration', function () { + // given + juryCertificationDTO.assessmentResultStatus = null; + juryCertificationDTO.hasScoringConfiguration = true; + + // when + const juryCertification = JuryCertification.from({ + juryCertificationDTO, + certificationIssueReports: [], + competenceMarkDTOs: [], + }); + + // then + expect(juryCertification.status).to.be.null; + }); + }); }); describe('#get reachedResultKey', function () { @@ -322,6 +356,18 @@ describe('Unit | Domain | Models | JuryCertification', function () { }); }); }); + + context('when status is "pending-scoring"', function () { + it('returns null', function () { + const juryCertification = domainBuilder.certification.sessionManagement.buildJuryCertification({ + version: AlgorithmEngineVersion.V3, + certificationFramework: Frameworks.EDU_2ND_DEGRE, + status: 'pending-scoring', + }); + + expect(juryCertification.reachedResultKey).to.be.null; + }); + }); }); describe('#updateEduV3ExternalJuryResult', function () { diff --git a/api/tests/certification/session-management/unit/domain/read-models/JuryCertificationSummary_test.js b/api/tests/certification/session-management/unit/domain/read-models/JuryCertificationSummary_test.js index 8dff382f858..190e4ecaaf3 100644 --- a/api/tests/certification/session-management/unit/domain/read-models/JuryCertificationSummary_test.js +++ b/api/tests/certification/session-management/unit/domain/read-models/JuryCertificationSummary_test.js @@ -387,6 +387,18 @@ describe('Unit | Domain | Models | JuryCertificationSummary', function () { }); }); }); + + context('when status is "started" (no assessment result)', function () { + it('returns null', function () { + const juryCertificationSummary = domainBuilder.certification.sessionManagement.buildJuryCertificationSummary({ + certificationFramework: Frameworks.EDU_2ND_DEGRE, + algorithmVersion: AlgorithmEngineVersion.V3, + status: null, + }); + + expect(juryCertificationSummary.reachedResultKey).to.be.null; + }); + }); }); }); }); From 88ebc6e5f3274256adf612d23a2884706779fc36 Mon Sep 17 00:00:00 2001 From: Lou Goubin <103123143+lgoubin@users.noreply.github.com> Date: Thu, 3 Sep 2026 09:42:09 +0200 Subject: [PATCH 10/11] refactor(admin): handle non-scored certification display better --- .../certification/informations/global-actions.gjs | 6 +++++- admin/app/models/certification.js | 8 ++++++++ admin/app/models/jury-certification-summary.js | 1 + admin/tests/unit/models/certification-test.js | 1 + admin/translations/en.json | 1 + admin/translations/fr.json | 1 + 6 files changed, 17 insertions(+), 1 deletion(-) diff --git a/admin/app/components/certifications/certification/informations/global-actions.gjs b/admin/app/components/certifications/certification/informations/global-actions.gjs index b75cf8eb78f..442dae3b6e4 100644 --- a/admin/app/components/certifications/certification/informations/global-actions.gjs +++ b/admin/app/components/certifications/certification/informations/global-actions.gjs @@ -56,7 +56,11 @@ export default class CertificationInformationGlobalActions extends Component { } get displayRescoringCertificationButton() { - return Boolean(!this.args.certification.isPublished && this.args.session.finalizedAt); + return Boolean( + this.args.certification.status !== 'pending-scoring' && + !this.args.certification.isPublished && + this.args.session.finalizedAt, + ); } @action diff --git a/admin/app/models/certification.js b/admin/app/models/certification.js index a976fb678aa..5ccfdc50369 100644 --- a/admin/app/models/certification.js +++ b/admin/app/models/certification.js @@ -14,6 +14,7 @@ export const assessmentResultStatus = { CANCELLED: 'cancelled', CANCELLED_BY_JURY: 'cancelled_by_jury', ERROR: 'error', + PENDING_SCORING: 'pending-scoring', VALIDATED: 'validated', REJECTED: 'rejected', }; @@ -78,6 +79,12 @@ export default class Certification extends Model { } get statusLabelAndValue() { + if (this.status === assessmentResultStatus.PENDING_SCORING) { + return { + value: assessmentResultStatus.PENDING_SCORING, + label: this.intl.t('pages.certifications.certification.details.v3.assessment-result-status.pending-scoring'), + }; + } return certificationStatuses.find((certificationStatus) => certificationStatus.value === this.status); } @@ -117,6 +124,7 @@ export default class Certification extends Model { } get result() { + if (!this.reachedResultKey) return null; return this.intl.t(`common.certification.meshLevels.${this.reachedResultKey}`, { pixScore: this.pixScore, }); diff --git a/admin/app/models/jury-certification-summary.js b/admin/app/models/jury-certification-summary.js index 0acf7ef2313..6a2c05c386c 100644 --- a/admin/app/models/jury-certification-summary.js +++ b/admin/app/models/jury-certification-summary.js @@ -66,6 +66,7 @@ export default class JuryCertificationSummary extends Model { } get result() { + if (!this.reachedResultKey) return null; return this.intl.t(`common.certification.meshLevels.${this.reachedResultKey}`, { pixScore: this.pixScore, }); diff --git a/admin/tests/unit/models/certification-test.js b/admin/tests/unit/models/certification-test.js index b5d8b857fde..a9cbca8e1ea 100644 --- a/admin/tests/unit/models/certification-test.js +++ b/admin/tests/unit/models/certification-test.js @@ -136,6 +136,7 @@ module('Unit | Model | certification', function (hooks) { module('#statusLabelAndValue', function () { [ { value: assessmentStates.STARTED, label: 'Démarrée' }, + { value: assessmentResultStatus.PENDING_SCORING, label: 'En attente de scoring' }, { value: assessmentResultStatus.ERROR, label: 'En erreur' }, { value: assessmentResultStatus.VALIDATED, label: 'Validée' }, { value: assessmentResultStatus.REJECTED, label: 'Rejetée' }, diff --git a/admin/translations/en.json b/admin/translations/en.json index 0578eadaba1..00200ac913f 100644 --- a/admin/translations/en.json +++ b/admin/translations/en.json @@ -1691,6 +1691,7 @@ "cancelled_by_jury": "Cancelled", "error": "Error", "fraud": "Rejected for fraud", + "pending-scoring": "Pending scoring", "rejected": "Rejected", "validated": "Validated" }, diff --git a/admin/translations/fr.json b/admin/translations/fr.json index ee620ee438c..adc02dd17a1 100644 --- a/admin/translations/fr.json +++ b/admin/translations/fr.json @@ -1692,6 +1692,7 @@ "cancelled_by_jury": "Annulée", "error": "Erreur", "fraud": "Rejetée pour fraude", + "pending-scoring": "En attente de scoring", "rejected": "Rejetée", "validated": "Validée" }, From 5bb984d92d320f40bbcc7f06b4ef3f6b7ab15445 Mon Sep 17 00:00:00 2001 From: Lou Goubin <103123143+lgoubin@users.noreply.github.com> Date: Thu, 3 Sep 2026 14:21:42 +0200 Subject: [PATCH 11/11] refactor(admin): improve translation to include the scoring of the certifications --- admin/translations/en.json | 8 ++++---- admin/translations/fr.json | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/admin/translations/en.json b/admin/translations/en.json index 00200ac913f..2de3b3f162e 100644 --- a/admin/translations/en.json +++ b/admin/translations/en.json @@ -657,10 +657,10 @@ "save-error": "An error occurred while saving the scoring.", "save-scoring-button": "Save scoring", "save-scoring-modal": { - "content": "This action will save the scoring configuration on the active version.", + "content": "This action will save the scoring configuration to the active version and will trigger the scoring that has already been applied to that version. This action is irreversible, the scoring cannot be modified.", "title": "Confirm scoring save" }, - "success-notification": "The scoring has been successfully updated", + "success-notification": "The scoring has been successfully activated. Certifications already completed for this version will be scored.", "title": "Bounds of capacity by mesh" }, "title": "Creating a Certification Release" @@ -668,9 +668,9 @@ }, "deletion-modal": { "action-button": "Confirm deletion", - "content": "You are about to delete a certification version !!!!", + "content": "You are about to delete a certification version.", "error-message": "An error has occured", - "success-message": "That version has indeed been removed", + "success-message": "That version has indeed been removed.", "title": "Confirmation of version deletion" }, "labels": { diff --git a/admin/translations/fr.json b/admin/translations/fr.json index adc02dd17a1..87453aea7f4 100644 --- a/admin/translations/fr.json +++ b/admin/translations/fr.json @@ -663,10 +663,10 @@ "save-error": "Une erreur s'est produite lors de l'enregistrement du scoring.", "save-scoring-button": "Enregistrer le scoring", "save-scoring-modal": { - "content": "Cette action va enregistrer la configuration de scoring sur la version active.", + "content": "Cette action va enregistrer la configuration de scoring sur la version active et va déclencher le scoring déjà passée sur cette version. Cette action est irréversible, le scoring ne pourra pas être modifié.", "title": "Confirmation de l'enregistrement du scoring" }, - "success-notification": "Le scoring a bien été mis à jour", + "success-notification": "Le scoring a bien été activé. Les certifications déjà passée sur la version seront scorées.", "title": "Bornes de capacités par mailles" }, "title": "Création d'une version de certification" @@ -676,7 +676,7 @@ "action-button": "Confirmer la suppression", "content": "Vous vous apprêtez à supprimer une version.", "error-message": "Une erreur s'est produite", - "success-message": "La version à bien été supprimée", + "success-message": "La version à bien été supprimée.", "title": "Confirmation de suppression d'une version" }, "labels": {