Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TECH] Seuls les pilotes V3 pourront être pilotes séparation Pix/Pix+ (PIX-13171). #9395

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ export default class AuthenticatedCertificationCentersGetController extends Cont
await this.model.certificationCenter.save();
this.notifications.success('Centre de certification mis à jour avec succès.');
} catch (error) {
if (get(error, 'errors[0].code') === 'V3_PILOT_NOT_AUTHORIZED') {
if (get(error, 'errors[0].code') === 'PILOT_FEATURES_CONFLICT') {
return this.notifications.error(
this.intl.t('pages.certification-centers.notifications.update.errors.update-to-v3-pilot'),
this.intl.t('pages.certification-centers.notifications.update.errors.pilot-features-incompatibilities'),
);
}
this.notifications.error("Une erreur est survenue, le centre de certification n'a pas été mis à jour.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ module('Acceptance | authenticated/certification-centers/get', function (hooks)
});
this.server.patch(
`/admin/certification-centers/${certificationCenter.id}`,
{ errors: [{ code: 'V3_PILOT_NOT_AUTHORIZED' }] },
{ errors: [{ code: 'PILOT_FEATURES_CONFLICT' }] },
403,
);
const screen = await visit(`/certification-centers/${certificationCenter.id}`);
Expand All @@ -222,7 +222,7 @@ module('Acceptance | authenticated/certification-centers/get', function (hooks)
assert
.dom(
screen.getByText(
'Ce centre de certification est incompatible avec le pilote certification v3 car il est déjà pilote pour la séparation Pix/Pix+',
'Il y a une incompatibilité entre les fonctionnalités pilotes auxquelles vous souhaitez habiliter le centre. Merci de rafraîchir la page.',
),
)
.exists();
Expand Down
2 changes: 1 addition & 1 deletion admin/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@
},
"update": {
"errors": {
"update-to-v3-pilot": "Ce centre de certification est incompatible avec le pilote certification v3 car il est déjà pilote pour la séparation Pix/Pix+"
"pilot-features-incompatibilities": "There are incompatibilities between the pilot features this center is habilitated to. Please refresh this page."
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion admin/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@
},
"update": {
"errors": {
"update-to-v3-pilot": "Ce centre de certification est incompatible avec le pilote certification v3 car il est déjà pilote pour la séparation Pix/Pix+"
"pilot-features-incompatibilities": "Il y a une incompatibilité entre les fonctionnalités pilotes auxquelles vous souhaitez habiliter le centre. Merci de rafraîchir la page."
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions api/lib/domain/usecases/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@ function requirePoleEmploiNotifier() {
* @typedef {sessionPublicationService} SessionPublicationService
* @typedef {sessionRepository} SessionRepository
* @typedef {centerRepository} CenterRepository
* @typedef {certificationCenterForAdminRepository} CertificationCenterForAdminRepository
* @typedef {complementaryCertificationHabilitationRepository} ComplementaryCertificationHabilitationRepository
* @typedef {dataProtectionOfficerRepository} DataProtectionOfficerRepository
*/

const dependencies = {
Expand Down
145 changes: 105 additions & 40 deletions api/lib/domain/usecases/update-certification-center.js
Original file line number Diff line number Diff line change
@@ -1,57 +1,112 @@
/**
* @typedef {import('./index.js').CenterRepository} CenterRepository
* @typedef {import('./index.js').CertificationCenterForAdminRepository} CertificationCenterForAdminRepository
* @typedef {import('./index.js').ComplementaryCertificationHabilitationRepository} ComplementaryCertificationHabilitationRepository
* @typedef {import('./index.js').DataProtectionOfficerRepository} DataProtectionOfficerRepository
*/
import bluebird from 'bluebird';

import { CenterForAdminFactory } from '../../../src/certification/enrolment/domain/models/factories/CenterForAdminFactory.js';
import { V3PilotNotAuthorizedForCertificationCenterError } from '../../../src/shared/domain/errors.js';
import { CertificationCenterPilotFeaturesConflictError } from '../../../src/shared/domain/errors.js';
import { withTransaction } from '../../infrastructure/DomainTransaction.js';
import { ComplementaryCertificationHabilitation, DataProtectionOfficer } from '../models/index.js';
import * as certificationCenterCreationValidator from '../validators/certification-center-creation-validator.js';

async function _addOrUpdateDataProtectionOfficer({
certificationCenterId,
certificationCenterInformation,
dataProtectionOfficerRepository,
}) {
const dataProtectionOfficer = new DataProtectionOfficer({
firstName: certificationCenterInformation.dataProtectionOfficerFirstName ?? '',
lastName: certificationCenterInformation.dataProtectionOfficerLastName ?? '',
email: certificationCenterInformation.dataProtectionOfficerEmail ?? '',
certificationCenterId,
});

const dataProtectionOfficerFound = await dataProtectionOfficerRepository.get({
certificationCenterId,
});

if (dataProtectionOfficerFound) return dataProtectionOfficerRepository.update(dataProtectionOfficer);

return dataProtectionOfficerRepository.create(dataProtectionOfficer);
}

/**
* @param {Object} params
* @param {number} params.certificationCenterId
* @param {Object} params.certificationCenterInformation - see deserializer
* @param {Array<number>} params.complementaryCertificationIds
* @param {CenterRepository} params.centerRepository
* @param {CertificationCenterForAdminRepository} params.certificationCenterForAdminRepository
* @param {ComplementaryCertificationHabilitationRepository} params.ComplementaryCertificationHabilitationRepository
* @param {DataProtectionOfficerRepository} params.dataProtectionOfficerRepository
*/
const updateCertificationCenter = async function ({
certificationCenterId,
certificationCenterInformation,
complementaryCertificationIds,
centerRepository,
certificationCenterForAdminRepository,
complementaryCertificationHabilitationRepository,
dataProtectionOfficerRepository,
centerRepository,
}) {
certificationCenterCreationValidator.validate(certificationCenterInformation);

const certificationCenter = await centerRepository.getById({
id: certificationCenterId,
await _updateCenter({
certificationCenterId,
certificationCenterInformation,
complementaryCertificationIds,
centerRepository,
certificationCenterForAdminRepository,
complementaryCertificationHabilitationRepository,
});

if (certificationCenterInformation.isV3Pilot && certificationCenter.isComplementaryAlonePilot) {
throw new V3PilotNotAuthorizedForCertificationCenterError();
const dataProtectionOfficer = await _addOrUpdateDataProtectionOfficer({
certificationCenterId,
certificationCenterInformation,
dataProtectionOfficerRepository,
});

const updatedCertificationCenter = await centerRepository.getById({ id: certificationCenterId });
return CenterForAdminFactory.fromCenterAndDataProtectionOfficer({
center: updatedCertificationCenter,
dataProtectionOfficer,
});
};

export { updateCertificationCenter };

const _updateCenter = withTransaction(
/**
* @param {Object} params
* @param {CenterRepository} params.centerRepository
* @param {CertificationCenterForAdminRepository} params.certificationCenterForAdminRepository
* @param {ComplementaryCertificationHabilitationRepository} params.complementaryCertificationHabilitationRepository
*/
async ({
certificationCenterInformation,
certificationCenterId,
complementaryCertificationIds,
Steph0 marked this conversation as resolved.
Show resolved Hide resolved
centerRepository,
certificationCenterForAdminRepository,
complementaryCertificationHabilitationRepository,
}) => {
const certificationCenter = await centerRepository.getById({
id: certificationCenterId,
});

_verifyCenterPilotFeaturesCompatibility({
currentCenter: certificationCenter,
newCenterData: certificationCenterInformation,
});

await certificationCenterForAdminRepository.update(certificationCenterInformation);

await _updateHabilitations({
certificationCenterId,
complementaryCertificationIds,
complementaryCertificationHabilitationRepository,
});
},
{ isolationLevel: 'repeatable read' },
);

const _verifyCenterPilotFeaturesCompatibility = ({ currentCenter, newCenterData }) => {
if (currentCenter.isComplementaryAlonePilot && !newCenterData.isV3Pilot) {
throw new CertificationCenterPilotFeaturesConflictError();
}
};

/**
* @param {Object} params
* @param {ComplementaryCertificationHabilitationRepository} params.complementaryCertificationHabilitationRepository
*/
const _updateHabilitations = async ({
certificationCenterId,
complementaryCertificationIds,
complementaryCertificationHabilitationRepository,
}) => {
await complementaryCertificationHabilitationRepository.deleteByCertificationCenterId(certificationCenterId);

if (complementaryCertificationIds) {
Expand All @@ -63,20 +118,30 @@ const updateCertificationCenter = async function ({
return complementaryCertificationHabilitationRepository.save(complementaryCertificationHabilitation);
});
}
};

await certificationCenterForAdminRepository.update(certificationCenterInformation);
const updatedCertificationCenter = await centerRepository.getById({ id: certificationCenterId });
const _addOrUpdateDataProtectionOfficer = withTransaction(
/**
* @param {Object} params
* @param {DataProtectionOfficerRepository} params.dataProtectionOfficerRepository
*/
async ({ certificationCenterId, certificationCenterInformation, dataProtectionOfficerRepository }) => {
const dataProtectionOfficer = new DataProtectionOfficer({
firstName: certificationCenterInformation.dataProtectionOfficerFirstName ?? '',
lastName: certificationCenterInformation.dataProtectionOfficerLastName ?? '',
email: certificationCenterInformation.dataProtectionOfficerEmail ?? '',
certificationCenterId,
});

const dataProtectionOfficer = await _addOrUpdateDataProtectionOfficer({
certificationCenterId,
certificationCenterInformation,
dataProtectionOfficerRepository,
});
const dataProtectionOfficerFound = await dataProtectionOfficerRepository.get({
certificationCenterId,
});

return CenterForAdminFactory.fromCenterAndDataProtectionOfficer({
center: updatedCertificationCenter,
dataProtectionOfficer,
});
};
if (dataProtectionOfficerFound) {
return dataProtectionOfficerRepository.update(dataProtectionOfficer);
}

export { updateCertificationCenter };
return dataProtectionOfficerRepository.create(dataProtectionOfficer);
},
{ isolationLevel: 'repeatable read' },
);
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import _ from 'lodash';

import { knex } from '../../../db/knex-database-connection.js';
import { CenterForAdmin } from '../../../src/certification/enrolment/domain/models/CenterForAdmin.js';
import { DomainTransaction } from '../DomainTransaction.js';

const save = async function (certificationCenter) {
const knexConn = DomainTransaction.getConnection();
const data = _toDTO(certificationCenter);
const [certificationCenterCreated] = await knex('certification-centers').returning('*').insert(data);
const [certificationCenterCreated] = await knexConn('certification-centers').returning('*').insert(data);
return _toDomain(certificationCenterCreated);
};

const update = async function (certificationCenter) {
const knexConn = DomainTransaction.getConnection();
const data = _toDTO(certificationCenter);
return knex('certification-centers').update(data).where({ id: certificationCenter.id });
return knexConn('certification-centers').update(data).where({ id: certificationCenter.id });
};

export { save, update };
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { knex } from '../../../db/knex-database-connection.js';
import { DomainTransaction } from '../DomainTransaction.js';

const COMPLEMENTARY_CERTIFICATION_HABILITATIONS_TABLE_NAME = 'complementary-certification-habilitations';

const save = async function (complementaryCertification) {
const knexConn = DomainTransaction.getConnection();
const columnsToSave = {
complementaryCertificationId: complementaryCertification.complementaryCertificationId,
certificationCenterId: complementaryCertification.certificationCenterId,
};
return knex(COMPLEMENTARY_CERTIFICATION_HABILITATIONS_TABLE_NAME).insert(columnsToSave);
return knexConn(COMPLEMENTARY_CERTIFICATION_HABILITATIONS_TABLE_NAME).insert(columnsToSave);
};

const deleteByCertificationCenterId = async function (certificationCenterId) {
return knex(COMPLEMENTARY_CERTIFICATION_HABILITATIONS_TABLE_NAME).delete().where({ certificationCenterId });
const knexConn = DomainTransaction.getConnection();
return knexConn(COMPLEMENTARY_CERTIFICATION_HABILITATIONS_TABLE_NAME).delete().where({ certificationCenterId });
};

export { deleteByCertificationCenterId, save };
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ async function batchAddDataProtectionOfficerToOrganization(
}

async function get({ organizationId = null, certificationCenterId = null }) {
const [dataProtectionOfficerRow] = await knex(DATA_PROTECTION_OFFICERS_TABLE_NAME)
const knexConn = DomainTransaction.getConnection();
const [dataProtectionOfficerRow] = await knexConn(DATA_PROTECTION_OFFICERS_TABLE_NAME)
.where({ organizationId, certificationCenterId })
.returning('*');

Expand Down Expand Up @@ -43,16 +44,16 @@ async function create(dataProtectionOfficer, { knexTransaction } = DomainTransac
async function update(dataProtectionOfficer) {
const { firstName, lastName, email, organizationId, certificationCenterId } = dataProtectionOfficer;
const updatedAt = new Date();
const knexConn = DomainTransaction.getConnection();

const query = knex(DATA_PROTECTION_OFFICERS_TABLE_NAME).update({
const query = knexConn(DATA_PROTECTION_OFFICERS_TABLE_NAME).update({
firstName,
lastName,
email,
updatedAt,
});

if (organizationId) query.where({ organizationId });
else query.where({ certificationCenterId });
organizationId ? query.where({ organizationId }) : query.where({ certificationCenterId });

const [dataProtectionOfficerRow] = await query.returning('*');

Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
import { knex } from '../../../../../db/knex-database-connection.js';
import { DomainTransaction } from '../../../../shared/domain/DomainTransaction.js';
import { NotFoundError } from '../../../../shared/domain/errors.js';
import { CERTIFICATION_FEATURES } from '../../../shared/domain/constants.js';
import { Center } from '../../domain/models/Center.js';
import { Habilitation } from '../../domain/models/Habilitation.js';

const getById = async ({ id }) => {
const center = await knex
const knexConn = DomainTransaction.getConnection();
const center = await knexConn
.select({
id: 'certification-centers.id',
name: 'certification-centers.name',
type: 'certification-centers.type',
externalId: 'certification-centers.externalId',
habilitations: knex.raw(
habilitations: knexConn.raw(
`json_agg(json_build_object(
'complementaryCertificationId', "complementary-certification-habilitations"."complementaryCertificationId",
'key', "complementary-certifications"."key",
'label', "complementary-certifications"."label"
))`,
),
features: knex.raw('array_remove(array_agg(DISTINCT "certificationCenterFeatures"."key"), NULL)'),
features: knexConn.raw('array_remove(array_agg(DISTINCT "certificationCenterFeatures"."key"), NULL)'),
createdAt: 'certification-centers.createdAt',
updatedAt: 'certification-centers.updatedAt',
isV3Pilot: 'certification-centers.isV3Pilot',
Expand Down
4 changes: 2 additions & 2 deletions api/src/shared/application/error-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import { extractLocaleFromRequest } from '../../shared/infrastructure/utils/requ
import * as DomainErrors from '../domain/errors.js';
import {
AutonomousCourseRequiresATargetProfileWithSimplifiedAccessError,
CertificationCenterPilotFeaturesConflictError,
EntityValidationError,
OidcError,
TargetProfileRequiresToBeLinkedToAutonomousCourseOrganization,
V3PilotNotAuthorizedForCertificationCenterError,
} from '../domain/errors.js';
import * as errorSerializer from '../infrastructure/serializers/jsonapi/error-serializer.js';
import { domainErrorMapper } from './domain-error-mapper.js';
Expand Down Expand Up @@ -166,7 +166,7 @@ function _mapToHttpError(error) {
return new HttpErrors.ForbiddenError(error.message, error.code);
}

if (error instanceof V3PilotNotAuthorizedForCertificationCenterError) {
if (error instanceof CertificationCenterPilotFeaturesConflictError) {
return new HttpErrors.ForbiddenError(error.message, error.code);
}

Expand Down
9 changes: 3 additions & 6 deletions api/src/shared/domain/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,11 +289,8 @@ class UserNotAuthorizedToUpdatePasswordError extends DomainError {
}
}

class V3PilotNotAuthorizedForCertificationCenterError extends DomainError {
constructor(
message = 'Certification center is not authorized to switch to a V3 pilot.',
code = 'V3_PILOT_NOT_AUTHORIZED',
) {
class CertificationCenterPilotFeaturesConflictError extends DomainError {
constructor(message = 'Certification center pilot features incompatibility', code = 'PILOT_FEATURES_CONFLICT') {
super(message);
this.code = code;
}
Expand All @@ -305,6 +302,7 @@ export {
AssessmentResultNotCreatedError,
AutonomousCourseRequiresATargetProfileWithSimplifiedAccessError,
CertificationAttestationGenerationError,
CertificationCenterPilotFeaturesConflictError,
CsvImportError,
DomainError,
EntityValidationError,
Expand All @@ -327,5 +325,4 @@ export {
TargetProfileRequiresToBeLinkedToAutonomousCourseOrganization,
UserNotAuthorizedToAccessEntityError,
UserNotAuthorizedToUpdatePasswordError,
V3PilotNotAuthorizedForCertificationCenterError,
};
Loading