From 660bb26990dfaca7fd7071330bc48345f2de1d76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Auricoste?= Date: Tue, 24 Oct 2023 14:34:44 +0200 Subject: [PATCH] fix: wip --- .../schema/appointments/appointment.schema.ts | 7 + .../schema/multiCompte/buildMongooseModel.ts | 11 + .../model/schema/multiCompte/cfa.schema.ts | 46 +++ .../schema/multiCompte/entreprise.schema.ts | 58 ++++ .../multiCompte/roleManagement.schema.ts | 72 +++++ .../model/schema/multiCompte/user2.schema.ts | 84 +++++ .../common/model/schema/user/user.schema.ts | 2 +- server/src/common/utils/asyncUtils.ts | 7 + server/src/common/utils/enumUtils.ts | 12 + server/src/jobs/multiCompte/migrationUsers.ts | 299 ++++++++++++++++++ server/src/services/constant.service.ts | 24 +- shared/constants/appointment.ts | 4 + shared/models/appointments.model.ts | 3 + shared/models/cfa.model.ts | 23 +- shared/models/entreprise.model.ts | 25 ++ shared/models/enumToZod.ts | 6 + shared/models/roleManagement.model.ts | 48 +++ shared/models/user2.model.ts | 48 +++ 18 files changed, 758 insertions(+), 21 deletions(-) create mode 100644 server/src/common/model/schema/multiCompte/buildMongooseModel.ts create mode 100644 server/src/common/model/schema/multiCompte/cfa.schema.ts create mode 100644 server/src/common/model/schema/multiCompte/entreprise.schema.ts create mode 100644 server/src/common/model/schema/multiCompte/roleManagement.schema.ts create mode 100644 server/src/common/model/schema/multiCompte/user2.schema.ts create mode 100644 server/src/common/utils/enumUtils.ts create mode 100644 server/src/jobs/multiCompte/migrationUsers.ts create mode 100644 shared/constants/appointment.ts create mode 100644 shared/models/entreprise.model.ts create mode 100644 shared/models/enumToZod.ts create mode 100644 shared/models/roleManagement.model.ts create mode 100644 shared/models/user2.model.ts diff --git a/server/src/common/model/schema/appointments/appointment.schema.ts b/server/src/common/model/schema/appointments/appointment.schema.ts index f200b62e07..c634727c83 100644 --- a/server/src/common/model/schema/appointments/appointment.schema.ts +++ b/server/src/common/model/schema/appointments/appointment.schema.ts @@ -1,4 +1,5 @@ import { IAppointment } from "shared" +import { AppointmentUserType } from "shared/constants/appointment" import { model, Schema } from "../../../mongodb" import { mongoosePagination, Pagination } from "../_shared/mongoose-paginate" @@ -152,6 +153,12 @@ export const appointmentSchema = new Schema( default: false, description: "Si l'enregistrement est anonymisé", }, + applicant_user_type: { + type: String, + enum: [...Object.values(AppointmentUserType), null], + default: null, + description: "Role du demandeur : parent ou etudiant", + }, }, { versionKey: false, diff --git a/server/src/common/model/schema/multiCompte/buildMongooseModel.ts b/server/src/common/model/schema/multiCompte/buildMongooseModel.ts new file mode 100644 index 0000000000..bf28f9d21a --- /dev/null +++ b/server/src/common/model/schema/multiCompte/buildMongooseModel.ts @@ -0,0 +1,11 @@ +import mongoose, { Schema } from "mongoose" + +import { mongoosePagination, Pagination } from "../_shared/mongoose-paginate" + +const { model } = mongoose + +export const buildMongooseModel = (schema: Schema, tableName: string) => { + schema.plugin(mongoosePagination) + + return model>(tableName, schema) +} diff --git a/server/src/common/model/schema/multiCompte/cfa.schema.ts b/server/src/common/model/schema/multiCompte/cfa.schema.ts new file mode 100644 index 0000000000..27193e6015 --- /dev/null +++ b/server/src/common/model/schema/multiCompte/cfa.schema.ts @@ -0,0 +1,46 @@ +import { ICFA } from "shared/models/cfa.model.js" + +import { Schema } from "../../../mongodb.js" + +import { buildMongooseModel } from "./buildMongooseModel.js" + +const cfaSchema = new Schema( + { + establishment_siret: { + type: String, + description: "Siret de l'établissement", + }, + establishment_raison_sociale: { + type: String, + description: "Raison social de l'établissement", + }, + establishment_enseigne: { + type: String, + default: null, + description: "Enseigne de l'établissement", + }, + address_detail: { + type: Object, + description: "Detail de l'adresse de l'établissement", + }, + address: { + type: String, + description: "Adresse de l'établissement", + }, + geo_coordinates: { + type: String, + default: null, + description: "Latitude/Longitude de l'adresse de l'entreprise", + }, + origin: { + type: String, + description: "Origine de la creation (ex: Campagne mail, lien web, etc...) pour suivi", + }, + }, + { + timestamps: true, + versionKey: false, + } +) + +export const cfaRepository = buildMongooseModel(cfaSchema, "cfa") diff --git a/server/src/common/model/schema/multiCompte/entreprise.schema.ts b/server/src/common/model/schema/multiCompte/entreprise.schema.ts new file mode 100644 index 0000000000..dc3450402e --- /dev/null +++ b/server/src/common/model/schema/multiCompte/entreprise.schema.ts @@ -0,0 +1,58 @@ +import { IEntreprise } from "shared/models/entreprise.model.js" + +import { Schema } from "../../../mongodb.js" + +import { buildMongooseModel } from "./buildMongooseModel.js" + +const entrepriseSchema = new Schema( + { + establishment_siret: { + type: String, + description: "Siret de l'établissement", + }, + opco: { + type: String, + default: null, + description: "Information sur l'opco de l'entreprise", + }, + idcc: { + type: String, + description: "Identifiant convention collective de l'entreprise", + }, + establishment_raison_sociale: { + type: String, + description: "Raison social de l'établissement", + }, + establishment_enseigne: { + type: String, + default: null, + description: "Enseigne de l'établissement", + }, + address_detail: { + type: Object, + description: "Detail de l'adresse de l'établissement", + }, + address: { + type: String, + description: "Adresse de l'établissement", + }, + geo_coordinates: { + type: String, + default: null, + description: "Latitude/Longitude de l'adresse de l'entreprise", + }, + establishment_id: { + type: String, + description: "Si l'utilisateur est une entreprise, l'objet doit contenir un identifiant de formulaire unique", + }, + origin: { + type: String, + description: "Origine de la creation de l'utilisateur (ex: Campagne mail, lien web, etc...) pour suivi", + }, + }, + { + timestamps: true, + } +) + +export const entrepriseRepository = buildMongooseModel(entrepriseSchema, "entreprise") diff --git a/server/src/common/model/schema/multiCompte/roleManagement.schema.ts b/server/src/common/model/schema/multiCompte/roleManagement.schema.ts new file mode 100644 index 0000000000..40f19439b4 --- /dev/null +++ b/server/src/common/model/schema/multiCompte/roleManagement.schema.ts @@ -0,0 +1,72 @@ +import { AccessEntityType, AccessStatus, IRoleManagement, IRoleManagementEvent } from "shared/models/roleManagement.model.js" + +import { VALIDATION_UTILISATEUR } from "../../../../services/constant.service.js" +import { Schema } from "../../../mongodb.js" + +import { buildMongooseModel } from "./buildMongooseModel.js" + +const roleManagementEventSchema = new Schema( + { + validation_type: { + type: String, + enum: Object.values(VALIDATION_UTILISATEUR), + description: "Indique si l'action est ordonnée par un utilisateur ou le serveur", + }, + status: { + type: String, + enum: Object.values(AccessStatus), + description: "Statut de l'utilisateur", + }, + reason: { + type: String, + description: "Raison du changement de statut", + }, + granted_by: { + type: String, + default: null, + description: "Utilisateur à l'origine du changement", + }, + date: { + type: Date, + default: () => new Date(), + description: "Date de l'évènement", + }, + }, + { _id: false } +) + +const roleManagementSchema = new Schema( + { + accessor_id: { + type: String, + description: "ID de l'entité ayant accès", + }, + accessor_type: { + type: String, + enum: Object.values(AccessEntityType), + description: "Type de l'entité ayant accès", + }, + accessed_id: { + type: String, + description: "ID de l'entité sur laquelle l'accès est exercé", + }, + accessed_type: { + type: String, + enum: Object.values(AccessEntityType), + description: "Type de l'entité sur laquelle l'accès est exercé", + }, + history: { + type: [roleManagementEventSchema], + description: "Evénements liés au cycle de vie de l'accès", + }, + origin: { + type: String, + description: "Origine de la creation", + }, + }, + { + timestamps: true, + } +) + +export const roleManagementRepository = buildMongooseModel(roleManagementSchema, "roleManagement") diff --git a/server/src/common/model/schema/multiCompte/user2.schema.ts b/server/src/common/model/schema/multiCompte/user2.schema.ts new file mode 100644 index 0000000000..c89ca85a03 --- /dev/null +++ b/server/src/common/model/schema/multiCompte/user2.schema.ts @@ -0,0 +1,84 @@ +import { IUser2, IUserStatusEvent, UserEventType } from "shared/models/user2.model.js" + +import { VALIDATION_UTILISATEUR } from "../../../../services/constant.service.js" +import { Schema } from "../../../mongodb.js" + +import { buildMongooseModel } from "./buildMongooseModel.js" + +const userStatusEventSchema = new Schema( + { + validation_type: { + type: String, + enum: Object.values(VALIDATION_UTILISATEUR), + description: "Indique si l'action est ordonnée par un utilisateur ou le serveur", + }, + status: { + type: String, + enum: Object.values(UserEventType), + description: "Statut de l'utilisateur", + }, + reason: { + type: String, + description: "Raison du changement de statut", + }, + granted_by: { + type: String, + default: null, + description: "Utilisateur à l'origine du changement", + }, + date: { + type: Date, + default: () => new Date(), + description: "Date de l'évènement", + }, + }, + { _id: false } +) + +const User2Schema = new Schema( + { + firstname: { + type: String, + default: null, + description: "Le prénom", + }, + lastname: { + type: String, + default: null, + description: "Le nom", + }, + phone: { + type: String, + default: null, + description: "Le numéro de téléphone", + }, + email: { + type: String, + default: null, + description: "L'email", + }, + last_connection: { + type: Date, + default: null, + description: "Date de dernière connexion", + }, + is_anonymized: { + type: Boolean, + default: false, + description: "Si l'enregistrement est anonymisé", + }, + history: { + type: [userStatusEventSchema], + description: "Evénements liés au cycle de vie de l'utilisateur", + }, + origin: { + type: String, + description: "Origine de la creation de l'utilisateur (ex: Campagne mail, lien web, etc...) pour suivi", + }, + }, + { + timestamps: true, + } +) + +export const user2Repository = buildMongooseModel(User2Schema, "user2") diff --git a/server/src/common/model/schema/user/user.schema.ts b/server/src/common/model/schema/user/user.schema.ts index 9bdcfaa846..4f25ab3d88 100644 --- a/server/src/common/model/schema/user/user.schema.ts +++ b/server/src/common/model/schema/user/user.schema.ts @@ -44,7 +44,7 @@ export const userSchema = new Schema( role: { type: String, default: null, - description: "candidat | cfa | administrator", + description: "candidat | administrator", }, last_action_date: { type: Date, diff --git a/server/src/common/utils/asyncUtils.ts b/server/src/common/utils/asyncUtils.ts index 502389b0b6..d6acea50b9 100644 --- a/server/src/common/utils/asyncUtils.ts +++ b/server/src/common/utils/asyncUtils.ts @@ -4,6 +4,13 @@ export const asyncForEach = async (array: T[], callback: (item: T, index: num } } +export const asyncForEachGrouped = async (array: T[], groupSize: number, callback: (item: T, index: number) => Promise) => { + for (let index = 0; index < array.length; index += groupSize) { + const group = array.slice(index, index + groupSize) + await Promise.all(group.map((item, itemIndex) => callback(item, index + itemIndex))) + } +} + export const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) export function timeout(promise, millis) { diff --git a/server/src/common/utils/enumUtils.ts b/server/src/common/utils/enumUtils.ts new file mode 100644 index 0000000000..c5c2fa24bd --- /dev/null +++ b/server/src/common/utils/enumUtils.ts @@ -0,0 +1,12 @@ +export const parseEnum = (enumObj: Record, value: string | null): T | null => { + return Object.values(enumObj).find((enumValue) => enumValue.toLowerCase() === value?.toLowerCase()) ?? null +} +export const isEnum = (enumValues: Record, value: unknown): value is T => typeof value === "string" && parseEnum(enumValues, value) !== null + +export const parseEnumOrError = (enumObj: Record, value: string | null): T => { + const enumValue = parseEnum(enumObj, value) + if (enumValue === null) { + throw new Error(`could not parse ${value} as enum ${JSON.stringify(Object.values(enumObj))}`) + } + return enumValue +} diff --git a/server/src/jobs/multiCompte/migrationUsers.ts b/server/src/jobs/multiCompte/migrationUsers.ts new file mode 100644 index 0000000000..f09b8413c0 --- /dev/null +++ b/server/src/jobs/multiCompte/migrationUsers.ts @@ -0,0 +1,299 @@ +import dayjs from "dayjs" +import { AppointmentUserType } from "shared/constants/appointment.js" +import { ICFA } from "shared/models/cfa.model.js" +import { IEntreprise } from "shared/models/entreprise.model.js" +import { AccessEntityType, AccessStatus, IRoleManagement, IRoleManagementEvent } from "shared/models/roleManagement.model.js" +import { UserEventType, IUser2 } from "shared/models/user2.model.js" +import { IUserRecruteur } from "shared/models/usersRecruteur.model.js" + +import { logger } from "../../common/logger.js" +import { Appointment, User, UserRecruteur } from "../../common/model/index.js" +import { cfaRepository } from "../../common/model/schema/multiCompte/cfa.schema.js" +import { entrepriseRepository } from "../../common/model/schema/multiCompte/entreprise.schema.js" +import { roleManagementRepository } from "../../common/model/schema/multiCompte/roleManagement.schema.js" +import { user2Repository } from "../../common/model/schema/multiCompte/user2.schema.js" +import { asyncForEachGrouped } from "../../common/utils/asyncUtils.js" +import { parseEnumOrError } from "../../common/utils/enumUtils.js" +import { notifyToSlack } from "../../common/utils/slackUtils.js" +import { ENTREPRISE, ETAT_UTILISATEUR, OPCOS, VALIDATION_UTILISATEUR } from "../../services/constant.service.js" + +const migrationCandidats = async (now: Date) => { + logger.info(`Migration: lecture des user candidats...`) + + // l'utilisateur admin n'est pas repris + const candidats = await User.find({ role: "candidat" }) + logger.info(`Migration: ${candidats.length} user candidats à mettre à jour`) + const stats = { success: 0, failure: 0, alreadyExist: 0 } + + await asyncForEachGrouped(candidats, 100, async (candidat, index) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { username, password, firstname, lastname, phone, email, role, type, last_action_date, is_anonymized, _id } = candidat + index % 1000 === 0 && logger.info(`import du candidat n°${index}`) + try { + if (type) { + await Appointment.updateMany({ applicant_id: candidat._id }, { $set: { applicant_user_type: parseEnumOrError(AppointmentUserType, type) } }) + } + const existingUser = await user2Repository.findOne({ email }) + if (existingUser) { + await Appointment.updateMany({ applicant_id: candidat._id }, { $set: { applicant_id: existingUser._id } }) + if (dayjs(candidat.last_action_date).isAfter(existingUser.last_connection)) { + await user2Repository.updateOne({ _id: existingUser._id }, { last_connection: candidat.last_action_date }) + } + stats.alreadyExist++ + return + } + const newUser: Omit = { + firstname, + lastname, + phone, + email, + last_connection: last_action_date, + is_anonymized: is_anonymized, + createdAt: last_action_date, + updatedAt: last_action_date, + origin: "migration user candidat", + history: [ + { + date: now, + reason: "migration", + status: is_anonymized ? UserEventType.DESACTIVE : UserEventType.ACTIF, + validation_type: VALIDATION_UTILISATEUR.AUTO, + }, + ], + } + await user2Repository.create({ ...newUser, _id: candidat._id }) + stats.success++ + } catch (err) { + logger.error(`erreur lors de l'import du user candidat avec id=${_id}`) + logger.error(err) + stats.failure++ + } + }) + logger.info(`Migration: user candidats terminés`) + const message = `${stats.success} user candidats repris avec succès. + ${stats.failure} user candidats en erreur. + ${stats.alreadyExist} users en doublon.` + logger.info(message) + await notifyToSlack({ + subject: "Migration multi-compte", + message, + error: stats.failure > 0, + }) + return stats +} + +const migrationRecruteurs = async () => { + logger.info(`Migration: lecture des user recruteurs...`) + const userRecruteurs: IUserRecruteur[] = await UserRecruteur.find({}) + logger.info(`Migration: ${userRecruteurs.length} user recruteurs à mettre à jour`) + const stats = { success: 0, failure: 0, entrepriseCreated: 0, cfaCreated: 0, userCreated: 0, adminAccess: 0, opcoAccess: 0 } + + await asyncForEachGrouped(userRecruteurs, 100, async (userRecruteur, index) => { + const { + last_name, + first_name, + opco, + idcc, + establishment_raison_sociale, + establishment_enseigne, + establishment_siret, + address_detail, + address, + geo_coordinates, + phone, + email, + scope, + type, + establishment_id, + origin: originRaw, + is_email_checked, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + is_qualiopi, + status, + last_connection, + createdAt, + updatedAt, + } = userRecruteur + const origin = originRaw || "user migration" + index % 1000 === 0 && logger.info(`import du user recruteur n°${index}`) + try { + const fieldsUpdate: Omit = { + firstname: first_name ?? "", + lastname: last_name ?? "", + phone: phone ?? "", + email, + last_connection: last_connection, + is_anonymized: false, + createdAt, + updatedAt, + origin, + history: [ + ...(is_email_checked + ? [ + { + date: createdAt, + reason: "migration", + status: UserEventType.VALIDATION_EMAIL, + validation_type: VALIDATION_UTILISATEUR.AUTO, + granted_by: "migration", + }, + ] + : []), + { + date: createdAt, + reason: "migration", + status: UserEventType.ACTIF, + validation_type: VALIDATION_UTILISATEUR.AUTO, + granted_by: "migration", + }, + ], + } + await user2Repository.create({ ...fieldsUpdate, _id: userRecruteur._id }) + stats.userCreated++ + if (type === ENTREPRISE) { + if (!establishment_siret) { + throw new Error("inattendu pour une ENTERPRISE: pas de establishment_siret") + } + if (!address_detail) { + throw new Error("inattendu pour une ENTERPRISE: pas de address_detail") + } + const entreprise: IEntreprise = { + establishment_siret, + address, + address_detail, + establishment_enseigne, + establishment_id, + establishment_raison_sociale, + geo_coordinates, + idcc, + opco, + origin, + createdAt, + updatedAt, + } + const createdEntreprise = await entrepriseRepository.create({ ...entreprise, _id: userRecruteur._id }) + stats.entrepriseCreated++ + const roleManagement: Omit = { + accessor_id: userRecruteur._id, + accessor_type: AccessEntityType.USER, + accessed_type: AccessEntityType.ENTREPRISE, + accessed_id: createdEntreprise._id, + createdAt: userRecruteur.createdAt, + updatedAt: userRecruteur.updatedAt, + origin, + history: userRecruteurStatusToRoleManagementStatus(status), + } + await roleManagementRepository.create(roleManagement) + } else if (type === "CFA") { + if (!establishment_siret) { + throw new Error("inattendu pour un CFA: pas de establishment_siret") + } + const cfa: Omit = { + establishment_siret, + address, + address_detail, + establishment_enseigne, + establishment_raison_sociale, + geo_coordinates, + origin, + createdAt, + updatedAt, + } + const createdCfa = await cfaRepository.create({ ...cfa, _id: userRecruteur._id }) + stats.cfaCreated++ + const roleManagement: Omit = { + accessor_id: userRecruteur._id, + accessor_type: AccessEntityType.USER, + accessed_type: AccessEntityType.CFA, + accessed_id: createdCfa._id, + createdAt: userRecruteur.createdAt, + updatedAt: userRecruteur.updatedAt, + origin, + history: userRecruteurStatusToRoleManagementStatus(status), + } + await roleManagementRepository.create(roleManagement) + } else if (type === "ADMIN") { + const roleManagement: Omit = { + accessor_id: userRecruteur._id, + accessor_type: AccessEntityType.USER, + accessed_type: AccessEntityType.ADMIN, + accessed_id: "", + createdAt: userRecruteur.createdAt, + updatedAt: userRecruteur.updatedAt, + origin, + history: userRecruteurStatusToRoleManagementStatus(status), + } + await roleManagementRepository.create(roleManagement) + stats.adminAccess++ + } else if (type === "OPCO") { + const opco = parseEnumOrError(OPCOS, scope ?? null) + const roleManagement: Omit = { + accessor_id: userRecruteur._id, + accessor_type: AccessEntityType.USER, + accessed_type: AccessEntityType.OPCO, + accessed_id: opco, + createdAt: userRecruteur.createdAt, + updatedAt: userRecruteur.updatedAt, + origin, + history: userRecruteurStatusToRoleManagementStatus(status), + } + await roleManagementRepository.create(roleManagement) + stats.opcoAccess++ + } + stats.success++ + } catch (err) { + logger.error(`erreur lors de l'import du user recruteur avec id=${userRecruteur._id}`) + logger.error(err) + stats.failure++ + } + }) + logger.info(`Migration: user candidats terminés`) + const message = `${stats.success} user recruteurs repris avec succès. + ${stats.failure} user recruteurs en erreur. + ${stats.userCreated} user créés. + ${stats.entrepriseCreated} entreprises créées. + ${stats.cfaCreated} CFA créés. + ` + logger.info(message) + await notifyToSlack({ + subject: "Migration multi-compte", + message, + error: stats.failure > 0, + }) + return stats +} + +function userRecruteurStatusToRoleManagementStatus(allStatus: IUserRecruteur["status"]): IRoleManagementEvent[] { + return allStatus.flatMap((statusEvent) => { + const { date, reason, status, user, validation_type } = statusEvent + const statusMapping: Record = { + [ETAT_UTILISATEUR.DESACTIVE]: AccessStatus.DENIED, + [ETAT_UTILISATEUR.VALIDE]: AccessStatus.GRANTED, + [ETAT_UTILISATEUR.ATTENTE]: AccessStatus.AWAITING_VALIDATION, + [ETAT_UTILISATEUR.ERROR]: null, + } + const accessStatus = status ? statusMapping[status] : null + if (accessStatus && date) { + const newEvent: IRoleManagementEvent = { + date, + reason: reason ?? "", + validation_type: validation_type, + granted_by: user, + status: accessStatus, + } + return [newEvent] + } else { + return [] + } + }) +} + +export const migrationUsers = async () => { + await user2Repository.deleteMany({}) + await entrepriseRepository.deleteMany({}) + await cfaRepository.deleteMany({}) + await roleManagementRepository.deleteMany({}) + const now = new Date() + await migrationRecruteurs() + await migrationCandidats(now) +} diff --git a/server/src/services/constant.service.ts b/server/src/services/constant.service.ts index 64b58ac07d..8842fa10f9 100644 --- a/server/src/services/constant.service.ts +++ b/server/src/services/constant.service.ts @@ -40,18 +40,18 @@ export const REGEX = { GEO: /^(-?\d+(\.\d+)?),\s*(-?\d+(\.\d+)?)$/, TELEPHONE: /^[0-9]{10}$/, } -export const OPCOS = { - AFDAS: "AFDAS", - AKTO: "AKTO / Opco entreprises et salariés des services à forte intensité de main d'oeuvre", - ATLAS: "ATLAS", - CONSTRUCTYS: "Constructys", - OPCOMMERCE: "L'Opcommerce", - OCAPIAT: "OCAPIAT", - OPCO2I: "OPCO 2i", - EP: "Opco entreprises de proximité", - MOBILITE: "Opco Mobilités", - SANTE: "Opco Santé", - UNIFORMATION: "Uniformation, l'Opco de la Cohésion sociale", +export enum OPCOS { + AFDAS = "AFDAS", + AKTO = "AKTO / Opco entreprises et salariés des services à forte intensité de main d'oeuvre", + ATLAS = "ATLAS", + CONSTRUCTYS = "Constructys", + OPCOMMERCE = "L'Opcommerce", + OCAPIAT = "OCAPIAT", + OPCO2I = "OPCO 2i", + EP = "Opco entreprises de proximité", + MOBILITE = "Opco Mobilités", + SANTE = "Opco Santé", + UNIFORMATION = "Uniformation, l'Opco de la Cohésion sociale", } export const NIVEAUX_POUR_LBA = { diff --git a/shared/constants/appointment.ts b/shared/constants/appointment.ts new file mode 100644 index 0000000000..088cd9b463 --- /dev/null +++ b/shared/constants/appointment.ts @@ -0,0 +1,4 @@ +export enum AppointmentUserType { + PARENT = "parent", + ETUDIENT = "etudiant", +} diff --git a/shared/models/appointments.model.ts b/shared/models/appointments.model.ts index d22af97484..ed527a76bb 100644 --- a/shared/models/appointments.model.ts +++ b/shared/models/appointments.model.ts @@ -1,8 +1,10 @@ import { Jsonify } from "type-fest" +import { AppointmentUserType } from "../constants/appointment" import { z } from "../helpers/zodWithOpenApi" import { zObjectId } from "./common" +import { enumToZod } from "./enumToZod" export const ZMailing = z .object({ @@ -34,6 +36,7 @@ export const ZAppointment = z created_at: z.date().default(() => new Date()), cfa_recipient_email: z.string(), is_anonymized: z.boolean().default(false), + applicant_user_type: enumToZod(AppointmentUserType).nullish(), }) .strict() .openapi("Appointment") diff --git a/shared/models/cfa.model.ts b/shared/models/cfa.model.ts index aba270a3bf..3436385dc2 100644 --- a/shared/models/cfa.model.ts +++ b/shared/models/cfa.model.ts @@ -1,15 +1,22 @@ +import { Jsonify } from "type-fest" + import { z } from "../helpers/zodWithOpenApi" +import { ZGlobalAddress } from "./address.model" + export const zCFA = z .object({ - establishment_state: z.string(), - is_qualiopi: z.string(), establishment_siret: z.string(), - establishment_raison_sociale: z.string(), - contacts: z.string(), - address_detail: z.string(), - address: z.string(), - geo_coordinates: z.string(), + establishment_raison_sociale: z.string().nullish(), + establishment_enseigne: z.string().nullish(), + address_detail: ZGlobalAddress.nullish(), + address: z.string().nullish(), + geo_coordinates: z.string().nullish(), + origin: z.string().nullish(), + createdAt: z.date(), + updatedAt: z.date(), }) .strict() - .openapi("Model") + +export type ICFA = z.output +export type ICFAJson = Jsonify> diff --git a/shared/models/entreprise.model.ts b/shared/models/entreprise.model.ts new file mode 100644 index 0000000000..37c7198f27 --- /dev/null +++ b/shared/models/entreprise.model.ts @@ -0,0 +1,25 @@ +import { Jsonify } from "type-fest" + +import { z } from "../helpers/zodWithOpenApi" + +import { ZGlobalAddress } from "./address.model" + +export const ZEntreprise = z + .object({ + establishment_siret: z.string(), + opco: z.string().nullish(), + idcc: z.string().nullish(), + establishment_raison_sociale: z.string().nullish(), + establishment_enseigne: z.string().nullish(), + address_detail: ZGlobalAddress.nullish(), + address: z.string().nullish(), + geo_coordinates: z.string().nullish(), + origin: z.string().nullish(), + establishment_id: z.string().nullish(), + createdAt: z.date(), + updatedAt: z.date(), + }) + .strict() + +export type IEntreprise = z.output +export type IEntrepriseJson = Jsonify> diff --git a/shared/models/enumToZod.ts b/shared/models/enumToZod.ts new file mode 100644 index 0000000000..f14f6ea382 --- /dev/null +++ b/shared/models/enumToZod.ts @@ -0,0 +1,6 @@ +import { z } from "../helpers/zodWithOpenApi" + +export function enumToZod(enumObject: Record) { + const enumValues = Object.values(enumObject) as string[] + return z.enum([enumValues[0], ...enumValues.slice(1)]) +} diff --git a/shared/models/roleManagement.model.ts b/shared/models/roleManagement.model.ts new file mode 100644 index 0000000000..e51e35e6bf --- /dev/null +++ b/shared/models/roleManagement.model.ts @@ -0,0 +1,48 @@ +import { z } from "../helpers/zodWithOpenApi" + +import { zObjectId } from "./common" +import { enumToZod } from "./enumToZod" +import { ZValidationUtilisateur } from "./user2.model.js" + +export enum AccessEntityType { + USER = "USER", + ENTREPRISE = "ENTREPRISE", + CFA = "CFA", + OPCO = "OPCO", + ADMIN = "ADMIN", +} + +export enum AccessStatus { + GRANTED = "GRANTED", + DENIED = "DENIED", + AWAITING_VALIDATION = "AWAITING_VALIDATION", +} + +export const ZRoleManagementEvent = z + .object({ + validation_type: ZValidationUtilisateur, + status: enumToZod(AccessStatus), + reason: z.string(), + date: z.date(), + granted_by: z.string().nullish(), + }) + .strict() + +export const ZAccessEntityType = enumToZod(AccessEntityType) + +export const ZRoleManagement = z + .object({ + accessor_id: zObjectId, + accessor_type: ZAccessEntityType, + accessed_id: z.string(), + accessed_type: ZAccessEntityType, + origin: z.string(), + history: z.array(ZRoleManagementEvent), + createdAt: z.date(), + updatedAt: z.date(), + }) + .strict() + +export type IRoleManagement = z.output + +export type IRoleManagementEvent = z.output diff --git a/shared/models/user2.model.ts b/shared/models/user2.model.ts new file mode 100644 index 0000000000..a66acca3cc --- /dev/null +++ b/shared/models/user2.model.ts @@ -0,0 +1,48 @@ +import { Jsonify } from "type-fest" + +import { VALIDATION_UTILISATEUR } from "../constants/recruteur" +import { extensions } from "../helpers/zodHelpers/zodPrimitives" +import { z } from "../helpers/zodWithOpenApi" + +import { zObjectId } from "./common" +import { enumToZod } from "./enumToZod" + +export enum UserEventType { + ACTIF = "ACTIF", + VALIDATION_EMAIL = "VALIDATION_EMAIL", + DESACTIVE = "DESACTIVE", +} + +export const ZValidationUtilisateur = enumToZod(VALIDATION_UTILISATEUR) + +export const ZUserStatusEvent = z + .object({ + validation_type: ZValidationUtilisateur, + status: enumToZod(UserEventType), + reason: z.string(), + granted_by: z.string().nullish(), + date: z.date(), + }) + .strict() + +export const ZUser2 = z + .object({ + id: zObjectId, + firstname: z.string(), + lastname: z.string(), + phone: extensions.phone(), + email: z.string().email(), + last_connection: z.date().nullish(), + is_anonymized: z.boolean(), + origin: z.string().nullish(), + history: z.array(ZUserStatusEvent), + createdAt: z.date(), + updatedAt: z.date(), + }) + .strict() + +export type IUser2 = z.output +export type IUser2Json = Jsonify> + +export type IUserStatusEvent = z.output +export type IUserStatusEventJson = Jsonify>