This commit is contained in:
Eduardo Lopes 2026-01-23 12:30:19 -03:00 committed by GitHub
commit a5c16cf881
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 3498 additions and 109 deletions

View File

@ -1,9 +1,10 @@
using System.IdentityModel.Tokens.Jwt; using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims; using System.Security.Claims;
using System.Text; using System.Text;
using line_gestao_api.Data;
using line_gestao_api.Dtos; using line_gestao_api.Dtos;
using line_gestao_api.Models; using line_gestao_api.Models;
using line_gestao_api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -15,80 +16,101 @@ namespace line_gestao_api.Controllers;
[Route("auth")] [Route("auth")]
public class AuthController : ControllerBase public class AuthController : ControllerBase
{ {
private readonly AppDbContext _db; private readonly UserManager<ApplicationUser> _userManager;
private readonly ITenantProvider _tenantProvider;
private readonly IConfiguration _config; private readonly IConfiguration _config;
public AuthController(AppDbContext db, IConfiguration config) public AuthController(UserManager<ApplicationUser> userManager, ITenantProvider tenantProvider, IConfiguration config)
{ {
_db = db; _userManager = userManager;
_tenantProvider = tenantProvider;
_config = config; _config = config;
} }
[HttpPost("register")] [HttpPost("register")]
[Authorize(Roles = "admin")]
public async Task<IActionResult> Register(RegisterRequest req) public async Task<IActionResult> Register(RegisterRequest req)
{ {
// ✅ NOVO: valida confirmação de senha (não vai pro banco, só valida)
if (req.Password != req.ConfirmPassword) if (req.Password != req.ConfirmPassword)
return BadRequest("As senhas não conferem."); return BadRequest("As senhas não conferem.");
var email = req.Email.Trim().ToLower(); var tenantId = _tenantProvider.TenantId;
if (tenantId == null)
{
return Unauthorized("Tenant inválido.");
}
var exists = await _db.Users.AnyAsync(u => u.Email.ToLower() == email); var email = req.Email.Trim().ToLowerInvariant();
var normalizedEmail = _userManager.NormalizeEmail(email);
var exists = await _userManager.Users.AnyAsync(u => u.NormalizedEmail == normalizedEmail && u.TenantId == tenantId);
if (exists) return BadRequest("E-mail já cadastrado."); if (exists) return BadRequest("E-mail já cadastrado.");
// ✅ NOVO: normaliza telefone (salva somente dígitos) var user = new ApplicationUser
var phoneDigits = new string((req.Phone ?? string.Empty).Where(char.IsDigit).ToArray());
var user = new User
{ {
Name = req.Name.Trim(), Name = req.Name.Trim(),
Email = email, Email = email,
Phone = phoneDigits // ✅ NOVO UserName = email,
TenantId = tenantId.Value,
IsActive = true,
EmailConfirmed = true
}; };
var hasher = new PasswordHasher<User>(); var createResult = await _userManager.CreateAsync(user, req.Password);
user.PasswordHash = hasher.HashPassword(user, req.Password); if (!createResult.Succeeded)
{
return BadRequest(createResult.Errors.Select(e => e.Description).ToList());
}
_db.Users.Add(user); await _userManager.AddToRoleAsync(user, "leitura");
await _db.SaveChangesAsync();
var token = GenerateJwt(user); var token = await GenerateJwtAsync(user);
return Ok(new AuthResponse(token)); return Ok(new AuthResponse(token));
} }
[HttpPost("login")] [HttpPost("login")]
public async Task<IActionResult> Login(LoginRequest req) public async Task<IActionResult> Login(LoginRequest req)
{ {
var email = req.Email.Trim().ToLower(); var email = req.Email.Trim().ToLowerInvariant();
var normalizedEmail = _userManager.NormalizeEmail(email);
var user = await _db.Users.FirstOrDefaultAsync(u => u.Email.ToLower() == email); var users = await _userManager.Users
if (user is null) return Unauthorized("Credenciais inválidas."); .Where(u => u.NormalizedEmail == normalizedEmail)
.ToListAsync();
var hasher = new PasswordHasher<User>(); if (users.Count != 1)
var result = hasher.VerifyHashedPassword(user, user.PasswordHash, req.Password);
if (result == PasswordVerificationResult.Failed)
return Unauthorized("Credenciais inválidas."); return Unauthorized("Credenciais inválidas.");
var token = GenerateJwt(user); var user = users[0];
if (!user.IsActive)
return Unauthorized("Usuário desativado.");
var valid = await _userManager.CheckPasswordAsync(user, req.Password);
if (!valid)
return Unauthorized("Credenciais inválidas.");
var token = await GenerateJwtAsync(user);
return Ok(new AuthResponse(token)); return Ok(new AuthResponse(token));
} }
private string GenerateJwt(User user) private async Task<string> GenerateJwtAsync(ApplicationUser user)
{ {
var key = _config["Jwt:Key"]!; var key = _config["Jwt:Key"]!;
var issuer = _config["Jwt:Issuer"]!; var issuer = _config["Jwt:Issuer"]!;
var audience = _config["Jwt:Audience"]!; var audience = _config["Jwt:Audience"]!;
var expiresMinutes = int.Parse(_config["Jwt:ExpiresMinutes"]!); var expiresMinutes = int.Parse(_config["Jwt:ExpiresMinutes"]!);
var roles = await _userManager.GetRolesAsync(user);
var claims = new List<Claim> var claims = new List<Claim>
{ {
new(JwtRegisteredClaimNames.Sub, user.Id.ToString()), new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
new(JwtRegisteredClaimNames.Email, user.Email), new(JwtRegisteredClaimNames.Email, user.Email ?? string.Empty),
new("name", user.Name), new("name", user.Name),
new("phone", user.Phone ?? string.Empty) // ✅ NOVO (opcional, mas útil) new("tenantId", user.TenantId.ToString())
}; };
claims.AddRange(roles.Select(r => new Claim(ClaimTypes.Role, r)));
var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)); var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
var creds = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256); var creds = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256);

View File

@ -340,6 +340,22 @@ namespace line_gestao_api.Controllers
return NoContent(); 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) // ✅ 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

@ -0,0 +1,283 @@
using line_gestao_api.Data;
using line_gestao_api.Dtos;
using line_gestao_api.Models;
using line_gestao_api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace line_gestao_api.Controllers;
[ApiController]
[Route("api/users")]
[Authorize]
public class UsersController : ControllerBase
{
private readonly AppDbContext _db;
private readonly UserManager<ApplicationUser> _userManager;
private readonly RoleManager<IdentityRole<Guid>> _roleManager;
private readonly ITenantProvider _tenantProvider;
public UsersController(
AppDbContext db,
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole<Guid>> roleManager,
ITenantProvider tenantProvider)
{
_db = db;
_userManager = userManager;
_roleManager = roleManager;
_tenantProvider = tenantProvider;
}
[HttpPost]
[Authorize(Roles = "admin")]
public async Task<ActionResult<UserListItemDto>> Create([FromBody] UserCreateRequest req)
{
var errors = ValidateCreate(req);
if (errors.Count > 0)
{
return BadRequest(new ValidationErrorResponse { Errors = errors });
}
if (_tenantProvider.TenantId == null)
{
return Unauthorized();
}
var tenantId = _tenantProvider.TenantId.Value;
var email = req.Email.Trim().ToLowerInvariant();
var normalizedEmail = _userManager.NormalizeEmail(email);
var exists = await _userManager.Users
.AnyAsync(u => u.TenantId == tenantId && u.NormalizedEmail == normalizedEmail);
if (exists)
{
return BadRequest(new ValidationErrorResponse
{
Errors = new List<ValidationErrorDto>
{
new() { Field = "email", Message = "E-mail já cadastrado." }
}
});
}
var role = req.Permissao.Trim().ToLowerInvariant();
if (!await _roleManager.RoleExistsAsync(role))
{
return BadRequest(new ValidationErrorResponse
{
Errors = new List<ValidationErrorDto>
{
new() { Field = "permissao", Message = "Permissão inválida." }
}
});
}
var user = new ApplicationUser
{
Name = req.Nome.Trim(),
Email = email,
UserName = email,
TenantId = tenantId,
IsActive = true,
EmailConfirmed = true
};
var createResult = await _userManager.CreateAsync(user, req.Senha);
if (!createResult.Succeeded)
{
return BadRequest(new ValidationErrorResponse
{
Errors = createResult.Errors.Select(e => new ValidationErrorDto
{
Field = "senha",
Message = e.Description
}).ToList()
});
}
await _userManager.AddToRoleAsync(user, role);
var response = new UserListItemDto
{
Id = user.Id,
Nome = user.Name,
Email = user.Email ?? string.Empty,
Permissao = role,
TenantId = user.TenantId,
Ativo = user.IsActive
};
return CreatedAtAction(nameof(GetById), new { id = user.Id }, response);
}
[HttpGet]
[Authorize(Roles = "admin")]
public async Task<ActionResult<PagedResult<UserListItemDto>>> GetAll(
[FromQuery] string? search,
[FromQuery] string? permissao,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20)
{
page = page < 1 ? 1 : page;
pageSize = pageSize < 1 ? 20 : pageSize;
var usersQuery = _userManager.Users.AsNoTracking();
if (!string.IsNullOrWhiteSpace(search))
{
var term = search.Trim();
usersQuery = usersQuery.Where(u =>
EF.Functions.ILike(u.Name, $"%{term}%") ||
EF.Functions.ILike(u.Email ?? string.Empty, $"%{term}%"));
}
IQueryable<Guid> userIdsByRole = Enumerable.Empty<Guid>().AsQueryable();
if (!string.IsNullOrWhiteSpace(permissao))
{
var roleName = permissao.Trim().ToLowerInvariant();
userIdsByRole = from ur in _db.UserRoles
join r in _db.Roles on ur.RoleId equals r.Id
where r.Name == roleName
select ur.UserId;
usersQuery = usersQuery.Where(u => userIdsByRole.Contains(u.Id));
}
var total = await usersQuery.CountAsync();
var users = await usersQuery
.OrderBy(u => u.Name)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
var roleLookup = await (from ur in _db.UserRoles
join r in _db.Roles on ur.RoleId equals r.Id
where users.Select(u => u.Id).Contains(ur.UserId)
select new { ur.UserId, Role = r.Name ?? "" })
.ToListAsync();
var roleMap = roleLookup
.GroupBy(x => x.UserId)
.ToDictionary(g => g.Key, g => g.First().Role);
var items = users.Select(u => new UserListItemDto
{
Id = u.Id,
Nome = u.Name,
Email = u.Email ?? string.Empty,
Permissao = roleMap.GetValueOrDefault(u.Id, string.Empty),
TenantId = u.TenantId,
Ativo = u.IsActive
}).ToList();
return Ok(new PagedResult<UserListItemDto>
{
Page = page,
PageSize = pageSize,
Total = total,
Items = items
});
}
[HttpGet("{id:guid}")]
[Authorize(Roles = "admin")]
public async Task<ActionResult<UserListItemDto>> GetById(Guid id)
{
var user = await _userManager.Users.AsNoTracking().FirstOrDefaultAsync(u => u.Id == id);
if (user == null)
{
return NotFound();
}
var roles = await _userManager.GetRolesAsync(user);
var role = roles.FirstOrDefault() ?? string.Empty;
return Ok(new UserListItemDto
{
Id = user.Id,
Nome = user.Name,
Email = user.Email ?? string.Empty,
Permissao = role,
TenantId = user.TenantId,
Ativo = user.IsActive
});
}
[HttpPatch("{id:guid}")]
[Authorize(Roles = "admin")]
public async Task<IActionResult> Update(Guid id, [FromBody] UserUpdateRequest req)
{
var user = await _userManager.Users.FirstOrDefaultAsync(u => u.Id == id);
if (user == null)
{
return NotFound();
}
if (req.Ativo.HasValue)
{
user.IsActive = req.Ativo.Value;
}
if (!string.IsNullOrWhiteSpace(req.Permissao))
{
var roleName = req.Permissao.Trim().ToLowerInvariant();
if (!await _roleManager.RoleExistsAsync(roleName))
{
return BadRequest(new ValidationErrorResponse
{
Errors = new List<ValidationErrorDto>
{
new() { Field = "permissao", Message = "Permissão inválida." }
}
});
}
var existingRoles = await _userManager.GetRolesAsync(user);
if (existingRoles.Count > 0)
{
await _userManager.RemoveFromRolesAsync(user, existingRoles);
}
await _userManager.AddToRoleAsync(user, roleName);
}
await _userManager.UpdateAsync(user);
return NoContent();
}
private static List<ValidationErrorDto> ValidateCreate(UserCreateRequest req)
{
var errors = new List<ValidationErrorDto>();
if (string.IsNullOrWhiteSpace(req.Nome))
{
errors.Add(new ValidationErrorDto { Field = "nome", Message = "Nome é obrigatório." });
}
if (string.IsNullOrWhiteSpace(req.Email))
{
errors.Add(new ValidationErrorDto { Field = "email", Message = "Email é obrigatório." });
}
if (string.IsNullOrWhiteSpace(req.Senha) || req.Senha.Length < 6)
{
errors.Add(new ValidationErrorDto { Field = "senha", Message = "Senha deve ter pelo menos 6 caracteres." });
}
if (req.Senha != req.ConfirmarSenha)
{
errors.Add(new ValidationErrorDto { Field = "confirmarSenha", Message = "Confirmação de senha inválida." });
}
if (string.IsNullOrWhiteSpace(req.Permissao))
{
errors.Add(new ValidationErrorDto { Field = "permissao", Message = "Permissão é obrigatória." });
}
return errors;
}
}

View File

@ -1,13 +1,21 @@
using Microsoft.EntityFrameworkCore; using line_gestao_api.Models;
using line_gestao_api.Models; using line_gestao_api.Services;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace line_gestao_api.Data; namespace line_gestao_api.Data;
public class AppDbContext : DbContext public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid>, Guid>
{ {
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { } private readonly ITenantProvider _tenantProvider;
public DbSet<User> Users => Set<User>(); public AppDbContext(DbContextOptions<AppDbContext> options, ITenantProvider tenantProvider) : base(options)
{
_tenantProvider = tenantProvider;
}
public DbSet<Tenant> Tenants => Set<Tenant>();
// ✅ tabela para espelhar a planilha (GERAL) // ✅ tabela para espelhar a planilha (GERAL)
public DbSet<MobileLine> MobileLines => Set<MobileLine>(); public DbSet<MobileLine> MobileLines => Set<MobileLine>();
@ -27,24 +35,30 @@ public class AppDbContext : DbContext
// ✅ tabela TROCA DE NÚMERO // ✅ tabela TROCA DE NÚMERO
public DbSet<TrocaNumeroLine> TrocaNumeroLines => Set<TrocaNumeroLine>(); public DbSet<TrocaNumeroLine> TrocaNumeroLines => Set<TrocaNumeroLine>();
// ✅ tabela NOTIFICAÇÕES
public DbSet<Notification> Notifications => Set<Notification>();
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
// ========================= // =========================
// ✅ USER // ✅ USER (Identity)
// ========================= // =========================
modelBuilder.Entity<User>() modelBuilder.Entity<ApplicationUser>(e =>
.HasIndex(u => u.Email) {
.IsUnique(); e.Property(x => x.Name).HasMaxLength(120);
e.HasIndex(x => new { x.TenantId, x.NormalizedEmail })
.IsUnique();
});
// ========================= // =========================
// ✅ GERAL (MobileLine) // ✅ GERAL (MobileLine)
// ========================= // =========================
modelBuilder.Entity<MobileLine>(e => modelBuilder.Entity<MobileLine>(e =>
{ {
// Mantém UNIQUE por Linha (se Linha puder ser null no banco, Postgres aceita múltiplos nulls) // Mantém UNIQUE por Linha por tenant (se Linha puder ser null no banco, Postgres aceita múltiplos nulls)
e.HasIndex(x => x.Linha).IsUnique(); e.HasIndex(x => new { x.TenantId, x.Linha }).IsUnique();
// performance // performance
e.HasIndex(x => x.Chip); e.HasIndex(x => x.Chip);
@ -63,6 +77,7 @@ public class AppDbContext : DbContext
e.HasIndex(x => x.ICCID); e.HasIndex(x => x.ICCID);
e.HasIndex(x => x.LinhaAntiga); e.HasIndex(x => x.LinhaAntiga);
e.HasIndex(x => x.LinhaNova); e.HasIndex(x => x.LinhaNova);
e.HasIndex(x => x.TenantId);
// FK + index // FK + index
e.HasIndex(x => x.MobileLineId); e.HasIndex(x => x.MobileLineId);
@ -90,6 +105,7 @@ public class AppDbContext : DbContext
e.HasIndex(x => x.Cliente); e.HasIndex(x => x.Cliente);
e.HasIndex(x => new { x.Tipo, x.Cliente }); e.HasIndex(x => new { x.Tipo, x.Cliente });
e.HasIndex(x => x.Item); e.HasIndex(x => x.Item);
e.HasIndex(x => x.TenantId);
}); });
// ========================= // =========================
@ -103,6 +119,7 @@ public class AppDbContext : DbContext
e.HasIndex(x => x.Linha); e.HasIndex(x => x.Linha);
e.HasIndex(x => x.Cpf); e.HasIndex(x => x.Cpf);
e.HasIndex(x => x.Email); e.HasIndex(x => x.Email);
e.HasIndex(x => x.TenantId);
}); });
// ========================= // =========================
@ -114,6 +131,7 @@ public class AppDbContext : DbContext
e.HasIndex(x => x.Cliente); e.HasIndex(x => x.Cliente);
e.HasIndex(x => x.Linha); e.HasIndex(x => x.Linha);
e.HasIndex(x => x.DtTerminoFidelizacao); e.HasIndex(x => x.DtTerminoFidelizacao);
e.HasIndex(x => x.TenantId);
}); });
// ========================= // =========================
@ -126,6 +144,69 @@ public class AppDbContext : DbContext
e.HasIndex(x => x.LinhaNova); e.HasIndex(x => x.LinhaNova);
e.HasIndex(x => x.ICCID); e.HasIndex(x => x.ICCID);
e.HasIndex(x => x.DataTroca); e.HasIndex(x => x.DataTroca);
e.HasIndex(x => x.TenantId);
}); });
// =========================
// ✅ 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.HasIndex(x => x.TenantId);
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);
});
modelBuilder.Entity<MobileLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<MuregLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<BillingClient>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<UserData>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<VigenciaLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<TrocaNumeroLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<Notification>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<ApplicationUser>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
}
public override int SaveChanges()
{
ApplyTenantIds();
return base.SaveChanges();
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
ApplyTenantIds();
return base.SaveChangesAsync(cancellationToken);
}
private void ApplyTenantIds()
{
if (_tenantProvider.TenantId == null)
{
return;
}
var tenantId = _tenantProvider.TenantId.Value;
foreach (var entry in ChangeTracker.Entries<ITenantEntity>().Where(e => e.State == EntityState.Added))
{
if (entry.Entity.TenantId == Guid.Empty)
{
entry.Entity.TenantId = tenantId;
}
}
} }
} }

82
Data/SeedData.cs Normal file
View File

@ -0,0 +1,82 @@
using line_gestao_api.Models;
using line_gestao_api.Services;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
namespace line_gestao_api.Data;
public class SeedOptions
{
public string DefaultTenantName { get; set; } = "Default";
public string AdminName { get; set; } = "Administrador";
public string AdminEmail { get; set; } = "admin@linegestao.local";
public string AdminPassword { get; set; } = "Admin123!";
}
public static class SeedData
{
public static readonly Guid DefaultTenantId = Guid.Parse("11111111-1111-1111-1111-111111111111");
public static async Task EnsureSeedDataAsync(IServiceProvider services)
{
using var scope = services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole<Guid>>>();
var tenantProvider = scope.ServiceProvider.GetRequiredService<ITenantProvider>();
var options = scope.ServiceProvider.GetRequiredService<IOptions<SeedOptions>>().Value;
await db.Database.MigrateAsync();
var roles = new[] { "admin", "gestor", "operador", "leitura" };
foreach (var role in roles)
{
if (!await roleManager.RoleExistsAsync(role))
{
await roleManager.CreateAsync(new IdentityRole<Guid>(role));
}
}
var tenant = await db.Tenants.FirstOrDefaultAsync(t => t.Id == DefaultTenantId);
if (tenant == null)
{
tenant = new Tenant
{
Id = DefaultTenantId,
Name = options.DefaultTenantName,
CreatedAt = DateTime.UtcNow
};
db.Tenants.Add(tenant);
await db.SaveChangesAsync();
}
tenantProvider.SetTenantId(tenant.Id);
var normalizedEmail = userManager.NormalizeEmail(options.AdminEmail);
var existingAdmin = await userManager.Users
.FirstOrDefaultAsync(u => u.NormalizedEmail == normalizedEmail && u.TenantId == tenant.Id);
if (existingAdmin == null)
{
var adminUser = new ApplicationUser
{
UserName = options.AdminEmail,
Email = options.AdminEmail,
Name = options.AdminName,
TenantId = tenant.Id,
EmailConfirmed = true,
IsActive = true
};
var createResult = await userManager.CreateAsync(adminUser, options.AdminPassword);
if (createResult.Succeeded)
{
await userManager.AddToRoleAsync(adminUser, "admin");
}
}
tenantProvider.SetTenantId(null);
}
}

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; }
}

37
Dtos/UserDtos.cs Normal file
View File

@ -0,0 +1,37 @@
namespace line_gestao_api.Dtos;
public class UserCreateRequest
{
public string Nome { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Senha { get; set; } = string.Empty;
public string ConfirmarSenha { get; set; } = string.Empty;
public string Permissao { get; set; } = string.Empty;
}
public class UserUpdateRequest
{
public string? Permissao { get; set; }
public bool? Ativo { get; set; }
}
public class UserListItemDto
{
public Guid Id { get; set; }
public string Nome { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Permissao { get; set; } = string.Empty;
public Guid TenantId { get; set; }
public bool Ativo { get; set; }
}
public class ValidationErrorDto
{
public string Field { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
}
public class ValidationErrorResponse
{
public List<ValidationErrorDto> Errors { get; set; } = new();
}

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

@ -0,0 +1,837 @@
// <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("20260210120000_AddIdentityAndTenants")]
partial class AddIdentityAndTenants
{
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.ApplicationUser", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("IsActive")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail");
b.HasIndex("NormalizedUserName")
.IsUnique();
b.HasIndex("TenantId", "NormalizedEmail")
.IsUnique();
b.ToTable("AspNetUsers", (string)null);
});
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<Guid>("TenantId")
.HasColumnType("uuid");
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("TenantId");
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<Guid>("TenantId")
.HasColumnType("uuid");
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("Skil");
b.HasIndex("Status");
b.HasIndex("TenantId", "Linha")
.IsUnique();
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<Guid>("TenantId")
.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.HasIndex("TenantId");
b.ToTable("MuregLines");
});
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<Guid>("TenantId")
.HasColumnType("uuid");
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("TenantId");
b.HasIndex("UserId");
b.HasIndex("VigenciaLineId");
b.ToTable("Notifications");
});
modelBuilder.Entity("line_gestao_api.Models.Tenant", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Tenants");
});
modelBuilder.Entity("line_gestao_api.Models.TrocaNumeroLine", b =>
{
b.Property<Guid>("Id")
.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<Guid>("TenantId")
.HasColumnType("uuid");
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.HasIndex("TenantId");
b.ToTable("TrocaNumeroLines");
});
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<Guid>("TenantId")
.HasColumnType("uuid");
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.HasIndex("TenantId");
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<Guid>("TenantId")
.HasColumnType("uuid");
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.HasIndex("TenantId");
b.ToTable("VigenciaLines");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole<Guid>", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique();
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<Guid>("RoleId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<Guid>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<Guid>("RoleId")
.HasColumnType("uuid");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
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.Notification", b =>
{
b.HasOne("line_gestao_api.Models.ApplicationUser", "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("Microsoft.AspNetCore.Identity.IdentityRoleClaim<Guid>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<Guid>", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<Guid>", b =>
{
b.HasOne("line_gestao_api.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<Guid>", b =>
{
b.HasOne("line_gestao_api.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<Guid>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<Guid>", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("line_gestao_api.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<Guid>", b =>
{
b.HasOne("line_gestao_api.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("line_gestao_api.Models.MobileLine", b =>
{
b.Navigation("Muregs");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,456 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace line_gestao_api.Migrations
{
/// <inheritdoc />
public partial class AddIdentityAndTenants : Migration
{
private static readonly Guid DefaultTenantId = Guid.Parse("11111111-1111-1111-1111-111111111111");
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Notifications_Users_UserId",
table: "Notifications");
migrationBuilder.DropTable(
name: "Users");
migrationBuilder.CreateTable(
name: "Tenants",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Tenants", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetRoles",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetUsers",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Name = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
TenantId = table.Column<Guid>(type: "uuid", nullable: false, defaultValue: DefaultTenantId),
IsActive = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedUserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
Email = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
EmailConfirmed = table.Column<bool>(type: "boolean", nullable: false),
PasswordHash = table.Column<string>(type: "text", nullable: true),
SecurityStamp = table.Column<string>(type: "text", nullable: true),
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true),
PhoneNumber = table.Column<string>(type: "text", nullable: true),
PhoneNumberConfirmed = table.Column<bool>(type: "boolean", nullable: false),
TwoFactorEnabled = table.Column<bool>(type: "boolean", nullable: false),
LockoutEnd = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
LockoutEnabled = table.Column<bool>(type: "boolean", nullable: false),
AccessFailedCount = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetRoleClaims",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
RoleId = table.Column<Guid>(type: "uuid", nullable: false),
ClaimType = table.Column<string>(type: "text", nullable: true),
ClaimValue = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
table.ForeignKey(
name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
column: x => x.RoleId,
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserClaims",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
UserId = table.Column<Guid>(type: "uuid", nullable: false),
ClaimType = table.Column<string>(type: "text", nullable: true),
ClaimValue = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
table.ForeignKey(
name: "FK_AspNetUserClaims_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserLogins",
columns: table => new
{
LoginProvider = table.Column<string>(type: "text", nullable: false),
ProviderKey = table.Column<string>(type: "text", nullable: false),
ProviderDisplayName = table.Column<string>(type: "text", nullable: true),
UserId = table.Column<Guid>(type: "uuid", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
table.ForeignKey(
name: "FK_AspNetUserLogins_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserRoles",
columns: table => new
{
UserId = table.Column<Guid>(type: "uuid", nullable: false),
RoleId = table.Column<Guid>(type: "uuid", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
column: x => x.RoleId,
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserTokens",
columns: table => new
{
UserId = table.Column<Guid>(type: "uuid", nullable: false),
LoginProvider = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Value = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
table.ForeignKey(
name: "FK_AspNetUserTokens_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.AddColumn<Guid>(
name: "TenantId",
table: "MobileLines",
type: "uuid",
nullable: false,
defaultValue: DefaultTenantId);
migrationBuilder.AddColumn<Guid>(
name: "TenantId",
table: "MuregLines",
type: "uuid",
nullable: false,
defaultValue: DefaultTenantId);
migrationBuilder.AddColumn<Guid>(
name: "TenantId",
table: "billing_clients",
type: "uuid",
nullable: false,
defaultValue: DefaultTenantId);
migrationBuilder.AddColumn<Guid>(
name: "TenantId",
table: "UserDatas",
type: "uuid",
nullable: false,
defaultValue: DefaultTenantId);
migrationBuilder.AddColumn<Guid>(
name: "TenantId",
table: "VigenciaLines",
type: "uuid",
nullable: false,
defaultValue: DefaultTenantId);
migrationBuilder.AddColumn<Guid>(
name: "TenantId",
table: "TrocaNumeroLines",
type: "uuid",
nullable: false,
defaultValue: DefaultTenantId);
migrationBuilder.AddColumn<Guid>(
name: "TenantId",
table: "Notifications",
type: "uuid",
nullable: false,
defaultValue: DefaultTenantId);
migrationBuilder.DropIndex(
name: "IX_MobileLines_Linha",
table: "MobileLines");
migrationBuilder.CreateIndex(
name: "IX_MobileLines_TenantId_Linha",
table: "MobileLines",
columns: new[] { "TenantId", "Linha" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_MuregLines_TenantId",
table: "MuregLines",
column: "TenantId");
migrationBuilder.CreateIndex(
name: "IX_billing_clients_TenantId",
table: "billing_clients",
column: "TenantId");
migrationBuilder.CreateIndex(
name: "IX_UserDatas_TenantId",
table: "UserDatas",
column: "TenantId");
migrationBuilder.CreateIndex(
name: "IX_VigenciaLines_TenantId",
table: "VigenciaLines",
column: "TenantId");
migrationBuilder.CreateIndex(
name: "IX_TrocaNumeroLines_TenantId",
table: "TrocaNumeroLines",
column: "TenantId");
migrationBuilder.CreateIndex(
name: "IX_Notifications_TenantId",
table: "Notifications",
column: "TenantId");
migrationBuilder.CreateIndex(
name: "IX_AspNetRoles_NormalizedName",
table: "AspNetRoles",
column: "NormalizedName",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_AspNetUsers_NormalizedEmail",
table: "AspNetUsers",
column: "NormalizedEmail");
migrationBuilder.CreateIndex(
name: "IX_AspNetUsers_NormalizedUserName",
table: "AspNetUsers",
column: "NormalizedUserName",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_AspNetUsers_TenantId_NormalizedEmail",
table: "AspNetUsers",
columns: new[] { "TenantId", "NormalizedEmail" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_AspNetRoleClaims_RoleId",
table: "AspNetRoleClaims",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "IX_AspNetUserClaims_UserId",
table: "AspNetUserClaims",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AspNetUserLogins_UserId",
table: "AspNetUserLogins",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AspNetUserRoles_RoleId",
table: "AspNetUserRoles",
column: "RoleId");
migrationBuilder.AddForeignKey(
name: "FK_Notifications_AspNetUsers_UserId",
table: "Notifications",
column: "UserId",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.InsertData(
table: "Tenants",
columns: new[] { "Id", "Name", "CreatedAt" },
values: new object[] { DefaultTenantId, "Default", DateTime.UtcNow });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Notifications_AspNetUsers_UserId",
table: "Notifications");
migrationBuilder.DropTable(
name: "AspNetRoleClaims");
migrationBuilder.DropTable(
name: "AspNetUserClaims");
migrationBuilder.DropTable(
name: "AspNetUserLogins");
migrationBuilder.DropTable(
name: "AspNetUserRoles");
migrationBuilder.DropTable(
name: "AspNetUserTokens");
migrationBuilder.DropTable(
name: "AspNetRoles");
migrationBuilder.DropTable(
name: "AspNetUsers");
migrationBuilder.DropTable(
name: "Tenants");
migrationBuilder.DropIndex(
name: "IX_MobileLines_TenantId_Linha",
table: "MobileLines");
migrationBuilder.DropIndex(
name: "IX_MuregLines_TenantId",
table: "MuregLines");
migrationBuilder.DropIndex(
name: "IX_billing_clients_TenantId",
table: "billing_clients");
migrationBuilder.DropIndex(
name: "IX_UserDatas_TenantId",
table: "UserDatas");
migrationBuilder.DropIndex(
name: "IX_VigenciaLines_TenantId",
table: "VigenciaLines");
migrationBuilder.DropIndex(
name: "IX_TrocaNumeroLines_TenantId",
table: "TrocaNumeroLines");
migrationBuilder.DropIndex(
name: "IX_Notifications_TenantId",
table: "Notifications");
migrationBuilder.DropColumn(
name: "TenantId",
table: "MobileLines");
migrationBuilder.DropColumn(
name: "TenantId",
table: "MuregLines");
migrationBuilder.DropColumn(
name: "TenantId",
table: "billing_clients");
migrationBuilder.DropColumn(
name: "TenantId",
table: "UserDatas");
migrationBuilder.DropColumn(
name: "TenantId",
table: "VigenciaLines");
migrationBuilder.DropColumn(
name: "TenantId",
table: "TrocaNumeroLines");
migrationBuilder.DropColumn(
name: "TenantId",
table: "Notifications");
migrationBuilder.CreateIndex(
name: "IX_MobileLines_Linha",
table: "MobileLines",
column: "Linha",
unique: true);
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Name = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
Email = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
Phone = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
PasswordHash = table.Column<string>(type: "text", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_Users_Email",
table: "Users",
column: "Email",
unique: true);
migrationBuilder.AddForeignKey(
name: "FK_Notifications_Users_UserId",
table: "Notifications",
column: "UserId",
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
}
}

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace line_gestao_api.Migrations
{
/// <inheritdoc />
public partial class RenameUserNameIndexSafe : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("ALTER INDEX IF EXISTS \"IX_AspNetUsers_NormalizedUserName\" RENAME TO \"UserNameIndex\"");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("ALTER INDEX IF EXISTS \"UserNameIndex\" RENAME TO \"IX_AspNetUsers_NormalizedUserName\"");
}
}
}

View File

@ -1,4 +1,4 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
@ -22,6 +22,85 @@ namespace line_gestao_api.Migrations
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("line_gestao_api.Models.ApplicationUser", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("IsActive")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail");
b.HasIndex("NormalizedUserName")
.IsUnique();
b.HasIndex("TenantId", "NormalizedEmail")
.IsUnique();
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("line_gestao_api.Models.BillingClient", b => modelBuilder.Entity("line_gestao_api.Models.BillingClient", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -57,6 +136,9 @@ namespace line_gestao_api.Migrations
b.Property<int?>("QtdLinhas") b.Property<int?>("QtdLinhas")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<string>("Tipo") b.Property<string>("Tipo")
.IsRequired() .IsRequired()
.HasMaxLength(2) .HasMaxLength(2)
@ -77,6 +159,8 @@ namespace line_gestao_api.Migrations
b.HasIndex("Item"); b.HasIndex("Item");
b.HasIndex("TenantId");
b.HasIndex("Tipo"); b.HasIndex("Tipo");
b.HasIndex("Tipo", "Cliente"); b.HasIndex("Tipo", "Cliente");
@ -169,6 +253,9 @@ namespace line_gestao_api.Migrations
.HasMaxLength(80) .HasMaxLength(80)
.HasColumnType("character varying(80)"); .HasColumnType("character varying(80)");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<DateTime>("UpdatedAt") b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
@ -204,13 +291,13 @@ namespace line_gestao_api.Migrations
b.HasIndex("Cliente"); b.HasIndex("Cliente");
b.HasIndex("Linha")
.IsUnique();
b.HasIndex("Skil"); b.HasIndex("Skil");
b.HasIndex("Status"); b.HasIndex("Status");
b.HasIndex("TenantId", "Linha")
.IsUnique();
b.HasIndex("Usuario"); b.HasIndex("Usuario");
b.ToTable("MobileLines"); b.ToTable("MobileLines");
@ -246,6 +333,9 @@ namespace line_gestao_api.Migrations
b.Property<Guid>("MobileLineId") b.Property<Guid>("MobileLineId")
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<DateTime>("UpdatedAt") b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
@ -261,13 +351,107 @@ namespace line_gestao_api.Migrations
b.HasIndex("MobileLineId"); b.HasIndex("MobileLineId");
b.HasIndex("TenantId");
b.ToTable("MuregLines"); b.ToTable("MuregLines");
}); });
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<Guid>("TenantId")
.HasColumnType("uuid");
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("TenantId");
b.HasIndex("UserId");
b.HasIndex("VigenciaLineId");
b.ToTable("Notifications");
});
modelBuilder.Entity("line_gestao_api.Models.Tenant", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Tenants");
});
modelBuilder.Entity("line_gestao_api.Models.TrocaNumeroLine", b => modelBuilder.Entity("line_gestao_api.Models.TrocaNumeroLine", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<DateTime>("CreatedAt") b.Property<DateTime>("CreatedAt")
@ -294,6 +478,9 @@ namespace line_gestao_api.Migrations
b.Property<string>("Observacao") b.Property<string>("Observacao")
.HasColumnType("text"); .HasColumnType("text");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<DateTime>("UpdatedAt") b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
@ -309,45 +496,11 @@ namespace line_gestao_api.Migrations
b.HasIndex("LinhaNova"); b.HasIndex("LinhaNova");
b.HasIndex("TenantId");
b.ToTable("TrocaNumeroLines"); 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.UserData", b => modelBuilder.Entity("line_gestao_api.Models.UserData", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -384,6 +537,9 @@ namespace line_gestao_api.Migrations
b.Property<string>("Rg") b.Property<string>("Rg")
.HasColumnType("text"); .HasColumnType("text");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<string>("TelefoneFixo") b.Property<string>("TelefoneFixo")
.HasColumnType("text"); .HasColumnType("text");
@ -402,6 +558,8 @@ namespace line_gestao_api.Migrations
b.HasIndex("Linha"); b.HasIndex("Linha");
b.HasIndex("TenantId");
b.ToTable("UserDatas"); b.ToTable("UserDatas");
}); });
@ -435,6 +593,9 @@ namespace line_gestao_api.Migrations
b.Property<string>("PlanoContrato") b.Property<string>("PlanoContrato")
.HasColumnType("text"); .HasColumnType("text");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<decimal?>("Total") b.Property<decimal?>("Total")
.HasColumnType("numeric"); .HasColumnType("numeric");
@ -454,9 +615,137 @@ namespace line_gestao_api.Migrations
b.HasIndex("Linha"); b.HasIndex("Linha");
b.HasIndex("TenantId");
b.ToTable("VigenciaLines"); b.ToTable("VigenciaLines");
}); });
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole<Guid>", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique();
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<Guid>("RoleId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<Guid>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<Guid>("RoleId")
.HasColumnType("uuid");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("line_gestao_api.Models.MuregLine", b => modelBuilder.Entity("line_gestao_api.Models.MuregLine", b =>
{ {
b.HasOne("line_gestao_api.Models.MobileLine", "MobileLine") b.HasOne("line_gestao_api.Models.MobileLine", "MobileLine")
@ -468,6 +757,74 @@ namespace line_gestao_api.Migrations
b.Navigation("MobileLine"); b.Navigation("MobileLine");
}); });
modelBuilder.Entity("line_gestao_api.Models.Notification", b =>
{
b.HasOne("line_gestao_api.Models.ApplicationUser", "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("Microsoft.AspNetCore.Identity.IdentityRoleClaim<Guid>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<Guid>", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<Guid>", b =>
{
b.HasOne("line_gestao_api.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<Guid>", b =>
{
b.HasOne("line_gestao_api.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<Guid>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<Guid>", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("line_gestao_api.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<Guid>", b =>
{
b.HasOne("line_gestao_api.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("line_gestao_api.Models.MobileLine", b => modelBuilder.Entity("line_gestao_api.Models.MobileLine", b =>
{ {
b.Navigation("Muregs"); b.Navigation("Muregs");

11
Models/ApplicationUser.cs Normal file
View File

@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Identity;
namespace line_gestao_api.Models;
public class ApplicationUser : IdentityUser<Guid>, ITenantEntity
{
public string Name { get; set; } = string.Empty;
public Guid TenantId { get; set; }
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

View File

@ -1,8 +1,9 @@
namespace line_gestao_api.Models namespace line_gestao_api.Models
{ {
public class BillingClient public class BillingClient : ITenantEntity
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid TenantId { get; set; }
public string Tipo { get; set; } = "PF"; // "PF" ou "PJ" public string Tipo { get; set; } = "PF"; // "PF" ou "PJ"
public int Item { get; set; } public int Item { get; set; }

6
Models/ITenantEntity.cs Normal file
View File

@ -0,0 +1,6 @@
namespace line_gestao_api.Models;
public interface ITenantEntity
{
Guid TenantId { get; set; }
}

View File

@ -2,10 +2,12 @@
namespace line_gestao_api.Models namespace line_gestao_api.Models
{ {
public class MobileLine public class MobileLine : ITenantEntity
{ {
public Guid Id { get; set; } = Guid.NewGuid(); public Guid Id { get; set; } = Guid.NewGuid();
public Guid TenantId { get; set; }
public int Item { get; set; } public int Item { get; set; }
[MaxLength(80)] [MaxLength(80)]
public string? Conta { get; set; } public string? Conta { get; set; }

View File

@ -2,10 +2,12 @@
namespace line_gestao_api.Models namespace line_gestao_api.Models
{ {
public class MuregLine public class MuregLine : ITenantEntity
{ {
public Guid Id { get; set; } = Guid.NewGuid(); public Guid Id { get; set; } = Guid.NewGuid();
public Guid TenantId { get; set; }
public int Item { get; set; } public int Item { get; set; }
// Linha escolhida da GERAL no momento do mureg // Linha escolhida da GERAL no momento do mureg

42
Models/Notification.cs Normal file
View File

@ -0,0 +1,42 @@
using System.ComponentModel.DataAnnotations;
namespace line_gestao_api.Models;
public class Notification : ITenantEntity
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid TenantId { get; set; }
[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 ApplicationUser? 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; }
}

8
Models/Tenant.cs Normal file
View File

@ -0,0 +1,8 @@
namespace line_gestao_api.Models;
public class Tenant
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

View File

@ -2,10 +2,12 @@
namespace line_gestao_api.Models namespace line_gestao_api.Models
{ {
public class TrocaNumeroLine public class TrocaNumeroLine : ITenantEntity
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid TenantId { get; set; }
public int Item { get; set; } public int Item { get; set; }
public string? LinhaAntiga { get; set; } public string? LinhaAntiga { get; set; }

View File

@ -1,23 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace line_gestao_api.Models;
public class User
{
public Guid Id { get; set; } = Guid.NewGuid();
[Required, MaxLength(120)]
public string Name { get; set; } = string.Empty;
[Required, MaxLength(120)]
public string Email { get; set; } = string.Empty;
[MaxLength(20)]
public string Phone { get; set; } = string.Empty;
[Required]
public string PasswordHash { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

View File

@ -3,11 +3,13 @@ using System.ComponentModel.DataAnnotations.Schema;
namespace line_gestao_api.Models namespace line_gestao_api.Models
{ {
public class UserData public class UserData : ITenantEntity
{ {
[Key] [Key]
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid TenantId { get; set; }
public int Item { get; set; } public int Item { get; set; }
public string? Linha { get; set; } public string? Linha { get; set; }

View File

@ -2,10 +2,12 @@
namespace line_gestao_api.Models namespace line_gestao_api.Models
{ {
public class VigenciaLine public class VigenciaLine : ITenantEntity
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid TenantId { get; set; }
public int Item { get; set; } public int Item { get; set; }
public string? Conta { get; set; } public string? Conta { get; set; }

View File

@ -1,7 +1,10 @@
using System.Text; using System.Text;
using line_gestao_api.Data; using line_gestao_api.Data;
using line_gestao_api.Models;
using line_gestao_api.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
@ -30,6 +33,18 @@ builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")) options.UseNpgsql(builder.Configuration.GetConnectionString("Default"))
); );
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ITenantProvider, TenantProvider>();
builder.Services.AddIdentityCore<ApplicationUser>(options =>
{
options.Password.RequiredLength = 6;
options.User.RequireUniqueEmail = false;
})
.AddRoles<IdentityRole<Guid>>()
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
// ✅ Swagger // ✅ Swagger
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); builder.Services.AddSwaggerGen();
@ -57,6 +72,11 @@ builder.Services
builder.Services.AddAuthorization(); builder.Services.AddAuthorization();
builder.Services.Configure<NotificationOptions>(builder.Configuration.GetSection("Notifications"));
builder.Services.AddHostedService<VigenciaNotificationBackgroundService>();
builder.Services.Configure<SeedOptions>(builder.Configuration.GetSection("Seed"));
var app = builder.Build(); var app = builder.Build();
if (app.Environment.IsDevelopment()) if (app.Environment.IsDevelopment())
@ -71,8 +91,11 @@ app.UseHttpsRedirection();
app.UseCors("Front"); app.UseCors("Front");
app.UseAuthentication(); app.UseAuthentication();
app.UseMiddleware<TenantMiddleware>();
app.UseAuthorization(); app.UseAuthorization();
app.MapControllers(); app.MapControllers();
await SeedData.EnsureSeedDataAsync(app.Services);
app.Run(); app.Run();

View File

@ -0,0 +1,7 @@
namespace line_gestao_api.Services;
public interface ITenantProvider
{
Guid? TenantId { get; }
void SetTenantId(Guid? tenantId);
}

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,36 @@
using System.Security.Claims;
namespace line_gestao_api.Services;
public class TenantMiddleware
{
private readonly RequestDelegate _next;
public TenantMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context, ITenantProvider tenantProvider)
{
Guid? tenantId = null;
var claim = context.User.FindFirst("tenantId")?.Value
?? context.User.FindFirst("tenant")?.Value;
if (Guid.TryParse(claim, out var parsed))
{
tenantId = parsed;
}
tenantProvider.SetTenantId(tenantId);
try
{
await _next(context);
}
finally
{
tenantProvider.SetTenantId(null);
}
}
}

View File

@ -0,0 +1,30 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
namespace line_gestao_api.Services;
public class TenantProvider : ITenantProvider
{
private readonly IHttpContextAccessor _httpContextAccessor;
private static readonly AsyncLocal<Guid?> CurrentTenant = new();
public TenantProvider(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public Guid? TenantId => CurrentTenant.Value ?? ResolveFromClaims();
public void SetTenantId(Guid? tenantId)
{
CurrentTenant.Value = tenantId;
}
private Guid? ResolveFromClaims()
{
var claim = _httpContextAccessor.HttpContext?.User?.FindFirst("tenantId")?.Value
?? _httpContextAccessor.HttpContext?.User?.FindFirst("tenant")?.Value;
return Guid.TryParse(claim, out var tenantId) ? tenantId : null;
}
}

View File

@ -0,0 +1,272 @@
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}";
}
}

View File

@ -4,5 +4,15 @@
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
},
"Notifications": {
"CheckIntervalMinutes": 60,
"ReminderDays": [30, 15, 7]
},
"Seed": {
"DefaultTenantName": "Default",
"AdminName": "Administrador",
"AdminEmail": "admin@linegestao.local",
"AdminPassword": "Admin123!"
} }
} }

View File

@ -7,5 +7,15 @@
"Issuer": "LineGestao", "Issuer": "LineGestao",
"Audience": "LineGestao", "Audience": "LineGestao",
"ExpiresMinutes": 120 "ExpiresMinutes": 120
},
"Notifications": {
"CheckIntervalMinutes": 60,
"ReminderDays": [30, 15, 7]
},
"Seed": {
"DefaultTenantName": "Default",
"AdminName": "Administrador",
"AdminEmail": "admin@linegestao.local",
"AdminPassword": "Admin123!"
} }
} }

View File

@ -25,6 +25,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="ClosedXML" Version="0.105.0" /> <PackageReference Include="ClosedXML" Version="0.105.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">