From 1b4f4a674ac533c0b51260ba35ab91dd2cf9486d Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero Date: Thu, 2 Apr 2026 12:38:10 +0200 Subject: Basic push notification system for service alerts Co-authored-by: Copilot --- .../AlertPhaseNotificationHostedService.cs | 77 ++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 src/Enmarcha.Backend/Services/AlertPhaseNotificationHostedService.cs (limited to 'src/Enmarcha.Backend/Services/AlertPhaseNotificationHostedService.cs') diff --git a/src/Enmarcha.Backend/Services/AlertPhaseNotificationHostedService.cs b/src/Enmarcha.Backend/Services/AlertPhaseNotificationHostedService.cs new file mode 100644 index 0000000..5cbbaee --- /dev/null +++ b/src/Enmarcha.Backend/Services/AlertPhaseNotificationHostedService.cs @@ -0,0 +1,77 @@ +using Enmarcha.Backend.Data; +using Microsoft.EntityFrameworkCore; + +namespace Enmarcha.Backend.Services; + +/// +/// Background service that automatically sends push notifications when a service alert +/// transitions into the PreNotice or Active phase without having been notified yet. +/// Runs every 60 seconds and also immediately on startup to handle any missed transitions. +/// +public class AlertPhaseNotificationHostedService( + IServiceScopeFactory scopeFactory, + ILogger logger) : IHostedService, IDisposable +{ + private Timer? _timer; + + public Task StartAsync(CancellationToken cancellationToken) + { + // Run immediately, then every 60 seconds + _timer = new Timer(_ => _ = RunAsync(), null, TimeSpan.Zero, TimeSpan.FromSeconds(60)); + return Task.CompletedTask; + } + + private async Task RunAsync() + { + try + { + using var scope = scopeFactory.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var pushService = scope.ServiceProvider.GetRequiredService(); + + var now = DateTime.UtcNow; + + // Find alerts that are published and not yet hidden, but haven't been notified + // for their current phase (PreNotice: published but event not yet started; + // Active: event in progress). + var alertsToNotify = await db.ServiceAlerts + .Where(a => + a.PublishDate <= now && a.HiddenDate > now && + ( + // PreNotice: published, event hasn't started, no prenotice notification sent yet + (a.EventStartDate > now && a.PreNoticeNotifiedAt == null) || + // Active: event started and not finished, no active notification sent yet + (a.EventStartDate <= now && a.EventEndDate > now && a.ActiveNotifiedAt == null) + )) + .ToListAsync(); + + if (alertsToNotify.Count == 0) return; + + logger.LogInformation("Sending push notifications for {Count} alert(s)", alertsToNotify.Count); + + foreach (var alert in alertsToNotify) + { + try + { + await pushService.SendAlertAsync(alert); + } + catch (Exception ex) + { + logger.LogError(ex, "Error sending push notification for alert {AlertId}", alert.Id); + } + } + } + catch (Exception ex) + { + logger.LogError(ex, "Unexpected error in {ServiceName}", nameof(AlertPhaseNotificationHostedService)); + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _timer?.Change(Timeout.Infinite, 0); + return Task.CompletedTask; + } + + public void Dispose() => _timer?.Dispose(); +} -- cgit v1.3