///////////////////////////////////////////////// // 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 l’adhé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 d’email, 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 d’erreur, 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 = "

Pour l'administrateur de l'application ADUZUKI,


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.

"; mail_html += "

- nombre de professeurs à ajouter : "; mail_html += attempted; mail_html += "

"; mail_html += "

- nombre de professeurs créés : "; mail_html += creates; mail_html += "

"; mail_html += "

- nombre de professeurs non ajoutés pour cause probable de doublon : "; mail_html += skipped_existing; mail_html += "

"; mail_html += "

- nombre d'erreurs : "; mail_html += errors; mail_html += "

"; mail_html += "

Julie Chaumard

Développeuse web de l'agence Parisweb.art

"; 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 d’erreur 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 }