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 2256: jobOpportunity #1337

Merged
merged 18 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
39 changes: 36 additions & 3 deletions server/src/http/controllers/jobs.controller.v2.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Boom from "boom"
import { IJob, ILbaItemFtJob, ILbaItemLbaJob, JOB_STATUS, assertUnreachable, zRoutes } from "shared"
import { LBA_ITEM_TYPE } from "shared/constants/lbaitem"
import { JOB_OPPORTUNITY_TYPE, LBA_ITEM_TYPE } from "shared/constants/lbaitem"
import { IJobOpportunityRomeRncp } from "shared/routes/jobOpportunity.routes"

import { getDbCollection } from "@/common/utils/mongodbUtils"
import { getUserFromRequest } from "@/security/authenticationService"
Expand All @@ -10,6 +11,7 @@ import { getFileSignedURL } from "../../common/utils/awsUtils"
import { trackApiCall } from "../../common/utils/sendTrackingEvent"
import { sentryCaptureException } from "../../common/utils/sentryUtils"
import { getNearEtablissementsFromRomes } from "../../services/catalogue.service"
import { getRomesFromRncp } from "../../services/certification.service"
import { ACTIVE, ANNULEE, POURVUE } from "../../services/constant.service"
import dayjs from "../../services/dayjs.service"
import { entrepriseOnboardingWorkflow } from "../../services/etablissement.service"
Expand All @@ -27,8 +29,8 @@ import {
provideOffre,
} from "../../services/formulaire.service"
import { getFtJobFromIdV2 } from "../../services/ftjob.service"
import { getJobsQuery } from "../../services/jobOpportunity.service"
import { getCompanyFromSiret } from "../../services/lbacompany.service"
import { formatRecruteurLbaToJobOpportunity, getJobsQuery } from "../../services/jobOpportunity.service"
import { getCompanyFromSiret, getRecruteursLbaFromDB } from "../../services/lbacompany.service"
import { addOffreDetailView, getLbaJobByIdV2 } from "../../services/lbajob.service"
import { getFicheMetierFromDB } from "../../services/rome.service"
import { Server } from "../server"
Expand Down Expand Up @@ -486,4 +488,35 @@ export default (server: Server) => {
}
}
)

server.get(
`/jobs/${JOB_OPPORTUNITY_TYPE.RECRUTEURS_LBA}`,
{ schema: zRoutes.get["/jobs/recruteurs_lba"], onRequest: server.auth(zRoutes.get["/jobs/recruteurs_lba"]) },
kevbarns marked this conversation as resolved.
Show resolved Hide resolved
async (req, res) => {
const payload: IJobOpportunityRomeRncp = { ...req.query }
if ("rncp" in payload) {
payload.romes = await getRomesFromRncp(payload.rncp)
if (!payload.romes) {
throw Boom.internal(`Aucun code ROME n'a été trouvé à partir du code RNCP ${payload.rncp}`)
}
}
const result = await getRecruteursLbaFromDB({ ...payload, romes: payload.romes as string[] })
kevbarns marked this conversation as resolved.
Show resolved Hide resolved
return res.send(formatRecruteurLbaToJobOpportunity(result))
}
)
server.get(
`/jobs/${JOB_OPPORTUNITY_TYPE.OFFRES_EMPLOI_LBA}`,
{ schema: zRoutes.get["/jobs/offres_emploi_lba"], onRequest: server.auth(zRoutes.get["/jobs/offres_emploi_lba"]) },
async () => {}
)
server.get(
`/jobs/${JOB_OPPORTUNITY_TYPE.OFFRES_EMPLOI_PARTENAIRES}`,
{ schema: zRoutes.get["/jobs/offres_emploi_partenaires"], onRequest: server.auth(zRoutes.get["/jobs/offres_emploi_partenaires"]) },
async () => {}
)
server.get(
`/jobs/${JOB_OPPORTUNITY_TYPE.OFFRES_EMPLOI_FRANCE_TRAVAIL}`,
{ schema: zRoutes.get["/jobs/offres_emploi_france_travail"], onRequest: server.auth(zRoutes.get["/jobs/offres_emploi_france_travail"]) },
async () => {}
)
}
44 changes: 44 additions & 0 deletions server/src/services/certification.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import axios from "axios"

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

import { CertificationAPIApprentissage } from "./queryValidator.service.types"

const getFirstCertificationFromAPIApprentissage = async (rncp: string): Promise<CertificationAPIApprentissage | null> => {
try {
const { data } = await axios.get<CertificationAPIApprentissage[]>(`${config.apiApprentissage.baseUrl}/certification/v1?identifiant.rncp=${rncp}`, {
headers: { Authorization: `Bearer ${config.apiApprentissage.apiKey}` },
})

if (!data.length) return null

return data[0]
} catch (error: any) {
sentryCaptureException(error, { responseData: error.response?.data })
return null
}
}

const getRomesFromCertification = (certification: CertificationAPIApprentissage) => {
return certification.domaines.rome.rncp.map((x) => x.code).join(",")
}
export const getRomesFromRncp = async (rncp: string): Promise<string | null> => {
let certification = await getFirstCertificationFromAPIApprentissage(rncp)
if (!certification) {
return null
}
if (certification.periode_validite.rncp.actif) {
return getRomesFromCertification(certification)
} else {
const latestRNCP = certification.continuite.rncp.findLast((rncp) => rncp.actif === true)
if (!latestRNCP) {
return getRomesFromCertification(certification)
}
certification = await getFirstCertificationFromAPIApprentissage(latestRNCP.code)
if (!certification) {
return null
}
return getRomesFromCertification(certification)
}
}
44 changes: 43 additions & 1 deletion server/src/services/jobOpportunity.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { LBA_ITEM_TYPE, allLbaItemType } from "shared/constants/lbaitem"
import { ILbaCompany } from "shared"
import { JOB_OPPORTUNITY_TYPE, LBA_ITEM_TYPE, allLbaItemType } from "shared/constants/lbaitem"
import { IJobOpportunity } from "shared/interface/jobOpportunity.types"

import { IApiError } from "../common/utils/errorManager"
import { trackApiCall } from "../common/utils/sendTrackingEvent"
Expand Down Expand Up @@ -168,3 +170,43 @@ export const getJobsQuery = async (

return result
}

export const formatRecruteurLbaToJobOpportunity = (recruteursLba: ILbaCompany[]): IJobOpportunity[] => {
const formattedResult: IJobOpportunity[] = []
recruteursLba.map((recruteurLba) => {
formattedResult.push({
kevbarns marked this conversation as resolved.
Show resolved Hide resolved
identifiant: {
id: recruteurLba.siret,
type: JOB_OPPORTUNITY_TYPE.RECRUTEURS_LBA,
},
contract: null,
jobOffre: null,
workplace: {
description: null,
name: recruteurLba.enseigne ?? recruteurLba.raison_sociale,
siret: recruteurLba.siret,
size: recruteurLba.company_size,
website: recruteurLba.website,
location: {
address: `${recruteurLba.street_number} ${recruteurLba.street_name} ${recruteurLba.zip_code} ${recruteurLba.city}`,
lattitude: recruteurLba.geo_coordinates.split(",")[0],
longitude: recruteurLba.geo_coordinates.split(",")[1],
},
domaine: {
opco: recruteurLba.opco,
idcc: null,
naf: {
code: recruteurLba.naf_code,
label: recruteurLba.naf_label,
},
},
},
apply: {
url: null,
phone: recruteurLba.phone,
email: recruteurLba.email,
},
})
})
return formattedResult
}
kevbarns marked this conversation as resolved.
Show resolved Hide resolved
39 changes: 38 additions & 1 deletion server/src/services/lbacompany.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,47 @@ const transformCompanies = ({
return transformedCompanies
}

type IRecruteursLbaSearchParams = {
romes: string[]
latitude: number
longitude: number
radius: number
opco?: string
opcoUrl?: string
}

export const getRecruteursLbaFromDB = async ({ radius = 10, ...params }: IRecruteursLbaSearchParams): Promise<ILbaCompany[] | []> => {
const { romes, opco, opcoUrl, latitude, longitude } = params
kevbarns marked this conversation as resolved.
Show resolved Hide resolved
const query: any = {
kevbarns marked this conversation as resolved.
Show resolved Hide resolved
rome_codes: { $in: romes },
}
if (opco) {
query.opco_short_name = opco.toUpperCase()
}
if (opcoUrl) {
query.opco_url = opcoUrl.toLowerCase()
}
return (await getDbCollection("recruteurslba")
.aggregate([
{
$geoNear: {
near: { type: "Point", coordinates: [longitude, latitude] },
distanceField: "distance",
maxDistance: radius * 1000,
query,
},
},
{
$limit: 150,
},
])
.toArray()) as ILbaCompany[]
}

/**
* Retourne des sociétés issues de l'algo matchant les critères en paramètres
*/
const getCompanies = async ({
export const getCompanies = async ({
romes,
latitude,
longitude,
Expand Down
43 changes: 1 addition & 42 deletions server/src/services/queryValidator.service.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,10 @@
import axios from "axios"
import { allLbaItemTypeOLD } from "shared/constants/lbaitem"

import { isOriginLocal } from "../common/utils/isOriginLocal"
import { regionCodeToDepartmentList } from "../common/utils/regionInseeCodes"
import { sentryCaptureException } from "../common/utils/sentryUtils"
import config from "../config"

import { getRomesFromRncp } from "./certification.service"
import { TFormationSearchQuery, TJobSearchQuery } from "./jobOpportunity.service.types"
import { CertificationAPIApprentissage } from "./queryValidator.service.types"

const getFirstCertificationFromAPIApprentissage = async (rncp: string): Promise<CertificationAPIApprentissage | null> => {
try {
const { data } = await axios.get<CertificationAPIApprentissage[]>(`${config.apiApprentissage.baseUrl}/certification/v1?identifiant.rncp=${rncp}`, {
headers: { Authorization: `Bearer ${config.apiApprentissage.apiKey}` },
})

if (!data.length) return null

return data[0]
} catch (error: any) {
sentryCaptureException(error, { responseData: error.response?.data })
return null
}
}

const getRomesFromCertification = (certification: CertificationAPIApprentissage) => {
return certification.domaines.rome.rncp.map((x) => x.code).join(",")
}
const getRomesFromRncp = async (rncp: string): Promise<string | null> => {
let certification = await getFirstCertificationFromAPIApprentissage(rncp)
if (!certification) {
return null
}
if (certification.periode_validite.rncp.actif) {
return getRomesFromCertification(certification)
} else {
const latestRNCP = certification.continuite.rncp.findLast((rncp) => rncp.actif === true)
if (!latestRNCP) {
return getRomesFromCertification(certification)
}
certification = await getFirstCertificationFromAPIApprentissage(latestRNCP.code)
if (!certification) {
return null
}
return getRomesFromCertification(certification)
}
}

/**
* Contrôle le format d'un code RNCP
Expand Down
7 changes: 7 additions & 0 deletions shared/constants/lbaitem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ export enum LBA_ITEM_TYPE {
OFFRES_EMPLOI_PARTENAIRES = "offres_emploi_partenaires",
}

export enum JOB_OPPORTUNITY_TYPE {
RECRUTEURS_LBA = "recruteurs_lba",
OFFRES_EMPLOI_LBA = "offres_emploi_lba",
OFFRES_EMPLOI_PARTENAIRES = "offres_emploi_partenaires",
OFFRES_EMPLOI_FRANCE_TRAVAIL = "offres_emploi_france_travail",
}

export enum LBA_ITEM_TYPE_OLD {
FORMATION = "formation",
MATCHA = "matcha",
Expand Down
6 changes: 6 additions & 0 deletions shared/constants/recruteur.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,9 @@ export const TRAINING_RYTHM = {
"2S3S": "2 semaines / 3 semaines",
"6S6S": "6 semaines / 6 semaines",
}

export const TRAINING_REMOTE_TYPE = {
PRESENTIEL: "Présentiel",
TELETRAVAIL: "Télétravail",
HYBRID: "Hybride",
}
24 changes: 10 additions & 14 deletions shared/constants/regex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,6 @@ export const REPETITION_VOIE_MAPPING = {
["Quinquiès"]: "C",
}

export const CFD_REGEX_PATTERN = "^[A-Z0-9]{8}$"
export const CODE_INSEE_PATTERN = "^[0-9]{1}[0-9A-Z]{1}[0-9]{3}$"
export const CODE_POSTAL_PATTERN = "^[0-9]{5}$"
export const RNCP_REGEX_PATTERN = "^(RNCP)?[0-9]{2,5}$"
export const SIRET_REGEX_PATTERN = "^[0-9]{14}$"
export const CODE_NAF_REGEX_PATTERN = "^[0-9]{4}[A-Z]$"
export const UAI_REGEX_PATTERN = "^[0-9]{7}[a-zA-Z]$"

// Numero INE (Identifiant National Elève)
// Le numero INE composé de 11 caractères,
// soit 10 chiffres et 1 lettre soit 9 chiffres et 2 lettres (depuis la rentrée 2018).
Expand All @@ -24,13 +16,17 @@ const INE_BEA_REGEX_PATTERN = "^[0-9_]{10}[a-zA-Z]{1}$"
const INE_APPRENTISSAGE_REGEX_PATTERN = "^[0-9]{4}A[0-9]{5}[a-zA-Z]{1}$"
export const INE_REGEX_PATTERN = `^(${INE_RNIE_REGEX_PATTERN}|${INE_BEA_REGEX_PATTERN}|${INE_APPRENTISSAGE_REGEX_PATTERN})$`

export const CFD_REGEX = new RegExp(CFD_REGEX_PATTERN)
export const CODE_POSTAL_REGEX = new RegExp(CODE_POSTAL_PATTERN)
export const CFD_REGEX = new RegExp("^[A-Z0-9]{8}$")
export const CODE_POSTAL_REGEX = new RegExp("^[0-9]{5}$")
export const INE_REGEX = new RegExp(INE_REGEX_PATTERN)
export const RNCP_REGEX = new RegExp(RNCP_REGEX_PATTERN)
export const SIRET_REGEX = new RegExp(SIRET_REGEX_PATTERN)
export const CODE_NAF_REGEX = new RegExp(CODE_NAF_REGEX_PATTERN)
export const UAI_REGEX = new RegExp(UAI_REGEX_PATTERN)
export const RNCP_REGEX = new RegExp("^(RNCP)?[0-9]{2,5}$")
export const SIRET_REGEX = new RegExp("^[0-9]{14}$")
export const CODE_NAF_REGEX = new RegExp("^[0-9]{4}[A-Z]$")
export const UAI_REGEX = new RegExp("^[0-9]{7}[a-zA-Z]$")
export const CODE_ROME_REGEX = new RegExp("^[A-Z]d{4}$")
export const LATITUDE_REGEX = new RegExp("^[-+]?([1-8]?d(.d+)?|90(.0+)?)$")
export const LONGITUDE_REGEX = new RegExp("^[-+]?((1[0-7]d(.d+)?|180(.0+)?|d{1,2}(.d+)?))$")
export const CODE_INSEE_REGEX = new RegExp("^[0-9]{1}[0-9A-Z]{1}[0-9]{3}$")

export const isValidCFD = (cfd: string) => typeof cfd === "string" && CFD_REGEX.test(cfd)
export const isValidINE = (ine: string) => typeof ine === "string" && INE_REGEX.test(ine)
17 changes: 16 additions & 1 deletion shared/helpers/zodHelpers/zodPrimitives.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { capitalize } from "lodash-es"

import { CODE_NAF_REGEX, SIRET_REGEX, UAI_REGEX } from "../../constants/regex"
import { CODE_INSEE_REGEX, CODE_NAF_REGEX, CODE_ROME_REGEX, LATITUDE_REGEX, LONGITUDE_REGEX, RNCP_REGEX, SIRET_REGEX, UAI_REGEX } from "../../constants/regex"
import { validateSIRET } from "../../validators/siretValidator"
import { removeUrlsFromText } from "../common"
import { z } from "../zodWithOpenApi"
Expand Down Expand Up @@ -101,4 +101,19 @@ export const extensions = {
}
return z.enum([values[0], ...values.slice(1)])
},
romeCode: () => z.string().trim().regex(CODE_ROME_REGEX, "Code ROME invalide"),
rncpCode: () => z.string().trim().regex(RNCP_REGEX, "Code RNCP invalide"),
latitude: () =>
z
.string()
.trim()
.regex(LATITUDE_REGEX, "Latitude invalide")
.transform((val) => parseFloat(val)),
longitude: () =>
z
.string()
.trim()
.regex(LONGITUDE_REGEX, "Longitude invalide")
.transform((val) => parseFloat(val)),
inseeCode: () => z.string().trim().regex(CODE_INSEE_REGEX, "Code INSEE invalide"),
}
Loading
Loading