Files
aduzuki/controllers/sync_prof_strapiController.js
2026-07-20 16:33:04 +02:00

374 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/////////////////////////////////////////////////
// APPELS DES MODULES
/////////////////////////////////////////////////
const path = require("path");
// CALCUL DE DATE
const moment = require('moment');
// uuid
const { v4: uuidv4 } = require('uuid');
const mysql = require('mysql2');
const mysql_promise = require('mysql2/promise');
const fetch = require('node-fetch');
const fs = require('fs/promises');
const nodemailer = require('nodemailer');
/////////////////////////////////////////////////
// APPELS DES MODULES PERSOS
/////////////////////////////////////////////////
// loggings
const logger = require("../public/js/app/logger_adhesion");
// DB
const { connect_DB_promise } = require('../modules/db/databaseconnect');
////////////////////////////////////////////////////
// FILE NAME
////////////////////////////////////////////////////
const filename = path.parse(__filename).base;
const runSync_prof_strapi = async function(req, res) {
// LOG - Récupération du numéro de session web
// en mode CRON il n'y a pas de session web, donc pas de req.session, donc on met "cron" à la place
const sid = req?.session?.id || "cron";
logger.info(`sid=${sid}`, { numsession: sid, label: filename });
logger.info("======================================", { numsession: sid, label: filename });
logger.info("======== FUNCTION sync_prof_strapi ==========", { numsession: sid, label: filename });
logger.info("======================================", { numsession: sid, label: filename });
////////////////////////////////////////////////////
// VARIABLES
////////////////////////////////////////////////////
// On récupère les variables de ENV
const {
DOMAIN_STRAPI,
strapi_token,
db_connection_host,
db_connection_port,
db_connection_user,
db_connection_password,
db_connection_database
} = process.env;
logger.info(`DOMAIN_STRAPI=${DOMAIN_STRAPI}, db_connection_host=${db_connection_host}`, { numsession: sid, label: filename });
////////////////////////////////////////////////////
// CONNEXION DATABASE
////////////////////////////////////////////////////
// Initialisation de la connexion avec la BD
const db_connection_promise = await mysql_promise.createConnection({
host: db_connection_host,
port: db_connection_port,
user: db_connection_user,
password: db_connection_password,
database: db_connection_database
});
//console.log("db_connection_promise = ", db_connection_promise);
////////////////////////////////////////////////////
// FONCTIONS
////////////////////////////////////////////////////
// Récupération de la lat, long et région, de toutes les villes de France et Dom-TOM, depuis le json
// pour l'ajout de la lat, long et region dans les données de strapi pour le prof
let CP_MAP = null;
async function loadCpMap(filePath = "src/data/cp_region_lat_long.json") {
if (CP_MAP) return CP_MAP;
const buf = await fs.readFile(filePath, "utf8");
CP_MAP = JSON.parse(buf);
return CP_MAP;
}
// ==== GEO helpers ====
// Fonctions pour normaliser le code postal saisie dans le formulaire par ladhérent
function normalizeCp(cp) {
if (!cp) return null;
const s = String(cp).trim().replace(/\s+/g, "");
return /^\d{5}$/.test(s) ? s : null;
}
function pickProfPostalCode(cp) {
return (
normalizeCp(cp) || null
);
}
// helper pour convertir en nombre sûr (gère "48,85" et strings)
const toNum = (v) => {
if (v == null) return null;
const n = (typeof v === "number") ? v : parseFloat(String(v).replace(",", "."));
return Number.isFinite(n) ? n : null;
};
// ==== FONCTION D'AJOUT DES PROFS DANS STRAPI ====
const upsert = async (payload) => {
logger.info("======== FUNCTION upsert ==========", { numsession: sid, label: filename });
// on cherche les professeurs dont adherent_mail est égal à payload.data.adherent_mail. Si pas demail, on met "".
// URLSearchParams classe native de JavaScript (dans Node.js et dans les navigateurs) qui sert à construire et manipuler des chaînes de requête (?clé=valeur&clé2=valeur2)
// ex de sortie q = filters%5Badherent_mail%5D%5B%24eq%5D=julie%40test.com
logger.info(`search = ${payload.data.adherent_mail}`, { numsession: sid, label: filename });
const q = new URLSearchParams({ "filters[adherent_mail][$eq]": payload.data.adherent_mail || "" });
// URL d'interogration strapi
const getUrl = `${DOMAIN_STRAPI}/api/adherent-professeurs?${q.toString()}`;
// Recherche dans Strapi
const r0 = await fetch(getUrl, { headers: { Authorization: `Bearer ${strapi_token}` }});
// lis le corps de la réponse HTTP et convertis-le depuis du JSON en objet JavaScript utilisable
const j0 = await r0.json();
const found = j0?.data?.[0];
logger.info(`found = ${found?.attributes?.adherent_mail}`, { numsession: sid, label: filename });
//console.log("found = ", found);
if (found?.id) {
// déjà présent → on NE LÈVE PAS derreur, on signale juste un skip
return { action: "skip_existing", id: found.id };
} else {
const postUrl = `${DOMAIN_STRAPI}/api/adherent-professeurs`;
logger.info(`postUrl = ${postUrl}`, { numsession: sid, label: filename });
const r = await fetch(postUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${strapi_token}`
},
body: JSON.stringify(payload)
});
if (!r.ok) {
const txt = await r.text();
throw new Error(`POST ${postUrl} -> ${r.status} ${txt}`);
}
const j = await r.json();
return { action: "create", id: j?.data?.id };
}
};
////////////////////////////////////////////////////
// LANCEMENT DES FONCTIONS
////////////////////////////////////////////////////
try {
// ==== AJOUT DES PROFS DANS STRAPI ====
let creates = 0, errors = 0, attempted = 0, skipped_existing = 0;
// ==== RÉCUPÉRATION DES DONNÉES SUR LES PROFS DANS LA DB ADUZUKI (SQL) ====
const [rows] = await db_connection_promise.query(`
SELECT
ID,
association_suzuki,
adherent_nom,
adherent_prenom,
adherent_mail,
adherent_telephone_mobile,
adherent_prof_instrument1,
adherent_prof_instrument1_esa,
adherent_prof_instrument2,
adherent_prof_instrument2_esa,
adherent_prof_sece,
adherent_prof_ecole1,
adherent_prof_ecole1_mail,
adherent_prof_ecole1_web,
adherent_prof_ecole1_cp,
adherent_prof_ecole1_ville,
adherent_prof_ecole2,
adherent_prof_ecole2_mail,
adherent_prof_ecole2_web,
adherent_prof_ecole2_cp,
adherent_prof_ecole2_ville,
adherent_prof_ecole3,
adherent_prof_ecole3_mail,
adherent_prof_ecole3_web,
adherent_prof_ecole3_adresse,
adherent_prof_ecole3_cp,
adherent_prof_ecole3_ville
FROM adherent_professeur
`);
//console.log('rows = ', rows);
// Lancement de la récupération des lat, long et régions depuis le json
const cpMap = await loadCpMap("src/data/cp_region_lat_long.json");
//console.log("CP_MAP = ", CP_MAP);
// TRAITEMENT DE CHAQUE PROF DE LA BD POUR L'AJOUTER DANS STRAPI
for (const r of rows) {
// 3.1 choisir le CP prioritaire
console.log(`r.adherent_prof_ecole1_cp = ${r.adherent_prof_ecole1_cp}`);
const cp_1 = pickProfPostalCode(r.adherent_prof_ecole1_cp);
const cp_2 = pickProfPostalCode(r.adherent_prof_ecole2_cp);
const cp_3 = pickProfPostalCode(r.adherent_prof_ecole3_cp);
console.log(`cp_1 = ${cp_1}, cp_2 = ${cp_2} `);
let geo_lat_1 = null, geo_lng_1 = null, geo_region_1 = null;
let geo_lat_2 = null, geo_lng_2 = null, geo_region_2 = null;
let geo_lat_3 = null, geo_lng_3 = null, geo_region_3 = null;
console.log("cpMap[cp_1] =", cpMap[cp_1]);
if (cp_1 && cpMap[cp_1]) {
console.log("cpMap[cp_1] =", cpMap[cp_1]);
const meta = cpMap[cp_1];
// tolérance: on ne renseigne que si lat/lng sont présents
geo_lat_1 = toNum(meta.lat);
geo_lng_1 = toNum(meta.lng);
geo_region_1 = meta.region ? String(meta.region) : null;
}
console.log("cpMap[cp_2] =", cpMap[cp_2]);
if (cp_2 && cpMap[cp_2]) {
console.log("cpMap[cp_2] =", cpMap[cp_2]);
const meta = cpMap[cp_2];
// tolérance: on ne renseigne que si lat/lng sont présents
geo_lat_2 = toNum(meta.lat);
geo_lng_2 = toNum(meta.lng);
geo_region_2 = meta.region ? String(meta.region) : null;
}
if (cp_3 && cpMap[cp_2]) {
const meta = cpMap[cp_3];
// tolérance: on ne renseigne que si lat/lng sont présents
geo_lat_3 = toNum(meta.lat);
geo_lng_3 = toNum(meta.lng);
geo_region_3 = meta.region ? String(meta.region) : null;
}
// 3.2 payload de base
console.log("geo_lat_2 =", geo_lat_2);
const data = {
association_suzuki: r.association_suzuki?.trim() || null,
adherent_nom: r.adherent_nom?.trim() || null,
adherent_prenom: r.adherent_prenom?.trim() || null,
adherent_mail: r.adherent_mail?.toString().trim().toLowerCase() || null,
adherent_telephone_mobile: r.adherent_telephone_mobile?.trim() || null,
adherent_prof_instrument1: r.adherent_prof_instrument1?.trim() || null,
adherent_prof_instrument1_niveau: r.adherent_prof_instrument1_esa?.trim() || null,
adherent_prof_instrument2: r.adherent_prof_instrument2?.trim() || null,
adherent_prof_instrument2_niveau: r.adherent_prof_instrument2_esa?.trim() || null,
adherent_prof_sece: r.adherent_prof_sece?.trim() || null,
adherent_prof_ecole1: r.adherent_prof_ecole1?.trim() || null,
adherent_prof_ecole1_mail: r.adherent_prof_ecole1_mail?.trim() || null,
adherent_prof_ecole1_web: r.adherent_prof_ecole1_web?.trim() || null,
adherent_prof_ecole1_cp: r.adherent_prof_ecole1_cp?.trim() || null,
adherent_prof_ecole1_ville: r.adherent_prof_ecole1_ville?.trim() || null,
adherent_prof_ecole2: r.adherent_prof_ecole2?.trim() || null,
adherent_prof_ecole2_mail: r.adherent_prof_ecole2_mail?.trim() || null,
adherent_prof_ecole2_web: r.adherent_prof_ecole2_web?.trim() || null,
adherent_prof_ecole2_cp: r.adherent_prof_ecole2_cp?.trim() || null,
adherent_prof_ecole2_ville: r.adherent_prof_ecole2_ville?.trim() || null,
adherent_prof_ecole3: r.adherent_prof_ecole3?.trim() || null,
adherent_prof_ecole3_mail: r.adherent_prof_ecole3_mail?.trim() || null,
adherent_prof_ecole3_web: r.adherent_prof_ecole3_web?.trim() || null,
adherent_prof_ecole3_adresse: r.adherent_prof_ecole3_adresse?.trim() || null,
adherent_prof_ecole3_cp: r.adherent_prof_ecole3_cp?.trim() || null,
adherent_prof_ecole3_ville: r.adherent_prof_ecole3_ville?.trim() || null,
adherent_prof_ecole3_ville: r.adherent_prof_ecole3_ville?.trim() || null,
Saison: "2025/2026",
// >>> Injecte UNIQUEMENT des scalaires, si valides
...(geo_region_1 ? { adherent_prof_ecole1_region: geo_region_1 } : {}),
...(geo_lat_1 != null ? { adherent_prof_ecole1_lat: geo_lat_1 } : {}),
...(geo_lng_1 != null ? { adherent_prof_ecole1_long: geo_lng_1 } : {}),
...(geo_region_2 ? { adherent_prof_ecole2_region: geo_region_2 } : {}),
...(geo_lat_2 != null ? { adherent_prof_ecole2_lat: geo_lat_2 } : {}),
...(geo_lng_2 != null ? { adherent_prof_ecole2_long: geo_lng_2 } : {}),
...(geo_region_3 ? { adherent_prof_ecole3_region: geo_region_3 } : {}),
...(geo_lat_3 != null ? { adherent_prof_ecole3_lat: geo_lat_3 } : {}),
...(geo_lng_3 != null ? { adherent_prof_ecole3_long: geo_lng_3 } : {})
};
console.log("data", data);
// champs obligatoires minimum
if (!data.adherent_mail || !data.adherent_nom || !data.adherent_prenom) {
console.warn(`skip: mail/nom/prenom manquants`);
continue;
}
try {
attempted++;
const res = await upsert({ data });
if (res.action === "create") creates++;
else if (res.action === "skip_existing") skipped_existing++;
} catch (e) {
errors++;
console.error(`ETL error ->`, e.message);
}
}
await db_connection_promise.end();
logger.info(`Done. attempted=${attempted} skipped_existing=${skipped_existing} created=${creates} errors=${errors}`, { numsession: sid, label: filename });
///////////////////////////////////////////////////////////////////////
// ENVOIE MAIL APRES EXECUTION
logger.info("MAIL MAJ ANNUAIRE PROF ENVOI AVANT", { numsession: sid });
var transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.nodemailer_user,
pass: process.env.nodemailer_pass
}
});
var mail_objet = "ADUZUKI - CRON MAJ ANNUAIRE PROF";
mail_html = "<p>Pour l'administrateur de l'application ADUZUKI,</p><br><p>Ceci est le mail d'envoi pour confirmer l'exécution du Webhook de mise à jour de l'annuaire des profs qui se fait la nuit à 4h.</p>";
mail_html += "<p>- nombre de <strong>professeurs à ajouter</strong> : ";
mail_html += attempted;
mail_html += "</p>";
mail_html += "<p>- nombre de <strong>professeurs créés</strong> : ";
mail_html += creates;
mail_html += "</p>";
mail_html += "<p>- nombre de <strong>professeurs non ajoutés</strong> pour cause probable de doublon : ";
mail_html += skipped_existing;
mail_html += "</p>";
mail_html += "<p>- nombre d'<strong>erreurs</strong> : ";
mail_html += errors;
mail_html += "</p>";
mail_html += "<br><p>Julie Chaumard</p><p>Développeuse web de l'agence Parisweb.art</p>";
var mailOptions = {
from: 'afps.inscription@gmail.com',
to: "projet@parisweb.art",
subject: mail_objet,
html: mail_html
};
transporter.sendMail(mailOptions, function(error, info){
if (error) {
logger.info("error" + error, { numsession: sid });
} else {
logger.info("Email envoyé: " + info.response, { numsession: sid });
}
});
logger.info("MAIL MAJ ANNUAIRE PROF ENVOI APRES", { numsession: sid });
// --- Réponse HTTP IMMÉDIATE
const retour = {
ok: true,
attempted,
creates,
skipped_existing,
errors
};
logger.info("sync_prof_strapi terminé", { label: filename, numsession: sid });
res.status(200).json(retour);
} catch (e) {
logger.error("sync_prof_strapi FAILED", {
label: filename,
numsession: sid,
message: e.message,
stack: e.stack
});
// Réponse derreur JSON claire
return res.status(500).json({ ok: false, error: e.message });
}
}
/////////////////////////////////////////////////
// EXPORT DE MODULE POUR QU'IL SOIT LU DANS LES AUTRES FICHIERS JS
/////////////////////////////////////////////////
module.exports = {
runSync_prof_strapi
}