// =========================================================\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
\r
console.log('🔧 Service d\'alertes initialisé');\r
\r
- // ─────────────────────────────────────────────────────\r
+ // =========================================================\r
// RETOURNER L'OBJET SERVICE\r
- // ─────────────────────────────────────────────────────\r
+ // =========================================================\r
return {\r
\r
// =====================================================\r
/**\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
}\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
* @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
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
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
* 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
*/\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
\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
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