From e19325244b6fa1f84da10ba2a828e5fa36118463 Mon Sep 17 00:00:00 2001 From: Yvonnick Frin Date: Wed, 26 Jun 2024 13:53:44 +0200 Subject: [PATCH 01/12] feat(api): add getByCampaignIds method in campaignParticipationRepository --- .../domain/models/CampaignParticipation.js | 4 +- .../campaign-participation-repository.js | 9 ++++ .../campaign-participation-repository_test.js | 44 +++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/api/src/prescription/campaign-participation/domain/models/CampaignParticipation.js b/api/src/prescription/campaign-participation/domain/models/CampaignParticipation.js index 85008c8b50b..7402ab7cd33 100644 --- a/api/src/prescription/campaign-participation/domain/models/CampaignParticipation.js +++ b/api/src/prescription/campaign-participation/domain/models/CampaignParticipation.js @@ -36,8 +36,8 @@ class CampaignParticipation { this.assessments = assessments; this.userId = userId; this.status = status; - this.validatedSkillsCount = validatedSkillsCount; - this.pixScore = pixScore; + this.validatedSkillsCount = validatedSkillsCount || null; + this.pixScore = pixScore || null; this.organizationLearnerId = organizationLearnerId; } diff --git a/api/src/prescription/campaign-participation/infrastructure/repositories/campaign-participation-repository.js b/api/src/prescription/campaign-participation/infrastructure/repositories/campaign-participation-repository.js index 35e342e0c23..cbbb7c6b825 100644 --- a/api/src/prescription/campaign-participation/infrastructure/repositories/campaign-participation-repository.js +++ b/api/src/prescription/campaign-participation/infrastructure/repositories/campaign-participation-repository.js @@ -55,6 +55,14 @@ const get = async function (id, domainTransaction) { }); }; +const getByCampaignIds = async function (campaignIds) { + const knexConn = ApplicationTransaction.getConnection(); + const campaignParticipations = await knexConn('campaign-participations') + .whereNull('deletedAt') + .whereIn('campaignId', campaignIds); + return campaignParticipations.map((campaignParticipation) => new CampaignParticipation(campaignParticipation)); +}; + const getAllCampaignParticipationsInCampaignForASameLearner = async function ({ campaignId, campaignParticipationId }) { const knexConn = DomainTransaction.getConnection(); const result = await knexConn('campaign-participations') @@ -102,6 +110,7 @@ const remove = async function ({ id, deletedAt, deletedBy }) { export { get, getAllCampaignParticipationsInCampaignForASameLearner, + getByCampaignIds, getCampaignParticipationsForOrganizationLearner, remove, update, diff --git a/api/tests/prescription/campaign-participation/integration/infrastructure/repositories/campaign-participation-repository_test.js b/api/tests/prescription/campaign-participation/integration/infrastructure/repositories/campaign-participation-repository_test.js index b245e892944..33afa79d847 100644 --- a/api/tests/prescription/campaign-participation/integration/infrastructure/repositories/campaign-participation-repository_test.js +++ b/api/tests/prescription/campaign-participation/integration/infrastructure/repositories/campaign-participation-repository_test.js @@ -205,6 +205,50 @@ describe('Integration | Repository | Campaign Participation', function () { }); }); + describe('#getByCampaignIds', function () { + it('should return participations', async function () { + // given + const firstCampaignParticipationToUpdate = databaseBuilder.factory.buildCampaignParticipation({ + deletedAt: null, + deletedBy: null, + }); + const secondCampaignParticipationToUpdate = databaseBuilder.factory.buildCampaignParticipation({ + deletedAt: null, + deletedBy: null, + }); + databaseBuilder.factory.buildCampaignParticipation(); + await databaseBuilder.commit(); + + // when + const participations = await campaignParticipationRepository.getByCampaignIds([ + firstCampaignParticipationToUpdate.campaignId, + secondCampaignParticipationToUpdate.campaignId, + ]); + + // then + expect(participations).to.be.deep.equal([ + new CampaignParticipation(firstCampaignParticipationToUpdate), + new CampaignParticipation(secondCampaignParticipationToUpdate), + ]); + }); + + it('should not return deleted participations', async function () { + // given + const userId = databaseBuilder.factory.buildUser().id; + const deletedParticipation = databaseBuilder.factory.buildCampaignParticipation({ + deletedAt: new Date(), + deletedBy: userId, + }); + await databaseBuilder.commit(); + + // when + const participations = await campaignParticipationRepository.getByCampaignIds([deletedParticipation.campaignId]); + + // then + expect(participations.length).to.equal(0); + }); + }); + describe('#update', function () { it('save the changes of the campaignParticipation', async function () { const campaignParticipationId = 12; From c8b67cd0e33c3da6df4e53e1ef114eabdfc052d4 Mon Sep 17 00:00:00 2001 From: Yvonnick Frin Date: Wed, 26 Jun 2024 13:55:06 +0200 Subject: [PATCH 02/12] feat(api): add batchUpdate method in campaignParticipationRepository --- .../campaign-participation-repository.js | 31 +++++++---- .../campaign-participation-repository_test.js | 54 +++++++++++++++++++ 2 files changed, 76 insertions(+), 9 deletions(-) diff --git a/api/src/prescription/campaign-participation/infrastructure/repositories/campaign-participation-repository.js b/api/src/prescription/campaign-participation/infrastructure/repositories/campaign-participation-repository.js index cbbb7c6b825..0f372cf0be2 100644 --- a/api/src/prescription/campaign-participation/infrastructure/repositories/campaign-participation-repository.js +++ b/api/src/prescription/campaign-participation/infrastructure/repositories/campaign-participation-repository.js @@ -1,3 +1,5 @@ +import lodash from 'lodash'; + import { knex } from '../../../../../db/knex-database-connection.js'; import { NotFoundError } from '../../../../../lib/domain/errors.js'; import { Campaign } from '../../../../../lib/domain/models/Campaign.js'; @@ -9,6 +11,19 @@ import { ApplicationTransaction } from '../../../shared/infrastructure/Applicati import { CampaignParticipation } from '../../domain/models/CampaignParticipation.js'; import { AvailableCampaignParticipation } from '../../domain/read-models/AvailableCampaignParticipation.js'; +const { pick } = lodash; + +const CAMPAIGN_PARTICIPATION_ATTRIBUTES = [ + 'participantExternalId', + 'sharedAt', + 'status', + 'campaignId', + 'userId', + 'organizationLearnerId', + 'deletedAt', + 'deletedBy', +]; + const updateWithSnapshot = async function (campaignParticipation) { const domainTransaction = ApplicationTransaction.getTransactionAsDomainTransaction(); await this.update(campaignParticipation); @@ -29,16 +44,13 @@ const updateWithSnapshot = async function (campaignParticipation) { const update = async function (campaignParticipation, domainTransaction) { const knexConn = ApplicationTransaction.getConnection(domainTransaction); - const attributes = { - participantExternalId: campaignParticipation.participantExternalId, - sharedAt: campaignParticipation.sharedAt, - status: campaignParticipation.status, - campaignId: campaignParticipation.campaignId, - userId: campaignParticipation.userId, - organizationLearnerId: campaignParticipation.organizationLearnerId, - }; + await knexConn('campaign-participations') + .where({ id: campaignParticipation.id }) + .update(pick(campaignParticipation, CAMPAIGN_PARTICIPATION_ATTRIBUTES)); +}; - await knexConn('campaign-participations').where({ id: campaignParticipation.id }).update(attributes); +const batchUpdate = async function (campaignParticipations) { + return Promise.all(campaignParticipations.map((campaignParticipation) => update(campaignParticipation))); }; const get = async function (id, domainTransaction) { @@ -108,6 +120,7 @@ const remove = async function ({ id, deletedAt, deletedBy }) { }; export { + batchUpdate, get, getAllCampaignParticipationsInCampaignForASameLearner, getByCampaignIds, diff --git a/api/tests/prescription/campaign-participation/integration/infrastructure/repositories/campaign-participation-repository_test.js b/api/tests/prescription/campaign-participation/integration/infrastructure/repositories/campaign-participation-repository_test.js index 33afa79d847..6bd90734bb0 100644 --- a/api/tests/prescription/campaign-participation/integration/infrastructure/repositories/campaign-participation-repository_test.js +++ b/api/tests/prescription/campaign-participation/integration/infrastructure/repositories/campaign-participation-repository_test.js @@ -304,6 +304,60 @@ describe('Integration | Repository | Campaign Participation', function () { }); }); + describe('#batchUpdate', function () { + let clock; + const frozenTime = new Date('1987-09-01T00:00:00Z'); + + beforeEach(async function () { + clock = sinon.useFakeTimers({ now: frozenTime, toFake: ['Date'] }); + }); + + afterEach(function () { + clock.restore(); + }); + + it('save the changes of multiple campaignParticipations', async function () { + // given + const user = databaseBuilder.factory.buildUser(); + const firstCampaignParticipationToUpdate = databaseBuilder.factory.buildCampaignParticipation({ + deletedAt: null, + deletedBy: null, + }); + const secondCampaignParticipationToUpdate = databaseBuilder.factory.buildCampaignParticipation({ + deletedAt: null, + deletedBy: null, + }); + + await databaseBuilder.commit(); + + // when + const participations = [ + new CampaignParticipation(firstCampaignParticipationToUpdate), + new CampaignParticipation(secondCampaignParticipationToUpdate), + ]; + participations.forEach((participation) => { + participation.delete(user.id); + }); + await campaignParticipationRepository.batchUpdate(participations); + + // then + const updatedFirstParticipation = await campaignParticipationRepository.get( + firstCampaignParticipationToUpdate.id, + ); + const updatedSecondParticipation = await campaignParticipationRepository.get( + secondCampaignParticipationToUpdate.id, + ); + expect(updatedFirstParticipation).to.deep.include({ + deletedAt: frozenTime, + deletedBy: user.id, + }); + expect(updatedSecondParticipation).to.deep.include({ + deletedAt: frozenTime, + deletedBy: user.id, + }); + }); + }); + describe('#getAllCampaignParticipationsInCampaignForASameLearner', function () { let campaignId; let organizationLearnerId; From 4026fd50c9c027ed315a6cb172456b513c3f2cd7 Mon Sep 17 00:00:00 2001 From: Yvonnick Frin Date: Wed, 26 Jun 2024 14:37:02 +0200 Subject: [PATCH 03/12] tests?(api): fix typo in tests --- .../repositories/campaign-participation-repository_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/tests/prescription/campaign-participation/integration/infrastructure/repositories/campaign-participation-repository_test.js b/api/tests/prescription/campaign-participation/integration/infrastructure/repositories/campaign-participation-repository_test.js index 6bd90734bb0..edd9f08188f 100644 --- a/api/tests/prescription/campaign-participation/integration/infrastructure/repositories/campaign-participation-repository_test.js +++ b/api/tests/prescription/campaign-participation/integration/infrastructure/repositories/campaign-participation-repository_test.js @@ -279,7 +279,7 @@ describe('Integration | Repository | Campaign Participation', function () { expect(campaignParticipation.status).to.equals(SHARED); }); - it('should not update because the leaner can not have 2 active participations for the same campaign', async function () { + it('should not update because the learner can not have 2 active participations for the same campaign', async function () { const campaignId = databaseBuilder.factory.buildCampaign().id; const userId = databaseBuilder.factory.buildUser().id; const organizationLearnerId = databaseBuilder.factory.buildOrganizationLearner({ userId }).id; From ca385567fea576aa494432302e43b08f1ebfdf31 Mon Sep 17 00:00:00 2001 From: Yvonnick Frin Date: Wed, 26 Jun 2024 15:02:26 +0200 Subject: [PATCH 04/12] feat(api): add delete method on Campaign model --- .../prescription/campaign/domain/errors.js | 7 ++++ .../campaign/domain/models/Campaign.js | 27 +++++++++++-- api/src/shared/application/error-manager.js | 6 ++- .../unit/domain/models/Campaign_test.js | 40 +++++++++++++++++++ 4 files changed, 76 insertions(+), 4 deletions(-) diff --git a/api/src/prescription/campaign/domain/errors.js b/api/src/prescription/campaign/domain/errors.js index 033459f3fc3..67cf305a176 100644 --- a/api/src/prescription/campaign/domain/errors.js +++ b/api/src/prescription/campaign/domain/errors.js @@ -46,10 +46,17 @@ class ArchivedCampaignError extends DomainError { } } +class DeletedCampaignError extends DomainError { + constructor(message = 'Cette campagne est déjà supprimée.') { + super(message); + } +} + export { ArchivedCampaignError, CampaignCodeFormatError, CampaignUniqueCodeError, + DeletedCampaignError, IsForAbsoluteNoviceUpdateError, MultipleSendingsUpdateError, SwapCampaignMismatchOrganizationError, diff --git a/api/src/prescription/campaign/domain/models/Campaign.js b/api/src/prescription/campaign/domain/models/Campaign.js index 69ea485073f..91d3ba33c54 100644 --- a/api/src/prescription/campaign/domain/models/Campaign.js +++ b/api/src/prescription/campaign/domain/models/Campaign.js @@ -1,7 +1,12 @@ import { ObjectValidationError } from '../../../../../lib/domain/errors.js'; import { CampaignTypes } from '../../../shared/domain/constants.js'; -import { ArchivedCampaignError } from '../errors.js'; -import { CampaignCodeFormatError, IsForAbsoluteNoviceUpdateError, MultipleSendingsUpdateError } from '../errors.js'; +import { + ArchivedCampaignError, + CampaignCodeFormatError, + DeletedCampaignError, + IsForAbsoluteNoviceUpdateError, + MultipleSendingsUpdateError, +} from '../errors.js'; class Campaign { constructor({ @@ -27,6 +32,8 @@ class Campaign { ownerId, archivedAt, archivedBy, + deletedAt = null, + deletedBy = null, participationCount, } = {}) { this.id = id; @@ -52,6 +59,8 @@ class Campaign { this.createdAt = createdAt; this.archivedAt = archivedAt; this.archivedBy = archivedBy; + this.deletedAt = deletedAt; + this.deletedBy = deletedBy; this.hasParticipation = participationCount > 0; } @@ -67,9 +76,21 @@ class Campaign { return Boolean(this.archivedAt); } + delete(userId) { + if (this.deletedAt) { + throw new DeletedCampaignError(); + } + if (!userId) { + throw new ObjectValidationError('userId Missing'); + } + + this.deletedAt = new Date(); + this.deletedBy = userId; + } + archive(archivedAt, archivedBy) { if (this.archivedAt) { - throw new ArchivedCampaignError('Campaign Already Archived'); + throw new ArchivedCampaignError(); } if (!archivedAt) { throw new ObjectValidationError('ArchivedAt Missing'); diff --git a/api/src/shared/application/error-manager.js b/api/src/shared/application/error-manager.js index ce4cf72e52e..266466369dd 100644 --- a/api/src/shared/application/error-manager.js +++ b/api/src/shared/application/error-manager.js @@ -5,7 +5,7 @@ import * as translations from '../../../translations/index.js'; import { AdminMemberError } from '../../authorization/domain/errors.js'; import { CsvWithNoSessionDataError } from '../../certification/session-management/domain/errors.js'; import { EmptyAnswerError } from '../../evaluation/domain/errors.js'; -import { ArchivedCampaignError } from '../../prescription/campaign/domain/errors.js'; +import { ArchivedCampaignError, DeletedCampaignError } from '../../prescription/campaign/domain/errors.js'; import { CampaignParticipationDeletedError } from '../../prescription/campaign-participation/domain/errors.js'; import { AggregateImportError, SiecleXmlImportError } from '../../prescription/learner-management/domain/errors.js'; import { OrganizationCantGetPlacesStatisticsError } from '../../prescription/organization-place/domain/errors.js'; @@ -138,6 +138,10 @@ function _mapToHttpError(error) { return new HttpErrors.PreconditionFailedError(error.message); } + if (error instanceof DeletedCampaignError) { + return new HttpErrors.PreconditionFailedError(error.message); + } + if (error instanceof CsvWithNoSessionDataError) { return new HttpErrors.UnprocessableEntityError(error.message, error.code); } diff --git a/api/tests/prescription/campaign/unit/domain/models/Campaign_test.js b/api/tests/prescription/campaign/unit/domain/models/Campaign_test.js index 6b239d3ebe2..13c6f499465 100644 --- a/api/tests/prescription/campaign/unit/domain/models/Campaign_test.js +++ b/api/tests/prescription/campaign/unit/domain/models/Campaign_test.js @@ -1,15 +1,21 @@ +import sinon from 'sinon'; + import { ObjectValidationError } from '../../../../../../lib/domain/errors.js'; import { ArchivedCampaignError, CampaignCodeFormatError, + DeletedCampaignError, } from '../../../../../../src/prescription/campaign/domain/errors.js'; import { Campaign } from '../../../../../../src/prescription/campaign/domain/models/Campaign.js'; import { catchErr, expect } from '../../../../../test-helper.js'; describe('Campaign', function () { let campaign; + let clock; + const now = new Date('2022-11-28T12:00:00Z'); beforeEach(function () { + clock = sinon.useFakeTimers({ now, toFake: ['Date'] }); campaign = new Campaign({ id: 1, code: 'RIGHTCODE', @@ -19,6 +25,40 @@ describe('Campaign', function () { }); }); + afterEach(function () { + clock.restore(); + }); + + describe('#delete', function () { + it('deletes the campaign', function () { + const campaign = new Campaign({ id: 1, code: 'ABC123' }); + + campaign.delete(1); + + expect(campaign).to.deep.includes({ id: 1, code: 'ABC123', deletedAt: now, deletedBy: 1 }); + }); + + context('when the campaign is already deleted', function () { + it('throws an exception', async function () { + const campaign = new Campaign({ id: 1, code: 'ABC123', deletedAt: new Date('2023-01-01'), deletedBy: 2 }); + + const error = await catchErr(campaign.delete, campaign)(1); + + expect(error).to.be.an.instanceOf(DeletedCampaignError); + }); + }); + + context('when the given userId is not provided', function () { + it('throws an exception', async function () { + const campaign = new Campaign({ id: 1, code: 'ABC123' }); + + const error = await catchErr(campaign.delete, campaign)(); + + expect(error).to.be.an.instanceOf(ObjectValidationError); + }); + }); + }); + describe('#archive', function () { it('archives the campaigns', function () { const campaign = new Campaign({ id: 1, code: 'ABC123', archivedAt: null, archivedBy: null }); From 89d7f6345d9dedb5fb5c7097c20d23a67b6da70f Mon Sep 17 00:00:00 2001 From: Yvonnick Frin Date: Wed, 26 Jun 2024 15:03:43 +0200 Subject: [PATCH 05/12] feat(api): add batchUpdate method in CampaignAdministrationRepository --- .../campaign-administration-repository.js | 22 +++++++++- ...campaign-administration-repository_test.js | 40 +++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/api/src/prescription/campaign/infrastructure/repositories/campaign-administration-repository.js b/api/src/prescription/campaign/infrastructure/repositories/campaign-administration-repository.js index c63acf4aee5..6800e1244e9 100644 --- a/api/src/prescription/campaign/infrastructure/repositories/campaign-administration-repository.js +++ b/api/src/prescription/campaign/infrastructure/repositories/campaign-administration-repository.js @@ -4,10 +4,13 @@ import _ from 'lodash'; import { knex } from '../../../../../db/knex-database-connection.js'; import * as skillRepository from '../../../../shared/infrastructure/repositories/skill-repository.js'; +import { ApplicationTransaction } from '../../../shared/infrastructure/ApplicationTransaction.js'; import { UnknownCampaignId } from '../../domain/errors.js'; import { Campaign } from '../../domain/models/Campaign.js'; const CAMPAIGN_ATTRIBUTES = [ + 'deletedAt', + 'deletedBy', 'archivedAt', 'archivedBy', 'name', @@ -53,7 +56,8 @@ const get = async function (id) { }; const update = async function (campaign) { - const [editedCampaign] = await knex('campaigns') + const knexConn = ApplicationTransaction.getConnection(); + const [editedCampaign] = await knexConn('campaigns') .where({ id: campaign.id }) .update(_.pick(campaign, CAMPAIGN_ATTRIBUTES)) .returning('*'); @@ -61,6 +65,10 @@ const update = async function (campaign) { return new Campaign(editedCampaign); }; +const batchUpdate = async function (campaigns) { + return Promise.all(campaigns.map((campaign) => update(campaign))); +}; + const save = async function (campaigns, dependencies = { skillRepository }) { const trx = await knex.transaction(); const campaignsToCreate = _.isArray(campaigns) ? campaigns : [campaigns]; @@ -140,4 +148,14 @@ const archiveCampaigns = function (campaignIds, userId) { }); }; -export { archiveCampaigns, get, getByCode, isCodeAvailable, isFromSameOrganization, save, swapCampaignCodes, update }; +export { + archiveCampaigns, + batchUpdate, + get, + getByCode, + isCodeAvailable, + isFromSameOrganization, + save, + swapCampaignCodes, + update, +}; diff --git a/api/tests/prescription/campaign/integration/infrastructure/repositories/campaign-administration-repository_test.js b/api/tests/prescription/campaign/integration/infrastructure/repositories/campaign-administration-repository_test.js index 53d1d7da06a..8959b220318 100644 --- a/api/tests/prescription/campaign/integration/infrastructure/repositories/campaign-administration-repository_test.js +++ b/api/tests/prescription/campaign/integration/infrastructure/repositories/campaign-administration-repository_test.js @@ -464,6 +464,46 @@ describe('Integration | Repository | Campaign Administration', function () { }); }); + describe('#batchUpdate', function () { + let clock; + const frozenTime = new Date('1992-07-07'); + + beforeEach(async function () { + clock = sinon.useFakeTimers({ now: frozenTime, toFake: ['Date'] }); + }); + + afterEach(function () { + clock.restore(); + }); + + it('should update campaigns', async function () { + const user = databaseBuilder.factory.buildUser(); + const firstCampaign = new Campaign(databaseBuilder.factory.buildCampaign()); + const secondCampaign = new Campaign(databaseBuilder.factory.buildCampaign()); + + await databaseBuilder.commit(); + // given + firstCampaign.delete(user.id); + secondCampaign.delete(user.id); + + // when + await campaignAdministrationRepository.batchUpdate([firstCampaign, secondCampaign]); + + const firstCampaignUpdated = await campaignAdministrationRepository.get(firstCampaign.id); + const secondCampaignUpdated = await campaignAdministrationRepository.get(secondCampaign.id); + + // then + expect(firstCampaignUpdated).to.deep.include({ + deletedAt: frozenTime, + deletedBy: user.id, + }); + expect(secondCampaignUpdated).to.deep.include({ + deletedAt: frozenTime, + deletedBy: user.id, + }); + }); + }); + describe('#update', function () { let campaign; From d051ad35192df5b45d61fec06d617461ab867d26 Mon Sep 17 00:00:00 2001 From: Yvonnick Frin Date: Wed, 26 Jun 2024 15:20:40 +0200 Subject: [PATCH 06/12] feat(api): add getByIds method in campaignAdministrationRepository --- .../campaign-administration-repository.js | 10 ++++++++ ...campaign-administration-repository_test.js | 25 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/api/src/prescription/campaign/infrastructure/repositories/campaign-administration-repository.js b/api/src/prescription/campaign/infrastructure/repositories/campaign-administration-repository.js index 6800e1244e9..a688270399a 100644 --- a/api/src/prescription/campaign/infrastructure/repositories/campaign-administration-repository.js +++ b/api/src/prescription/campaign/infrastructure/repositories/campaign-administration-repository.js @@ -31,6 +31,15 @@ const CAMPAIGN_ATTRIBUTES = [ 'customResultPageButtonUrl', ]; +const getByIds = async (ids) => { + const knexConn = ApplicationTransaction.getConnection(); + const campaigns = await knexConn('campaigns').whereIn('id', ids); + + if (campaigns.length === 0) return null; + + return campaigns.map((campaign) => new Campaign(campaign)); +}; + const getByCode = async function (code) { const campaign = await knex.select('id').from('campaigns').where({ code }).first(); @@ -153,6 +162,7 @@ export { batchUpdate, get, getByCode, + getByIds, isCodeAvailable, isFromSameOrganization, save, diff --git a/api/tests/prescription/campaign/integration/infrastructure/repositories/campaign-administration-repository_test.js b/api/tests/prescription/campaign/integration/infrastructure/repositories/campaign-administration-repository_test.js index 8959b220318..06d43d3d158 100644 --- a/api/tests/prescription/campaign/integration/infrastructure/repositories/campaign-administration-repository_test.js +++ b/api/tests/prescription/campaign/integration/infrastructure/repositories/campaign-administration-repository_test.js @@ -30,6 +30,31 @@ describe('Integration | Repository | Campaign Administration', function () { }); }); + describe('#getByIds', function () { + it('should return null if campaigns does not exists', async function () { + // given & when + const campaigns = await campaignAdministrationRepository.getByIds([1, 2]); + + // then + expect(campaigns).to.be.null; + }); + + it('should return campaigns for given ids', async function () { + // given + const firstCampaign = new Campaign(databaseBuilder.factory.buildCampaign()); + const secondCampaign = new Campaign(databaseBuilder.factory.buildCampaign()); + databaseBuilder.factory.buildCampaign(); + + await databaseBuilder.commit(); + + // when + const campaigns = await campaignAdministrationRepository.getByIds([firstCampaign.id, secondCampaign.id]); + + // then + expect(campaigns).to.deep.equal([firstCampaign, secondCampaign]); + }); + }); + describe('#save', function () { context('when campaign is of type ASSESSMENT', function () { it('should save the given campaign with type ASSESSMENT', async function () { From 7f9921a38bbc28f3dcd54e54a37143f26361988c Mon Sep 17 00:00:00 2001 From: Alexandre Monney Date: Wed, 26 Jun 2024 17:27:05 +0200 Subject: [PATCH 07/12] feat(api): create organization membership repository --- .../read-models/OrganizationMembership.js | 7 +++++ .../infrastructure/repositories/index.js | 15 +++++++++++ .../organization-membership-repository.js | 9 +++++++ ...organization-membership-repository_test.js | 27 +++++++++++++++++++ 4 files changed, 58 insertions(+) create mode 100644 api/src/prescription/campaign/domain/read-models/OrganizationMembership.js create mode 100644 api/src/prescription/campaign/infrastructure/repositories/index.js create mode 100644 api/src/prescription/campaign/infrastructure/repositories/organization-membership-repository.js create mode 100644 api/tests/prescription/campaign/unit/infrastrucutre/repositories/organization-membership-repository_test.js diff --git a/api/src/prescription/campaign/domain/read-models/OrganizationMembership.js b/api/src/prescription/campaign/domain/read-models/OrganizationMembership.js new file mode 100644 index 00000000000..793e8ad3366 --- /dev/null +++ b/api/src/prescription/campaign/domain/read-models/OrganizationMembership.js @@ -0,0 +1,7 @@ +class OrganizationMembership { + constructor({ isAdmin } = {}) { + this.isAdmin = isAdmin; + } +} + +export { OrganizationMembership }; diff --git a/api/src/prescription/campaign/infrastructure/repositories/index.js b/api/src/prescription/campaign/infrastructure/repositories/index.js new file mode 100644 index 00000000000..4e0fb4f197d --- /dev/null +++ b/api/src/prescription/campaign/infrastructure/repositories/index.js @@ -0,0 +1,15 @@ +import { injectDependencies } from '../../../../shared/infrastructure/utils/dependency-injection.js'; +import * as organizationApi from '../../../../team/application/api/organization.js'; +import * as organizationMembershipRepository from './organization-membership-repository.js'; + +const repositoriesWithoutInjectedDependencies = { + organizationMembershipRepository, +}; + +const dependencies = { + organizationApi, +}; + +const repositories = injectDependencies(repositoriesWithoutInjectedDependencies, dependencies); + +export { repositories }; diff --git a/api/src/prescription/campaign/infrastructure/repositories/organization-membership-repository.js b/api/src/prescription/campaign/infrastructure/repositories/organization-membership-repository.js new file mode 100644 index 00000000000..24236e212cb --- /dev/null +++ b/api/src/prescription/campaign/infrastructure/repositories/organization-membership-repository.js @@ -0,0 +1,9 @@ +import { OrganizationMembership } from '../../domain/read-models/OrganizationMembership.js'; + +const getByUserIdAndOrganizationId = async ({ userId, organizationId, organizationApi }) => { + const organizationMembership = await organizationApi.getOrganizationMembership({ userId, organizationId }); + + return new OrganizationMembership(organizationMembership); +}; + +export { getByUserIdAndOrganizationId }; diff --git a/api/tests/prescription/campaign/unit/infrastrucutre/repositories/organization-membership-repository_test.js b/api/tests/prescription/campaign/unit/infrastrucutre/repositories/organization-membership-repository_test.js new file mode 100644 index 00000000000..440eaff50e8 --- /dev/null +++ b/api/tests/prescription/campaign/unit/infrastrucutre/repositories/organization-membership-repository_test.js @@ -0,0 +1,27 @@ +import { OrganizationMembership } from '../../../../../../src/prescription/campaign/domain/read-models/OrganizationMembership.js'; +import { getByUserIdAndOrganizationId } from '../../../../../../src/prescription/campaign/infrastructure/repositories/organization-membership-repository.js'; +import { expect, sinon } from '../../../../../test-helper.js'; + +describe('Unit | Repositories | Organization Membership Repository', function () { + it('should returns the corresponding OrganizationMemberShip Model', async function () { + // given + const isAdminSymbol = Symbol('isAdmin'); + const userId = Symbol('userId'); + const organizationId = Symbol('organizationId'); + const organizationApiStub = { + getOrganizationMembership: sinon.stub(), + }; + organizationApiStub.getOrganizationMembership + .withArgs({ userId, organizationId }) + .resolves({ isAdmin: isAdminSymbol }); + // when + const membership = await getByUserIdAndOrganizationId({ + userId, + organizationId, + organizationApi: organizationApiStub, + }); + // then + expect(membership).to.be.an.instanceOf(OrganizationMembership); + expect(membership.isAdmin).to.equal(isAdminSymbol); + }); +}); From 452ceb1e365c86e2b337836ea7dd1495eda5c2ae Mon Sep 17 00:00:00 2001 From: Alexandre Monney Date: Thu, 27 Jun 2024 16:22:32 +0200 Subject: [PATCH 08/12] tech(api): move method from campaign participation repository in campaign context to campaign-participation context --- .../campaign-participation-repository.js | 44 ++++ .../campaign/domain/usecases/index.js | 2 +- .../campaign-participation-repository.js | 45 ---- .../campaign-participation-repository_test.js | 199 ++++++++++++++++- ...files-collection-results-to-stream_test.js | 2 +- .../campaign-participation-repository_test.js | 203 ------------------ 6 files changed, 244 insertions(+), 251 deletions(-) delete mode 100644 api/src/prescription/campaign/infrastructure/repositories/campaign-participation-repository.js delete mode 100644 api/tests/prescription/campaign/integration/infrastructure/repositories/campaign-participation-repository_test.js diff --git a/api/src/prescription/campaign-participation/infrastructure/repositories/campaign-participation-repository.js b/api/src/prescription/campaign-participation/infrastructure/repositories/campaign-participation-repository.js index 0f372cf0be2..4892899f0c4 100644 --- a/api/src/prescription/campaign-participation/infrastructure/repositories/campaign-participation-repository.js +++ b/api/src/prescription/campaign-participation/infrastructure/repositories/campaign-participation-repository.js @@ -13,6 +13,8 @@ import { AvailableCampaignParticipation } from '../../domain/read-models/Availab const { pick } = lodash; +import { CampaignParticipationStatuses } from '../../../shared/domain/constants.js'; + const CAMPAIGN_PARTICIPATION_ATTRIBUTES = [ 'participantExternalId', 'sharedAt', @@ -119,8 +121,50 @@ const remove = async function ({ id, deletedAt, deletedBy }) { return await knexConn('campaign-participations').where({ id }).update({ deletedAt, deletedBy }); }; +const findProfilesCollectionResultDataByCampaignId = async function (campaignId) { + const results = await knex('campaign-participations') + .select([ + 'campaign-participations.*', + 'view-active-organization-learners.studentNumber', + 'view-active-organization-learners.division', + 'view-active-organization-learners.group', + 'view-active-organization-learners.firstName', + 'view-active-organization-learners.lastName', + ]) + .join( + 'view-active-organization-learners', + 'view-active-organization-learners.id', + 'campaign-participations.organizationLearnerId', + ) + .where({ campaignId, 'campaign-participations.deletedAt': null }) + .orderBy('lastName', 'ASC') + .orderBy('firstName', 'ASC') + .orderBy('createdAt', 'DESC'); + + return results.map(_rowToResult); +}; + +function _rowToResult(row) { + return { + id: row.id, + createdAt: new Date(row.createdAt), + isShared: row.status === CampaignParticipationStatuses.SHARED, + sharedAt: row.sharedAt ? new Date(row.sharedAt) : null, + participantExternalId: row.participantExternalId, + userId: row.userId, + isCompleted: row.state === 'completed', + studentNumber: row.studentNumber, + participantFirstName: row.firstName, + participantLastName: row.lastName, + division: row.division, + pixScore: row.pixScore, + group: row.group, + }; +} + export { batchUpdate, + findProfilesCollectionResultDataByCampaignId, get, getAllCampaignParticipationsInCampaignForASameLearner, getByCampaignIds, diff --git a/api/src/prescription/campaign/domain/usecases/index.js b/api/src/prescription/campaign/domain/usecases/index.js index 16e53b2ee40..bd64624efed 100644 --- a/api/src/prescription/campaign/domain/usecases/index.js +++ b/api/src/prescription/campaign/domain/usecases/index.js @@ -18,6 +18,7 @@ import * as organizationRepository from '../../../../shared/infrastructure/repos import { injectDependencies } from '../../../../shared/infrastructure/utils/dependency-injection.js'; import { importNamedExportsFromDirectory } from '../../../../shared/infrastructure/utils/import-named-exports-from-directory.js'; import * as campaignAnalysisRepository from '../../../campaign-participation/infrastructure/repositories/campaign-analysis-repository.js'; +import * as campaignParticipationRepository from '../../../campaign-participation/infrastructure/repositories/campaign-participation-repository.js'; import * as campaignAdministrationRepository from '../../infrastructure/repositories/campaign-administration-repository.js'; import * as campaignAssessmentParticipationResultListRepository from '../../infrastructure/repositories/campaign-assessment-participation-result-list-repository.js'; import * as campaignCollectiveResultRepository from '../../infrastructure/repositories/campaign-collective-result-repository.js'; @@ -26,7 +27,6 @@ import * as campaignCreatorRepository from '../../infrastructure/repositories/ca import * as campaignManagementRepository from '../../infrastructure/repositories/campaign-management-repository.js'; import { campaignParticipantActivityRepository } from '../../infrastructure/repositories/campaign-participant-activity-repository.js'; import * as campaignParticipationInfoRepository from '../../infrastructure/repositories/campaign-participation-info-repository.js'; -import * as campaignParticipationRepository from '../../infrastructure/repositories/campaign-participation-repository.js'; import * as campaignParticipationsStatsRepository from '../../infrastructure/repositories/campaign-participations-stats-repository.js'; import * as campaignProfilesCollectionParticipationSummaryRepository from '../../infrastructure/repositories/campaign-profiles-collection-participation-summary-repository.js'; import * as campaignReportRepository from '../../infrastructure/repositories/campaign-report-repository.js'; diff --git a/api/src/prescription/campaign/infrastructure/repositories/campaign-participation-repository.js b/api/src/prescription/campaign/infrastructure/repositories/campaign-participation-repository.js deleted file mode 100644 index 75bd827574c..00000000000 --- a/api/src/prescription/campaign/infrastructure/repositories/campaign-participation-repository.js +++ /dev/null @@ -1,45 +0,0 @@ -import { knex } from '../../../../../db/knex-database-connection.js'; -import { CampaignParticipationStatuses } from '../../../shared/domain/constants.js'; - -const findProfilesCollectionResultDataByCampaignId = async function (campaignId) { - const results = await knex('campaign-participations') - .select([ - 'campaign-participations.*', - 'view-active-organization-learners.studentNumber', - 'view-active-organization-learners.division', - 'view-active-organization-learners.group', - 'view-active-organization-learners.firstName', - 'view-active-organization-learners.lastName', - ]) - .join( - 'view-active-organization-learners', - 'view-active-organization-learners.id', - 'campaign-participations.organizationLearnerId', - ) - .where({ campaignId, 'campaign-participations.deletedAt': null }) - .orderBy('lastName', 'ASC') - .orderBy('firstName', 'ASC') - .orderBy('createdAt', 'DESC'); - - return results.map(_rowToResult); -}; - -export { findProfilesCollectionResultDataByCampaignId }; - -function _rowToResult(row) { - return { - id: row.id, - createdAt: new Date(row.createdAt), - isShared: row.status === CampaignParticipationStatuses.SHARED, - sharedAt: row.sharedAt ? new Date(row.sharedAt) : null, - participantExternalId: row.participantExternalId, - userId: row.userId, - isCompleted: row.state === 'completed', - studentNumber: row.studentNumber, - participantFirstName: row.firstName, - participantLastName: row.lastName, - division: row.division, - pixScore: row.pixScore, - group: row.group, - }; -} diff --git a/api/tests/prescription/campaign-participation/integration/infrastructure/repositories/campaign-participation-repository_test.js b/api/tests/prescription/campaign-participation/integration/infrastructure/repositories/campaign-participation-repository_test.js index edd9f08188f..61a640e9bd2 100644 --- a/api/tests/prescription/campaign-participation/integration/infrastructure/repositories/campaign-participation-repository_test.js +++ b/api/tests/prescription/campaign-participation/integration/infrastructure/repositories/campaign-participation-repository_test.js @@ -1,9 +1,14 @@ +import _ from 'lodash'; + import { NotFoundError } from '../../../../../../lib/domain/errors.js'; import { DomainTransaction } from '../../../../../../lib/infrastructure/DomainTransaction.js'; import { CampaignParticipation } from '../../../../../../src/prescription/campaign-participation/domain/models/CampaignParticipation.js'; import { AvailableCampaignParticipation } from '../../../../../../src/prescription/campaign-participation/domain/read-models/AvailableCampaignParticipation.js'; import * as campaignParticipationRepository from '../../../../../../src/prescription/campaign-participation/infrastructure/repositories/campaign-participation-repository.js'; -import { CampaignParticipationStatuses } from '../../../../../../src/prescription/shared/domain/constants.js'; +import { + CampaignParticipationStatuses, + CampaignTypes, +} from '../../../../../../src/prescription/shared/domain/constants.js'; import { ApplicationTransaction } from '../../../../../../src/prescription/shared/infrastructure/ApplicationTransaction.js'; import { catchErr, databaseBuilder, expect, knex, sinon } from '../../../../../test-helper.js'; @@ -629,4 +634,196 @@ describe('Integration | Repository | Campaign Participation', function () { }); }); }); + + describe('#findProfilesCollectionResultDataByCampaignId', function () { + let campaign1; + let campaign2; + let campaignParticipation1; + let organizationId; + + beforeEach(async function () { + organizationId = databaseBuilder.factory.buildOrganization().id; + campaign1 = databaseBuilder.factory.buildCampaign({ organizationId, type: CampaignTypes.PROFILES_COLLECTION }); + campaign2 = databaseBuilder.factory.buildCampaign({ organizationId, type: CampaignTypes.PROFILES_COLLECTION }); + + campaignParticipation1 = databaseBuilder.factory.buildCampaignParticipationWithOrganizationLearner( + { organizationId, firstName: 'Hubert', lastName: 'Parterre', division: '6emeD' }, + { + campaignId: campaign1.id, + createdAt: new Date('2017-03-15T14:59:35Z'), + }, + ); + databaseBuilder.factory.buildCampaignParticipation({ + campaignId: campaign2.id, + }); + await databaseBuilder.commit(); + }); + + it('should return the campaign-participation linked to the given campaign', async function () { + // given + const campaignId = campaign1.id; + + // when + const participationResultDatas = + await campaignParticipationRepository.findProfilesCollectionResultDataByCampaignId(campaignId); + + // then + const attributes = participationResultDatas.map((participationResultData) => + _.pick(participationResultData, ['id', 'isShared', 'sharedAt', 'participantExternalId', 'userId']), + ); + expect(attributes).to.deep.equal([ + { + id: campaignParticipation1.id, + isShared: true, + sharedAt: campaignParticipation1.sharedAt, + participantExternalId: campaignParticipation1.participantExternalId, + userId: campaignParticipation1.userId, + }, + ]); + }); + + it('should not return the deleted campaign-participation linked to the given campaign', async function () { + // given + const campaignId = campaign1.id; + databaseBuilder.factory.buildCampaignParticipationWithOrganizationLearner( + { organizationId, firstName: 'Piere', lastName: 'Pi air', division: '6emeD' }, + { + campaignId: campaign1.id, + createdAt: new Date('2017-03-15T14:59:35Z'), + deletedAt: new Date(), + }, + ); + await databaseBuilder.commit(); + + // when + const participationResultDatas = + await campaignParticipationRepository.findProfilesCollectionResultDataByCampaignId(campaignId); + + // then + const attributes = participationResultDatas.map((participationResultData) => + _.pick(participationResultData, ['id', 'isShared', 'sharedAt', 'participantExternalId', 'userId']), + ); + expect(attributes).to.deep.equal([ + { + id: campaignParticipation1.id, + isShared: true, + sharedAt: campaignParticipation1.sharedAt, + participantExternalId: campaignParticipation1.participantExternalId, + userId: campaignParticipation1.userId, + }, + ]); + }); + + it('should return the campaign participation with firstName and lastName from the organization learner', async function () { + // given + const campaignId = campaign1.id; + + // when + const participationResultDatas = + await campaignParticipationRepository.findProfilesCollectionResultDataByCampaignId(campaignId); + + // then + const attributes = participationResultDatas.map((participationResultData) => + _.pick(participationResultData, ['participantFirstName', 'participantLastName', 'division']), + ); + expect(attributes).to.deep.equal([ + { + participantFirstName: 'Hubert', + participantLastName: 'Parterre', + division: '6emeD', + }, + ]); + }); + + context('when a participant has several organization-learners for different organizations', function () { + let campaign; + let otherCampaign; + + beforeEach(async function () { + const organizationId = databaseBuilder.factory.buildOrganization().id; + const otherOrganizationId = databaseBuilder.factory.buildOrganization().id; + campaign = databaseBuilder.factory.buildCampaign({ organizationId }); + otherCampaign = databaseBuilder.factory.buildCampaign({ organizationId }); + const organizationLearnerId = databaseBuilder.factory.buildOrganizationLearner({ + organizationId, + division: '3eme', + }).id; + const otherOrganizationLearnerId = databaseBuilder.factory.buildOrganizationLearner({ + organizationId: otherOrganizationId, + division: '2nd', + }).id; + databaseBuilder.factory.buildCampaignParticipation({ campaignId: campaign.id, organizationLearnerId }); + databaseBuilder.factory.buildCampaignParticipation({ + campaignId: otherCampaign.id, + organizationLearnerId, + }); + databaseBuilder.factory.buildCampaignParticipation({ + campaignId: otherCampaign.id, + organizationLearnerId: otherOrganizationLearnerId, + }); + + await databaseBuilder.commit(); + }); + + it('should return the division of the school registration linked to the campaign', async function () { + const campaignParticipationInfos = + await campaignParticipationRepository.findProfilesCollectionResultDataByCampaignId(campaign.id); + + expect(campaignParticipationInfos.length).to.equal(1); + expect(campaignParticipationInfos[0].division).to.equal('3eme'); + }); + }); + + context('When the participant has improved its participation', function () { + it('should return all campaign-participation', async function () { + // given + const campaignId = databaseBuilder.factory.buildCampaign({ + type: CampaignTypes.PROFILES_COLLECTION, + multipleSendings: true, + }).id; + + const campaignParticipation = databaseBuilder.factory.buildCampaignParticipation({ + campaignId: campaignId, + createdAt: new Date('2016-01-15T14:50:35Z'), + isImproved: true, + }); + const improvedCampaignParticipation = databaseBuilder.factory.buildCampaignParticipation({ + campaignId: campaignId, + createdAt: new Date('2016-07-15T14:59:35Z'), + isImproved: false, + }); + await databaseBuilder.commit(); + + // when + const participationResultDatas = + await campaignParticipationRepository.findProfilesCollectionResultDataByCampaignId(campaignId); + + // then + expect(participationResultDatas).to.lengthOf(2); + expect(participationResultDatas[0].id).to.eq(improvedCampaignParticipation.id); + expect(participationResultDatas[1].id).to.eq(campaignParticipation.id); + }); + }); + + context('When sharedAt is null', function () { + it('Should return null as shared date', async function () { + // given + const campaign = databaseBuilder.factory.buildCampaign({ sharedAt: null }); + databaseBuilder.factory.buildCampaignParticipation({ + campaignId: campaign.id, + status: STARTED, + sharedAt: null, + }); + + await databaseBuilder.commit(); + + // when + const participationResultDatas = + await campaignParticipationRepository.findProfilesCollectionResultDataByCampaignId(campaign.id); + + // then + expect(participationResultDatas[0].sharedAt).to.equal(null); + }); + }); + }); }); diff --git a/api/tests/prescription/campaign/integration/domain/usecases/start-writing-campaign-profiles-collection-results-to-stream_test.js b/api/tests/prescription/campaign/integration/domain/usecases/start-writing-campaign-profiles-collection-results-to-stream_test.js index 9d71fd0ca9f..3f6d6025b32 100644 --- a/api/tests/prescription/campaign/integration/domain/usecases/start-writing-campaign-profiles-collection-results-to-stream_test.js +++ b/api/tests/prescription/campaign/integration/domain/usecases/start-writing-campaign-profiles-collection-results-to-stream_test.js @@ -7,7 +7,7 @@ import * as placementProfileService from '../../../../../../lib/domain/services/ import * as campaignRepository from '../../../../../../lib/infrastructure/repositories/campaign-repository.js'; import * as userRepository from '../../../../../../src/identity-access-management/infrastructure/repositories/user.repository.js'; import { startWritingCampaignProfilesCollectionResultsToStream } from '../../../../../../src/prescription/campaign/domain/usecases/start-writing-campaign-profiles-collection-results-to-stream.js'; -import * as campaignParticipationRepository from '../../../../../../src/prescription/campaign/infrastructure/repositories/campaign-participation-repository.js'; +import * as campaignParticipationRepository from '../../../../../../src/prescription/campaign-participation/infrastructure/repositories/campaign-participation-repository.js'; import { CampaignParticipationStatuses } from '../../../../../../src/prescription/shared/domain/constants.js'; import * as competenceRepository from '../../../../../../src/shared/infrastructure/repositories/competence-repository.js'; import * as organizationRepository from '../../../../../../src/shared/infrastructure/repositories/organization-repository.js'; diff --git a/api/tests/prescription/campaign/integration/infrastructure/repositories/campaign-participation-repository_test.js b/api/tests/prescription/campaign/integration/infrastructure/repositories/campaign-participation-repository_test.js deleted file mode 100644 index 488e222df49..00000000000 --- a/api/tests/prescription/campaign/integration/infrastructure/repositories/campaign-participation-repository_test.js +++ /dev/null @@ -1,203 +0,0 @@ -import _ from 'lodash'; - -import * as campaignParticipationRepository from '../../../../../../src/prescription/campaign/infrastructure/repositories/campaign-participation-repository.js'; -import { - CampaignParticipationStatuses, - CampaignTypes, -} from '../../../../../../src/prescription/shared/domain/constants.js'; -import { databaseBuilder, expect } from '../../../../../test-helper.js'; -const { STARTED } = CampaignParticipationStatuses; - -describe('Integration | Repository | Campaign Participation', function () { - describe('#findProfilesCollectionResultDataByCampaignId', function () { - let campaign1; - let campaign2; - let campaignParticipation1; - let organizationId; - - beforeEach(async function () { - organizationId = databaseBuilder.factory.buildOrganization().id; - campaign1 = databaseBuilder.factory.buildCampaign({ organizationId, type: CampaignTypes.PROFILES_COLLECTION }); - campaign2 = databaseBuilder.factory.buildCampaign({ organizationId, type: CampaignTypes.PROFILES_COLLECTION }); - - campaignParticipation1 = databaseBuilder.factory.buildCampaignParticipationWithOrganizationLearner( - { organizationId, firstName: 'Hubert', lastName: 'Parterre', division: '6emeD' }, - { - campaignId: campaign1.id, - createdAt: new Date('2017-03-15T14:59:35Z'), - }, - ); - databaseBuilder.factory.buildCampaignParticipation({ - campaignId: campaign2.id, - }); - await databaseBuilder.commit(); - }); - - it('should return the campaign-participation linked to the given campaign', async function () { - // given - const campaignId = campaign1.id; - - // when - const participationResultDatas = - await campaignParticipationRepository.findProfilesCollectionResultDataByCampaignId(campaignId); - - // then - const attributes = participationResultDatas.map((participationResultData) => - _.pick(participationResultData, ['id', 'isShared', 'sharedAt', 'participantExternalId', 'userId']), - ); - expect(attributes).to.deep.equal([ - { - id: campaignParticipation1.id, - isShared: true, - sharedAt: campaignParticipation1.sharedAt, - participantExternalId: campaignParticipation1.participantExternalId, - userId: campaignParticipation1.userId, - }, - ]); - }); - - it('should not return the deleted campaign-participation linked to the given campaign', async function () { - // given - const campaignId = campaign1.id; - databaseBuilder.factory.buildCampaignParticipationWithOrganizationLearner( - { organizationId, firstName: 'Piere', lastName: 'Pi air', division: '6emeD' }, - { - campaignId: campaign1.id, - createdAt: new Date('2017-03-15T14:59:35Z'), - deletedAt: new Date(), - }, - ); - await databaseBuilder.commit(); - - // when - const participationResultDatas = - await campaignParticipationRepository.findProfilesCollectionResultDataByCampaignId(campaignId); - - // then - const attributes = participationResultDatas.map((participationResultData) => - _.pick(participationResultData, ['id', 'isShared', 'sharedAt', 'participantExternalId', 'userId']), - ); - expect(attributes).to.deep.equal([ - { - id: campaignParticipation1.id, - isShared: true, - sharedAt: campaignParticipation1.sharedAt, - participantExternalId: campaignParticipation1.participantExternalId, - userId: campaignParticipation1.userId, - }, - ]); - }); - - it('should return the campaign participation with firstName and lastName from the organization learner', async function () { - // given - const campaignId = campaign1.id; - - // when - const participationResultDatas = - await campaignParticipationRepository.findProfilesCollectionResultDataByCampaignId(campaignId); - - // then - const attributes = participationResultDatas.map((participationResultData) => - _.pick(participationResultData, ['participantFirstName', 'participantLastName', 'division']), - ); - expect(attributes).to.deep.equal([ - { - participantFirstName: 'Hubert', - participantLastName: 'Parterre', - division: '6emeD', - }, - ]); - }); - - context('when a participant has several organization-learners for different organizations', function () { - let campaign; - let otherCampaign; - - beforeEach(async function () { - const organizationId = databaseBuilder.factory.buildOrganization().id; - const otherOrganizationId = databaseBuilder.factory.buildOrganization().id; - campaign = databaseBuilder.factory.buildCampaign({ organizationId }); - otherCampaign = databaseBuilder.factory.buildCampaign({ organizationId }); - const organizationLearnerId = databaseBuilder.factory.buildOrganizationLearner({ - organizationId, - division: '3eme', - }).id; - const otherOrganizationLearnerId = databaseBuilder.factory.buildOrganizationLearner({ - organizationId: otherOrganizationId, - division: '2nd', - }).id; - databaseBuilder.factory.buildCampaignParticipation({ campaignId: campaign.id, organizationLearnerId }); - databaseBuilder.factory.buildCampaignParticipation({ - campaignId: otherCampaign.id, - organizationLearnerId, - }); - databaseBuilder.factory.buildCampaignParticipation({ - campaignId: otherCampaign.id, - organizationLearnerId: otherOrganizationLearnerId, - }); - - await databaseBuilder.commit(); - }); - - it('should return the division of the school registration linked to the campaign', async function () { - const campaignParticipationInfos = - await campaignParticipationRepository.findProfilesCollectionResultDataByCampaignId(campaign.id); - - expect(campaignParticipationInfos.length).to.equal(1); - expect(campaignParticipationInfos[0].division).to.equal('3eme'); - }); - }); - - context('When the participant has improved its participation', function () { - it('should return all campaign-participation', async function () { - // given - const campaignId = databaseBuilder.factory.buildCampaign({ - type: CampaignTypes.PROFILES_COLLECTION, - multipleSendings: true, - }).id; - - const campaignParticipation = databaseBuilder.factory.buildCampaignParticipation({ - campaignId: campaignId, - createdAt: new Date('2016-01-15T14:50:35Z'), - isImproved: true, - }); - const improvedCampaignParticipation = databaseBuilder.factory.buildCampaignParticipation({ - campaignId: campaignId, - createdAt: new Date('2016-07-15T14:59:35Z'), - isImproved: false, - }); - await databaseBuilder.commit(); - - // when - const participationResultDatas = - await campaignParticipationRepository.findProfilesCollectionResultDataByCampaignId(campaignId); - - // then - expect(participationResultDatas).to.lengthOf(2); - expect(participationResultDatas[0].id).to.eq(improvedCampaignParticipation.id); - expect(participationResultDatas[1].id).to.eq(campaignParticipation.id); - }); - }); - - context('When sharedAt is null', function () { - it('Should return null as shared date', async function () { - // given - const campaign = databaseBuilder.factory.buildCampaign({ sharedAt: null }); - databaseBuilder.factory.buildCampaignParticipation({ - campaignId: campaign.id, - status: STARTED, - sharedAt: null, - }); - - await databaseBuilder.commit(); - - // when - const participationResultDatas = - await campaignParticipationRepository.findProfilesCollectionResultDataByCampaignId(campaign.id); - - // then - expect(participationResultDatas[0].sharedAt).to.equal(null); - }); - }); - }); -}); From 955072f50f3c053a4e1011d64460bb4fad19664b Mon Sep 17 00:00:00 2001 From: Alexandre Monney Date: Thu, 27 Jun 2024 16:32:27 +0200 Subject: [PATCH 09/12] feat(api): add getter isDeleted on Campaign model --- .../campaign/domain/models/Campaign.js | 4 ++++ .../unit/domain/models/Campaign_test.js | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/api/src/prescription/campaign/domain/models/Campaign.js b/api/src/prescription/campaign/domain/models/Campaign.js index 91d3ba33c54..a184ad5c9e8 100644 --- a/api/src/prescription/campaign/domain/models/Campaign.js +++ b/api/src/prescription/campaign/domain/models/Campaign.js @@ -76,6 +76,10 @@ class Campaign { return Boolean(this.archivedAt); } + get isDeleted() { + return Boolean(this.deletedAt); + } + delete(userId) { if (this.deletedAt) { throw new DeletedCampaignError(); diff --git a/api/tests/prescription/campaign/unit/domain/models/Campaign_test.js b/api/tests/prescription/campaign/unit/domain/models/Campaign_test.js index 13c6f499465..5b520448fba 100644 --- a/api/tests/prescription/campaign/unit/domain/models/Campaign_test.js +++ b/api/tests/prescription/campaign/unit/domain/models/Campaign_test.js @@ -29,6 +29,26 @@ describe('Campaign', function () { clock.restore(); }); + describe('#isDeleted', function () { + it('returns true', function () { + campaign = new Campaign({ + deletedAt: new Date(), + deletedBy: 1, + }); + + expect(campaign.isDeleted).to.be.true; + }); + + it('returns false', function () { + campaign = new Campaign({ + deletedAt: null, + deletedBy: null, + }); + + expect(campaign.isDeleted).to.be.false; + }); + }); + describe('#delete', function () { it('deletes the campaign', function () { const campaign = new Campaign({ id: 1, code: 'ABC123' }); From 0e1216138fbd52392843f399d4b184e0f81cdaea Mon Sep 17 00:00:00 2001 From: Alexandre Monney Date: Thu, 27 Jun 2024 16:33:39 +0200 Subject: [PATCH 10/12] feat(api): create CampaignsDestructor aggregate --- .../domain/models/CampaignsDestructor.js | 59 +++++++++++++ .../domain/models/CampaignsDestructor_test.js | 83 +++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 api/src/prescription/campaign/domain/models/CampaignsDestructor.js create mode 100644 api/tests/prescription/campaign/unit/domain/models/CampaignsDestructor_test.js diff --git a/api/src/prescription/campaign/domain/models/CampaignsDestructor.js b/api/src/prescription/campaign/domain/models/CampaignsDestructor.js new file mode 100644 index 00000000000..e6c94ff47fe --- /dev/null +++ b/api/src/prescription/campaign/domain/models/CampaignsDestructor.js @@ -0,0 +1,59 @@ +import { ObjectValidationError } from '../../../../../lib/domain/errors.js'; + +/** + * @typedef {import ('./Campaign.js').Campaign} Campaign + * @typedef {import ('../../../campaign-participation/domain/models/CampaignParticipation.js').CampaignParticipation} CampaignParticipation + * @typedef {import ('../read-models/OrganizationMembership.js').OrganizationMembership} OrganizationMembership + */ + +class CampaignsDestructor { + #campaignsToDelete; + #campaignParticipationsToDelete; + #userId; + #organizationId; + #membership; + + /** + * @param {Object} params + * @param {Array} params.campaignsToDelete - campaigns object to be deleted + * @param {Array} params.campaignParticipationsToDelete - campaigns participations object to be deleted + * @param {number} params.userId - userId for deletedBy + * @param {number} params.organizationId - organizationId to check if campaigns belongs to given organizationId + * @param {OrganizationMembership} params.membership - class with property isAdmin to check is user is admin in organization or not + */ + constructor({ campaignsToDelete, campaignParticipationsToDelete, userId, organizationId, membership }) { + this.#campaignsToDelete = campaignsToDelete; + this.#campaignParticipationsToDelete = campaignParticipationsToDelete; + this.#userId = userId; + this.#organizationId = organizationId; + this.#membership = membership; + this.#validate(); + } + + #validate() { + const isUserOwnerOfAllCampaigns = this.#campaignsToDelete.every((campaign) => campaign.ownerId === this.#userId); + const isAllCampaignsBelongsToOrganization = this.#campaignsToDelete.every( + (campaign) => campaign.organizationId === this.#organizationId, + ); + + if (!isAllCampaignsBelongsToOrganization) + throw new ObjectValidationError('Some campaigns does not belong to organization.'); + if (!this.#membership.isAdmin && !isUserOwnerOfAllCampaigns) + throw new ObjectValidationError('User does not have right to delete some campaigns.'); + } + + delete() { + this.#campaignParticipationsToDelete.forEach((campaignParticipation) => campaignParticipation.delete(this.#userId)); + this.#campaignsToDelete.forEach((campaign) => campaign.delete(this.#userId)); + } + + get campaignParticipations() { + return this.#campaignParticipationsToDelete; + } + + get campaigns() { + return this.#campaignsToDelete; + } +} + +export { CampaignsDestructor }; diff --git a/api/tests/prescription/campaign/unit/domain/models/CampaignsDestructor_test.js b/api/tests/prescription/campaign/unit/domain/models/CampaignsDestructor_test.js new file mode 100644 index 00000000000..aeb99dc6632 --- /dev/null +++ b/api/tests/prescription/campaign/unit/domain/models/CampaignsDestructor_test.js @@ -0,0 +1,83 @@ +import { ObjectValidationError } from '../../../../../../lib/domain/errors.js'; +import { Campaign } from '../../../../../../src/prescription/campaign/domain/models/Campaign.js'; +import { CampaignsDestructor } from '../../../../../../src/prescription/campaign/domain/models/CampaignsDestructor.js'; +import { OrganizationMembership } from '../../../../../../src/prescription/campaign/domain/read-models/OrganizationMembership.js'; +import { CampaignParticipation } from '../../../../../../src/prescription/campaign-participation/domain/models/CampaignParticipation.js'; +import { expect } from '../../../../../test-helper.js'; + +describe('CampaignsDestructor', function () { + describe('when datas are invalid', function () { + it('throws an error when some campaigns does not belong to organization', function () { + try { + new CampaignsDestructor({ + organizationId: 1, + campaignsToDelete: [new Campaign({ organizationId: 2 })], + }); + } catch (error) { + expect(error).to.be.instanceOf(ObjectValidationError); + expect(error.message).to.equal('Some campaigns does not belong to organization.'); + } + }); + + it('throws an error when user is not owner', function () { + try { + new CampaignsDestructor({ + membership: new OrganizationMembership({ isAdmin: false }), + userId: 1, + campaignsToDelete: [new Campaign({ ownerId: 2 })], + }); + } catch (error) { + expect(error).to.be.instanceOf(ObjectValidationError); + expect(error.message).to.equal('User does not have right to delete some campaigns.'); + } + }); + }); + + describe('#campaignParticipations', function () { + it('returns campaign participations', function () { + const participations = [new CampaignParticipation()]; + + const destructor = new CampaignsDestructor({ + membership: new OrganizationMembership({ isAdmin: true }), + campaignsToDelete: [], + campaignParticipationsToDelete: participations, + }); + + expect(destructor.campaignParticipations).to.deep.equal(participations); + }); + }); + + describe('#campaigns', function () { + it('returns campaigns', function () { + const campaigns = [new Campaign()]; + + const destructor = new CampaignsDestructor({ + membership: new OrganizationMembership({ isAdmin: true }), + campaignsToDelete: campaigns, + }); + + expect(destructor.campaigns).to.deep.equal(campaigns); + }); + }); + + describe('#delete', function () { + it('deletes campaigns and campaign participations', function () { + const participations = [new CampaignParticipation()]; + const organizationId = 7; + const campaigns = [new Campaign({ organizationId })]; + + const destructor = new CampaignsDestructor({ + userId: 1, + organizationId, + membership: new OrganizationMembership({ isAdmin: true }), + campaignsToDelete: campaigns, + campaignParticipationsToDelete: participations, + }); + + destructor.delete(); + + expect(destructor.campaigns[0].isDeleted).to.be.true; + expect(destructor.campaignParticipations[0].isDeleted).to.be.true; + }); + }); +}); From 09afccaccd012cf2d4eeea0ca5eee699b14a2ee9 Mon Sep 17 00:00:00 2001 From: Alexandre Monney Date: Thu, 27 Jun 2024 16:34:09 +0200 Subject: [PATCH 11/12] feat(api): create delete campaigns usecase --- .../domain/usecases/delete-campaigns.js | 28 +++++++++ .../campaign/domain/usecases/index.js | 6 +- .../domain/usecases/delete-campaigns_test.js | 61 +++++++++++++++++++ 3 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 api/src/prescription/campaign/domain/usecases/delete-campaigns.js create mode 100644 api/tests/prescription/campaign/integration/domain/usecases/delete-campaigns_test.js diff --git a/api/src/prescription/campaign/domain/usecases/delete-campaigns.js b/api/src/prescription/campaign/domain/usecases/delete-campaigns.js new file mode 100644 index 00000000000..dd7f0ef7645 --- /dev/null +++ b/api/src/prescription/campaign/domain/usecases/delete-campaigns.js @@ -0,0 +1,28 @@ +import { CampaignsDestructor } from '../models/CampaignsDestructor.js'; + +const deleteCampaigns = async ({ + userId, + organizationId, + campaignIds, + organizationMembershipRepository, + campaignAdministrationRepository, + campaignParticipationRepository, +}) => { + const membership = await organizationMembershipRepository.getByUserIdAndOrganizationId({ userId, organizationId }); + const campaignsToDelete = await campaignAdministrationRepository.getByIds(campaignIds); + const campaignParticipationsToDelete = await campaignParticipationRepository.getByCampaignIds(campaignIds); + + const campaignDestructor = new CampaignsDestructor({ + campaignsToDelete, + campaignParticipationsToDelete, + userId, + organizationId, + membership, + }); + campaignDestructor.delete(); + + await campaignParticipationRepository.batchUpdate(campaignParticipationsToDelete); + await campaignAdministrationRepository.batchUpdate(campaignsToDelete); +}; + +export { deleteCampaigns }; diff --git a/api/src/prescription/campaign/domain/usecases/index.js b/api/src/prescription/campaign/domain/usecases/index.js index bd64624efed..839907df546 100644 --- a/api/src/prescription/campaign/domain/usecases/index.js +++ b/api/src/prescription/campaign/domain/usecases/index.js @@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url'; import * as placementProfileService from '../../../../../lib/domain/services/placement-profile-service.js'; import * as badgeAcquisitionRepository from '../../../../../lib/infrastructure/repositories/badge-acquisition-repository.js'; import * as campaignRepository from '../../../../../lib/infrastructure/repositories/campaign-repository.js'; -import { repositories } from '../../../../../lib/infrastructure/repositories/index.js'; +import { repositories as libRepositories } from '../../../../../lib/infrastructure/repositories/index.js'; import * as knowledgeElementRepository from '../../../../../lib/infrastructure/repositories/knowledge-element-repository.js'; import * as knowledgeElementSnapshotRepository from '../../../../../lib/infrastructure/repositories/knowledge-element-snapshot-repository.js'; import * as learningContentRepository from '../../../../../lib/infrastructure/repositories/learning-content-repository.js'; @@ -33,6 +33,7 @@ import * as campaignReportRepository from '../../infrastructure/repositories/cam import * as campaignToJoinRepository from '../../infrastructure/repositories/campaign-to-join-repository.js'; import * as divisionRepository from '../../infrastructure/repositories/division-repository.js'; import * as groupRepository from '../../infrastructure/repositories/group-repository.js'; +import { repositories } from '../../infrastructure/repositories/index.js'; import * as targetProfileRepository from '../../infrastructure/repositories/target-profile-repository.js'; import * as campaignCsvExportService from '../services/campaign-csv-export-service.js'; import * as campaignUpdateValidator from '../validators/campaign-update-validator.js'; @@ -51,6 +52,7 @@ const dependencies = { campaignProfilesCollectionParticipationSummaryRepository, campaignParticipationInfoRepository, campaignReportRepository, + organizationMembershipRepository: repositories.organizationMembershipRepository, campaignAssessmentParticipationResultListRepository, codeGenerator, campaignUpdateValidator, @@ -69,7 +71,7 @@ const dependencies = { campaignCollectiveResultRepository, campaignToJoinRepository, campaignParticipationsStatsRepository, - tutorialRepository: repositories.tutorialRepository, + tutorialRepository: libRepositories.tutorialRepository, }; const path = dirname(fileURLToPath(import.meta.url)); diff --git a/api/tests/prescription/campaign/integration/domain/usecases/delete-campaigns_test.js b/api/tests/prescription/campaign/integration/domain/usecases/delete-campaigns_test.js new file mode 100644 index 00000000000..1f7d50cbff5 --- /dev/null +++ b/api/tests/prescription/campaign/integration/domain/usecases/delete-campaigns_test.js @@ -0,0 +1,61 @@ +import { usecases } from '../../../../../../src/prescription/campaign/domain/usecases/index.js'; +import * as campaignAdministrationRepository from '../../../../../../src/prescription/campaign/infrastructure/repositories/campaign-administration-repository.js'; +import * as campaignParticipationRepository from '../../../../../../src/prescription/campaign-participation/infrastructure/repositories/campaign-participation-repository.js'; +import { databaseBuilder, expect, sinon } from '../../../../../test-helper.js'; + +describe('Integration | UseCases | delete-campaign', function () { + describe('success case', function () { + let clock; + let now; + + beforeEach(function () { + now = new Date('1992-07-07'); + clock = sinon.useFakeTimers({ now, toFake: ['Date'] }); + }); + + afterEach(async function () { + clock.restore(); + }); + + it('should not throw', async function () { + // given + const userId = databaseBuilder.factory.buildUser().id; + const organizationId = databaseBuilder.factory.buildOrganization().id; + databaseBuilder.factory.buildMembership({ userId, organizationId, organizationRole: 'MEMBER' }); + const campaignId = databaseBuilder.factory.buildCampaign({ ownerId: userId, organizationId }).id; + databaseBuilder.factory.buildCampaignParticipation({ campaignId }); + + await databaseBuilder.commit(); + let error; + try { + await usecases.deleteCampaigns({ userId, organizationId, campaignIds: [campaignId] }); + } catch (e) { + error = e; + } + + // when & then + expect(error).to.be.undefined; + }); + + it('should delete campaign for given id and participation associated', async function () { + // given + const userId = databaseBuilder.factory.buildUser().id; + const organizationId = databaseBuilder.factory.buildOrganization().id; + databaseBuilder.factory.buildMembership({ userId, organizationId, organizationRole: 'MEMBER' }); + const campaignId = databaseBuilder.factory.buildCampaign({ ownerId: userId, organizationId }).id; + const campaignParticipationId = databaseBuilder.factory.buildCampaignParticipation({ campaignId }).id; + + await databaseBuilder.commit(); + + await usecases.deleteCampaigns({ userId, organizationId, campaignIds: [campaignId] }); + const updatedCampaign = await campaignAdministrationRepository.get(campaignId); + const updatedCampaignParticipation = await campaignParticipationRepository.get(campaignParticipationId); + + // when & then + expect(updatedCampaign.deletedAt).to.deep.equal(now); + expect(updatedCampaign.deletedBy).to.equal(userId); + expect(updatedCampaignParticipation.deletedAt).to.deep.equal(now); + expect(updatedCampaignParticipation.deletedBy).to.equal(userId); + }); + }); +}); From a7aeb5440a18a9410a3a3893da557328adda326b Mon Sep 17 00:00:00 2001 From: Alexandre Monney Date: Thu, 27 Jun 2024 17:31:39 +0200 Subject: [PATCH 12/12] feat(api): create route and deleteCampaigns controller method --- .../campaign-administration-route.js | 28 +++++++ .../campaign-adminstration-controller.js | 12 +++ .../campaign-administration-route_test.js | 75 +++++++++++++++++++ 3 files changed, 115 insertions(+) diff --git a/api/src/prescription/campaign/application/campaign-administration-route.js b/api/src/prescription/campaign/application/campaign-administration-route.js index 9a891fe1a85..e0943dfaaad 100644 --- a/api/src/prescription/campaign/application/campaign-administration-route.js +++ b/api/src/prescription/campaign/application/campaign-administration-route.js @@ -291,6 +291,34 @@ const register = async function (server) { ], }, }, + { + method: 'DELETE', + path: '/api/organizations/{organizationId}/campaigns', + config: { + pre: [ + { + method: securityPreHandlers.checkUserBelongsToOrganization, + }, + ], + validate: { + params: Joi.object({ + organizationId: identifiersType.organizationId, + }), + payload: Joi.object({ + data: Joi.array() + .required() + .items(Joi.object({ type: Joi.string().required(), id: identifiersType.campaignId })), + }), + }, + handler: campaignAdministrationController.deleteCampaigns, + notes: [ + '- **Cette route est restreinte aux utilisateurs authentifiés**\n' + + "- Suppression d'une ou plusieurs campagne(s)\n" + + '- L‘utilisateur doit appartenir à l‘organisation', + ], + tags: ['api', 'orga', 'campaign'], + }, + }, ]); }; diff --git a/api/src/prescription/campaign/application/campaign-adminstration-controller.js b/api/src/prescription/campaign/application/campaign-adminstration-controller.js index 33d455a9170..6f8146a51ac 100644 --- a/api/src/prescription/campaign/application/campaign-adminstration-controller.js +++ b/api/src/prescription/campaign/application/campaign-adminstration-controller.js @@ -5,6 +5,7 @@ import * as csvSerializer from '../../../../lib/infrastructure/serializers/csv/c import { usecases } from '../../../../src/prescription/campaign/domain/usecases/index.js'; import * as queryParamsUtils from '../../../shared/infrastructure/utils/query-params-utils.js'; import * as requestResponseUtils from '../../../shared/infrastructure/utils/request-response-utils.js'; +import { extractUserIdFromRequest } from '../../../shared/infrastructure/utils/request-response-utils.js'; import * as csvCampaignsIdsParser from '../infrastructure/serializers/csv/csv-campaigns-ids-parser.js'; import * as campaignManagementSerializer from '../infrastructure/serializers/jsonapi/campaign-management-serializer.js'; import * as campaignReportSerializer from '../infrastructure/serializers/jsonapi/campaign-report-serializer.js'; @@ -133,6 +134,16 @@ const findPaginatedCampaignManagements = async function ( return dependencies.campaignManagementSerializer.serialize(campaigns, meta); }; +const deleteCampaigns = async function (request, h) { + const userId = extractUserIdFromRequest(request); + const { organizationId } = request.params; + const campaignIds = request.deserializedPayload.map(({ id }) => id); + + await usecases.deleteCampaigns({ userId, organizationId, campaignIds }); + + return h.response(null).code(204); +}; + const campaignAdministrationController = { save, update, @@ -144,6 +155,7 @@ const campaignAdministrationController = { archiveCampaign, archiveCampaigns, unarchiveCampaign, + deleteCampaigns, }; export { campaignAdministrationController }; diff --git a/api/tests/prescription/campaign/integration/application/campaign-administration-route_test.js b/api/tests/prescription/campaign/integration/application/campaign-administration-route_test.js index de44e0332f8..84917499f72 100644 --- a/api/tests/prescription/campaign/integration/application/campaign-administration-route_test.js +++ b/api/tests/prescription/campaign/integration/application/campaign-administration-route_test.js @@ -1,5 +1,9 @@ +import { ObjectValidationError } from '../../../../../lib/domain/errors.js'; import * as moduleUnderTest from '../../../../../src/prescription/campaign/application/campaign-administration-route.js'; import { campaignAdministrationController } from '../../../../../src/prescription/campaign/application/campaign-adminstration-controller.js'; +import { DeletedCampaignError } from '../../../../../src/prescription/campaign/domain/errors.js'; +import { usecases } from '../../../../../src/prescription/campaign/domain/usecases/index.js'; +import { securityPreHandlers } from '../../../../../src/shared/application/security-pre-handlers.js'; import { databaseBuilder, expect, @@ -83,4 +87,75 @@ describe('Integration | Application | Route | campaign administration router', f expect(response.statusCode).to.equal(403); }); }); + + describe('DELETE /api/organizations/{organizationId}/campaigns', function () { + it('return a 204 status code in success case', async function () { + const userId = 1; + const organizationId = 2; + const campaignIds = [1]; + sinon.stub(usecases, 'deleteCampaigns'); + sinon.stub(securityPreHandlers, 'checkUserBelongsToOrganization').callsFake((request, h) => h.response(true)); + + // given + const method = 'DELETE'; + const url = '/api/organizations/2/campaigns'; + const payload = { + data: [{ type: 'campaigns', id: campaignIds[0] }], + }; + httpTestServer = new HttpTestServer(); + httpTestServer.setupDeserialization(); + await httpTestServer.register(moduleUnderTest); + + const headers = { + authorization: generateValidRequestAuthorizationHeader(userId), + }; + + // when + const response = await httpTestServer.request(method, url, payload, null, headers); + // then + expect(securityPreHandlers.checkUserBelongsToOrganization).to.have.been.calledOnce; + expect(usecases.deleteCampaigns).to.have.been.calledWithExactly({ userId, organizationId, campaignIds }); + expect(response.statusCode).to.equal(204); + }); + + it('return a 422 status code if an ObjectValidationError is thrown', async function () { + sinon.stub(usecases, 'deleteCampaigns'); + sinon.stub(securityPreHandlers, 'checkUserBelongsToOrganization').callsFake((request, h) => h.response(true)); + usecases.deleteCampaigns.rejects(new ObjectValidationError()); + // given + const method = 'DELETE'; + const url = '/api/organizations/2/campaigns'; + const payload = { + data: [], + }; + httpTestServer = new HttpTestServer(); + httpTestServer.setupDeserialization(); + await httpTestServer.register(moduleUnderTest); + + // when + const response = await httpTestServer.request(method, url, payload); + // then + expect(response.statusCode).to.equal(422); + }); + + it('return a 412 status code if an DeletedCampaignError is thrown', async function () { + sinon.stub(usecases, 'deleteCampaigns'); + sinon.stub(securityPreHandlers, 'checkUserBelongsToOrganization').callsFake((request, h) => h.response(true)); + usecases.deleteCampaigns.rejects(new DeletedCampaignError()); + // given + const method = 'DELETE'; + const url = '/api/organizations/2/campaigns'; + const payload = { + data: [], + }; + httpTestServer = new HttpTestServer(); + httpTestServer.setupDeserialization(); + await httpTestServer.register(moduleUnderTest); + + // when + const response = await httpTestServer.request(method, url, payload); + // then + expect(response.statusCode).to.equal(412); + }); + }); });