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({
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;