#nullable enable using Baya.Application.Contracts.Common; using Baya.Application.Contracts.Notifications; using Baya.Application.Models.Common; using Baya.Application.Models.Notifications; using Microsoft.EntityFrameworkCore; using NotificationEntity = Baya.Domain.Entities.Notifications.Notification; namespace Baya.Infrastructure.Persistence.Services.Notifications; /// /// Reads and per-user commands over notifications. Every method is scoped to the passed /// userId (the authenticated caller). Retention hard-deletes read notifications past the window /// and never touches unread ones. /// internal sealed class NotificationService(ApplicationDbContext db, IDateTimeProvider dateTimeProvider) : INotificationService { public async ValueTask> ListMineAsync(int userId, int page, int pageSize, CancellationToken cancellationToken = default) { var query = db.Set() .AsNoTracking() .Where(n => n.UserId == userId) // Unread first, then newest-first via the monotonic identity (deterministic, no timestamp ties). .OrderBy(n => n.IsRead) .ThenByDescending(n => n.Id); var total = await query.CountAsync(cancellationToken); var items = await query .Skip((page - 1) * pageSize) .Take(pageSize) .Select(n => new NotificationDto(n.Id, n.Type, n.Title, n.Body, n.DataJson, n.IsRead, n.ReadAt, n.CreatedAt)) .ToListAsync(cancellationToken); return new PagedResult(items, total, page, pageSize); } public ValueTask GetUnreadCountAsync(int userId, CancellationToken cancellationToken = default) => new(db.Set().AsNoTracking().CountAsync(n => n.UserId == userId && !n.IsRead, cancellationToken)); public async ValueTask MarkReadAsync(int userId, long notificationId, CancellationToken cancellationToken = default) { var notification = await db.Set() .FirstOrDefaultAsync(n => n.Id == notificationId && n.UserId == userId, cancellationToken); if (notification is null) return false; if (!notification.IsRead) { notification.IsRead = true; notification.ReadAt = dateTimeProvider.UtcNow; await db.SaveChangesAsync(cancellationToken); } return true; } public async ValueTask MarkAllReadAsync(int userId, CancellationToken cancellationToken = default) { var now = dateTimeProvider.UtcNow; return await db.Set() .Where(n => n.UserId == userId && !n.IsRead) .ExecuteUpdateAsync( s => s.SetProperty(n => n.IsRead, true).SetProperty(n => n.ReadAt, now), cancellationToken); } public async ValueTask PurgeOldReadAsync(int retentionDays, CancellationToken cancellationToken = default) { var cutoff = dateTimeProvider.UtcNow.AddDays(-retentionDays); // Read-only rows are the only purge candidates (unread is never deleted). The age cutoff is // applied in memory so the delete is a single id-keyed statement that translates on every // provider; the candidate set is bounded (only read notifications). var readRows = await db.Set() .Where(n => n.IsRead) .Select(n => new { n.Id, n.CreatedAt }) .ToListAsync(cancellationToken); var expiredIds = readRows.Where(n => n.CreatedAt < cutoff).Select(n => n.Id).ToList(); if (expiredIds.Count == 0) return 0; return await db.Set() .Where(n => expiredIds.Contains(n.Id)) .ExecuteDeleteAsync(cancellationToken); } }