Skip to content

Commit

Permalink
feat(stage3e2de): formulaire de candidature (backend) (#2573)
Browse files Browse the repository at this point in the history
* feat(stage3e2de): recuperer les label metiers par appellationCodes - repo

* feat(stage3e2de): récupérer les appelations metiers par appelations code - usecase

* feat(stage3e2de): récupérer les appelations metiers par appellation code - controller WIP

* feat(stage3e2de): envoyer candidature (back)

* feat(stage3e2de): passer le mapping du wording de mode de contact vers le front

* feat(stage3eme): enum ModeDeContact

* feat(stage3eme): ajout données dans objet stage3eme2de

* fix(stage3e2de): test controller renommage handler en pagesHandler

* fix: retour review

* fix: retour review

* refacto(stage3eme): fix import

---------

Co-authored-by: Suxue LI <[email protected]>
  • Loading branch information
Naorid and suli-octo authored Jan 24, 2024
1 parent ca5df29 commit f109b9a
Show file tree
Hide file tree
Showing 25 changed files with 863 additions and 171 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ListeResultatsStage3eEt2de,
} from '~/client/components/features/Stages3eEt2de/Rechercher/FormulaireRecherche/ListeResultatsStage3eEt2de';
import { mockSmallScreen } from '~/client/components/window.mock';
import { ModeDeContact } from '~/server/stage-3e-et-2de/domain/candidatureStage3eEt2de';
import { aResultatRechercheStage3eEt2de, aStage3eEt2de } from '~/server/stage-3e-et-2de/domain/stage3eEt2de.fixture';

describe('<ListeResultatsStage3eEt2de />', () => {
Expand Down Expand Up @@ -105,15 +106,22 @@ describe('<ListeResultatsStage3eEt2de />', () => {
expect(resultats[1]).toHaveTextContent('2 rue de la Paix');
expect(resultats[1]).toHaveTextContent('75000 Paris');
});
});
describe('tags', () => {
it('ajoute un tag correspondant au nombre de salariés, si l’information est présente', () => {
it('un lien pour candidater', () => {
// GIVEN
const resultatRecherche = aResultatRechercheStage3eEt2de({
nombreDeResultats: 1,
nombreDeResultats: 2,
resultats: [
aStage3eEt2de({
nombreDeSalaries: '42',
appellationCodes: ['1234', '5678'],
modeDeContact: ModeDeContact.IN_PERSON,
nomEntreprise: 'Entreprise 1',
siret: '12345678912345',
}),
aStage3eEt2de({
appellationCodes: ['1236', '5679'],
modeDeContact: ModeDeContact.EMAIL,
nomEntreprise: 'Entreprise 2',
siret: '12345678912346',
}),
],
});
Expand All @@ -123,18 +131,45 @@ describe('<ListeResultatsStage3eEt2de />', () => {

// THEN
const resultatsUl = screen.getByRole('list', { name: 'Stages de 3e et 2de' });
const tagsList = within(resultatsUl).getByRole('list', { name: 'Caractéristiques de l‘offre' });
const tagNombreDeSalariés = within(tagsList).getByText('42 salariés');
expect(tagNombreDeSalariés).toBeVisible();
const lienCandidature = within(resultatsUl).getAllByRole('link', { name: 'Candidater' });
expect(lienCandidature).toHaveLength(resultatRecherche.nombreDeResultats);
expect(lienCandidature[0]).toHaveAttribute('href', '/stages-3e-et-2de/candidater?appellationCodes=1234%2C5678&modeDeContact=IN_PERSON&nomEntreprise=Entreprise+1&siret=12345678912345');
expect(lienCandidature[1]).toHaveAttribute('href', '/stages-3e-et-2de/candidater?appellationCodes=1236%2C5679&modeDeContact=EMAIL&nomEntreprise=Entreprise+2&siret=12345678912346');
});

it('ajoute un tag correspondant au mode de contact, si l’information est présente', () => {
describe('lorsque le mode de contact est inconnu', () => {
it('le lien pour candidater n’est pas affiché', () => {
// GIVEN
const resultatRecherche = aResultatRechercheStage3eEt2de({
nombreDeResultats: 1,
resultats: [
aStage3eEt2de({
modeDeContact: ModeDeContact.IN_PERSON,
}),
aStage3eEt2de({
modeDeContact: undefined,
}),
],
});

// WHEN
render(<ListeResultatsStage3eEt2de resultatList={resultatRecherche} />);

// THEN
const resultatsUl = screen.getByRole('list', { name: 'Stages de 3e et 2de' });
const lienCandidature = within(resultatsUl).queryAllByRole('link', { name: 'Candidater' });
expect(lienCandidature).toHaveLength(1);
});
});
});
describe('tags', () => {
it('ajoute un tag correspondant au nombre de salariés, si l’information est présente', () => {
// GIVEN
const resultatRecherche = aResultatRechercheStage3eEt2de({
nombreDeResultats: 1,
resultats: [
aStage3eEt2de({
modeDeContact: 'Pigeon voyageur',
nombreDeSalaries: '42',
}),
],
});
Expand All @@ -145,8 +180,95 @@ describe('<ListeResultatsStage3eEt2de />', () => {
// THEN
const resultatsUl = screen.getByRole('list', { name: 'Stages de 3e et 2de' });
const tagsList = within(resultatsUl).getByRole('list', { name: 'Caractéristiques de l‘offre' });
const tagModeDeContact = within(tagsList).getByText('Pigeon voyageur');
expect(tagModeDeContact).toBeVisible();
const tagNombreDeSalariés = within(tagsList).getByText('42 salariés');
expect(tagNombreDeSalariés).toBeVisible();
});

describe('ajoute un tag correspondant au mode de contact', () => {
it('si la candidature se fait en personne ajoute "Candidature en personne"', () => {
// GIVEN
const resultatRecherche = aResultatRechercheStage3eEt2de({
nombreDeResultats: 1,
resultats: [
aStage3eEt2de({
modeDeContact: ModeDeContact.IN_PERSON,
}),
],
});

// WHEN
render(<ListeResultatsStage3eEt2de resultatList={resultatRecherche} />);

// THEN
const resultatsUl = screen.getByRole('list', { name: 'Stages de 3e et 2de' });
const tagsList = within(resultatsUl).getByRole('list', { name: 'Caractéristiques de l‘offre' });
const tagModeDeContact = within(tagsList).getByText('Candidature en personne');
expect(tagModeDeContact).toBeVisible();
});
it('si la candidature se fait par e-mail ajoute "Candidature par e-mail', () => {
// GIVEN
const resultatRecherche = aResultatRechercheStage3eEt2de({
nombreDeResultats: 1,
resultats: [
aStage3eEt2de({
modeDeContact: ModeDeContact.EMAIL,
}),
],
});

// WHEN
render(<ListeResultatsStage3eEt2de resultatList={resultatRecherche} />);

// THEN
const resultatsUl = screen.getByRole('list', { name: 'Stages de 3e et 2de' });
const tagsList = within(resultatsUl).getByRole('list', { name: 'Caractéristiques de l‘offre' });
const tagModeDeContact = within(tagsList).getByText('Candidature par e-mail');
expect(tagModeDeContact).toBeVisible();
});
it('si la candidature se fait par téléphone ajoute "Candidature par téléphone"', () => {
// GIVEN
const resultatRecherche = aResultatRechercheStage3eEt2de({
nombreDeResultats: 1,
resultats: [
aStage3eEt2de({
modeDeContact: ModeDeContact.PHONE,
}),
],
});

// WHEN
render(<ListeResultatsStage3eEt2de resultatList={resultatRecherche} />);

// THEN
const resultatsUl = screen.getByRole('list', { name: 'Stages de 3e et 2de' });
const tagsList = within(resultatsUl).getByRole('list', { name: 'Caractéristiques de l‘offre' });
const tagModeDeContact = within(tagsList).getByText('Candidature par téléphone');
expect(tagModeDeContact).toBeVisible();
});
it('si l’information n’est pas connue n’ajoute pas de tag', () => {
// GIVEN
const resultatRecherche = aResultatRechercheStage3eEt2de({
nombreDeResultats: 1,
resultats: [
aStage3eEt2de({
modeDeContact: undefined,
}),
],
});

// WHEN
render(<ListeResultatsStage3eEt2de resultatList={resultatRecherche} />);

// THEN
const resultatsUl = screen.getByRole('list', { name: 'Stages de 3e et 2de' });
const tagsUl = within(resultatsUl).getByRole('list', { name: 'Caractéristiques de l‘offre' });
const tagModeDeContactTel = within(tagsUl).queryByText('Candidature par téléphone');
const tagModeDeContactEmail = within(tagsUl).queryByText('Candidature par e-mail');
const tagModeDeContactEnPersonne = within(tagsUl).queryByText('Candidature en personne');
expect(tagModeDeContactTel).not.toBeInTheDocument();
expect(tagModeDeContactEmail).not.toBeInTheDocument();
expect(tagModeDeContactEnPersonne).not.toBeInTheDocument();
});
});

it('ajoute un tag correspond si l’offre est accessible aux personnes en situation de handicap', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { v4 as uuidv4 } from 'uuid';

import {
getModeDeContactWording,
} from '~/client/components/features/Stages3eEt2de/Rechercher/FormulaireRecherche/getModeDeContactWording';
import {
ListeRésultatsRechercherSolution,
} from '~/client/components/layouts/RechercherSolution/ListeRésultats/ListeRésultatsRechercherSolution';
import { RésultatRechercherSolution } from '~/client/components/layouts/RechercherSolution/Résultat/RésultatRechercherSolution';
import {
RésultatRechercherSolution,
} from '~/client/components/layouts/RechercherSolution/Résultat/RésultatRechercherSolution';
import { ResultatRechercheStage3eEt2de, Stage3eEt2de } from '~/server/stage-3e-et-2de/domain/stage3eEt2de';

interface ListeResultatsStage3eEt2deProps {
Expand All @@ -30,13 +35,25 @@ function ResultatStage3eEt2de(stage3eEt2de: Stage3eEt2de) {
if (stage3eEt2de.nombreDeSalaries) {
étiquetteOffreList.push(`${stage3eEt2de.nombreDeSalaries} salariés`);
}

if (stage3eEt2de.modeDeContact) {
étiquetteOffreList.push(stage3eEt2de.modeDeContact);
const modeDeContactWording = getModeDeContactWording(stage3eEt2de.modeDeContact);
modeDeContactWording && étiquetteOffreList.push(modeDeContactWording);
}
if (stage3eEt2de.accessiblePersonnesEnSituationDeHandicap) {
étiquetteOffreList.push('Handi-accessible');
}

const paramsLienOffre = {
appellationCodes: stage3eEt2de.appellationCodes.toString(),
modeDeContact: stage3eEt2de.modeDeContact ? stage3eEt2de.modeDeContact.toString() : '',
nomEntreprise: stage3eEt2de.nomEntreprise,
siret: stage3eEt2de.siret,
};

const lienOffre = stage3eEt2de.modeDeContact ? `/stages-3e-et-2de/candidater?${new URLSearchParams(paramsLienOffre).toString()}` : undefined;
const intituléLienOffre = stage3eEt2de.modeDeContact ? 'Candidater' : undefined;

return (
<li key={uuidv4()}>
<RésultatRechercherSolution
Expand All @@ -46,6 +63,8 @@ function ResultatStage3eEt2de(stage3eEt2de: Stage3eEt2de) {
<p>{stage3eEt2de.adresse.rueEtNumero}, {stage3eEt2de.adresse.codePostal} {stage3eEt2de.adresse.ville}</p>
</>}
étiquetteOffreList={étiquetteOffreList}
lienOffre={lienOffre}
intituléLienOffre={intituléLienOffre}
/>
</li>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ModeDeContact } from '~/server/stage-3e-et-2de/domain/candidatureStage3eEt2de';

export function getModeDeContactWording(modeDeContact: ModeDeContact): string | undefined {
switch (modeDeContact) {
case ModeDeContact.IN_PERSON:
return 'Candidature en personne';
case ModeDeContact.EMAIL:
return 'Candidature par e-mail';
case ModeDeContact.PHONE:
return 'Candidature par téléphone';
default:
return undefined;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { testApiHandler } from 'next-test-api-route-handler';
import nock from 'nock';

import handler from '~/pages/api/stages-3e-et-2de/candidature/index.controller';
import { ModeDeContact } from '~/server/stage-3e-et-2de/domain/candidatureStage3eEt2de';
import { aCandidatureStage3eEt2de } from '~/server/stage-3e-et-2de/domain/candidatureStage3eEt2de.fixture';
import {
anApiImmersionFacileStage3eEt2deCandidature,
} from '~/server/stage-3e-et-2de/infra/repositories/apiImmersionFacileStage3eEt2de.fixture';

describe('candidature stage 3e et 2de', () => {
it('envoie une candidature', async () => {
// Given
const candidature = aCandidatureStage3eEt2de();

nock('https://staging.immersion-facile.beta.gouv.fr/api/v2', {
reqheaders: { Authorization: 'API_IMMERSION_FACILE_STAGE_3EME_API_KEY' },
})
.post('/contact-establishment',
{ ...anApiImmersionFacileStage3eEt2deCandidature() })
.reply(201);

// When
await testApiHandler({
pagesHandler: (req, res) => handler(req, res),
params: { ...candidature },
test: async ({ fetch }) => {
const response = await fetch({
body: JSON.stringify(candidature),
method: 'POST',
});

// Then
expect(response.status).toBe(200);
},
url: '/stages-3e-et-2de/candidature',
});
});

it.each([
{ modeDeContact: 'invalid' },
{ appellationCode: undefined },
{ email: 1 },
])('répond une 400 quand le paramètre %o est incorrect', async (candidature) => {
// When
await testApiHandler({
pagesHandler: (req, res) => handler(req, res),
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
params: { ...aCandidatureStage3eEt2de(candidature) },
test: async ({ fetch }) => {
const response = await fetch({
body: JSON.stringify(candidature),
method: 'POST',
});

// Then
expect(response.status).toBe(400);
},
url: '/stages-3e-et-2de/candidature',
});
});

it.each([
{ modeDeContact: ModeDeContact.PHONE },
{ modeDeContact: ModeDeContact.EMAIL },
{ modeDeContact: ModeDeContact.IN_PERSON },
{ otherQuery: 'otherQuery' },
])('répond une 200 quand le paramètre %o est correct', async (candidature) => {
// Given
nock('https://staging.immersion-facile.beta.gouv.fr/api/v2', {
reqheaders: { Authorization: 'API_IMMERSION_FACILE_STAGE_3EME_API_KEY' },
})
.post('/contact-establishment',
{ ...anApiImmersionFacileStage3eEt2deCandidature(candidature.modeDeContact ? { contactMode: candidature.modeDeContact } : {}) })
.reply(201);

// When
await testApiHandler({
pagesHandler: (req, res) => handler(req, res),
params: { ...aCandidatureStage3eEt2de(candidature) },
test: async ({ fetch }) => {
const response = await fetch({
body: JSON.stringify(candidature),
method: 'POST',
});

// Then
expect(response.status).toBe(200);
},
url: '/stages-3e-et-2de/candidature',
});
});
});
41 changes: 41 additions & 0 deletions src/pages/api/stages-3e-et-2de/candidature/index.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Joi from 'joi';
import { NextApiRequest, NextApiResponse } from 'next';

import { withMonitoring } from '~/pages/api/middlewares/monitoring/monitoring.middleware';
import { withValidation } from '~/pages/api/middlewares/validation/validation.middleware';
import { ErrorHttpResponse } from '~/pages/api/utils/response/response.type';
import { handleResponse } from '~/pages/api/utils/response/response.util';
import {
CandidatureStage3eEt2de,
ModeDeContact,
} from '~/server/stage-3e-et-2de/domain/candidatureStage3eEt2de';
import { dependencies } from '~/server/start';

export const envoyerCandidatureStage3eEt2deQuerySchema = Joi.object({
appellationCode: Joi.string().required(),
email: Joi.string().email().required(),
modeDeContact: Joi.string().valid(...Object.values(ModeDeContact)).required(),
nom: Joi.string().required(),
prenom: Joi.string().required(),
siret: Joi.string().required(),
}).options({ allowUnknown: true });

export async function envoyerCandidatureStage3eEt2deHandler(req: NextApiRequest, res: NextApiResponse<undefined | ErrorHttpResponse>) {
const candidature = mapCandidature(req);
const reponseEnvoieCandidature = await dependencies.stage3eEt2deDependencies.envoyerCandidatureStage3eEt2deUseCase.handle(candidature);
return handleResponse(reponseEnvoieCandidature, res);
}

export default withMonitoring(withValidation({ query: envoyerCandidatureStage3eEt2deQuerySchema }, envoyerCandidatureStage3eEt2deHandler));

function mapCandidature(req: NextApiRequest): CandidatureStage3eEt2de {
const query = req.query;
return {
appellationCode: String(query.appellationCode),
email: String(query.email),
modeDeContact: ModeDeContact[query.modeDeContact as keyof typeof ModeDeContact],
nom: String(query.nom),
prenom: String(query.prenom),
siret: String(query.siret),
};
}
Loading

0 comments on commit f109b9a

Please sign in to comment.