Notifications cronjob

This commit is contained in:
2026-02-01 22:29:48 -06:00
parent 48e0884bf7
commit 8ca10d0b35
5 changed files with 367 additions and 7 deletions

View File

@@ -218,7 +218,7 @@ export default function Home({
<img
src={grhWatermark}
alt="Gestión de Recursos Hídricos"
className="relative z-0 h-16 w-auto opacity-80 select-none pointer-events-none shrink-0"
className="relative z-0 h-20 w-auto opacity-80 select-none pointer-events-none shrink-0"
draggable={false}
/>
</div>

View File

@@ -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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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',
});
}
}

View File

@@ -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)');
}
/**

View File

@@ -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;

View File

@@ -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;