using JobTrackerApi.Data; using JobTrackerApi.Models; using Microsoft.EntityFrameworkCore; namespace JobTrackerApi.Services; public sealed class UserNotificationStore(JobTrackerContext db, TimeProvider timeProvider) { public Task GetAsync(Guid notificationId, CancellationToken cancellationToken) { EnsureOwnerScope(); return db.UserNotifications.AsNoTracking().FirstOrDefaultAsync(notification => notification.Id == notificationId, cancellationToken); } public Task> ListAsync(int limit, CancellationToken cancellationToken) { EnsureOwnerScope(); if (limit is < 1 or > 100) throw new ArgumentOutOfRangeException(nameof(limit)); return db.UserNotifications.AsNoTracking() .Where(notification => notification.DismissedAtUtc == null) .OrderByDescending(notification => notification.CreatedAtUtc) .Take(limit) .ToListAsync(cancellationToken); } public Task UnreadCountAsync(CancellationToken cancellationToken) { EnsureOwnerScope(); return db.UserNotifications.CountAsync( notification => notification.DismissedAtUtc == null && notification.ReadAtUtc == null, cancellationToken); } public Task MarkReadAsync(Guid notificationId, CancellationToken cancellationToken) { EnsureOwnerScope(); var now = timeProvider.GetUtcNow().UtcDateTime; return db.UserNotifications .Where(notification => notification.Id == notificationId && notification.ReadAtUtc == null) .ExecuteUpdateAsync(setters => setters.SetProperty(notification => notification.ReadAtUtc, now), cancellationToken); } public Task DismissAsync(Guid notificationId, CancellationToken cancellationToken) { EnsureOwnerScope(); var now = timeProvider.GetUtcNow().UtcDateTime; return db.UserNotifications .Where(notification => notification.Id == notificationId && notification.DismissedAtUtc == null) .ExecuteUpdateAsync(setters => setters.SetProperty(notification => notification.DismissedAtUtc, now), cancellationToken); } private void EnsureOwnerScope() { if (db.CurrentUserId is null) throw new InvalidOperationException("Notification access requires an authenticated owner scope."); } }