From 8ca10d0b35650b203c917c1fe5a55927662fd4dd Mon Sep 17 00:00:00 2001 From: Esteban Date: Sun, 1 Feb 2026 22:29:48 -0600 Subject: [PATCH] Notifications cronjob --- src/pages/Home.tsx | 2 +- water-api/src/controllers/test.controller.ts | 269 +++++++++++++++++++ water-api/src/jobs/negativeFlowDetection.ts | 23 +- water-api/src/routes/index.ts | 11 + water-api/src/routes/test.routes.ts | 69 +++++ 5 files changed, 367 insertions(+), 7 deletions(-) create mode 100644 water-api/src/controllers/test.controller.ts create mode 100644 water-api/src/routes/test.routes.ts diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 544fb42..95ba6aa 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -218,7 +218,7 @@ export default function Home({ Gestión de Recursos Hídricos diff --git a/water-api/src/controllers/test.controller.ts b/water-api/src/controllers/test.controller.ts new file mode 100644 index 0000000..09e3fd7 --- /dev/null +++ b/water-api/src/controllers/test.controller.ts @@ -0,0 +1,269 @@ +import { Request, Response } from 'express'; +import { triggerNegativeFlowDetection } from '../jobs/negativeFlowDetection'; +import { AuthenticatedRequest } from '../middleware/auth.middleware'; +import { query } from '../config/database'; + +/** + * POST /api/test/create-negative-flow-meter + * Create a test meter with negative flow for testing notifications + */ +export async function createTestMeterWithNegativeFlow(req: AuthenticatedRequest, res: Response): Promise { + try { + console.log('🧪 [Test] Creating test meter with negative flow...'); + + // Get first active concentrator + const concentratorResult = await query(` + SELECT id, project_id + FROM concentrators + WHERE status = 'ACTIVE' + LIMIT 1 + `); + + if (concentratorResult.rows.length === 0) { + res.status(400).json({ + success: false, + error: 'No active concentrators found', + message: 'Please create a concentrator first before creating test meter', + }); + return; + } + + const concentrator = concentratorResult.rows[0]; + + // Check if test meter already exists + const existingMeterResult = await query(` + SELECT id FROM meters WHERE serial_number = 'TEST-NEGATIVE-001' + `); + + let meterId: string; + let action: string; + + if (existingMeterResult.rows.length > 0) { + // Update existing test meter + meterId = existingMeterResult.rows[0].id; + action = 'updated'; + + await query(` + UPDATE meters + SET + last_reading_value = -25.75, + last_reading_at = CURRENT_TIMESTAMP, + status = 'ACTIVE', + updated_at = CURRENT_TIMESTAMP + WHERE id = $1 + `, [meterId]); + + console.log(`✅ [Test] Test meter updated: ${meterId}`); + } else { + // Create new test meter + action = 'created'; + + const createResult = await query(` + INSERT INTO meters ( + serial_number, + meter_id, + name, + concentrator_id, + location, + type, + status, + last_reading_value, + last_reading_at, + installation_date, + created_at, + updated_at + ) VALUES ( + 'TEST-NEGATIVE-001', + 'TEST-NEG-001', + 'Test Meter - Negative Flow', + $1, + 'Test Location - Building A', + 'LORA', + 'ACTIVE', + -25.75, + CURRENT_TIMESTAMP, + '2024-01-01', + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ) RETURNING id + `, [concentrator.id]); + + meterId = createResult.rows[0].id; + console.log(`✅ [Test] Test meter created: ${meterId}`); + } + + // Get the created/updated meter details + const meterDetails = await query(` + SELECT + m.id, + m.serial_number, + m.name, + m.last_reading_value, + m.status, + m.concentrator_id, + c.project_id + FROM meters m + JOIN concentrators c ON c.id = m.concentrator_id + WHERE m.id = $1 + `, [meterId]); + + res.status(200).json({ + success: true, + message: `Test meter ${action} successfully`, + data: { + action, + meter: meterDetails.rows[0], + instructions: { + next_step: 'Trigger the notification job', + endpoint: 'POST /api/test/trigger-negative-flow', + }, + }, + }); + } catch (error) { + console.error('❌ [Test] Error creating test meter:', error); + res.status(500).json({ + success: false, + error: 'Failed to create test meter', + message: error instanceof Error ? error.message : 'Unknown error', + }); + } +} + +/** + * POST /api/test/trigger-negative-flow + * Manually trigger the negative flow detection job for testing + * This endpoint simulates what the cron job does at 1:00 AM + */ +export async function triggerNegativeFlowJob(req: AuthenticatedRequest, res: Response): Promise { + try { + console.log('🧪 [Test] Manually triggering negative flow detection...'); + + await triggerNegativeFlowDetection(); + + res.status(200).json({ + success: true, + message: 'Negative flow detection job triggered successfully', + timestamp: new Date().toISOString(), + }); + } catch (error) { + console.error('❌ [Test] Error triggering negative flow detection:', error); + res.status(500).json({ + success: false, + error: 'Failed to trigger negative flow detection', + message: error instanceof Error ? error.message : 'Unknown error', + }); + } +} + +/** + * GET /api/test/negative-flow-meters + * Get list of meters with negative flow for testing + */ +export async function getNegativeFlowMeters(req: AuthenticatedRequest, res: Response): Promise { + try { + const { getMetersWithNegativeFlow } = await import('../services/notification.service'); + + const meters = await getMetersWithNegativeFlow(); + + res.status(200).json({ + success: true, + data: { + count: meters.length, + meters: meters, + }, + }); + } catch (error) { + console.error('❌ [Test] Error fetching negative flow meters:', error); + res.status(500).json({ + success: false, + error: 'Failed to fetch negative flow meters', + message: error instanceof Error ? error.message : 'Unknown error', + }); + } +} + +/** + * GET /api/test/notifications-info + * Get information about the notification system + */ +export async function getNotificationsInfo(req: AuthenticatedRequest, res: Response): Promise { + try { + // Count notifications by type + const typeCountResult = await query(` + SELECT notification_type, COUNT(*) as count + FROM notifications + GROUP BY notification_type + `); + + // Count unread notifications + const unreadResult = await query(` + SELECT COUNT(*) as count + FROM notifications + WHERE is_read = false + `); + + // Recent notifications + const recentResult = await query(` + SELECT * + FROM notifications + ORDER BY created_at DESC + LIMIT 10 + `); + + res.status(200).json({ + success: true, + data: { + totalUnread: parseInt(unreadResult.rows[0].count, 10), + byType: typeCountResult.rows, + recentNotifications: recentResult.rows, + }, + }); + } catch (error) { + console.error('❌ [Test] Error fetching notifications info:', error); + res.status(500).json({ + success: false, + error: 'Failed to fetch notifications info', + message: error instanceof Error ? error.message : 'Unknown error', + }); + } +} + +/** + * DELETE /api/test/cleanup-test-data + * Clean up test meter and notifications + */ +export async function cleanupTestData(req: AuthenticatedRequest, res: Response): Promise { + try { + console.log('🧹 [Test] Cleaning up test data...'); + + // Delete notifications for test meter + const notificationsResult = await query(` + DELETE FROM notifications + WHERE meter_serial_number = 'TEST-NEGATIVE-001' + RETURNING id + `); + + // Delete test meter + const meterResult = await query(` + DELETE FROM meters + WHERE serial_number = 'TEST-NEGATIVE-001' + RETURNING id + `); + + res.status(200).json({ + success: true, + message: 'Test data cleaned up successfully', + data: { + notifications_deleted: notificationsResult.rows.length, + meters_deleted: meterResult.rows.length, + }, + }); + } catch (error) { + console.error('❌ [Test] Error cleaning up test data:', error); + res.status(500).json({ + success: false, + error: 'Failed to clean up test data', + message: error instanceof Error ? error.message : 'Unknown error', + }); + } +} diff --git a/water-api/src/jobs/negativeFlowDetection.ts b/water-api/src/jobs/negativeFlowDetection.ts index 493bcd1..d5f269b 100644 --- a/water-api/src/jobs/negativeFlowDetection.ts +++ b/water-api/src/jobs/negativeFlowDetection.ts @@ -2,15 +2,20 @@ import cron from 'node-cron'; import * as notificationService from '../services/notification.service'; /** - * Cron job that runs daily at 1:00 AM to detect meters with negative flow - * and create notifications for responsible users + * Cron job that runs three times daily at 1:00 AM, 1:15 AM, and 1:30 AM PST + * to detect meters with negative flow and create notifications for responsible users + * + * Timezone: America/Los_Angeles (Pacific Standard Time) + * 1:00 AM PST = 3:00 AM CST (Jalisco, Mexico) + * 1:15 AM PST = 3:15 AM CST (Jalisco, Mexico) + * 1:30 AM PST = 3:30 AM CST (Jalisco, Mexico) */ export function scheduleNegativeFlowDetection(): void { - // Schedule: Every day at 1:00 AM + // Schedule: Every day at 1:00 AM, 1:15 AM, and 1:30 AM Pacific Standard Time // Cron format: minute hour day-of-month month day-of-week - // '0 1 * * *' = At 01:00 (1:00 AM) every day + // '0,15,30 1 * * *' = At 01:00, 01:15, and 01:30 (1:00 AM, 1:15 AM, 1:30 AM) every day - cron.schedule('0 1 * * *', async () => { + cron.schedule('0,15,30 1 * * *', async () => { console.log('🔍 [Cron] Starting negative flow detection job...'); const startTime = Date.now(); @@ -100,9 +105,15 @@ export function scheduleNegativeFlowDetection(): void { } catch (error) { console.error('❌ [Cron] Fatal error in negative flow detection job:', error); } + }, { + timezone: 'America/Los_Angeles' // Pacific Standard Time (PST/PDT) }); - console.log('⏰ [Cron] Negative flow detection job scheduled (daily at 1:00 AM)'); + console.log('⏰ [Cron] Negative flow detection job scheduled (3 times daily at PST):'); + console.log(' • 1:00 AM PST (3:00 AM CST)'); + console.log(' • 1:15 AM PST (3:15 AM CST)'); + console.log(' • 1:30 AM PST (3:30 AM CST)'); + console.log(' Timezone: America/Los_Angeles (Pacific Time)'); } /** diff --git a/water-api/src/routes/index.ts b/water-api/src/routes/index.ts index cd9fa0a..e767957 100644 --- a/water-api/src/routes/index.ts +++ b/water-api/src/routes/index.ts @@ -14,6 +14,7 @@ import readingRoutes from './reading.routes'; import bulkUploadRoutes from './bulk-upload.routes'; import auditRoutes from './audit.routes'; import notificationRoutes from './notification.routes'; +import testRoutes from './test.routes'; // Create main router const router = Router(); @@ -153,4 +154,14 @@ router.use('/audit-logs', auditRoutes); */ router.use('/notifications', notificationRoutes); +/** + * Test routes (for development/testing only): + * - POST /test/trigger-negative-flow - Manually trigger negative flow detection + * - GET /test/negative-flow-meters - Get meters with negative flow + * - GET /test/notifications-info - Get notifications system info + */ +if (process.env.NODE_ENV === 'development') { + router.use('/test', testRoutes); +} + export default router; diff --git a/water-api/src/routes/test.routes.ts b/water-api/src/routes/test.routes.ts new file mode 100644 index 0000000..627e8bd --- /dev/null +++ b/water-api/src/routes/test.routes.ts @@ -0,0 +1,69 @@ +import { Router } from 'express'; +import { authenticateToken, requireRole } from '../middleware/auth.middleware'; +import * as testController from '../controllers/test.controller'; + +const router = Router(); + +/** + * Test routes for development and testing purposes + * All endpoints require authentication and ADMIN role + */ + +/** + * POST /api/test/create-negative-flow-meter + * Create a test meter with negative flow + * This creates or updates the test meter TEST-NEGATIVE-001 + */ +router.post( + '/create-negative-flow-meter', + authenticateToken, + requireRole('ADMIN'), + testController.createTestMeterWithNegativeFlow +); + +/** + * POST /api/test/trigger-negative-flow + * Manually trigger the negative flow detection job + * This simulates the cron job that runs at 1:00 AM + */ +router.post( + '/trigger-negative-flow', + authenticateToken, + requireRole('ADMIN'), + testController.triggerNegativeFlowJob +); + +/** + * GET /api/test/negative-flow-meters + * Get list of meters currently with negative flow + */ +router.get( + '/negative-flow-meters', + authenticateToken, + requireRole('ADMIN'), + testController.getNegativeFlowMeters +); + +/** + * GET /api/test/notifications-info + * Get information about notifications in the system + */ +router.get( + '/notifications-info', + authenticateToken, + requireRole('ADMIN'), + testController.getNotificationsInfo +); + +/** + * DELETE /api/test/cleanup-test-data + * Clean up test meter and related notifications + */ +router.delete( + '/cleanup-test-data', + authenticateToken, + requireRole('ADMIN'), + testController.cleanupTestData +); + +export default router;