using Microsoft.EntityFrameworkCore; using Nexus.Api.Data; using Nexus.Api.Models; using System.Text.Json; namespace Nexus.Api.Services; public sealed class NotificationService(NexusDbContext db, ILiveUpdateService liveUpdateService) : INotificationService { public async Task CreateAsync(string type, string title, string? message, string forUser, Guid? taskId = null, CancellationToken ct = default) { var notification = new Notification { Type = type, Title = title, Message = message, ForUser = forUser.ToLowerInvariant(), TaskId = taskId }; db.Notifications.Add(notification); db.OutboxEvents.Add(CreateNotificationEvent("notification.created", notification)); await db.SaveChangesAsync(ct); await PublishSnapshotAsync(notification.ForUser, ct); return notification; } public async Task> GetForUserAsync(string forUser, int limit = 50, bool unreadOnly = false, CancellationToken ct = default) { var query = db.Notifications .Where(n => n.ForUser == forUser.ToLowerInvariant()); if (unreadOnly) query = query.Where(n => !n.IsRead); return await query .OrderByDescending(n => n.CreatedAt) .Take(limit) .ToListAsync(ct); } public async Task MarkAsReadAsync(Guid id, CancellationToken ct = default) { var notification = await db.Notifications.FindAsync([id], ct); if (notification is null) return new NotificationReadResult(null, false); var changed = !notification.IsRead; if (changed) { notification.IsRead = true; db.OutboxEvents.Add(CreateNotificationEvent("notification.updated", notification)); await db.SaveChangesAsync(ct); } await PublishSnapshotAsync(notification.ForUser, ct); return new NotificationReadResult(notification, changed); } public async Task MarkAllAsReadAsync(string forUser, CancellationToken ct = default) { var normalizedUser = forUser.ToLowerInvariant(); int count; if (db.Database.IsRelational()) { await using var transaction = await db.Database.BeginTransactionAsync(ct); count = await db.Notifications .Where(n => n.ForUser == normalizedUser && !n.IsRead) .ExecuteUpdateAsync(s => s.SetProperty(n => n.IsRead, true), ct); if (count > 0) { db.OutboxEvents.Add(CreateNotificationCollectionEvent( "notification.read_all", count)); await db.SaveChangesAsync(ct); } await transaction.CommitAsync(ct); } else { var unread = await db.Notifications .Where(n => n.ForUser == normalizedUser && !n.IsRead) .ToListAsync(ct); foreach (var notification in unread) notification.IsRead = true; count = unread.Count; if (count > 0) { db.OutboxEvents.Add(CreateNotificationCollectionEvent( "notification.read_all", count)); await db.SaveChangesAsync(ct); } } await PublishSnapshotAsync(normalizedUser, ct); return count; } public async Task GetUnreadCountAsync(string forUser, CancellationToken ct = default) { return await db.Notifications .CountAsync(n => n.ForUser == forUser.ToLowerInvariant() && !n.IsRead, ct); } public async Task GetSnapshotAsync(string forUser, int limit = 50, bool unreadOnly = false, CancellationToken ct = default) { var normalizedUser = forUser.ToLowerInvariant(); var notifications = await GetForUserAsync(normalizedUser, limit, unreadOnly, ct); var unreadCount = await GetUnreadCountAsync(normalizedUser, ct); return new NotificationSnapshotDto( notifications.Select(MapToDto).ToList(), unreadCount, normalizedUser); } private async Task PublishSnapshotAsync(string forUser, CancellationToken ct) { var snapshot = await GetSnapshotAsync(forUser, ct: ct); liveUpdateService.Publish("notifications.snapshot", snapshot, "notifications"); } private static NotificationDto MapToDto(Notification n) => new( n.Id, n.Type, n.Title, n.Message, n.ForUser, n.TaskId, n.IsRead, n.CreatedAt); private static OutboxEvent CreateNotificationEvent( string type, Notification notification) => new() { Type = type, AggregateType = "notification", AggregateId = notification.Id.ToString(), AggregateRevision = notification.IsRead ? 1 : 0, PayloadJson = JsonSerializer.Serialize(new { notificationType = notification.Type, notification.TaskId, notification.IsRead }) }; private static OutboxEvent CreateNotificationCollectionEvent( string type, int affectedCount) => new() { Type = type, AggregateType = "notification", AggregateId = "*", AggregateRevision = 0, PayloadJson = JsonSerializer.Serialize(new { affectedCount }) }; }