aboutsummaryrefslogtreecommitdiff
path: root/src/Enmarcha.Backend/Services/AlertPhaseNotificationHostedService.cs
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2026-04-02 12:38:10 +0200
committerAriel Costas Guerrero <ariel@costas.dev>2026-04-02 12:45:33 +0200
commit1b4f4a674ac533c0b51260ba35ab91dd2cf9486d (patch)
tree9fdaf418bef86c51737bcf203483089c9e2b908b /src/Enmarcha.Backend/Services/AlertPhaseNotificationHostedService.cs
parent749e04d6fc2304bb29920db297d1fa4d73b57648 (diff)
Basic push notification system for service alerts
Co-authored-by: Copilot <copilot@github.com>
Diffstat (limited to 'src/Enmarcha.Backend/Services/AlertPhaseNotificationHostedService.cs')
-rw-r--r--src/Enmarcha.Backend/Services/AlertPhaseNotificationHostedService.cs77
1 files changed, 77 insertions, 0 deletions
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;
+
+/// <summary>
+/// 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.
+/// </summary>
+public class AlertPhaseNotificationHostedService(
+ IServiceScopeFactory scopeFactory,
+ ILogger<AlertPhaseNotificationHostedService> 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<AppDbContext>();
+ var pushService = scope.ServiceProvider.GetRequiredService<IPushNotificationService>();
+
+ 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();
+}