Skip to content

Commit b074a3f

Browse files
authored
feat(api): add update-organizations-in-batch usecase
1 parent 11965ce commit b074a3f

File tree

3 files changed

+220
-0
lines changed

3 files changed

+220
-0
lines changed

api/src/organizational-entities/domain/usecases/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const usecasesWithoutInjectedDependencies = {
2828
* @typedef OrganizationalEntitiesUsecases
2929
* @property {addOrganizationFeatureInBatch} addOrganizationFeatureInBatch
3030
* @property {attachChildOrganizationToOrganization} attachChildOrganizationToOrganization
31+
* @property {updateOrganizationsInBatch} updateOrganizationsInBatch
3132
*/
3233

3334
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { createReadStream } from 'node:fs';
2+
3+
import { DomainTransaction } from '../../../shared/domain/DomainTransaction.js';
4+
import { CsvColumn } from '../../../shared/infrastructure/serializers/csv/csv-column.js';
5+
import { CsvParser } from '../../../shared/infrastructure/serializers/csv/csv-parser.js';
6+
import { getDataBuffer } from '../../../shared/infrastructure/utils/buffer.js';
7+
import { OrganizationBatchUpdateDTO } from '../dtos/OrganizationBatchUpdateDTO.js';
8+
9+
const CSV_HEADER = {
10+
columns: [
11+
new CsvColumn({
12+
isRequired: true,
13+
name: 'Organization ID',
14+
property: 'id',
15+
}),
16+
new CsvColumn({
17+
name: 'Organization Name',
18+
property: 'name',
19+
}),
20+
new CsvColumn({
21+
name: 'Organization External ID',
22+
property: 'externalId',
23+
}),
24+
new CsvColumn({
25+
name: 'Organization Parent ID',
26+
property: 'parentOrganizationId',
27+
}),
28+
new CsvColumn({
29+
name: 'Organization Identity Provider Code',
30+
property: 'identityProviderForCampaigns',
31+
}),
32+
new CsvColumn({
33+
name: 'Organization Documentation URL',
34+
property: 'documentationUrl',
35+
}),
36+
new CsvColumn({
37+
name: 'Organization Province Code',
38+
property: 'provinceCode',
39+
}),
40+
new CsvColumn({
41+
name: 'DPO Last Name',
42+
property: 'dataProtectionOfficerLastName',
43+
}),
44+
new CsvColumn({
45+
name: 'DPO First Name',
46+
property: 'dataProtectionOfficerFirstName',
47+
}),
48+
new CsvColumn({
49+
name: 'DPO E-mail',
50+
property: 'dataProtectionOfficerEmail',
51+
}),
52+
],
53+
};
54+
55+
/**
56+
* @typedef {function} updateOrganizationsInBatch
57+
* @param {Object} params
58+
* @param {string} params.filePath
59+
* @param {OrganizationForAdminRepository} params.organizationForAdminRepository
60+
* @return {Promise<void>}
61+
*/
62+
export const updateOrganizationsInBatch = async function ({ filePath, organizationForAdminRepository }) {
63+
const organizationBatchUpdateDtos = await _getCsvData(filePath);
64+
65+
if (organizationBatchUpdateDtos.length === 0) return;
66+
67+
await DomainTransaction.execute(async (domainTransaction) => {
68+
await Promise.all(
69+
organizationBatchUpdateDtos.map(async (organizationBatchUpdateDto) => {
70+
const organization = await organizationForAdminRepository.get(organizationBatchUpdateDto.id, domainTransaction);
71+
organization.updateFromOrganizationBatchUpdateDto(organizationBatchUpdateDto);
72+
await organizationForAdminRepository.update(organization, domainTransaction);
73+
}),
74+
);
75+
});
76+
};
77+
78+
async function _getCsvData(filePath) {
79+
const stream = createReadStream(filePath);
80+
const buffer = await getDataBuffer(stream);
81+
const csvParser = new CsvParser(buffer, CSV_HEADER);
82+
const csvData = csvParser.parse();
83+
return csvData.map((row) => new OrganizationBatchUpdateDTO(row));
84+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { NotFoundError } from '../../../../../lib/domain/errors.js';
2+
import { OrganizationBatchUpdateDTO } from '../../../../../src/organizational-entities/domain/dtos/OrganizationBatchUpdateDTO.js';
3+
import { updateOrganizationsInBatch } from '../../../../../src/organizational-entities/domain/usecases/update-organizations-in-batch.usecase.js';
4+
import { DomainTransaction } from '../../../../../src/shared/domain/DomainTransaction.js';
5+
import { catchErr, createTempFile, domainBuilder, expect, removeTempFile, sinon } from '../../../../test-helper.js';
6+
7+
describe('Unit | Organizational Entities | Domain | UseCase | update-organizations-in-batch', function () {
8+
let domainTransaction, filePath, organizationForAdminRepository;
9+
const csvHeaders =
10+
'Organization ID;Organization Name;Organization External ID;Organization Parent ID;Organization Identity Provider Code;Organization Documentation URL;Organization Province Code;DPO Last Name;DPO First Name;DPO E-mail';
11+
12+
beforeEach(function () {
13+
domainTransaction = {
14+
knexTransaction: Symbol('transaction'),
15+
};
16+
sinon.stub(DomainTransaction, 'execute').callsFake((callback) => {
17+
return callback(domainTransaction);
18+
});
19+
20+
organizationForAdminRepository = {
21+
get: sinon.stub(),
22+
update: sinon.stub(),
23+
};
24+
});
25+
26+
afterEach(async function () {
27+
sinon.restore();
28+
29+
if (filePath) {
30+
await removeTempFile(filePath);
31+
}
32+
});
33+
34+
context('when parsing a CSV file without organization', function () {
35+
it('does nothing', async function () {
36+
// given
37+
const fileData = `${csvHeaders}`;
38+
filePath = await createTempFile('test.csv', fileData);
39+
40+
// when
41+
await updateOrganizationsInBatch({ filePath, organizationForAdminRepository });
42+
43+
// then
44+
expect(DomainTransaction.execute).to.not.have.been.called;
45+
expect(organizationForAdminRepository.get).to.not.have.been.called;
46+
expect(organizationForAdminRepository.update).to.not.have.been.called;
47+
});
48+
});
49+
50+
context('when parsing a CSV file which contains a list of organizations to update', function () {
51+
let csvData;
52+
53+
beforeEach(async function () {
54+
const fileData = `${csvHeaders}
55+
1;;12;;OIDC_EXAMPLE_NET;https://doc.url;;Troisjour;Adam;
56+
2;New Name;;;;;;;Cali;`;
57+
filePath = await createTempFile('test.csv', fileData);
58+
csvData = [
59+
new OrganizationBatchUpdateDTO({
60+
id: '1',
61+
externalId: '12',
62+
identityProviderForCampaigns: 'OIDC_EXAMPLE_NET',
63+
documentationUrl: 'https://doc.url',
64+
dataProtectionOfficerLastName: 'Troisjour',
65+
dataProtectionOfficerFirstName: 'Adam',
66+
}),
67+
new OrganizationBatchUpdateDTO({
68+
id: '2',
69+
name: 'New Name',
70+
dataProtectionOfficerFirstName: 'Cali',
71+
}),
72+
];
73+
});
74+
75+
it('calls n times "organizationForAdminRepository.get" to retrieve an organization', async function () {
76+
// given
77+
organizationForAdminRepository.get.onCall(0).resolves(domainBuilder.buildOrganizationForAdmin({ id: 1 }));
78+
organizationForAdminRepository.get.onCall(1).resolves(domainBuilder.buildOrganizationForAdmin({ id: 2 }));
79+
80+
// when
81+
await updateOrganizationsInBatch({ filePath, organizationForAdminRepository });
82+
83+
// then
84+
expect(DomainTransaction.execute).to.have.been.called;
85+
expect(organizationForAdminRepository.get).to.have.been.callCount(2);
86+
expect(organizationForAdminRepository.get.getCall(0)).to.have.been.calledWithExactly('1', domainTransaction);
87+
expect(organizationForAdminRepository.get.getCall(1)).to.have.been.calledWithExactly('2', domainTransaction);
88+
});
89+
90+
it('calls n times "organizationForAdminRepository.update" to update an organization', async function () {
91+
// given
92+
const firstOrganization = domainBuilder.buildOrganizationForAdmin({ id: 1 });
93+
const secondOrganization = domainBuilder.buildOrganizationForAdmin({ id: 2 });
94+
organizationForAdminRepository.get.onCall(0).resolves(firstOrganization);
95+
organizationForAdminRepository.get.onCall(1).resolves(secondOrganization);
96+
97+
const expectedFirstOrganization = domainBuilder.buildOrganizationForAdmin({ id: 1 });
98+
expectedFirstOrganization.updateFromOrganizationBatchUpdateDto(csvData[0]);
99+
const expectedSecondOrganization = domainBuilder.buildOrganizationForAdmin({ id: 2 });
100+
expectedSecondOrganization.updateFromOrganizationBatchUpdateDto(csvData[1]);
101+
102+
// when
103+
await updateOrganizationsInBatch({ filePath, organizationForAdminRepository });
104+
105+
// then
106+
expect(DomainTransaction.execute).to.have.been.called;
107+
expect(organizationForAdminRepository.update).to.have.been.callCount(2);
108+
expect(organizationForAdminRepository.update.getCall(0)).to.have.been.calledWithExactly(
109+
expectedFirstOrganization,
110+
domainTransaction,
111+
);
112+
expect(organizationForAdminRepository.update.getCall(1)).to.have.been.calledWithExactly(
113+
expectedSecondOrganization,
114+
domainTransaction,
115+
);
116+
});
117+
118+
context('when an organization does not exist', function () {
119+
it('throws an NotFoundError', async function () {
120+
// given
121+
organizationForAdminRepository.get.rejects(new NotFoundError('Not found organization for ID 1'));
122+
123+
// when
124+
const error = await catchErr(updateOrganizationsInBatch)({ filePath, organizationForAdminRepository });
125+
126+
// then
127+
expect(DomainTransaction.execute).to.have.been.called;
128+
expect(organizationForAdminRepository.get).to.have.been.called;
129+
expect(organizationForAdminRepository.update).to.not.have.been.called;
130+
expect(error).to.be.instanceOf(NotFoundError);
131+
expect(error.message).to.equal('Not found organization for ID 1');
132+
});
133+
});
134+
});
135+
});

0 commit comments

Comments
 (0)