line-gestao-api/Services/VigenciaNotificationSyncSer...

362 lines
12 KiB
C#

using System.Globalization;
using line_gestao_api.Data;
using line_gestao_api.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
namespace line_gestao_api.Services;
public class VigenciaNotificationSyncService : IVigenciaNotificationSyncService
{
private readonly AppDbContext _db;
private readonly ITenantProvider _tenantProvider;
private readonly NotificationOptions _options;
private readonly ILogger<VigenciaNotificationSyncService> _logger;
public VigenciaNotificationSyncService(
AppDbContext db,
ITenantProvider tenantProvider,
IOptions<NotificationOptions> options,
ILogger<VigenciaNotificationSyncService> logger)
{
_db = db;
_tenantProvider = tenantProvider;
_options = options.Value;
_logger = logger;
}
public async Task SyncCurrentTenantAsync(CancellationToken cancellationToken = default)
{
var tenantId = _tenantProvider.TenantId;
if (!tenantId.HasValue || tenantId.Value == Guid.Empty)
{
return;
}
await SyncTenantAsync(tenantId.Value, cancellationToken);
}
public async Task SyncTenantAsync(Guid tenantId, CancellationToken cancellationToken = default)
{
if (tenantId == Guid.Empty)
{
return;
}
var previousTenant = _tenantProvider.TenantId;
try
{
_tenantProvider.SetTenantId(tenantId);
await ProcessTenantAsync(tenantId, cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Falha ao sincronizar notificações de vigência para tenant {TenantId}.", tenantId);
}
finally
{
_tenantProvider.SetTenantId(previousTenant);
}
}
private async Task ProcessTenantAsync(Guid tenantId, CancellationToken cancellationToken)
{
var today = DateTime.SpecifyKind(DateTime.UtcNow.Date, DateTimeKind.Utc);
var notifyAllFutureDates = _options.NotifyAllFutureDates;
var reminderDays = _options.ReminderDays
.Distinct()
.Where(d => d > 0)
.OrderBy(d => d)
.ToList();
var users = await _db.Users.AsNoTracking()
.Select(u => new { u.Id, u.Name, u.Email })
.ToListAsync(cancellationToken);
var userByName = users
.Where(u => !string.IsNullOrWhiteSpace(u.Name))
.ToDictionary(u => u.Name.Trim().ToLowerInvariant(), u => u.Id);
var userByEmail = users
.Where(u => !string.IsNullOrWhiteSpace(u.Email))
.ToDictionary(u => u.Email!.Trim().ToLowerInvariant(), u => u.Id);
var vigencias = await _db.VigenciaLines.AsNoTracking()
.Where(v => v.DtTerminoFidelizacao != null)
.ToListAsync(cancellationToken);
await CleanupOutdatedNotificationsAsync(vigencias, notifyAllFutureDates, reminderDays, today, cancellationToken);
var candidates = new List<Notification>();
foreach (var vigencia in vigencias)
{
if (vigencia.DtTerminoFidelizacao is null)
{
continue;
}
var endDate = DateTime.SpecifyKind(vigencia.DtTerminoFidelizacao.Value.Date, DateTimeKind.Utc);
var usuario = vigencia.Usuario?.Trim();
var cliente = vigencia.Cliente?.Trim();
var linha = vigencia.Linha?.Trim();
var usuarioKey = usuario?.ToLowerInvariant();
Guid? userId = null;
if (!string.IsNullOrWhiteSpace(usuarioKey))
{
if (userByEmail.TryGetValue(usuarioKey, out var matchedByEmail))
{
userId = matchedByEmail;
}
else if (userByName.TryGetValue(usuarioKey, out var matchedByName))
{
userId = matchedByName;
}
}
if (endDate < today)
{
candidates.Add(BuildNotification(
tipo: "Vencido",
titulo: $"Linha vencida{FormatLinha(linha)}",
mensagem: $"A linha{FormatLinha(linha)} do cliente {cliente ?? "(sem cliente)"} venceu em {endDate:dd/MM/yyyy}.",
referenciaData: endDate,
diasParaVencer: 0,
userId: userId,
usuario: usuario,
cliente: cliente,
linha: linha,
vigenciaLineId: vigencia.Id,
tenantId: tenantId));
continue;
}
var daysUntil = (endDate - today).Days;
if (!notifyAllFutureDates && !reminderDays.Contains(daysUntil))
{
continue;
}
candidates.Add(BuildNotification(
tipo: "AVencer",
titulo: $"Linha a vencer em {daysUntil} dias{FormatLinha(linha)}",
mensagem: $"A linha{FormatLinha(linha)} do cliente {cliente ?? "(sem cliente)"} vence em {endDate:dd/MM/yyyy}.",
referenciaData: endDate,
diasParaVencer: daysUntil,
userId: userId,
usuario: usuario,
cliente: cliente,
linha: linha,
vigenciaLineId: vigencia.Id,
tenantId: tenantId));
}
if (candidates.Count == 0)
{
return;
}
var candidateTipos = candidates.Select(c => c.Tipo).Distinct().ToList();
var candidateDates = candidates
.Where(c => c.ReferenciaData.HasValue)
.Select(c => DateTime.SpecifyKind(c.ReferenciaData!.Value.Date, DateTimeKind.Utc))
.Distinct()
.ToList();
List<Notification> existingNotifications = new();
if (candidateDates.Count > 0)
{
var minCandidateUtc = candidateDates.Min();
var maxCandidateUtcExclusive = candidateDates.Max().AddDays(1);
var candidateDateKeys = candidateDates
.Select(d => d.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture))
.ToHashSet();
existingNotifications = await _db.Notifications.AsNoTracking()
.Where(n => n.TenantId == tenantId)
.Where(n => candidateTipos.Contains(n.Tipo))
.Where(n => n.ReferenciaData != null &&
n.ReferenciaData.Value >= minCandidateUtc &&
n.ReferenciaData.Value < maxCandidateUtcExclusive)
.ToListAsync(cancellationToken);
existingNotifications = existingNotifications
.Where(n => n.ReferenciaData != null &&
candidateDateKeys.Contains(n.ReferenciaData.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)))
.ToList();
}
var existingSet = new HashSet<string>(existingNotifications.Select(n =>
BuildDedupKey(
n.Tipo,
n.ReferenciaData!.Value,
n.DiasParaVencer ?? 0,
n.Usuario,
n.Cliente,
n.Linha)));
var toInsert = candidates
.Where(c => !existingSet.Contains(c.DedupKey))
.ToList();
if (toInsert.Count == 0)
{
return;
}
await _db.Notifications.AddRangeAsync(toInsert, cancellationToken);
await _db.SaveChangesAsync(cancellationToken);
}
private async Task CleanupOutdatedNotificationsAsync(
IReadOnlyCollection<VigenciaLine> vigencias,
bool notifyAllFutureDates,
IReadOnlyCollection<int> reminderDays,
DateTime today,
CancellationToken cancellationToken)
{
var vigenciasById = vigencias.ToDictionary(v => v.Id, v => v);
var vigenciasByLinha = vigencias
.Where(v => !string.IsNullOrWhiteSpace(v.Linha))
.GroupBy(v => v.Linha!)
.Select(g => g.OrderByDescending(v => v.UpdatedAt).First())
.ToDictionary(v => v.Linha!, v => v);
var existingNotifications = await _db.Notifications.AsNoTracking()
.Where(n => n.Tipo == "Vencido" || n.Tipo == "AVencer")
.ToListAsync(cancellationToken);
if (existingNotifications.Count == 0)
{
return;
}
var idsToDelete = new List<Guid>();
foreach (var notification in existingNotifications)
{
var vigencia = ResolveVigencia(notification, vigenciasById, vigenciasByLinha);
if (vigencia?.DtTerminoFidelizacao is null)
{
idsToDelete.Add(notification.Id);
continue;
}
var endDate = DateTime.SpecifyKind(vigencia.DtTerminoFidelizacao.Value.Date, DateTimeKind.Utc);
if (endDate < today)
{
if (notification.Tipo != "Vencido")
{
idsToDelete.Add(notification.Id);
}
continue;
}
var daysUntil = (endDate - today).Days;
if (notification.Tipo == "Vencido")
{
idsToDelete.Add(notification.Id);
continue;
}
if (notification.DiasParaVencer != daysUntil)
{
idsToDelete.Add(notification.Id);
continue;
}
if (!notifyAllFutureDates && !reminderDays.Contains(daysUntil))
{
idsToDelete.Add(notification.Id);
}
}
if (idsToDelete.Count == 0)
{
return;
}
await _db.Notifications
.Where(n => idsToDelete.Contains(n.Id))
.ExecuteDeleteAsync(cancellationToken);
}
private static VigenciaLine? ResolveVigencia(
Notification notification,
IReadOnlyDictionary<Guid, VigenciaLine> vigenciasById,
IReadOnlyDictionary<string, VigenciaLine> vigenciasByLinha)
{
if (notification.VigenciaLineId.HasValue
&& vigenciasById.TryGetValue(notification.VigenciaLineId.Value, out var byId))
{
return byId;
}
if (!string.IsNullOrWhiteSpace(notification.Linha)
&& vigenciasByLinha.TryGetValue(notification.Linha, out var byLinha))
{
return byLinha;
}
return null;
}
private static Notification BuildNotification(
string tipo,
string titulo,
string mensagem,
DateTime referenciaData,
int diasParaVencer,
Guid? userId,
string? usuario,
string? cliente,
string? linha,
Guid vigenciaLineId,
Guid tenantId)
{
return new Notification
{
Tipo = tipo,
Titulo = titulo,
Mensagem = mensagem,
Data = DateTime.UtcNow,
ReferenciaData = referenciaData,
DiasParaVencer = diasParaVencer,
Lida = false,
DedupKey = BuildDedupKey(tipo, referenciaData, diasParaVencer, usuario, cliente, linha),
UserId = userId,
Usuario = usuario,
Cliente = cliente,
Linha = linha,
VigenciaLineId = vigenciaLineId,
TenantId = tenantId
};
}
private static string BuildDedupKey(
string tipo,
DateTime referenciaData,
int diasParaVencer,
string? usuario,
string? cliente,
string? linha)
{
var parts = new[]
{
tipo.Trim().ToLowerInvariant(),
referenciaData.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
diasParaVencer.ToString(CultureInfo.InvariantCulture),
(usuario ?? string.Empty).Trim().ToLowerInvariant(),
(cliente ?? string.Empty).Trim().ToLowerInvariant(),
(linha ?? string.Empty).Trim().ToLowerInvariant()
};
return string.Join('|', parts);
}
private static string FormatLinha(string? linha)
{
return string.IsNullOrWhiteSpace(linha) ? string.Empty : $" {linha}";
}
}