diff --git a/api/db/seeds/data/team-acces/build-organizations.js b/api/db/seeds/data/team-acces/build-organizations.js new file mode 100644 index 00000000000..8320f5b85f4 --- /dev/null +++ b/api/db/seeds/data/team-acces/build-organizations.js @@ -0,0 +1,24 @@ +export function buildOrganizations(databaseBuilder) { + _buildOrganizationWithoutAdmins(databaseBuilder); +} + +function _buildOrganizationWithoutAdmins(databaseBuilder) { + const organization = databaseBuilder.factory.buildOrganization({ + type: 'PRO', + name: 'Accis', + }); + + const tag1 = databaseBuilder.factory.buildTag({ name: 'tag1' }); + const tag2 = databaseBuilder.factory.buildTag({ name: 'tag2' }); + databaseBuilder.factory.buildDataProtectionOfficer.withOrganizationId({ + firstName: 'justin', + lastName: 'instant', + email: 'justin-instant@example.net', + organizationId: organization.id, + }); + databaseBuilder.factory.buildOrganizationTag({ + organizationId: organization.id, + tagId: tag1.id, + }); + databaseBuilder.factory.buildOrganizationTag({ organizationId: organization.id, tagId: tag2.id }); +} diff --git a/api/db/seeds/data/team-acces/data-builder.js b/api/db/seeds/data/team-acces/data-builder.js index b81b15a0716..677eb050574 100644 --- a/api/db/seeds/data/team-acces/data-builder.js +++ b/api/db/seeds/data/team-acces/data-builder.js @@ -3,6 +3,7 @@ import { buildBlockedUsers } from './build-blocked-users.js'; import { buildCertificationCenters } from './build-certification-centers.js'; import { buildOidcProviders } from './build-oidc-providers.js'; import { buildOrganizationUsers } from './build-organization-users.js'; +import { buildOrganizations } from './build-organizations.js'; import { buildPixAdminRoles } from './build-pix-admin-roles.js'; import { buildResetPasswordUsers } from './build-reset-password-users.js'; import { buildScoOrganizationLearners } from './build-sco-organization-learners.js'; @@ -20,6 +21,7 @@ async function teamAccesDataBuilder(databaseBuilder) { buildScoOrganizationLearners(databaseBuilder); await buildCertificationCenters(databaseBuilder); await buildOidcProviders(databaseBuilder); + await buildOrganizations(databaseBuilder); } export { teamAccesDataBuilder }; diff --git a/api/src/organizational-entities/infrastructure/repositories/data-protection-officer.repository.js b/api/src/organizational-entities/infrastructure/repositories/data-protection-officer.repository.js index 48145d3ffc5..a0cbee74e3b 100644 --- a/api/src/organizational-entities/infrastructure/repositories/data-protection-officer.repository.js +++ b/api/src/organizational-entities/infrastructure/repositories/data-protection-officer.repository.js @@ -35,6 +35,11 @@ async function create(dataProtectionOfficer) { return new DataProtectionOfficer(dataProtectionOfficerRow); } +async function deleteDpoByOrganizationId(organizationId) { + const knexConn = DomainTransaction.getConnection(); + await knexConn(DATA_PROTECTION_OFFICERS_TABLE_NAME).where({ organizationId }).delete(); +} + async function update(dataProtectionOfficer) { const knexConn = DomainTransaction.getConnection(); const { firstName, lastName, email, organizationId, certificationCenterId } = dataProtectionOfficer; @@ -54,4 +59,4 @@ async function update(dataProtectionOfficer) { return new DataProtectionOfficer(dataProtectionOfficerRow); } -export { batchAddDataProtectionOfficerToOrganization, create, get, update }; +export { batchAddDataProtectionOfficerToOrganization, create, deleteDpoByOrganizationId, get, update }; diff --git a/api/src/organizational-entities/infrastructure/repositories/organization-for-admin.repository.js b/api/src/organizational-entities/infrastructure/repositories/organization-for-admin.repository.js index 9002edfd6df..3fcb038ff4d 100644 --- a/api/src/organizational-entities/infrastructure/repositories/organization-for-admin.repository.js +++ b/api/src/organizational-entities/infrastructure/repositories/organization-for-admin.repository.js @@ -217,14 +217,14 @@ const update = async function (organization) { }; /** - * @typedef {Object} OrganizationForAdminRepository - * @property {archive} archive - * @property {exist} exist - * @property {findChildrenByParentOrganizationId} findChildrenByParentOrganizationId - * @property {get} get - * @property {save} save - * @property {update} update + * @type {function} + * @param organizationId + * @return {Promise} */ +const deleteById = async function (organizationId) { + const knexConn = DomainTransaction.getConnection(); + await knexConn(ORGANIZATIONS_TABLE_NAME).where({ id: organizationId }).delete(); +}; async function _addOrUpdateDataProtectionOfficer(knexConn, dataProtectionOfficer) { await knexConn(DATA_PROTECTION_OFFICERS_TABLE_NAME) @@ -320,4 +320,12 @@ function _toDomain(rawOrganization) { return organization; } -export const organizationForAdminRepository = { archive, exist, findChildrenByParentOrganizationId, get, save, update }; +export const organizationForAdminRepository = { + archive, + exist, + findChildrenByParentOrganizationId, + get, + save, + update, + deleteById, +}; diff --git a/api/src/organizational-entities/infrastructure/repositories/organization-tag.repository.js b/api/src/organizational-entities/infrastructure/repositories/organization-tag.repository.js index be4ad54798a..0b3407cb31b 100644 --- a/api/src/organizational-entities/infrastructure/repositories/organization-tag.repository.js +++ b/api/src/organizational-entities/infrastructure/repositories/organization-tag.repository.js @@ -52,4 +52,9 @@ const getRecentlyUsedTags = async function ({ tagId, numberOfRecentTags }) { return tags.map(({ tagId: id, name }) => new Tag({ id, name })); }; -export { batchCreate, create, getRecentlyUsedTags, isExistingByOrganizationIdAndTagId }; +const deleteTagsByOrganizationId = async function (organizationId) { + const knexConn = DomainTransaction.getConnection(); + await knexConn('organization-tags').where({ organizationId }).delete(); +}; + +export { batchCreate, create, deleteTagsByOrganizationId, getRecentlyUsedTags, isExistingByOrganizationIdAndTagId }; diff --git a/api/src/organizational-entities/scripts/delete-organizations-script.js b/api/src/organizational-entities/scripts/delete-organizations-script.js new file mode 100644 index 00000000000..d651e311380 --- /dev/null +++ b/api/src/organizational-entities/scripts/delete-organizations-script.js @@ -0,0 +1,64 @@ +import Joi from 'joi'; + +import { csvFileParser } from '../../shared/application/scripts/parsers.js'; +import { Script } from '../../shared/application/scripts/script.js'; +import { ScriptRunner } from '../../shared/application/scripts/script-runner.js'; +import * as dataProtectionOfficerRepository from '../infrastructure/repositories/data-protection-officer.repository.js'; +import { organizationForAdminRepository } from '../infrastructure/repositories/organization-for-admin.repository.js'; +import * as organizationTagRepository from '../infrastructure/repositories/organization-tag.repository.js'; + +const columnsSchema = [{ name: 'Organization ID', schema: Joi.number().required() }]; + +export class DeleteOrganizationsScript extends Script { + constructor() { + super({ + description: 'Delete all organizations and associated tags', + permanent: false, + options: { + file: { + type: 'string', + describe: 'File path to CSV file with organizations to delete', + demandOption: true, + requiresArg: true, + coerce: csvFileParser(columnsSchema), + }, + dryRun: { + type: 'boolean', + describe: 'Run the script without actually deleting anything', + default: false, + }, + }, + }); + } + + async handle({ + options, + logger, + dependencies = { organizationForAdminRepository, organizationTagRepository, dataProtectionOfficerRepository }, + }) { + const { file, dryRun } = options; + + let count = 0; + for (const row of file) { + const organizationId = row['Organization ID']; + if (!dryRun) { + logger.info(organizationId); + // Delete data protection officer via data-protection-officer.repository + await dependencies.dataProtectionOfficerRepository.deleteDpoByOrganizationId(organizationId); + // delete organization tags via organization-tags.repository + await dependencies.organizationTagRepository.deleteTagsByOrganizationId(organizationId); + // delete organizationvia organization-for-admin.repository + await dependencies.organizationForAdminRepository.deleteById(organizationId); + } + count++; + } + + if (dryRun) { + logger.info(`Would delete ${count} organizations.`); + } else { + logger.info(`Deleted ${count} organizations.`); + } + } +} + +await ScriptRunner.execute(import.meta.url, DeleteOrganizationsScript); diff --git a/api/tests/organizational-entities/integration/infrastructure/repositories/data-protection-officer.repository.test.js b/api/tests/organizational-entities/integration/infrastructure/repositories/data-protection-officer.repository.test.js index af712496859..d9c3a493430 100644 --- a/api/tests/organizational-entities/integration/infrastructure/repositories/data-protection-officer.repository.test.js +++ b/api/tests/organizational-entities/integration/infrastructure/repositories/data-protection-officer.repository.test.js @@ -80,6 +80,37 @@ describe('Integration | Organizational Entities | Repository | data-protection-o }); }); + describe('#deleteByOrganizationId', function () { + it('deletes DPO from data protection officers table by organisation id ', async function () { + // given + const organization = databaseBuilder.factory.buildOrganization(); + const organization2 = databaseBuilder.factory.buildOrganization(); + databaseBuilder.factory.buildDataProtectionOfficer.withOrganizationId({ + firstName: 'Justin', + lastName: 'Ninstan', + email: 'justin-ninstan@example.net', + organizationId: organization.id, + }); + databaseBuilder.factory.buildDataProtectionOfficer.withOrganizationId({ + firstName: 'Justin', + lastName: 'Ninstan', + email: 'justin-ninstan@example.net', + organizationId: organization2.id, + }); + + await databaseBuilder.commit(); + + // when + + await dataProtectionOfficerRepository.deleteDpoByOrganizationId(organization2.id); + + // then + const dataProtectionOfficers = await knex('data-protection-officers').select(); + expect(dataProtectionOfficers).to.have.lengthOf(1); + expect(dataProtectionOfficers[0]).to.have.property('organizationId', organization.id); + }); + }); + describe('#get', function () { context('when DPO exists', function () { it('returns a DPO domain object', async function () { diff --git a/api/tests/organizational-entities/integration/infrastructure/repositories/organization-for-admin.repository.test.js b/api/tests/organizational-entities/integration/infrastructure/repositories/organization-for-admin.repository.test.js index 32a6b568ab9..d085cd1b0cd 100644 --- a/api/tests/organizational-entities/integration/infrastructure/repositories/organization-for-admin.repository.test.js +++ b/api/tests/organizational-entities/integration/infrastructure/repositories/organization-for-admin.repository.test.js @@ -869,4 +869,22 @@ describe('Integration | Organizational Entities | Infrastructure | Repository | expect(nbOrganizationsAfterUpdate).to.equal(nbOrganizationsBeforeUpdate); }); }); + + describe('#deleteById', function () { + it('deletes organization by id', async function () { + // given + const organization = databaseBuilder.factory.buildOrganization(); + const organization2 = databaseBuilder.factory.buildOrganization(); + await databaseBuilder.commit(); + + // when + await organizationForAdminRepository.deleteById(organization.id); + + // then + const organizationDeleted = await knex('organizations').where({ id: organization.id }); + expect(organizationDeleted).to.have.lengthOf(0); + const organizationNotDeleted = await knex('organizations').where({ id: organization2.id }); + expect(organizationNotDeleted).to.have.lengthOf(1); + }); + }); }); diff --git a/api/tests/organizational-entities/integration/infrastructure/repositories/organization-tag.repository.test.js b/api/tests/organizational-entities/integration/infrastructure/repositories/organization-tag.repository.test.js index edf86c64221..692b023b8b9 100644 --- a/api/tests/organizational-entities/integration/infrastructure/repositories/organization-tag.repository.test.js +++ b/api/tests/organizational-entities/integration/infrastructure/repositories/organization-tag.repository.test.js @@ -96,4 +96,34 @@ describe('Integration | Organizational Entities | Infrastructure | Repository | expect(foundOrganizations).to.have.lengthOf(2); }); }); + + describe('#deleteTag', function () { + it('delete organizationTags', async function () { + // given + const organization1 = databaseBuilder.factory.buildOrganization(); + const organization2 = databaseBuilder.factory.buildOrganization(); + const tag1 = databaseBuilder.factory.buildTag({ name: 'tag1' }); + const tag2 = databaseBuilder.factory.buildTag({ name: 'tag2' }); + databaseBuilder.factory.buildOrganizationTag({ + organizationId: organization1.id, + tagId: tag1.id, + }); + databaseBuilder.factory.buildOrganizationTag({ + organizationId: organization2.id, + tagId: tag2.id, + }); + databaseBuilder.factory.buildOrganizationTag({ + organizationId: organization1.id, + tagId: tag2.id, + }); + await databaseBuilder.commit(); + // when + await organizationTagRepository.deleteTagsByOrganizationId(organization1.id); + // then + const organizationTags = await knex('organization-tags').select(); + expect(organizationTags).to.have.lengthOf(1); + expect(organizationTags[0]).to.have.property('tagId', tag2.id); + expect(organizationTags[0]).to.have.property('organizationId', organization2.id); + }); + }); }); diff --git a/api/tests/organizational-entities/unit/scripts/delete-organizations-script.test.js b/api/tests/organizational-entities/unit/scripts/delete-organizations-script.test.js new file mode 100644 index 00000000000..e69e1ad7f35 --- /dev/null +++ b/api/tests/organizational-entities/unit/scripts/delete-organizations-script.test.js @@ -0,0 +1,40 @@ +import { DeleteOrganizationsScript } from '../../../../src/organizational-entities/scripts/./delete-organizations-script.js'; +import { expect, sinon } from '../../../test-helper.js'; + +describe('DeleteOrganizationsScript', function () { + describe('Handle', function () { + let script; + let logger; + let organizationForAdminRepository; + let organizationTagRepository; + let dataProtectionOfficerRepository; + + beforeEach(function () { + script = new DeleteOrganizationsScript(); + logger = { info: sinon.spy() }; + organizationForAdminRepository = { deleteById: sinon.stub() }; + organizationTagRepository = { deleteTagsByOrganizationId: sinon.stub() }; + dataProtectionOfficerRepository = { deleteDpoByOrganizationId: sinon.stub() }; + }); + + it('handles data correctly', async function () { + const file = [{ 'Organization ID': 1 }, { 'Organization ID': 2 }]; + + await script.handle({ + options: { file }, + logger, + dependencies: { + organizationForAdminRepository, + organizationTagRepository, + dataProtectionOfficerRepository, + }, + }); + expect(organizationForAdminRepository.deleteById.calledWith(1)).to.be.true; + expect(organizationForAdminRepository.deleteById.calledWith(2)).to.be.true; + expect(organizationTagRepository.deleteTagsByOrganizationId.calledWith(1)).to.be.true; + expect(organizationTagRepository.deleteTagsByOrganizationId.calledWith(2)).to.be.true; + expect(dataProtectionOfficerRepository.deleteDpoByOrganizationId.calledWith(1)).to.be.true; + expect(dataProtectionOfficerRepository.deleteDpoByOrganizationId.calledWith(2)).to.be.true; + }); + }); +});