Skip to content

Commit

Permalink
[FEATURE] Supprimer les learners précédent l'ajout de l'import à form…
Browse files Browse the repository at this point in the history
  • Loading branch information
pix-service-auto-merge authored Dec 4, 2024
2 parents 3625dd9 + 93c2cc9 commit 1094427
Show file tree
Hide file tree
Showing 18 changed files with 358 additions and 95 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { HttpErrors } from '../../shared/application/http-errors.js';
import { DomainErrorMappingConfiguration } from '../../shared/application/models/domain-error-mapping-configuration.js';
import {
AlreadyExistingOrganizationFeatureError,
DpoEmailInvalid,
FeatureNotFound,
FeatureParamsNotProcessable,
Expand All @@ -16,10 +15,6 @@ const organizationalEntitiesDomainErrorMappingConfiguration = [
name: UnableToAttachChildOrganizationToParentOrganizationError.name,
httpErrorFn: (error) => new HttpErrors.ConflictError(error.message, error.code, error.meta),
},
{
name: AlreadyExistingOrganizationFeatureError.name,
httpErrorFn: (error) => new HttpErrors.ConflictError(error.message, error.code, error.meta),
},
{
name: OrganizationNotFound.name,
httpErrorFn: (error) => new HttpErrors.UnprocessableEntityError(error.message, error.code, error.meta),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ const attachChildOrganization = async function (request, h) {
};

const addOrganizationFeatureInBatch = async function (request, h) {
await usecases.addOrganizationFeatureInBatch({ filePath: request.payload.path });
await usecases.addOrganizationFeatureInBatch({
userId: request.auth.credentials.userId,
filePath: request.payload.path,
});
return h.response().code(204);
};

Expand Down
13 changes: 0 additions & 13 deletions api/src/organizational-entities/domain/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,6 @@ class UnableToAttachChildOrganizationToParentOrganizationError extends DomainErr
}
}

class AlreadyExistingOrganizationFeatureError extends DomainError {
constructor({
code = 'ALREADY_EXISTING_ORGANIZATION_FEATURE',
message = 'Unable to add feature to organization',
meta,
} = {}) {
super(message);
this.code = code;
this.meta = meta;
}
}

class DpoEmailInvalid extends DomainError {
constructor({ code = 'DPO_EMAIL_INVALID', message = 'DPO email invalid', meta } = {}) {
super(message);
Expand Down Expand Up @@ -72,7 +60,6 @@ class FeatureParamsNotProcessable extends DomainError {
}

export {
AlreadyExistingOrganizationFeatureError,
DpoEmailInvalid,
FeatureNotFound,
FeatureParamsNotProcessable,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
class OrganizationFeature {
constructor({ featureId, organizationId, params }) {
#deleteLearner;
constructor({ featureId, organizationId, params, deleteLearner }) {
this.featureId = parseInt(featureId, 10);
this.organizationId = parseInt(organizationId, 10);
this.params = params ? JSON.parse(params) : null;

this.#deleteLearner = deleteLearner === 'Y';
}

get deleteLearner() {
return this.#deleteLearner;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { createReadStream } from 'node:fs';

import { CsvColumn } from '../../../../lib/infrastructure/serializers/csv/csv-column.js';
import { getDataBuffer } from '../../../prescription/learner-management/infrastructure/utils/bufferize/get-data-buffer.js';
import { withTransaction } from '../../../shared/domain/DomainTransaction.js';
import { CsvParser } from '../../../shared/infrastructure/serializers/csv/csv-parser.js';
import { FeatureParamsNotProcessable } from '../errors.js';
import { OrganizationFeature } from '../models/OrganizationFeature.js';
Expand All @@ -27,31 +28,43 @@ const organizationFeatureCsvHeader = {
name: 'Params',
isRequired: false,
}),
new CsvColumn({
property: 'deleteLearner',
name: 'Delete Learner',
isRequired: false,
}),
],
};

/**
* @param {Object} params - A parameter object.
* @param {string} params.featureId - feature id to add.
* @param {string} params.filePath - path of csv file wich contains organizations and params.
* @param {OrganizationFeatureRepository} params.organizationFeatureRepository - organizationRepository to use.
* @param {Object} params.dependencies
* @returns {Promise<void>}
*/
async function addOrganizationFeatureInBatch({ filePath, organizationFeatureRepository }) {
const stream = createReadStream(filePath);
const buffer = await getDataBuffer(stream);
export const addOrganizationFeatureInBatch = withTransaction(
/**
* @param {Object} params - A parameter object.
* @param {Number} params.userId - user connected performing the action
* @param {string} params.filePath - path of csv file wich contains organizations and params.
* @param {OrganizationFeatureRepository} params.organizationFeatureRepository - organizationRepository to use.
* @param {Object} params.dependencies
* @returns {Promise<void>}
*/
async ({ userId, filePath, organizationFeatureRepository, learnersApi }) => {
const stream = createReadStream(filePath);
const buffer = await getDataBuffer(stream);

const csvParser = new CsvParser(buffer, organizationFeatureCsvHeader);
const csvData = csvParser.parse();
const data = csvData.map(({ featureId, organizationId, params, deleteLearner }) => {
try {
return new OrganizationFeature({ featureId, organizationId, params, deleteLearner });
} catch (err) {
throw new FeatureParamsNotProcessable();
}
});

const csvParser = new CsvParser(buffer, organizationFeatureCsvHeader);
const csvData = csvParser.parse();
const data = csvData.map(({ featureId, organizationId, params }) => {
try {
return new OrganizationFeature({ featureId, organizationId, params: params });
} catch (err) {
throw new FeatureParamsNotProcessable();
}
});
return organizationFeatureRepository.saveInBatch(data);
}
data.forEach(async ({ organizationId, deleteLearner }) => {
if (deleteLearner) {
await learnersApi.deleteOrganizationLearnerBeforeImportFeature({ userId, organizationId });
}
});

export { addOrganizationFeatureInBatch };
return organizationFeatureRepository.saveInBatch(data);
},
);
4 changes: 3 additions & 1 deletion api/src/organizational-entities/domain/usecases/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

import * as organizationTagRepository from '../../../../lib/infrastructure/repositories/organization-tag-repository.js';
import * as learnersApi from '../../../prescription/learner-management/application/api/learners-api.js';
import * as schoolRepository from '../../../school/infrastructure/repositories/school-repository.js';
import { injectDependencies } from '../../../shared/infrastructure/utils/dependency-injection.js';
import { importNamedExportsFromDirectory } from '../../../shared/infrastructure/utils/import-named-exports-from-directory.js';
Expand All @@ -12,10 +13,10 @@ import * as dataProtectionOfficerRepository from '../../infrastructure/repositor
import * as organizationFeatureRepository from '../../infrastructure/repositories/organization-feature-repository.js';
import { organizationForAdminRepository } from '../../infrastructure/repositories/organization-for-admin.repository.js';
import { tagRepository } from '../../infrastructure/repositories/tag.repository.js';

const path = dirname(fileURLToPath(import.meta.url));

/**
* @typedef {import ('../../../prescription/learner-management/application/api/learners-api.js')} learnersApi
* @typedef {import ('../../infrastructure/repositories/certification-center.repository.js')} CertificationCenterRepository
* @typedef {import ('../../infrastructure/repositories/certification-center-for-admin-repository.js')} CertificationCenterForAdminRepository
* @typedef {import ('../../infrastructure/repositories/complementary-certification-habilitation-repository.js')} ComplementaryCertificationHabilitationRepository
Expand All @@ -34,6 +35,7 @@ const repositories = {
organizationForAdminRepository,
organizationFeatureRepository,
schoolRepository,
learnersApi,
organizationTagRepository,
tagRepository,
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/**
* @module OrganizationFeatureRepository
*/
import { knex } from '../../../../db/knex-database-connection.js';
import * as knexUtils from '../../../../src/shared/infrastructure/utils/knex-utils.js';
import { AlreadyExistingOrganizationFeatureError, FeatureNotFound, OrganizationNotFound } from '../../domain/errors.js';
import { DomainTransaction } from '../../../shared/domain/DomainTransaction.js';
import { FeatureNotFound, OrganizationNotFound } from '../../domain/errors.js';
import { OrganizationFeatureItem } from '../../domain/models/OrganizationFeatureItem.js';

/**
Expand All @@ -13,20 +13,18 @@ import { OrganizationFeatureItem } from '../../domain/models/OrganizationFeature
* @typedef {import('../../domain/models/OrganizationFeatureItem.js').OrganizationFeatureItem} OrganizationFeatureItem
*/

const DEFAULT_BATCH_SIZE = 100;

/**
**
* @param {OrganizationFeature[]} organizations
*/
async function saveInBatch(organizationFeatures, batchSize = DEFAULT_BATCH_SIZE) {
async function saveInBatch(organizationFeatures) {
try {
await knex.batchInsert('organization-features', organizationFeatures, batchSize);
const knexConn = DomainTransaction.getConnection();
await knexConn('organization-features')
.insert(organizationFeatures)
.onConflict(['featureId', 'organizationId'])
.ignore();
} catch (err) {
if (knexUtils.isUniqConstraintViolated(err)) {
throw new AlreadyExistingOrganizationFeatureError();
}

if (knexUtils.foreignKeyConstraintViolated(err) && err.constraint.includes('featureid')) {
throw new FeatureNotFound();
}
Expand All @@ -51,7 +49,8 @@ async function saveInBatch(organizationFeatures, batchSize = DEFAULT_BATCH_SIZE)
* @returns {Promise<OrganizationFeatureItem>}
*/
async function findAllOrganizationFeaturesFromOrganizationId({ organizationId }) {
const organizationFeatures = await knex
const knexConn = DomainTransaction.getConnection();
const organizationFeatures = await knexConn
.select('key', 'params')
.from('organization-features')
.join('features', 'features.id', 'organization-features.featureId')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { withTransaction } from '../../../../shared/domain/DomainTransaction.js';
import { usecases } from '../../domain/usecases/index.js';

/**
Expand All @@ -17,3 +18,26 @@ export const hasBeenLearner = async ({ userId }) => {

return isLearner;
};

/**
* delete organization learner before adding import feature
*
* @param {object} params
* @param {number} params.userId - The ID of the user wich request the action
* @param {number} params.organizationId - The ID of the organizationId to find learner to delete
* @returns {Promise<void>}
* @throws TypeError - Throw when params.userId or params.organizationId is not defined
*/
export const deleteOrganizationLearnerBeforeImportFeature = withTransaction(async ({ userId, organizationId }) => {
if (!userId) {
throw new TypeError('userId is required');
}

if (!organizationId) {
throw new TypeError('organizationId is required');
}

const organizationLearnerIds = await usecases.findOrganizationLearnersBeforeImportFeature({ organizationId });

return usecases.deleteOrganizationLearners({ userId, organizationId, organizationLearnerIds });
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* @typedef {import('./index.js').OrganizationLearnerRepository} OrganizationLearnerRepository
*/

/**
* @param{number} organizationId
* @param{OrganizationLearnerRepository} organizationLearnerRepository
* @returns {Promise<number[]>}
*/
const findOrganizationLearnersBeforeImportFeature = async function ({ organizationId, organizationLearnerRepository }) {
return organizationLearnerRepository.findOrganizationLearnerIdsBeforeImportFeatureFromOrganizationId({
organizationId,
});
};

export { findOrganizationLearnersBeforeImportFeature };
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
UserCouldNotBeReconciledError,
} from '../../../../shared/domain/errors.js';
import { OrganizationLearner } from '../../../../shared/domain/models/index.js';
import { ApplicationTransaction } from '../../../shared/infrastructure/ApplicationTransaction.js';
import { CommonOrganizationLearner } from '../../domain/models/CommonOrganizationLearner.js';
import { OrganizationLearnerForAdmin } from '../../domain/read-models/OrganizationLearnerForAdmin.js';
import * as studentRepository from './student-repository.js';
Expand Down Expand Up @@ -163,7 +162,7 @@ function _shouldStudentToImportBeReconciled(
}

const saveCommonOrganizationLearners = function (learners) {
const knex = ApplicationTransaction.getConnection();
const knex = DomainTransaction.getConnection();

return Promise.all(
learners.map((learner) => {
Expand All @@ -182,7 +181,7 @@ const disableCommonOrganizationLearnersFromOrganizationId = function ({
organizationId,
excludeOrganizationLearnerIds = [],
}) {
const knex = ApplicationTransaction.getConnection();
const knex = DomainTransaction.getConnection();
return knex('organization-learners')
.where({ organizationId, isDisabled: false })
.whereNull('deletedAt')
Expand All @@ -191,7 +190,7 @@ const disableCommonOrganizationLearnersFromOrganizationId = function ({
};

const findAllCommonLearnersFromOrganizationId = async function ({ organizationId }) {
const knex = ApplicationTransaction.getConnection();
const knex = DomainTransaction.getConnection();

const existingLearners = await knex('view-active-organization-learners')
.select(['firstName', 'id', 'lastName', 'userId', 'organizationId', 'attributes'])
Expand All @@ -215,7 +214,7 @@ const findAllCommonOrganizationLearnerByReconciliationInfos = async function ({
organizationId,
reconciliationInformations,
}) {
const knex = ApplicationTransaction.getConnection();
const knex = DomainTransaction.getConnection();

const query = knex('view-active-organization-learners')
.select('firstName', 'lastName', 'id', 'attributes', 'userId')
Expand All @@ -234,7 +233,7 @@ const findAllCommonOrganizationLearnerByReconciliationInfos = async function ({
};

const update = async function (organizationLearner) {
const knex = ApplicationTransaction.getConnection();
const knex = DomainTransaction.getConnection();

const { id, ...attributes } = organizationLearner;
const updatedRows = await knex('organization-learners').update(attributes).where({ id });
Expand Down Expand Up @@ -303,6 +302,18 @@ const reconcileUserToOrganizationLearner = async function ({ userId, organizatio
}
};

/**
* @function
* @name findOrganizationLearnerIdsBeforeImportFeatureFromOrganizationId
* @param {Object} params
* @param {number} params.organizationId
* @returns {Promise<number[]>}
*/
const findOrganizationLearnerIdsBeforeImportFeatureFromOrganizationId = async function ({ organizationId }) {
const knexConn = DomainTransaction.getConnection();
return knexConn('view-active-organization-learners').where({ organizationId }).whereNull('attributes').pluck('id');
};

export {
addOrUpdateOrganizationOfOrganizationLearners,
countByUserId,
Expand All @@ -312,6 +323,7 @@ export {
findAllCommonLearnersFromOrganizationId,
findAllCommonOrganizationLearnerByReconciliationInfos,
findByUserId,
findOrganizationLearnerIdsBeforeImportFeatureFromOrganizationId,
findOrganizationLearnerIdsByOrganizationId,
getOrganizationLearnerForAdmin,
reconcileUserByNationalStudentIdAndOrganizationId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { organizationAdminController } from '../../../../../src/organizational-entities/application/organization/organization.admin.controller.js';
import * as organizationAdminRoutes from '../../../../../src/organizational-entities/application/organization/organization.admin.route.js';
import {
AlreadyExistingOrganizationFeatureError,
DpoEmailInvalid,
FeatureNotFound,
FeatureParamsNotProcessable,
Expand Down Expand Up @@ -93,20 +92,6 @@ describe('Integration | Organizational Entities | Application | Route | Admin |
expect(response.statusCode).to.equal(422);
});
});

context('when trying to add already existing feature on organization', function () {
it('returns a 409 HTTP status code', async function () {
organizationAdminController.addOrganizationFeatureInBatch.rejects(
new AlreadyExistingOrganizationFeatureError(),
);

// when
const response = await httpTestServer.request(method, url, payload);

// then
expect(response.statusCode).to.equal(409);
});
});
});

describe('POST /api/admin/organizations/update-organizations', function () {
Expand Down
Loading

0 comments on commit 1094427

Please sign in to comment.