Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Gestion des doublons d'INE dans Parcoursup (PIX-16361). #11341

Open
wants to merge 2 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { parcoursupDomainErrorMappingConfiguration } from '../../../parcoursup/application/http-error-mapper-configuration.js';
import { HttpErrors } from '../../../shared/application/http-errors.js';
import { DomainErrorMappingConfiguration } from '../../../shared/application/models/domain-error-mapping-configuration.js';
import { configurationDomainErrorMappingConfiguration } from '../../configuration/application/http-error-mapper-configuration.js';
Expand All @@ -24,5 +25,6 @@ certificationDomainErrorMappingConfiguration.push(
...enrolmentDomainErrorMappingConfiguration,
...sessionDomainErrorMappingConfiguration,
...configurationDomainErrorMappingConfiguration,
...parcoursupDomainErrorMappingConfiguration,
);
export { certificationDomainErrorMappingConfiguration };
11 changes: 10 additions & 1 deletion api/src/parcoursup/application/certification-controller.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { usecases } from '../domain/usecases/index.js';

const getCertificationResult = async function (request) {
return usecases.getCertificationResult(request.payload);
const { ine, organizationUai, lastName, firstName, birthdate, verificationCode } = request.payload;

return usecases.getCertificationResult({
ine,
organizationUai,
lastName,
firstName,
birthdate,
verificationCode,
});
};

export const certificationController = {
Expand Down
1 change: 1 addition & 0 deletions api/src/parcoursup/application/certification-route.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ const register = async function (server) {
401: responseObjectErrorDoc,
403: responseObjectErrorDoc,
404: responseObjectErrorDoc,
409: responseObjectErrorDoc,
},
},
},
Expand Down
10 changes: 10 additions & 0 deletions api/src/parcoursup/application/http-error-mapper-configuration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { HttpErrors } from '../../shared/application/http-errors.js';
import { DomainErrorMappingConfiguration } from '../../shared/application/models/domain-error-mapping-configuration.js';
import { MoreThanOneMatchingCertificationError } from '../domain/errors.js';

export const parcoursupDomainErrorMappingConfiguration = [
{
name: MoreThanOneMatchingCertificationError.name,
httpErrorFn: (error) => new HttpErrors.ConflictError(error.message),
},
].map((domainErrorMappingConfiguration) => new DomainErrorMappingConfiguration(domainErrorMappingConfiguration));
7 changes: 7 additions & 0 deletions api/src/parcoursup/domain/errors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { DomainError } from '../../shared/domain/errors.js';

export class MoreThanOneMatchingCertificationError extends DomainError {
constructor(message = 'More than one candidate found for current search parameters') {
super(message);
}
}
20 changes: 17 additions & 3 deletions api/src/parcoursup/domain/read-models/CertificationResult.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
class CertificationResult {
/**
* @typedef {import('./Competence.js').Competence} Competence
*/

export class CertificationResult {
/**
* @param {Object} props
* @param {string} props.[ine]
* @param {string} props.[organizationUai]
* @param {string} props.lastName
* @param {string} props.firstName
* @param {string} props.birthdate
* @param {string} props.status
* @param {string} props.pixScore
* @param {Date} props.certificationDate
* @param {Array<Competence>} props.competences
*/
constructor({
ine,
organizationUai,
Expand All @@ -21,5 +37,3 @@ class CertificationResult {
this.competences = competences;
}
}

export { CertificationResult };
15 changes: 15 additions & 0 deletions api/src/parcoursup/domain/read-models/Competence.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export class Competence {
/**
* @param {Object} props
* @param {string} props.code
* @param {string} props.name
* @param {string} props.areaName
* @param {string} props.level
*/
constructor({ code, name, areaName, level }) {
this.code = code;
this.name = name;
this.areaName = areaName;
this.level = level;
}
}
33 changes: 25 additions & 8 deletions api/src/parcoursup/domain/usecases/get-certification-result.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
/**
* @typedef {import('../../domain/usecases/index.js').CertificationRepository} CertificationRepository
* @typedef {import('../read-models/CertificationResult.js').CertificationResult} CertificationResult
* @typedef {import('../../../shared/domain/errors.js').NotFoundError} NotFoundError
*/

import { MoreThanOneMatchingCertificationError } from '../errors.js';

/**
* @param {Object} params
* @param {string} params.ine
Expand All @@ -11,25 +15,38 @@
* @param {string} params.birthdate - Format YYYY-MM-DD
* @param {string} params.verificationCode
* @param {CertificationRepository} params.certificationRepository
*
* @returns {CertificationResult} matching candidate certification result
* @throws {MoreThanOneMatchingCertificationError} in some cases (INE for example) there might be duplicates
* @throws {NotFoundError} if no certification exists for this candidate
**/
export const getCertificationResult = function ({
export const getCertificationResult = async ({
ine,
organizationUai,
lastName,
firstName,
birthdate,
verificationCode,
certificationRepository,
}) {
}) => {
let certifications = [];

if (ine) {
return certificationRepository.getByINE({ ine });
certifications = await certificationRepository.getByINE({ ine });
} else if (organizationUai) {
certifications = await certificationRepository.getByOrganizationUAI({
organizationUai,
lastName,
firstName,
birthdate,
});
} else if (verificationCode) {
certifications = await certificationRepository.getByVerificationCode({ verificationCode });
}

if (organizationUai) {
return certificationRepository.getByOrganizationUAI({ organizationUai, lastName, firstName, birthdate });
if (certifications.length !== 1) {
throw new MoreThanOneMatchingCertificationError();
}

if (verificationCode) {
return certificationRepository.getByVerificationCode({ verificationCode });
}
return certifications.shift();
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { datamartKnex } from '../../../../db/knex-database-connection.js';
import { NotFoundError } from '../../../shared/domain/errors.js';
import { CertificationResult } from '../../domain/read-models/CertificationResult.js';
import { Competence } from '../../domain/read-models/Competence.js';

const getByINE = async ({ ine }) => {
return _getBySearchParams({
Expand All @@ -19,7 +20,26 @@ const getByOrganizationUAI = async ({ organizationUai, lastName, firstName, birt

const _getBySearchParams = async (searchParams) => {
const certificationResultDto = await datamartKnex('data_export_parcoursup_certif_result')
.select(
.select({
national_student_id: 'national_student_id',
organization_uai: 'organization_uai',
last_name: 'last_name',
first_name: 'first_name',
birthdate: 'birthdate',
status: 'status',
pix_score: 'pix_score',
certification_date: 'certification_date',
competences: datamartKnex.raw(
`json_agg(json_build_object(
'competence_code', "competence_code",
'competence_name', "competence_name",
'area_name', "area_name",
'competence_level', "competence_level"
))`,
),
})
.where(searchParams)
.groupBy(
'national_student_id',
'organization_uai',
'last_name',
Expand All @@ -28,12 +48,8 @@ const _getBySearchParams = async (searchParams) => {
'status',
'pix_score',
'certification_date',
'competence_code',
'competence_name',
'area_name',
'competence_level',
)
.where(searchParams);
);

if (!certificationResultDto.length) {
throw new NotFoundError('No certifications found for given search parameters');
}
Expand All @@ -43,46 +59,59 @@ const _getBySearchParams = async (searchParams) => {

const getByVerificationCode = async ({ verificationCode }) => {
const certificationResultDto = await datamartKnex('data_export_parcoursup_certif_result_code_validation')
.select(
'last_name',
'first_name',
'birthdate',
'status',
'pix_score',
'certification_date',
'competence_code',
'competence_name',
'area_name',
'competence_level',
)
.select({
last_name: 'last_name',
first_name: 'first_name',
birthdate: 'birthdate',
status: 'status',
pix_score: 'pix_score',
certification_date: 'certification_date',
competences: datamartKnex.raw(
`json_agg(json_build_object(
'competence_code', "competence_code",
'competence_name', "competence_name",
'area_name', "area_name",
'competence_level', "competence_level"
))`,
),
})
.where({
certification_code_verification: verificationCode,
});
})
.groupBy('last_name', 'first_name', 'birthdate', 'status', 'pix_score', 'certification_date');

if (!certificationResultDto.length) {
throw new NotFoundError('No certifications found for given search parameters');
}

return _toDomain(certificationResultDto);
};

/**
* @returns {Array<CertificationResult>}
*/
const _toDomain = (certificationResultDto) => {
return new CertificationResult({
ine: certificationResultDto[0].national_student_id,
organizationUai: certificationResultDto[0].organization_uai,
lastName: certificationResultDto[0].last_name,
firstName: certificationResultDto[0].first_name,
birthdate: certificationResultDto[0].birthdate,
status: certificationResultDto[0].status,
pixScore: certificationResultDto[0].pix_score,
certificationDate: certificationResultDto[0].certification_date,
competences: certificationResultDto.map((certificationResultDto) => {
return {
code: certificationResultDto.competence_code,
name: certificationResultDto.competence_name,
areaName: certificationResultDto.area_name,
level: certificationResultDto.competence_level,
};
}),
return certificationResultDto.map((certificationResult) => {
const competences = certificationResult.competences.map((competence) => {
return new Competence({
code: competence.competence_code,
name: competence.competence_name,
areaName: competence.area_name,
level: competence.competence_level,
});
});

return new CertificationResult({
ine: certificationResult.national_student_id,
organizationUai: certificationResult.organization_uai,
lastName: certificationResult.last_name,
firstName: certificationResult.first_name,
birthdate: certificationResult.birthdate,
status: certificationResult.status,
pixScore: certificationResult.pix_score,
certificationDate: certificationResult.certification_date,
competences,
});
});
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ describe('Parcoursup | Acceptance | Application | certification-route', function
PARCOURSUP_CLIENT_ID,
PARCOURSUP_SCOPE,
PARCOURSUP_SOURCE,
expectedCertification;
expectedCertification,
certificationResultData;

beforeEach(async function () {
server = await createServer();
Expand All @@ -30,7 +31,7 @@ describe('Parcoursup | Acceptance | Application | certification-route', function
firstName = 'PRENOM-ELEVE';
birthdate = '2000-01-01';

const certificationResultData = {
certificationResultData = {
nationalStudentId: ine,
organizationUai,
lastName,
Expand Down
Loading