]> git.digitality.be Git - pdw25-26/commitdiff
Fix: règle HOLD + seuil confiance utilisateur (cahier des charges)
authorSteph Ponzo <ponzo.stephane2@gmail.com>
Tue, 24 Feb 2026 17:44:00 +0000 (18:44 +0100)
committerSteph Ponzo <ponzo.stephane2@gmail.com>
Tue, 24 Feb 2026 17:44:00 +0000 (18:44 +0100)
Wallette/server/modules/alerts/adapters/mysql.adapter.js
Wallette/server/modules/alerts/alerts.service.js

index 2f73ec2e441dce49cb738b10cf8bdf12982c93f0..e2bc80256546451e0e8ffb6c357cc21961bfcfbd 100644 (file)
@@ -52,12 +52,23 @@ export function createMySQLAdapter(dbConnection, options = {}) {
         // =====================================================\r
         // 1. RÉCUPÉRER LES RÈGLES D'ALERTE ACTIVES\r
         // =====================================================\r
+        /**\r
+         * Récupère les règles d'alerte pour un signal donné\r
+         *\r
+         * IMPORTANT - Cahier des charges :\r
+         * - Récupère notify_on_hold de users (pour règle HOLD)\r
+         * - Récupère min_confidence_notify de users (fallback si rule.min_confidence est NULL)\r
+         * - Utilise COALESCE pour le seuil de confiance\r
+         */\r
         async findActiveRulesForSignal(userId, pairId) {\r
             try {\r
                 const sql = `\r
                     SELECT\r
                         ar.*,\r
-                        u.email\r
+                        u.email,\r
+                        u.notify_on_hold,\r
+                        u.min_confidence_notify,\r
+                        COALESCE(ar.min_confidence, u.min_confidence_notify, 0.7) AS effective_min_confidence\r
                     FROM ${tables.alerts} ar\r
                     JOIN ${tables.users} u ON ar.user_id = u.user_id\r
                     WHERE ar.enabled = 1\r
@@ -210,7 +221,7 @@ export function createMySQLAdapter(dbConnection, options = {}) {
                     ruleData.enabled !== undefined ? ruleData.enabled : 1,\r
                     ruleData.ruleType || 'SIGNAL_THRESHOLD',\r
                     ruleData.severity || 'INFO',\r
-                    ruleData.minConfidence || 0.7,\r
+                    ruleData.minConfidence || null,  // NULL = utiliser users.min_confidence_notify\r
                     ruleData.channel || 'CONSOLE',\r
                     JSON.stringify(ruleData.params || {}),\r
                     ruleData.cooldownMs || 60000,\r
@@ -341,4 +352,3 @@ export function createMySQLAdapter(dbConnection, options = {}) {
         },\r
     };\r
 }\r
-\r
index 8df13fb64a1328721b1786aac43430ad5c2c5f3b..7921193fc21c9d27fd9f2da5ca675102dd5a7243 100644 (file)
@@ -1,11 +1,8 @@
 // =========================================================\r
-// ALERTS SERVICE - VERSION RÉUTILISABLE\r
+// ALERTS SERVICE\r
 // =========================================================\r
 // RÔLE : Logique métier du système d'alertes\r
 // =========================================================\r
-// PRINCIPE : INJECTION DE DÉPENDANCES\r
-// Au lieu d'importer le Repository directement, on le reçoit en paramètre\r
-// =========================================================\r
 \r
 import { v4 as uuidv4 } from 'uuid';\r
 \r
@@ -49,9 +46,9 @@ export function createAlertsService(alertsRepo, options = {}) {
 \r
     console.log('🔧 Service d\'alertes initialisé');\r
 \r
-    // ─────────────────────────────────────────────────────\r
+    // =========================================================\r
     // RETOURNER L'OBJET SERVICE\r
-    // ─────────────────────────────────────────────────────\r
+    // =========================================================\r
     return {\r
 \r
     // =====================================================\r
@@ -60,35 +57,53 @@ export function createAlertsService(alertsRepo, options = {}) {
     /**\r
      * Détermine si une alerte doit être envoyée selon la règle\r
      *\r
-     * Cette fonction applique les RÈGLES MÉTIER :\r
-     * - Vérification de la confidence minimum\r
+     * Cette fonction applique les RÈGLES MÉTIER du cahier des charges :\r
+     * - HOLD : pas d'alerte sauf si notify_on_hold = true\r
+     * - Vérification de la confidence minimum (rule ou user)\r
      * - Vérification du cooldown (anti-spam)\r
      *\r
-     * @param {Object} rule - Règle d'alerte\r
+     * @param {Object} rule - Règle d'alerte (avec notify_on_hold et effective_min_confidence)\r
      * @param {Object} signal - Signal reçu\r
      * @returns {boolean} true si l'alerte doit être envoyée\r
      */\r
     shouldSendAlert(rule, signal) {\r
-        console.log(`Vérification règle ${rule.rule_id}...`);\r
+        console.log(`   Vérification règle ${rule.rule_id}...`);\r
+\r
+        // =========================================================\r
+        // CRITÈRE 0 : Règle HOLD (NOUVEAU - Cahier des charges)\r
+        // =========================================================\r
+        // Si le signal est HOLD, on n'envoie PAS d'alerte\r
+        // SAUF si l'utilisateur a activé notify_on_hold\r
+\r
+        if (signal.action === 'HOLD') {\r
+            if (!rule.notify_on_hold) {\r
+                console.log(`   ⏸️  Signal HOLD ignoré (notify_on_hold = false)`);\r
+                return false;\r
+            } else {\r
+                console.log(`   ✅ Signal HOLD accepté (notify_on_hold = true)`);\r
+            }\r
+        }\r
 \r
-        // ─────────────────────────────────────────────────\r
+        // =========================================================\r
         // CRITÈRE 1 : Vérifier la confidence minimum\r
-        // ─────────────────────────────────────────────────\r
-        // Si le signal a une confidence trop basse, on n'envoie pas\r
-        // Exemple : rule.min_confidence = 0.80 (80%)\r
-        //           signal.confidence = 0.75 (75%)\r
-        //           → Ne pas envoyer (75% < 80%)\r
-\r
-        if (signal.confidence < rule.min_confidence) {\r
-            console.log(`Confidence trop basse`);\r
-            console.log(`Signal : ${(signal.confidence * 100).toFixed(2)}%`);\r
-            console.log(`Minimum: ${(rule.min_confidence * 100).toFixed(2)}%`);\r
+        // =========================================================\r
+        // Utilise effective_min_confidence qui est :\r
+        // - rule.min_confidence si défini\r
+        // - sinon users.min_confidence_notify si défini\r
+        // - sinon 0.7 par défaut (COALESCE dans la requête SQL)\r
+\r
+        const minConfidence = parseFloat(rule.effective_min_confidence) || parseFloat(rule.min_confidence) || 0.7;\r
+\r
+        if (signal.confidence < minConfidence) {\r
+            console.log(`   ❌ Confidence trop basse`);\r
+            console.log(`      Signal : ${(signal.confidence * 100).toFixed(2)}%`);\r
+            console.log(`      Minimum: ${(minConfidence * 100).toFixed(2)}%`);\r
             return false;\r
         }\r
 \r
-        // ─────────────────────────────────────────────────\r
+        // =========================================================\r
         // CRITÈRE 2 : Vérifier le cooldown (anti-spam)\r
-        // ─────────────────────────────────────────────────\r
+        // =========================================================\r
         // Le cooldown évite d'envoyer 100 emails en 1 minute\r
         // Exemple : cooldown_ms = 60000 (1 minute)\r
         //           Dernière alerte envoyée il y a 30 secondes\r
@@ -111,10 +126,10 @@ export function createAlertsService(alertsRepo, options = {}) {
             }\r
         }\r
 \r
-        // ─────────────────────────────────────────────────\r
+        // =========================================================\r
         // TOUS LES CRITÈRES SONT OK !\r
-        // ─────────────────────────────────────────────────\r
-        console.log(`   Tous les critères sont remplis`);\r
+        // =========================================================\r
+        console.log(`   ✅ Tous les critères sont remplis`);\r
         return true;\r
     },\r
 \r
@@ -134,32 +149,32 @@ export function createAlertsService(alertsRepo, options = {}) {
      * @returns {Promise<string>} 'SENT' ou 'FAILED'\r
      */\r
     async sendViaChannel(rule, signal) {\r
-        console.log(`Envoi via ${rule.channel}...`);\r
+        console.log(`   📤 Envoi via ${rule.channel}...`);\r
 \r
         let status = 'SENT';\r
 \r
         try {\r
-            // ─────────────────────────────────────────────\r
+            // =========================================================\r
             // SWITCH selon le canal configuré\r
-            // ─────────────────────────────────────────────\r
+            // =========================================================\r
             switch (rule.channel) {\r
 \r
-                // ── EMAIL ────────────────────────────────\r
+                //      EMAIL\r
                 case 'EMAIL':\r
                     // Le destinataire est l'email de l'utilisateur\r
                     // On l'a récupéré via le JOIN dans la requête SQL\r
                     await sendAlertEmail(rule.email, signal);\r
                     break;\r
 \r
-                // ── TELEGRAM ─────────────────────────────\r
+                //      TELEGRAM\r
                 case 'TELEGRAM':\r
                     // Le chatId est stocké dans rule.params (JSON)\r
                     // Exemple de params : {"chatId": "123456789"}\r
-                    const params = JSON.parse(rule.params);\r
+                    const params = typeof rule.params === 'string' ? JSON.parse(rule.params) : rule.params;\r
                     const chatId = params.chatId;\r
 \r
                     if (!chatId) {\r
-                        console.error(' Pas de chatId dans params');\r
+                        console.error('   ❌ Pas de chatId dans params');\r
                         status = 'FAILED';\r
                         break;\r
                     }\r
@@ -167,13 +182,13 @@ export function createAlertsService(alertsRepo, options = {}) {
                     await sendTelegramAlert(chatId, signal);\r
                     break;\r
 \r
-                // ── DISCORD ──────────────────────────────\r
+                //      DISCORD\r
                 case 'DISCORD':\r
-                    const discordParams = JSON.parse(rule.params);\r
+                    const discordParams = typeof rule.params === 'string' ? JSON.parse(rule.params) : rule.params;\r
                     const webhookUrl = discordParams.webhookUrl;\r
 \r
                     if (!webhookUrl) {\r
-                        console.error(' Pas de webhookUrl dans params');\r
+                        console.error('   ❌ Pas de webhookUrl dans params');\r
                         status = 'FAILED';\r
                         break;\r
                     }\r
@@ -181,30 +196,30 @@ export function createAlertsService(alertsRepo, options = {}) {
                     await sendDiscordAlert(webhookUrl, signal);\r
                     break;\r
 \r
-                // ── CONSOLE ──────────────────────────────\r
+                //      CONSOLE\r
                 case 'CONSOLE':\r
                     // Pas de paramètres nécessaires\r
                     await sendConsoleAlert(signal);\r
                     break;\r
 \r
-                // ── WEB ──────────────────────────────────\r
+                //      WEB\r
                 case 'WEB':\r
                     // Envoyer à l'interface web de l'utilisateur\r
                     await sendWebAlert(rule.user_id, signal);\r
                     break;\r
 \r
-                // ── CANAL INCONNU ────────────────────────\r
+                //      CANAL INCONNU\r
                 default:\r
-                    console.warn(` Canal inconnu: ${rule.channel}`);\r
+                    console.warn(`   ⚠️ Canal inconnu: ${rule.channel}`);\r
                     status = 'FAILED';\r
             }\r
 \r
         } catch (error) {\r
-            // ─────────────────────────────────────────────\r
+            // =========================================================\r
             // GESTION DES ERREURS\r
-            // ─────────────────────────────────────────────\r
-            console.error(` Erreur lors de l'envoi via ${rule.channel}:`);\r
-            console.error(`   ${error.message}`);\r
+            // =========================================================\r
+            console.error(`   ❌ Erreur lors de l'envoi via ${rule.channel}:`);\r
+            console.error(`      ${error.message}`);\r
             status = 'FAILED';\r
         }\r
 \r
@@ -220,7 +235,7 @@ export function createAlertsService(alertsRepo, options = {}) {
          * WORKFLOW :\r
          * 1. Récupérer les règles actives pour ce signal\r
          * 2. Pour chaque règle :\r
-         *    a. Vérifier si alerte doit être envoyée\r
+         *    a. Vérifier si alerte doit être envoyée (HOLD, confidence, cooldown)\r
          *    b. Envoyer via le bon canal\r
          *    c. Enregistrer dans l'historique\r
          *    d. Mettre à jour le timestamp\r
@@ -235,16 +250,17 @@ export function createAlertsService(alertsRepo, options = {}) {
          */\r
         async processSignal(signal) {\r
             console.log('\n' + '═'.repeat(80));\r
-            console.log(` TRAITEMENT DU SIGNAL`);\r
+            console.log(' TRAITEMENT DU SIGNAL');\r
             console.log(`   Action : ${signal.action}`);\r
             console.log(`   Paire  : ${signal.pair}`);\r
             console.log(`   User   : ${signal.userId}`);\r
+            console.log(`   Conf.  : ${(signal.confidence * 100).toFixed(2)}%`);\r
             console.log('═'.repeat(80) + '\n');\r
 \r
             try {\r
-                // ─────────────────────────────────────────────\r
+                // =========================================================\r
                 // ÉTAPE 1 : Récupérer les règles actives\r
-                // ─────────────────────────────────────────────\r
+                // =========================================================\r
                 // Utilise le repository injecté (pas d'import hardcodé)\r
                 const rules = await alertsRepo.findActiveRulesForSignal(\r
                     signal.userId,\r
@@ -259,17 +275,21 @@ export function createAlertsService(alertsRepo, options = {}) {
 \r
                 console.log(` ${rules.length} règle(s) trouvée(s)\n`);\r
 \r
-                // ─────────────────────────────────────────────\r
+                // =========================================================\r
                 // ÉTAPE 2 : Traiter chaque règle\r
-                // ─────────────────────────────────────────────\r
+                // =========================================================\r
                 let sentCount = 0;\r
+                let skippedCount = 0;\r
 \r
                 for (const rule of rules) {\r
                     console.log(`┌─ Règle: ${rule.rule_type} (${rule.channel})`);\r
+                    console.log(`│  Seuil confiance: ${(parseFloat(rule.effective_min_confidence || rule.min_confidence || 0.7) * 100).toFixed(0)}%`);\r
+                    console.log(`│  notify_on_hold: ${rule.notify_on_hold ? 'oui' : 'non'}`);\r
 \r
                     // Vérifier si on doit envoyer\r
                     if (!this.shouldSendAlert(rule, signal)) {\r
-                        console.log(`└─  Alerte NON envoyée\n`);\r
+                        console.log(`└─ ❌ Alerte NON envoyée\n`);\r
+                        skippedCount++;\r
                         continue;\r
                     }\r
 \r
@@ -298,19 +318,20 @@ export function createAlertsService(alertsRepo, options = {}) {
                     if (status === 'SENT') {\r
                         await alertsRepo.updateLastNotified(rule.rule_id, Date.now());\r
                         sentCount++;\r
-                        console.log(`└─  Alerte envoyée avec succès\n`);\r
+                        console.log(`└─  Alerte envoyée avec succès\n`);\r
                     } else {\r
-                        console.log(`└─  Échec de l'envoi\n`);\r
+                        console.log(`└─  Échec de l'envoi\n`);\r
                     }\r
                 }\r
 \r
-                // ─────────────────────────────────────────────\r
+                // =========================================================\r
                 // RÉSUMÉ FINAL\r
-                // ─────────────────────────────────────────────\r
+                // =========================================================\r
                 console.log('═'.repeat(80));\r
-                console.log(` RÉSUMÉ`);\r
+                console.log(' RÉSUMÉ');\r
                 console.log(`   Règles vérifiées : ${rules.length}`);\r
                 console.log(`   Alertes envoyées : ${sentCount}`);\r
+                console.log(`   Alertes ignorées : ${skippedCount}`);\r
                 console.log('═'.repeat(80) + '\n');\r
 \r
             } catch (error) {\r