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

feat: LBAC-1686 décommissionnement api LBF et usage tél du catalogue #801

Merged
merged 11 commits into from
Nov 10, 2023
1 change: 0 additions & 1 deletion .infra/.env_server
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ LBA_LOG_TYPE=console
LBA_SLACK_WEBHOOK_URL={{ vault.LBA_SLACK_WEBHOOK_URL }}
LBA_JOB_SLACK_WEBHOOK={{ vault.LBA_JOB_SLACK_WEBHOOK }}
LBA_MONGODB_URI={{ vault[env_type].LBA_MONGODB_URI }}
LBA_LABONNEFORMATION_PASSWORD={{ vault.LBA_LABONNEFORMATION_PASSWORD }}
LBA_CATALOGUE_URL=https://catalogue-apprentissage.intercariforef.org
LBA_SERVER_SENTRY_DSN={{ vault.LBA_SERVER_SENTRY_DSN }}
LBA_SECRET_UPDATE_ROMES_METIERS={{ vault.LBA_SECRET_UPDATE_ROMES_METIERS }}
Expand Down
742 changes: 369 additions & 373 deletions .infra/vault/vault.yml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ fileignoreconfig:
- filename: .infra/files/configs/mongodb/seed.gpg
checksum: 3679dcde17d9d606ef7b27ad632122fcf19a4180849ed4cd1cbc98c312dd0d29
- filename: .infra/vault/vault.yml
checksum: 1a62fbc5e3e877b5241d59c61fda05a61b6c8bb4fb7f5662f2f1bc44c288782b
checksum: 9ac87932d132cd238bc2194daadd08b26491985332c9ed15ccfd6f14a0645839
- filename: server/.env.test
checksum: 870b319418d2bd31728a0925d70ae4b6ef0ac6980ddf3feeb2702e32f868115d
- filename: server/src/common/apis/Pe.ts
Expand Down
1 change: 0 additions & 1 deletion server/.env.test
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ LBA_LOG_TYPE=console
LBA_SLACK_WEBHOOK_URL=https://hooks.slack.com
LBA_JOB_SLACK_WEBHOOK=https://hooks.slack.com
LBA_MONGODB_URI=mongodb://127.0.0.1:27017/lba-test-VITEST_POOL_ID?retryWrites=true&w=1&j=false
LBA_LABONNEFORMATION_PASSWORD=LBA_LABONNEFORMATION_PASSWORD
LBA_CATALOGUE_URL=https://catalogue-apprentissage.intercariforef.org
LBA_SERVER_SENTRY_DSN=https://[email protected]/1
LBA_SECRET_UPDATE_ROMES_METIERS=1234
Expand Down
39 changes: 0 additions & 39 deletions server/src/common/apis/Pe.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import crypto from "crypto"
import { createReadStream } from "fs"
import querystring from "querystring"

Expand All @@ -15,51 +14,13 @@ import { sentryCaptureException } from "../utils/sentryUtils"

import getApiClient from "./client"

const LBF_API_BASE_URL = `https://labonneformation.pole-emploi.fr/api/v1`
const PE_IO_API_ROME_V1_BASE_URL = "https://api.pole-emploi.io/partenaire/rome/v1"
const PE_IO_API_OFFRES_BASE_URL = "https://api.pole-emploi.io/partenaire/offresdemploi/v2"
const PE_AUTH_BASE_URL = "https://entreprise.pole-emploi.fr/connexion/oauth2"
const PE_PORTAIL_BASE_URL = "https://portail-partenaire.pole-emploi.fr/partenaire"

const axiosClient = getApiClient({})

/**
* Construit et retourne les paramètres de la requête vers LBF
* @param {string} id L'id RCO de la formation dont on veut récupérer les données sur LBF
* @returns {string}
*/
const getLbfQueryParams = (id: string): string => {
// le timestamp doit être uriencodé avec le format ISO sans les millis
let date = new Date().toISOString()
date = encodeURIComponent(date.substring(0, date.lastIndexOf(".")))

let queryParams = `user=LBA&uid=${id}&timestamp=${date}`

const hmac = crypto.createHmac("md5", config.laBonneFormationPassword)
const data = hmac.update(queryParams)
const signature = data.digest("hex")

// le param signature doit contenir un hash des autres params chiffré avec le mdp attribué à LBA
queryParams += "&signature=" + signature

return queryParams
}

/**
* @description Get LBF formation description
* @param {string} id
*/
export const getLBFFormationDescription = async (id: string) => {
try {
const { data } = await axiosClient.get(`${LBF_API_BASE_URL}/detail?${getLbfQueryParams(id)}`)

return data
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
throw new ApiError("Api LBF", error.message, error.code || error.response?.status, error?.response?.status)
}
}

const ROME_ACESS = querystring.stringify({
grant_type: "client_credentials",
client_id: config.esdClientId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ const mnaFormationSchema = new Schema<IFormationCatalogue>(
type: String,
description: "Domaine et sous domaine ONISEP (séparateur ;)",
},

rncp_code: {
index: true,
type: String,
Expand Down Expand Up @@ -347,19 +346,16 @@ const mnaFormationSchema = new Schema<IFormationCatalogue>(
type: String,
description: "Qui a réalisé la derniere modification",
},

// Flags
to_update: {
index: true,
type: Boolean,
description: "Formation à mette à jour lors du script d'enrichissement",
},

update_error: {
type: String,
description: "Erreur lors de la mise à jour de la formation",
},

lieu_formation_geo_coordonnees: {
type: String,
implicit_type: "geo_point",
Expand Down Expand Up @@ -468,6 +464,11 @@ const mnaFormationSchema = new Schema<IFormationCatalogue>(
type: String,
description: "Les objectifs de la formation",
},
num_tel: {
type: String,
default: null,
description: "Numéro de téléphone de contact de la formation",
},
...etablissementGestionnaireInfo,
...etablissementFormateurInfo,
...etablissementReferenceInfo,
Expand Down
1 change: 0 additions & 1 deletion server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ const config = {
mongodb: {
uri: env.get("LBA_MONGODB_URI").required().asString(),
},
laBonneFormationPassword: env.get("LBA_LABONNEFORMATION_PASSWORD").required().asString(),
catalogueUrl: env.get("LBA_CATALOGUE_URL").required().asString(),
serverSentryDsn: env.get("LBA_SERVER_SENTRY_DSN").required().asString(),
secretUpdateRomesMetiers: env.get("LBA_SECRET_UPDATE_ROMES_METIERS").required().asString(), //TODO: rename
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { zRoutes } from "shared/index.js"

import { trackApiCall } from "../../../common/utils/sendTrackingEvent.js"
import { getFormationDescriptionQuery, getFormationQuery, getFormationsQuery } from "../../../services/formation.service.js"
import { getFormationQuery, getFormationsQuery } from "../../../services/formation.service.js"
import { Server } from "../../server"

const config = {
Expand Down Expand Up @@ -85,32 +85,4 @@ export default (server: Server) => {
return res.send(result)
}
)

server.get(
"/v1/formations/formationDescription/:id",
{
schema: zRoutes.get["/v1/formations/formationDescription/:id"],
config,
// TODO: AttachValidation Error ?
},
async (req, res) => {
const { id } = req.params
const result = await getFormationDescriptionQuery({
id,
})

if ("error" in result) {
const { status } = result
if (status === 400) {
return res.status(400).send({ error: "wrong_parameters" })
} else if (status === 404) {
return res.status(404).send({ error: "not_found" })
} else {
return res.status(500).send({ error: "internal_error" })
}
}

return res.send(result)
}
)
}
1 change: 1 addition & 0 deletions server/src/services/catalogue.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ const neededFieldsFromCatalogue = {
date_fin: 1,
modalites_entrees_sorties: 1,
bcn_mefs_10: 1,
num_tel: 1,
}

/**
Expand Down
49 changes: 5 additions & 44 deletions server/src/services/formation.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,9 @@ import dayjs from "dayjs"
import { groupBy, maxBy } from "lodash-es"
import type { IFormationCatalogue } from "shared"

import { getLBFFormationDescription } from "@/common/apis/Pe"
import { logger } from "@/common/logger"

import { search } from "../common/esClient/index"
import { FormationCatalogue } from "../common/model/index"
import { IApiError, manageApiError } from "../common/utils/errorManager"
import { IApiError } from "../common/utils/errorManager"
import { roundDistance } from "../common/utils/geolib"
import { regionCodeToDepartmentList } from "../common/utils/regionInseeCodes"
import { trackApiCall } from "../common/utils/sendTrackingEvent"
Expand Down Expand Up @@ -396,7 +393,9 @@ const transformFormationForIdea = (rawFormation: IFormationEsResult): ILbaItemFo
idRco: rawFormation.source.id_formation ?? null,
idRcoFormation: rawFormation.source.id_rco_formation ?? null,

contact: null,
contact: {
phone: rawFormation.source.num_tel ?? null,
},

place: {
distance: rawFormation.sort ? roundDistance(rawFormation.sort[0]) : null,
Expand Down Expand Up @@ -614,45 +613,6 @@ export const getFormationQuery = async ({ id, caller }: { id: string; caller?: s
}
}

/**
* Supprime les adresses emails de la payload provenant de l'api La Bonne Formation
* @param {any} data la payload issue de LBF
* @return {any}
*/
const removeEmailFromLBFData = (data: any): any => {
if (data?.organisme?.contact?.email) {
data.organisme.contact.email = ""
}

if (data?.sessions?.length) {
data.sessions.forEach((_, idx) => {
if (data.sessions[idx]?.contact?.email) {
data.sessions[idx].contact.email = ""
}
})
}

return data
}

/**
* Récupère depuis l'api LBF des éléments de description indisponibles depuis le catalogue
* @returns {Promise<IApiError | any>}
*/
export const getFormationDescriptionQuery = async ({ id }: { id: string }): Promise<IApiError | any> => {
try {
const formationDescription = await getLBFFormationDescription(id)

logger.info(`Call formationDescription. params=${id}`)
return removeEmailFromLBFData(formationDescription)
} catch (error) {
return manageApiError({
error,
errorTitle: `getting training description from Labonneformation`,
})
}
}

/**
* Retourne les formations matchant les critères dans la requête
* TODO: déporter les ctrls dans le controller, appeler directement getRegionFormations depuis le controller
Expand Down Expand Up @@ -767,6 +727,7 @@ const getFormationEsQueryIndexFragment = (limit: number, options: "with_descript
"id_rco_formation",
"id_formation",
"cle_ministere_educatif",
"num_tel",
].concat(options.indexOf("with_description") >= 0 ? ["objectif", "contenu"] : []),
}
}
Expand Down
2 changes: 2 additions & 0 deletions shared/models/formation.model.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { extensions } from "../helpers/zodHelpers/zodPrimitives"
import { z } from "../helpers/zodWithOpenApi"

import { ZAppointment } from "./appointments.model"
Expand Down Expand Up @@ -180,6 +181,7 @@ export const zFormationCatalogueSchema = z
date_debut: z.array(z.string()).nullish(),
date_fin: z.array(z.string()).nullish(),
modalites_entrees_sorties: z.array(z.boolean()).nullish(),
num_tel: extensions.phone().nullable().describe("Numéro de téléphone de contact"),
})
.strict()
.extend(etablissementFormateurSchema.shape)
Expand Down
28 changes: 0 additions & 28 deletions shared/routes/v1Formations.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,33 +76,5 @@ export const zV1FormationsRoutes = {
description: "Get one formation identified by it's clé ministère éducatif",
},
},
"/v1/formations/formationDescription/:id": {
method: "get",
path: "/v1/formations/formationDescription/:id",
params: z
.object({
id: z.string(),
})
.strict(),
response: {
// eslint-disable-next-line zod/require-strict
"200": z.any(),
"400": z.union([ZResError, ZLbacError]).openapi({
description: "Bad Request",
}),
"404": z.union([ZResError, ZLbacError]).openapi({
description: "Not Found",
}),
"500": z.union([ZResError, ZLbacError]).openapi({
description: "Internal Server Error",
}),
},
securityScheme: null,
openapi: {
tags: ["Formations"] as string[],
operationId: "getFormationDescription",
description: "Get details for one formation identified by it's clé ministère éducatif",
},
},
},
} as const satisfies IRoutesDef
12 changes: 2 additions & 10 deletions ui/services/fetchTrainingDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,11 @@ export default async function fetchTrainingDetails(training, errorCallbackFn = _
}

const trainingsApi = `${_apiEndpoint}/v1/formations/formation/${encodeURIComponent(training.id)}`
const lbfApi = `${_apiEndpoint}/v1/formations/formationDescription/${training.idRco}`

const [response, lbfResponse] = await Promise.all([_axios.get(trainingsApi), _axios.get(lbfApi)])
const response = await _axios.get(trainingsApi)

const isSimulatedError = _.includes(_.get(_window, "location.href", ""), "trainingDetailError=true")
const isAxiosError = !!_.get(response, "data.error") || !!_.get(lbfResponse, "data.error")
const isAxiosError = !!_.get(response, "data.error")

const isError = isAxiosError || isSimulatedError

Expand All @@ -30,13 +29,6 @@ export default async function fetchTrainingDetails(training, errorCallbackFn = _
}
} else if (response.data?.results?.length) {
res = response.data.results[0]

// remplacement des coordonnées de contact catalogue par celles de lbf
const contactLbf = lbfResponse.data.organisme.contact

res.contact = res.contact || {}
res.contact.phone = contactLbf?.tel || res.contact.phone
res.company.url = contactLbf?.url || res.company.url
}

return res
Expand Down