273 lines
9.2 KiB
C#
273 lines
9.2 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 VigenciaNotificationBackgroundService : BackgroundService
|
|
{
|
|
private readonly IServiceScopeFactory _scopeFactory;
|
|
private readonly ILogger<VigenciaNotificationBackgroundService> _logger;
|
|
private readonly NotificationOptions _options;
|
|
|
|
public VigenciaNotificationBackgroundService(
|
|
IServiceScopeFactory scopeFactory,
|
|
IOptions<NotificationOptions> options,
|
|
ILogger<VigenciaNotificationBackgroundService> logger)
|
|
{
|
|
_scopeFactory = scopeFactory;
|
|
_logger = logger;
|
|
_options = options.Value;
|
|
}
|
|
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
{
|
|
var intervalMinutes = _options.CheckIntervalMinutes <= 0 ? 60 : _options.CheckIntervalMinutes;
|
|
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(intervalMinutes));
|
|
|
|
await RunOnceAsync(stoppingToken);
|
|
|
|
while (await timer.WaitForNextTickAsync(stoppingToken))
|
|
{
|
|
await RunOnceAsync(stoppingToken);
|
|
}
|
|
}
|
|
|
|
private async Task RunOnceAsync(CancellationToken stoppingToken)
|
|
{
|
|
try
|
|
{
|
|
using var scope = _scopeFactory.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
var tenantProvider = scope.ServiceProvider.GetRequiredService<ITenantProvider>();
|
|
|
|
if (!await TableExistsAsync(db, "Notifications", stoppingToken))
|
|
{
|
|
_logger.LogWarning("Tabela Notifications ainda não existe. Aguardando migrations.");
|
|
return;
|
|
}
|
|
|
|
var tenants = await db.Tenants.AsNoTracking().ToListAsync(stoppingToken);
|
|
if (tenants.Count == 0)
|
|
{
|
|
_logger.LogWarning("Nenhum tenant encontrado para gerar notificações.");
|
|
return;
|
|
}
|
|
|
|
foreach (var tenant in tenants)
|
|
{
|
|
tenantProvider.SetTenantId(tenant.Id);
|
|
await ProcessTenantAsync(db, tenant.Id, stoppingToken);
|
|
}
|
|
|
|
tenantProvider.SetTenantId(null);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Erro ao gerar notificações de vigência.");
|
|
}
|
|
}
|
|
|
|
private static async Task<bool> TableExistsAsync(AppDbContext db, string tableName, CancellationToken stoppingToken)
|
|
{
|
|
if (!db.Database.IsRelational())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var connection = db.Database.GetDbConnection();
|
|
if (connection.State != System.Data.ConnectionState.Open)
|
|
{
|
|
await connection.OpenAsync(stoppingToken);
|
|
}
|
|
|
|
await using var command = connection.CreateCommand();
|
|
command.CommandText = "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = @tableName)";
|
|
var parameter = command.CreateParameter();
|
|
parameter.ParameterName = "tableName";
|
|
parameter.Value = tableName;
|
|
command.Parameters.Add(parameter);
|
|
|
|
var result = await command.ExecuteScalarAsync(stoppingToken);
|
|
return result is bool exists && exists;
|
|
}
|
|
|
|
private async Task ProcessTenantAsync(AppDbContext db, Guid tenantId, CancellationToken stoppingToken)
|
|
{
|
|
var today = DateTime.UtcNow.Date;
|
|
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(stoppingToken);
|
|
|
|
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(stoppingToken);
|
|
|
|
var candidates = new List<Notification>();
|
|
foreach (var vigencia in vigencias)
|
|
{
|
|
if (vigencia.DtTerminoFidelizacao is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var endDate = vigencia.DtTerminoFidelizacao.Value.Date;
|
|
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)
|
|
{
|
|
var notification = 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);
|
|
|
|
candidates.Add(notification);
|
|
continue;
|
|
}
|
|
|
|
var daysUntil = (endDate - today).Days;
|
|
if (reminderDays.Contains(daysUntil))
|
|
{
|
|
var notification = 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);
|
|
|
|
candidates.Add(notification);
|
|
}
|
|
}
|
|
|
|
if (candidates.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var dedupKeys = candidates.Select(c => c.DedupKey).Distinct().ToList();
|
|
var existingKeys = await db.Notifications.AsNoTracking()
|
|
.Where(n => dedupKeys.Contains(n.DedupKey))
|
|
.Select(n => n.DedupKey)
|
|
.ToListAsync(stoppingToken);
|
|
|
|
var existingSet = new HashSet<string>(existingKeys);
|
|
var toInsert = candidates
|
|
.Where(c => !existingSet.Contains(c.DedupKey))
|
|
.ToList();
|
|
|
|
if (toInsert.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
await db.Notifications.AddRangeAsync(toInsert, stoppingToken);
|
|
await db.SaveChangesAsync(stoppingToken);
|
|
}
|
|
|
|
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, vigenciaLineId, referenciaData, diasParaVencer, usuario, cliente, linha),
|
|
UserId = userId,
|
|
Usuario = usuario,
|
|
Cliente = cliente,
|
|
Linha = linha,
|
|
VigenciaLineId = vigenciaLineId,
|
|
TenantId = tenantId
|
|
};
|
|
}
|
|
|
|
private static string BuildDedupKey(
|
|
string tipo,
|
|
Guid vigenciaLineId,
|
|
DateTime referenciaData,
|
|
int diasParaVencer,
|
|
string? usuario,
|
|
string? cliente,
|
|
string? linha)
|
|
{
|
|
var parts = new[]
|
|
{
|
|
tipo.Trim().ToLowerInvariant(),
|
|
vigenciaLineId.ToString(),
|
|
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}";
|
|
}
|
|
}
|