-// =========================================================
-// MYSQL ADAPTER - Couche d'accès MySQL
-// =========================================================
-// RÔLE : Contient TOUT le SQL pour MySQL/MariaDB
-// =========================================================
-// Ce fichier isole le SQL du reste du module.
-// Pour utiliser MongoDB, créer un mongo.adapter.js
-// qui implémente les mêmes méthodes.
-// =========================================================
-
-import { v4 as uuidv4 } from 'uuid';
-
-// =========================================================
-// FACTORY FUNCTION : Créer un adapter MySQL
-// =========================================================
-/**
- * Crée un adapter MySQL pour le module d'alertes
- *
- * @param {Object} dbConnection - Connexion MySQL (mysql2/promise)
- * @param {Object} options - Configuration des noms de tables
- * @param {string} options.alertsTable - Nom de la table des règles (défaut: 'alert_rules')
- * @param {string} options.usersTable - Nom de la table users (défaut: 'users')
- * @param {string} options.eventsTable - Nom de la table events (défaut: 'alert_events')
- *
- * @returns {Object} Adapter avec toutes les méthodes d'accès DB
- *
- * @example
- * import db from './config/db.js';
- * import { createMySQLAdapter } from './adapters/mysql.adapter.js';
- *
- * const adapter = createMySQLAdapter(db);
- * const rules = await adapter.findActiveRulesForSignal('user-123', 1);
- */
-export function createMySQLAdapter(dbConnection, options = {}) {
-
- // =========================================================
- // CONFIGURATION DES NOMS DE TABLES
- // =========================================================
- const tables = {
- alerts: options.alertsTable || 'alert_rules',
- users: options.usersTable || 'users',
- events: options.eventsTable || 'alert_events'
- };
-
- console.log(`🔌 Adapter MySQL configuré avec tables: ${JSON.stringify(tables)}`);
-
- // =========================================================
- // RETOURNER L'OBJET ADAPTER
- // =========================================================
- return {
-
- // =====================================================
- // 1. RÉCUPÉRER LES RÈGLES D'ALERTE ACTIVES
- // =====================================================
- async findActiveRulesForSignal(userId, pairId) {
- try {
- const sql = `
- SELECT
- ar.*,
- u.email
- FROM ${tables.alerts} ar
- JOIN ${tables.users} u ON ar.user_id = u.user_id
- WHERE ar.enabled = 1
- AND ar.user_id = ?
- AND (ar.pair_id = ? OR ar.pair_id IS NULL)
- ORDER BY ar.severity DESC
- `;
-
- const [rows] = await dbConnection.execute(sql, [userId, pairId]);
-
- console.log(`[MySQL] Règles trouvées pour user ${userId} : ${rows.length}`);
- return rows;
-
- } catch (error) {
- console.error('[MySQL] Erreur dans findActiveRulesForSignal:', error);
- throw error;
- }
- },
-
- // =====================================================
- // 2. SAUVEGARDER UN ÉVÉNEMENT D'ALERTE
- // =====================================================
- async saveAlertEvent(eventData) {
- try {
- const sql = `
- INSERT INTO ${tables.events} (
- alert_event_id,
- rule_id,
- timestamp_ms,
- severity,
- channel,
- send_status,
- title,
- message,
- signal_action,
- confidence,
- correlation_id,
- created_at_ms
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- `;
-
- const values = [
- eventData.alert_event_id || uuidv4(),
- eventData.rule_id,
- eventData.timestamp_ms || Date.now(),
- eventData.severity,
- eventData.channel,
- eventData.send_status,
- eventData.title,
- eventData.message,
- eventData.signal_action,
- eventData.confidence,
- eventData.correlation_id || null,
- Date.now()
- ];
-
- await dbConnection.execute(sql, values);
- console.log(`[MySQL] Événement d'alerte sauvegardé : ${eventData.title}`);
-
- } catch (error) {
- console.error('[MySQL] Erreur dans saveAlertEvent:', error);
- throw error;
- }
- },
-
- // =====================================================
- // 3. METTRE À JOUR LA DERNIÈRE NOTIFICATION
- // =====================================================
- async updateLastNotified(ruleId, timestamp) {
- try {
- const sql = `
- UPDATE ${tables.alerts}
- SET last_notified_at_ms = ?,
- updated_at_ms = ?
- WHERE rule_id = ?
- `;
-
- await dbConnection.execute(sql, [timestamp, Date.now(), ruleId]);
- console.log(`[MySQL] Timestamp mis à jour pour rule ${ruleId}`);
-
- } catch (error) {
- console.error('[MySQL] Erreur dans updateLastNotified:', error);
- throw error;
- }
- },
-
- // =====================================================
- // 4. RÉCUPÉRER L'HISTORIQUE DES ALERTES
- // =====================================================
- async getAlertHistory(userId, limit = 50) {
- try {
- const sql = `
- SELECT
- ae.*,
- ar.channel,
- ar.rule_type
- FROM ${tables.events} ae
- JOIN ${tables.alerts} ar ON ae.rule_id = ar.rule_id
- WHERE ar.user_id = ?
- ORDER BY ae.timestamp_ms DESC
- LIMIT ?
- `;
-
- const [rows] = await dbConnection.execute(sql, [userId, limit]);
- return rows;
-
- } catch (error) {
- console.error('[MySQL] Erreur dans getAlertHistory:', error);
- throw error;
- }
- },
-
- // =====================================================
- // 5. CRÉER UNE RÈGLE D'ALERTE (CRUD - Create)
- // =====================================================
- /**
- * Crée une nouvelle règle d'alerte
- * @param {Object} ruleData - Données de la règle
- * @returns {Promise<Object>} Règle créée avec son ID
- */
- async createRule(ruleData) {
- try {
- const ruleId = ruleData.ruleId || uuidv4();
- const now = Date.now();
-
- const sql = `
- INSERT INTO ${tables.alerts} (
- rule_id,
- user_id,
- pair_id,
- enabled,
- rule_type,
- severity,
- min_confidence,
- channel,
- params,
- cooldown_ms,
- created_at_ms,
- updated_at_ms
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- `;
-
- const values = [
- ruleId,
- ruleData.userId,
- ruleData.pairId || null,
- ruleData.enabled !== undefined ? ruleData.enabled : 1,
- ruleData.ruleType || 'SIGNAL_THRESHOLD',
- ruleData.severity || 'INFO',
- ruleData.minConfidence || 0.7,
- ruleData.channel || 'CONSOLE',
- JSON.stringify(ruleData.params || {}),
- ruleData.cooldownMs || 60000,
- now,
- now
- ];
-
- await dbConnection.execute(sql, values);
- console.log(`[MySQL] Règle créée : ${ruleId}`);
-
- return { ruleId, ...ruleData, createdAt: now };
-
- } catch (error) {
- console.error('[MySQL] Erreur dans createRule:', error);
- throw error;
- }
- },
-
- // =====================================================
- // 6. MODIFIER UNE RÈGLE D'ALERTE (CRUD - Update)
- // =====================================================
- /**
- * Met à jour une règle existante
- * @param {string} ruleId - ID de la règle
- * @param {Object} updates - Champs à mettre à jour
- * @returns {Promise<boolean>} true si modifié
- */
- async updateRule(ruleId, updates) {
- try {
- // Construire la requête dynamiquement
- const fields = [];
- const values = [];
-
- if (updates.minConfidence !== undefined) {
- fields.push('min_confidence = ?');
- values.push(updates.minConfidence);
- }
- if (updates.enabled !== undefined) {
- fields.push('enabled = ?');
- values.push(updates.enabled);
- }
- if (updates.channel !== undefined) {
- fields.push('channel = ?');
- values.push(updates.channel);
- }
- if (updates.severity !== undefined) {
- fields.push('severity = ?');
- values.push(updates.severity);
- }
- if (updates.cooldownMs !== undefined) {
- fields.push('cooldown_ms = ?');
- values.push(updates.cooldownMs);
- }
- if (updates.pairId !== undefined) {
- fields.push('pair_id = ?');
- values.push(updates.pairId);
- }
-
- // Toujours mettre à jour le timestamp
- fields.push('updated_at_ms = ?');
- values.push(Date.now());
-
- // Ajouter le ruleId à la fin pour le WHERE
- values.push(ruleId);
-
- const sql = `
- UPDATE ${tables.alerts}
- SET ${fields.join(', ')}
- WHERE rule_id = ?
- `;
-
- const [result] = await dbConnection.execute(sql, values);
- console.log(`[MySQL] Règle ${ruleId} mise à jour`);
-
- return result.affectedRows > 0;
-
- } catch (error) {
- console.error('[MySQL] Erreur dans updateRule:', error);
- throw error;
- }
- },
-
- // =====================================================
- // 7. SUPPRIMER UNE RÈGLE D'ALERTE (CRUD - Delete)
- // =====================================================
- /**
- * Supprime une règle
- * @param {string} ruleId - ID de la règle
- * @returns {Promise<boolean>} true si supprimé
- */
- async deleteRule(ruleId) {
- try {
- const sql = `DELETE FROM ${tables.alerts} WHERE rule_id = ?`;
-
- const [result] = await dbConnection.execute(sql, [ruleId]);
- console.log(`[MySQL] Règle ${ruleId} supprimée`);
-
- return result.affectedRows > 0;
-
- } catch (error) {
- console.error('[MySQL] Erreur dans deleteRule:', error);
- throw error;
- }
- },
-
- // =====================================================
- // 8. RÉCUPÉRER UNE RÈGLE PAR ID (CRUD - Read)
- // =====================================================
- /**
- * Récupère une règle par son ID
- * @param {string} ruleId - ID de la règle
- * @returns {Promise<Object|null>} La règle ou null
- */
- async findRuleById(ruleId) {
- try {
- const sql = `
- SELECT * FROM ${tables.alerts}
- WHERE rule_id = ?
- `;
-
- const [rows] = await dbConnection.execute(sql, [ruleId]);
- return rows.length > 0 ? rows[0] : null;
-
- } catch (error) {
- console.error('[MySQL] Erreur dans findRuleById:', error);
- throw error;
- }
- },
- };
-}
-
+// =========================================================\r
+// MYSQL ADAPTER - Couche d'accès MySQL\r
+// =========================================================\r
+// RÔLE : Contient TOUT le SQL pour MySQL/MariaDB\r
+// =========================================================\r
+// Ce fichier isole le SQL du reste du module.\r
+// Pour utiliser MongoDB, créer un mongo.adapter.js\r
+// qui implémente les mêmes méthodes.\r
+// =========================================================\r
+\r
+import { v4 as uuidv4 } from 'uuid';\r
+\r
+// =========================================================\r
+// FACTORY FUNCTION : Créer un adapter MySQL\r
+// =========================================================\r
+/**\r
+ * Crée un adapter MySQL pour le module d'alertes\r
+ *\r
+ * @param {Object} dbConnection - Connexion MySQL (mysql2/promise)\r
+ * @param {Object} options - Configuration des noms de tables\r
+ * @param {string} options.alertsTable - Nom de la table des règles (défaut: 'alert_rules')\r
+ * @param {string} options.usersTable - Nom de la table users (défaut: 'users')\r
+ * @param {string} options.eventsTable - Nom de la table events (défaut: 'alert_events')\r
+ *\r
+ * @returns {Object} Adapter avec toutes les méthodes d'accès DB\r
+ *\r
+ * @example\r
+ * import db from './config/db.js';\r
+ * import { createMySQLAdapter } from './adapters/mysql.adapter.js';\r
+ *\r
+ * const adapter = createMySQLAdapter(db);\r
+ * const rules = await adapter.findActiveRulesForSignal('user-123', 1);\r
+ */\r
+export function createMySQLAdapter(dbConnection, options = {}) {\r
+\r
+ // =========================================================\r
+ // CONFIGURATION DES NOMS DE TABLES\r
+ // =========================================================\r
+ const tables = {\r
+ alerts: options.alertsTable || 'alert_rules',\r
+ users: options.usersTable || 'users',\r
+ events: options.eventsTable || 'alert_events'\r
+ };\r
+\r
+ console.log(`🔌 Adapter MySQL configuré avec tables: ${JSON.stringify(tables)}`);\r
+\r
+ // =========================================================\r
+ // RETOURNER L'OBJET ADAPTER\r
+ // =========================================================\r
+ return {\r
+\r
+ // =====================================================\r
+ // 1. RÉCUPÉRER LES RÈGLES D'ALERTE ACTIVES\r
+ // =====================================================\r
+ async findActiveRulesForSignal(userId, pairId) {\r
+ try {\r
+ const sql = `\r
+ SELECT\r
+ ar.*,\r
+ u.email\r
+ FROM ${tables.alerts} ar\r
+ JOIN ${tables.users} u ON ar.user_id = u.user_id\r
+ WHERE ar.enabled = 1\r
+ AND ar.user_id = ?\r
+ AND (ar.pair_id = ? OR ar.pair_id IS NULL)\r
+ ORDER BY ar.severity DESC\r
+ `;\r
+\r
+ const [rows] = await dbConnection.execute(sql, [userId, pairId]);\r
+\r
+ console.log(`[MySQL] Règles trouvées pour user ${userId} : ${rows.length}`);\r
+ return rows;\r
+\r
+ } catch (error) {\r
+ console.error('[MySQL] Erreur dans findActiveRulesForSignal:', error);\r
+ throw error;\r
+ }\r
+ },\r
+\r
+ // =====================================================\r
+ // 2. SAUVEGARDER UN ÉVÉNEMENT D'ALERTE\r
+ // =====================================================\r
+ async saveAlertEvent(eventData) {\r
+ try {\r
+ const sql = `\r
+ INSERT INTO ${tables.events} (\r
+ alert_event_id,\r
+ rule_id,\r
+ timestamp_ms,\r
+ severity,\r
+ channel,\r
+ send_status,\r
+ title,\r
+ message,\r
+ signal_action,\r
+ confidence,\r
+ correlation_id,\r
+ created_at_ms\r
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\r
+ `;\r
+\r
+ const values = [\r
+ eventData.alert_event_id || uuidv4(),\r
+ eventData.rule_id,\r
+ eventData.timestamp_ms || Date.now(),\r
+ eventData.severity,\r
+ eventData.channel,\r
+ eventData.send_status,\r
+ eventData.title,\r
+ eventData.message,\r
+ eventData.signal_action,\r
+ eventData.confidence,\r
+ eventData.correlation_id || null,\r
+ Date.now()\r
+ ];\r
+\r
+ await dbConnection.execute(sql, values);\r
+ console.log(`[MySQL] Événement d'alerte sauvegardé : ${eventData.title}`);\r
+\r
+ } catch (error) {\r
+ console.error('[MySQL] Erreur dans saveAlertEvent:', error);\r
+ throw error;\r
+ }\r
+ },\r
+\r
+ // =====================================================\r
+ // 3. METTRE À JOUR LA DERNIÈRE NOTIFICATION\r
+ // =====================================================\r
+ async updateLastNotified(ruleId, timestamp) {\r
+ try {\r
+ const sql = `\r
+ UPDATE ${tables.alerts}\r
+ SET last_notified_at_ms = ?,\r
+ updated_at_ms = ?\r
+ WHERE rule_id = ?\r
+ `;\r
+\r
+ await dbConnection.execute(sql, [timestamp, Date.now(), ruleId]);\r
+ console.log(`[MySQL] Timestamp mis à jour pour rule ${ruleId}`);\r
+\r
+ } catch (error) {\r
+ console.error('[MySQL] Erreur dans updateLastNotified:', error);\r
+ throw error;\r
+ }\r
+ },\r
+\r
+ // =====================================================\r
+ // 4. RÉCUPÉRER L'HISTORIQUE DES ALERTES\r
+ // =====================================================\r
+ async getAlertHistory(userId, limit = 50) {\r
+ try {\r
+ const sql = `\r
+ SELECT\r
+ ae.*,\r
+ ar.channel,\r
+ ar.rule_type\r
+ FROM ${tables.events} ae\r
+ JOIN ${tables.alerts} ar ON ae.rule_id = ar.rule_id\r
+ WHERE ar.user_id = ?\r
+ ORDER BY ae.timestamp_ms DESC\r
+ LIMIT ?\r
+ `;\r
+\r
+ const [rows] = await dbConnection.execute(sql, [userId, limit]);\r
+ return rows;\r
+\r
+ } catch (error) {\r
+ console.error('[MySQL] Erreur dans getAlertHistory:', error);\r
+ throw error;\r
+ }\r
+ },\r
+\r
+ // =====================================================\r
+ // 5. CRÉER UNE RÈGLE D'ALERTE (CRUD - Create)\r
+ // =====================================================\r
+ /**\r
+ * Crée une nouvelle règle d'alerte\r
+ * @param {Object} ruleData - Données de la règle\r
+ * @returns {Promise<Object>} Règle créée avec son ID\r
+ */\r
+ async createRule(ruleData) {\r
+ try {\r
+ const ruleId = ruleData.ruleId || uuidv4();\r
+ const now = Date.now();\r
+\r
+ const sql = `\r
+ INSERT INTO ${tables.alerts} (\r
+ rule_id,\r
+ user_id,\r
+ pair_id,\r
+ enabled,\r
+ rule_type,\r
+ severity,\r
+ min_confidence,\r
+ channel,\r
+ params,\r
+ cooldown_ms,\r
+ created_at_ms,\r
+ updated_at_ms\r
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\r
+ `;\r
+\r
+ const values = [\r
+ ruleId,\r
+ ruleData.userId,\r
+ ruleData.pairId || null,\r
+ ruleData.enabled !== undefined ? ruleData.enabled : 1,\r
+ ruleData.ruleType || 'SIGNAL_THRESHOLD',\r
+ ruleData.severity || 'INFO',\r
+ ruleData.minConfidence || 0.7,\r
+ ruleData.channel || 'CONSOLE',\r
+ JSON.stringify(ruleData.params || {}),\r
+ ruleData.cooldownMs || 60000,\r
+ now,\r
+ now\r
+ ];\r
+\r
+ await dbConnection.execute(sql, values);\r
+ console.log(`[MySQL] Règle créée : ${ruleId}`);\r
+\r
+ return { ruleId, ...ruleData, createdAt: now };\r
+\r
+ } catch (error) {\r
+ console.error('[MySQL] Erreur dans createRule:', error);\r
+ throw error;\r
+ }\r
+ },\r
+\r
+ // =====================================================\r
+ // 6. MODIFIER UNE RÈGLE D'ALERTE (CRUD - Update)\r
+ // =====================================================\r
+ /**\r
+ * Met à jour une règle existante\r
+ * @param {string} ruleId - ID de la règle\r
+ * @param {Object} updates - Champs à mettre à jour\r
+ * @returns {Promise<boolean>} true si modifié\r
+ */\r
+ async updateRule(ruleId, updates) {\r
+ try {\r
+ // Construire la requête dynamiquement\r
+ const fields = [];\r
+ const values = [];\r
+\r
+ if (updates.minConfidence !== undefined) {\r
+ fields.push('min_confidence = ?');\r
+ values.push(updates.minConfidence);\r
+ }\r
+ if (updates.enabled !== undefined) {\r
+ fields.push('enabled = ?');\r
+ values.push(updates.enabled);\r
+ }\r
+ if (updates.channel !== undefined) {\r
+ fields.push('channel = ?');\r
+ values.push(updates.channel);\r
+ }\r
+ if (updates.severity !== undefined) {\r
+ fields.push('severity = ?');\r
+ values.push(updates.severity);\r
+ }\r
+ if (updates.cooldownMs !== undefined) {\r
+ fields.push('cooldown_ms = ?');\r
+ values.push(updates.cooldownMs);\r
+ }\r
+ if (updates.pairId !== undefined) {\r
+ fields.push('pair_id = ?');\r
+ values.push(updates.pairId);\r
+ }\r
+\r
+ // Toujours mettre à jour le timestamp\r
+ fields.push('updated_at_ms = ?');\r
+ values.push(Date.now());\r
+\r
+ // Ajouter le ruleId à la fin pour le WHERE\r
+ values.push(ruleId);\r
+\r
+ const sql = `\r
+ UPDATE ${tables.alerts}\r
+ SET ${fields.join(', ')}\r
+ WHERE rule_id = ?\r
+ `;\r
+\r
+ const [result] = await dbConnection.execute(sql, values);\r
+ console.log(`[MySQL] Règle ${ruleId} mise à jour`);\r
+\r
+ return result.affectedRows > 0;\r
+\r
+ } catch (error) {\r
+ console.error('[MySQL] Erreur dans updateRule:', error);\r
+ throw error;\r
+ }\r
+ },\r
+\r
+ // =====================================================\r
+ // 7. SUPPRIMER UNE RÈGLE D'ALERTE (CRUD - Delete)\r
+ // =====================================================\r
+ /**\r
+ * Supprime une règle\r
+ * @param {string} ruleId - ID de la règle\r
+ * @returns {Promise<boolean>} true si supprimé\r
+ */\r
+ async deleteRule(ruleId) {\r
+ try {\r
+ const sql = `DELETE FROM ${tables.alerts} WHERE rule_id = ?`;\r
+\r
+ const [result] = await dbConnection.execute(sql, [ruleId]);\r
+ console.log(`[MySQL] Règle ${ruleId} supprimée`);\r
+\r
+ return result.affectedRows > 0;\r
+\r
+ } catch (error) {\r
+ console.error('[MySQL] Erreur dans deleteRule:', error);\r
+ throw error;\r
+ }\r
+ },\r
+\r
+ // =====================================================\r
+ // 8. RÉCUPÉRER UNE RÈGLE PAR ID (CRUD - Read)\r
+ // =====================================================\r
+ /**\r
+ * Récupère une règle par son ID\r
+ * @param {string} ruleId - ID de la règle\r
+ * @returns {Promise<Object|null>} La règle ou null\r
+ */\r
+ async findRuleById(ruleId) {\r
+ try {\r
+ const sql = `\r
+ SELECT * FROM ${tables.alerts}\r
+ WHERE rule_id = ?\r
+ `;\r
+\r
+ const [rows] = await dbConnection.execute(sql, [ruleId]);\r
+ return rows.length > 0 ? rows[0] : null;\r
+\r
+ } catch (error) {\r
+ console.error('[MySQL] Erreur dans findRuleById:', error);\r
+ throw error;\r
+ }\r
+ },\r
+ };\r
+}\r
+\r
-// =========================================================
-// ALERTS CONTROLLER - VERSION RÉUTILISABLE
-// =========================================================
-// RÔLE : Gestion des requêtes et réponses HTTP
-// =========================================================
-
-
-/**
- * Crée un controller d'alertes réutilisable
- *
- * @param {Object} alertsService - Service d'alertes (créé par createAlertsService)
- * @param {Object} alertsRepo - Repository d'alertes (pour certaines méthodes)
- *
- * @returns {Object} Controller avec toutes les méthodes
- */
-export function createAlertsController(alertsService, alertsRepo) {
-
- console.log("Controller d'alertes initialisé");
-
- return {
-
- // =====================================================
- // POST /api/alerts/process-signal
- // =====================================================
- /**
- * Endpoint pour traiter un signal crypto
- *
- * Appelé par le module Strategy (Sacha) quand il
- * génère un signal BUY/SELL/HOLD
- *
- * Body attendu :
- * {
- * "userId": "uuid",
- * "pairId": 1,
- * "pair": "BTC/EUR",
- * "action": "BUY",
- * "confidence": 0.87,
- * "criticality": "WARNING",
- * "reason": "Indicators show...",
- * "priceAtSignal": 45000.50
- * }
- */
- async processSignal(req, res) {
- try {
- // ==================================================================================
- // 1. RÉCUPÉRER LES DONNÉES DU BODY
- // ==================================================================================
- const signal = req.body;
-
- // ==================================================================================
- // 2. VALIDATION DES DONNÉES
- // ==================================================================================
- // Vérifier que tous les champs obligatoires sont présents
- const requiredFields = ['userId', 'pairId', 'pair', 'action', 'confidence', 'criticality'];
- /**
- * on remplit le tableau des champs manquant ou faux du contenu de signal
- */
- const missingFields = requiredFields.filter(field => !signal[field]);
- // dans ce cas on retourne une erreur et le noms des champs en cause
- if (missingFields.length > 0) {
- return res.status(400).json({
- success: false,
- error: 'Champs manquants',
- missingFields: missingFields
- });
- }
-
- // Valider le format de action
- const validActions = ['BUY', 'SELL', 'HOLD', 'STOP_LOSS'];
- if (!validActions.includes(signal.action)) {
- return res.status(400).json({
- success: false,
- error: `Action invalide. Doit être: ${validActions.join(', ')}`
- });
- }
-
- // Valider le format de criticality
- const validCriticalities = ['CRITICAL', 'WARNING', 'INFO'];
- if (!validCriticalities.includes(signal.criticality)) {
- return res.status(400).json({
- success: false,
- error: `Criticality invalide. Doit être: ${validCriticalities.join(', ')}`
- });
- }
-
- // Valider la confidence (doit être entre 0 et 1)
- if (signal.confidence < 0 || signal.confidence > 1) {
- return res.status(400).json({
- success: false,
- error: 'Confidence doit être entre 0 et 1'
- });
- }
-
- // =================================================================================
- // 3. APPELER LE SERVICE (injecté)
- // =================================================================================
- // Le service contient toute la logique
- await alertsService.processSignal(signal);
-
- // =================================================================================
- // 4. RÉPONDRE AU CLIENT
- // =================================================================================
- res.status(200).json({
- success: true,
- message: 'Signal traité avec succès',
- signal: {
- action: signal.action,
- pair: signal.pair,
- confidence: signal.confidence
- }
- });
-
- } catch (error) {
- // =================================================================================
- // GESTION DES ERREURS
- // =================================================================================
- console.error(' Erreur dans processSignal controller:', error);
-
- res.status(500).json({
- success: false,
- error: 'Erreur serveur lors du traitement du signal',
- details: process.env.NODE_ENV === 'development' ? error.message : undefined
- });
- }
- },
-
- // =====================================================
- // GET /api/alerts/rules/:userId
- // =====================================================
- /**
- * Récupère toutes les règles d'alerte d'un utilisateur
- *
- * Utile pour afficher la configuration dans l'interface
- */
- async getRules(req, res) {
- try {
- const { userId } = req.params;
-
- // Validation
- if (!userId) {
- return res.status(400).json({
- success: false,
- error: 'userId manquant'
- });
- }
-
- // Récupérer les règles (via repo injecté)
- const rules = await alertsRepo.findActiveRulesForSignal(userId, null);
-
- res.status(200).json({
- success: true,
- count: rules.length,
- rules: rules
- });
-
- } catch (error) {
- console.error('Erreur dans getRules controller:', error);
-
- res.status(500).json({
- success: false,
- error: 'Erreur serveur'
- });
- }
- },
-
- // =====================================================
- // GET /api/alerts/history/:userId
- // =====================================================
- /**
- * Récupère l'historique des alertes d'un utilisateur
- */
- async getHistory(req, res) {
- try {
- const { userId } = req.params;
- const limit = parseInt(req.query.limit) || 50;
-
- if (!userId) {
- return res.status(400).json({
- success: false,
- error: 'userId manquant'
- });
- }
-
- // Récupérer l'historique (via repo injecté)
- const history = await alertsRepo.getAlertHistory(userId, limit);
-
- res.status(200).json({
- success: true,
- count: history.length,
- history: history
- });
-
- } catch (error) {
- console.error('Erreur dans getHistory controller:', error);
-
- res.status(500).json({
- success: false,
- error: 'Erreur serveur'
- });
- }
- },
-
- // =========================================================================================
- // CRUD
- //==========================================================================================
-
-
- // =====================================================
- // POST /api/alerts/rules - CRÉER UNE RÈGLE
- // =====================================================
- /**
- * Crée une nouvelle règle d'alerte
- *
- * Body attendu :
- * {
- * "userId": "uuid",
- * "pairId": 1,
- * "channel": "EMAIL",
- * "minConfidence": 0.8,
- * "severity": "WARNING",
- * "cooldownMs": 60000
- * }
- */
- async createRule(req, res) {
- try {
- const ruleData = req.body;
-
- // Validation des champs obligatoires
- if (!ruleData.userId || !ruleData.channel) {
- return res.status(400).json({
- success: false,
- error: 'Champs manquants (userId, channel requis)'
- });
- }
-
- // Appeler le repo (qui appelle l'adapter)
- const newRule = await alertsRepo.createRule(ruleData);
-
- res.status(201).json({
- success: true,
- rule: newRule,
- message: 'Règle créée avec succès'
- });
-
- } catch (error) {
- console.error('❌ Erreur createRule:', error);
- res.status(500).json({
- success: false,
- error: error.message
- });
- }
- },
-
- // =====================================================
- // PUT /api/alerts/rules/:id - MODIFIER UNE RÈGLE
- // =====================================================
- /**
- * Met à jour une règle existante
- *
- * Body possible :
- * {
- * "minConfidence": 0.9,
- * "enabled": true,
- * "channel": "CONSOLE",
- * "severity": "CRITICAL"
- * }
- */
- async updateRule(req, res) {
- try {
- const { id } = req.params;
- const updates = req.body;
-
- // Vérifier qu'il y a quelque chose à mettre à jour
- if (Object.keys(updates).length === 0) {
- return res.status(400).json({
- success: false,
- error: 'Aucune donnée à mettre à jour'
- });
- }
-
- // Appeler le repo (qui appelle l'adapter)
- const updated = await alertsRepo.updateRule(id, updates);
-
- if (!updated) {
- return res.status(404).json({
- success: false,
- error: 'Règle non trouvée'
- });
- }
-
- res.json({
- success: true,
- message: 'Règle mise à jour'
- });
-
- } catch (error) {
- console.error('❌ Erreur updateRule:', error);
- res.status(500).json({
- success: false,
- error: error.message
- });
- }
- },
-
- // =====================================================
- // DELETE /api/alerts/rules/:id - SUPPRIMER UNE RÈGLE
- // =====================================================
- async deleteRule(req, res) {
- try {
- const { id } = req.params;
-
- // Appeler le repo (qui appelle l'adapter)
- const deleted = await alertsRepo.deleteRule(id);
-
- if (!deleted) {
- return res.status(404).json({
- success: false,
- error: 'Règle non trouvée'
- });
- }
-
- res.json({
- success: true,
- message: 'Règle supprimée'
- });
-
- } catch (error) {
- console.error('❌ Erreur deleteRule:', error);
- res.status(500).json({
- success: false,
- error: error.message
- });
- }
- },
-
- // =====================================================
- // GET /api/alerts/rules/detail/:id - DÉTAIL D'UNE RÈGLE
- // =====================================================
- async getRuleById(req, res) {
- try {
- const { id } = req.params;
-
- const rule = await alertsRepo.findRuleById(id);
-
- if (!rule) {
- return res.status(404).json({
- success: false,
- error: 'Règle non trouvée'
- });
- }
-
- res.json({
- success: true,
- rule
- });
-
- } catch (error) {
- console.error('❌ Erreur getRuleById:', error);
- res.status(500).json({
- success: false,
- error: error.message
- });
- }
- }
- };
-}
-
+// =========================================================\r
+// ALERTS CONTROLLER - VERSION RÉUTILISABLE\r
+// =========================================================\r
+// RÔLE : Gestion des requêtes et réponses HTTP\r
+// =========================================================\r
+\r
+\r
+/**\r
+ * Crée un controller d'alertes réutilisable\r
+ *\r
+ * @param {Object} alertsService - Service d'alertes (créé par createAlertsService)\r
+ * @param {Object} alertsRepo - Repository d'alertes (pour certaines méthodes)\r
+ *\r
+ * @returns {Object} Controller avec toutes les méthodes\r
+ */\r
+export function createAlertsController(alertsService, alertsRepo) {\r
+\r
+ console.log("Controller d'alertes initialisé");\r
+\r
+ return {\r
+\r
+ // =====================================================\r
+ // POST /api/alerts/process-signal\r
+ // =====================================================\r
+ /**\r
+ * Endpoint pour traiter un signal crypto\r
+ *\r
+ * Appelé par le module Strategy (Sacha) quand il\r
+ * génère un signal BUY/SELL/HOLD\r
+ *\r
+ * Body attendu :\r
+ * {\r
+ * "userId": "uuid",\r
+ * "pairId": 1,\r
+ * "pair": "BTC/EUR",\r
+ * "action": "BUY",\r
+ * "confidence": 0.87,\r
+ * "criticality": "WARNING",\r
+ * "reason": "Indicators show...",\r
+ * "priceAtSignal": 45000.50\r
+ * }\r
+ */\r
+ async processSignal(req, res) {\r
+ try {\r
+ // ==================================================================================\r
+ // 1. RÉCUPÉRER LES DONNÉES DU BODY\r
+ // ==================================================================================\r
+ const signal = req.body;\r
+\r
+ // ==================================================================================\r
+ // 2. VALIDATION DES DONNÉES\r
+ // ==================================================================================\r
+ // Vérifier que tous les champs obligatoires sont présents\r
+ const requiredFields = ['userId', 'pairId', 'pair', 'action', 'confidence', 'criticality'];\r
+ /**\r
+ * on remplit le tableau des champs manquant ou faux du contenu de signal\r
+ */\r
+ const missingFields = requiredFields.filter(field => !signal[field]);\r
+ // dans ce cas on retourne une erreur et le noms des champs en cause\r
+ if (missingFields.length > 0) {\r
+ return res.status(400).json({\r
+ success: false,\r
+ error: 'Champs manquants',\r
+ missingFields: missingFields\r
+ });\r
+ }\r
+\r
+ // Valider le format de action\r
+ const validActions = ['BUY', 'SELL', 'HOLD', 'STOP_LOSS'];\r
+ if (!validActions.includes(signal.action)) {\r
+ return res.status(400).json({\r
+ success: false,\r
+ error: `Action invalide. Doit être: ${validActions.join(', ')}`\r
+ });\r
+ }\r
+\r
+ // Valider le format de criticality\r
+ const validCriticalities = ['CRITICAL', 'WARNING', 'INFO'];\r
+ if (!validCriticalities.includes(signal.criticality)) {\r
+ return res.status(400).json({\r
+ success: false,\r
+ error: `Criticality invalide. Doit être: ${validCriticalities.join(', ')}`\r
+ });\r
+ }\r
+\r
+ // Valider la confidence (doit être entre 0 et 1)\r
+ if (signal.confidence < 0 || signal.confidence > 1) {\r
+ return res.status(400).json({\r
+ success: false,\r
+ error: 'Confidence doit être entre 0 et 1'\r
+ });\r
+ }\r
+\r
+ // =================================================================================\r
+ // 3. APPELER LE SERVICE (injecté)\r
+ // =================================================================================\r
+ // Le service contient toute la logique\r
+ await alertsService.processSignal(signal);\r
+\r
+ // =================================================================================\r
+ // 4. RÉPONDRE AU CLIENT\r
+ // =================================================================================\r
+ res.status(200).json({\r
+ success: true,\r
+ message: 'Signal traité avec succès',\r
+ signal: {\r
+ action: signal.action,\r
+ pair: signal.pair,\r
+ confidence: signal.confidence\r
+ }\r
+ });\r
+\r
+ } catch (error) {\r
+ // =================================================================================\r
+ // GESTION DES ERREURS\r
+ // =================================================================================\r
+ console.error(' Erreur dans processSignal controller:', error);\r
+\r
+ res.status(500).json({\r
+ success: false,\r
+ error: 'Erreur serveur lors du traitement du signal',\r
+ details: process.env.NODE_ENV === 'development' ? error.message : undefined\r
+ });\r
+ }\r
+ },\r
+\r
+ // =====================================================\r
+ // GET /api/alerts/rules/:userId\r
+ // =====================================================\r
+ /**\r
+ * Récupère toutes les règles d'alerte d'un utilisateur\r
+ *\r
+ * Utile pour afficher la configuration dans l'interface\r
+ */\r
+ async getRules(req, res) {\r
+ try {\r
+ const { userId } = req.params;\r
+\r
+ // Validation\r
+ if (!userId) {\r
+ return res.status(400).json({\r
+ success: false,\r
+ error: 'userId manquant'\r
+ });\r
+ }\r
+\r
+ // Récupérer les règles (via repo injecté)\r
+ const rules = await alertsRepo.findActiveRulesForSignal(userId, null);\r
+\r
+ res.status(200).json({\r
+ success: true,\r
+ count: rules.length,\r
+ rules: rules\r
+ });\r
+\r
+ } catch (error) {\r
+ console.error('Erreur dans getRules controller:', error);\r
+\r
+ res.status(500).json({\r
+ success: false,\r
+ error: 'Erreur serveur'\r
+ });\r
+ }\r
+ },\r
+\r
+ // =====================================================\r
+ // GET /api/alerts/history/:userId\r
+ // =====================================================\r
+ /**\r
+ * Récupère l'historique des alertes d'un utilisateur\r
+ */\r
+ async getHistory(req, res) {\r
+ try {\r
+ const { userId } = req.params;\r
+ const limit = parseInt(req.query.limit) || 50;\r
+\r
+ if (!userId) {\r
+ return res.status(400).json({\r
+ success: false,\r
+ error: 'userId manquant'\r
+ });\r
+ }\r
+\r
+ // Récupérer l'historique (via repo injecté)\r
+ const history = await alertsRepo.getAlertHistory(userId, limit);\r
+\r
+ res.status(200).json({\r
+ success: true,\r
+ count: history.length,\r
+ history: history\r
+ });\r
+\r
+ } catch (error) {\r
+ console.error('Erreur dans getHistory controller:', error);\r
+\r
+ res.status(500).json({\r
+ success: false,\r
+ error: 'Erreur serveur'\r
+ });\r
+ }\r
+ },\r
+\r
+ // =========================================================================================\r
+ // CRUD\r
+ //==========================================================================================\r
+\r
+\r
+ // =====================================================\r
+ // POST /api/alerts/rules - CRÉER UNE RÈGLE\r
+ // =====================================================\r
+ /**\r
+ * Crée une nouvelle règle d'alerte\r
+ * \r
+ * Body attendu :\r
+ * {\r
+ * "userId": "uuid",\r
+ * "pairId": 1,\r
+ * "channel": "EMAIL",\r
+ * "minConfidence": 0.8,\r
+ * "severity": "WARNING",\r
+ * "cooldownMs": 60000\r
+ * }\r
+ */\r
+ async createRule(req, res) {\r
+ try {\r
+ const ruleData = req.body;\r
+\r
+ // Validation des champs obligatoires\r
+ if (!ruleData.userId || !ruleData.channel) {\r
+ return res.status(400).json({\r
+ success: false,\r
+ error: 'Champs manquants (userId, channel requis)'\r
+ });\r
+ }\r
+\r
+ // Appeler le repo (qui appelle l'adapter)\r
+ const newRule = await alertsRepo.createRule(ruleData);\r
+\r
+ res.status(201).json({\r
+ success: true,\r
+ rule: newRule,\r
+ message: 'Règle créée avec succès'\r
+ });\r
+\r
+ } catch (error) {\r
+ console.error('❌ Erreur createRule:', error);\r
+ res.status(500).json({\r
+ success: false,\r
+ error: error.message\r
+ });\r
+ }\r
+ },\r
+\r
+ // =====================================================\r
+ // PUT /api/alerts/rules/:id - MODIFIER UNE RÈGLE\r
+ // =====================================================\r
+ /**\r
+ * Met à jour une règle existante\r
+ * \r
+ * Body possible :\r
+ * {\r
+ * "minConfidence": 0.9,\r
+ * "enabled": true,\r
+ * "channel": "CONSOLE",\r
+ * "severity": "CRITICAL"\r
+ * }\r
+ */\r
+ async updateRule(req, res) {\r
+ try {\r
+ const { id } = req.params;\r
+ const updates = req.body;\r
+\r
+ // Vérifier qu'il y a quelque chose à mettre à jour\r
+ if (Object.keys(updates).length === 0) {\r
+ return res.status(400).json({\r
+ success: false,\r
+ error: 'Aucune donnée à mettre à jour'\r
+ });\r
+ }\r
+\r
+ // Appeler le repo (qui appelle l'adapter)\r
+ const updated = await alertsRepo.updateRule(id, updates);\r
+\r
+ if (!updated) {\r
+ return res.status(404).json({\r
+ success: false,\r
+ error: 'Règle non trouvée'\r
+ });\r
+ }\r
+\r
+ res.json({\r
+ success: true,\r
+ message: 'Règle mise à jour'\r
+ });\r
+\r
+ } catch (error) {\r
+ console.error('❌ Erreur updateRule:', error);\r
+ res.status(500).json({\r
+ success: false,\r
+ error: error.message\r
+ });\r
+ }\r
+ },\r
+\r
+ // =====================================================\r
+ // DELETE /api/alerts/rules/:id - SUPPRIMER UNE RÈGLE\r
+ // =====================================================\r
+ async deleteRule(req, res) {\r
+ try {\r
+ const { id } = req.params;\r
+\r
+ // Appeler le repo (qui appelle l'adapter)\r
+ const deleted = await alertsRepo.deleteRule(id);\r
+\r
+ if (!deleted) {\r
+ return res.status(404).json({\r
+ success: false,\r
+ error: 'Règle non trouvée'\r
+ });\r
+ }\r
+\r
+ res.json({\r
+ success: true,\r
+ message: 'Règle supprimée'\r
+ });\r
+\r
+ } catch (error) {\r
+ console.error('❌ Erreur deleteRule:', error);\r
+ res.status(500).json({\r
+ success: false,\r
+ error: error.message\r
+ });\r
+ }\r
+ },\r
+\r
+ // =====================================================\r
+ // GET /api/alerts/rules/detail/:id - DÉTAIL D'UNE RÈGLE\r
+ // =====================================================\r
+ async getRuleById(req, res) {\r
+ try {\r
+ const { id } = req.params;\r
+\r
+ const rule = await alertsRepo.findRuleById(id);\r
+\r
+ if (!rule) {\r
+ return res.status(404).json({\r
+ success: false,\r
+ error: 'Règle non trouvée'\r
+ });\r
+ }\r
+\r
+ res.json({\r
+ success: true,\r
+ rule\r
+ });\r
+\r
+ } catch (error) {\r
+ console.error('❌ Erreur getRuleById:', error);\r
+ res.status(500).json({\r
+ success: false,\r
+ error: error.message\r
+ });\r
+ }\r
+ }\r
+ };\r
+}\r
+\r
-// =========================================================
-// ALERTS REPOSITORY - VERSION AVEC ADAPTER
-// =========================================================
-// RÔLE : Interface entre le Service et l'Adapter
-// =========================================================
-// Le repo ne fait PLUS de SQL directement.
-// Il délègue à l'adapter injecté.
-// =========================================================
-
-// =========================================================
-// FACTORY FUNCTION : Créer un repository
-// =========================================================
-/**
- * Crée un repository d'alertes réutilisable
- *
- * @param {Object} adapter - Adapter de base de données (MySQL, MongoDB, etc.)
- * @returns {Object} Repository avec toutes les méthodes
- *
- * @example
- * // Avec MySQL
- * import { createMySQLAdapter } from './adapters/mysql.adapter.js';
- * import { createAlertsRepo } from './alerts.repo.js';
- *
- * const adapter = createMySQLAdapter(db);
- * const repo = createAlertsRepo(adapter);
- *
- * @example
- * // Avec MongoDB (adapter à créer)
- * import { createMongoAdapter } from './adapters/mongo.adapter.js';
- *
- * const adapter = createMongoAdapter(mongoClient);
- * const repo = createAlertsRepo(adapter);
- */
-export function createAlertsRepo(adapter) {
-
- console.log(`Repository d'alertes initialisé`);
-
-// =========================================================
-// RETOURNER L'OBJET REPOSITORY
-// =========================================================
- return {
-
- // =====================================================
- // 1. RÉCUPÉRER LES RÈGLES D'ALERTE ACTIVES
- // =====================================================
- /**
- * Récupère les règles d'alerte pour un signal donné
- * @param {string} userId - ID de l'utilisateur
- * @param {number} pairId - ID de la paire crypto
- * @returns {Promise<Array>} Liste des règles actives
- */
- async findActiveRulesForSignal(userId, pairId) {
- return adapter.findActiveRulesForSignal(userId, pairId);
- },
-
- // =====================================================
- // 2. SAUVEGARDER UN ÉVÉNEMENT D'ALERTE
- // =====================================================
- /**
- * Enregistre qu'une alerte a été envoyée
- * @param {Object} eventData - Données de l'événement
- */
- async saveAlertEvent(eventData) {
- return adapter.saveAlertEvent(eventData);
- },
-
- // =====================================================
- // 3. METTRE À JOUR LA DERNIÈRE NOTIFICATION
- // =====================================================
- /**
- * Met à jour le timestamp pour le cooldown (anti-spam)
- * @param {string} ruleId - ID de la règle
- * @param {number} timestamp - Timestamp en millisecondes
- */
- async updateLastNotified(ruleId, timestamp) {
- return adapter.updateLastNotified(ruleId, timestamp);
- },
-
- // =====================================================
- // 4. RÉCUPÉRER L'HISTORIQUE DES ALERTES
- // =====================================================
- /**
- * Récupère les dernières alertes d'un utilisateur
- * @param {string} userId - ID de l'utilisateur
- * @param {number} limit - Nombre max de résultats
- * @returns {Promise<Array>} Historique des alertes
- */
- async getAlertHistory(userId, limit = 50) {
- return adapter.getAlertHistory(userId, limit);
- },
-
- // =====================================================
- // 5. CRÉER UNE RÈGLE D'ALERTE (CRUD - Create)
- // =====================================================
- /**
- * Crée une nouvelle règle d'alerte
- * @param {Object} ruleData - Données de la règle
- * @returns {Promise<Object>} Règle créée
- */
- async createRule(ruleData) {
- return adapter.createRule(ruleData);
- },
-
- // =====================================================
- // 6. MODIFIER UNE RÈGLE D'ALERTE (CRUD - Update)
- // =====================================================
- /**
- * Met à jour une règle existante
- * @param {string} ruleId - ID de la règle
- * @param {Object} updates - Champs à mettre à jour
- * @returns {Promise<boolean>} true si modifié
- */
- async updateRule(ruleId, updates) {
- return adapter.updateRule(ruleId, updates);
- },
-
- // =====================================================
- // 7. SUPPRIMER UNE RÈGLE D'ALERTE (CRUD - Delete)
- // =====================================================
- /**
- * Supprime une règle
- * @param {string} ruleId - ID de la règle
- * @returns {Promise<boolean>} true si supprimé
- */
- async deleteRule(ruleId) {
- return adapter.deleteRule(ruleId);
- },
-
- // =====================================================
- // 8. RÉCUPÉRER UNE RÈGLE PAR ID (CRUD - Read)
- // =====================================================
- /**
- * Récupère une règle par son ID
- * @param {string} ruleId - ID de la règle
- * @returns {Promise<Object|null>} La règle ou null
- */
- async findRuleById(ruleId) {
- return adapter.findRuleById(ruleId);
- },
- };
-}
+// =========================================================\r
+// ALERTS REPOSITORY - VERSION AVEC ADAPTER\r
+// =========================================================\r
+// RÔLE : Interface entre le Service et l'Adapter\r
+// =========================================================\r
+// Le repo ne fait PLUS de SQL directement.\r
+// Il délègue à l'adapter injecté.\r
+// =========================================================\r
+\r
+// =========================================================\r
+// FACTORY FUNCTION : Créer un repository\r
+// =========================================================\r
+/**\r
+ * Crée un repository d'alertes réutilisable\r
+ *\r
+ * @param {Object} adapter - Adapter de base de données (MySQL, MongoDB, etc.)\r
+ * @returns {Object} Repository avec toutes les méthodes\r
+ *\r
+ * @example\r
+ * // Avec MySQL\r
+ * import { createMySQLAdapter } from './adapters/mysql.adapter.js';\r
+ * import { createAlertsRepo } from './alerts.repo.js';\r
+ *\r
+ * const adapter = createMySQLAdapter(db);\r
+ * const repo = createAlertsRepo(adapter);\r
+ *\r
+ * @example\r
+ * // Avec MongoDB (adapter à créer)\r
+ * import { createMongoAdapter } from './adapters/mongo.adapter.js';\r
+ *\r
+ * const adapter = createMongoAdapter(mongoClient);\r
+ * const repo = createAlertsRepo(adapter);\r
+ */\r
+export function createAlertsRepo(adapter) {\r
+\r
+ console.log(`Repository d'alertes initialisé`);\r
+\r
+// =========================================================\r
+// RETOURNER L'OBJET REPOSITORY\r
+// =========================================================\r
+ return {\r
+\r
+ // =====================================================\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
+ * @param {string} userId - ID de l'utilisateur\r
+ * @param {number} pairId - ID de la paire crypto\r
+ * @returns {Promise<Array>} Liste des règles actives\r
+ */\r
+ async findActiveRulesForSignal(userId, pairId) {\r
+ return adapter.findActiveRulesForSignal(userId, pairId);\r
+ },\r
+\r
+ // =====================================================\r
+ // 2. SAUVEGARDER UN ÉVÉNEMENT D'ALERTE\r
+ // =====================================================\r
+ /**\r
+ * Enregistre qu'une alerte a été envoyée\r
+ * @param {Object} eventData - Données de l'événement\r
+ */\r
+ async saveAlertEvent(eventData) {\r
+ return adapter.saveAlertEvent(eventData);\r
+ },\r
+\r
+ // =====================================================\r
+ // 3. METTRE À JOUR LA DERNIÈRE NOTIFICATION\r
+ // =====================================================\r
+ /**\r
+ * Met à jour le timestamp pour le cooldown (anti-spam)\r
+ * @param {string} ruleId - ID de la règle\r
+ * @param {number} timestamp - Timestamp en millisecondes\r
+ */\r
+ async updateLastNotified(ruleId, timestamp) {\r
+ return adapter.updateLastNotified(ruleId, timestamp);\r
+ },\r
+\r
+ // =====================================================\r
+ // 4. RÉCUPÉRER L'HISTORIQUE DES ALERTES\r
+ // =====================================================\r
+ /**\r
+ * Récupère les dernières alertes d'un utilisateur\r
+ * @param {string} userId - ID de l'utilisateur\r
+ * @param {number} limit - Nombre max de résultats\r
+ * @returns {Promise<Array>} Historique des alertes\r
+ */\r
+ async getAlertHistory(userId, limit = 50) {\r
+ return adapter.getAlertHistory(userId, limit);\r
+ },\r
+\r
+ // =====================================================\r
+ // 5. CRÉER UNE RÈGLE D'ALERTE (CRUD - Create)\r
+ // =====================================================\r
+ /**\r
+ * Crée une nouvelle règle d'alerte\r
+ * @param {Object} ruleData - Données de la règle\r
+ * @returns {Promise<Object>} Règle créée\r
+ */\r
+ async createRule(ruleData) {\r
+ return adapter.createRule(ruleData);\r
+ },\r
+\r
+ // =====================================================\r
+ // 6. MODIFIER UNE RÈGLE D'ALERTE (CRUD - Update)\r
+ // =====================================================\r
+ /**\r
+ * Met à jour une règle existante\r
+ * @param {string} ruleId - ID de la règle\r
+ * @param {Object} updates - Champs à mettre à jour\r
+ * @returns {Promise<boolean>} true si modifié\r
+ */\r
+ async updateRule(ruleId, updates) {\r
+ return adapter.updateRule(ruleId, updates);\r
+ },\r
+\r
+ // =====================================================\r
+ // 7. SUPPRIMER UNE RÈGLE D'ALERTE (CRUD - Delete)\r
+ // =====================================================\r
+ /**\r
+ * Supprime une règle\r
+ * @param {string} ruleId - ID de la règle\r
+ * @returns {Promise<boolean>} true si supprimé\r
+ */\r
+ async deleteRule(ruleId) {\r
+ return adapter.deleteRule(ruleId);\r
+ },\r
+\r
+ // =====================================================\r
+ // 8. RÉCUPÉRER UNE RÈGLE PAR ID (CRUD - Read)\r
+ // =====================================================\r
+ /**\r
+ * Récupère une règle par son ID\r
+ * @param {string} ruleId - ID de la règle\r
+ * @returns {Promise<Object|null>} La règle ou null\r
+ */\r
+ async findRuleById(ruleId) {\r
+ return adapter.findRuleById(ruleId);\r
+ },\r
+ };\r
+}\r
-// =========================================================
-// ALERTS ROUTER - VERSION RÉUTILISABLE
-// =========================================================
-// RÔLE : Définir les routes (endpoints) de l'API
-// =========================================================
-
-
-import express from 'express';
-
-// =========================================================
-// FACTORY FUNCTION : Créer un router
-// =========================================================
-/**
- * Crée un router d'alertes réutilisable
- *
- * @param {Object} alertsController - Controller d'alertes (créé par createAlertsController)
- *
- * @returns {express.Router} Router Express configuré
- */
-export function createAlertsRouter(alertsController) {
-
- const router = express.Router();
-
- console.log("Router d'alertes initialisé");
-
-// =========================================================
- // DÉFINITION DES ROUTES
-// =========================================================
- // POST /api/alerts/process-signal
-// =========================================================
- /**
- * Traiter un signal crypto et envoyer les alertes
- *
- * Exemple d'appel :
- * POST http://localhost:3000/api/alerts/process-signal
- * Body : {
- * "userId": "user-123",
- * "pairId": 1,
- * "pair": "BTC/EUR",
- * "action": "BUY",
- * "confidence": 0.85,
- * "criticality": "WARNING",
- * "reason": "RSI oversold + MACD crossover"
- * }
- */
- router.post('/process-signal', alertsController.processSignal);
-
-// =============================================================================
- // GET /api/alerts/rules/:userId
-// =============================================================================
- /**
- * Récupérer les règles d'alerte d'un utilisateur
- *
- * Exemple d'appel :
- * GET http://localhost:3000/api/alerts/rules/user-123
- */
- router.get('/rules/:userId', alertsController.getRules);
-
-// =============================================================================
- // GET /api/alerts/history/:userId
-// =============================================================================
- /**
- * Récupérer l'historique des alertes d'un utilisateur
- *
- * Exemple d'appel :
- * GET http://localhost:3000/api/alerts/history/user-123?limit=20
- */
- router.get('/history/:userId', alertsController.getHistory);
-
-// =============================================================================
-// CRUD
-// =============================================================================
- router.post('/rules', alertsController.createRule);
- router.get('/rules/detail/:id', alertsController.getRuleById);
- router.put('/rules/:id', alertsController.updateRule);
- router.delete('/rules/:id', alertsController.deleteRule);
-
- return router;
-}
-
+// =========================================================\r
+// ALERTS ROUTER - VERSION RÉUTILISABLE\r
+// =========================================================\r
+// RÔLE : Définir les routes (endpoints) de l'API\r
+// =========================================================\r
+\r
+\r
+import express from 'express';\r
+\r
+// =========================================================\r
+// FACTORY FUNCTION : Créer un router\r
+// =========================================================\r
+/**\r
+ * Crée un router d'alertes réutilisable\r
+ *\r
+ * @param {Object} alertsController - Controller d'alertes (créé par createAlertsController)\r
+ *\r
+ * @returns {express.Router} Router Express configuré\r
+ */\r
+export function createAlertsRouter(alertsController) {\r
+\r
+ const router = express.Router();\r
+\r
+ console.log("Router d'alertes initialisé");\r
+\r
+// =========================================================\r
+ // DÉFINITION DES ROUTES\r
+// =========================================================\r
+ // POST /api/alerts/process-signal\r
+// =========================================================\r
+ /**\r
+ * Traiter un signal crypto et envoyer les alertes\r
+ *\r
+ * Exemple d'appel :\r
+ * POST http://localhost:3000/api/alerts/process-signal\r
+ * Body : {\r
+ * "userId": "user-123",\r
+ * "pairId": 1,\r
+ * "pair": "BTC/EUR",\r
+ * "action": "BUY",\r
+ * "confidence": 0.85,\r
+ * "criticality": "WARNING",\r
+ * "reason": "RSI oversold + MACD crossover"\r
+ * }\r
+ */\r
+ router.post('/process-signal', alertsController.processSignal);\r
+\r
+// =============================================================================\r
+ // GET /api/alerts/rules/:userId\r
+// =============================================================================\r
+ /**\r
+ * Récupérer les règles d'alerte d'un utilisateur\r
+ *\r
+ * Exemple d'appel :\r
+ * GET http://localhost:3000/api/alerts/rules/user-123\r
+ */\r
+ router.get('/rules/:userId', alertsController.getRules);\r
+\r
+// =============================================================================\r
+ // GET /api/alerts/history/:userId\r
+// =============================================================================\r
+ /**\r
+ * Récupérer l'historique des alertes d'un utilisateur\r
+ *\r
+ * Exemple d'appel :\r
+ * GET http://localhost:3000/api/alerts/history/user-123?limit=20\r
+ */\r
+ router.get('/history/:userId', alertsController.getHistory);\r
+\r
+// =============================================================================\r
+// CRUD\r
+// =============================================================================\r
+ router.post('/rules', alertsController.createRule);\r
+ router.get('/rules/detail/:id', alertsController.getRuleById);\r
+ router.put('/rules/:id', alertsController.updateRule);\r
+ router.delete('/rules/:id', alertsController.deleteRule);\r
+\r
+ return router;\r
+}\r
+\r
-// =========================================================
-// ALERTS SERVICE - VERSION RÉUTILISABLE
-// =========================================================
-// RÔLE : Logique métier du système d'alertes
-// =========================================================
-// PRINCIPE : INJECTION DE DÉPENDANCES
-// Au lieu d'importer le Repository directement, on le reçoit en paramètre
-// =========================================================
-
-import { v4 as uuidv4 } from 'uuid';
-
-// Import des canaux
-import { sendAlertEmail } from './channels/mailer.js';
-import { sendConsoleAlert } from './channels/console.js';
-import { sendTelegramAlert } from './channels/telegram.js';
-import { sendDiscordAlert } from './channels/discord.js';
-import { sendWebAlert } from './channels/web.js';
-
-// =========================================================
-// FACTORY FUNCTION : Créer un service
-// =========================================================
-/**
- * Crée un service d'alertes réutilisable
- *
- * @param {Object} alertsRepo - Repository d'alertes (créé par createAlertsRepo)
- * @param {Object} options - Configuration optionnelle
- * @param {number} options.defaultCooldown - Cooldown par défaut en ms (60000 = 1min)
- *
- * @returns {Object} Service avec toutes les méthodes
- *
- * @example
- * // Dans Wall-e-tte
- * import db from './config/db.js';
- * import { createAlertsRepo } from './modules/alerts/alerts.repo.js';
- * import { createAlertsService } from './modules/alerts/alerts.service.js';
- *
- * const repo = createAlertsRepo(db);
- * const service = createAlertsService(repo);
- *
- * await service.processSignal(signal);
- *
- * @example
- * // Dans un autre projet
- * const repo = createAlertsRepo(strangersDb, { ... });
- * const service = createAlertsService(repo);
- * await service.processSignal(signal); // Marche pareil !
- */
-export function createAlertsService(alertsRepo, options = {}) {
-
- console.log('🔧 Service d\'alertes initialisé');
-
- // ─────────────────────────────────────────────────────
- // RETOURNER L'OBJET SERVICE
- // ─────────────────────────────────────────────────────
- return {
-
- // =====================================================
- // 1. VÉRIFIER SI UNE ALERTE DOIT ÊTRE ENVOYÉE
- // =====================================================
- /**
- * Détermine si une alerte doit être envoyée selon la règle
- *
- * Cette fonction applique les RÈGLES MÉTIER :
- * - Vérification de la confidence minimum
- * - Vérification du cooldown (anti-spam)
- *
- * @param {Object} rule - Règle d'alerte
- * @param {Object} signal - Signal reçu
- * @returns {boolean} true si l'alerte doit être envoyée
- */
- shouldSendAlert(rule, signal) {
- console.log(`Vérification règle ${rule.rule_id}...`);
-
- // ─────────────────────────────────────────────────
- // CRITÈRE 1 : Vérifier la confidence minimum
- // ─────────────────────────────────────────────────
- // Si le signal a une confidence trop basse, on n'envoie pas
- // Exemple : rule.min_confidence = 0.80 (80%)
- // signal.confidence = 0.75 (75%)
- // → Ne pas envoyer (75% < 80%)
-
- if (signal.confidence < rule.min_confidence) {
- console.log(`Confidence trop basse`);
- console.log(`Signal : ${(signal.confidence * 100).toFixed(2)}%`);
- console.log(`Minimum: ${(rule.min_confidence * 100).toFixed(2)}%`);
- return false;
- }
-
- // ─────────────────────────────────────────────────
- // CRITÈRE 2 : Vérifier le cooldown (anti-spam)
- // ─────────────────────────────────────────────────
- // Le cooldown évite d'envoyer 100 emails en 1 minute
- // Exemple : cooldown_ms = 60000 (1 minute)
- // Dernière alerte envoyée il y a 30 secondes
- // → Trop tôt, ne pas envoyer
-
- const now = Date.now();
-
- if (rule.last_notified_at_ms) {
- // Calculer le temps écoulé depuis la dernière alerte
- const timeSinceLastAlert = now - rule.last_notified_at_ms;
-
- // Si pas assez de temps écoulé, ne pas envoyer
- if (timeSinceLastAlert < rule.cooldown_ms) {
- const remainingMs = rule.cooldown_ms - timeSinceLastAlert;
- const remainingSec = Math.ceil(remainingMs / 1000);
-
- console.log(` ⏳ Cooldown actif`);
- console.log(` Attendre encore ${remainingSec}s`);
- return false;
- }
- }
-
- // ─────────────────────────────────────────────────
- // TOUS LES CRITÈRES SONT OK !
- // ─────────────────────────────────────────────────
- console.log(` Tous les critères sont remplis`);
- return true;
- },
-
- // =====================================================
- // 2. ENVOYER UNE ALERTE VIA LE BON CANAL
- // =====================================================
- /**
- * Envoie une alerte via le canal spécifié dans la règle
- *
- * Cette fonction :
- * - Choisit la bonne fonction d'envoi (email, telegram, etc.)
- * - Prépare les paramètres spécifiques au canal
- * - Gère les erreurs d'envoi
- *
- * @param {Object} rule - Règle d'alerte avec le canal
- * @param {Object} signal - Signal à envoyer
- * @returns {Promise<string>} 'SENT' ou 'FAILED'
- */
- async sendViaChannel(rule, signal) {
- console.log(`Envoi via ${rule.channel}...`);
-
- let status = 'SENT';
-
- try {
- // ─────────────────────────────────────────────
- // SWITCH selon le canal configuré
- // ─────────────────────────────────────────────
- switch (rule.channel) {
-
- // ── EMAIL ────────────────────────────────
- case 'EMAIL':
- // Le destinataire est l'email de l'utilisateur
- // On l'a récupéré via le JOIN dans la requête SQL
- await sendAlertEmail(rule.email, signal);
- break;
-
- // ── TELEGRAM ─────────────────────────────
- case 'TELEGRAM':
- // Le chatId est stocké dans rule.params (JSON)
- // Exemple de params : {"chatId": "123456789"}
- const params = JSON.parse(rule.params);
- const chatId = params.chatId;
-
- if (!chatId) {
- console.error(' Pas de chatId dans params');
- status = 'FAILED';
- break;
- }
-
- await sendTelegramAlert(chatId, signal);
- break;
-
- // ── DISCORD ──────────────────────────────
- case 'DISCORD':
- const discordParams = JSON.parse(rule.params);
- const webhookUrl = discordParams.webhookUrl;
-
- if (!webhookUrl) {
- console.error(' Pas de webhookUrl dans params');
- status = 'FAILED';
- break;
- }
-
- await sendDiscordAlert(webhookUrl, signal);
- break;
-
- // ── CONSOLE ──────────────────────────────
- case 'CONSOLE':
- // Pas de paramètres nécessaires
- await sendConsoleAlert(signal);
- break;
-
- // ── WEB ──────────────────────────────────
- case 'WEB':
- // Envoyer à l'interface web de l'utilisateur
- await sendWebAlert(rule.user_id, signal);
- break;
-
- // ── CANAL INCONNU ────────────────────────
- default:
- console.warn(` Canal inconnu: ${rule.channel}`);
- status = 'FAILED';
- }
-
- } catch (error) {
- // ─────────────────────────────────────────────
- // GESTION DES ERREURS
- // ─────────────────────────────────────────────
- console.error(` Erreur lors de l'envoi via ${rule.channel}:`);
- console.error(` ${error.message}`);
- status = 'FAILED';
- }
-
- return status;
- },
-
- // =====================================================
- // 3. TRAITER UN SIGNAL ET ENVOYER LES ALERTES
- // =====================================================
- /**
- * Fonction principale qui traite un signal crypto
- *
- * WORKFLOW :
- * 1. Récupérer les règles actives pour ce signal
- * 2. Pour chaque règle :
- * a. Vérifier si alerte doit être envoyée
- * b. Envoyer via le bon canal
- * c. Enregistrer dans l'historique
- * d. Mettre à jour le timestamp
- *
- * @param {Object} signal - Signal crypto reçu
- * @param {string} signal.userId - ID utilisateur
- * @param {number} signal.pairId - ID de la paire
- * @param {string} signal.action - BUY, SELL, HOLD, STOP_LOSS
- * @param {number} signal.confidence - 0-1
- * @param {string} signal.criticality - CRITICAL, WARNING, INFO
- * @param {string} signal.reason - Explication
- */
- async processSignal(signal) {
- console.log('\n' + '═'.repeat(80));
- console.log(` TRAITEMENT DU SIGNAL`);
- console.log(` Action : ${signal.action}`);
- console.log(` Paire : ${signal.pair}`);
- console.log(` User : ${signal.userId}`);
- console.log('═'.repeat(80) + '\n');
-
- try {
- // ─────────────────────────────────────────────
- // ÉTAPE 1 : Récupérer les règles actives
- // ─────────────────────────────────────────────
- // Utilise le repository injecté (pas d'import hardcodé)
- const rules = await alertsRepo.findActiveRulesForSignal(
- signal.userId,
- signal.pairId
- );
-
- if (rules.length === 0) {
- console.log(' Aucune règle active trouvée pour ce signal');
- console.log(' → Vérifie que des règles sont configurées en DB\n');
- return;
- }
-
- console.log(` ${rules.length} règle(s) trouvée(s)\n`);
-
- // ─────────────────────────────────────────────
- // ÉTAPE 2 : Traiter chaque règle
- // ─────────────────────────────────────────────
- let sentCount = 0;
-
- for (const rule of rules) {
- console.log(`┌─ Règle: ${rule.rule_type} (${rule.channel})`);
-
- // Vérifier si on doit envoyer
- if (!this.shouldSendAlert(rule, signal)) {
- console.log(`└─ Alerte NON envoyée\n`);
- continue;
- }
-
- // Envoyer via le canal
- const status = await this.sendViaChannel(rule, signal);
-
- // Préparer les données de l'événement
- const eventData = {
- alert_event_id: uuidv4(),
- rule_id: rule.rule_id,
- timestamp_ms: Date.now(),
- severity: signal.criticality,
- channel: rule.channel,
- send_status: status,
- title: `${signal.action} ${signal.pair}`,
- message: signal.reason,
- signal_action: signal.action,
- confidence: signal.confidence,
- correlation_id: signal.correlationId || null
- };
-
- // Sauvegarder dans alert_events (via repo injecté)
- await alertsRepo.saveAlertEvent(eventData);
-
- // Mettre à jour last_notified_at si envoi réussi (via repo injecté)
- if (status === 'SENT') {
- await alertsRepo.updateLastNotified(rule.rule_id, Date.now());
- sentCount++;
- console.log(`└─ Alerte envoyée avec succès\n`);
- } else {
- console.log(`└─ Échec de l'envoi\n`);
- }
- }
-
- // ─────────────────────────────────────────────
- // RÉSUMÉ FINAL
- // ─────────────────────────────────────────────
- console.log('═'.repeat(80));
- console.log(` RÉSUMÉ`);
- console.log(` Règles vérifiées : ${rules.length}`);
- console.log(` Alertes envoyées : ${sentCount}`);
- console.log('═'.repeat(80) + '\n');
-
- } catch (error) {
- console.error(' ERREUR CRITIQUE dans processSignal:');
- console.error(error);
- throw error; // Remonter l'erreur pour que le controller puisse la gérer
- }
- }
- };
-}
+// =========================================================\r
+// ALERTS SERVICE - VERSION RÉUTILISABLE\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
+// Import des canaux\r
+import { sendAlertEmail } from './channels/mailer.js';\r
+import { sendConsoleAlert } from './channels/console.js';\r
+import { sendTelegramAlert } from './channels/telegram.js';\r
+import { sendDiscordAlert } from './channels/discord.js';\r
+import { sendWebAlert } from './channels/web.js';\r
+\r
+// =========================================================\r
+// FACTORY FUNCTION : Créer un service\r
+// =========================================================\r
+/**\r
+ * Crée un service d'alertes réutilisable\r
+ *\r
+ * @param {Object} alertsRepo - Repository d'alertes (créé par createAlertsRepo)\r
+ * @param {Object} options - Configuration optionnelle\r
+ * @param {number} options.defaultCooldown - Cooldown par défaut en ms (60000 = 1min)\r
+ *\r
+ * @returns {Object} Service avec toutes les méthodes\r
+ *\r
+ * @example\r
+ * // Dans Wall-e-tte\r
+ * import db from './config/db.js';\r
+ * import { createAlertsRepo } from './modules/alerts/alerts.repo.js';\r
+ * import { createAlertsService } from './modules/alerts/alerts.service.js';\r
+ *\r
+ * const repo = createAlertsRepo(db);\r
+ * const service = createAlertsService(repo);\r
+ *\r
+ * await service.processSignal(signal);\r
+ *\r
+ * @example\r
+ * // Dans un autre projet\r
+ * const repo = createAlertsRepo(strangersDb, { ... });\r
+ * const service = createAlertsService(repo);\r
+ * await service.processSignal(signal); // Marche pareil !\r
+ */\r
+export function createAlertsService(alertsRepo, options = {}) {\r
+\r
+ console.log('🔧 Service d\'alertes initialisé');\r
+\r
+ // ─────────────────────────────────────────────────────\r
+ // RETOURNER L'OBJET SERVICE\r
+ // ─────────────────────────────────────────────────────\r
+ return {\r
+\r
+ // =====================================================\r
+ // 1. VÉRIFIER SI UNE ALERTE DOIT ÊTRE ENVOYÉE\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
+ * - Vérification du cooldown (anti-spam)\r
+ *\r
+ * @param {Object} rule - Règle d'alerte\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
+\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
+ return false;\r
+ }\r
+\r
+ // ─────────────────────────────────────────────────\r
+ // CRITÈRE 2 : Vérifier le cooldown (anti-spam)\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
+ // → Trop tôt, ne pas envoyer\r
+\r
+ const now = Date.now();\r
+\r
+ if (rule.last_notified_at_ms) {\r
+ // Calculer le temps écoulé depuis la dernière alerte\r
+ const timeSinceLastAlert = now - rule.last_notified_at_ms;\r
+\r
+ // Si pas assez de temps écoulé, ne pas envoyer\r
+ if (timeSinceLastAlert < rule.cooldown_ms) {\r
+ const remainingMs = rule.cooldown_ms - timeSinceLastAlert;\r
+ const remainingSec = Math.ceil(remainingMs / 1000);\r
+\r
+ console.log(` ⏳ Cooldown actif`);\r
+ console.log(` Attendre encore ${remainingSec}s`);\r
+ return false;\r
+ }\r
+ }\r
+\r
+ // ─────────────────────────────────────────────────\r
+ // TOUS LES CRITÈRES SONT OK !\r
+ // ─────────────────────────────────────────────────\r
+ console.log(` Tous les critères sont remplis`);\r
+ return true;\r
+ },\r
+\r
+ // =====================================================\r
+ // 2. ENVOYER UNE ALERTE VIA LE BON CANAL\r
+ // =====================================================\r
+ /**\r
+ * Envoie une alerte via le canal spécifié dans la règle\r
+ *\r
+ * Cette fonction :\r
+ * - Choisit la bonne fonction d'envoi (email, telegram, etc.)\r
+ * - Prépare les paramètres spécifiques au canal\r
+ * - Gère les erreurs d'envoi\r
+ *\r
+ * @param {Object} rule - Règle d'alerte avec le canal\r
+ * @param {Object} signal - Signal à envoyer\r
+ * @returns {Promise<string>} 'SENT' ou 'FAILED'\r
+ */\r
+ async sendViaChannel(rule, signal) {\r
+ console.log(`Envoi via ${rule.channel}...`);\r
+\r
+ let status = 'SENT';\r
+\r
+ try {\r
+ // ─────────────────────────────────────────────\r
+ // SWITCH selon le canal configuré\r
+ // ─────────────────────────────────────────────\r
+ switch (rule.channel) {\r
+\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
+ 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 chatId = params.chatId;\r
+\r
+ if (!chatId) {\r
+ console.error(' Pas de chatId dans params');\r
+ status = 'FAILED';\r
+ break;\r
+ }\r
+\r
+ await sendTelegramAlert(chatId, signal);\r
+ break;\r
+\r
+ // ── DISCORD ──────────────────────────────\r
+ case 'DISCORD':\r
+ const discordParams = JSON.parse(rule.params);\r
+ const webhookUrl = discordParams.webhookUrl;\r
+\r
+ if (!webhookUrl) {\r
+ console.error(' Pas de webhookUrl dans params');\r
+ status = 'FAILED';\r
+ break;\r
+ }\r
+\r
+ await sendDiscordAlert(webhookUrl, signal);\r
+ break;\r
+\r
+ // ── CONSOLE ──────────────────────────────\r
+ case 'CONSOLE':\r
+ // Pas de paramètres nécessaires\r
+ await sendConsoleAlert(signal);\r
+ break;\r
+\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
+ default:\r
+ console.warn(` Canal inconnu: ${rule.channel}`);\r
+ status = 'FAILED';\r
+ }\r
+\r
+ } catch (error) {\r
+ // ─────────────────────────────────────────────\r
+ // GESTION DES ERREURS\r
+ // ─────────────────────────────────────────────\r
+ console.error(` Erreur lors de l'envoi via ${rule.channel}:`);\r
+ console.error(` ${error.message}`);\r
+ status = 'FAILED';\r
+ }\r
+\r
+ return status;\r
+ },\r
+\r
+ // =====================================================\r
+ // 3. TRAITER UN SIGNAL ET ENVOYER LES ALERTES\r
+ // =====================================================\r
+ /**\r
+ * Fonction principale qui traite un signal crypto\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
+ * b. Envoyer via le bon canal\r
+ * c. Enregistrer dans l'historique\r
+ * d. Mettre à jour le timestamp\r
+ *\r
+ * @param {Object} signal - Signal crypto reçu\r
+ * @param {string} signal.userId - ID utilisateur\r
+ * @param {number} signal.pairId - ID de la paire\r
+ * @param {string} signal.action - BUY, SELL, HOLD, STOP_LOSS\r
+ * @param {number} signal.confidence - 0-1\r
+ * @param {string} signal.criticality - CRITICAL, WARNING, INFO\r
+ * @param {string} signal.reason - Explication\r
+ */\r
+ async processSignal(signal) {\r
+ console.log('\n' + '═'.repeat(80));\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('═'.repeat(80) + '\n');\r
+\r
+ try {\r
+ // ─────────────────────────────────────────────\r
+ // ÉTAPE 1 : Récupérer les règles actives\r
+ // ─────────────────────────────────────────────\r
+ // Utilise le repository injecté (pas d'import hardcodé)\r
+ const rules = await alertsRepo.findActiveRulesForSignal(\r
+ signal.userId,\r
+ signal.pairId\r
+ );\r
+\r
+ if (rules.length === 0) {\r
+ console.log(' Aucune règle active trouvée pour ce signal');\r
+ console.log(' → Vérifie que des règles sont configurées en DB\n');\r
+ return;\r
+ }\r
+\r
+ console.log(` ${rules.length} règle(s) trouvée(s)\n`);\r
+\r
+ // ─────────────────────────────────────────────\r
+ // ÉTAPE 2 : Traiter chaque règle\r
+ // ─────────────────────────────────────────────\r
+ let sentCount = 0;\r
+\r
+ for (const rule of rules) {\r
+ console.log(`┌─ Règle: ${rule.rule_type} (${rule.channel})`);\r
+\r
+ // Vérifier si on doit envoyer\r
+ if (!this.shouldSendAlert(rule, signal)) {\r
+ console.log(`└─ Alerte NON envoyée\n`);\r
+ continue;\r
+ }\r
+\r
+ // Envoyer via le canal\r
+ const status = await this.sendViaChannel(rule, signal);\r
+\r
+ // Préparer les données de l'événement\r
+ const eventData = {\r
+ alert_event_id: uuidv4(),\r
+ rule_id: rule.rule_id,\r
+ timestamp_ms: Date.now(),\r
+ severity: signal.criticality,\r
+ channel: rule.channel,\r
+ send_status: status,\r
+ title: `${signal.action} ${signal.pair}`,\r
+ message: signal.reason,\r
+ signal_action: signal.action,\r
+ confidence: signal.confidence,\r
+ correlation_id: signal.correlationId || null\r
+ };\r
+\r
+ // Sauvegarder dans alert_events (via repo injecté)\r
+ await alertsRepo.saveAlertEvent(eventData);\r
+\r
+ // Mettre à jour last_notified_at si envoi réussi (via repo injecté)\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
+ } else {\r
+ console.log(`└─ Échec de l'envoi\n`);\r
+ }\r
+ }\r
+\r
+ // ─────────────────────────────────────────────\r
+ // RÉSUMÉ FINAL\r
+ // ─────────────────────────────────────────────\r
+ console.log('═'.repeat(80));\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('═'.repeat(80) + '\n');\r
+\r
+ } catch (error) {\r
+ console.error(' ERREUR CRITIQUE dans processSignal:');\r
+ console.error(error);\r
+ throw error; // Remonter l'erreur pour que le controller puisse la gérer\r
+ }\r
+ }\r
+ };\r
+}\r
-// =========================================================
-// CANAL CONSOLE - Notifications dans la console
-// =========================================================
-// RÔLE : Afficher les alertes dans le terminal
-// =========================================================
-// UTILITÉ :
-// - Développement et debugging
-// - Tests rapides sans configurer email/telegram
-// - Logs pour surveillance serveur
-// =========================================================
-
-// =========================================================
-// CODES COULEUR ANSI POUR LE TERMINAL (merci GPT)
-// =========================================================
-// Ces codes permettent de colorer le texte dans la console
-const COLORS = {
- RESET: '\x1b[0m',
-
- // Couleurs de texte
- RED: '\x1b[31m',
- GREEN: '\x1b[32m',
- YELLOW: '\x1b[33m',
- BLUE: '\x1b[34m',
- MAGENTA: '\x1b[35m',
- CYAN: '\x1b[36m',
- WHITE: '\x1b[37m',
-
- // Styles
- BOLD: '\x1b[1m',
- DIM: '\x1b[2m',
-
- // Fond
- BG_RED: '\x1b[41m',
- BG_YELLOW: '\x1b[43m',
- BG_BLUE: '\x1b[44m'
-};
-
-// =========================================================
-// FONCTION PRINCIPALE : Afficher une alerte dans la console
-// =========================================================
-/**
- * Affiche une alerte formatée dans la console
- *
- * @param {Object} signal - Objet contenant les infos du signal
- * @param {string} signal.action - BUY, SELL, HOLD, STOP_LOSS
- * @param {string} signal.pair - Paire crypto (ex: BTC/EUR)
- * @param {number} signal.confidence - Niveau de confiance (0-1)
- * @param {string} signal.reason - Raison de l'alerte
- * @param {string} signal.criticality - CRITICAL, WARNING, INFO
- *
- * @returns {Promise<string>} Toujours 'SENT' (console ne peut pas échouer)
- */
-export async function sendConsoleAlert(signal) {
-
- // =====================================================
- // 1. DÉTERMINER LA COULEUR SELON LA CRITICITÉ
- // =====================================================
- let color;
- let bgColor;
- let icon;
-
- switch (signal.criticality) {
- case 'CRITICAL':
- color = COLORS.RED;
- bgColor = COLORS.BG_RED;
- icon = '🚨';
- break;
- case 'WARNING':
- color = COLORS.YELLOW;
- bgColor = COLORS.BG_YELLOW;
- icon = '⚠️';
- break;
- case 'INFO':
- default:
- color = COLORS.BLUE;
- bgColor = COLORS.BG_BLUE;
- icon = 'ℹ️';
- break;
- }
-
- // =====================================================
- // 2. FORMATER LA CONFIDENCE EN POURCENTAGE
- // =====================================================
- const confidencePercent = (parseFloat(signal.confidence) * 100).toFixed(2);
-
- // =====================================================
- // 3. CRÉER LE TIMESTAMP
- // =====================================================
- const timestamp = new Date().toLocaleString('fr-FR', {
- day: '2-digit',
- month: '2-digit',
- year: 'numeric',
- hour: '2-digit',
- minute: '2-digit',
- second: '2-digit'
- });
-
- // =====================================================
- // 4. AFFICHER L'ALERTE AVEC MISE EN FORME
- // =====================================================
- console.log('\n'); // Ligne vide avant
- console.log('═'.repeat(80)); // Ligne de séparation
-
- // En-tête avec couleur de fond
- console.log(
- `${bgColor}${COLORS.WHITE}${COLORS.BOLD} ` +
- `${icon} ALERTE ${signal.criticality} `.padEnd(80) +
- `${COLORS.RESET}`
- );
-
- // Ligne de séparation
- console.log('─'.repeat(80));
-
- // Action recommandée (en couleur et gras)
- console.log(
- `${COLORS.BOLD}${color}ACTION :${COLORS.RESET} ` +
- `${getActionEmoji(signal.action)} ${signal.action}`
- );
-
- // Paire crypto
- console.log(
- `${COLORS.BOLD}${COLORS.CYAN}PAIRE :${COLORS.RESET} ` +
- `${signal.pair}`
- );
-
- // Niveau de confiance avec barre visuelle
- const confidenceBar = createProgressBar(signal.confidence);
- console.log(
- `${COLORS.BOLD}${COLORS.MAGENTA}CONFIANCE :${COLORS.RESET} ` +
- `${confidenceBar} ${confidencePercent}%`
- );
-
- // Ligne de séparation
- console.log('─'.repeat(80));
-
- // Raison (analyse)
- console.log(`${COLORS.BOLD}ANALYSE :${COLORS.RESET}`);
- console.log(wrapText(signal.reason, 76)); // Wrap à 76 caractères
-
- // Ligne de séparation
- console.log('─'.repeat(80));
-
- // Timestamp en grisé
- console.log(`${COLORS.DIM}📅 ${timestamp}${COLORS.RESET}`);
-
- // Ligne de fin
- console.log('═'.repeat(80));
- console.log('\n'); // Ligne vide après
-
- // =====================================================
- // 5. RETOURNER LE STATUT
- // =====================================================
- // La console ne peut jamais échouer, donc toujours SENT
- return 'SENT';
-}
-
-// =========================================================
-// FONCTIONS UTILITAIRES
-// =========================================================
-
-/**
- * Retourne l'emoji correspondant à l'action
- */
-function getActionEmoji(action) {
- const emojis = {
- 'BUY': '🟢',
- 'SELL': '🔴',
- 'HOLD': '🟡',
- 'STOP_LOSS': '🛑'
- };
- return emojis[action] || '📊';
-}
-
-/**
- * Crée une barre de progression visuelle pour la confidence
- * Exemple : ████████░░ (80%)
- */
-function createProgressBar(confidence, length = 20) {
- const filled = Math.round(confidence * length);
- const empty = length - filled;
-
- const bar =
- COLORS.GREEN + '█'.repeat(filled) +
- COLORS.DIM + '░'.repeat(empty) +
- COLORS.RESET;
-
- return `[${bar}]`;
-}
-
-/**
- * Coupe le texte en plusieurs lignes si trop long
- * Utile pour que la raison ne dépasse pas la largeur du terminal
- */
-function wrapText(text, maxWidth) {
- const words = text.split(' ');
- const lines = [];
- let currentLine = ' '; // Indentation
-
- words.forEach(word => {
- if ((currentLine + word).length > maxWidth) {
- lines.push(currentLine);
- currentLine = ' ' + word + ' ';
- } else {
- currentLine += word + ' ';
- }
- });
-
- if (currentLine.trim()) {
- lines.push(currentLine);
- }
-
- return lines.join('\n');
-}
-
-// =========================================================
-// EXPORT D'UNE FONCTION DE TEST
-// =========================================================
-/**
- * Fonction pour tester rapidement le canal console
- * Usage : node test-console.js
- */
-export function testConsoleAlert() {
- const fakeSignal = {
- action: 'BUY',
- pair: 'BTC/EUR',
- confidence: 0.87,
- criticality: 'WARNING',
- reason: 'Les indicateurs techniques montrent une tendance haussière forte. Le RSI est à 45 (zone neutre), le MACD vient de croiser la ligne de signal vers le haut, et le volume des échanges augmente significativement.'
- };
-
- sendConsoleAlert(fakeSignal);
-}
-
+// =========================================================\r
+// CANAL CONSOLE - Notifications dans la console\r
+// =========================================================\r
+// RÔLE : Afficher les alertes dans le terminal\r
+// =========================================================\r
+// UTILITÉ :\r
+// - Développement et debugging\r
+// - Tests rapides sans configurer email/telegram\r
+// - Logs pour surveillance serveur\r
+// =========================================================\r
+\r
+// =========================================================\r
+// CODES COULEUR ANSI POUR LE TERMINAL (merci GPT)\r
+// =========================================================\r
+// Ces codes permettent de colorer le texte dans la console\r
+const COLORS = {\r
+ RESET: '\x1b[0m',\r
+\r
+ // Couleurs de texte\r
+ RED: '\x1b[31m',\r
+ GREEN: '\x1b[32m',\r
+ YELLOW: '\x1b[33m',\r
+ BLUE: '\x1b[34m',\r
+ MAGENTA: '\x1b[35m',\r
+ CYAN: '\x1b[36m',\r
+ WHITE: '\x1b[37m',\r
+\r
+ // Styles\r
+ BOLD: '\x1b[1m',\r
+ DIM: '\x1b[2m',\r
+\r
+ // Fond\r
+ BG_RED: '\x1b[41m',\r
+ BG_YELLOW: '\x1b[43m',\r
+ BG_BLUE: '\x1b[44m'\r
+};\r
+\r
+// =========================================================\r
+// FONCTION PRINCIPALE : Afficher une alerte dans la console\r
+// =========================================================\r
+/**\r
+ * Affiche une alerte formatée dans la console\r
+ *\r
+ * @param {Object} signal - Objet contenant les infos du signal\r
+ * @param {string} signal.action - BUY, SELL, HOLD, STOP_LOSS\r
+ * @param {string} signal.pair - Paire crypto (ex: BTC/EUR)\r
+ * @param {number} signal.confidence - Niveau de confiance (0-1)\r
+ * @param {string} signal.reason - Raison de l'alerte\r
+ * @param {string} signal.criticality - CRITICAL, WARNING, INFO\r
+ *\r
+ * @returns {Promise<string>} Toujours 'SENT' (console ne peut pas échouer)\r
+ */\r
+export async function sendConsoleAlert(signal) {\r
+\r
+ // =====================================================\r
+ // 1. DÉTERMINER LA COULEUR SELON LA CRITICITÉ\r
+ // =====================================================\r
+ let color;\r
+ let bgColor;\r
+ let icon;\r
+\r
+ switch (signal.criticality) {\r
+ case 'CRITICAL':\r
+ color = COLORS.RED;\r
+ bgColor = COLORS.BG_RED;\r
+ icon = '🚨';\r
+ break;\r
+ case 'WARNING':\r
+ color = COLORS.YELLOW;\r
+ bgColor = COLORS.BG_YELLOW;\r
+ icon = '⚠️';\r
+ break;\r
+ case 'INFO':\r
+ default:\r
+ color = COLORS.BLUE;\r
+ bgColor = COLORS.BG_BLUE;\r
+ icon = 'ℹ️';\r
+ break;\r
+ }\r
+\r
+ // =====================================================\r
+ // 2. FORMATER LA CONFIDENCE EN POURCENTAGE\r
+ // =====================================================\r
+ const confidencePercent = (parseFloat(signal.confidence) * 100).toFixed(2);\r
+\r
+ // =====================================================\r
+ // 3. CRÉER LE TIMESTAMP\r
+ // =====================================================\r
+ const timestamp = new Date().toLocaleString('fr-FR', {\r
+ day: '2-digit',\r
+ month: '2-digit',\r
+ year: 'numeric',\r
+ hour: '2-digit',\r
+ minute: '2-digit',\r
+ second: '2-digit'\r
+ });\r
+\r
+ // =====================================================\r
+ // 4. AFFICHER L'ALERTE AVEC MISE EN FORME\r
+ // =====================================================\r
+ console.log('\n'); // Ligne vide avant\r
+ console.log('═'.repeat(80)); // Ligne de séparation\r
+\r
+ // En-tête avec couleur de fond\r
+ console.log(\r
+ `${bgColor}${COLORS.WHITE}${COLORS.BOLD} ` +\r
+ `${icon} ALERTE ${signal.criticality} `.padEnd(80) +\r
+ `${COLORS.RESET}`\r
+ );\r
+\r
+ // Ligne de séparation\r
+ console.log('─'.repeat(80));\r
+\r
+ // Action recommandée (en couleur et gras)\r
+ console.log(\r
+ `${COLORS.BOLD}${color}ACTION :${COLORS.RESET} ` +\r
+ `${getActionEmoji(signal.action)} ${signal.action}`\r
+ );\r
+\r
+ // Paire crypto\r
+ console.log(\r
+ `${COLORS.BOLD}${COLORS.CYAN}PAIRE :${COLORS.RESET} ` +\r
+ `${signal.pair}`\r
+ );\r
+\r
+ // Niveau de confiance avec barre visuelle\r
+ const confidenceBar = createProgressBar(signal.confidence);\r
+ console.log(\r
+ `${COLORS.BOLD}${COLORS.MAGENTA}CONFIANCE :${COLORS.RESET} ` +\r
+ `${confidenceBar} ${confidencePercent}%`\r
+ );\r
+\r
+ // Ligne de séparation\r
+ console.log('─'.repeat(80));\r
+\r
+ // Raison (analyse)\r
+ console.log(`${COLORS.BOLD}ANALYSE :${COLORS.RESET}`);\r
+ console.log(wrapText(signal.reason, 76)); // Wrap à 76 caractères\r
+\r
+ // Ligne de séparation\r
+ console.log('─'.repeat(80));\r
+\r
+ // Timestamp en grisé\r
+ console.log(`${COLORS.DIM}📅 ${timestamp}${COLORS.RESET}`);\r
+\r
+ // Ligne de fin\r
+ console.log('═'.repeat(80));\r
+ console.log('\n'); // Ligne vide après\r
+\r
+ // =====================================================\r
+ // 5. RETOURNER LE STATUT\r
+ // =====================================================\r
+ // La console ne peut jamais échouer, donc toujours SENT\r
+ return 'SENT';\r
+}\r
+\r
+// =========================================================\r
+// FONCTIONS UTILITAIRES\r
+// =========================================================\r
+\r
+/**\r
+ * Retourne l'emoji correspondant à l'action\r
+ */\r
+function getActionEmoji(action) {\r
+ const emojis = {\r
+ 'BUY': '🟢',\r
+ 'SELL': '🔴',\r
+ 'HOLD': '🟡',\r
+ 'STOP_LOSS': '🛑'\r
+ };\r
+ return emojis[action] || '📊';\r
+}\r
+\r
+/**\r
+ * Crée une barre de progression visuelle pour la confidence\r
+ * Exemple : ████████░░ (80%)\r
+ */\r
+function createProgressBar(confidence, length = 20) {\r
+ const filled = Math.round(confidence * length);\r
+ const empty = length - filled;\r
+\r
+ const bar =\r
+ COLORS.GREEN + '█'.repeat(filled) +\r
+ COLORS.DIM + '░'.repeat(empty) +\r
+ COLORS.RESET;\r
+\r
+ return `[${bar}]`;\r
+}\r
+\r
+/**\r
+ * Coupe le texte en plusieurs lignes si trop long\r
+ * Utile pour que la raison ne dépasse pas la largeur du terminal\r
+ */\r
+function wrapText(text, maxWidth) {\r
+ const words = text.split(' ');\r
+ const lines = [];\r
+ let currentLine = ' '; // Indentation\r
+\r
+ words.forEach(word => {\r
+ if ((currentLine + word).length > maxWidth) {\r
+ lines.push(currentLine);\r
+ currentLine = ' ' + word + ' ';\r
+ } else {\r
+ currentLine += word + ' ';\r
+ }\r
+ });\r
+\r
+ if (currentLine.trim()) {\r
+ lines.push(currentLine);\r
+ }\r
+\r
+ return lines.join('\n');\r
+}\r
+\r
+// =========================================================\r
+// EXPORT D'UNE FONCTION DE TEST\r
+// =========================================================\r
+/**\r
+ * Fonction pour tester rapidement le canal console\r
+ * Usage : node test-console.js\r
+ */\r
+export function testConsoleAlert() {\r
+ const fakeSignal = {\r
+ action: 'BUY',\r
+ pair: 'BTC/EUR',\r
+ confidence: 0.87,\r
+ criticality: 'WARNING',\r
+ reason: 'Les indicateurs techniques montrent une tendance haussière forte. Le RSI est à 45 (zone neutre), le MACD vient de croiser la ligne de signal vers le haut, et le volume des échanges augmente significativement.'\r
+ };\r
+\r
+ sendConsoleAlert(fakeSignal);\r
+}\r
+\r
-// !!! A FAIRE !!!
-
-// =========================================================
-// CANAL DISCORD - Notifications via Discord Webhook
-// =========================================================
-// RÔLE : Envoyer des alertes dans un channel Discord
-// =========================================================
-
-/**
- * Envoie une alerte via Discord Webhook
- *
- * @param {string} webhookUrl - URL du webhook Discord
- * @param {Object} signal - Signal crypto
- * @returns {Promise<string>} 'SENT' ou 'FAILED'
- */
-export async function sendDiscordAlert(webhookUrl, signal) {
- // TODO: À implémenter si nécessaire
-
- console.log('💬 Canal Discord pas encore implémenté');
-
- return 'FAILED';
-
-}
+// !!! A FAIRE !!!\r
+\r
+// =========================================================\r
+// CANAL DISCORD - Notifications via Discord Webhook\r
+// =========================================================\r
+// RÔLE : Envoyer des alertes dans un channel Discord\r
+// =========================================================\r
+\r
+/**\r
+ * Envoie une alerte via Discord Webhook\r
+ *\r
+ * @param {string} webhookUrl - URL du webhook Discord\r
+ * @param {Object} signal - Signal crypto\r
+ * @returns {Promise<string>} 'SENT' ou 'FAILED'\r
+ */\r
+export async function sendDiscordAlert(webhookUrl, signal) {\r
+ // TODO: À implémenter si nécessaire\r
+\r
+ console.log('💬 Canal Discord pas encore implémenté');\r
+\r
+ return 'FAILED';\r
+\r
+}\r
-// =========================================================
-// CANAL EMAIL - Notifications par email
-// =========================================================
-// RÔLE : Envoyer des alertes par email avec Nodemailer
-// =========================================================
-
-import nodemailer from 'nodemailer';
-
-// =========================================================
-// FONCTION PRINCIPALE : Envoyer une alerte par email
-// =========================================================
-/**
- * Envoie une alerte par email
- *
- * @param {string} to - Adresse email du destinataire
- * @param {Object} signal - Objet contenant les infos du signal
- * @param {string} signal.action - BUY, SELL, HOLD, STOP_LOSS
- * @param {string} signal.pair - Paire crypto (ex: BTC/EUR)
- * @param {number} signal.confidence - Niveau de confiance (0-1)
- * @param {string} signal.reason - Raison de l'alerte
- * @param {string} signal.criticality - CRITICAL, WARNING, INFO
- *
- * @returns {Promise<string>} 'SENT' ou 'FAILED'
- */
-export async function sendAlertEmail(to, signal) {
-
- // =====================================================
- // 1. CRÉER LE TRANSPORTEUR SMTP
- // =====================================================
- // Le transporteur est l'objet qui envoie les emails
- // Il se connecte au serveur SMTP (ex: Gmail, Mailtrap, etc.)
- const transporter = nodemailer.createTransport({
- host: process.env.MAIL_HOST, // ex: smtp.gmail.com
- port: parseInt(process.env.MAIL_PORT), // ex: 587
- secure: process.env.MAIL_PORT === '465', // true pour le port 465, false pour les autres
- auth: {
- user: process.env.MAIL_USER, // Ton adresse email
- pass: process.env.MAIL_PASS // Ton mot de passe ou App Password
- }
- });
-
- try {
- // =====================================================
- // 2. FORMATER LES DONNÉES
- // =====================================================
- // Convertir la confidence en pourcentage
- const confidencePercent = (parseFloat(signal.confidence) * 100).toFixed(2);
-
- // Choisir l'emoji selon l'action
- const emoji = getEmojiForAction(signal.action);
-
- // Choisir la couleur selon la criticité
- const color = getColorForCriticality(signal.criticality);
-
- // =====================================================
- // 3. CRÉER LE CONTENU DE L'EMAIL
- // =====================================================
- const mailOptions = {
- from: `"Wall-e-tte Bot 🤖" <${process.env.MAIL_USER}>`,
- to: to,
- subject: `${emoji} Alerte ${signal.criticality} : ${signal.action} sur ${signal.pair}`,
-
- // Version texte (pour clients email sans HTML)
- text: `
-🚨 ALERTE CRYPTO 🚨
-
-Action recommandée : ${signal.action}
-Paire : ${signal.pair}
-Confiance : ${confidencePercent}%
-Criticité : ${signal.criticality}
-
-Raison :
-${signal.reason}
-
----
-Wall-e-tte - Votre conseiller crypto
- `.trim(),
-
- // Version HTML (plus jolie)
- html: `
- <!DOCTYPE html>
- <html>
- <head>
- <style>
- body {
- font-family: Arial, sans-serif;
- background-color: #f4f4f4;
- margin: 0;
- padding: 0;
- }
- .container {
- max-width: 600px;
- margin: 20px auto;
- background-color: white;
- border-radius: 10px;
- overflow: hidden;
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
- }
- .header {
- background-color: ${color};
- color: white;
- padding: 20px;
- text-align: center;
- }
- .content {
- padding: 30px;
- }
- .alert-box {
- background-color: #f9f9f9;
- border-left: 4px solid ${color};
- padding: 15px;
- margin: 20px 0;
- }
- .footer {
- background-color: #333;
- color: white;
- text-align: center;
- padding: 15px;
- font-size: 12px;
- }
- .stat {
- display: inline-block;
- margin: 10px 20px;
- }
- .stat-label {
- color: #666;
- font-size: 12px;
- text-transform: uppercase;
- }
- .stat-value {
- font-size: 24px;
- font-weight: bold;
- color: ${color};
- }
- </style>
- </head>
- <body>
- <div class="container">
- <div class="header">
- <h1>${emoji} ALERTE ${signal.criticality}</h1>
- </div>
- <div class="content">
- <h2>Recommandation : ${signal.action}</h2>
- <p><strong>Paire :</strong> ${signal.pair}</p>
-
- <div class="stat">
- <div class="stat-label">Confiance</div>
- <div class="stat-value">${confidencePercent}%</div>
- </div>
-
- <div class="alert-box">
- <h3>Analyse :</h3>
- <p>${signal.reason}</p>
- </div>
-
- <p style="color: #666; font-size: 12px;">
- Timestamp : ${new Date().toLocaleString('fr-FR')}
- </p>
- </div>
- <div class="footer">
- Wall-e-tte - Votre conseiller crypto automatisé
- </div>
- </div>
- </body>
- </html>
- `
- };
-
- // =====================================================
- // 4. ENVOYER L'EMAIL
- // =====================================================
- const info = await transporter.sendMail(mailOptions);
-
- console.log(`Email envoyé avec succès !`);
- console.log(` → Destinataire : ${to}`);
- console.log(` → Message ID : ${info.messageId}`);
-
- return 'SENT';
-
- } catch (error) {
- // =====================================================
- // 5. GESTION DES ERREURS
- // =====================================================
- console.error('Erreur lors de l\'envoi de l\'email :');
- console.error(` → ${error.message}`);
-
- // Erreurs communes et solutions
- if (error.message.includes('authentication')) {
- console.error(' Vérifie MAIL_USER et MAIL_PASS dans .env');
- } else if (error.message.includes('ECONNREFUSED')) {
- console.error(' Vérifie MAIL_HOST et MAIL_PORT dans .env');
- }
-
- return 'FAILED';
- }
-}
-
-// =========================================================
-// FONCTIONS UTILITAIRES
-// =========================================================
-
-/**
- * Retourne un emoji selon l'action recommandée
- */
-function getEmojiForAction(action) {
- const emojis = {
- 'BUY': '🟢',
- 'SELL': '🔴',
- 'HOLD': '🟡',
- 'STOP_LOSS': '🛑'
- };
- return emojis[action] || '📊';
-}
-
-/**
- * Retourne une couleur selon le niveau de criticité
- */
-function getColorForCriticality(criticality) {
- const colors = {
- 'CRITICAL': '#dc3545', // Rouge
- 'WARNING': '#ffc107', // Orange/Jaune
- 'INFO': '#17a2b8' // Bleu
- };
- return colors[criticality] || '#6c757d';
-}
-
+// =========================================================\r
+// CANAL EMAIL - Notifications par email\r
+// =========================================================\r
+// RÔLE : Envoyer des alertes par email avec Nodemailer\r
+// =========================================================\r
+\r
+import nodemailer from 'nodemailer';\r
+\r
+// =========================================================\r
+// FONCTION PRINCIPALE : Envoyer une alerte par email\r
+// =========================================================\r
+/**\r
+ * Envoie une alerte par email\r
+ *\r
+ * @param {string} to - Adresse email du destinataire\r
+ * @param {Object} signal - Objet contenant les infos du signal\r
+ * @param {string} signal.action - BUY, SELL, HOLD, STOP_LOSS\r
+ * @param {string} signal.pair - Paire crypto (ex: BTC/EUR)\r
+ * @param {number} signal.confidence - Niveau de confiance (0-1)\r
+ * @param {string} signal.reason - Raison de l'alerte\r
+ * @param {string} signal.criticality - CRITICAL, WARNING, INFO\r
+ *\r
+ * @returns {Promise<string>} 'SENT' ou 'FAILED'\r
+ */\r
+export async function sendAlertEmail(to, signal) {\r
+\r
+ // =====================================================\r
+ // 1. CRÉER LE TRANSPORTEUR SMTP\r
+ // =====================================================\r
+ // Le transporteur est l'objet qui envoie les emails\r
+ // Il se connecte au serveur SMTP (ex: Gmail, Mailtrap, etc.)\r
+ const transporter = nodemailer.createTransport({\r
+ host: process.env.MAIL_HOST, // ex: smtp.gmail.com\r
+ port: parseInt(process.env.MAIL_PORT), // ex: 587\r
+ secure: process.env.MAIL_PORT === '465', // true pour le port 465, false pour les autres\r
+ auth: {\r
+ user: process.env.MAIL_USER, // Ton adresse email\r
+ pass: process.env.MAIL_PASS // Ton mot de passe ou App Password\r
+ }\r
+ });\r
+\r
+ try {\r
+ // =====================================================\r
+ // 2. FORMATER LES DONNÉES\r
+ // =====================================================\r
+ // Convertir la confidence en pourcentage\r
+ const confidencePercent = (parseFloat(signal.confidence) * 100).toFixed(2);\r
+\r
+ // Choisir l'emoji selon l'action\r
+ const emoji = getEmojiForAction(signal.action);\r
+\r
+ // Choisir la couleur selon la criticité\r
+ const color = getColorForCriticality(signal.criticality);\r
+\r
+ // =====================================================\r
+ // 3. CRÉER LE CONTENU DE L'EMAIL\r
+ // =====================================================\r
+ const mailOptions = {\r
+ from: `"Wall-e-tte Bot 🤖" <${process.env.MAIL_USER}>`,\r
+ to: to,\r
+ subject: `${emoji} Alerte ${signal.criticality} : ${signal.action} sur ${signal.pair}`,\r
+\r
+ // Version texte (pour clients email sans HTML)\r
+ text: `\r
+🚨 ALERTE CRYPTO 🚨\r
+\r
+Action recommandée : ${signal.action}\r
+Paire : ${signal.pair}\r
+Confiance : ${confidencePercent}%\r
+Criticité : ${signal.criticality}\r
+\r
+Raison :\r
+${signal.reason}\r
+\r
+---\r
+Wall-e-tte - Votre conseiller crypto\r
+ `.trim(),\r
+\r
+ // Version HTML (plus jolie)\r
+ html: `\r
+ <!DOCTYPE html>\r
+ <html>\r
+ <head>\r
+ <style>\r
+ body {\r
+ font-family: Arial, sans-serif;\r
+ background-color: #f4f4f4;\r
+ margin: 0;\r
+ padding: 0;\r
+ }\r
+ .container {\r
+ max-width: 600px;\r
+ margin: 20px auto;\r
+ background-color: white;\r
+ border-radius: 10px;\r
+ overflow: hidden;\r
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);\r
+ }\r
+ .header {\r
+ background-color: ${color};\r
+ color: white;\r
+ padding: 20px;\r
+ text-align: center;\r
+ }\r
+ .content {\r
+ padding: 30px;\r
+ }\r
+ .alert-box {\r
+ background-color: #f9f9f9;\r
+ border-left: 4px solid ${color};\r
+ padding: 15px;\r
+ margin: 20px 0;\r
+ }\r
+ .footer {\r
+ background-color: #333;\r
+ color: white;\r
+ text-align: center;\r
+ padding: 15px;\r
+ font-size: 12px;\r
+ }\r
+ .stat {\r
+ display: inline-block;\r
+ margin: 10px 20px;\r
+ }\r
+ .stat-label {\r
+ color: #666;\r
+ font-size: 12px;\r
+ text-transform: uppercase;\r
+ }\r
+ .stat-value {\r
+ font-size: 24px;\r
+ font-weight: bold;\r
+ color: ${color};\r
+ }\r
+ </style>\r
+ </head>\r
+ <body>\r
+ <div class="container">\r
+ <div class="header">\r
+ <h1>${emoji} ALERTE ${signal.criticality}</h1>\r
+ </div>\r
+ <div class="content">\r
+ <h2>Recommandation : ${signal.action}</h2>\r
+ <p><strong>Paire :</strong> ${signal.pair}</p>\r
+\r
+ <div class="stat">\r
+ <div class="stat-label">Confiance</div>\r
+ <div class="stat-value">${confidencePercent}%</div>\r
+ </div>\r
+\r
+ <div class="alert-box">\r
+ <h3>Analyse :</h3>\r
+ <p>${signal.reason}</p>\r
+ </div>\r
+\r
+ <p style="color: #666; font-size: 12px;">\r
+ Timestamp : ${new Date().toLocaleString('fr-FR')}\r
+ </p>\r
+ </div>\r
+ <div class="footer">\r
+ Wall-e-tte - Votre conseiller crypto automatisé\r
+ </div>\r
+ </div>\r
+ </body>\r
+ </html>\r
+ `\r
+ };\r
+\r
+ // =====================================================\r
+ // 4. ENVOYER L'EMAIL\r
+ // =====================================================\r
+ const info = await transporter.sendMail(mailOptions);\r
+\r
+ console.log(`Email envoyé avec succès !`);\r
+ console.log(` → Destinataire : ${to}`);\r
+ console.log(` → Message ID : ${info.messageId}`);\r
+\r
+ return 'SENT';\r
+\r
+ } catch (error) {\r
+ // =====================================================\r
+ // 5. GESTION DES ERREURS\r
+ // =====================================================\r
+ console.error('Erreur lors de l\'envoi de l\'email :');\r
+ console.error(` → ${error.message}`);\r
+\r
+ // Erreurs communes et solutions\r
+ if (error.message.includes('authentication')) {\r
+ console.error(' Vérifie MAIL_USER et MAIL_PASS dans .env');\r
+ } else if (error.message.includes('ECONNREFUSED')) {\r
+ console.error(' Vérifie MAIL_HOST et MAIL_PORT dans .env');\r
+ }\r
+\r
+ return 'FAILED';\r
+ }\r
+}\r
+\r
+// =========================================================\r
+// FONCTIONS UTILITAIRES\r
+// =========================================================\r
+\r
+/**\r
+ * Retourne un emoji selon l'action recommandée\r
+ */\r
+function getEmojiForAction(action) {\r
+ const emojis = {\r
+ 'BUY': '🟢',\r
+ 'SELL': '🔴',\r
+ 'HOLD': '🟡',\r
+ 'STOP_LOSS': '🛑'\r
+ };\r
+ return emojis[action] || '📊';\r
+}\r
+\r
+/**\r
+ * Retourne une couleur selon le niveau de criticité\r
+ */\r
+function getColorForCriticality(criticality) {\r
+ const colors = {\r
+ 'CRITICAL': '#dc3545', // Rouge\r
+ 'WARNING': '#ffc107', // Orange/Jaune\r
+ 'INFO': '#17a2b8' // Bleu\r
+ };\r
+ return colors[criticality] || '#6c757d';\r
+}\r
+\r
-// !!! À FAIRE !!!
-
-
-// =========================================================
-// CANAL TELEGRAM - Notifications via Telegram Bot
-// =========================================================
-// RÔLE : Envoyer des alertes via un bot Telegram
-// =========================================================
-
-// =========================================================
-// TODO : CONFIGURATION
-// =========================================================
-/*
- Pour utiliser Telegram, il faut :
-
- 1. Créer un bot Telegram :
- - Parler à @BotFather sur Telegram
- - Utiliser /newbot
- - Récupérer le TOKEN
-
- 2. Récupérer ton CHAT_ID :
- - Parler à ton bot
- - Aller sur : https://api.telegram.org/bot<TOKEN>/getUpdates
- - Récupérer ton chat_id dans le JSON
-
- 3. Ajouter dans .env :
- TELEGRAM_BOT_TOKEN=123456:ABC-DEF...
- TELEGRAM_CHAT_ID=123456789
-
- 4. Tester l'envoi :
- - Utiliser fetch() ou axios
- - POST vers https://api.telegram.org/bot<TOKEN>/sendMessage
-*/
-
-// =========================================================
-// FONCTION À IMPLÉMENTER
-// =========================================================
-/**
- * Envoie une alerte via Telegram
- *
- * @param {string} chatId - ID du chat Telegram
- * @param {Object} signal - Signal crypto
- * @returns {Promise<string>} 'SENT' ou 'FAILED'
- */
-export async function sendTelegramAlert(chatId, signal) {
- // TODO: À implémenter ensemble
-
- console.log('📱 Canal Telegram pas encore implémenté');
- console.log(` → chatId: ${chatId}`);
- console.log(` → signal: ${signal.action} sur ${signal.pair}`);
-
- // Pour l'instant, on retourne FAILED
- return 'FAILED';
-}
-
+// !!! À FAIRE !!!\r
+\r
+\r
+// =========================================================\r
+// CANAL TELEGRAM - Notifications via Telegram Bot\r
+// =========================================================\r
+// RÔLE : Envoyer des alertes via un bot Telegram\r
+// =========================================================\r
+\r
+// =========================================================\r
+// TODO : CONFIGURATION\r
+// =========================================================\r
+/*\r
+ Pour utiliser Telegram, il faut :\r
+\r
+ 1. Créer un bot Telegram :\r
+ - Parler à @BotFather sur Telegram\r
+ - Utiliser /newbot\r
+ - Récupérer le TOKEN\r
+\r
+ 2. Récupérer ton CHAT_ID :\r
+ - Parler à ton bot\r
+ - Aller sur : https://api.telegram.org/bot<TOKEN>/getUpdates\r
+ - Récupérer ton chat_id dans le JSON\r
+\r
+ 3. Ajouter dans .env :\r
+ TELEGRAM_BOT_TOKEN=123456:ABC-DEF...\r
+ TELEGRAM_CHAT_ID=123456789\r
+\r
+ 4. Tester l'envoi :\r
+ - Utiliser fetch() ou axios\r
+ - POST vers https://api.telegram.org/bot<TOKEN>/sendMessage\r
+*/\r
+\r
+// =========================================================\r
+// FONCTION À IMPLÉMENTER\r
+// =========================================================\r
+/**\r
+ * Envoie une alerte via Telegram\r
+ *\r
+ * @param {string} chatId - ID du chat Telegram\r
+ * @param {Object} signal - Signal crypto\r
+ * @returns {Promise<string>} 'SENT' ou 'FAILED'\r
+ */\r
+export async function sendTelegramAlert(chatId, signal) {\r
+ // TODO: À implémenter ensemble\r
+\r
+ console.log('📱 Canal Telegram pas encore implémenté');\r
+ console.log(` → chatId: ${chatId}`);\r
+ console.log(` → signal: ${signal.action} sur ${signal.pair}`);\r
+\r
+ // Pour l'instant, on retourne FAILED\r
+ return 'FAILED';\r
+}\r
+\r
-// =========================================================
-// CANAL WEB - Notifications temps réel via Socket.IO
-// =========================================================
-// RÔLE : Envoyer des alertes en temps réel aux clients
-// web (Océane) et mobile (Thibaud)
-// =========================================================
-// DÉPENDANCE : socketManager.js doit être initialisé
-// =========================================================
-
-import { sendAlertToUser, isUserConnected } from '../socketManager.js';
-
-/**
- * Envoie une alerte via WebSocket à un utilisateur
- *
- * @param {string} userId - ID de l'utilisateur
- * @param {Object} signal - Signal crypto à envoyer
- * @param {string} signal.action - 'BUY', 'SELL', ou 'HOLD'
- * @param {string} signal.pair - Paire crypto (ex: 'BTC/EUR')
- * @param {number} signal.confidence - Niveau de confiance (0-1)
- * @param {string} signal.reason - Raison du signal
- * @param {string} [signal.criticality] - 'INFO', 'WARNING', 'CRITICAL'
- * @returns {Promise<string>} 'SENT' ou 'FAILED'
- *
- * @example
- * const status = await sendWebAlert('user-123', {
- * action: 'BUY',
- * pair: 'BTC/EUR',
- * confidence: 0.85,
- * reason: 'RSI indique une survente',
- * criticality: 'WARNING'
- * });
- */
-export async function sendWebAlert(userId, signal) {
- // Vérifier si l'utilisateur est connecté
- if (!isUserConnected(userId)) {
- console.log(`⚠️ [Web Channel] User ${userId} non connecté - alerte non envoyée`);
- return 'FAILED';
- }
-
- // Formater l'alerte pour les clients
- const alertPayload = {
- // Données du signal
- action: signal.action,
- pair: signal.pair,
- confidence: signal.confidence,
- reason: signal.reason,
-
- // Métadonnées
- alertLevel: signal.criticality || 'INFO',
- timestamp: Date.now(),
-
- // Prix si disponible
- ...(signal.priceAtSignal && { price: signal.priceAtSignal })
- };
-
- // Envoyer via Socket.IO
- const sent = sendAlertToUser(userId, alertPayload);
-
- if (sent) {
- console.log(`✅ [Web Channel] Alerte envoyée à ${userId} : ${signal.action} ${signal.pair}`);
- return 'SENT';
- }
-
- return 'FAILED';
-}
-
-/**
- * Vérifie si le canal web est disponible pour un utilisateur
- * (utile pour le service avant de tenter l'envoi)
- *
- * @param {string} userId - ID de l'utilisateur
- * @returns {boolean} true si l'utilisateur peut recevoir des alertes web
- */
-export function isWebChannelAvailable(userId) {
- return isUserConnected(userId);
-}
+// =========================================================\r
+// CANAL WEB - Notifications temps réel via Socket.IO\r
+// =========================================================\r
+// RÔLE : Envoyer des alertes en temps réel aux clients\r
+// web (Océane) et mobile (Thibaud)\r
+// =========================================================\r
+// DÉPENDANCE : socketManager.js doit être initialisé\r
+// =========================================================\r
+\r
+import { sendAlertToUser, isUserConnected } from '../socketManager.js';\r
+\r
+/**\r
+ * Envoie une alerte via WebSocket à un utilisateur\r
+ *\r
+ * @param {string} userId - ID de l'utilisateur\r
+ * @param {Object} signal - Signal crypto à envoyer\r
+ * @param {string} signal.action - 'BUY', 'SELL', ou 'HOLD'\r
+ * @param {string} signal.pair - Paire crypto (ex: 'BTC/EUR')\r
+ * @param {number} signal.confidence - Niveau de confiance (0-1)\r
+ * @param {string} signal.reason - Raison du signal\r
+ * @param {string} [signal.criticality] - 'INFO', 'WARNING', 'CRITICAL'\r
+ * @returns {Promise<string>} 'SENT' ou 'FAILED'\r
+ * \r
+ * @example\r
+ * const status = await sendWebAlert('user-123', {\r
+ * action: 'BUY',\r
+ * pair: 'BTC/EUR',\r
+ * confidence: 0.85,\r
+ * reason: 'RSI indique une survente',\r
+ * criticality: 'WARNING'\r
+ * });\r
+ */\r
+export async function sendWebAlert(userId, signal) {\r
+ // Vérifier si l'utilisateur est connecté\r
+ if (!isUserConnected(userId)) {\r
+ console.log(`⚠️ [Web Channel] User ${userId} non connecté - alerte non envoyée`);\r
+ return 'FAILED';\r
+ }\r
+\r
+ // Formater l'alerte pour les clients\r
+ const alertPayload = {\r
+ // Données du signal\r
+ action: signal.action,\r
+ pair: signal.pair,\r
+ confidence: signal.confidence,\r
+ reason: signal.reason,\r
+ \r
+ // Métadonnées\r
+ alertLevel: signal.criticality || 'INFO',\r
+ timestamp: Date.now(),\r
+ \r
+ // Prix si disponible\r
+ ...(signal.priceAtSignal && { price: signal.priceAtSignal })\r
+ };\r
+\r
+ // Envoyer via Socket.IO\r
+ const sent = sendAlertToUser(userId, alertPayload);\r
+\r
+ if (sent) {\r
+ console.log(`✅ [Web Channel] Alerte envoyée à ${userId} : ${signal.action} ${signal.pair}`);\r
+ return 'SENT';\r
+ }\r
+\r
+ return 'FAILED';\r
+}\r
+\r
+/**\r
+ * Vérifie si le canal web est disponible pour un utilisateur\r
+ * (utile pour le service avant de tenter l'envoi)\r
+ * \r
+ * @param {string} userId - ID de l'utilisateur\r
+ * @returns {boolean} true si l'utilisateur peut recevoir des alertes web\r
+ */\r
+export function isWebChannelAvailable(userId) {\r
+ return isUserConnected(userId);\r
+}\r
-// =========================================================
-// MODULE ALERTS - Point d'entrée
-// =========================================================
-
-// Export des factory functions principales
-export { createAlertsRepo } from './alerts.repo.js';
-export { createAlertsService } from './alerts.service.js';
-export { createAlertsController } from './alerts.controller.js';
-export { createAlertsRouter } from './alerts.router.js';
-
-// Export des adapters (pour différentes bases de données)
-export { createMySQLAdapter } from './adapters/mysql.adapter.js';
-
-// Export du gestionnaire Socket.IO
-export {
- initSocketIO,
- sendAlertToUser,
- broadcastAlert,
- isUserConnected,
- getConnectedUsersCount,
- getIO
-} from './socketManager.js';
-
-// Export des canaux (utiles si on veut les tester séparément)
-export { sendAlertEmail } from './channels/mailer.js';
-export { sendConsoleAlert } from './channels/console.js';
-export { sendTelegramAlert } from './channels/telegram.js';
-export { sendDiscordAlert } from './channels/discord.js';
-export { sendWebAlert } from './channels/web.js';
-
-// =========================================================
-// EXEMPLE D'UTILISATION RAPIDE
-// =========================================================
-/*
- DANS WALL-E-TTE :
-
- // 1. Import du module
- import db from './config/db.js';
- import {
- createAlertsRepo,
- createAlertsService,
- createAlertsController,
- createAlertsRouter
- } from './modules/alerts/index.js';
-
- // 2. Initialiser le module (dans l'ordre !)
- const alertsRepo = createAlertsRepo(db);
- const alertsService = createAlertsService(alertsRepo);
- const alertsController = createAlertsController(alertsService, alertsRepo);
- const alertsRouter = createAlertsRouter(alertsController);
-
- // 3. Utiliser dans Express
- app.use('/api/alerts', alertsRouter);
-
- // 4. Ou utiliser directement le service
- await alertsService.processSignal(mySignal);
-
- ═══════════════════════════════════════════════════════
-
- DANS UN AUTRE PROJET :
-
- // Exactement pareil !
- import myDb from './my-db-config.js';
- import {
- createAlertsRepo,
- createAlertsService
- } from '@wall-e-tte/alerts-module';
-
- const repo = createAlertsRepo(myDb, {
- alertsTable: 'my_notifications',
- usersTable: 'my_users'
- });
- const service = createAlertsService(repo);
-
- await service.processSignal(signal); // Ça marche !
-*/
+// =========================================================\r
+// MODULE ALERTS - Point d'entrée\r
+// =========================================================\r
+\r
+// Export des factory functions principales\r
+export { createAlertsRepo } from './alerts.repo.js';\r
+export { createAlertsService } from './alerts.service.js';\r
+export { createAlertsController } from './alerts.controller.js';\r
+export { createAlertsRouter } from './alerts.router.js';\r
+\r
+// Export des adapters (pour différentes bases de données)\r
+export { createMySQLAdapter } from './adapters/mysql.adapter.js';\r
+\r
+// Export du gestionnaire Socket.IO\r
+export {\r
+ initSocketIO,\r
+ sendAlertToUser,\r
+ broadcastAlert,\r
+ isUserConnected,\r
+ getConnectedUsersCount,\r
+ getIO\r
+} from './socketManager.js';\r
+\r
+// Export des canaux (utiles si on veut les tester séparément)\r
+export { sendAlertEmail } from './channels/mailer.js';\r
+export { sendConsoleAlert } from './channels/console.js';\r
+export { sendTelegramAlert } from './channels/telegram.js';\r
+export { sendDiscordAlert } from './channels/discord.js';\r
+export { sendWebAlert } from './channels/web.js';\r
+\r
+// =========================================================\r
+// EXEMPLE D'UTILISATION RAPIDE\r
+// =========================================================\r
+/*\r
+ DANS WALL-E-TTE :\r
+\r
+ // 1. Import du module\r
+ import db from './config/db.js';\r
+ import {\r
+ createAlertsRepo,\r
+ createAlertsService,\r
+ createAlertsController,\r
+ createAlertsRouter\r
+ } from './modules/alerts/index.js';\r
+\r
+ // 2. Initialiser le module (dans l'ordre !)\r
+ const alertsRepo = createAlertsRepo(db);\r
+ const alertsService = createAlertsService(alertsRepo);\r
+ const alertsController = createAlertsController(alertsService, alertsRepo);\r
+ const alertsRouter = createAlertsRouter(alertsController);\r
+\r
+ // 3. Utiliser dans Express\r
+ app.use('/api/alerts', alertsRouter);\r
+\r
+ // 4. Ou utiliser directement le service\r
+ await alertsService.processSignal(mySignal);\r
+\r
+ ═══════════════════════════════════════════════════════\r
+\r
+ DANS UN AUTRE PROJET :\r
+\r
+ // Exactement pareil !\r
+ import myDb from './my-db-config.js';\r
+ import {\r
+ createAlertsRepo,\r
+ createAlertsService\r
+ } from '@wall-e-tte/alerts-module';\r
+\r
+ const repo = createAlertsRepo(myDb, {\r
+ alertsTable: 'my_notifications',\r
+ usersTable: 'my_users'\r
+ });\r
+ const service = createAlertsService(repo);\r
+\r
+ await service.processSignal(signal); // Ça marche !\r
+*/\r
-// =========================================================
-// SOCKET MANAGER - Gestion des connexions WebSocket
-// =========================================================
-// RÔLE : Centraliser la gestion des connexions Socket.IO
-// pour le module d'alertes
-// =========================================================
-// UTILISÉ PAR : web.js
-// =========================================================
-
-import { Server } from 'socket.io';
-
-// =========================================================
-// STOCKAGE DES CONNEXIONS
-// =========================================================
-// Map : userId → socket
-// Permet d'envoyer une alerte à un utilisateur spécifique
-const userSockets = new Map();
-
-// Instance du serveur Socket.IO
-let io = null;
-
-// =========================================================
-// INITIALISATION DU SERVEUR SOCKET.IO
-// =========================================================
-/**
- * Initialise Socket.IO sur un serveur HTTP existant
- *
- * @param {Object} httpServer - Serveur HTTP (Express)
- * @param {Object} options - Options Socket.IO (optionnel)
- * @returns {Server} Instance Socket.IO
- *
- * @example
- * // Dans le fichier principal du serveur (index.js)
- * import express from 'express';
- * import { createServer } from 'http';
- * import { initSocketIO } from './modules/alerts/socketManager.js';
- *
- * const app = express();
- * const httpServer = createServer(app);
- * const io = initSocketIO(httpServer);
- *
- * httpServer.listen(3000);
- */
-export function initSocketIO(httpServer, options = {}) {
- // Configuration par défaut + options personnalisées
- const defaultOptions = {
- cors: {
- origin: "*", // En prod, restreindre aux domaines autorisés
- methods: ["GET", "POST"]
- }
- };
-
- io = new Server(httpServer, { ...defaultOptions, ...options });
-
- // ─────────────────────────────────────────────────────
- // GESTION DES CONNEXIONS
- // ─────────────────────────────────────────────────────
- io.on('connection', (socket) => {
- console.log(` [Socket.IO] Nouvelle connexion : ${socket.id}`);
-
- // ─────────────────────────────────────────────────
- // EVENT : Authentification de l'utilisateur
- // ─────────────────────────────────────────────────
- // Le client doit envoyer son userId après connexion
- socket.on('auth', (userId) => {
- if (!userId) {
- console.log(` [Socket.IO] Auth sans userId (socket: ${socket.id})`);
- return;
- }
-
- // Stocker la correspondance userId → socket
- userSockets.set(userId, socket);
- socket.userId = userId; // Garder une référence sur le socket
-
- console.log(` [Socket.IO] Utilisateur authentifié : ${userId}`);
- console.log(` Utilisateurs connectés : ${userSockets.size}`);
-
- // Confirmer l'authentification au client
- socket.emit('auth_success', {
- userId,
- message: 'Connecté au système d\'alertes'
- });
- });
-
- // ─────────────────────────────────────────────────
- // EVENT : Déconnexion
- // ─────────────────────────────────────────────────
- socket.on('disconnect', (reason) => {
- if (socket.userId) {
- userSockets.delete(socket.userId);
- console.log(`🔌 [Socket.IO] Déconnexion : ${socket.userId} (${reason})`);
- console.log(` Utilisateurs connectés : ${userSockets.size}`);
- }
- });
-
- // ─────────────────────────────────────────────────
- // EVENT : Ping (test de connexion)
- // ─────────────────────────────────────────────────
- socket.on('ping_alerts', () => {
- socket.emit('pong_alerts', {
- timestamp: Date.now(),
- status: 'ok'
- });
- });
- });
-
- console.log(' [Socket.IO] Serveur initialisé');
- return io;
-}
-
-// =========================================================
-// FONCTIONS D'ENVOI D'ALERTES
-// =========================================================
-
-/**
- * Envoie une alerte à un utilisateur spécifique
- *
- * @param {string} userId - ID de l'utilisateur cible
- * @param {Object} alertData - Données de l'alerte
- * @returns {boolean} true si envoyé, false si utilisateur non connecté
- */
-export function sendAlertToUser(userId, alertData) {
- const socket = userSockets.get(userId);
-
- if (socket) {
- socket.emit('alert', {
- ...alertData,
- timestamp: Date.now()
- });
- console.log(` [Socket.IO] Alerte envoyée à ${userId}`);
- return true;
- }
-
- console.log(` [Socket.IO] Utilisateur ${userId} non connecté`);
- return false;
-}
-
-/**
- * Envoie une alerte à tous les utilisateurs connectés
- *
- * @param {Object} alertData - Données de l'alerte
- * @returns {number} Nombre d'utilisateurs notifiés
- */
-export function broadcastAlert(alertData) {
- if (!io) {
- console.error(' [Socket.IO] Serveur non initialisé');
- return 0;
- }
-
- io.emit('alert', {
- ...alertData,
- timestamp: Date.now()
- });
-
- console.log(` [Socket.IO] Alerte broadcast à ${userSockets.size} utilisateurs`);
- return userSockets.size;
-}
-
-// =========================================================
-// FONCTIONS UTILITAIRES
-// =========================================================
-
-/**
- * Vérifie si un utilisateur est connecté
- * @param {string} userId
- * @returns {boolean}
- */
-export function isUserConnected(userId) {
- return userSockets.has(userId);
-}
-
-/**
- * Retourne le nombre d'utilisateurs connectés
- * @returns {number}
- */
-export function getConnectedUsersCount() {
- return userSockets.size;
-}
-
-/**
- * Retourne la liste des userIds connectés
- * @returns {string[]}
- */
-export function getConnectedUserIds() {
- return Array.from(userSockets.keys());
-}
-
-/**
- * Retourne l'instance Socket.IO (pour usage avancé)
- * @returns {Server|null}
- */
-export function getIO() {
- return io;
-}
+// =========================================================\r
+// SOCKET MANAGER - Gestion des connexions WebSocket\r
+// =========================================================\r
+// RÔLE : Centraliser la gestion des connexions Socket.IO\r
+// pour le module d'alertes\r
+// =========================================================\r
+// UTILISÉ PAR : web.js\r
+// =========================================================\r
+\r
+import { Server } from 'socket.io';\r
+\r
+// =========================================================\r
+// STOCKAGE DES CONNEXIONS\r
+// =========================================================\r
+// Map : userId → socket\r
+// Permet d'envoyer une alerte à un utilisateur spécifique\r
+const userSockets = new Map();\r
+\r
+// Instance du serveur Socket.IO\r
+let io = null;\r
+\r
+// =========================================================\r
+// INITIALISATION DU SERVEUR SOCKET.IO\r
+// =========================================================\r
+/**\r
+ * Initialise Socket.IO sur un serveur HTTP existant\r
+ *\r
+ * @param {Object} httpServer - Serveur HTTP (Express)\r
+ * @param {Object} options - Options Socket.IO (optionnel)\r
+ * @returns {Server} Instance Socket.IO\r
+ *\r
+ * @example\r
+ * // Dans le fichier principal du serveur (index.js)\r
+ * import express from 'express';\r
+ * import { createServer } from 'http';\r
+ * import { initSocketIO } from './modules/alerts/socketManager.js';\r
+ *\r
+ * const app = express();\r
+ * const httpServer = createServer(app);\r
+ * const io = initSocketIO(httpServer);\r
+ *\r
+ * httpServer.listen(3000);\r
+ */\r
+export function initSocketIO(httpServer, options = {}) {\r
+ // Configuration par défaut + options personnalisées\r
+ const defaultOptions = {\r
+ cors: {\r
+ origin: "*", // En prod, restreindre aux domaines autorisés\r
+ methods: ["GET", "POST"]\r
+ }\r
+ };\r
+\r
+ io = new Server(httpServer, { ...defaultOptions, ...options });\r
+\r
+ // ─────────────────────────────────────────────────────\r
+ // GESTION DES CONNEXIONS\r
+ // ─────────────────────────────────────────────────────\r
+ io.on('connection', (socket) => {\r
+ console.log(` [Socket.IO] Nouvelle connexion : ${socket.id}`);\r
+\r
+ // ─────────────────────────────────────────────────\r
+ // EVENT : Authentification de l'utilisateur\r
+ // ─────────────────────────────────────────────────\r
+ // Le client doit envoyer son userId après connexion\r
+ socket.on('auth', (userId) => {\r
+ if (!userId) {\r
+ console.log(` [Socket.IO] Auth sans userId (socket: ${socket.id})`);\r
+ return;\r
+ }\r
+\r
+ // Stocker la correspondance userId → socket\r
+ userSockets.set(userId, socket);\r
+ socket.userId = userId; // Garder une référence sur le socket\r
+\r
+ console.log(` [Socket.IO] Utilisateur authentifié : ${userId}`);\r
+ console.log(` Utilisateurs connectés : ${userSockets.size}`);\r
+\r
+ // Confirmer l'authentification au client\r
+ socket.emit('auth_success', {\r
+ userId,\r
+ message: 'Connecté au système d\'alertes'\r
+ });\r
+ });\r
+\r
+ // ─────────────────────────────────────────────────\r
+ // EVENT : Déconnexion\r
+ // ─────────────────────────────────────────────────\r
+ socket.on('disconnect', (reason) => {\r
+ if (socket.userId) {\r
+ userSockets.delete(socket.userId);\r
+ console.log(`🔌 [Socket.IO] Déconnexion : ${socket.userId} (${reason})`);\r
+ console.log(` Utilisateurs connectés : ${userSockets.size}`);\r
+ }\r
+ });\r
+\r
+ // ─────────────────────────────────────────────────\r
+ // EVENT : Ping (test de connexion)\r
+ // ─────────────────────────────────────────────────\r
+ socket.on('ping_alerts', () => {\r
+ socket.emit('pong_alerts', {\r
+ timestamp: Date.now(),\r
+ status: 'ok'\r
+ });\r
+ });\r
+ });\r
+\r
+ console.log(' [Socket.IO] Serveur initialisé');\r
+ return io;\r
+}\r
+\r
+// =========================================================\r
+// FONCTIONS D'ENVOI D'ALERTES\r
+// =========================================================\r
+\r
+/**\r
+ * Envoie une alerte à un utilisateur spécifique\r
+ *\r
+ * @param {string} userId - ID de l'utilisateur cible\r
+ * @param {Object} alertData - Données de l'alerte\r
+ * @returns {boolean} true si envoyé, false si utilisateur non connecté\r
+ */\r
+export function sendAlertToUser(userId, alertData) {\r
+ const socket = userSockets.get(userId);\r
+\r
+ if (socket) {\r
+ socket.emit('alert', {\r
+ ...alertData,\r
+ timestamp: Date.now()\r
+ });\r
+ console.log(` [Socket.IO] Alerte envoyée à ${userId}`);\r
+ return true;\r
+ }\r
+\r
+ console.log(` [Socket.IO] Utilisateur ${userId} non connecté`);\r
+ return false;\r
+}\r
+\r
+/**\r
+ * Envoie une alerte à tous les utilisateurs connectés\r
+ *\r
+ * @param {Object} alertData - Données de l'alerte\r
+ * @returns {number} Nombre d'utilisateurs notifiés\r
+ */\r
+export function broadcastAlert(alertData) {\r
+ if (!io) {\r
+ console.error(' [Socket.IO] Serveur non initialisé');\r
+ return 0;\r
+ }\r
+\r
+ io.emit('alert', {\r
+ ...alertData,\r
+ timestamp: Date.now()\r
+ });\r
+\r
+ console.log(` [Socket.IO] Alerte broadcast à ${userSockets.size} utilisateurs`);\r
+ return userSockets.size;\r
+}\r
+\r
+// =========================================================\r
+// FONCTIONS UTILITAIRES\r
+// =========================================================\r
+\r
+/**\r
+ * Vérifie si un utilisateur est connecté\r
+ * @param {string} userId\r
+ * @returns {boolean}\r
+ */\r
+export function isUserConnected(userId) {\r
+ return userSockets.has(userId);\r
+}\r
+\r
+/**\r
+ * Retourne le nombre d'utilisateurs connectés\r
+ * @returns {number}\r
+ */\r
+export function getConnectedUsersCount() {\r
+ return userSockets.size;\r
+}\r
+\r
+/**\r
+ * Retourne la liste des userIds connectés\r
+ * @returns {string[]}\r
+ */\r
+export function getConnectedUserIds() {\r
+ return Array.from(userSockets.keys());\r
+}\r
+\r
+/**\r
+ * Retourne l'instance Socket.IO (pour usage avancé)\r
+ * @returns {Server|null}\r
+ */\r
+export function getIO() {\r
+ return io;\r
+}\r
-// =========================================================
-// SCRIPT DE TEST - Module Alerts RÉUTILISABLE
-// =========================================================
-// Ce script teste ton module d'alertes avec la nouvelle
-// architecture réutilisable (injection de dépendances)
-// =========================================================
-// USAGE : node test-alerts.js
-// =========================================================
-
-import dotenv from 'dotenv';
-import path from 'path';
-import { fileURLToPath } from 'url';
-
-// Charger les variables d'environnement
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = path.dirname(__filename);
-dotenv.config({ path: path.resolve(__dirname, '.env') });
-
-// =========================================================
-// INITIALISATION DU MODULE
-// =========================================================
-import db from './config/db.js';
-import { createMySQLAdapter } from './modules/alerts/adapters/mysql.adapter.js';
-import { createAlertsRepo } from './modules/alerts/alerts.repo.js';
-import { createAlertsService } from './modules/alerts/alerts.service.js';
-
-// 1. Créer l'adapter MySQL avec la connexion DB
-const mysqlAdapter = createMySQLAdapter(db);
-
-// 2. Créer le repository avec l'adapter
-const alertsRepo = createAlertsRepo(mysqlAdapter);
-
-// 3. Créer le service avec le repository
-const alertsService = createAlertsService(alertsRepo);
-
-// =========================================================
-// FONCTION DE TEST PRINCIPALE
-// =========================================================
-async function runTest() {
- console.log('\n' + '═'.repeat(80));
- console.log('TEST DU MODULE ALERTS');
- console.log('═'.repeat(80) + '\n');
-
- try {
- // =========================================================
- // ÉTAPE 1 : Vérifier la connexion DB
- // =========================================================
- console.log('Test de connexion à la base de données\n');
-
- // Utilise le repository créé avec injection de dépendances
- const rules = await alertsRepo.findActiveRulesForSignal('test', 1);
- console.log(`Connexion DB OK`);
- console.log(` Règles trouvées : ${rules.length}\n`);
-
- if (rules.length === 0) {
- console.log('ATTENTION : Aucune règle trouvée en DB');
- }
-
- // =========================================================
- // ÉTAPE 2 : Test du canal CONSOLE
- // =========================================================
- console.log('─'.repeat(80));
- console.log('Test du canal CONSOLE\n');
-
- const { sendConsoleAlert } = await import('./modules/alerts/channels/console.js');
-
- const testSignalConsole = {
- action: 'BUY',
- pair: 'BTC/EUR',
- confidence: 0.87,
- criticality: 'WARNING',
- reason: 'Test du canal console : Les indicateurs techniques montrent une tendance haussière. Le RSI est à 45, le MACD vient de croiser.'
- };
-
- await sendConsoleAlert(testSignalConsole);
- console.log('Test CONSOLE réussi\n');
-
- // =========================================================
- // ÉTAPE 3 : Test du canal EMAIL
- // =========================================================
- console.log('─'.repeat(80));
- console.log('Test du canal EMAIL\n');
-
- if (!process.env.MAIL_USER || !process.env.MAIL_PASS) {
- console.log('Email non configuré (MAIL_USER et MAIL_PASS manquants dans .env)');
- console.log(' Pour tester configurez vos variables dans .env\n');
- } else {
- console.log('Configuration email détectée');
- console.log(` Host : ${process.env.MAIL_HOST}`);
- console.log(` User : ${process.env.MAIL_USER}`);
- console.log(' Envoi d\'un email de test...\n');
-
- const { sendAlertEmail } = await import('./modules/alerts/channels/mailer.js');
-
- const testSignalEmail = {
- action: 'SELL',
- pair: 'ETH/EUR',
- confidence: 0.92,
- criticality: 'CRITICAL',
- reason: 'Test du canal email : Signal de vente critique détecté. Forte baisse imminente selon les indicateurs.'
- };
-
- const status = await sendAlertEmail(process.env.MAIL_USER, testSignalEmail);
-
- if (status === 'SENT') {
- console.log('Test EMAIL réussi ! Vérifiez votre boîte mail.\n');
- } else {
- console.log('Test EMAIL échoué. Vérifiez votre configuration.\n');
- }
- }
-
- // =========================================================
- // ÉTAPE 4 : Test du service complet
- // =========================================================
- if (rules.length > 0) {
- console.log('─'.repeat(80));
- console.log('Test du service complet\n');
-
- const testSignalService = {
- userId: rules[0].user_id,
- pairId: rules[0].pair_id || 1,
- pair: 'BTC/EUR',
- action: 'BUY',
- confidence: 0.85,
- criticality: 'INFO',
- reason: 'Test complet du service : Signal généré automatiquement pour tester le workflow complet.',
- priceAtSignal: 45000.50
- };
-
- // Utilise le service créé avec injection de dépendances
- await alertsService.processSignal(testSignalService);
- console.log('Test SERVICE complet réussi !\n');
- }
-
- // =========================================================
- // RÉSUMÉ FINAL
- // =========================================================
- console.log('═'.repeat(80));
- console.log('TOUS LES TESTS SONT TERMINÉS');
- console.log('═'.repeat(80));
- console.log('\n RÉSUMÉ :');
- console.log(' Connexion DB');
- console.log(' Canal CONSOLE');
- console.log(` ${process.env.MAIL_USER ? 'OK : ' : 'FAIL : '} Canal EMAIL ${process.env.MAIL_USER ? '' : '(non configuré)'}`);
- console.log(` ${rules.length > 0 ? 'OK : ' : 'FAIL : '} Service complet ${rules.length > 0 ? '' : '(pas de règles en DB)'}`);
-
- } catch (error) {
- console.error('\n ERREUR LORS DU TEST :');
- console.error(error);
- console.error('\n VÉRIFICATIONS :');
- console.error(' - Le fichier .env existe et contient les bonnes valeurs ?');
- console.error(' - La base de données est démarrée ?');
- console.error(' - Les tables sont créées (schema.sql) ?');
- console.error('\n');
- } finally {
- // Quitter proprement
- process.exit();
- }
-}
-
-// =========================================================
-// LANCEMENT DU TEST
-// =========================================================
-runTest();
+// =========================================================\r
+// SCRIPT DE TEST - Module Alerts RÉUTILISABLE\r
+// =========================================================\r
+// Ce script teste ton module d'alertes avec la nouvelle\r
+// architecture réutilisable (injection de dépendances)\r
+// =========================================================\r
+// USAGE : node test-alerts.js\r
+// =========================================================\r
+\r
+import dotenv from 'dotenv';\r
+import path from 'path';\r
+import { fileURLToPath } from 'url';\r
+\r
+// Charger les variables d'environnement\r
+const __filename = fileURLToPath(import.meta.url);\r
+const __dirname = path.dirname(__filename);\r
+dotenv.config({ path: path.resolve(__dirname, '.env') });\r
+\r
+// =========================================================\r
+// INITIALISATION DU MODULE\r
+// =========================================================\r
+import db from './config/db.js';\r
+import { createMySQLAdapter } from './modules/alerts/adapters/mysql.adapter.js';\r
+import { createAlertsRepo } from './modules/alerts/alerts.repo.js';\r
+import { createAlertsService } from './modules/alerts/alerts.service.js';\r
+\r
+// 1. Créer l'adapter MySQL avec la connexion DB\r
+const mysqlAdapter = createMySQLAdapter(db);\r
+\r
+// 2. Créer le repository avec l'adapter\r
+const alertsRepo = createAlertsRepo(mysqlAdapter);\r
+\r
+// 3. Créer le service avec le repository\r
+const alertsService = createAlertsService(alertsRepo);\r
+\r
+// =========================================================\r
+// FONCTION DE TEST PRINCIPALE\r
+// =========================================================\r
+async function runTest() {\r
+ console.log('\n' + '═'.repeat(80));\r
+ console.log('TEST DU MODULE ALERTS');\r
+ console.log('═'.repeat(80) + '\n');\r
+\r
+ try {\r
+ // =========================================================\r
+ // ÉTAPE 1 : Vérifier la connexion DB\r
+ // =========================================================\r
+ console.log('Test de connexion à la base de données\n');\r
+\r
+ // Utilise le repository créé avec injection de dépendances\r
+ const rules = await alertsRepo.findActiveRulesForSignal('test', 1);\r
+ console.log(`Connexion DB OK`);\r
+ console.log(` Règles trouvées : ${rules.length}\n`);\r
+\r
+ if (rules.length === 0) {\r
+ console.log('ATTENTION : Aucune règle trouvée en DB');\r
+ }\r
+\r
+ // =========================================================\r
+ // ÉTAPE 2 : Test du canal CONSOLE\r
+ // =========================================================\r
+ console.log('─'.repeat(80));\r
+ console.log('Test du canal CONSOLE\n');\r
+\r
+ const { sendConsoleAlert } = await import('./modules/alerts/channels/console.js');\r
+\r
+ const testSignalConsole = {\r
+ action: 'BUY',\r
+ pair: 'BTC/EUR',\r
+ confidence: 0.87,\r
+ criticality: 'WARNING',\r
+ reason: 'Test du canal console : Les indicateurs techniques montrent une tendance haussière. Le RSI est à 45, le MACD vient de croiser.'\r
+ };\r
+\r
+ await sendConsoleAlert(testSignalConsole);\r
+ console.log('Test CONSOLE réussi\n');\r
+\r
+ // =========================================================\r
+ // ÉTAPE 3 : Test du canal EMAIL\r
+ // =========================================================\r
+ console.log('─'.repeat(80));\r
+ console.log('Test du canal EMAIL\n');\r
+\r
+ if (!process.env.MAIL_USER || !process.env.MAIL_PASS) {\r
+ console.log('Email non configuré (MAIL_USER et MAIL_PASS manquants dans .env)');\r
+ console.log(' Pour tester configurez vos variables dans .env\n');\r
+ } else {\r
+ console.log('Configuration email détectée');\r
+ console.log(` Host : ${process.env.MAIL_HOST}`);\r
+ console.log(` User : ${process.env.MAIL_USER}`);\r
+ console.log(' Envoi d\'un email de test...\n');\r
+\r
+ const { sendAlertEmail } = await import('./modules/alerts/channels/mailer.js');\r
+\r
+ const testSignalEmail = {\r
+ action: 'SELL',\r
+ pair: 'ETH/EUR',\r
+ confidence: 0.92,\r
+ criticality: 'CRITICAL',\r
+ reason: 'Test du canal email : Signal de vente critique détecté. Forte baisse imminente selon les indicateurs.'\r
+ };\r
+\r
+ const status = await sendAlertEmail(process.env.MAIL_USER, testSignalEmail);\r
+\r
+ if (status === 'SENT') {\r
+ console.log('Test EMAIL réussi ! Vérifiez votre boîte mail.\n');\r
+ } else {\r
+ console.log('Test EMAIL échoué. Vérifiez votre configuration.\n');\r
+ }\r
+ }\r
+\r
+ // =========================================================\r
+ // ÉTAPE 4 : Test du service complet\r
+ // =========================================================\r
+ if (rules.length > 0) {\r
+ console.log('─'.repeat(80));\r
+ console.log('Test du service complet\n');\r
+\r
+ const testSignalService = {\r
+ userId: rules[0].user_id,\r
+ pairId: rules[0].pair_id || 1,\r
+ pair: 'BTC/EUR',\r
+ action: 'BUY',\r
+ confidence: 0.85,\r
+ criticality: 'INFO',\r
+ reason: 'Test complet du service : Signal généré automatiquement pour tester le workflow complet.',\r
+ priceAtSignal: 45000.50\r
+ };\r
+\r
+ // Utilise le service créé avec injection de dépendances\r
+ await alertsService.processSignal(testSignalService);\r
+ console.log('Test SERVICE complet réussi !\n');\r
+ }\r
+\r
+ // =========================================================\r
+ // RÉSUMÉ FINAL\r
+ // =========================================================\r
+ console.log('═'.repeat(80));\r
+ console.log('TOUS LES TESTS SONT TERMINÉS');\r
+ console.log('═'.repeat(80));\r
+ console.log('\n RÉSUMÉ :');\r
+ console.log(' Connexion DB');\r
+ console.log(' Canal CONSOLE');\r
+ console.log(` ${process.env.MAIL_USER ? 'OK : ' : 'FAIL : '} Canal EMAIL ${process.env.MAIL_USER ? '' : '(non configuré)'}`);\r
+ console.log(` ${rules.length > 0 ? 'OK : ' : 'FAIL : '} Service complet ${rules.length > 0 ? '' : '(pas de règles en DB)'}`);\r
+\r
+ } catch (error) {\r
+ console.error('\n ERREUR LORS DU TEST :');\r
+ console.error(error);\r
+ console.error('\n VÉRIFICATIONS :');\r
+ console.error(' - Le fichier .env existe et contient les bonnes valeurs ?');\r
+ console.error(' - La base de données est démarrée ?');\r
+ console.error(' - Les tables sont créées (schema.sql) ?');\r
+ console.error('\n');\r
+ } finally {\r
+ // Quitter proprement\r
+ process.exit();\r
+ }\r
+}\r
+\r
+// =========================================================\r
+// LANCEMENT DU TEST\r
+// =========================================================\r
+runTest();\r
-// =========================================================
-// INITIALISATION DU MODULE ALERTS
-// =========================================================
-// Ce fichier montre comment initialiser et utiliser
-// le module d'alertes dans Wall-e-tte
-// =========================================================
-
-import db from '../config/db.js';
-import {
- createAlertsController,
- createAlertsRepo,
- createAlertsRouter,
- createAlertsService,
- createMySQLAdapter
-} from './alerts/index.js';
-
-// =========================================================
-// FONCTION D'INITIALISATION
-// =========================================================
-/**
- * Initialise tout le module d'alertes
- *
- * @param {Object} dbConnection - Connexion à la base de données
- * @param {Object} options - Options de configuration
- * @returns {Object} { repo, service, controller, router }
- */
-export function initializeAlertsModule(dbConnection = db, options = {}) {
-
- console.log('\n' + '═'.repeat(80)); // Pour creer une ligne de separation horizontale | /n pour saut de ligne
- console.log('INITIALISATION DU MODULE ALERTS');
- console.log('═'.repeat(80) + '\n');
-
- // ─────────────────────────────────────────────────────
- // ÉTAPE 1 : Créer l'Adapter MySQL
- // ─────────────────────────────────────────────────────
- // L'adapter contient les requêtes SQL spécifiques
- const adapter = createMySQLAdapter(dbConnection, {
- alertsTable: options.alertsTable || 'alert_rules',
- usersTable: options.usersTable || 'users',
- eventsTable: options.eventsTable || 'alert_events'
- });
-
- // ─────────────────────────────────────────────────────
- // ÉTAPE 2 : Créer le Repository
- // ─────────────────────────────────────────────────────
- // Le repository gère les requêtes SQL
- const alertsRepo = createAlertsRepo(adapter, {
- alertsTable: options.alertsTable || 'alert_rules',
- usersTable: options.usersTable || 'users',
- eventsTable: options.eventsTable || 'alert_events'
- });
-
- // ─────────────────────────────────────────────────────
- // ÉTAPE 3 : Créer le Service
- // ─────────────────────────────────────────────────────
- // Le service contient la logique métier
- const alertsService = createAlertsService(alertsRepo, {
- defaultCooldown: options.defaultCooldown || 60000
- });
-
- // ─────────────────────────────────────────────────────
- // ÉTAPE 4 : Créer le Controller
- // ─────────────────────────────────────────────────────
- // Le controller gère les requêtes HTTP
- // (Plus besoin de db car le CRUD passe par le repo)
- const alertsController = createAlertsController(alertsService, alertsRepo);
-
- // ─────────────────────────────────────────────────────
- // ÉTAPE 5 : Créer le Router
- // ─────────────────────────────────────────────────────
- // Le router définit les routes API
- const alertsRouter = createAlertsRouter(alertsController);
-
- console.log('Module Alerts complètement initialisé !');
- console.log('═'.repeat(80) + '\n');
-
- // ─────────────────────────────────────────────────────
- // RETOURNER TOUT
- // ─────────────────────────────────────────────────────
- return {
- adapter,
- repo: alertsRepo,
- service: alertsService,
- controller: alertsController,
- router: alertsRouter
- };
-}
-
-// =========================================================
-// EXPORT PAR DÉFAUT
-// =========================================================
-// Pour Wall-e-tte, initialiser avec la DB par défaut
-export default initializeAlertsModule();
-
-// =========================================================
-// EXEMPLE D'UTILISATION
-// =========================================================
-/*
- UTILISATION SIMPLE (dans Wall-e-tte) :
-
- // Option 1 : Import par défaut (utilise la DB par défaut)
- import alerts from './modules/init-alerts.js';
-
- app.use('/api/alerts', alerts.router);
- await alerts.service.processSignal(signal);
-
-================================================================================
-
- // Option 2 : Import avec configuration personnalisée
- import { initializeAlertsModule } from './modules/init-alerts.js';
- import myDb from './my-db.js';
-
- const alerts = initializeAlertsModule(myDb, {
- alertsTable: 'mes_alertes',
- defaultCooldown: 120000 // 2 minutes
- });
-
- app.use('/api/alerts', alerts.router);
-
-================================================================================
-
- UTILISATION DANS UN AUTRE PROJET :
-
- // Copier ce fichier et changer juste l'import de 'db'
- import strangerDb from './stranger-db-config.js';
- import {
- createAlertsRepo,
- createAlertsService
- } from '@wall-e-tte/alerts';
-
- const repo = createAlertsRepo(strangerDb);
- const service = createAlertsService(repo);
-
-*/
+// =========================================================\r
+// INITIALISATION DU MODULE ALERTS\r
+// =========================================================\r
+// Ce fichier montre comment initialiser et utiliser\r
+// le module d'alertes dans Wall-e-tte\r
+// =========================================================\r
+\r
+import db from '../config/db.js';\r
+import {\r
+ createAlertsController,\r
+ createAlertsRepo,\r
+ createAlertsRouter,\r
+ createAlertsService,\r
+ createMySQLAdapter\r
+} from './alerts/index.js';\r
+\r
+// =========================================================\r
+// FONCTION D'INITIALISATION\r
+// =========================================================\r
+/**\r
+ * Initialise tout le module d'alertes\r
+ *\r
+ * @param {Object} dbConnection - Connexion à la base de données\r
+ * @param {Object} options - Options de configuration\r
+ * @returns {Object} { repo, service, controller, router }\r
+ */\r
+export function initializeAlertsModule(dbConnection = db, options = {}) {\r
+\r
+ console.log('\n' + '═'.repeat(80)); // Pour creer une ligne de separation horizontale | /n pour saut de ligne\r
+ console.log('INITIALISATION DU MODULE ALERTS');\r
+ console.log('═'.repeat(80) + '\n');\r
+\r
+ // ─────────────────────────────────────────────────────\r
+ // ÉTAPE 1 : Créer l'Adapter MySQL\r
+ // ─────────────────────────────────────────────────────\r
+ // L'adapter contient les requêtes SQL spécifiques\r
+ const adapter = createMySQLAdapter(dbConnection, {\r
+ alertsTable: options.alertsTable || 'alert_rules',\r
+ usersTable: options.usersTable || 'users',\r
+ eventsTable: options.eventsTable || 'alert_events'\r
+ });\r
+\r
+ // ─────────────────────────────────────────────────────\r
+ // ÉTAPE 2 : Créer le Repository\r
+ // ─────────────────────────────────────────────────────\r
+ // Le repository gère les requêtes SQL\r
+ const alertsRepo = createAlertsRepo(adapter, {\r
+ alertsTable: options.alertsTable || 'alert_rules',\r
+ usersTable: options.usersTable || 'users',\r
+ eventsTable: options.eventsTable || 'alert_events'\r
+ });\r
+\r
+ // ─────────────────────────────────────────────────────\r
+ // ÉTAPE 3 : Créer le Service\r
+ // ─────────────────────────────────────────────────────\r
+ // Le service contient la logique métier\r
+ const alertsService = createAlertsService(alertsRepo, {\r
+ defaultCooldown: options.defaultCooldown || 60000\r
+ });\r
+\r
+ // ─────────────────────────────────────────────────────\r
+ // ÉTAPE 4 : Créer le Controller\r
+ // ─────────────────────────────────────────────────────\r
+ // Le controller gère les requêtes HTTP\r
+ // (Plus besoin de db car le CRUD passe par le repo)\r
+ const alertsController = createAlertsController(alertsService, alertsRepo);\r
+\r
+ // ─────────────────────────────────────────────────────\r
+ // ÉTAPE 5 : Créer le Router\r
+ // ─────────────────────────────────────────────────────\r
+ // Le router définit les routes API\r
+ const alertsRouter = createAlertsRouter(alertsController);\r
+\r
+ console.log('Module Alerts complètement initialisé !');\r
+ console.log('═'.repeat(80) + '\n');\r
+\r
+ // ─────────────────────────────────────────────────────\r
+ // RETOURNER TOUT\r
+ // ─────────────────────────────────────────────────────\r
+ return {\r
+ adapter,\r
+ repo: alertsRepo,\r
+ service: alertsService,\r
+ controller: alertsController,\r
+ router: alertsRouter\r
+ };\r
+}\r
+\r
+// =========================================================\r
+// EXPORT PAR DÉFAUT\r
+// =========================================================\r
+// Pour Wall-e-tte, initialiser avec la DB par défaut\r
+export default initializeAlertsModule();\r
+\r
+// =========================================================\r
+// EXEMPLE D'UTILISATION\r
+// =========================================================\r
+/*\r
+ UTILISATION SIMPLE (dans Wall-e-tte) :\r
+\r
+ // Option 1 : Import par défaut (utilise la DB par défaut)\r
+ import alerts from './modules/init-alerts.js';\r
+\r
+ app.use('/api/alerts', alerts.router);\r
+ await alerts.service.processSignal(signal);\r
+\r
+================================================================================\r
+\r
+ // Option 2 : Import avec configuration personnalisée\r
+ import { initializeAlertsModule } from './modules/init-alerts.js';\r
+ import myDb from './my-db.js';\r
+\r
+ const alerts = initializeAlertsModule(myDb, {\r
+ alertsTable: 'mes_alertes',\r
+ defaultCooldown: 120000 // 2 minutes\r
+ });\r
+\r
+ app.use('/api/alerts', alerts.router);\r
+\r
+================================================================================\r
+\r
+ UTILISATION DANS UN AUTRE PROJET :\r
+\r
+ // Copier ce fichier et changer juste l'import de 'db'\r
+ import strangerDb from './stranger-db-config.js';\r
+ import {\r
+ createAlertsRepo,\r
+ createAlertsService\r
+ } from '@wall-e-tte/alerts';\r
+\r
+ const repo = createAlertsRepo(strangerDb);\r
+ const service = createAlertsService(repo);\r
+\r
+*/\r