From f77b8ebe7fc31d6aead1037e56392c6f121a7f60 Mon Sep 17 00:00:00 2001 From: clement-duport Date: Fri, 16 Jun 2023 14:39:06 +0200 Subject: [PATCH 1/2] create contact establishment api v2 and make appelation code and appelation label mandatory in immersion offer entity v2 --- .../ImmersionOfferEntityV2Builder.ts | 6 ++- .../input/ContactEstablishmentPublicV1.dto.ts | 20 +++++-- .../ContactEstablishmentPublicV1.schema.ts | 21 ++++++-- .../input/ContactEstablishmentPublicV2.dto.ts | 39 ++++++++++++++ .../ContactEstablishmentPublicV2.schema.ts | 6 +++ .../contactEstablishment.e2e.test.ts | 2 +- ...entAggregateRepository.integration.test.ts | 1 - .../entities/ImmersionOfferEntity.ts | 4 +- .../ContactEstablishment.unit.test.ts | 2 +- ...rtDiscussionAggregateFromContactRequest.ts | 2 +- ...onAggregateFromContactRequest.unit.test.ts | 4 +- .../notifications/NotifyContactRequest.ts | 10 ++-- .../NotifyContactRequest.unit.test.ts | 6 ++- .../valueObjects/LaBonneBoiteCompanyVO.ts | 54 +------------------ ...rmEstablishmentToEstablishmentAggregate.ts | 7 ++- .../app/components/search/ContactByEmail.tsx | 8 +-- .../app/components/search/ContactByPhone.tsx | 8 +-- .../app/components/search/ContactInPerson.tsx | 8 +-- .../components/search/ContactModalContent.tsx | 20 ++++--- .../components/search/SearchListResults.tsx | 5 +- .../src/app/pages/group/GroupListResults.tsx | 5 +- .../contactEstablishmentRequest.dto.ts | 4 +- .../contactEstablishmentRequest.schema.ts | 4 +- 23 files changed, 139 insertions(+), 107 deletions(-) create mode 100644 back/src/adapters/primary/routers/DtoAndSchemas/v2/input/ContactEstablishmentPublicV2.dto.ts create mode 100644 back/src/adapters/primary/routers/DtoAndSchemas/v2/input/ContactEstablishmentPublicV2.schema.ts diff --git a/back/src/_testBuilders/ImmersionOfferEntityV2Builder.ts b/back/src/_testBuilders/ImmersionOfferEntityV2Builder.ts index e18225ae1e..f59312a849 100644 --- a/back/src/_testBuilders/ImmersionOfferEntityV2Builder.ts +++ b/back/src/_testBuilders/ImmersionOfferEntityV2Builder.ts @@ -3,6 +3,8 @@ import { ImmersionOfferEntityV2 } from "../domain/immersionOffer/entities/Immers const validImmersionOfferEntityV2: ImmersionOfferEntityV2 = { romeCode: "B1805", + appellationLabel: "Styliste", + appellationCode: "19540", score: 4.5, createdAt: new Date("2022-05-15T12:00:00.000"), }; @@ -23,13 +25,13 @@ export class ImmersionOfferEntityV2Builder }); } - withAppellationCode(appellationCode: string | undefined) { + withAppellationCode(appellationCode: string) { return new ImmersionOfferEntityV2Builder({ ...this.entity, appellationCode, }); } - withAppellationLabel(appellationLabel: string | undefined) { + withAppellationLabel(appellationLabel: string) { return new ImmersionOfferEntityV2Builder({ ...this.entity, appellationLabel, diff --git a/back/src/adapters/primary/routers/DtoAndSchemas/v1/input/ContactEstablishmentPublicV1.dto.ts b/back/src/adapters/primary/routers/DtoAndSchemas/v1/input/ContactEstablishmentPublicV1.dto.ts index e2303d06c6..60849cff52 100644 --- a/back/src/adapters/primary/routers/DtoAndSchemas/v1/input/ContactEstablishmentPublicV1.dto.ts +++ b/back/src/adapters/primary/routers/DtoAndSchemas/v1/input/ContactEstablishmentPublicV1.dto.ts @@ -18,9 +18,11 @@ export type ContactEstablishmentByMailPublicV1Dto = ContactInformationPublicV1<"EMAIL"> & { message: string; }; -type ContactEstablishmentInPersonPublicV1Dto = + +export type ContactEstablishmentInPersonPublicV1Dto = ContactInformationPublicV1<"IN_PERSON">; -type ContactEstablishmentByPhonePublicV1Dto = + +export type ContactEstablishmentByPhonePublicV1Dto = ContactInformationPublicV1<"PHONE">; export type ContactEstablishmentPublicV1Dto = @@ -31,12 +33,24 @@ export type ContactEstablishmentPublicV1Dto = export const contactEstablishmentPublicV1ToDomain = ( contactRequest: ContactEstablishmentPublicV1Dto, ): ContactEstablishmentRequestDto => { + // const { + // contactMode, + // potentialBeneficiaryEmail, + // offer, + // potentialBeneficiaryFirstName, + // potentialBeneficiaryLastName, + // siret, + // } = contactRequest; if (contactRequest.contactMode === "EMAIL") return { ...contactRequest, + romeCode: contactRequest.offer.romeCode, potentialBeneficiaryPhone: "Numéro de téléphone non communiqué", immersionObjective: null, }; - return contactRequest; + return { + ...contactRequest, + romeCode: contactRequest.offer.romeCode, + }; }; diff --git a/back/src/adapters/primary/routers/DtoAndSchemas/v1/input/ContactEstablishmentPublicV1.schema.ts b/back/src/adapters/primary/routers/DtoAndSchemas/v1/input/ContactEstablishmentPublicV1.schema.ts index b94eef73fc..bd368a07cd 100644 --- a/back/src/adapters/primary/routers/DtoAndSchemas/v1/input/ContactEstablishmentPublicV1.schema.ts +++ b/back/src/adapters/primary/routers/DtoAndSchemas/v1/input/ContactEstablishmentPublicV1.schema.ts @@ -1,15 +1,17 @@ import { z } from "zod"; import { - contactEstablishmentByPhoneSchema, - contactEstablishmentInPersonSchema, emailSchema, preferEmailContactSchema, + preferInPersonContactSchema, + preferPhoneContactSchema, romeSchema, siretSchema, zTrimmedString, } from "shared"; import { ContactEstablishmentByMailPublicV1Dto, + ContactEstablishmentByPhonePublicV1Dto, + ContactEstablishmentInPersonPublicV1Dto, ContactEstablishmentPublicV1Dto, } from "./ContactEstablishmentPublicV1.dto"; @@ -28,11 +30,22 @@ const contactEstablishmentByMailSchemaV1: z.Schema = + z.object({ + ...commonFields, + contactMode: preferPhoneContactSchema, + }); +const contactEstablishmentInPersonSchemaV1: z.Schema = + z.object({ + ...commonFields, + contactMode: preferInPersonContactSchema, + }); + const contactEstablishmentRequestSchemaV1: z.Schema = z.union([ contactEstablishmentByMailSchemaV1, - contactEstablishmentByPhoneSchema, - contactEstablishmentInPersonSchema, + contactEstablishmentByPhoneSchemaV1, + contactEstablishmentInPersonSchemaV1, ]); export const contactEstablishmentPublicV1Schema: z.Schema = diff --git a/back/src/adapters/primary/routers/DtoAndSchemas/v2/input/ContactEstablishmentPublicV2.dto.ts b/back/src/adapters/primary/routers/DtoAndSchemas/v2/input/ContactEstablishmentPublicV2.dto.ts new file mode 100644 index 0000000000..ad8b031952 --- /dev/null +++ b/back/src/adapters/primary/routers/DtoAndSchemas/v2/input/ContactEstablishmentPublicV2.dto.ts @@ -0,0 +1,39 @@ +import { + ContactEstablishmentRequestDto, + ContactMethod, + ImmersionObjective, + RomeCode, + SiretDto, +} from "shared"; + +type ContactInformationPublicV2 = { + romeCode: RomeCode; + siret: SiretDto; + potentialBeneficiaryFirstName: string; + potentialBeneficiaryLastName: string; + potentialBeneficiaryEmail: string; + contactMode: T; +}; + +type ContactEstablishmentByMailPublicV2Dto = + ContactInformationPublicV2<"EMAIL"> & { + message: string; + potentialBeneficiaryPhone: string; + immersionObjective: ImmersionObjective | null; + potentialBeneficiaryResumeLink?: string; + }; + +type ContactEstablishmentInPersonPublicV2Dto = + ContactInformationPublicV2<"IN_PERSON">; + +type ContactEstablishmentByPhonePublicV2Dto = + ContactInformationPublicV2<"PHONE">; + +export type ContactEstablishmentPublicV2Dto = + | ContactEstablishmentByPhonePublicV2Dto + | ContactEstablishmentInPersonPublicV2Dto + | ContactEstablishmentByMailPublicV2Dto; + +export const contactEstablishmentPublicV2ToDomain = ( + contactRequest: ContactEstablishmentPublicV2Dto, +): ContactEstablishmentRequestDto => contactRequest; diff --git a/back/src/adapters/primary/routers/DtoAndSchemas/v2/input/ContactEstablishmentPublicV2.schema.ts b/back/src/adapters/primary/routers/DtoAndSchemas/v2/input/ContactEstablishmentPublicV2.schema.ts new file mode 100644 index 0000000000..78c9cf3b69 --- /dev/null +++ b/back/src/adapters/primary/routers/DtoAndSchemas/v2/input/ContactEstablishmentPublicV2.schema.ts @@ -0,0 +1,6 @@ +import { z } from "zod"; +import { contactEstablishmentRequestSchema } from "shared"; +import { ContactEstablishmentPublicV2Dto } from "./ContactEstablishmentPublicV2.dto"; + +export const contactEstablishmentPublicV2Schema: z.Schema = + contactEstablishmentRequestSchema; diff --git a/back/src/adapters/primary/routers/createEstablishment/contactEstablishment.e2e.test.ts b/back/src/adapters/primary/routers/createEstablishment/contactEstablishment.e2e.test.ts index aa00b6d8b0..c22c82e008 100644 --- a/back/src/adapters/primary/routers/createEstablishment/contactEstablishment.e2e.test.ts +++ b/back/src/adapters/primary/routers/createEstablishment/contactEstablishment.e2e.test.ts @@ -20,7 +20,7 @@ const siret = "11112222333344"; const contactId = "theContactId"; const validRequest: ContactEstablishmentRequestDto = { - offer: { romeLabel: "Stylisme", romeCode: "B1805" }, + romeCode: "B1805", siret, contactMode: "EMAIL", potentialBeneficiaryFirstName: "potential_beneficiary_first_name", diff --git a/back/src/adapters/secondary/pg/PgEstablishmentAggregateRepository.integration.test.ts b/back/src/adapters/secondary/pg/PgEstablishmentAggregateRepository.integration.test.ts index b9b3f6c243..933df1bbc9 100644 --- a/back/src/adapters/secondary/pg/PgEstablishmentAggregateRepository.integration.test.ts +++ b/back/src/adapters/secondary/pg/PgEstablishmentAggregateRepository.integration.test.ts @@ -1064,7 +1064,6 @@ describe("Postgres implementation of immersion offer repository", () => { .withEstablishmentSiret(existingSiret) .withImmersionOffers([ new ImmersionOfferEntityV2Builder() // Offer with code A1402 without an appellation - .withAppellationCode(undefined) .withRomeCode("A1402") .build(), new ImmersionOfferEntityV2Builder() // Offer with code A1401 and an appellation diff --git a/back/src/domain/immersionOffer/entities/ImmersionOfferEntity.ts b/back/src/domain/immersionOffer/entities/ImmersionOfferEntity.ts index 94cfb4e299..22006c5f93 100644 --- a/back/src/domain/immersionOffer/entities/ImmersionOfferEntity.ts +++ b/back/src/domain/immersionOffer/entities/ImmersionOfferEntity.ts @@ -3,7 +3,7 @@ import { RomeCode } from "shared"; export type ImmersionOfferEntityV2 = { romeCode: RomeCode; score: number; - appellationCode?: string; // TODO : make it mandatory - appellationLabel?: string; // TODO : make it mandatory + appellationCode: string; + appellationLabel: string; createdAt: Date; }; diff --git a/back/src/domain/immersionOffer/useCases/ContactEstablishment.unit.test.ts b/back/src/domain/immersionOffer/useCases/ContactEstablishment.unit.test.ts index 0dc601f1b0..e29f150fb6 100644 --- a/back/src/domain/immersionOffer/useCases/ContactEstablishment.unit.test.ts +++ b/back/src/domain/immersionOffer/useCases/ContactEstablishment.unit.test.ts @@ -23,7 +23,7 @@ const siret = "11112222333344"; const contactId = "theContactId"; const validRequest: ContactEstablishmentRequestDto = { - offer: { romeLabel: "my rome label", romeCode: "B1805" }, + romeCode: "B1805", siret, contactMode: "PHONE", potentialBeneficiaryFirstName: "potential_beneficiary_first_name", diff --git a/back/src/domain/immersionOffer/useCases/InsertDiscussionAggregateFromContactRequest.ts b/back/src/domain/immersionOffer/useCases/InsertDiscussionAggregateFromContactRequest.ts index d2cd12600a..72013cd0aa 100644 --- a/back/src/domain/immersionOffer/useCases/InsertDiscussionAggregateFromContactRequest.ts +++ b/back/src/domain/immersionOffer/useCases/InsertDiscussionAggregateFromContactRequest.ts @@ -34,7 +34,7 @@ export class InsertDiscussionAggregateFromContactRequest extends TransactionalUs potentialBeneficiaryFirstName: params.potentialBeneficiaryFirstName, potentialBeneficiaryLastName: params.potentialBeneficiaryLastName, potentialBeneficiaryEmail: params.potentialBeneficiaryEmail, - romeCode: params.offer.romeCode, + romeCode: params.romeCode, siret: params.siret, contactMode: params.contactMode, createdAt: now, diff --git a/back/src/domain/immersionOffer/useCases/InsertDiscussionAggregateFromContactRequest.unit.test.ts b/back/src/domain/immersionOffer/useCases/InsertDiscussionAggregateFromContactRequest.unit.test.ts index 6d6db123c0..47df00364b 100644 --- a/back/src/domain/immersionOffer/useCases/InsertDiscussionAggregateFromContactRequest.unit.test.ts +++ b/back/src/domain/immersionOffer/useCases/InsertDiscussionAggregateFromContactRequest.unit.test.ts @@ -52,7 +52,7 @@ describe("Insert discussion aggregate from contact request DTO", () => { // Act const contactRequestDto: ContactEstablishmentRequestDto = { - offer: { romeCode: "A1289", romeLabel: "Guitariste" }, + romeCode: "A1289", siret: "01234567891011", potentialBeneficiaryFirstName: "Antoine", potentialBeneficiaryLastName: "Tourasse", @@ -144,7 +144,7 @@ describe("Insert discussion aggregate from contact request DTO", () => { uuidGenerator.setNextUuid("discussion2"); const secondContactRequestDto: ContactEstablishmentRequestDto = { - offer: { romeCode: "A1289", romeLabel: "Guitariste" }, + romeCode: "A1289", siret, potentialBeneficiaryFirstName: "Bob", potentialBeneficiaryLastName: "Marley", diff --git a/back/src/domain/immersionOffer/useCases/notifications/NotifyContactRequest.ts b/back/src/domain/immersionOffer/useCases/notifications/NotifyContactRequest.ts index 3f5fe8da6d..a536a769fd 100644 --- a/back/src/domain/immersionOffer/useCases/notifications/NotifyContactRequest.ts +++ b/back/src/domain/immersionOffer/useCases/notifications/NotifyContactRequest.ts @@ -24,7 +24,7 @@ export class NotifyContactRequest extends TransactionalUseCase { - const { siret, offer: rome } = payload; + const { siret } = payload; const establishmentAggregate = await uow.establishmentAggregateRepository.getEstablishmentAggregateBySiret( @@ -59,6 +59,10 @@ export class NotifyContactRequest extends TransactionalUseCase email !== contact.email, ); + const immersionOffer = establishmentAggregate.immersionOffers.at(0); + + if (!immersionOffer) throw new Error("No immesion offer found"); + await this.saveNotificationAndRelatedEvent(uow, { kind: "email", templatedContent: { @@ -69,9 +73,7 @@ export class NotifyContactRequest extends TransactionalUseCase - ({ romeCode, appellationCode }: AppellationDto): ImmersionOfferEntityV2 => ({ + ({ + romeCode, + appellationCode, + appellationLabel, + }: AppellationDto): ImmersionOfferEntityV2 => ({ romeCode, appellationCode, + appellationLabel, score: offerFromFormScore, createdAt: timeGateway.now(), }); diff --git a/front/src/app/components/search/ContactByEmail.tsx b/front/src/app/components/search/ContactByEmail.tsx index 54d9b242a1..fca2ab6040 100644 --- a/front/src/app/components/search/ContactByEmail.tsx +++ b/front/src/app/components/search/ContactByEmail.tsx @@ -11,7 +11,7 @@ import { contactEstablishmentByMailFormSchema, conventionObjectiveOptions, domElementIds, - RomeDto, + RomeCode, SiretDto, } from "shared"; import { makeFieldError } from "src/app/hooks/formContents.hooks"; @@ -20,7 +20,7 @@ import { EmailValidationInput } from "../forms/commons/EmailValidationInput"; type ContactByEmailProps = { siret: SiretDto; - offer: RomeDto; + romeCode: RomeCode; onSuccess: () => void; onClose: () => void; }; @@ -36,13 +36,13 @@ En vous remerciant,`; export const ContactByEmail = ({ siret, - offer, + romeCode, onSuccess, onClose, }: ContactByEmailProps) => { const initialValues: ContactEstablishmentByMailDto = { siret, - offer, + romeCode, contactMode: "EMAIL", potentialBeneficiaryFirstName: "", potentialBeneficiaryLastName: "", diff --git a/front/src/app/components/search/ContactByPhone.tsx b/front/src/app/components/search/ContactByPhone.tsx index 2459bcab07..bf161437a7 100644 --- a/front/src/app/components/search/ContactByPhone.tsx +++ b/front/src/app/components/search/ContactByPhone.tsx @@ -7,7 +7,7 @@ import { ContactEstablishmentByPhoneDto, contactEstablishmentByPhoneSchema, domElementIds, - RomeDto, + RomeCode, SiretDto, } from "shared"; import { makeFieldError } from "src/app/hooks/formContents.hooks"; @@ -15,18 +15,18 @@ import { immersionSearchGateway } from "src/config/dependencies"; type ContactByPhoneProps = { siret: SiretDto; - offer: RomeDto; + romeCode: RomeCode; onSuccess: () => void; }; export const ContactByPhone = ({ siret, - offer, + romeCode, onSuccess, }: ContactByPhoneProps) => { const initialValues: ContactEstablishmentByPhoneDto = { siret, - offer, + romeCode, contactMode: "PHONE", potentialBeneficiaryFirstName: "", potentialBeneficiaryLastName: "", diff --git a/front/src/app/components/search/ContactInPerson.tsx b/front/src/app/components/search/ContactInPerson.tsx index d2978187a3..02b643bac9 100644 --- a/front/src/app/components/search/ContactInPerson.tsx +++ b/front/src/app/components/search/ContactInPerson.tsx @@ -7,7 +7,7 @@ import { ContactEstablishmentInPersonDto, contactEstablishmentInPersonSchema, domElementIds, - RomeDto, + RomeCode, SiretDto, } from "shared"; import { makeFieldError } from "src/app/hooks/formContents.hooks"; @@ -15,18 +15,18 @@ import { immersionSearchGateway } from "src/config/dependencies"; type ContactInPersonProps = { siret: SiretDto; - offer: RomeDto; + romeCode: RomeCode; onSuccess: () => void; }; export const ContactInPerson = ({ siret, - offer, + romeCode, onSuccess, }: ContactInPersonProps) => { const initialValues: ContactEstablishmentInPersonDto = { siret, - offer, + romeCode, contactMode: "IN_PERSON", potentialBeneficiaryFirstName: "", potentialBeneficiaryLastName: "", diff --git a/front/src/app/components/search/ContactModalContent.tsx b/front/src/app/components/search/ContactModalContent.tsx index 06cd8eea66..21b670bf83 100644 --- a/front/src/app/components/search/ContactModalContent.tsx +++ b/front/src/app/components/search/ContactModalContent.tsx @@ -2,7 +2,7 @@ import React from "react"; import { fr } from "@codegouvfr/react-dsfr"; import { ContactMethod, - RomeDto, + RomeCode, SearchImmersionResultDto, SiretDto, } from "shared"; @@ -13,7 +13,7 @@ import { ContactInPerson } from "./ContactInPerson"; export type ContactModalContentProps = { contactMethod?: ContactMethod; siret: SiretDto; - offer: RomeDto; + romeCode: RomeCode; searchResultData?: SearchImmersionResultDto; onSuccess: () => void; onClose: () => void; @@ -22,7 +22,7 @@ export type ContactModalContentProps = { export const ModalContactContent = ({ contactMethod, siret, - offer, + romeCode, onSuccess, searchResultData, onClose, @@ -32,18 +32,26 @@ export const ModalContactContent = ({ return ( ); case "PHONE": return ( - + ); case "IN_PERSON": return ( - + ); default: return ; diff --git a/front/src/app/components/search/SearchListResults.tsx b/front/src/app/components/search/SearchListResults.tsx index af681927e2..2d7d76f07e 100644 --- a/front/src/app/components/search/SearchListResults.tsx +++ b/front/src/app/components/search/SearchListResults.tsx @@ -100,10 +100,7 @@ export const SearchListResults = () => { onButtonClick={() => { setModalContent({ siret: searchResult.siret, - offer: { - romeCode: searchResult.rome, - romeLabel: searchResult.romeLabel, - }, + romeCode: searchResult.rome, contactMethod: searchResult.contactMode, searchResultData: searchResult, onClose: () => closeContactModal(), diff --git a/front/src/app/pages/group/GroupListResults.tsx b/front/src/app/pages/group/GroupListResults.tsx index 5cb07be2b0..698b38701d 100644 --- a/front/src/app/pages/group/GroupListResults.tsx +++ b/front/src/app/pages/group/GroupListResults.tsx @@ -71,10 +71,7 @@ export const GroupListResults = ({ results }: GroupListResultsProps) => { onButtonClick={() => { setModalContent({ siret: searchResult.siret, - offer: { - romeCode: searchResult.rome, - romeLabel: searchResult.romeLabel, - }, + romeCode: searchResult.rome, contactMethod: searchResult.contactMode, searchResultData: searchResult, onClose: () => closeContactModal(), diff --git a/shared/src/contactEstablishmentRequest/contactEstablishmentRequest.dto.ts b/shared/src/contactEstablishmentRequest/contactEstablishmentRequest.dto.ts index 2fa26c8095..a13fff16d1 100644 --- a/shared/src/contactEstablishmentRequest/contactEstablishmentRequest.dto.ts +++ b/shared/src/contactEstablishmentRequest/contactEstablishmentRequest.dto.ts @@ -1,10 +1,10 @@ import { ImmersionObjective } from "../convention/convention.dto"; import { ContactMethod } from "../formEstablishment/FormEstablishment.dto"; -import { RomeDto } from "../romeAndAppellationDtos/romeAndAppellation.dto"; +import { RomeCode } from "../romeAndAppellationDtos/romeAndAppellation.dto"; import { SiretDto } from "../siret/siret"; export type ContactInformations = { - offer: RomeDto; + romeCode: RomeCode; siret: SiretDto; potentialBeneficiaryFirstName: string; potentialBeneficiaryLastName: string; diff --git a/shared/src/contactEstablishmentRequest/contactEstablishmentRequest.schema.ts b/shared/src/contactEstablishmentRequest/contactEstablishmentRequest.schema.ts index 28f4b4f38c..ed5915d4bd 100644 --- a/shared/src/contactEstablishmentRequest/contactEstablishmentRequest.schema.ts +++ b/shared/src/contactEstablishmentRequest/contactEstablishmentRequest.schema.ts @@ -4,7 +4,7 @@ import { ImmersionObjective, } from "../convention/convention.dto"; import { emailSchema } from "../email/email.schema"; -import { romeSchema } from "../romeAndAppellationDtos/romeAndAppellation.schema"; +import { romeCodeSchema } from "../rome"; import { siretSchema } from "../siret/siret.schema"; import { localization, @@ -20,7 +20,7 @@ import { } from "./contactEstablishmentRequest.dto"; const commonFields = { - offer: romeSchema, + romeCode: romeCodeSchema, siret: siretSchema, potentialBeneficiaryFirstName: zTrimmedString, potentialBeneficiaryLastName: zTrimmedString, From 880f55aedd3f68504dc3f3bf42ba2900f17e4aef Mon Sep 17 00:00:00 2001 From: clement-duport Date: Wed, 21 Jun 2023 10:36:37 +0200 Subject: [PATCH 2/2] create api key auth router v2 plus test and update api v2 contact request documentation --- .../input/ContactEstablishmentPublicV1.dto.ts | 8 - .../contactEstablishmentPublicV2.e2e.test.ts | 96 ++++++++++++ .../createApiKeyAuthRouter.v2.ts | 37 +++++ .../searchImmersionApi.e2e.test.ts | 119 +-------------- .../searchImmersionApiPublicV1.e2e.test.ts | 141 ++++++++++++++++++ back/src/adapters/primary/server.ts | 2 + ...entAggregateRepository.integration.test.ts | 2 +- doc/api/search/contactEstablishmentV2.md | 82 ++++++++++ 8 files changed, 361 insertions(+), 126 deletions(-) create mode 100644 back/src/adapters/primary/routers/apiKeyAuthRouter/contactEstablishmentPublicV2.e2e.test.ts create mode 100644 back/src/adapters/primary/routers/apiKeyAuthRouter/createApiKeyAuthRouter.v2.ts create mode 100644 back/src/adapters/primary/routers/apiKeyAuthRouter/searchImmersionApiPublicV1.e2e.test.ts create mode 100644 doc/api/search/contactEstablishmentV2.md diff --git a/back/src/adapters/primary/routers/DtoAndSchemas/v1/input/ContactEstablishmentPublicV1.dto.ts b/back/src/adapters/primary/routers/DtoAndSchemas/v1/input/ContactEstablishmentPublicV1.dto.ts index 60849cff52..0bbd4cfb2f 100644 --- a/back/src/adapters/primary/routers/DtoAndSchemas/v1/input/ContactEstablishmentPublicV1.dto.ts +++ b/back/src/adapters/primary/routers/DtoAndSchemas/v1/input/ContactEstablishmentPublicV1.dto.ts @@ -33,14 +33,6 @@ export type ContactEstablishmentPublicV1Dto = export const contactEstablishmentPublicV1ToDomain = ( contactRequest: ContactEstablishmentPublicV1Dto, ): ContactEstablishmentRequestDto => { - // const { - // contactMode, - // potentialBeneficiaryEmail, - // offer, - // potentialBeneficiaryFirstName, - // potentialBeneficiaryLastName, - // siret, - // } = contactRequest; if (contactRequest.contactMode === "EMAIL") return { ...contactRequest, diff --git a/back/src/adapters/primary/routers/apiKeyAuthRouter/contactEstablishmentPublicV2.e2e.test.ts b/back/src/adapters/primary/routers/apiKeyAuthRouter/contactEstablishmentPublicV2.e2e.test.ts new file mode 100644 index 0000000000..e924b8b04f --- /dev/null +++ b/back/src/adapters/primary/routers/apiKeyAuthRouter/contactEstablishmentPublicV2.e2e.test.ts @@ -0,0 +1,96 @@ +import { SuperTest, Test } from "supertest"; +import { rueSaintHonoreDto } from "../../../../_testBuilders/addressDtos"; +import { AppConfigBuilder } from "../../../../_testBuilders/AppConfigBuilder"; +import { buildTestApp } from "../../../../_testBuilders/buildTestApp"; +import { ContactEntityBuilder } from "../../../../_testBuilders/ContactEntityBuilder"; +import { EstablishmentAggregateBuilder } from "../../../../_testBuilders/EstablishmentAggregateBuilder"; +import { EstablishmentEntityBuilder } from "../../../../_testBuilders/EstablishmentEntityBuilder"; +import { ImmersionOfferEntityV2Builder } from "../../../../_testBuilders/ImmersionOfferEntityV2Builder"; +import { + InMemoryEstablishmentAggregateRepository, + TEST_POSITION, +} from "../../../secondary/immersionOffer/InMemoryEstablishmentAggregateRepository"; +import { validAuthorizedApiKeyId } from "../../../secondary/InMemoryApiConsumerRepository"; +import { ContactEstablishmentPublicV2Dto } from "../DtoAndSchemas/v2/input/ContactEstablishmentPublicV2.dto"; + +const contactEstablishment: ContactEstablishmentPublicV2Dto = { + contactMode: "EMAIL", + message: "Salut !", + siret: "11112222333344", + romeCode: "A0000", + potentialBeneficiaryEmail: "john.doe@mail.com", + potentialBeneficiaryFirstName: "John", + potentialBeneficiaryLastName: "Doe", + immersionObjective: "Confirmer un projet professionnel", + potentialBeneficiaryPhone: "0654334567", +}; + +describe("POST contact-establishment public V2 route", () => { + let request: SuperTest; + let establishmentAggregateRepository: InMemoryEstablishmentAggregateRepository; + let authToken: string; + + beforeEach(async () => { + const config = new AppConfigBuilder() + .withRepositories("IN_MEMORY") + .withAuthorizedApiKeyIds([validAuthorizedApiKeyId]) + .build(); + + const { + request: testAppRequest, + generateApiJwt, + inMemoryUow, + } = await buildTestApp(config); + request = testAppRequest; + authToken = generateApiJwt({ + id: validAuthorizedApiKeyId, + }); + establishmentAggregateRepository = + inMemoryUow.establishmentAggregateRepository; + }); + + it("refuses to contact if no api key is provided", async () => { + const response = await request.post(`/v2/contact-establishment`).send({}); + expect(response.status).toBe(401); + }); + + it("returns 404 if siret not found", async () => { + const response = await request + .post(`/v2/contact-establishment`) + .set("Authorization", authToken) + .send(contactEstablishment); + + expect(response.status).toBe(404); + }); + + it("contacts the establishment when everything goes right", async () => { + await establishmentAggregateRepository.insertEstablishmentAggregates([ + new EstablishmentAggregateBuilder() + .withEstablishment( + new EstablishmentEntityBuilder() + .withSiret(contactEstablishment.siret) + .withPosition(TEST_POSITION) + .withNumberOfEmployeeRange("10-19") + .withAddress(rueSaintHonoreDto) + .build(), + ) + .withContact( + new ContactEntityBuilder().withContactMethod("EMAIL").build(), + ) + .withImmersionOffers([ + new ImmersionOfferEntityV2Builder() + .withRomeCode(contactEstablishment.romeCode) + .build(), + ]) + .build(), + ]); + + const response = await request + .post(`/v2/contact-establishment`) + .set("Authorization", authToken) + .send(contactEstablishment); + + expect(response.status).toBe(200); + expect(response.body).toBe(""); + }); +}); diff --git a/back/src/adapters/primary/routers/apiKeyAuthRouter/createApiKeyAuthRouter.v2.ts b/back/src/adapters/primary/routers/apiKeyAuthRouter/createApiKeyAuthRouter.v2.ts new file mode 100644 index 0000000000..17a7328ac6 --- /dev/null +++ b/back/src/adapters/primary/routers/apiKeyAuthRouter/createApiKeyAuthRouter.v2.ts @@ -0,0 +1,37 @@ +import { Router } from "express"; +import { contactEstablishmentRoute, pipeWithValue } from "shared"; +import { createLogger } from "../../../../utils/logger"; +import type { AppDependencies } from "../../config/createAppDependencies"; +import { + ForbiddenError, + validateAndParseZodSchema, +} from "../../helpers/httpErrors"; +import { sendHttpResponse } from "../../helpers/sendHttpResponse"; +import { contactEstablishmentPublicV2ToDomain } from "../DtoAndSchemas/v2/input/ContactEstablishmentPublicV2.dto"; +import { contactEstablishmentPublicV2Schema } from "../DtoAndSchemas/v2/input/ContactEstablishmentPublicV2.schema"; + +const logger = createLogger(__filename); + +export const createApiKeyAuthRouterV2 = (deps: AppDependencies) => { + const publicV2Router = Router({ mergeParams: true }); + + publicV2Router.use(deps.apiKeyAuthMiddleware); + + publicV2Router.route(`/${contactEstablishmentRoute}`).post(async (req, res) => + sendHttpResponse(req, res, () => { + if (!req.apiConsumer?.isAuthorized) throw new ForbiddenError(); + return pipeWithValue( + validateAndParseZodSchema( + contactEstablishmentPublicV2Schema, + req.body, + logger, + ), + contactEstablishmentPublicV2ToDomain, + (contactRequest) => + deps.useCases.contactEstablishment.execute(contactRequest), + ); + }), + ); + + return publicV2Router; +}; diff --git a/back/src/adapters/primary/routers/apiKeyAuthRouter/searchImmersionApi.e2e.test.ts b/back/src/adapters/primary/routers/apiKeyAuthRouter/searchImmersionApi.e2e.test.ts index cdb5e1687a..4b4e706b06 100644 --- a/back/src/adapters/primary/routers/apiKeyAuthRouter/searchImmersionApi.e2e.test.ts +++ b/back/src/adapters/primary/routers/apiKeyAuthRouter/searchImmersionApi.e2e.test.ts @@ -1,5 +1,5 @@ import { SuperTest, Test } from "supertest"; -import { immersionOffersRoute, searchImmersionRoute__v0 } from "shared"; +import { searchImmersionRoute__v0 } from "shared"; import { avenueChampsElysees, avenueChampsElyseesDto, @@ -11,27 +11,18 @@ import { EstablishmentEntityBuilder, } from "../../../../_testBuilders/EstablishmentEntityBuilder"; import { ImmersionOfferEntityV2Builder } from "../../../../_testBuilders/ImmersionOfferEntityV2Builder"; -import { GenerateApiConsumerJwt } from "../../../../domain/auth/jwt"; import { InMemoryEstablishmentAggregateRepository } from "../../../secondary/immersionOffer/InMemoryEstablishmentAggregateRepository"; import { SearchImmersionResultPublicV0 } from "../DtoAndSchemas/v0/output/SearchImmersionResultPublicV0.dto"; -import { SearchImmersionResultPublicV1 } from "../DtoAndSchemas/v1/output/SearchImmersionResultPublicV1.dto"; describe("search-immersion route", () => { let request: SuperTest; let establishmentAggregateRepository: InMemoryEstablishmentAggregateRepository; - let generateApiJwt: GenerateApiConsumerJwt; - beforeEach(async () => { - const { - request: testAppRequest, - generateApiJwt: testAppGenerateApiJwt, - inMemoryUow, - } = await buildTestApp(); + const { request: testAppRequest, inMemoryUow } = await buildTestApp(); request = testAppRequest; establishmentAggregateRepository = inMemoryUow.establishmentAggregateRepository; - generateApiJwt = testAppGenerateApiJwt; }); describe(`v0 - /${searchImmersionRoute__v0}`, () => { @@ -115,110 +106,4 @@ describe("search-immersion route", () => { .expect(400, /Code ROME incorrect/); }); }); - describe(`v1 - /v1/${immersionOffersRoute}`, () => { - describe("verify consumer is authenticated and authorized", () => { - it("rejects unauthenticated requests", async () => { - await request - .get( - `/v1/${immersionOffersRoute}?rome=XXXXX&distance_km=30&longitude=2.34999&latitude=48.8531&sortedBy=distance`, - ) - .expect(401, { error: "forbidden: unauthenticated" }); - }); - it("rejects unauthorized consumer", async () => { - await request - .get( - `/v1/${immersionOffersRoute}?rome=XXXXX&distance_km=30&longitude=2.34999&latitude=48.8531&sortedBy=distance`, - ) - .set("Authorization", generateApiJwt({ id: "my-unauthorized-id" })) - .expect(403, { - error: "forbidden: unauthorised consumer Id", - }); - }); - }); - describe("authenficated consumer", () => { - it("with given rome and position", async () => { - // Prepare - await establishmentAggregateRepository.insertEstablishmentAggregates([ - new EstablishmentAggregateBuilder() - .withImmersionOffers([ - new ImmersionOfferEntityV2Builder().withRomeCode("A1000").build(), - ]) - .withEstablishment( - new EstablishmentEntityBuilder() - .withPosition({ - lat: 48.8531, - lon: 2.34999, - }) - .withWebsite("www.jobs.fr") - .build(), - ) - .build(), - ]); - - // Act and assert - const expectedResult: SearchImmersionResultPublicV1[] = [ - { - address: avenueChampsElysees, - naf: defaultNafCode, - nafLabel: "test_naf_label", - name: "Company inside repository", - website: "www.jobs.fr", - additionalInformation: "", - rome: "A1000", - romeLabel: "test_rome_label", - appellationLabels: ["test_appellation_label"], - siret: "78000403200019", - voluntaryToImmersion: true, - contactMode: "EMAIL", - numberOfEmployeeRange: "10-19", - distance_m: 719436, - position: { lat: 43.8666, lon: 8.3333 }, - city: avenueChampsElyseesDto.city, - }, - ]; - const response = await request - .get( - `/v1/${immersionOffersRoute}?rome=A1000&distance_km=30&longitude=2.34999&latitude=48.8531&sortedBy=distance&address=5%20rue%20des%20champs%20elysees%2044000%20Nantes`, - ) - .set("Authorization", generateApiJwt({ id: "my-authorized-id" })); - expect(response.body).toEqual(expectedResult); - expect(response.status).toBe(200); - }); - it("accept address with only city", async () => { - const response = await request - .get( - `/v1/${immersionOffersRoute}?rome=A1000&distance_km=30&longitude=2.34999&latitude=48.8531&sortedBy=distance&address=Lyon`, - ) - .set("Authorization", generateApiJwt({ id: "my-authorized-id" })); - expect(response.status).toBe(200); - }); - it("with no specified rome", async () => { - await request - .get( - `/v1/${immersionOffersRoute}?distance_km=30&longitude=2.34999&latitude=48.8531&sortedBy=distance`, - ) - .set("Authorization", generateApiJwt({ id: "my-authorized-id" })) - .expect(200, []); - }); - it("with filter voluntaryToImmersion", async () => { - await request - .get( - `/v1/${immersionOffersRoute}?distance_km=30&longitude=2.34999&latitude=48.8531&voluntaryToImmersion=true&sortedBy=distance`, - ) - .set("Authorization", generateApiJwt({ id: "my-authorized-id" })) - .expect(200, []); - }); - }); - - // TODO add test which actually recovers data (and one with token, one without) - - it("rejects invalid requests with error code 400", async () => { - await request - .get( - `/v1/${immersionOffersRoute}?rome=XXXXX&distance_km=30&longitude=2.34999&latitude=48.8531&sortedBy=distance`, - ) - .set("Authorization", generateApiJwt({ id: "my-authorized-id" })) - .expect(400, /Code ROME incorrect/); - }); - }); }); diff --git a/back/src/adapters/primary/routers/apiKeyAuthRouter/searchImmersionApiPublicV1.e2e.test.ts b/back/src/adapters/primary/routers/apiKeyAuthRouter/searchImmersionApiPublicV1.e2e.test.ts new file mode 100644 index 0000000000..7ae78e2999 --- /dev/null +++ b/back/src/adapters/primary/routers/apiKeyAuthRouter/searchImmersionApiPublicV1.e2e.test.ts @@ -0,0 +1,141 @@ +import { SuperTest, Test } from "supertest"; +import { + avenueChampsElysees, + avenueChampsElyseesDto, +} from "../../../../_testBuilders/addressDtos"; +import { buildTestApp } from "../../../../_testBuilders/buildTestApp"; +import { EstablishmentAggregateBuilder } from "../../../../_testBuilders/EstablishmentAggregateBuilder"; +import { + defaultNafCode, + EstablishmentEntityBuilder, +} from "../../../../_testBuilders/EstablishmentEntityBuilder"; +import { ImmersionOfferEntityV2Builder } from "../../../../_testBuilders/ImmersionOfferEntityV2Builder"; +import { GenerateApiConsumerJwt } from "../../../../domain/auth/jwt"; +import { InMemoryEstablishmentAggregateRepository } from "../../../secondary/immersionOffer/InMemoryEstablishmentAggregateRepository"; +import { SearchImmersionResultPublicV1 } from "../DtoAndSchemas/v1/output/SearchImmersionResultPublicV1.dto"; + +describe("search-immersion route", () => { + let request: SuperTest; + let establishmentAggregateRepository: InMemoryEstablishmentAggregateRepository; + + let generateApiJwt: GenerateApiConsumerJwt; + + beforeEach(async () => { + const { + request: testAppRequest, + generateApiJwt: testAppGenerateApiJwt, + inMemoryUow, + } = await buildTestApp(); + request = testAppRequest; + establishmentAggregateRepository = + inMemoryUow.establishmentAggregateRepository; + generateApiJwt = testAppGenerateApiJwt; + }); + + describe(`v1 - /v1/immersion-offers`, () => { + describe("verify consumer is authenticated and authorized", () => { + it("rejects unauthenticated requests", async () => { + await request + .get( + `/v1/immersion-offers?rome=XXXXX&distance_km=30&longitude=2.34999&latitude=48.8531&sortedBy=distance`, + ) + .expect(401, { error: "forbidden: unauthenticated" }); + }); + it("rejects unauthorized consumer", async () => { + await request + .get( + `/v1/immersion-offers?rome=XXXXX&distance_km=30&longitude=2.34999&latitude=48.8531&sortedBy=distance`, + ) + .set("Authorization", generateApiJwt({ id: "my-unauthorized-id" })) + .expect(403, { + error: "forbidden: unauthorised consumer Id", + }); + }); + }); + describe("authenficated consumer", () => { + it("with given rome and position", async () => { + // Prepare + await establishmentAggregateRepository.insertEstablishmentAggregates([ + new EstablishmentAggregateBuilder() + .withImmersionOffers([ + new ImmersionOfferEntityV2Builder().withRomeCode("A1000").build(), + ]) + .withEstablishment( + new EstablishmentEntityBuilder() + .withPosition({ + lat: 48.8531, + lon: 2.34999, + }) + .withWebsite("www.jobs.fr") + .build(), + ) + .build(), + ]); + + // Act and assert + const expectedResult: SearchImmersionResultPublicV1[] = [ + { + address: avenueChampsElysees, + naf: defaultNafCode, + nafLabel: "test_naf_label", + name: "Company inside repository", + website: "www.jobs.fr", + additionalInformation: "", + rome: "A1000", + romeLabel: "test_rome_label", + appellationLabels: ["test_appellation_label"], + siret: "78000403200019", + voluntaryToImmersion: true, + contactMode: "EMAIL", + numberOfEmployeeRange: "10-19", + distance_m: 719436, + position: { lat: 43.8666, lon: 8.3333 }, + city: avenueChampsElyseesDto.city, + }, + ]; + const response = await request + .get( + `/v1/immersion-offers?rome=A1000&distance_km=30&longitude=2.34999&latitude=48.8531&sortedBy=distance&address=5%20rue%20des%20champs%20elysees%2044000%20Nantes`, + ) + .set("Authorization", generateApiJwt({ id: "my-authorized-id" })); + expect(response.body).toEqual(expectedResult); + expect(response.status).toBe(200); + }); + it("accept address with only city", async () => { + const response = await request + .get( + `/v1/immersion-offers?rome=A1000&distance_km=30&longitude=2.34999&latitude=48.8531&sortedBy=distance&address=Lyon`, + ) + .set("Authorization", generateApiJwt({ id: "my-authorized-id" })); + expect(response.status).toBe(200); + }); + it("with no specified rome", async () => { + await request + .get( + `/v1/immersion-offers?distance_km=30&longitude=2.34999&latitude=48.8531&sortedBy=distance`, + ) + .set("Authorization", generateApiJwt({ id: "my-authorized-id" })) + .expect(200, []); + }); + it("with filter voluntaryToImmersion", async () => { + await request + .get( + `/v1/immersion-offers?distance_km=30&longitude=2.34999&latitude=48.8531&voluntaryToImmersion=true&sortedBy=distance`, + ) + .set("Authorization", generateApiJwt({ id: "my-authorized-id" })) + .expect(200, []); + }); + }); + + // TODO add test which actually recovers data (and one with token, one without) + + it("rejects invalid requests with error code 400", async () => { + await request + .get( + `/v1/immersion-offers?rome=XXXXX&distance_km=30&longitude=2.34999&latitude=48.8531&sortedBy=distance`, + ) + .set("Authorization", generateApiJwt({ id: "my-authorized-id" })) + .expect(400, /Code ROME incorrect/); + }); + }); +}); diff --git a/back/src/adapters/primary/server.ts b/back/src/adapters/primary/server.ts index db9ba037fa..5956ee7f3e 100644 --- a/back/src/adapters/primary/server.ts +++ b/back/src/adapters/primary/server.ts @@ -21,6 +21,7 @@ import { createAdminRouter } from "./routers/admin/createAdminRouter"; import { createAgenciesRouter } from "./routers/agencies/createAgenciesRouter"; import { createApiKeyAuthRouter } from "./routers/apiKeyAuthRouter/createApiKeyAuthRouter"; import { createApiKeyAuthRouterV1 } from "./routers/apiKeyAuthRouter/createApiKeyAuthRouter.v1"; +import { createApiKeyAuthRouterV2 } from "./routers/apiKeyAuthRouter/createApiKeyAuthRouter.v2"; import { createConventionRouter } from "./routers/convention/createConventionRouter"; import { createEstablishmentRouter } from "./routers/createEstablishment/createEstablishmentRouter"; import { createValidateEmailRouter } from "./routers/emailValidation/createValidateEmailRouter"; @@ -82,6 +83,7 @@ export const createApp = async ( app.use(...createMagicLinkRouter(deps)); app.use(...createAdminRouter(deps)); app.use("/v1", createApiKeyAuthRouterV1(deps)); + app.use("/v2", createApiKeyAuthRouterV2(deps)); app.use(...createInclusionConnectedAllowedRouter(deps)); // ---- app.use(createFormCompletionRouter(deps)); diff --git a/back/src/adapters/secondary/pg/PgEstablishmentAggregateRepository.integration.test.ts b/back/src/adapters/secondary/pg/PgEstablishmentAggregateRepository.integration.test.ts index 933df1bbc9..efae988f5d 100644 --- a/back/src/adapters/secondary/pg/PgEstablishmentAggregateRepository.integration.test.ts +++ b/back/src/adapters/secondary/pg/PgEstablishmentAggregateRepository.integration.test.ts @@ -995,7 +995,7 @@ describe("Postgres implementation of immersion offer repository", () => { expectToEqual(actualSearchResultDto, { rome: "H2102", romeLabel: "Conduite d'équipement de production alimentaire", - appellationLabels: [], + appellationLabels: ["Styliste"], naf: establishment.nafDto.code, nafLabel: "Fabrication de pain et de pâtisserie fraîche", siret, diff --git a/doc/api/search/contactEstablishmentV2.md b/doc/api/search/contactEstablishmentV2.md new file mode 100644 index 0000000000..9b18ad2621 --- /dev/null +++ b/doc/api/search/contactEstablishmentV2.md @@ -0,0 +1,82 @@ +# Immersion facilitée recherche d'entreprises accueillantes API v2 + +Ceci est la documentation pour consommer l'api d'immersion facilité. + +Une clé API est nécessaire pour utiliser l'api. Veuillez vous mettre en contact avec l'équipe d'immersion facilité pour l'obtenir. + +Les contacts seront diffusés (si nous les avons) car vous êtes authentifiés avec la clé. +⚠️ IL NE FAUT PAS LES EXPOSER PUBLIQUEMENT ⚠️ + +Url de staging: + + +Url de production: + + +La clé API est à fournir en authorization header de toutes les requêtes. + +## Mise en contact + +La mise en contact peut se faire sur la route : + +POST`/contact-establishment` + +schema body : + +```typescript +type ContactEstablishmentRequestBody = { + romeCode: string; + siret: string; + potentialBeneficiaryFirstName: string; + potentialBeneficiaryLastName: string; + potentialBeneficiaryEmail: string; + contactMode: "EMAIL" | "IN_PERSON" | "PHONE"; + message: string; //EMAIL ONLY + potentialBeneficiaryPhone: string; //EMAIL ONLY + immersionObjective: + | "Confirmer un projet professionnel" + | "Découvrir un métier ou un secteur d'activité" + | "Initier une démarche de recrutement"; //EMAIL ONLY + potentialBeneficiaryResumeLink?: string; //EMAIL ONLY (optional) +}; +``` + +Vous devez fournir le mode de contact qui a été renseigné par l'entreprise (dans les résultats de recherche). + +Ce qui se passe: + +- EMAIL : L’entreprise va recevoir le message du candidat par email et c’est la responsabilité de l'entreprise de recontacter le candidat (le mail du candidat est fourni à l'entreprise). + +- PHONE : Dans le cas téléphone le candidat va recevoir un email avec le téléphone de la personne à contacter dans l’entreprise. + +- IN_PERSON : Dans le cas en personne le candidat reçoit un email avec le nom de la personne, et l’addresse de l’entreprise et doit se présenter en personne. + +Exemple de requête valide (EMAIL) : + +```bash + curl 'https://immersion-facile.beta.gouv.fr/api/v2/contact-establishment' \ + -H "authorization":"your-api-key" \ + -H 'Content-Type: application/json' \ + --data-raw '{"romeCode":"B1805","siret":"12345678901234","potentialBeneficiaryFirstName":"Jean","potentialBeneficiaryLastName":"Valjean","potentialBeneficiaryEmail":"Jean.Valjean@gmail.com","contactMode":"EMAIL","message":"Bonjour, \n\nJ’ai trouvé votre entreprise sur le site https://immersion-facile.beta.gouv.fr\n***Rédigez ici votre email de motivation en suivant nos conseils.***\n \nPourriez-vous me contacter par mail ou par téléphone pour me proposer un rendez-vous ? \nJe pourrais alors vous expliquer directement mon projet. \n \nEn vous remerciant,","potentialBeneficiaryPhone":"08635343637","immersionObjective":"Initier une démarche de recrutement","potentialBeneficiaryResumeLink":"http://Jeanb.com"}' \ + --compressed +``` + +Exemple de requête valide (PHONE) : + +```bash + curl 'https://immersion-facile.beta.gouv.fr/api/v2/contact-establishment' \ + -H "authorization":"your-api-key" \ + -H 'Content-Type: application/json' \ + --data-raw '{"romeCode":"B1805","siret":"12345678901234","potentialBeneficiaryFirstName":"Jean","potentialBeneficiaryLastName":"Valjean","potentialBeneficiaryEmail":"Jean.Valjean@gmail.com","contactMode":"PHONE"}' \ + --compressed +``` + +Exemple de requête valide (IN_PERSON) : + +```bash + curl 'https://immersion-facile.beta.gouv.fr/api/v2/contact-establishment' \ + -H "authorization":"your-api-key" \ + -H 'Content-Type: application/json' \ + --data-raw '{"romeCode":"B1805","siret":"12345678901234","potentialBeneficiaryFirstName":"Jean","potentialBeneficiaryLastName":"Valjean","potentialBeneficiaryEmail":"Jean.Valjean@gmail.com","contactMode":"IN_PERSON"}' \ + --compressed +```