Skip to content

Commit 93fb20a

Browse files
committed
feat(api): add v3 attestations generation for division
1 parent 967801c commit 93fb20a

File tree

5 files changed

+156
-52
lines changed

5 files changed

+156
-52
lines changed

api/src/certification/results/application/certification-attestation-controller.js

+18-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import dayjs from 'dayjs';
22

3+
import { normalizeAndRemoveAccents } from '../../../shared/infrastructure/utils/string-utils.js';
34
import { V3CertificationAttestation } from '../domain/models/V3CertificationAttestation.js';
45
import { usecases } from '../domain/usecases/index.js';
56
import * as certificationAttestationPdf from '../infrastructure/utils/pdf/certification-attestation-pdf.js';
@@ -95,7 +96,7 @@ const getCertificationPDFAttestationsForSession = async function (
9596
const downloadCertificationAttestationsForDivision = async function (
9697
request,
9798
h,
98-
dependencies = { certificationAttestationPdf },
99+
dependencies = { certificationAttestationPdf, v3CertificationAttestationPdf },
99100
) {
100101
const organizationId = request.params.organizationId;
101102
const { i18n } = request;
@@ -107,7 +108,22 @@ const downloadCertificationAttestationsForDivision = async function (
107108
});
108109

109110
if (attestations.every((attestation) => attestation instanceof V3CertificationAttestation)) {
110-
return h.response().code(200);
111+
const normalizedDivision = normalizeAndRemoveAccents(division);
112+
113+
const translatedFileName = i18n.__('certification-confirmation.file-name', {
114+
deliveredAt: dayjs(attestations[0].deliveredAt).format('YYYYMMDD'),
115+
});
116+
117+
return h
118+
.response(
119+
dependencies.v3CertificationAttestationPdf.generate({
120+
certificates: attestations,
121+
i18n,
122+
}),
123+
)
124+
.code(200)
125+
.header('Content-Disposition', `attachment; filename=${normalizedDivision}-${translatedFileName}`)
126+
.header('Content-Type', 'application/pdf');
111127
}
112128

113129
const { buffer } = await dependencies.certificationAttestationPdf.getCertificationAttestationsPdfBuffer({

api/src/shared/infrastructure/utils/string-utils.js

+12
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,24 @@ function toArrayOfFixedLengthStringsConservingWords(str, maxLength) {
7171
return result.map((str) => str.trim());
7272
}
7373

74+
function normalizeAndRemoveAccents(str) {
75+
return str
76+
.normalize('NFD')
77+
.replace(/[\u0300-\u036f]/g, '') // Remove accents
78+
.toLowerCase()
79+
.trim()
80+
.replace(/[^a-z0-9 -]/g, '') // Remove special characters
81+
.replace(/\s+/g, '-') // Replace spaces by dashes
82+
.replace(/-+/g, '-'); // Prevent double dashes
83+
}
84+
7485
export {
7586
cleanStringAndParseFloat,
7687
getArrayOfStrings,
7788
getArrayOfUpperStrings,
7889
isNumeric,
7990
normalize,
91+
normalizeAndRemoveAccents,
8092
normalizeAndSortChars,
8193
splitIntoWordsAndRemoveBackspaces,
8294
toArrayOfFixedLengthStringsConservingWords,

api/tests/certification/evaluation/acceptance/application/certification-attestation-route_test.js

+6
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@ describe('Certification | Results | Acceptance | Application | Routes | certific
359359

360360
const session = databaseBuilder.factory.buildSession({
361361
version: SESSIONS_VERSIONS.V3,
362+
publishedAt: new Date(),
362363
});
363364

364365
const candidate = databaseBuilder.factory.buildCertificationCandidate({
@@ -403,6 +404,11 @@ describe('Certification | Results | Acceptance | Application | Routes | certific
403404

404405
// then
405406
expect(response.statusCode).to.equal(200);
407+
expect(response.headers['content-type']).to.equal('application/pdf');
408+
expect(response.headers['content-disposition']).to.equal(
409+
`attachment; filename=${organizationLearner.division}-attestation-pix-${dayjs(session.publishedAt).format('YYYYMMDD')}.pdf`,
410+
);
411+
expect(response.file).not.to.be.null;
406412
});
407413
});
408414
});

api/tests/certification/results/unit/application/certification-attestation-controller_test.js

+106-50
Original file line numberDiff line numberDiff line change
@@ -232,56 +232,112 @@ describe('Certification | Results | Unit | Application | Controller | certificat
232232
clock.restore();
233233
});
234234

235-
it('should return binary attestations', async function () {
236-
// given
237-
const certifications = [
238-
domainBuilder.buildPrivateCertificateWithCompetenceTree(),
239-
domainBuilder.buildPrivateCertificateWithCompetenceTree(),
240-
];
241-
const organizationId = domainBuilder.buildOrganization().id;
242-
const division = '3b';
243-
const attestationsPDF = 'binary string';
244-
const userId = 1;
245-
const lang = FRENCH;
246-
const i18n = getI18n();
247-
248-
const request = {
249-
i18n,
250-
auth: { credentials: { userId } },
251-
params: { organizationId },
252-
query: { division, isFrenchDomainExtension: true, lang },
253-
};
254-
255-
sinon
256-
.stub(usecases, 'findCertificationAttestationsForDivision')
257-
.withArgs({
258-
division,
259-
organizationId,
260-
})
261-
.resolves(certifications);
262-
263-
const certificationAttestationPdfStub = {
264-
getCertificationAttestationsPdfBuffer: sinon.stub(),
265-
};
266-
267-
const dependencies = {
268-
certificationAttestationPdf: certificationAttestationPdfStub,
269-
};
270-
271-
certificationAttestationPdfStub.getCertificationAttestationsPdfBuffer
272-
.withArgs({ certificates: certifications, isFrenchDomainExtension: true, i18n })
273-
.resolves({ buffer: attestationsPDF });
274-
275-
// when
276-
const response = await certificationAttestationController.downloadCertificationAttestationsForDivision(
277-
request,
278-
hFake,
279-
dependencies,
280-
);
281-
282-
// then
283-
expect(response.source).to.deep.equal(attestationsPDF);
284-
expect(response.headers['Content-Disposition']).to.contains('attachment; filename=20190101_attestations_3b.pdf');
235+
describe('when attestations are for v3', function () {
236+
it('should return division attestations in PDF binary format', async function () {
237+
// given
238+
const userId = 1;
239+
const i18n = getI18n();
240+
241+
const v3CertificationAttestation = domainBuilder.certification.results.buildV3CertificationAttestation();
242+
const generatedPdf = Symbol('Stream');
243+
244+
const organizationId = domainBuilder.buildOrganization().id;
245+
const division = '3ème b';
246+
247+
const request = {
248+
i18n,
249+
auth: { credentials: { userId } },
250+
params: { organizationId },
251+
query: { division, isFrenchDomainExtension: true, lang: FRENCH },
252+
};
253+
254+
sinon
255+
.stub(usecases, 'findCertificationAttestationsForDivision')
256+
.withArgs({
257+
division,
258+
organizationId,
259+
})
260+
.resolves([v3CertificationAttestation, v3CertificationAttestation]);
261+
262+
const generatePdfStub = {
263+
generate: sinon.stub().returns(generatedPdf),
264+
};
265+
266+
// when
267+
const response = await certificationAttestationController.downloadCertificationAttestationsForDivision(
268+
request,
269+
hFake,
270+
{
271+
v3CertificationAttestationPdf: generatePdfStub,
272+
},
273+
);
274+
275+
// then
276+
expect(generatePdfStub.generate).calledOnceWithExactly({
277+
certificates: [v3CertificationAttestation, v3CertificationAttestation],
278+
i18n,
279+
});
280+
expect(response.source).to.deep.equal(generatedPdf);
281+
expect(response.headers['Content-Disposition']).to.contains(
282+
`attachment; filename=3eme-b-attestation-pix-${dayjs(v3CertificationAttestation.deliveredAt).format('YYYYMMDD')}.pdf`,
283+
);
284+
});
285+
});
286+
287+
describe('when attestations are for v2', function () {
288+
it('should return binary attestations', async function () {
289+
// given
290+
const certifications = [
291+
domainBuilder.buildPrivateCertificateWithCompetenceTree(),
292+
domainBuilder.buildPrivateCertificateWithCompetenceTree(),
293+
];
294+
const organizationId = domainBuilder.buildOrganization().id;
295+
const division = '3b';
296+
const attestationsPDF = 'binary string';
297+
const userId = 1;
298+
const lang = FRENCH;
299+
const i18n = getI18n();
300+
301+
const request = {
302+
i18n,
303+
auth: { credentials: { userId } },
304+
params: { organizationId },
305+
query: { division, isFrenchDomainExtension: true, lang },
306+
};
307+
308+
sinon
309+
.stub(usecases, 'findCertificationAttestationsForDivision')
310+
.withArgs({
311+
division,
312+
organizationId,
313+
})
314+
.resolves(certifications);
315+
316+
const certificationAttestationPdfStub = {
317+
getCertificationAttestationsPdfBuffer: sinon.stub(),
318+
};
319+
320+
const dependencies = {
321+
certificationAttestationPdf: certificationAttestationPdfStub,
322+
};
323+
324+
certificationAttestationPdfStub.getCertificationAttestationsPdfBuffer
325+
.withArgs({ certificates: certifications, isFrenchDomainExtension: true, i18n })
326+
.resolves({ buffer: attestationsPDF });
327+
328+
// when
329+
const response = await certificationAttestationController.downloadCertificationAttestationsForDivision(
330+
request,
331+
hFake,
332+
dependencies,
333+
);
334+
335+
// then
336+
expect(response.source).to.deep.equal(attestationsPDF);
337+
expect(response.headers['Content-Disposition']).to.contains(
338+
'attachment; filename=20190101_attestations_3b.pdf',
339+
);
340+
});
285341
});
286342
});
287343
});

api/tests/shared/unit/infrastructure/utils/string-utils_test.js

+14
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
getArrayOfUpperStrings,
55
isNumeric,
66
normalize,
7+
normalizeAndRemoveAccents,
78
normalizeAndSortChars,
89
splitIntoWordsAndRemoveBackspaces,
910
toArrayOfFixedLengthStringsConservingWords,
@@ -195,4 +196,17 @@ describe('Unit | Shared | infrastructure | Utils | string-utils', function () {
195196
]);
196197
});
197198
});
199+
200+
describe('#normalizeAndRemoveAccents', function () {
201+
it('should return a string without spaces and special characters', function () {
202+
// given
203+
const stringToSanitize = "chef-d'œuvre de string -- @ http://www.complexité.fr";
204+
205+
// when
206+
const result = normalizeAndRemoveAccents(stringToSanitize);
207+
208+
// then
209+
expect(result).to.equal('chef-duvre-de-string-httpwwwcomplexitefr');
210+
});
211+
});
198212
});

0 commit comments

Comments
 (0)