Skip to content

Commit

Permalink
[TECH] Seuls les pilotes V3 pourront être pilotes séparation Pix/Pix+ (
Browse files Browse the repository at this point in the history
  • Loading branch information
pix-service-auto-merge committed Jul 12, 2024
2 parents 732c2c0 + d7408d0 commit 5f14b7f
Show file tree
Hide file tree
Showing 15 changed files with 162 additions and 94 deletions.
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,
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

0 comments on commit 5f14b7f

Please sign in to comment.