Skip to content

Commit

Permalink
feat: implementation client france competences (#951)
Browse files Browse the repository at this point in the history
* fix: implementation client france competences

* fix: ajout des variables d env france competences

* fix: refactor appels fallbacks

* fix: tests
  • Loading branch information
remy-auricoste authored Jan 4, 2024
1 parent 98ae6eb commit 02d9b61
Show file tree
Hide file tree
Showing 7 changed files with 656 additions and 542 deletions.
1,017 changes: 512 additions & 505 deletions .infra/vault/vault.yml

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ fileignoreconfig:
- filename: .infra/local/mongod.conf
checksum: bb2ce0c27102259a5fa39da1fb4460af9ad6ad58adc715312e53dcd69c8e6be7
- filename: .infra/vault/vault.yml
checksum: 5dbf91e38257577b478ecaa42eaeb00db013ec2137f7278943c5698a72b225e2
checksum: 5075eab036fecc87ca01087fac712e175b4b0e56fdedc0b41061312e51f81851
- filename: docker-compose.yml
checksum: 8cdd1da6c1155f26b417a27e26311d4f00b7d8bd6c21f1f86c1c7cb3f0599e6a
- filename: server/.env.test
checksum: 403ca297bf064cc81749ca1568a7c9984684db1a55065c448676194a81a44777
checksum: 5da667c842b6afec68a287fa5b2f203c0fd369d417b2172c290e7f932890603f
- filename: server/src/common/model/schema/_shared/mongoose-paginate.ts
checksum: b6762a7cb5df9bbee1f0ce893827f0991ad01514f7122a848b3b5d49b620f238
- filename: server/src/config.ts
Expand Down Expand Up @@ -58,7 +58,7 @@ fileignoreconfig:
- filename: server/tests/unit/external/zod.test.ts
checksum: f77ba0fa55f1cbd5b708fdc9f0d5b229e84a1de6c433186d96aa94a7f6388f0c
- filename: server/tests/unit/security/accessTokenService.test.ts
checksum: 93088af55c76063f108e38800fcbb1e2237ca797d4dd5bee6ceb763f74bd2d4e
checksum: 9c157ea55171c74fa50b493577f1529f5c5c005c8137b390c9dfa719f6ba87ba
- filename: server/tests/unit/security/authorisationService.test.ts
checksum: 581074420be582973bbfcdfafe1f700ca32f56e331911609cdc1cb2fb2626383
- filename: server/tests/unit/util.test.ts
Expand Down Expand Up @@ -87,6 +87,8 @@ fileignoreconfig:
checksum: 7cce935653407e000b35e98bd365a003e538aed4fed432a9a404d4f2412dd2df
- filename: ui/components/ItemDetail/ItemDetail.tsx
checksum: 1fcc0442306f83b5e45bf7da67304527598d7749b9e2642c6d4628d3b4f15a9c
- filename: ui/components/SearchForTrainingsAndJobs/services/handleSessionStorage.ts
checksum: c3c1aceede1040a9001bdc27cebbec7b3aeb0bb251b8b4264bb9d33da074af60
- filename: ui/components/espace_pro/Admin/utilisateurs/UserList.tsx
checksum: a50177afa593bae5707bdba29ef27b8f2ed0bc58487491bfff580e7e1f422243
- filename: ui/components/espace_pro/Admin/utilisateurs/infoDetails/InfoDetails.tsx
Expand All @@ -101,8 +103,6 @@ fileignoreconfig:
checksum: 1ad48425b890a5ed3de19d079692e2ef7eac76483339a469a6cd9bc6d796ad26
- filename: ui/utils/api.utils.ts
checksum: 324cd501354cfff65447c2599c4cc8966aa8aac30dda7854623dd6f7f7b0d34e
- filename: ui/components/SearchForTrainingsAndJobs/services/handleSessionStorage.ts
checksum: c3c1aceede1040a9001bdc27cebbec7b3aeb0bb251b8b4264bb9d33da074af60
scopeconfig:
- scope: node
custom_patterns:
Expand Down
2 changes: 2 additions & 0 deletions server/.env.test
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,5 @@ LBA_S3_REGION=LBA_S3_REGION
LBA_S3_BUCKET=LBA_S3_BUCKET
LBA_ENTREPRISE_API_KEY=LBA_ENTREPRISE_API_KEY
PUBLIC_VERSION=0.0.0-local
LBA_FRANCE_COMPETENCE_API_KEY=LBA_FRANCE_COMPETENCE_API_KEY
LBA_FRANCE_COMPETENCE_TOKEN=LBA_FRANCE_COMPETENCE_TOKEN
87 changes: 87 additions & 0 deletions server/src/common/franceCompetencesClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import axios from "axios"
import { OPCOS } from "shared/constants/recruteur"
import { z } from "shared/helpers/zodWithOpenApi"
import { assertUnreachable } from "shared/utils"

import config from "@/config"

import { sentryCaptureException } from "./utils/sentryUtils"

const { baseUrl, apiKey, bearerToken } = config.franceCompetences

// exemple de réponse
// {
// "code": "01",
// "siret": "XXXXXXXXXX",
// "opcoRattachement": {
// "code": "06",
// "nom": "OPCO"
// },
// "opcoGestion": {
// "code": "06",
// "nom": "OPCO"
// }
// }

const ZOpcoResponse = z.union([
z.object({
code: z.literal("99").describe("correspond à un siret non trouvé"),
}),
z.object({
code: z.enum(["01", "02"]).describe("01: siret trouvé. 02: siret non trouvé mais préempté par un OPCO via l'application des déclarations"),
opcoRattachement: z.object({
code: z.string(),
nom: z.string(),
}),
opcoGestion: z
.object({
code: z.string(),
nom: z.string(),
})
.optional(),
}),
])

const mappingOpcoNames: Record<string, OPCOS> = {
AKTO: OPCOS.AKTO,
"OPCO EP": OPCOS.EP,
"UNIFORMATION COHESION SOCIALE": OPCOS.UNIFORMATION,
}

function isOpco(str: string): str is OPCOS {
return Boolean(Object.values(OPCOS).find((opco) => opco === str))
}

export const FCGetOpcoInfos = async (siret: string): Promise<OPCOS | null> => {
try {
const response = await axios.get(`${baseUrl}/siropartfc/${encodeURIComponent(siret)}`, {
headers: {
Authorization: `Bearer ${bearerToken}`,
"X-Gravitee-Api-Key": apiKey,
},
})
if (response.status === 200) {
const data = ZOpcoResponse.parse(response.data)
const { code } = data
if (code === "99" || code === "02") {
return null
} else if (code === "01") {
const {
opcoRattachement: { nom: opcoName },
} = data
const mappedOpco: OPCOS | undefined = mappingOpcoNames[opcoName] ?? (isOpco(opcoName) ? opcoName : undefined)
if (!mappedOpco) {
throw new Error(`opco inconnu: ${opcoName}`)
}
return mappedOpco
} else {
assertUnreachable(code)
}
} else {
throw new Error(`error while calling France Compétences: status=${response.status}, data=${response.data}`)
}
} catch (err) {
sentryCaptureException(err)
return null
}
}
5 changes: 5 additions & 0 deletions server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ const config = {
apiKey: env.get("LBA_ENTREPRISE_API_KEY").required().asString(),
simulateError: env.get("LBA_ENTREPRISE_SIMULATE_ERROR").default("false").asBool(),
},
franceCompetences: {
baseUrl: "https://api-preprod.francecompetences.fr",
apiKey: env.get("LBA_FRANCE_COMPETENCE_API_KEY").required().asString(),
bearerToken: env.get("LBA_FRANCE_COMPETENCE_TOKEN").required().asString(),
},
}

export default config
67 changes: 38 additions & 29 deletions server/src/services/etablissement.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { EDiffusibleStatus } from "shared/constants/diffusibleStatus"
import { BusinessErrorCodes } from "shared/constants/errorCodes"
import { ETAT_UTILISATEUR } from "shared/constants/recruteur"

import { FCGetOpcoInfos } from "@/common/franceCompetencesClient"
import { getStaticFilePath } from "@/common/utils/getStaticFilePath"
import { getHttpClient } from "@/common/utils/httpUtils"

Expand Down Expand Up @@ -192,13 +193,27 @@ export const getEtablissement = async (query: FilterQuery<IUserRecruteur>): Prom
* @param {String} siret
* @returns {Promise<Object>}
*/
export const getOpco = async (siret: string): Promise<ICFADock | null> => {
export const getOpcoFromCfaDock = async (siret: string): Promise<{ opco: string; idcc: string } | undefined> => {
try {
const { data } = await getHttpClient({ timeout: 5000 }).get<ICFADock>(`https://www.cfadock.fr/api/opcos?siret=${encodeURIComponent(siret)}`)
return data
if (!data) {
return undefined
}
const { searchStatus, opcoName, idcc } = data
switch (searchStatus) {
case "OK": {
return { opco: opcoName, idcc: idcc.toString() }
}
case "MULTIPLE_OPCO": {
return { opco: "Opco multiple", idcc: "Opco multiple, IDCC non défini" }
}
default: {
return undefined
}
}
} catch (err: any) {
sentryCaptureException(err)
return null
return undefined
}
}

Expand Down Expand Up @@ -587,39 +602,33 @@ export const isCompanyValid = async (userRecruteur: IUserRecruteur) => {

const errorFactory = (message: string, errorCode?: BusinessErrorCodes) => ({ error: true, message, errorCode })

const getOpcoDataRaw = async (siret: string) => {
const opcoResult: ICFADock | null = await getOpco(siret)
switch (opcoResult?.searchStatus) {
case "OK": {
return { opco: opcoResult.opcoName, idcc: opcoResult.idcc.toString() }
}
case "MULTIPLE_OPCO": {
return { opco: "Opco multiple", idcc: "Opco multiple, IDCC non défini" }
}
case null:
case "NOT_FOUND": {
const idccResult = await getIdcc(siret)
if (!idccResult) return undefined
const conventions = idccResult[0]?.conventions
if (conventions?.length) {
const num: number = conventions[0]?.num
const opcoByIdccResult = await getOpcoByIdcc(num)
if (opcoByIdccResult) {
return { opco: opcoByIdccResult.opcoName, idcc: opcoByIdccResult.idcc.toString() }
}
}
break
const getOpcoFromCfaDockByIdcc = async (siret: string): Promise<{ opco: string; idcc: string } | undefined> => {
const idccResult = await getIdcc(siret)
if (!idccResult) return undefined
const convention = idccResult.conventions.at(0)
if (convention) {
const { num } = convention
const opcoByIdccResult = await getOpcoByIdcc(num)
if (opcoByIdccResult) {
return { opco: opcoByIdccResult.opcoName, idcc: opcoByIdccResult.idcc.toString() }
}
}
return undefined
}

export const getOpcoData = async (siret: string) => {
const getOpcoFromFranceCompetences = async (siret: string): Promise<{ opco: string } | undefined> => {
const opcoOpt = await FCGetOpcoInfos(siret)
return opcoOpt ? { opco: opcoOpt } : undefined
}

const getOpcoDataRaw = async (siret: string): Promise<{ opco: string; idcc?: string } | undefined> => {
return (await getOpcoFromCfaDock(siret)) ?? (await getOpcoFromCfaDockByIdcc(siret)) ?? (await getOpcoFromFranceCompetences(siret))
}

export const getOpcoData = async (siret: string): Promise<{ opco: string; idcc?: string | null } | undefined> => {
const siren = siret.substring(0, 9)
const opcoFromDB = await getOpcoBySirenFromDB(siren)
if (opcoFromDB) {
const { opco, idcc } = opcoFromDB
return { opco, idcc }
return opcoFromDB
} else {
const result = await getOpcoDataRaw(siret)
if (result) {
Expand Down
10 changes: 7 additions & 3 deletions server/src/services/opco.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ import { CFADOCK_FILTER_LIMIT, fetchOpcosFromCFADock } from "./cfadock.service"

/**
* @description get opco from database collection OPCOS
* @param {string} siren
* @returns {Promise<IOpco>}
*/
export const getOpcoBySirenFromDB = (siren) => Opco.findOne({ siren })
export const getOpcoBySirenFromDB = async (siren: string) => {
const opcoFromDB = await Opco.findOne({ siren })
if (opcoFromDB) {
const { opco, idcc } = opcoFromDB
return { opco, idcc }
}
}

/**
* @description tente d'ajouter un opco en base et retourne une string indiquant le résultat
Expand Down

0 comments on commit 02d9b61

Please sign in to comment.