Notifications cronjob
This commit is contained in:
@@ -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>
|
||||
|
||||
269
water-api/src/controllers/test.controller.ts
Normal file
269
water-api/src/controllers/test.controller.ts
Normal 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',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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)');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
69
water-api/src/routes/test.routes.ts
Normal file
69
water-api/src/routes/test.routes.ts
Normal 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;
|
||||
Reference in New Issue
Block a user