This commit is contained in:
Eduardo Lopes 2026-01-22 19:58:44 -03:00 committed by GitHub
commit 8db41acb46
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1182 additions and 0 deletions

View File

@ -340,6 +340,22 @@ namespace line_gestao_api.Controllers
return NoContent();
}
// ==========================================================
// ✅ DELETE: /api/mureg/{id}
// Exclui registro MUREG
// ==========================================================
[HttpDelete("{id:guid}")]
public async Task<IActionResult> Delete(Guid id)
{
var entity = await _db.MuregLines.FirstOrDefaultAsync(x => x.Id == id);
if (entity == null) return NotFound();
_db.MuregLines.Remove(entity);
await _db.SaveChangesAsync();
return NoContent();
}
// ==========================================================
// ✅ POST: /api/mureg/import-excel (mantido)
// ==========================================================

View File

@ -0,0 +1,71 @@
using line_gestao_api.Data;
using line_gestao_api.Dtos;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace line_gestao_api.Controllers;
[ApiController]
[Route("api/notifications")]
[Authorize]
public class NotificationsController : ControllerBase
{
private readonly AppDbContext _db;
public NotificationsController(AppDbContext db)
{
_db = db;
}
[HttpGet]
[HttpGet("/notifications")]
public async Task<ActionResult<List<NotificationDto>>> GetNotifications()
{
var query = _db.Notifications.AsNoTracking();
var items = await query
.OrderByDescending(n => n.Data)
.Select(n => new NotificationDto
{
Id = n.Id,
Tipo = n.Tipo,
Titulo = n.Titulo,
Mensagem = n.Mensagem,
Data = n.Data,
ReferenciaData = n.ReferenciaData,
DiasParaVencer = n.DiasParaVencer,
Lida = n.Lida,
LidaEm = n.LidaEm,
VigenciaLineId = n.VigenciaLineId,
Cliente = n.Cliente,
Linha = n.Linha
})
.ToListAsync();
return Ok(items);
}
[HttpPatch("{id:guid}/read")]
[HttpPatch("/notifications/{id:guid}/read")]
public async Task<IActionResult> MarkAsRead(Guid id)
{
var notification = await _db.Notifications
.FirstOrDefaultAsync(n => n.Id == id);
if (notification is null)
{
return NotFound();
}
if (!notification.Lida)
{
notification.Lida = true;
notification.LidaEm = DateTime.UtcNow;
await _db.SaveChangesAsync();
}
return NoContent();
}
}

View File

@ -27,6 +27,9 @@ public class AppDbContext : DbContext
// ✅ tabela TROCA DE NÚMERO
public DbSet<TrocaNumeroLine> TrocaNumeroLines => Set<TrocaNumeroLine>();
// ✅ tabela NOTIFICAÇÕES
public DbSet<Notification> Notifications => Set<Notification>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
@ -127,5 +130,28 @@ public class AppDbContext : DbContext
e.HasIndex(x => x.ICCID);
e.HasIndex(x => x.DataTroca);
});
// =========================
// ✅ NOTIFICAÇÕES
// =========================
modelBuilder.Entity<Notification>(e =>
{
e.HasIndex(x => x.DedupKey).IsUnique();
e.HasIndex(x => x.UserId);
e.HasIndex(x => x.Cliente);
e.HasIndex(x => x.Lida);
e.HasIndex(x => x.Data);
e.HasIndex(x => x.VigenciaLineId);
e.HasOne(x => x.User)
.WithMany()
.HasForeignKey(x => x.UserId)
.OnDelete(DeleteBehavior.Restrict);
e.HasOne(x => x.VigenciaLine)
.WithMany()
.HasForeignKey(x => x.VigenciaLineId)
.OnDelete(DeleteBehavior.Restrict);
});
}
}

17
Dtos/NotificationDto.cs Normal file
View File

@ -0,0 +1,17 @@
namespace line_gestao_api.Dtos;
public class NotificationDto
{
public Guid Id { get; set; }
public string Tipo { get; set; } = string.Empty;
public string Titulo { get; set; } = string.Empty;
public string Mensagem { get; set; } = string.Empty;
public DateTime Data { get; set; }
public DateTime? ReferenciaData { get; set; }
public int? DiasParaVencer { get; set; }
public bool Lida { get; set; }
public DateTime? LidaEm { get; set; }
public Guid? VigenciaLineId { get; set; }
public string? Cliente { get; set; }
public string? Linha { get; set; }
}

View File

@ -0,0 +1,567 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using line_gestao_api.Data;
#nullable disable
namespace line_gestao_api.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260205120000_AddNotifications")]
partial class AddNotifications
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("line_gestao_api.Models.BillingClient", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Aparelho")
.HasColumnType("text");
b.Property<string>("Cliente")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("FormaPagamento")
.HasColumnType("text");
b.Property<decimal?>("FranquiaLine")
.HasColumnType("numeric");
b.Property<decimal?>("FranquiaVivo")
.HasColumnType("numeric");
b.Property<int>("Item")
.HasColumnType("integer");
b.Property<decimal?>("Lucro")
.HasColumnType("numeric");
b.Property<int?>("QtdLinhas")
.HasColumnType("integer");
b.Property<string>("Tipo")
.IsRequired()
.HasMaxLength(2)
.HasColumnType("character varying(2)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<decimal?>("ValorContratoLine")
.HasColumnType("numeric");
b.Property<decimal?>("ValorContratoVivo")
.HasColumnType("numeric");
b.HasKey("Id");
b.HasIndex("Cliente");
b.HasIndex("Item");
b.HasIndex("Tipo");
b.HasIndex("Tipo", "Cliente");
b.ToTable("billing_clients", (string)null);
});
modelBuilder.Entity("line_gestao_api.Models.MobileLine", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Cedente")
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<string>("Chip")
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<string>("Cliente")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Conta")
.HasMaxLength(80)
.HasColumnType("character varying(80)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DataBloqueio")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DataEntregaCliente")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DataEntregaOpera")
.HasColumnType("timestamp with time zone");
b.Property<decimal?>("Desconto")
.HasColumnType("numeric");
b.Property<decimal?>("FranquiaGestao")
.HasColumnType("numeric");
b.Property<decimal?>("FranquiaLine")
.HasColumnType("numeric");
b.Property<decimal?>("FranquiaVivo")
.HasColumnType("numeric");
b.Property<decimal?>("GestaoVozDados")
.HasColumnType("numeric");
b.Property<int>("Item")
.HasColumnType("integer");
b.Property<string>("Linha")
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal?>("LocacaoAp")
.HasColumnType("numeric");
b.Property<decimal?>("Lucro")
.HasColumnType("numeric");
b.Property<string>("Modalidade")
.HasMaxLength(80)
.HasColumnType("character varying(80)");
b.Property<string>("PlanoContrato")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<decimal?>("Skeelo")
.HasColumnType("numeric");
b.Property<string>("Skil")
.HasMaxLength(80)
.HasColumnType("character varying(80)");
b.Property<string>("Solicitante")
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<string>("Status")
.HasMaxLength(80)
.HasColumnType("character varying(80)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Usuario")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<decimal?>("ValorContratoLine")
.HasColumnType("numeric");
b.Property<decimal?>("ValorContratoVivo")
.HasColumnType("numeric");
b.Property<decimal?>("ValorPlanoVivo")
.HasColumnType("numeric");
b.Property<string>("VencConta")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<decimal?>("VivoGestaoDispositivo")
.HasColumnType("numeric");
b.Property<decimal?>("VivoNewsPlus")
.HasColumnType("numeric");
b.Property<decimal?>("VivoTravelMundo")
.HasColumnType("numeric");
b.HasKey("Id");
b.HasIndex("Chip");
b.HasIndex("Cliente");
b.HasIndex("Linha")
.IsUnique();
b.HasIndex("Skil");
b.HasIndex("Status");
b.HasIndex("Usuario");
b.ToTable("MobileLines");
});
modelBuilder.Entity("line_gestao_api.Models.MuregLine", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DataDaMureg")
.HasColumnType("timestamp with time zone");
b.Property<string>("ICCID")
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<int>("Item")
.HasColumnType("integer");
b.Property<string>("LinhaAntiga")
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("LinhaNova")
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<Guid>("MobileLineId")
.HasColumnType("uuid");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("ICCID");
b.HasIndex("Item");
b.HasIndex("LinhaAntiga");
b.HasIndex("LinhaNova");
b.HasIndex("MobileLineId");
b.ToTable("MuregLines");
});
modelBuilder.Entity("line_gestao_api.Models.TrocaNumeroLine", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DataTroca")
.HasColumnType("timestamp with time zone");
b.Property<string>("ICCID")
.HasColumnType("text");
b.Property<int>("Item")
.HasColumnType("integer");
b.Property<string>("LinhaAntiga")
.HasColumnType("text");
b.Property<string>("LinhaNova")
.HasColumnType("text");
b.Property<string>("Motivo")
.HasColumnType("text");
b.Property<string>("Observacao")
.HasColumnType("text");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("DataTroca");
b.HasIndex("ICCID");
b.HasIndex("Item");
b.HasIndex("LinhaAntiga");
b.HasIndex("LinhaNova");
b.ToTable("TrocaNumeroLines");
});
modelBuilder.Entity("line_gestao_api.Models.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Phone")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.HasKey("Id");
b.HasIndex("Email")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("line_gestao_api.Models.Notification", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Cliente")
.HasColumnType("text");
b.Property<DateTime>("Data")
.HasColumnType("timestamp with time zone");
b.Property<string>("DedupKey")
.IsRequired()
.HasColumnType("text");
b.Property<int?>("DiasParaVencer")
.HasColumnType("integer");
b.Property<string>("Linha")
.HasColumnType("text");
b.Property<bool>("Lida")
.HasColumnType("boolean");
b.Property<DateTime?>("LidaEm")
.HasColumnType("timestamp with time zone");
b.Property<string>("Mensagem")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime?>("ReferenciaData")
.HasColumnType("timestamp with time zone");
b.Property<string>("Tipo")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Titulo")
.IsRequired()
.HasColumnType("text");
b.Property<Guid?>("UserId")
.HasColumnType("uuid");
b.Property<string>("Usuario")
.HasColumnType("text");
b.Property<Guid?>("VigenciaLineId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("Cliente");
b.HasIndex("Data");
b.HasIndex("DedupKey")
.IsUnique();
b.HasIndex("Lida");
b.HasIndex("UserId");
b.HasIndex("VigenciaLineId");
b.ToTable("Notifications");
});
modelBuilder.Entity("line_gestao_api.Models.UserData", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Celular")
.HasColumnType("text");
b.Property<string>("Cliente")
.HasColumnType("text");
b.Property<string>("Cpf")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DataNascimento")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.HasColumnType("text");
b.Property<string>("Endereco")
.HasColumnType("text");
b.Property<int>("Item")
.HasColumnType("integer");
b.Property<string>("Linha")
.HasColumnType("text");
b.Property<string>("Rg")
.HasColumnType("text");
b.Property<string>("TelefoneFixo")
.HasColumnType("text");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("Cliente");
b.HasIndex("Cpf");
b.HasIndex("Email");
b.HasIndex("Item");
b.HasIndex("Linha");
b.ToTable("UserDatas");
});
modelBuilder.Entity("line_gestao_api.Models.VigenciaLine", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Cliente")
.HasColumnType("text");
b.Property<string>("Conta")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DtEfetivacaoServico")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DtTerminoFidelizacao")
.HasColumnType("timestamp with time zone");
b.Property<int>("Item")
.HasColumnType("integer");
b.Property<string>("Linha")
.HasColumnType("text");
b.Property<string>("PlanoContrato")
.HasColumnType("text");
b.Property<decimal?>("Total")
.HasColumnType("numeric");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Usuario")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Cliente");
b.HasIndex("DtTerminoFidelizacao");
b.HasIndex("Item");
b.HasIndex("Linha");
b.ToTable("VigenciaLines");
});
modelBuilder.Entity("line_gestao_api.Models.Notification", b =>
{
b.HasOne("line_gestao_api.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("line_gestao_api.Models.VigenciaLine", "VigenciaLine")
.WithMany()
.HasForeignKey("VigenciaLineId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("User");
b.Navigation("VigenciaLine");
});
modelBuilder.Entity("line_gestao_api.Models.MuregLine", b =>
{
b.HasOne("line_gestao_api.Models.MobileLine", "MobileLine")
.WithMany("Muregs")
.HasForeignKey("MobileLineId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("MobileLine");
});
modelBuilder.Entity("line_gestao_api.Models.MobileLine", b =>
{
b.Navigation("Muregs");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,90 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace line_gestao_api.Migrations
{
/// <inheritdoc />
public partial class AddNotifications : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Notifications",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Tipo = table.Column<string>(type: "text", nullable: false),
Titulo = table.Column<string>(type: "text", nullable: false),
Mensagem = table.Column<string>(type: "text", nullable: false),
Data = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
ReferenciaData = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
DiasParaVencer = table.Column<int>(type: "integer", nullable: true),
Lida = table.Column<bool>(type: "boolean", nullable: false),
LidaEm = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
DedupKey = table.Column<string>(type: "text", nullable: false),
UserId = table.Column<Guid>(type: "uuid", nullable: true),
VigenciaLineId = table.Column<Guid>(type: "uuid", nullable: true),
Usuario = table.Column<string>(type: "text", nullable: true),
Cliente = table.Column<string>(type: "text", nullable: true),
Linha = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Notifications", x => x.Id);
table.ForeignKey(
name: "FK_Notifications_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_Notifications_VigenciaLines_VigenciaLineId",
column: x => x.VigenciaLineId,
principalTable: "VigenciaLines",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_Notifications_Cliente",
table: "Notifications",
column: "Cliente");
migrationBuilder.CreateIndex(
name: "IX_Notifications_Data",
table: "Notifications",
column: "Data");
migrationBuilder.CreateIndex(
name: "IX_Notifications_DedupKey",
table: "Notifications",
column: "DedupKey",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Notifications_Lida",
table: "Notifications",
column: "Lida");
migrationBuilder.CreateIndex(
name: "IX_Notifications_UserId",
table: "Notifications",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_Notifications_VigenciaLineId",
table: "Notifications",
column: "VigenciaLineId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Notifications");
}
}
}

View File

@ -348,6 +348,76 @@ namespace line_gestao_api.Migrations
b.ToTable("Users");
});
modelBuilder.Entity("line_gestao_api.Models.Notification", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Cliente")
.HasColumnType("text");
b.Property<DateTime>("Data")
.HasColumnType("timestamp with time zone");
b.Property<string>("DedupKey")
.IsRequired()
.HasColumnType("text");
b.Property<int?>("DiasParaVencer")
.HasColumnType("integer");
b.Property<string>("Linha")
.HasColumnType("text");
b.Property<bool>("Lida")
.HasColumnType("boolean");
b.Property<DateTime?>("LidaEm")
.HasColumnType("timestamp with time zone");
b.Property<string>("Mensagem")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime?>("ReferenciaData")
.HasColumnType("timestamp with time zone");
b.Property<string>("Tipo")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Titulo")
.IsRequired()
.HasColumnType("text");
b.Property<Guid?>("UserId")
.HasColumnType("uuid");
b.Property<string>("Usuario")
.HasColumnType("text");
b.Property<Guid?>("VigenciaLineId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("Cliente");
b.HasIndex("Data");
b.HasIndex("DedupKey")
.IsUnique();
b.HasIndex("Lida");
b.HasIndex("UserId");
b.HasIndex("VigenciaLineId");
b.ToTable("Notifications");
});
modelBuilder.Entity("line_gestao_api.Models.UserData", b =>
{
b.Property<Guid>("Id")
@ -457,6 +527,23 @@ namespace line_gestao_api.Migrations
b.ToTable("VigenciaLines");
});
modelBuilder.Entity("line_gestao_api.Models.Notification", b =>
{
b.HasOne("line_gestao_api.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("line_gestao_api.Models.VigenciaLine", "VigenciaLine")
.WithMany()
.HasForeignKey("VigenciaLineId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("User");
b.Navigation("VigenciaLine");
});
modelBuilder.Entity("line_gestao_api.Models.MuregLine", b =>
{
b.HasOne("line_gestao_api.Models.MobileLine", "MobileLine")

40
Models/Notification.cs Normal file
View File

@ -0,0 +1,40 @@
using System.ComponentModel.DataAnnotations;
namespace line_gestao_api.Models;
public class Notification
{
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
public string Tipo { get; set; } = string.Empty;
[Required]
public string Titulo { get; set; } = string.Empty;
[Required]
public string Mensagem { get; set; } = string.Empty;
public DateTime Data { get; set; } = DateTime.UtcNow;
public DateTime? ReferenciaData { get; set; }
public int? DiasParaVencer { get; set; }
public bool Lida { get; set; }
public DateTime? LidaEm { get; set; }
[Required]
public string DedupKey { get; set; } = string.Empty;
public Guid? UserId { get; set; }
public User? User { get; set; }
public Guid? VigenciaLineId { get; set; }
public VigenciaLine? VigenciaLine { get; set; }
public string? Usuario { get; set; }
public string? Cliente { get; set; }
public string? Linha { get; set; }
}

View File

@ -1,5 +1,6 @@
using System.Text;
using line_gestao_api.Data;
using line_gestao_api.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.EntityFrameworkCore;
@ -57,6 +58,9 @@ builder.Services
builder.Services.AddAuthorization();
builder.Services.Configure<NotificationOptions>(builder.Configuration.GetSection("Notifications"));
builder.Services.AddHostedService<VigenciaNotificationBackgroundService>();
var app = builder.Build();
if (app.Environment.IsDevelopment())

View File

@ -0,0 +1,7 @@
namespace line_gestao_api.Services;
public class NotificationOptions
{
public int CheckIntervalMinutes { get; set; } = 60;
public List<int> ReminderDays { get; set; } = new() { 30, 15, 7 };
}

View File

@ -0,0 +1,249 @@
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>();
if (!await TableExistsAsync(db, "Notifications", stoppingToken))
{
_logger.LogWarning("Tabela Notifications ainda não existe. Aguardando migrations.");
return;
}
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);
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);
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);
}
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 static Notification BuildNotification(
string tipo,
string titulo,
string mensagem,
DateTime referenciaData,
int diasParaVencer,
Guid? userId,
string? usuario,
string? cliente,
string? linha,
Guid vigenciaLineId)
{
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
};
}
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}";
}
}

View File

@ -4,5 +4,9 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Notifications": {
"CheckIntervalMinutes": 60,
"ReminderDays": [30, 15, 7]
}
}

View File

@ -7,5 +7,9 @@
"Issuer": "LineGestao",
"Audience": "LineGestao",
"ExpiresMinutes": 120
},
"Notifications": {
"CheckIntervalMinutes": 60,
"ReminderDays": [30, 15, 7]
}
}