Skip to content

Commit

Permalink
[FEATURE] Rendre obligatoire le champs propriétaire lors de la créati…
Browse files Browse the repository at this point in the history
…on de campagne en masse sur Pix Admin (PIX-12577)

 #10240
  • Loading branch information
pix-service-auto-merge authored Oct 4, 2024
2 parents e0b1148 + bca28a6 commit 5175afc
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 51 deletions.
23 changes: 12 additions & 11 deletions api/src/prescription/campaign/domain/usecases/create-campaigns.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,16 @@ import { CampaignTypes } from '../../../shared/domain/constants.js';

const createCampaigns = async function ({
campaignsToCreate,
membershipRepository,
campaignAdministrationRepository,
campaignCreatorRepository,
codeGenerator,
userRepository,
organizationRepository,
}) {
const enrichedCampaignsData = await Promise.all(
campaignsToCreate.map(async (campaign) => {
let ownerId;
if (campaign.ownerId) {
ownerId = campaign.ownerId;
} else {
const [administrator] = await membershipRepository.findAdminsByOrganizationId({
organizationId: campaign.organizationId,
});
ownerId = administrator.user.id;
}
await _checkIfOwnerIsExistingUser(userRepository, campaign.ownerId);
await _checkIfOrganizationExists(organizationRepository, campaign.organizationId);

const generatedCampaignCode = await codeGenerator.generate(campaignAdministrationRepository);
const campaignCreator = await campaignCreatorRepository.get(campaign.organizationId);
Expand All @@ -26,12 +20,19 @@ const createCampaigns = async function ({
...campaign,
type: CampaignTypes.ASSESSMENT,
code: generatedCampaignCode,
ownerId,
});
}),
);

return campaignAdministrationRepository.save(enrichedCampaignsData);
};

const _checkIfOwnerIsExistingUser = async function (userRepository, userId) {
await userRepository.get(userId);
};

const _checkIfOrganizationExists = async function (organizationRepository, organizationId) {
await organizationRepository.get(organizationId);
};

export { createCampaigns };
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ const requiredFieldNamesForCampaignsImport = [
"Identifiant de l'organisation*",
'Nom de la campagne*',
'Identifiant du profil cible*',
'Identifiant du propriétaire*',
'Identifiant du créateur*',
];

Expand All @@ -206,7 +207,7 @@ async function parseForCampaignsImport(cleanedData, { parseCsvData } = csvHelper
"Identifiant de l'organisation*",
'Identifiant du profil cible*',
'Identifiant du créateur*',
'Identifiant du propriétaire',
'Identifiant du propriétaire*',
].includes(columnName)
) {
value = parseInt(value, 10);
Expand All @@ -231,7 +232,7 @@ async function parseForCampaignsImport(cleanedData, { parseCsvData } = csvHelper
title: data['Titre du parcours'],
customLandingPageText: data['Descriptif du parcours'],
multipleSendings: data['Envoi multiple'].toLowerCase() === 'oui' ? true : false,
ownerId: data['Identifiant du propriétaire'] || null,
ownerId: data['Identifiant du propriétaire*'],
customResultPageText: data['Texte de la page de fin de parcours'] || null,
customResultPageButtonText: data['Texte du bouton de la page de fin de parcours'] || null,
customResultPageButtonUrl: data['URL du bouton de la page de fin de parcours'] || null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,9 @@ describe('Acceptance | API | campaign-administration-route', function () {
const targetProfileId = databaseBuilder.factory.buildTargetProfile({ ownerOrganizationId: organizationId }).id;
await databaseBuilder.commit();

const buffer = `Identifiant de l'organisation*;Nom de la campagne*;Identifiant du profil cible*;Libellé de l'identifiant externe;Identifiant du créateur*;Titre du parcours;Descriptif du parcours;Envoi multiple
${organizationId};Parcours importé par CSV;${targetProfileId};numéro d'étudiant;${userId};;;non
${organizationId};Autre parcours importé par CSV;${targetProfileId};numéro d'étudiant;${userId};Titre;Superbe descriptif de parcours;oui`;
const buffer = `Identifiant de l'organisation*;Nom de la campagne*;Identifiant du profil cible*;Libellé de l'identifiant externe;Identifiant du créateur*;Titre du parcours;Descriptif du parcours;Envoi multiple;Identifiant du propriétaire*
${organizationId};Parcours importé par CSV;${targetProfileId};numéro d'étudiant;${userId};;;non;${userId};
${organizationId};Autre parcours importé par CSV;${targetProfileId};numéro d'étudiant;${userId};Titre;Superbe descriptif de parcours;oui;${userId};`;
const options = {
method: 'POST',
url: '/api/admin/campaigns',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,44 @@
import { createCampaigns } from '../../../../../../src/prescription/campaign/domain/usecases/create-campaigns.js';
import { CampaignTypes } from '../../../../../../src/prescription/shared/domain/constants.js';
import { domainBuilder, expect, sinon } from '../../../../../test-helper.js';
import { NotFoundError, UserNotFoundError } from '../../../../../../src/shared/domain/errors.js';
import { catchErr, domainBuilder, expect, sinon } from '../../../../../test-helper.js';

describe('Unit | UseCase | campaign-administration | create-campaigns', function () {
it('should create campaigns', async function () {
const campaignAdministrationRepository = {
let codeGeneratorStub;
let campaignsToCreate;
let campaignAdministrationRepositoryStub;
let campaignCreatorRepositoryStub;
let userRepositoryStub;
let organizationRepositoryStub;
let createdCampaignsSymbol;

beforeEach(function () {
campaignAdministrationRepositoryStub = {
save: sinon.stub(),
};
userRepositoryStub = {
get: sinon.stub(),
};
organizationRepositoryStub = {
get: sinon.stub(),
};
const code1 = Symbol('code1');
const code2 = Symbol('code2');
const administrator = domainBuilder.buildUser();
const someoneId = 454756;
const otherOrganization = domainBuilder.buildOrganization({ id: 3 });
const organization = domainBuilder.buildOrganization({ id: 4 });
const administratorMembership = domainBuilder.buildMembership({
user: administrator,
organization: otherOrganization,
organizationRole: 'ADMIN',
});

const codeGeneratorStub = {
codeGeneratorStub = {
generate: sinon
.stub()
.withArgs(campaignAdministrationRepository)
.withArgs(campaignAdministrationRepositoryStub)
.onFirstCall()
.resolves(code1)
.onSecondCall()
.resolves(code2),
};

const campaignsToCreate = [
campaignsToCreate = [
{
organizationId: organization.id,
name: 'My Campaign',
Expand All @@ -42,6 +51,7 @@ describe('Unit | UseCase | campaign-administration | create-campaigns', function
name: 'My other Campaign',
targetProfileId: 3,
creatorId: 1,
ownerId: someoneId,
},
];

Expand All @@ -54,7 +64,6 @@ describe('Unit | UseCase | campaign-administration | create-campaigns', function
{
...campaignsToCreate[1],
type: 'ASSESSMENT',
ownerId: administrator.id,
code: code2,
},
];
Expand All @@ -67,34 +76,73 @@ describe('Unit | UseCase | campaign-administration | create-campaigns', function
.withArgs({ ...campaignsToCreate[0], type: CampaignTypes.ASSESSMENT, code: code1, ownerId: someoneId })
.resolves(campaignsWithAllData[0]);
campaignCreatorPOJO.createCampaign
.withArgs({ ...campaignsToCreate[1], type: CampaignTypes.ASSESSMENT, code: code2, ownerId: administrator.id })
.withArgs({ ...campaignsToCreate[1], type: CampaignTypes.ASSESSMENT, code: code2, ownerId: someoneId })
.resolves(campaignsWithAllData[1]);

const campaignCreatorRepositoryStub = {
campaignCreatorRepositoryStub = {
get: sinon.stub(),
};

campaignCreatorRepositoryStub.get.withArgs(campaignsToCreate[0].organizationId).resolves(campaignCreatorPOJO);
campaignCreatorRepositoryStub.get.withArgs(campaignsToCreate[1].organizationId).resolves(campaignCreatorPOJO);

const createdCampaignsSymbol = Symbol('');
campaignAdministrationRepository.save.withArgs(campaignsWithAllData).resolves(createdCampaignsSymbol);
createdCampaignsSymbol = Symbol('');
campaignAdministrationRepositoryStub.save.withArgs(campaignsWithAllData).resolves(createdCampaignsSymbol);
});

const membershipRepository = {
findAdminsByOrganizationId: sinon.stub(),
};
membershipRepository.findAdminsByOrganizationId
.withArgs({ organizationId: otherOrganization.id })
.resolves([administratorMembership]);

const result = await createCampaigns({
campaignsToCreate,
membershipRepository,
campaignAdministrationRepository,
codeGenerator: codeGeneratorStub,
campaignCreatorRepository: campaignCreatorRepositoryStub,
describe('success case', function () {
it('should create campaigns', async function () {
organizationRepositoryStub.get.withArgs(campaignsToCreate[0].organizationId).resolves();
userRepositoryStub.get.withArgs(campaignsToCreate[0].ownerId).resolves();
organizationRepositoryStub.get.withArgs(campaignsToCreate[1].organizationId).resolves();
userRepositoryStub.get.withArgs(campaignsToCreate[1].ownerId).resolves();

const result = await createCampaigns({
campaignsToCreate,
campaignAdministrationRepository: campaignAdministrationRepositoryStub,
codeGenerator: codeGeneratorStub,
campaignCreatorRepository: campaignCreatorRepositoryStub,
userRepository: userRepositoryStub,
organizationRepository: organizationRepositoryStub,
});

expect(result).to.equal(createdCampaignsSymbol);
});
});

describe('errors case', function () {
it('should throw error if ownerId does not exist', async function () {
organizationRepositoryStub.get.withArgs(campaignsToCreate[0].organizationId).resolves();
userRepositoryStub.get.withArgs(campaignsToCreate[0].ownerId).rejects(new UserNotFoundError());

expect(result).to.equal(createdCampaignsSymbol);
const error = await catchErr(createCampaigns)({
campaignsToCreate,
campaignAdministrationRepository: campaignAdministrationRepositoryStub,
codeGenerator: codeGeneratorStub,
campaignCreatorRepository: campaignCreatorRepositoryStub,
userRepository: userRepositoryStub,
organizationRepository: organizationRepositoryStub,
});

expect(error).to.throw;
expect(error).to.be.an.instanceof(UserNotFoundError);
});

it('should throw error if organization does not exist', async function () {
organizationRepositoryStub.get.withArgs(campaignsToCreate[0].organizationId).rejects(new NotFoundError());
userRepositoryStub.get.withArgs(campaignsToCreate[0].ownerId).resolves();

const error = await catchErr(createCampaigns)({
campaignsToCreate,
campaignAdministrationRepository: campaignAdministrationRepositoryStub,
codeGenerator: codeGeneratorStub,
campaignCreatorRepository: campaignCreatorRepositoryStub,
userRepository: userRepositoryStub,
organizationRepository: organizationRepositoryStub,
});

expect(error).to.throw;
expect(error).to.be.an.instanceof(NotFoundError);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -1408,6 +1408,7 @@ describe('Unit | Serializer | CSV | csv-serializer', function () {
"Identifiant de l'organisation*",
'Nom de la campagne*',
'Identifiant du profil cible*',
'Identifiant du propriétaire*',
'Identifiant du créateur*',
];

Expand All @@ -1425,11 +1426,11 @@ describe('Unit | Serializer | CSV | csv-serializer', function () {

describe('#parseForCampaignsImport', function () {
const headerCsv =
"Identifiant de l'organisation*;Nom de la campagne*;Identifiant du profil cible*;Libellé de l'identifiant externe;Identifiant du créateur*;Titre du parcours;Descriptif du parcours;Envoi multiple;Identifiant du propriétaire;Texte de la page de fin de parcours;Texte du bouton de la page de fin de parcours;URL du bouton de la page de fin de parcours\n";
"Identifiant de l'organisation*;Nom de la campagne*;Identifiant du profil cible*;Libellé de l'identifiant externe;Identifiant du créateur*;Titre du parcours;Descriptif du parcours;Envoi multiple;Identifiant du propriétaire*;Texte de la page de fin de parcours;Texte du bouton de la page de fin de parcours;URL du bouton de la page de fin de parcours\n";

it('should return parsed campaign data', async function () {
// given
const csv = `${headerCsv}1;chaussette;1234;numéro étudiant;789;titre 1;descriptif 1;Oui;45\n2;chapeau;1234;identifiant;666;titre 2;descriptif 2;Non\n3;chausson;1234;identifiant;123;titre 3;descriptif 3;Non;;Bravo !;Cliquez ici;https://hmpg.net/`;
const csv = `${headerCsv}1;chaussette;1234;numéro étudiant;789;titre 1;descriptif 1;Oui;45\n2;chapeau;1234;identifiant;666;titre 2;descriptif 2;Non;77\n3;chausson;1234;identifiant;123;titre 3;descriptif 3;Non;88;Bravo !;Cliquez ici;https://hmpg.net/`;

// when
const parsedData = await csvSerializer.parseForCampaignsImport(csv);
Expand Down Expand Up @@ -1459,7 +1460,7 @@ describe('Unit | Serializer | CSV | csv-serializer', function () {
customLandingPageText: 'descriptif 2',
creatorId: 666,
multipleSendings: false,
ownerId: null,
ownerId: 77,
customResultPageText: null,
customResultPageButtonText: null,
customResultPageButtonUrl: null,
Expand All @@ -1473,7 +1474,7 @@ describe('Unit | Serializer | CSV | csv-serializer', function () {
customLandingPageText: 'descriptif 3',
creatorId: 123,
multipleSendings: false,
ownerId: null,
ownerId: 88,
customResultPageText: 'Bravo !',
customResultPageButtonText: 'Cliquez ici',
customResultPageButtonUrl: 'https://hmpg.net/',
Expand Down Expand Up @@ -1566,6 +1567,34 @@ describe('Unit | Serializer | CSV | csv-serializer', function () {
});
});

describe('when ownerId field is not valid', function () {
it('should throw an error when empty', async function () {
// given
const campaignWithoutOwnerIdCsv = `${headerCsv}1;chaussette;1234;numéro étudiant;12;titre;descriptif;false;;`;

// when
const error = await catchErr(csvSerializer.parseForCampaignsImport)(campaignWithoutOwnerIdCsv);

// then
expect(error).to.be.instanceOf(FileValidationError);
expect(error.code).to.equal('CSV_CONTENT_NOT_VALID');
expect(error.meta).to.equal('NaN is not a valid value for "Identifiant du propriétaire*"');
});

it('should throw an error when not a number', async function () {
// given
const campaignWithBadNumberForOwnerIdCsv = `${headerCsv}1;chaussette;1234;idpixlabel;1234;titre;descriptif;false;not a number`;

// when
const error = await catchErr(csvSerializer.parseForCampaignsImport)(campaignWithBadNumberForOwnerIdCsv);

// then
expect(error).to.be.instanceOf(FileValidationError);
expect(error.code).to.equal('CSV_CONTENT_NOT_VALID');
expect(error.meta).to.equal('NaN is not a valid value for "Identifiant du propriétaire*"');
});
});

describe('when name field is not valid', function () {
it('should throw an error', async function () {
// given
Expand Down

0 comments on commit 5175afc

Please sign in to comment.