Files
ridebus-admin/backend/src/services/alertSchedulerService.js
Zakhar Timoshenko c0d40f9b90 feat: add support for multiple route numbers in alert notifications
- Updated FCM alert notifications documentation to include handling of multiple route numbers.
- Enhanced Telegram cancellation import logic to support multiple route numbers, ensuring correct notifications are sent for all directions.
- Modified frontend components to allow selection of multiple route numbers for alerts, improving user experience.
- Implemented backend migration to add `route_numbers` column to alerts and adjusted related database constraints.
- Introduced new utility functions for normalizing and validating route numbers in alert processing.
2026-10-05 12:07:09 +03:00

108 lines
3.5 KiB
JavaScript

const { routeIdsSql } = require('./alertTargets');
const CLAIM_DUE_ALERTS_SQL = `
WITH due AS (
SELECT a.id
FROM alerts a
WHERE a.is_active = true
AND (a.start_time > a.created_at OR a.notification_pending = true)
AND a.start_time <= CURRENT_TIMESTAMP
AND (a.end_time IS NULL OR a.end_time > CURRENT_TIMESTAMP)
AND a.notification_sent_at IS NULL
AND a.notification_attempts < 5
AND (
a.notification_claimed_at IS NULL
OR a.notification_claimed_at < CURRENT_TIMESTAMP - INTERVAL '5 minutes'
)
ORDER BY a.start_time ASC, a.id ASC
LIMIT $1
FOR UPDATE SKIP LOCKED
)
UPDATE alerts a
SET notification_claimed_at = CURRENT_TIMESTAMP,
notification_attempts = a.notification_attempts + 1
FROM due
WHERE a.id = due.id
RETURNING a.*, ${routeIdsSql()} AS route_ids,
CASE WHEN cardinality(a.route_numbers)=1 THEN a.route_numbers[1] END AS notification_route_number
`;
class AlertSchedulerService {
constructor({ pool, notificationService, logger, batchSize = 50 } = {}) {
this.pool = pool || require('../config/database');
this.notificationService = notificationService || require('./alertNotificationService').getAlertNotificationService();
this.logger = logger || require('../config/logger');
this.batchSize = batchSize;
this.running = false;
}
async dispatchDueAlerts() {
if (this.running || !this.notificationService.enabled) {
return { dispatched: 0, skipped: this.running ? 'running' : 'disabled' };
}
this.running = true;
try {
const claimed = await this.pool.query(CLAIM_DUE_ALERTS_SQL, [this.batchSize]);
let dispatched = 0;
for (const row of claimed.rows) {
const { notification_route_number: routeNumber, ...alert } = row;
try {
const result = await this.notificationService.sendAlertCreated({
...alert,
route_number: routeNumber || ''
});
if (result.sent) {
await this.pool.query(
`UPDATE alerts
SET notification_sent_at = CURRENT_TIMESTAMP,
notification_claimed_at = NULL
WHERE id = $1`,
[alert.id]
);
dispatched += 1;
} else {
await this.releaseClaim(alert.id);
}
} catch (error) {
await this.releaseClaim(alert.id);
this.logger.error('Failed to dispatch scheduled alert notification', {
alertId: String(alert.id),
reason: error.message || 'Unknown notification error',
firebaseCode: error.code || 'unknown'
});
}
}
return { dispatched, claimed: claimed.rows.length };
} finally {
this.running = false;
}
}
async releaseClaim(alertId) {
await this.pool.query(
`UPDATE alerts
SET notification_claimed_at = NULL
WHERE id = $1 AND notification_sent_at IS NULL`,
[alertId]
);
}
}
function startAlertScheduler({ intervalMs = 30_000, service } = {}) {
const scheduler = service || new AlertSchedulerService();
const run = () => scheduler.dispatchDueAlerts().catch((error) => {
scheduler.logger.error('Scheduled alert delivery cycle failed', {
reason: error.message || 'Unknown scheduler error'
});
});
run();
const timer = setInterval(run, intervalMs);
timer.unref?.();
return () => clearInterval(timer);
}
module.exports = { AlertSchedulerService, CLAIM_DUE_ALERTS_SQL, startAlertScheduler };