Skip to content

Commit

Permalink
fix: LBAC 1841: ajout de script fix-data-validation-user-recruteurs-c…
Browse files Browse the repository at this point in the history
…fa + correction du typage adresse (#906)

* fix: ajout de script fix-data-validation-user-recruteurs-cfa + correction du typage adresse

* fix: modification du modele pour qu'il soit strict

* fix: revert talismanrc

* fix: revert talismanrc
  • Loading branch information
remy-auricoste authored Dec 13, 2023
1 parent b0b2b5e commit a965c3b
Show file tree
Hide file tree
Showing 12 changed files with 126 additions and 137 deletions.
4 changes: 1 addition & 3 deletions .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ fileignoreconfig:
- filename: server/.env.test
checksum: 2534c2dae48c1464b97489263621dcd516a676b28fdbb34e98267a10e00fd839
- filename: server/src/security/accessTokenService.ts
checksum: 671dbfb4d2d85ce2050c8c0755384028619e2ebb98616aca5bc7cd36704bb343
checksum: f05cafd17797362fc9bfb53062af2095ead2cbe2fa967fad23bd61b756052004
- filename: server/src/common/model/schema/_shared/mongoose-paginate.ts
checksum: b6762a7cb5df9bbee1f0ce893827f0991ad01514f7122a848b3b5d49b620f238
- filename: server/src/config.ts
Expand All @@ -43,8 +43,6 @@ fileignoreconfig:
checksum: d716e214d828109181a138f0ae253d5489a3c544b2625917b458d1e07886c408
- filename: server/src/jobs/lba_recruteur/formulaire/misc/removeVersionKeyFromRecruiters.ts
checksum: 3cd111d8c109cfec357bae48af70d0cf5644d02cd2c4b9afc5b8aa07bccbd535
- filename: server/src/security/accessTokenService.ts
checksum: f05cafd17797362fc9bfb53062af2095ead2cbe2fa967fad23bd61b756052004
- filename: server/src/services/application.service.ts
checksum: 935cd8f213565ba7bcc2925fca149aaa6cbe9bb5e393a13ab3525dff6ad17234
- filename: server/tests/integration/http/formationV1.test.ts
Expand Down
6 changes: 6 additions & 0 deletions server/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,12 @@ program
.option("-q, --queued", "Run job asynchronously", false)
.action(createJobAction("user-recruters:data-validation:fix"))

program
.command("fix-data-validation-user-recruteurs-cfa")
.description("Répare les data des userrecruteurs CFA")
.option("-q, --queued", "Run job asynchronously", false)
.action(createJobAction("user-recruters-cfa:data-validation:fix"))

program
.command("anonymize-user-recruteurs")
.description("Anonymize les userrecruteurs qui ne se sont pas connectés depuis plus de 2 ans")
Expand Down
77 changes: 0 additions & 77 deletions server/src/common/model/schema/_shared/shared.types.ts

This file was deleted.

3 changes: 3 additions & 0 deletions server/src/jobs/jobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { importReferentielOpcoFromConstructys } from "./lba_recruteur/opco/const
import { relanceOpco } from "./lba_recruteur/opco/relanceOpco"
import { createOffreCollection } from "./lba_recruteur/seed/createOffre"
import { fillRecruiterRaisonSociale } from "./lba_recruteur/user/misc/fillRecruiterRaisonSociale"
import { fixUserRecruiterCfaDataValidation } from "./lba_recruteur/user/misc/fixUserRecruteurCfaDataValidation"
import { fixUserRecruiterDataValidation } from "./lba_recruteur/user/misc/fixUserRecruteurDataValidation"
import { checkAwaitingCompaniesValidation } from "./lba_recruteur/user/misc/updateMissingActivationState"
import { updateSiretInfosInError } from "./lba_recruteur/user/misc/updateSiretInfosInError"
Expand Down Expand Up @@ -356,6 +357,8 @@ export async function runJob(job: IInternalJobsCronTask | IInternalJobsSimple):
return fixRecruiterDataValidation()
case "user-recruters:data-validation:fix":
return fixUserRecruiterDataValidation()
case "user-recruters-cfa:data-validation:fix":
return fixUserRecruiterCfaDataValidation()
case "referentiel-opco:constructys:import": {
const { parallelism } = job.payload
return importReferentielOpcoFromConstructys(parseInt(parallelism))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Boom from "boom"
import { ZCfaReferentielData } from "shared/models"

import { logger } from "@/common/logger"
import { UserRecruteur } from "@/common/model"
import { asyncForEach } from "@/common/utils/asyncUtils"
import { sentryCaptureException } from "@/common/utils/sentryUtils"
import { notifyToSlack } from "@/common/utils/slackUtils"
import { getOrganismeDeFormationDataFromSiret } from "@/services/etablissement.service"
import { updateUser } from "@/services/userRecruteur.service"

export const fixUserRecruiterCfaDataValidation = async () => {
const subject = "Fix data validations pour les userrecruteurs CFA : address_detail"
const userRecruteurs = await UserRecruteur.find({ type: "CFA" }).lean()
const stats = { success: 0, failure: 0, skip: 0 }
logger.info(`${subject}: ${userRecruteurs.length} user recruteurs à mettre à jour...`)
await asyncForEach(userRecruteurs, async (userRecruiter, index) => {
try {
index % 100 === 0 && logger.info("index", index)
const { establishment_siret, is_qualiopi, establishment_raison_sociale, address_detail, address, geo_coordinates } = userRecruiter
if (
!ZCfaReferentielData.pick({
is_qualiopi: true,
establishment_siret: true,
establishment_raison_sociale: true,
address_detail: true,
address: true,
geo_coordinates: true,
}).safeParse({
is_qualiopi,
establishment_siret,
establishment_raison_sociale,
address_detail,
address,
geo_coordinates,
}).success
) {
if (!establishment_siret) {
throw Boom.internal("Missing establishment_siret", { _id: userRecruiter._id })
}
const cfaData = await getOrganismeDeFormationDataFromSiret(establishment_siret, false)
await updateUser({ _id: userRecruiter._id }, cfaData)
stats.success++
} else {
stats.skip++
}
} catch (err) {
logger.error(err)
sentryCaptureException(err)
stats.failure++
}
})
await notifyToSlack({
subject,
message: `${stats.failure} erreurs. ${stats.success} mises à jour. ${stats.skip} ignorés.`,
error: stats.failure > 0,
})
return stats
}
48 changes: 30 additions & 18 deletions server/src/services/etablissement.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AxiosResponse } from "axios"
import Boom from "boom"
import type { FilterQuery } from "mongoose"
import { IEtablissement, ILbaCompany, IRecruiter, IReferentielData, IReferentielOpco, IUserRecruteur, ZUserRecruteurReferentielData } from "shared"
import { ICfaReferentielData, IEtablissement, IGeometry, ILbaCompany, IRecruiter, IReferentielOpco, IUserRecruteur, ZCfaReferentielData, assertUnreachable } from "shared"
import { EDiffusibleStatus } from "shared/constants/diffusibleStatus"
import { BusinessErrorCodes } from "shared/constants/errorCodes"
import { ETAT_UTILISATEUR } from "shared/constants/recruteur"
Expand Down Expand Up @@ -439,8 +439,6 @@ function getRaisonSocialeFromGouvResponse(d: IEtablissementGouv): string | undef

/**
* @description Format Entreprise data
* @param {IEtablissementGouv} data
* @returns {IFormatAPIEntreprise}
*/
export const formatEntrepriseData = (d: IEtablissementGouv): IFormatAPIEntreprise => {
if (!d.adresse) {
Expand All @@ -461,12 +459,27 @@ export const formatEntrepriseData = (d: IEtablissementGouv): IFormatAPIEntrepris
}
}

function geometryToGeoCoord(geometry: IGeometry): [number, number] {
const { type } = geometry
if (type === "Point") {
return geometry.coordinates
} else if (type === "Polygon") {
return geometry.coordinates[0][0]
} else {
assertUnreachable(type)
}
}

/**
* @description Format Referentiel data
* @param {IReferentiel} d
* @returns {Object}
*/
export const formatReferentielData = (d: IReferentiel): IReferentielData => {
export const formatReferentielData = (d: IReferentiel): ICfaReferentielData => {
const geojson = d.adresse?.geojson ?? d.lieux_de_formation.at(0)?.adresse?.geojson
if (!geojson) {
throw Boom.internal("impossible de lire la geometry")
}
const coords = geometryToGeoCoord(geojson.geometry)

const referentielData = {
establishment_state: d.etat_administratif,
is_qualiopi: d.qualiopi,
Expand All @@ -475,14 +488,11 @@ export const formatReferentielData = (d: IReferentiel): IReferentielData => {
contacts: d.contacts,
address_detail: d.adresse,
address: d.adresse?.label,
geo_coordinates: d.adresse
? `${d.adresse?.geojson.geometry.coordinates[1]},${d.adresse?.geojson.geometry.coordinates[0]}`
: `${d.lieux_de_formation[0]?.adresse?.geojson?.geometry.coordinates[0]},${d.lieux_de_formation[0]?.adresse?.geojson?.geometry.coordinates[1]}`,
geo_coordinates: `${coords[1]},${coords[0]}`,
}

const validation = ZUserRecruteurReferentielData.safeParse(referentielData)
const validation = ZCfaReferentielData.safeParse(referentielData)
if (!validation.success) {
sentryCaptureException(Boom.internal(`address format error for siret=${d.siret}.`, { validationError: validation.error }))
sentryCaptureException(Boom.internal(`erreur de validation sur les données du référentiel CFA pour le siret=${d.siret}.`, { validationError: validation.error }))
}
return referentielData
}
Expand Down Expand Up @@ -651,16 +661,18 @@ export const getEntrepriseDataFromSiret = async ({ siret, cfa_delegated_siret }:
return { ...entrepriseData, geo_coordinates: `${latitude},${longitude}` }
}

export const getOrganismeDeFormationDataFromSiret = async (siret: string) => {
const cfaUserRecruteurOpt = await getEtablissement({ establishment_siret: siret, type: CFA })
if (cfaUserRecruteurOpt) {
throw Boom.forbidden("Ce numéro siret est déjà associé à un compte utilisateur.", { reason: BusinessErrorCodes.ALREADY_EXISTS })
export const getOrganismeDeFormationDataFromSiret = async (siret: string, shouldValidate = true) => {
if (shouldValidate) {
const cfaUserRecruteurOpt = await getEtablissement({ establishment_siret: siret, type: CFA })
if (cfaUserRecruteurOpt) {
throw Boom.forbidden("Ce numéro siret est déjà associé à un compte utilisateur.", { reason: BusinessErrorCodes.ALREADY_EXISTS })
}
}
const referentiel = await getEtablissementFromReferentiel(siret)
if (!referentiel) {
throw Boom.badRequest("Le numéro siret n'est pas référencé comme centre de formation.", { reason: BusinessErrorCodes.UNKNOWN })
}
if (referentiel.etat_administratif === "fermé") {
if (shouldValidate && referentiel.etat_administratif === "fermé") {
throw Boom.badRequest("Le numéro siret indique un établissement fermé.", { reason: BusinessErrorCodes.CLOSED })
}
if (!referentiel.adresse) {
Expand All @@ -669,7 +681,7 @@ export const getOrganismeDeFormationDataFromSiret = async (siret: string) => {
})
}
const formattedReferentiel = formatReferentielData(referentiel)
if (!formattedReferentiel.is_qualiopi) {
if (shouldValidate && !formattedReferentiel.is_qualiopi) {
throw Boom.badRequest("L’organisme rattaché à ce SIRET n’est pas certifié Qualiopi", { reason: BusinessErrorCodes.NOT_QUALIOPI, ...formattedReferentiel })
}
return formattedReferentiel
Expand Down
4 changes: 1 addition & 3 deletions server/src/services/etablissement.service.types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { IRecruiter, IUserRecruteur } from "shared"
import { IAdresseCFA, IAdresseV3, IRecruiter, IUserRecruteur } from "shared"
import { Jsonify } from "type-fest"

import { IAdresseCFA, IAdresseV3 } from "../common/model/schema/_shared/shared.types"

export interface IFormatAPIReferentiel
extends Pick<IUserRecruteur, "establishment_raison_sociale" | "establishment_siret" | "is_qualiopi" | "address_detail" | "geo_coordinates" | "address"> {
establishment_state: string
Expand Down
25 changes: 21 additions & 4 deletions shared/models/address.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,29 @@ const ZAcademie = z
})
.strict()

const ZGeometry = z
const Z2DCoord = z.tuple([z.number(), z.number()])

const ZPointGeometry = z
.object({
coordinates: z.array(z.number()),
type: z.string(),
coordinates: Z2DCoord,
type: z.literal("Point"),
})
.strict()

const ZPolygonGeometry = z
.object({
coordinates: z.array(z.array(Z2DCoord)), // oui, il y a un niveau de tableau en trop mais ce sont les data qu'on récupère
type: z.literal("Polygon"),
})
.strict()

const ZGeometry = z.union([ZPointGeometry, ZPolygonGeometry])

export type IGeometry = z.input<typeof ZGeometry>

const ZProperties = z
.object({
score: z.number(),
score: z.number().nullish(),
source: z.string(),
})
.strict()
Expand Down Expand Up @@ -92,3 +105,7 @@ const ZAdresseV3 = z
.openapi("AdresseV3")

export const ZGlobalAddress = z.union([ZAdresseCFA, ZAdresseV2, ZAdresseV3])

export type IAdresseV3 = z.input<typeof ZAdresseV3>
export type IAdresseCFA = z.input<typeof ZAdresseCFA>
export type IGlobalAddress = z.input<typeof ZGlobalAddress>
15 changes: 0 additions & 15 deletions shared/models/cfa.model.ts

This file was deleted.

1 change: 0 additions & 1 deletion shared/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
export * from "./address.model"
export * from "./applications.model"
export * from "./appointments.model"
export * from "./cfa.model"
export * from "./credentials.model"
export * from "./elligibleTraining.model"
export * from "./emailBlacklist.model"
Expand Down
17 changes: 3 additions & 14 deletions shared/models/usersRecruteur.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const ZUserRecruteur = ZUserRecruteurWritable.omit({
is_qualiopi: ZUserRecruteurWritable.shape.is_qualiopi.nullish(),
})

export const zReferentielData = z
export const ZCfaReferentielData = z
.object({
establishment_state: z.string(),
is_qualiopi: z.boolean(),
Expand All @@ -77,11 +77,11 @@ export const zReferentielData = z
),
address_detail: ZGlobalAddress,
address: z.string(),
geo_coordinates: z.string().nullish(),
geo_coordinates: z.string().max(40).nullish(),
})
.strict()

export type IReferentielData = z.output<typeof zReferentielData>
export type ICfaReferentielData = z.output<typeof ZCfaReferentielData>

export type IUserStatusValidation = z.output<typeof ZUserStatusValidation>
export type IUserStatusValidationJson = Jsonify<z.input<typeof ZUserStatusValidation>>
Expand Down Expand Up @@ -161,14 +161,3 @@ export const ZAnonymizedUserRecruteur = ZUserRecruteur.pick({
})

export type IAnonymizedUserRecruteur = z.output<typeof ZAnonymizedUserRecruteur>

export const ZUserRecruteurReferentielData = ZUserRecruteur.pick({
establishment_state: true,
is_qualiopi: true,
establishment_siret: true,
establishment_raison_sociale: true,
contacts: true,
address_detail: true,
address: true,
geo_coordinates: true,
}).strict()
Loading

0 comments on commit a965c3b

Please sign in to comment.