// =========================================================\r
-// ALERTS CONTROLLER - VERSION RÉUTILISABLE\r
+// ALERTS CONTROLLER - CONTRAT API + CRUD\r
// =========================================================\r
-// RÔLE : Gestion des requêtes et réponses HTTP\r
+// Format de réponse uniforme :\r
+// Succès : { ok: true, data: { ... } }\r
+// Erreur : { ok: false, error: { code, message } }\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
+ * Crée un controller d'alertes selon le contrat API\r
+ * @param {Object} alertsService - Service d'alertes\r
+ * @param {Object} alertsRepo - Repository d'alertes\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
+ // =========================================================\r
+ // HELPERS - Format de réponse uniforme\r
+ // =========================================================\r
+\r
+ const sendSuccess = (res, data, status = 200) => {\r
+ res.status(status).json({ ok: true, data });\r
+ };\r
+\r
+ const sendError = (res, code, message, status = 400) => {\r
+ res.status(status).json({ ok: false, error: { code, message } });\r
+ };\r
+\r
return {\r
\r
// =====================================================\r
- // POST /api/alerts/process-signal\r
+ // CONTRAT API\r
// =====================================================\r
+\r
+ // GET /api/alerts?userId=...\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
+ * Liste les règles d'alerte d'un utilisateur\r
+ * Query params : userId (requis)\r
*/\r
- async processSignal(req, res) {\r
+ async getAlerts(req, res) {\r
try {\r
- // ==================================================================================\r
- // 1. RÉCUPÉRER LES DONNÉES DU BODY\r
- // ==================================================================================\r
- const signal = req.body;\r
+ const { userId } = req.query;\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
+ if (!userId) {\r
+ return sendError(res, 'MISSING_USER_ID', 'Le paramètre userId est requis', 400);\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
+ const alerts = await alertsRepo.findActiveRulesForSignal(userId, null);\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
+ sendSuccess(res, {\r
+ alerts,\r
+ count: alerts.length\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
+ } catch (error) {\r
+ console.error('Erreur getAlerts:', error);\r
+ sendError(res, 'SERVER_ERROR', error.message, 500);\r
+ }\r
+ },\r
+\r
+ // POST /api/alerts\r
+ /**\r
+ * Crée une nouvelle règle d'alerte\r
+ * Body : { userId, pairId, channel, minConfidence, severity, ... }\r
+ */\r
+ async createAlert(req, res) {\r
+ try {\r
+ const ruleData = req.body;\r
+\r
+ // Validation\r
+ if (!ruleData.userId) {\r
+ return sendError(res, 'MISSING_USER_ID', 'userId est requis', 400);\r
+ }\r
+ if (!ruleData.channel) {\r
+ return sendError(res, 'MISSING_CHANNEL', 'channel est requis', 400);\r
}\r
\r
- // =================================================================================\r
- // 3. APPELER LE SERVICE (injecté)\r
- // =================================================================================\r
- // Le service contient toute la logique\r
- await alertsService.processSignal(signal);\r
+ const newAlert = await alertsRepo.createRule(ruleData);\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
+ sendSuccess(res, { alert: newAlert }, 201);\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
+ console.error('Erreur createAlert:', error);\r
+ sendError(res, 'SERVER_ERROR', error.message, 500);\r
}\r
},\r
\r
- // =====================================================\r
- // GET /api/alerts/rules/:userId\r
- // =====================================================\r
+ // POST /api/alerts/:id/toggle\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
+ * Active ou désactive une règle d'alerte\r
+ * Params : id (requis)\r
*/\r
- async getRules(req, res) {\r
+ async toggleAlert(req, res) {\r
try {\r
- const { userId } = req.params;\r
+ const { id } = req.params;\r
\r
- // Validation\r
- if (!userId) {\r
- return res.status(400).json({\r
- success: false,\r
- error: 'userId manquant'\r
- });\r
+ // Récupérer la règle actuelle\r
+ const alert = await alertsRepo.findRuleById(id);\r
+\r
+ if (!alert) {\r
+ return sendError(res, 'ALERT_NOT_FOUND', 'Règle d\'alerte non trouvée', 404);\r
}\r
\r
- // Récupérer les règles (via repo injecté)\r
- const rules = await alertsRepo.findActiveRulesForSignal(userId, null);\r
+ // Inverser l'état enabled\r
+ const newEnabledState = !alert.enabled;\r
+ await alertsRepo.updateRule(id, { enabled: newEnabledState });\r
+\r
+ // Récupérer la règle mise à jour\r
+ const updatedAlert = await alertsRepo.findRuleById(id);\r
\r
- res.status(200).json({\r
- success: true,\r
- count: rules.length,\r
- rules: rules\r
+ sendSuccess(res, {\r
+ alert: updatedAlert,\r
+ enabled: newEnabledState\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
+ console.error('Erreur toggleAlert:', error);\r
+ sendError(res, 'SERVER_ERROR', error.message, 500);\r
}\r
},\r
\r
- // =====================================================\r
- // GET /api/alerts/history/:userId\r
- // =====================================================\r
+ // GET /api/alerts/events?userId=...&limit=...\r
/**\r
- * Récupère l'historique des alertes d'un utilisateur\r
+ * Récupère l'historique des alertes envoyées\r
+ * Query params : userId (requis), limit (optionnel, défaut 50)\r
*/\r
- async getHistory(req, res) {\r
+ async getEvents(req, res) {\r
try {\r
- const { userId } = req.params;\r
- const limit = parseInt(req.query.limit) || 50;\r
+ const { userId, limit } = req.query;\r
\r
if (!userId) {\r
- return res.status(400).json({\r
- success: false,\r
- error: 'userId manquant'\r
- });\r
+ return sendError(res, 'MISSING_USER_ID', 'Le paramètre userId est requis', 400);\r
}\r
\r
- // Récupérer l'historique (via repo injecté)\r
- const history = await alertsRepo.getAlertHistory(userId, limit);\r
+ const parsedLimit = parseInt(limit) || 50;\r
+ const events = await alertsRepo.getAlertHistory(userId, parsedLimit);\r
\r
- res.status(200).json({\r
- success: true,\r
- count: history.length,\r
- history: history\r
+ sendSuccess(res, {\r
+ events,\r
+ count: events.length,\r
+ limit: parsedLimit\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
+ console.error('Erreur getEvents:', error);\r
+ sendError(res, 'SERVER_ERROR', error.message, 500);\r
}\r
},\r
\r
- // =========================================================================================\r
- // CRUD\r
- //==========================================================================================\r
-\r
-\r
// =====================================================\r
- // POST /api/alerts/rules - CRÉER UNE RÈGLE\r
+ // CRUD COMPLET\r
// =====================================================\r
+\r
+ // GET /api/alerts/:id\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écupère le détail d'une règle\r
*/\r
- async createRule(req, res) {\r
+ async getAlertById(req, res) {\r
try {\r
- const ruleData = req.body;\r
+ const { id } = req.params;\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
+ const alert = await alertsRepo.findRuleById(id);\r
\r
- // Appeler le repo (qui appelle l'adapter)\r
- const newRule = await alertsRepo.createRule(ruleData);\r
+ if (!alert) {\r
+ return sendError(res, 'ALERT_NOT_FOUND', 'Règle d\'alerte non trouvée', 404);\r
+ }\r
\r
- res.status(201).json({\r
- success: true,\r
- rule: newRule,\r
- message: 'Règle créée avec succès'\r
- });\r
+ sendSuccess(res, { alert });\r
\r
} catch (error) {\r
- console.error('❌ Erreur createRule:', error);\r
- res.status(500).json({\r
- success: false,\r
- error: error.message\r
- });\r
+ console.error('❌ Erreur getAlertById:', error);\r
+ sendError(res, 'SERVER_ERROR', error.message, 500);\r
}\r
},\r
\r
- // =====================================================\r
- // PUT /api/alerts/rules/:id - MODIFIER UNE RÈGLE\r
- // =====================================================\r
+ // PUT /api/alerts/:id\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
+ * Modifie une règle d'alerte\r
+ * Body : { minConfidence, enabled, channel, severity, ... }\r
*/\r
- async updateRule(req, res) {\r
+ async updateAlert(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
+ return sendError(res, 'NO_DATA', 'Aucune donnée à mettre à jour', 400);\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
+ // Vérifier que la règle existe\r
+ const existingAlert = await alertsRepo.findRuleById(id);\r
+ if (!existingAlert) {\r
+ return sendError(res, 'ALERT_NOT_FOUND', 'Règle d\'alerte non trouvée', 404);\r
}\r
\r
- res.json({\r
- success: true,\r
- message: 'Règle mise à jour'\r
- });\r
+ await alertsRepo.updateRule(id, updates);\r
+\r
+ // Récupérer la règle mise à jour\r
+ const updatedAlert = await alertsRepo.findRuleById(id);\r
+\r
+ sendSuccess(res, { alert: updatedAlert });\r
\r
} catch (error) {\r
- console.error('❌ Erreur updateRule:', error);\r
- res.status(500).json({\r
- success: false,\r
- error: error.message\r
- });\r
+ console.error('Erreur updateAlert:', error);\r
+ sendError(res, 'SERVER_ERROR', error.message, 500);\r
}\r
},\r
\r
- // =====================================================\r
- // DELETE /api/alerts/rules/:id - SUPPRIMER UNE RÈGLE\r
- // =====================================================\r
- async deleteRule(req, res) {\r
+ // DELETE /api/alerts/:id\r
+ /**\r
+ * Supprime une règle d'alerte\r
+ */\r
+ async deleteAlert(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
+ return sendError(res, 'ALERT_NOT_FOUND', 'Règle d\'alerte non trouvée', 404);\r
}\r
\r
- res.json({\r
- success: true,\r
- message: 'Règle supprimée'\r
- });\r
+ sendSuccess(res, { deleted: true, id });\r
\r
} catch (error) {\r
- console.error('❌ Erreur deleteRule:', error);\r
- res.status(500).json({\r
- success: false,\r
- error: error.message\r
- });\r
+ console.error('❌ Erreur deleteAlert:', error);\r
+ sendError(res, 'SERVER_ERROR', error.message, 500);\r
}\r
},\r
\r
// =====================================================\r
- // GET /api/alerts/rules/detail/:id - DÉTAIL D'UNE RÈGLE\r
+ // ROUTE INTERNE (pour module Strategy)\r
// =====================================================\r
- async getRuleById(req, res) {\r
+\r
+ // POST /api/alerts/process-signal\r
+ /**\r
+ * Traite un signal du module Strategy\r
+ * Body : { userId, pairId, pair, action, confidence, criticality, reason }\r
+ */\r
+ async processSignal(req, res) {\r
try {\r
- const { id } = req.params;\r
+ const signal = req.body;\r
+\r
+ // Validation\r
+ const requiredFields = ['userId', 'pairId', 'pair', 'action', 'confidence', 'criticality'];\r
+ const missingFields = requiredFields.filter(field => signal[field] === undefined);\r
+\r
+ if (missingFields.length > 0) {\r
+ return sendError(\r
+ res,\r
+ 'MISSING_FIELDS',\r
+ `Champs manquants: ${missingFields.join(', ')}`,\r
+ 400\r
+ );\r
+ }\r
+\r
+ // Valider action\r
+ const validActions = ['BUY', 'SELL', 'HOLD', 'STOP_LOSS'];\r
+ if (!validActions.includes(signal.action)) {\r
+ return sendError(\r
+ res,\r
+ 'INVALID_ACTION',\r
+ `Action invalide. Valeurs acceptées: ${validActions.join(', ')}`,\r
+ 400\r
+ );\r
+ }\r
\r
- const rule = await alertsRepo.findRuleById(id);\r
+ // Valider criticality\r
+ const validCriticalities = ['CRITICAL', 'WARNING', 'INFO'];\r
+ if (!validCriticalities.includes(signal.criticality)) {\r
+ return sendError(\r
+ res,\r
+ 'INVALID_CRITICALITY',\r
+ `Criticality invalide. Valeurs acceptées: ${validCriticalities.join(', ')}`,\r
+ 400\r
+ );\r
+ }\r
\r
- if (!rule) {\r
- return res.status(404).json({\r
- success: false,\r
- error: 'Règle non trouvée'\r
- });\r
+ // Valider confidence\r
+ if (signal.confidence < 0 || signal.confidence > 1) {\r
+ return sendError(\r
+ res,\r
+ 'INVALID_CONFIDENCE',\r
+ 'Confidence doit être entre 0 et 1',\r
+ 400\r
+ );\r
}\r
\r
- res.json({\r
- success: true,\r
- rule\r
+ // Traiter le signal\r
+ await alertsService.processSignal(signal);\r
+\r
+ sendSuccess(res, {\r
+ processed: true,\r
+ signal: {\r
+ action: signal.action,\r
+ pair: signal.pair,\r
+ confidence: signal.confidence\r
+ }\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
+ console.error('Erreur processSignal:', error);\r
+ sendError(res, 'SERVER_ERROR', error.message, 500);\r
}\r
}\r
};\r
}\r
-\r
// =========================================================\r
-// ALERTS ROUTER - VERSION RÉUTILISABLE\r
+// ALERTS ROUTER - CONTRAT API + CRUD\r
// =========================================================\r
-// RÔLE : Définir les routes (endpoints) de l'API\r
+// Routes du contrat API :\r
+// GET /api/alerts?userId=... → Liste des alertes\r
+// POST /api/alerts → Créer une alerte\r
+// POST /api/alerts/:id/toggle → Activer/désactiver\r
+// GET /api/alerts/events?userId=... → Historique\r
+//\r
+// Routes supplémentaires (CRUD complet) :\r
+// GET /api/alerts/:id → Détail d'une alerte\r
+// PUT /api/alerts/:id → Modifier une alerte\r
+// DELETE /api/alerts/:id → Supprimer une alerte\r
+//\r
+// Route interne (pour module Strategy) :\r
+// POST /api/alerts/process-signal → Traiter un signal\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
+ * Crée un router d'alertes selon le contrat API\r
+ * @param {Object} alertsController - Controller d'alertes\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
+ // =========================================================\r
+ // CONTRAT API\r
+ // =========================================================\r
+\r
+ // GET /api/alerts?userId=...\r
+ // Liste les règles d'alerte d'un utilisateur\r
+ router.get('/', alertsController.getAlerts);\r
+\r
+ // POST /api/alerts\r
+ // Crée une nouvelle règle d'alerte\r
+ router.post('/', alertsController.createAlert);\r
+\r
+ // GET /api/alerts/events?userId=...&limit=...\r
+ // Récupère l'historique des alertes envoyées\r
+ // ⚠️ DOIT être AVANT /:id sinon "events" sera pris comme un id\r
+ router.get('/events', alertsController.getEvents);\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
+ // Traite un signal et envoie les notifications\r
+ // ⚠️ DOIT être AVANT /:id/toggle sinon "process-signal" sera pris comme un id\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
+ // POST /api/alerts/:id/toggle\r
+ // Active ou désactive une règle d'alerte\r
+ router.post('/:id/toggle', alertsController.toggleAlert);\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
+ // CRUD COMPLET (en plus du contrat)\r
+ // =========================================================\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
+ // GET /api/alerts/:id\r
+ // Détail d'une règle\r
+ router.get('/:id', alertsController.getAlertById);\r
+\r
+ // PUT /api/alerts/:id\r
+ // Modifier une règle\r
+ router.put('/:id', alertsController.updateAlert);\r
+\r
+ // DELETE /api/alerts/:id\r
+ // Supprimer une règle\r
+ router.delete('/:id', alertsController.deleteAlert);\r
\r
return router;\r
}\r
-\r