import BaseService from "@Services/BaseService"; import { BackendVariables } from "@Common/config/variables/Variables"; import { Service } from "typedi"; import jwt from "jsonwebtoken"; import UsersService from "@Services/super-admin/UsersService/UsersService"; import { IdNotJwtPayload } from "../AuthService/AuthService"; import User, { DeedType, Office, OfficeRole, Role, DocumentType } from "le-coffre-resources/dist/SuperAdmin"; import RolesService from "@Services/super-admin/RolesService/RolesService"; import OfficesService from "@Services/super-admin/OfficesService/OfficesService"; import { EOfficeStatus } from "@prisma/client"; import OfficeRolesService from "@Services/super-admin/OfficeRolesService/OfficeRolesService"; import DeedTypesService from "@Services/super-admin/DeedTypesService/DeedTypesService"; import DocumentTypesService from "@Services/super-admin/DocumentTypesService/DocumentTypesService"; import * as Sentry from "@sentry/node"; interface IIdNotToken { access_token: string; expires_in: number; id_token: string; token_type: string; } interface IRattachementData { entiteUrl: string; personneUrl: string; entite: { codeCrpcen: string; typeEntite: { name: string; }; ou: string; denominationSociale: string; statutEntite: { name: string; }; locationsUrl: string; }; personne: { numeroTelephonePro: string; prenom: string; nomUsuel: string; mobilePro: string; numeroAdherentCrpcen: string; civilite: string; }; typeLien: { name: string; }; numeroMobile: string; numeroTelephone: string; statutDuRattachement: boolean; mailRattachement: string; deleted: boolean; } interface IOfficeData { ou: string; intitule: string; denominationSociale: string; departementResidence: { libelle: string; code: string; }[]; libelle: string; codeCrpcen: string; locationsUrl: string; statutEntite: { name: string; }; typeEntite: { name: string; }; } interface IOfficeLocation { totalResultCount: number; page: number; size: number; result: { mail: string; numeroTelephone: string; adrGeo4: string; adrGeoCodePostal: string; adrGeoVille: string; ou: string; lastModified: string; deleted: boolean; }[]; } enum EIdnotRole { DIRECTEUR = "Directeur général du CSN", NOTAIRE_TITULAIRE = "Notaire titulaire", NOTAIRE_ASSOCIE = "Notaire associé", NOTAIRE_SALARIE = "Notaire salarié", COLLABORATEUR = "Collaborateur", SECRETAIRE_GENERAL = "Secrétaire général", SUPPLEANT = "Suppléant", ADMINISTRATEUR = "Administrateur", RESPONSABLE = "Responsable", CURATEUR = "Curateur", } @Service() export default class IdNotService extends BaseService { constructor( protected variables: BackendVariables, private userService: UsersService, private officeService: OfficesService, private rolesService: RolesService, private officeRolesService: OfficeRolesService, private deedTypesService: DeedTypesService, private documentTypesService: DocumentTypesService, ) { super(); } public async getIdNotToken(code: string) { const query = new URLSearchParams({ client_id: this.variables.IDNOT_CLIENT_ID, client_secret: this.variables.IDNOT_CLIENT_SECRET, redirect_uri: this.variables.IDNOT_REDIRECT_URL, code: code, grant_type: "authorization_code", }); try { const token = await fetch(this.variables.IDNOT_BASE_URL + this.variables.IDNOT_CONNEXION_URL + "?" + query, { method: "POST" }); if (token.status !== 200) console.error(await token.text()); const decodedToken = (await token.json()) as IIdNotToken; const decodedIdToken = jwt.decode(decodedToken.id_token) as IdNotJwtPayload; return decodedIdToken; } catch (error) { console.error("Error fetching " + this.variables.IDNOT_BASE_URL + this.variables.IDNOT_CONNEXION_URL ,error); return null; } } public async getRole(roleName: string): Promise { switch (roleName) { case EIdnotRole.NOTAIRE_TITULAIRE: return (await this.rolesService.get({ where: { name: "admin" } }))[0]!; case EIdnotRole.NOTAIRE_ASSOCIE: return (await this.rolesService.get({ where: { name: "admin" } }))[0]!; case EIdnotRole.NOTAIRE_SALARIE: return (await this.rolesService.get({ where: { name: "notary" } }))[0]!; case EIdnotRole.COLLABORATEUR: return (await this.rolesService.get({ where: { name: "notary" } }))[0]!; case EIdnotRole.SUPPLEANT: return (await this.rolesService.get({ where: { name: "notary" } }))[0]!; case EIdnotRole.ADMINISTRATEUR: return (await this.rolesService.get({ where: { name: "admin" } }))[0]!; case EIdnotRole.CURATEUR: return (await this.rolesService.get({ where: { name: "notary" } }))[0]!; default: return (await this.rolesService.get({ where: { name: "default" } }))[0]!; } } public async getOfficeRole(roleName: string, officeUid: string) { switch (roleName) { case EIdnotRole.NOTAIRE_TITULAIRE: return (await this.officeRolesService.get({ where: { AND: [{ name: "Notaire" }, { office_uid: officeUid }] } }))[0]!; case EIdnotRole.NOTAIRE_ASSOCIE: return (await this.officeRolesService.get({ where: { AND: [{ name: "Notaire" }, { office_uid: officeUid }] } }))[0]!; case EIdnotRole.NOTAIRE_SALARIE: return (await this.officeRolesService.get({ where: { AND: [{ name: "Notaire" }, { office_uid: officeUid }] } }))[0]!; case EIdnotRole.COLLABORATEUR: return (await this.officeRolesService.get({ where: { AND: [{ name: "Collaborateur" }, { office_uid: officeUid }] } }))[0]!; case EIdnotRole.SUPPLEANT: return (await this.officeRolesService.get({ where: { AND: [{ name: "Collaborateur" }, { office_uid: officeUid }] } }))[0]!; case EIdnotRole.ADMINISTRATEUR: return (await this.officeRolesService.get({ where: { AND: [{ name: "Collaborateur" }, { office_uid: officeUid }] } }))[0]!; case EIdnotRole.CURATEUR: return (await this.officeRolesService.get({ where: { AND: [{ name: "Collaborateur" }, { office_uid: officeUid }] } }))[0]!; default: return; } } public async getOfficeMemberships(officeId: string) { const officeInfos = await this.officeService.getByUid(officeId); const office = Office.hydrate(officeInfos!); const searchParams = new URLSearchParams({ key: this.variables.IDNOT_API_KEY, }); console.log("getOfficeMemberships"); const url = `${this.variables.IDNOT_API_BASE_URL}/api/pp/v2/entites/${office.idNot}/personnes?` + searchParams; try { console.log("Tentative de connexion à l'URL :", url); const response = await fetch(url, { method: "GET", }); return (await response.json()) as any; } catch (error) { console.error("Erreur lors de l'appel à fetch :", error); return null; } } public getOfficeStatus(statusName: string) { switch (statusName) { case "Pourvu": return EOfficeStatus.ACTIVATED; case "Pourvu mais décédé": return EOfficeStatus.ACTIVATED; case "Sans titulaire": return EOfficeStatus.ACTIVATED; case "Vacance": return EOfficeStatus.ACTIVATED; case "En activité": return EOfficeStatus.ACTIVATED; default: return EOfficeStatus.DESACTIVATED; } } public async updateUser(userId: string) { const userInfos = await this.userService.getByUid(userId, { contact: true, role: true, office_membership: true }); const user = User.hydrate(userInfos!); console.log("Got user from db:", JSON.stringify(user)); const searchParams = new URLSearchParams({ key: this.variables.IDNOT_API_KEY, }); let userRawData: Response; try { userRawData = await fetch( `${this.variables.IDNOT_API_BASE_URL}/api/pp/v2/rattachements/${user.idNot}_${user.office_membership!.idNot}?` + searchParams, { method: "GET", }, ) } catch (error) { console.error(`Error fetching user data for ${user.uid}: ${error}`); return; } if (userRawData.status === 404) { console.error(`User ${user.uid} not found in idnot`); return; } else if (userRawData.status !== 200) { console.error(`Error fetching user data for ${user.uid}: ${userRawData.status} - ${userRawData.statusText}`); return; } let userData = (await userRawData.json()) as IRattachementData; console.log("Got userData from idnot", JSON.stringify(userData)); if (userData.deleted) { let rattachements: any; console.log("Fetching rattachements for user", user.uid); try { const res = await fetch(`${this.variables.IDNOT_API_BASE_URL}/api/pp/v2/personnes/${user.idNot}/rattachements?` + searchParams, { method: "GET", }); if (res.status === 404) { console.error(`User ${user.uid} not found in idnot`); return; } else if (res.status !== 200) { console.error(`Error fetching rattachements for ${user.uid}: ${rattachements.status} - ${rattachements.statusText}`); return; } rattachements = await res.json(); } catch (error) { console.error("Error fetching rattachements", error); return; } console.log("rattachements", JSON.stringify(rattachements)); if (rattachements && rattachements.totalResultCount === 0 && rattachements.result.length === 0) { console.warn("User has no valid rattachements", user.uid); await this.userService.updateCheckedAt(user.uid!); return; } else { console.log(`Found ${rattachements.totalResultCount} rattachements for ${user.uid}`); } const rattachementsResults = rattachements.result as IRattachementData[]; if (!rattachementsResults) return; for (const rattachement of rattachementsResults) { if (rattachement.statutDuRattachement && !rattachement.deleted) { console.log("rattachementsResults", JSON.stringify(rattachementsResults)); let officeData: IOfficeData; try { officeData = (await ( await fetch(`${this.variables.IDNOT_API_BASE_URL + rattachement.entiteUrl}?` + searchParams, { method: "GET", }) ).json()) as IOfficeData; } catch (error) { console.error("Error fetching office data", error); continue; } console.log("officeData", JSON.stringify(officeData)); if (officeData.typeEntite.name === "office") { userData = rattachement; userData.entite = officeData; console.log("Updated userData", JSON.stringify(userData)); break; // Found the first valid office, no need to continue } } } } let update = false; if (user.office_membership!.idNot !== userData.entite.ou) { console.log("Updating user.office_membership"); update = true; let officeData = (await this.officeService.get({ where: { idNot: userData.entite.ou } }))[0]; if (!officeData) { let officeLocationData: IOfficeLocation; try { officeLocationData = (await ( await fetch(`${this.variables.IDNOT_API_BASE_URL + userData.entite.locationsUrl}?` + searchParams, { method: "GET", }) ).json()) as IOfficeLocation; } catch (error) { console.error("Error fetching office location data", error); return; } const office = { idNot: userData.entite.ou, name: userData.entite.denominationSociale ?? userData.entite.codeCrpcen, crpcen: userData.entite.codeCrpcen, office_status: this.getOfficeStatus(userData.entite.statutEntite.name), address: { address: officeLocationData.result[0]!.adrGeo4, city: officeLocationData.result[0]!.adrGeoVille.split(" ")[0] ?? officeLocationData.result[0]!.adrGeoVille, zip_code: Number(officeLocationData.result[0]!.adrGeoCodePostal), created_at: null, updated_at: null, }, created_at: null, updated_at: null, }; officeData = await this.officeService.create(office); } user.office_membership = officeData; console.log("Updated user.office_membership", JSON.stringify(user.office_membership)); } if (userData.mailRattachement && (user.contact!.email === null || user.contact!.email === undefined || user.contact!.email !== userData.mailRattachement)) { update = true; user.contact!.email = userData.mailRattachement; console.log("Updated user.contact.email", JSON.stringify(user.contact!.email)); } if (userData.numeroMobile && (user.contact!.cell_phone_number === null || user.contact!.cell_phone_number === undefined || user.contact!.cell_phone_number !== userData.numeroMobile)) { update = true; user.contact!.cell_phone_number = userData.numeroMobile; console.log("Updated user.contact.cell_phone_number", JSON.stringify(user.contact!.cell_phone_number)); } if (update) { // Filter out null values before updating to prevent Prisma errors const convertedUser = this.convertNullToUndefined(user); await this.userService.update(user.uid!, convertedUser); } await this.userService.updateCheckedAt(user.uid!); } public async updateOffice(officeId: string) { const officeInfos = await this.officeService.getByUid(officeId, { address: true }); const office = Office.hydrate(officeInfos!); const searchParams = new URLSearchParams({ key: this.variables.IDNOT_API_KEY, }); let officeRawData; try { officeRawData = await fetch(`${this.variables.IDNOT_API_BASE_URL}/api/pp/v2/entites/${office.idNot}?` + searchParams, { method: "GET", }); } catch (error) { console.error("Error fetching office data", error); return; } if (officeRawData.status === 404) { console.error("Fetching office raw data failed with status 404"); await this.officeService.updateCheckedAt(office.uid!); //await this.officeService.delete(office.uid!); return; } const officeData = (await officeRawData.json()) as IOfficeData; let updates = 0; //093051 = demo if (office.name !== officeData.denominationSociale && office.name !== officeData.codeCrpcen && office.crpcen !== "029178" && office.crpcen !== "035010" && office.crpcen !== "093051") { console.log(`Updating office name: ${office.uid} - ${office.name} - ${office.crpcen}`); updates++; office.name = officeData.denominationSociale ?? officeData.codeCrpcen; console.log(`New name: ${office.name}`); } if (office.office_status !== this.getOfficeStatus(officeData.statutEntite.name)) { console.log(`Updating office status: ${office.uid} - ${office.name} - ${office.crpcen}`); updates++; office.office_status = this.getOfficeStatus(officeData.statutEntite.name); console.log(`New status: ${office.office_status}`); } if (updates != 0) await this.officeService.update(office.uid!, office); await this.officeService.updateCheckedAt(office.uid!); } public async getOrCreateUser(decodedToken: IdNotJwtPayload) { let user = await this.userService.getByProvider("idNot", decodedToken.sub); if (!user) { const searchParams = new URLSearchParams({ key: this.variables.IDNOT_API_KEY, }); let userData: IRattachementData; try { userData = (await ( await fetch( `${this.variables.IDNOT_API_BASE_URL}/api/pp/v2/rattachements/${decodedToken.profile_idn}?` + searchParams, { method: "GET", }, ) ).json()) as IRattachementData; console.log("userData", userData); } catch (error) { console.error("Error fetching " + `${this.variables.IDNOT_API_BASE_URL}/api/pp/v2/rattachements/${decodedToken.profile_idn}?`, error); Sentry.captureException(error); return null; } if (!userData.statutDuRattachement || userData.entite.typeEntite.name !== "office") { console.info("User not attached to an office (May be a partner)"); return null; } let officeLocationData: IOfficeLocation; try { officeLocationData = (await ( await fetch(`${this.variables.IDNOT_API_BASE_URL + userData.entite.locationsUrl}?` + searchParams, { method: "GET" }) ).json()) as IOfficeLocation; } catch (error) { console.error("Error fetching" + `${this.variables.IDNOT_API_BASE_URL + userData.entite.locationsUrl}` , error); Sentry.captureException(error); return null; } console.log("officeLocationData", officeLocationData); const office = await this.officeService.get({ where: { idNot: decodedToken.entity_idn } }); // if(officeLocationData.result[0]!.adrGeoCodePostal.slice(0,2) !== "35") { // return null; // } const role = await this.getRole(userData.typeLien.name); const userToAdd = { idNot: decodedToken.sub, office_membership: { idNot: decodedToken.entity_idn, name: userData.entite.denominationSociale ?? userData.entite.codeCrpcen, crpcen: userData.entite.codeCrpcen, office_status: this.getOfficeStatus(userData.entite.statutEntite.name), address: { address: officeLocationData.result[0]!.adrGeo4, city: officeLocationData.result[0]!.adrGeoVille.split(" ")[0] ?? officeLocationData.result[0]!.adrGeoVille, //officeLocationData.result[0]!.adrPostaleVille, zip_code: Number(officeLocationData.result[0]!.adrGeoCodePostal), created_at: null, updated_at: null, }, created_at: null, updated_at: null, status: "ACTIVE", }, role: role, contact: { first_name: userData.personne.prenom, last_name: userData.personne.nomUsuel, email: userData.mailRattachement, phone_number: userData.numeroTelephone, cell_phone_number: userData.numeroMobile ?? userData.numeroTelephone, civility: userData.personne.civilite, created_at: null, updated_at: null, }, }; if (!userToAdd.contact.email) { console.error("User pro email empty"); return null; } let userHydrated = User.hydrate(userToAdd); const user = await this.userService.create(userHydrated); const userOffice = await this.officeService.getByUid(user.office_uid); userHydrated = User.hydrate(user); const userOfficeHydrated = Office.hydrate(userOffice!); if (office.length === 0) { const officeRoles = await this.officeRolesService.get({ where: { office: { idNot: "0000" } }, include: { office: true, rules: true }, }); const deedTypes = await this.deedTypesService.get({ where: { office: { idNot: "0000" } }, include: { office: true, document_types: { include: { office: true } } }, }); const documentTypes = await this.documentTypesService.get({ where: { office: { idNot: "0000" } }, include: { office: true }, }); const officeRolesHydrated = OfficeRole.hydrateArray(officeRoles); const deedTypesHydrated = DeedType.hydrateArray(deedTypes); const documentTypesHydrated = DocumentType.hydrateArray(documentTypes); await this.duplicateOfficeRoles(officeRolesHydrated, userOfficeHydrated); const documentTypesCreated = await this.duplicateDocumentTypes(documentTypesHydrated, userOfficeHydrated); await this.duplicateDeedTypes(deedTypesHydrated, documentTypesCreated, userOfficeHydrated); } const officeRole = await this.getOfficeRole(userData.typeLien.name, user.office_uid); if (officeRole) { const officeRoleHydrated = OfficeRole.hydrate(officeRole!); userHydrated.office_role = officeRoleHydrated; await this.userService.update(user.uid, userHydrated); } return user; } return user; } public async duplicateDocumentTypes(documentTypes: DocumentType[], office: Office): Promise { let newDocumentTypes: DocumentType[] = []; for (const documentType of documentTypes) { documentType.office = office; const documentTypeCreated = await this.documentTypesService.create(documentType); newDocumentTypes.push(DocumentType.hydrate(documentTypeCreated)); } return newDocumentTypes; } public async duplicateDeedTypes(deedTypes: DeedType[], documentTypes: DocumentType[], office: Office) { for (const deedType of deedTypes) { let newDocumentTypes: DocumentType[] = []; for (const document of deedType.document_types!) { const newDocumentType = documentTypes.find((documentType) => documentType.name === document.name); if (!newDocumentType) continue; newDocumentTypes.push(newDocumentType!); } deedType.document_types = newDocumentTypes; deedType.office = office; await this.deedTypesService.create(deedType); } } public async duplicateOfficeRoles(officeRoles: OfficeRole[], office: Office) { for (const officeRole of officeRoles) { officeRole.office = office; await this.officeRolesService.create(officeRole); } } public async updateUsers() { const usersReq = await this.userService.getUsersToBeChecked(); const users = User.hydrateArray(usersReq); for (const user of users) { await this.updateUser(user.uid!); } } public async updateOffices() { const officesReq = await this.officeService.getOfficesToBeChecked(); const offices = Office.hydrateArray(officesReq); console.log(`Updating ${offices.length} offices`); for (const office of offices) { await this.updateOffice(office.uid!); } } /** * Utility function to convert null values to undefined * This prevents Prisma from throwing errors about null values */ private convertNullToUndefined>(obj: T): T { const converted = { ...obj }; for (const [key, value] of Object.entries(converted)) { if (value === null) { console.log(`Converting null to undefined for key: ${key}`); (converted as any)[key] = undefined; } else if (typeof value === 'object' && !Array.isArray(value)) { (converted as any)[key] = this.convertNullToUndefined(value); } } return converted; } }